liusz 1 vecka sedan
incheckning
cb144b29b9
100 ändrade filer med 9763 tillägg och 0 borttagningar
  1. 36 0
      README.md
  2. 114 0
      pom.xml
  3. 100 0
      py-app-starter/pom.xml
  4. 32 0
      py-app-starter/src/main/java/com/poyee/PoyeeEcologyAppApplication.java
  5. 22 0
      py-app-starter/src/main/java/com/poyee/controller/EcologyCourierRecordController.java
  6. 22 0
      py-app-starter/src/main/java/com/poyee/controller/EcologyGoodsConfigController.java
  7. 111 0
      py-app-starter/src/main/java/com/poyee/controller/EcologyGoodsController.java
  8. 22 0
      py-app-starter/src/main/java/com/poyee/controller/EcologyOrderInfoController.java
  9. 22 0
      py-app-starter/src/main/java/com/poyee/controller/EcologyOrderInvoiceController.java
  10. 22 0
      py-app-starter/src/main/java/com/poyee/controller/EcologyOrderItemController.java
  11. 22 0
      py-app-starter/src/main/java/com/poyee/controller/EcologySkuController.java
  12. 27 0
      py-app-starter/src/main/java/com/poyee/controller/EcologySpuController.java
  13. 72 0
      py-app-starter/src/main/resources/application-dev.yml
  14. 64 0
      py-app-starter/src/main/resources/application-prod.yml
  15. 43 0
      py-app-starter/src/main/resources/application.yml
  16. 21 0
      py-app-starter/src/main/resources/logback-fluentd.xml
  17. 100 0
      py-app-starter/src/main/resources/logback.xml
  18. 14 0
      py-app-starter/src/main/resources/mybatis/mybatis-config.xml
  19. 171 0
      py-base/DB.md
  20. 74 0
      py-base/README.md
  21. 718 0
      py-base/base.md
  22. 192 0
      py-base/easy.md
  23. 346 0
      py-base/i18n.md
  24. 299 0
      py-base/pom.xml
  25. 593 0
      py-base/provider.md
  26. 47 0
      py-base/src/main/java/com/poyee/annotation/ApiAuthenticationToken.java
  27. 14 0
      py-base/src/main/java/com/poyee/annotation/CheckDataNull.java
  28. 21 0
      py-base/src/main/java/com/poyee/annotation/Desensit.java
  29. 16 0
      py-base/src/main/java/com/poyee/annotation/DesensitMethod.java
  30. 13 0
      py-base/src/main/java/com/poyee/annotation/DictTypeFormat.java
  31. 14 0
      py-base/src/main/java/com/poyee/annotation/ExcelI18nFromat.java
  32. 38 0
      py-base/src/main/java/com/poyee/annotation/I18n.java
  33. 34 0
      py-base/src/main/java/com/poyee/annotation/I18nToken.java
  34. 19 0
      py-base/src/main/java/com/poyee/annotation/JsonMapper.java
  35. 37 0
      py-base/src/main/java/com/poyee/annotation/Log.java
  36. 51 0
      py-base/src/main/java/com/poyee/annotation/MpjWapper.java
  37. 13 0
      py-base/src/main/java/com/poyee/annotation/PassToken.java
  38. 24 0
      py-base/src/main/java/com/poyee/annotation/Property.java
  39. 13 0
      py-base/src/main/java/com/poyee/annotation/ReadConverterExpFormat.java
  40. 60 0
      py-base/src/main/java/com/poyee/annotation/RedisCache.java
  41. 25 0
      py-base/src/main/java/com/poyee/annotation/RedisLock.java
  42. 36 0
      py-base/src/main/java/com/poyee/annotation/UserLoginToken.java
  43. 23 0
      py-base/src/main/java/com/poyee/annotation/db/CaseWhen.java
  44. 20 0
      py-base/src/main/java/com/poyee/annotation/db/Count.java
  45. 13 0
      py-base/src/main/java/com/poyee/annotation/db/Distinct.java
  46. 18 0
      py-base/src/main/java/com/poyee/annotation/db/Join.java
  47. 21 0
      py-base/src/main/java/com/poyee/annotation/db/LeftJoin.java
  48. 15 0
      py-base/src/main/java/com/poyee/annotation/db/OrderBy.java
  49. 19 0
      py-base/src/main/java/com/poyee/annotation/db/RightJoin.java
  50. 23 0
      py-base/src/main/java/com/poyee/annotation/db/Select.java
  51. 17 0
      py-base/src/main/java/com/poyee/annotation/db/Sum.java
  52. 88 0
      py-base/src/main/java/com/poyee/annotation/db/Where.java
  53. 30 0
      py-base/src/main/java/com/poyee/annotation/ds/DataSource.java
  54. 28 0
      py-base/src/main/java/com/poyee/annotation/ds/Transactional.java
  55. 35 0
      py-base/src/main/java/com/poyee/annotation/i18nSource.java
  56. 154 0
      py-base/src/main/java/com/poyee/aspectj/ApiAuthenticationTokenAspect.java
  57. 111 0
      py-base/src/main/java/com/poyee/aspectj/DataSourceAspect.java
  58. 126 0
      py-base/src/main/java/com/poyee/aspectj/DesensitAspect.java
  59. 45 0
      py-base/src/main/java/com/poyee/aspectj/FeignRequestInterceptor.java
  60. 134 0
      py-base/src/main/java/com/poyee/aspectj/I18nTokenAspect.java
  61. 207 0
      py-base/src/main/java/com/poyee/aspectj/LogAspect.java
  62. 38 0
      py-base/src/main/java/com/poyee/aspectj/MqConsumerAOP.java
  63. 305 0
      py-base/src/main/java/com/poyee/aspectj/RedisCacheAspect.java
  64. 211 0
      py-base/src/main/java/com/poyee/aspectj/RedisLockAspect.java
  65. 166 0
      py-base/src/main/java/com/poyee/aspectj/UserLoginTokenAspect.java
  66. 90 0
      py-base/src/main/java/com/poyee/aspectj/i18nAspect.java
  67. 15 0
      py-base/src/main/java/com/poyee/base/controller/ActuatorController.java
  68. 98 0
      py-base/src/main/java/com/poyee/base/controller/BaseController.java
  69. 154 0
      py-base/src/main/java/com/poyee/base/dto/AppBaseUser.java
  70. 19 0
      py-base/src/main/java/com/poyee/base/dto/BaseDto.java
  71. 71 0
      py-base/src/main/java/com/poyee/base/dto/BaseReq.java
  72. 28 0
      py-base/src/main/java/com/poyee/base/dto/MpjWrapper.java
  73. 91 0
      py-base/src/main/java/com/poyee/base/dto/Page.java
  74. 208 0
      py-base/src/main/java/com/poyee/base/dto/Result.java
  75. 73 0
      py-base/src/main/java/com/poyee/base/dto/UserInfo.java
  76. 43 0
      py-base/src/main/java/com/poyee/base/entity/BaseEntity.java
  77. 14 0
      py-base/src/main/java/com/poyee/base/mapper/IBaseMapper.java
  78. 201 0
      py-base/src/main/java/com/poyee/base/mapper/provider/BaseProvider.java
  79. 29 0
      py-base/src/main/java/com/poyee/base/mapper/provider/ErrMessageContent.java
  80. 273 0
      py-base/src/main/java/com/poyee/base/mapper/provider/IBaseProvider.java
  81. 27 0
      py-base/src/main/java/com/poyee/base/mapper/provider/domain/LeftJoinInfo.java
  82. 120 0
      py-base/src/main/java/com/poyee/base/mapper/provider/domain/MPage.java
  83. 16 0
      py-base/src/main/java/com/poyee/base/mapper/provider/domain/OrderByInfo.java
  84. 24 0
      py-base/src/main/java/com/poyee/base/mapper/provider/domain/TableInfo.java
  85. 99 0
      py-base/src/main/java/com/poyee/base/mapper/provider/domain/WhereInfo.java
  86. 154 0
      py-base/src/main/java/com/poyee/base/mapper/provider/util/FormatValueUtil.java
  87. 224 0
      py-base/src/main/java/com/poyee/base/mapper/provider/util/InsertUtil.java
  88. 76 0
      py-base/src/main/java/com/poyee/base/mapper/provider/util/JoinUtil.java
  89. 368 0
      py-base/src/main/java/com/poyee/base/mapper/provider/util/SelectUtil.java
  90. 641 0
      py-base/src/main/java/com/poyee/base/mapper/provider/util/SqlUtil.java
  91. 106 0
      py-base/src/main/java/com/poyee/base/mapper/provider/util/TableInfoUtil.java
  92. 167 0
      py-base/src/main/java/com/poyee/base/mapper/provider/util/UpdateUtil.java
  93. 184 0
      py-base/src/main/java/com/poyee/base/mapper/provider/util/WhereUtil.java
  94. 14 0
      py-base/src/main/java/com/poyee/base/service/BaseService.java
  95. 295 0
      py-base/src/main/java/com/poyee/base/service/impl/BaseServiceImpl.java
  96. 13 0
      py-base/src/main/java/com/poyee/common/CanonicalHostNamePropertyDefiner.java
  97. 64 0
      py-base/src/main/java/com/poyee/common/Result.java
  98. 97 0
      py-base/src/main/java/com/poyee/common/auth/UserRoleUtil.java
  99. 18 0
      py-base/src/main/java/com/poyee/common/enums/BusinessStatus.java
  100. 68 0
      py-base/src/main/java/com/poyee/common/enums/BusinessType.java

+ 36 - 0
README.md

@@ -0,0 +1,36 @@
+# 生态购2.0
+
+---------------------------
+### 说明
+```
+此服务支持 
+1.生态购2.0
+...
+后续支持更多功能
+此服务支持多数据源 根据不同数据源进行不同的业务
+ ·[弃用] 接口请求参数 dbVersion 设置不同的数据源,在执行查询时,会自动根据该参数进行数据源切换
+```
+--------------------------
+## 运行环境
+```
+1.jdk 8
+2.postgresql 9.6
+  |- 项目插件 42.2.18
+  |- mybatis-plus
+3.springboot 2.3.5.RELEASE
+4.插件
+  |- knife4j
+  |- lombok
+```
+--------------------------
+## 结构
+
+> py-starter  启动模块
+> 
+> py-base  基础模块
+> - POYEE BASE - 基础插件 [README.md](py-base/README.md)
+> 
+> 
+      
+### 接口文档
+> http://localhost:8080/doc.html </br>

+ 114 - 0
pom.xml

@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         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>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.3.5.RELEASE</version>
+    </parent>
+
+    <groupId>com.poyee</groupId>
+    <artifactId>poyee-ecology</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>pom</packaging>
+
+    <name>ecology</name>
+    <description>生态购2.0</description>
+
+    <properties>
+        <module.version>1.0-SNAPSHOT</module.version>
+        <java.version>1.8</java.version>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <springboot.version>2.3.5.RELEASE</springboot.version>
+        <druid.version>1.2.1</druid.version>
+        <commons.io.version>2.5</commons.io.version>
+        <fastjson.version>1.2.74</fastjson.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <version>${springboot.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.18.12</version>
+            <optional>true</optional>
+        </dependency>
+    </dependencies>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>Hoxton.SR8</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <!--base-->
+            <dependency>
+                <groupId>com.poyee</groupId>
+                <artifactId>py-base</artifactId>
+                <version>${module.version}</version>
+            </dependency>
+            <!--domain-->
+            <dependency>
+                <groupId>com.poyee</groupId>
+                <artifactId>py-domain</artifactId>
+                <version>${module.version}</version>
+            </dependency>
+            <!--dao-->
+            <dependency>
+                <groupId>com.poyee</groupId>
+                <artifactId>py-dao</artifactId>
+                <version>${module.version}</version>
+            </dependency>
+            <!--service-->
+            <dependency>
+                <groupId>com.poyee</groupId>
+                <artifactId>py-service</artifactId>
+                <version>${module.version}</version>
+            </dependency>
+            <!--feign-->
+            <dependency>
+                <groupId>com.poyee</groupId>
+                <artifactId>py-feign-clients</artifactId>
+                <version>${module.version}</version>
+            </dependency>
+
+        </dependencies>
+    </dependencyManagement>
+
+    <modules>
+        <module>py-base</module>
+        <module>py-domain</module>
+        <module>py-service</module>
+        <module>py-starter</module>
+        <module>py-app-starter</module>
+        <module>py-dao</module>
+        <module>py-feign-clients</module>
+    </modules>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.1</version>
+                <configuration>
+                    <source>${java.version}</source>
+                    <target>${java.version}</target>
+                    <encoding>${project.build.sourceEncoding}</encoding>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 100 - 0
py-app-starter/pom.xml

@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         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>com.poyee</groupId>
+        <artifactId>poyee-ecology</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>py-app-starter</artifactId>
+    <name>APP启动模块</name>
+    <description>APP启动模块</description>
+
+    <dependencies>
+        <!--springboot-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <!--热更新-->
+        <!--<dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+        </dependency>-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <!--base-->
+        <dependency>
+            <groupId>com.poyee</groupId>
+            <artifactId>py-base</artifactId>
+        </dependency>
+        <!--domain-->
+        <dependency>
+            <groupId>com.poyee</groupId>
+            <artifactId>py-domain</artifactId>
+        </dependency>
+        <!--dao-->
+        <dependency>
+            <groupId>com.poyee</groupId>
+            <artifactId>py-dao</artifactId>
+        </dependency>
+        <!--service-->
+        <dependency>
+            <groupId>com.poyee</groupId>
+            <artifactId>py-service</artifactId>
+        </dependency>
+        <!--feign-->
+        <dependency>
+            <groupId>com.poyee</groupId>
+            <artifactId>py-feign-clients</artifactId>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.1.1.RELEASE</version>
+                <configuration>
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+                    <includeSystemScope>true</includeSystemScope>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>3.0.0</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                    <warName>${project.artifactId}</warName>
+                </configuration>
+            </plugin>
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+
+
+</project>

+ 32 - 0
py-app-starter/src/main/java/com/poyee/PoyeeEcologyAppApplication.java

@@ -0,0 +1,32 @@
+package com.poyee;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * 项目启动
+ */
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, SecurityAutoConfiguration.class }
+        , scanBasePackages = {"com.poyee"})
+@EnableAsync
+@EnableAspectJAutoProxy(proxyTargetClass = true)
+@EnableTransactionManagement
+@EnableScheduling
+@EnableFeignClients(basePackages = "com.poyee.feign")
+@Slf4j
+public class PoyeeEcologyAppApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(PoyeeEcologyAppApplication.class, args);
+        log.info("(♥◠‿◠)ノ゙ 启动成功   ლ(´ڡ`ლ)゙");
+    }
+
+}

+ 22 - 0
py-app-starter/src/main/java/com/poyee/controller/EcologyCourierRecordController.java

@@ -0,0 +1,22 @@
+package com.poyee.controller;
+
+import com.poyee.base.controller.BaseController;
+import com.poyee.dto.EcologyCourierRecordDto;
+import com.poyee.req.EcologyCourierRecordReq;
+import com.poyee.service.IEcologyCourierRecordService;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ * 生态购订单快递信息表 前端控制器
+ * </p>
+ *
+ * @author lsz
+ * @since 2025-11-18
+ */
+@RestController
+@RequestMapping("/api/ecologyCourierRecord")
+public class EcologyCourierRecordController extends BaseController<IEcologyCourierRecordService, EcologyCourierRecordReq, EcologyCourierRecordDto> {
+
+}

+ 22 - 0
py-app-starter/src/main/java/com/poyee/controller/EcologyGoodsConfigController.java

@@ -0,0 +1,22 @@
+package com.poyee.controller;
+
+import com.poyee.base.controller.BaseController;
+import com.poyee.dto.EcologyGoodsConfigDto;
+import com.poyee.req.EcologyGoodsConfigReq;
+import com.poyee.service.IEcologyGoodsConfigService;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ * SPU产品扩展配置表 前端控制器
+ * </p>
+ *
+ * @author lsz
+ * @since 2025-11-18
+ */
+@RestController
+@RequestMapping("/api/ecologyGoodsConfig")
+public class EcologyGoodsConfigController extends BaseController<IEcologyGoodsConfigService, EcologyGoodsConfigReq, EcologyGoodsConfigDto> {
+
+}

+ 111 - 0
py-app-starter/src/main/java/com/poyee/controller/EcologyGoodsController.java

@@ -0,0 +1,111 @@
+package com.poyee.controller;
+
+import com.poyee.annotation.Log;
+import com.poyee.annotation.UserLoginToken;
+import com.poyee.base.controller.BaseController;
+import com.poyee.base.dto.Page;
+import com.poyee.base.dto.Result;
+import com.poyee.common.enums.BusinessType;
+import com.poyee.common.enums.Roles;
+import com.poyee.dto.EcologyMerCustomizeDto;
+import com.poyee.dto.goods.EcologyGoodsPageDto;
+import com.poyee.dto.permission.CheckMerPermissionDto;
+import com.poyee.req.EcologyMerCustomizeReq;
+import com.poyee.req.goods.EcologyGoodsBuyPageReq;
+import com.poyee.req.goods.EcologyGoodsPageReq;
+import com.poyee.service.EcologyGoodsService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 商品控制器
+ */
+@Api(tags = "商品管理")
+@RestController
+@RequestMapping("/api/ecologyGoods")
+public class EcologyGoodsController extends BaseController<EcologyGoodsService, EcologyMerCustomizeReq, EcologyMerCustomizeDto> {
+
+    /**
+     * 是否有 求购令 和热卖商品的权限
+     *
+     * @return 权限检查结果
+     */
+    @Log(title = "商品管理", businessType = BusinessType.SEARCH)
+    @ApiOperation(value = "检查用户是否有求购令和热卖商品的权限@(app 1.0.1)")
+    @GetMapping("/checkPermission")
+    @UserLoginToken(roles = {Roles.ADMIN, Roles.SHIPPING})
+    @ResponseBody
+    public Result<CheckMerPermissionDto> checkPermission() {
+        console("检查用户是否有求购令和热卖商品的权限","");
+        // 调用服务层检查权限
+        return baseService.checkMerCustomizePermission();
+    }
+
+
+    /**
+     * 获取商品列表
+     *
+     * @param req 查询条件
+     * @return 商品列表
+     */
+    @Log(title = "商品管理[热卖|销售单]", businessType = BusinessType.SEARCH)
+    @ApiOperation(value = "获取商品列表[热卖|销售单]@(app 1.0.1)")
+    @PostMapping(path = "/hots")
+    @UserLoginToken( roles = {Roles.ADMIN, Roles.SHIPPING} )
+    @ResponseBody
+    public Page<EcologyGoodsPageDto> hots(@RequestBody EcologyGoodsPageReq req) {
+        console("获取商品列表[热卖] req", req);
+        return baseService.hots(req);
+    }
+
+    /**
+     * 获取商品列表
+     *
+     * @param req 查询条件
+     * @return 商品列表
+     */
+    @Log(title = "商品管理[求购]", businessType = BusinessType.SEARCH)
+    @ApiOperation(value = "获取商品列表[求购]@(app 1.0.1)")
+    @PostMapping(path = "/buys")
+    @UserLoginToken( roles = {Roles.ADMIN, Roles.SHIPPING} )
+    @ResponseBody
+    public Page<EcologyGoodsPageDto> buys(@RequestBody EcologyGoodsBuyPageReq req) {
+        console("获取商品列表[求购] req", req);
+        return baseService.buys(req);
+    }
+
+
+    /**
+     * 获取单个求购令商品 [top 排名最高的]
+     */
+    @Log(title = "获取单个求购令商品 [top排名最高]", businessType = BusinessType.SEARCH)
+    @ApiOperation(value = "获取单个求购令商品 [top排名最高]@(app 1.0.1)")
+    @GetMapping("/buyTop")
+    @UserLoginToken( roles = {Roles.ADMIN, Roles.SHIPPING} )
+    @ResponseBody
+    public Result<EcologyGoodsPageDto> buyTop() {
+        console("获取单个求购令商品 [top 排名最高的]", "");
+        return baseService.buyTop();
+    }
+
+
+
+    /**
+     * 获取商品详情
+     *
+     * @param id 商品ID skuid
+     * @return 商品详情
+     */
+    @Log(title = "商品管理", businessType = BusinessType.SEARCH)
+    @ApiOperation(value = "获取商品详情@(app 1.0.1)")
+    @GetMapping("/detailById/{id}")
+    @UserLoginToken( roles = {Roles.ADMIN, Roles.SHIPPING} )
+    @ResponseBody
+    public Result detailById(@PathVariable("id") Long id) {
+        console("获取商品详情 id", id);
+        return baseService.detailById(id);
+    }
+
+
+}

+ 22 - 0
py-app-starter/src/main/java/com/poyee/controller/EcologyOrderInfoController.java

@@ -0,0 +1,22 @@
+package com.poyee.controller;
+
+import com.poyee.base.controller.BaseController;
+import com.poyee.dto.EcologyOrderInfoDto;
+import com.poyee.req.EcologyOrderInfoReq;
+import com.poyee.service.IEcologyOrderInfoService;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ * 订单主表 前端控制器
+ * </p>
+ *
+ * @author lsz
+ * @since 2025-11-18
+ */
+@RestController
+@RequestMapping("/api/ecologyOrderInfo")
+public class EcologyOrderInfoController extends BaseController<IEcologyOrderInfoService, EcologyOrderInfoReq, EcologyOrderInfoDto> {
+
+}

+ 22 - 0
py-app-starter/src/main/java/com/poyee/controller/EcologyOrderInvoiceController.java

@@ -0,0 +1,22 @@
+package com.poyee.controller;
+
+import com.poyee.base.controller.BaseController;
+import com.poyee.dto.EcologyOrderInvoiceDto;
+import com.poyee.req.EcologyOrderInvoiceReq;
+import com.poyee.service.IEcologyOrderInvoiceService;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ * 生态购订单发票表 前端控制器
+ * </p>
+ *
+ * @author lsz
+ * @since 2025-11-18
+ */
+@RestController
+@RequestMapping("/api/ecologyOrderInvoice")
+public class EcologyOrderInvoiceController extends BaseController<IEcologyOrderInvoiceService, EcologyOrderInvoiceReq, EcologyOrderInvoiceDto> {
+
+}

+ 22 - 0
py-app-starter/src/main/java/com/poyee/controller/EcologyOrderItemController.java

@@ -0,0 +1,22 @@
+package com.poyee.controller;
+
+import com.poyee.base.controller.BaseController;
+import com.poyee.dto.EcologyOrderItemDto;
+import com.poyee.req.EcologyOrderItemReq;
+import com.poyee.service.IEcologyOrderItemService;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ * 订单明细表(子表) 前端控制器
+ * </p>
+ *
+ * @author lsz
+ * @since 2025-11-18
+ */
+@RestController
+@RequestMapping("/api/ecologyOrderItem")
+public class EcologyOrderItemController extends BaseController<IEcologyOrderItemService, EcologyOrderItemReq, EcologyOrderItemDto> {
+
+}

+ 22 - 0
py-app-starter/src/main/java/com/poyee/controller/EcologySkuController.java

@@ -0,0 +1,22 @@
+package com.poyee.controller;
+
+import com.poyee.base.controller.BaseController;
+import com.poyee.dto.EcologySkuDto;
+import com.poyee.req.EcologySkuReq;
+import com.poyee.service.IEcologySkuService;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ * B2B产品生态购产品SKU表 前端控制器
+ * </p>
+ *
+ * @author lsz
+ * @since 2025-11-18
+ */
+@RestController
+@RequestMapping("/api/ecologySku")
+public class EcologySkuController extends BaseController<IEcologySkuService, EcologySkuReq, EcologySkuDto> {
+
+}

+ 27 - 0
py-app-starter/src/main/java/com/poyee/controller/EcologySpuController.java

@@ -0,0 +1,27 @@
+package com.poyee.controller;
+
+import com.poyee.base.controller.BaseController;
+import com.poyee.dto.EcologySpuDto;
+import com.poyee.req.EcologySpuReq;
+import com.poyee.service.IEcologySpuService;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ * B2B产品 生态购产品表(spu) 前端控制器
+ * </p>
+ *
+ * @author lsz
+ * @since 2025-11-18
+ */
+@RestController
+@RequestMapping("/api/ecologySpu")
+public class EcologySpuController extends BaseController<IEcologySpuService, EcologySpuReq, EcologySpuDto> {
+
+
+
+
+
+
+}

+ 72 - 0
py-app-starter/src/main/resources/application-dev.yml

@@ -0,0 +1,72 @@
+spring:
+  datasource:
+    url: jdbc:postgresql://192.168.50.8:5432/poyee_ecology
+    username: ${MASTER_DATASOURCE_USERNAME:postgres}
+    password: ${MASTER_DATASOURCE_PASSWORD:123456}
+    type: com.alibaba.druid.pool.DruidDataSource
+    driver-class-name: org.postgresql.Driver
+    dynamic:
+      #DataSourceConfig.java
+      primary: master
+      datasource:
+        master:
+          type: com.alibaba.druid.pool.DruidDataSource
+          driver-class-name: org.postgresql.Driver
+          url: ${MASTER_DATASOURCE_URL:jdbc:postgresql://192.168.50.8:5432/poyee_ecology}
+          username: ${MASTER_DATASOURCE_USERNAME:postgres}
+          password: ${MASTER_DATASOURCE_PASSWORD:123456}
+#        slave:
+#          type: com.alibaba.druid.pool.DruidDataSource
+#          driver-class-name: org.postgresql.Driver
+#          url: ${SLAVE_DATASOURCE_URL:jdbc:postgresql:////192.168.50.10:5432/tzy_system}
+#          username: ${SLAVE_DATASOURCE_USERNAME:postgres}
+#          password: ${MASTER_DATASOURCE_PASSWORD:123456}
+    druid:
+      # 初始连接数
+      initialSize: 5
+      # 最小连接池数量
+      minIdle: 10
+      # 最大连接池数量
+      maxActive: 20
+      # 配置获取连接等待超时的时间
+      maxWait: 60000
+#redis
+  redis:
+    database: 1
+#    host: ${REDIS_HOST:192.168.50.8}
+    host: 192.168.207.128
+    port: ${REDIS_PORT:6379}
+    password: #Pass2010    # 密码(默认为空)
+    timeout: 60000  # 连接超时时长(毫秒)
+    pool:
+      max-active: 1000  # 连接池最大连接数(使用负值表示没有限制)
+      max-wait: -1ms    # 连接池最大阻塞等待时间(使用负值表示没有限制)
+      max-idle: 10      # 连接池中的最大空闲连接
+      min-idle: 5       # 连接池中的最小空闲连接
+
+mybatis-plus:
+  configuration:
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+    map-underscore-to-camel-case: true
+  global-config:
+    db-config:
+      column-underline: false
+
+
+# knife4j 配置
+knife4j:
+  # 是否开启 knife4j
+  enabled: true
+  #生产模式屏蔽
+  production: true
+  basic: #是否需要登录
+    enable: true
+    username: admin
+    password: 123456
+#白名单设置
+whitelist:
+  id: 1593
+management:
+  health:
+    db:
+      enabled: false  # 禁用数据库健康检查

+ 64 - 0
py-app-starter/src/main/resources/application-prod.yml

@@ -0,0 +1,64 @@
+spring:
+  datasource:
+    dynamic:
+      primary: master
+      datasource:
+        master:
+          type: com.alibaba.druid.pool.DruidDataSource
+          driver-class-name: org.postgresql.Driver
+          url: ${MASTER_DATASOURCE_URL:jdbc:postgresql://192.168.50.8:15432/hs_sync_data}
+          username: ${MASTER_DATASOURCE_USERNAME:hs_sync}
+          password: ${MASTER_DATASOURCE_PASSWORD:Pass2025}
+#        slave:
+#          type: com.alibaba.druid.pool.DruidDataSource
+#          driver-class-name: org.postgresql.Driver
+#          url: ${SLAVE_DATASOURCE_URL:jdbc:postgresql://localhost:5432/py_test}
+#          username: ${SLAVE_DATASOURCE_USERNAME:postgres}
+#          password: ${SLAVE_DATASOURCE_PASSWORD:123456}
+    druid:
+      # 初始连接数
+      initialSize: 5
+      # 最小连接池数量
+      minIdle: 10
+      # 最大连接池数量
+      maxActive: 20
+      # 配置获取连接等待超时的时间
+      maxWait: 60000
+
+  #redis
+  redis:
+    database:
+    host: 192.168.207.128
+    port: 6379
+    password:
+    sentinel:
+      master: master
+      nodes: 192.168.207.128:26379
+    lettuce.pool:
+      max-active: 1000  # 连接池最大连接数(使用负值表示没有限制)
+      max-wait: -1ms    # 连接池最大阻塞等待时间(使用负值表示没有限制)
+      max-idle: 10      # 连接池中的最大空闲连接
+      min-idle: 5       # 连接池中的最小空闲连接
+
+mybatis-plus:
+  configuration:
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+    map-underscore-to-camel-case: true
+  global-config:
+    db-config:
+      column-underline: false
+
+
+# knife4j 配置
+knife4j:
+  # 是否开启 knife4j
+  enabled: true
+  #生产模式屏蔽
+  production: true
+  basic: #是否需要登录
+    enable: true
+    username: admin
+    password: 123456
+#白名单设置
+whitelist:
+  id: 1593

+ 43 - 0
py-app-starter/src/main/resources/application.yml

@@ -0,0 +1,43 @@
+poyee:
+  profile: c://tmp/
+
+spring:
+  profiles:
+    active: dev
+  application:
+    name: poyee-app-ecology
+
+  cloud:
+    loadbalancer:
+      retry:
+        enabled: true
+
+server:
+  port: 8087
+
+# MyBatis
+mybatis:
+  # 搜索指定包别名
+  typeAliasesPackage: com.poyee.dto,com.poyee.dto.req,com.poyee.**.dto,com.poyee.**.dto.req
+  # 配置mapper的扫描,找到所有的mapper.xml映射文件
+  mapperLocations: classpath*:mapper/*Mapper.xml,classpath*:mapper/**/*Mapper.xml
+  # 加载全局的配置文件
+  configLocation: classpath:mybatis/mybatis-config.xml
+
+# 日志配置
+logging:
+  console.enabled: ${CONSOLE_ENABLED:true}
+  file.enabled: ${FILE_ENABLED:false}
+  level:
+    com.poyee: debug
+    org.springframework: warn
+  fluentd:
+    enabled: ${FLUENTD_ENABLED:false}
+    host: ${FLUENTD_HOST:127.0.0.1}
+    port: ${FLUENTD_PORT:24225}
+
+
+# 商家用户信息服务URL配置
+mer-user-info:
+  service:
+    url: http://localhost

+ 21 - 0
py-app-starter/src/main/resources/logback-fluentd.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<included>
+    <appender name="FLUENT_SYNC" class="ch.qos.logback.more.appenders.DataFluentAppender">
+        <tag>logback</tag>
+        <label>poyee-dashboard.${hostname}</label>
+        <remoteHost>${logging.fluentd.host}</remoteHost>
+        <port>${logging.fluentd.port}</port>
+
+        <encoder charset="UTF-8">
+            <pattern>%logger{15}:%L - %msg</pattern>
+        </encoder>
+    </appender>
+
+    <appender name="FLUENT" class="ch.qos.logback.classic.AsyncAppender">
+        <queueSize>1000</queueSize>
+        <neverBlock>true</neverBlock>
+        <maxFlushTime>15000</maxFlushTime>
+        <includeCallerData>false</includeCallerData>
+        <appender-ref ref="FLUENT_SYNC" />
+    </appender>
+</included>

+ 100 - 0
py-app-starter/src/main/resources/logback.xml

@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="../logs" />
+    <!-- 日志输出格式 -->
+	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%X{TRACE_ID:-}] [%thread] %highlight(%-5level) %cyan(%logger{20}) - %yellow([%method,%line]) - %msg%n"/>
+    <springProperty scope="context" name="logging.console.enabled" source="logging.console.enabled"/>
+    <springProperty scope="context" name="logging.file.enabled" source="logging.file.enabled"/>
+    <springProperty scope="context" name="logging.fluentd.enabled" source="logging.fluentd.enabled"/>
+    <springProperty scope="context" name="logging.fluentd.host" source="logging.fluentd.host"/>
+    <springProperty scope="context" name="logging.fluentd.port" source="logging.fluentd.port"/>
+    <define name="hostname" class="com.poyee.common.CanonicalHostNamePropertyDefiner"/>
+
+    <if condition='p("logging.fluentd.enabled").equals("true")'>
+        <then>
+            <include resource="logback-fluentd.xml" />
+        </then>
+    </if>
+
+	<!-- 控制台输出 -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+			<charset>UTF-8</charset>
+		</encoder>
+	</appender>
+	
+	<!-- 系统日志输出 -->
+	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+			<charset>UTF-8</charset>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+	</appender>
+	
+	<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+			<!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+			<!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+	
+	<!--系统操作日志-->
+    <root level="info">
+        <if condition='p("logging.console.enabled").equals("true")'>
+            <then>
+                <appender-ref ref="console" />
+            </then>
+        </if>
+        <if condition='p("logging.file.enabled").equals("true")'>
+            <then>
+                <appender-ref ref="file_info" />
+                <appender-ref ref="file_error" />
+            </then>
+        </if>
+        <if condition='p("logging.fluentd.enabled").equals("true")'>
+            <then>
+                <appender-ref ref="FLUENT"/>
+            </then>
+        </if>
+    </root>
+
+    <!-- 系统模块日志级别控制  -->
+    <logger name="com.poyee" level="info" />
+    <!-- Spring日志级别控制  -->
+    <logger name="org.springframework" level="warn" />
+
+</configuration>

+ 14 - 0
py-app-starter/src/main/resources/mybatis/mybatis-config.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration
+        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-config.dtd">
+<configuration>
+
+    <settings>
+        <setting name="cacheEnabled"             value="true"  />  <!-- 全局映射器启用缓存 -->
+        <setting name="useGeneratedKeys"         value="true"  />  <!-- 允许 JDBC 支持自动生成主键 -->
+        <setting name="defaultExecutorType"      value="REUSE" />  <!-- 配置默认的执行器 -->
+        <setting name="logImpl"                  value="SLF4J" />  <!-- 指定 MyBatis 所用日志的具体实现 -->
+    </settings>
+
+</configuration>

+ 171 - 0
py-base/DB.md

@@ -0,0 +1,171 @@
+# DB注解包说明文档
+
+## 概述
+
+`com.poyee.annotation.db`包中包含了一系列用于数据库操作的注解,这些注解主要用于简化SQL查询构建、表关联、条件过滤、排序和聚合操作等。这些注解通常与MyBatis-Plus框架配合使用,用于增强其功能。
+
+## 注解列表
+
+### 1. Select
+
+**用途**:用于指定查询字段
+
+**属性**:
+- `table`:指定表类
+- `alias`:表别名,默认为空
+- `fieldName`:字段名,默认为空
+- `caseWhen`:聚合等条件取值,默认为空数组
+- `distinct`:是否去重,默认为false
+
+**示例**:
+```java
+@Select(table = UserEntity.class, fieldName = "username", distinct = true)
+private String username;
+```
+
+### 2. Join
+
+**用途**:用于外连接查询
+
+**属性**:
+- `table`:关联表
+- `on`:关联条件
+- `alias`:别名
+
+**示例**:
+```java
+@Join(table = "user_role", on = "user.id = user_role.user_id", alias = "ur")
+private String roleInfo;
+```
+
+### 3. LeftJoin
+
+**用途**:用于左连接查询
+
+**属性**:
+- `table`:关联表
+- `leftTable`:关联表(左表/主表),默认为void.class
+- `fieldName`:关联字段,默认为空
+- `leftFieldName`:左表关联字段,默认为空
+
+**示例**:
+```java
+@LeftJoin(table = RoleEntity.class, leftTable = UserEntity.class, fieldName = "id", leftFieldName = "roleId")
+private String roleName;
+```
+
+### 4. RightJoin
+
+**用途**:用于右连接查询
+
+**属性**:
+- `table`:表名
+- `on`:关联条件
+- `alias`:别名
+
+**示例**:
+```java
+@RightJoin(table = "user_role", on = "user.id = user_role.user_id", alias = "ur")
+private String roleInfo;
+```
+
+### 5. Where
+
+**用途**:用于指定查询条件
+
+**属性**:
+- `table`:表名/别名(二选一)
+- `alias`:别名/表名(二选一),默认为空
+- `field`:字段名(与orFields二选一),默认为空
+- `orFields`:字段名数组(与field二选一),默认为空数组
+- `columnType`:字段类型,默认为STRING
+- `operator`:操作符,默认为EQ(等于)
+- `isNull`:是否为空,默认为false
+- `isEnd`:是否为结束条件,默认为false
+
+**示例**:
+```java
+@Where(table = UserEntity.class, field = "status", operator = FieldOperator.EQ)
+private String status;
+```
+
+### 6. OrderBy
+
+**用途**:用于指定排序 使用到结果Dto 类的字段
+
+**属性**:
+- `sortType`:排序方式,默认为ASC(升序)
+- `index`:顺序,默认为1
+
+**示例**:
+```java
+@OrderBy(sortType = OrderBySortEnums.DESC, index = 2)
+private Date createTime;
+```
+
+### 7. Count
+
+**用途**:用于统计计数
+
+**属性**:
+- `distinct`:是否去重,默认为false
+- `caseWhen`:条件取值,默认为空数组
+
+**示例**:
+```java
+@Count(distinct = true)
+@TableField("user_id")
+private Long userCount;
+```
+
+### 8. Sum
+
+**用途**:用于求和
+
+**属性**:
+- `caseWhen`:条件取值,默认为空数组
+
+**示例**:
+```java
+@Sum
+@TableField("amount")
+private Double totalAmount;
+```
+
+### 9. CaseWhen
+
+**用途**:用于聚合等条件取值
+
+**属性**:
+- `when`:条件,默认为空
+- `then`:值,默认为空
+- `isElse`:是否是else,默认为false
+- `valueType`:值类型,默认为CONSTANT
+
+**示例**:
+```java
+@Count
+@TableField("status")
+private Long statusCount;
+
+@CaseWhen(when = "status = 1", then = "1", valueType = CaseWhenValueTypeEnums.CONSTANT)
+private Long activeCount;
+```
+
+## 使用场景
+
+这些注解主要用于以下场景:
+
+1. **复杂查询构建**:通过注解方式构建复杂的SQL查询,避免手写SQL
+2. **表关联**:使用Join、LeftJoin、RightJoin等注解实现表关联
+3. **条件过滤**:使用Where注解实现条件过滤
+4. **排序**:使用OrderBy注解实现结果排序
+5. **聚合操作**:使用Count、Sum等注解实现聚合操作
+6. **条件取值**:使用CaseWhen注解实现条件取值
+
+## 注意事项
+
+1. 这些注解通常需要与MyBatis-Plus框架配合使用
+2. 注解的使用需要遵循Java注解的规范
+3. 部分注解需要与其他注解(如TableField)一起使用才能生效
+4. 使用这些注解可以简化SQL构建,但也需要了解其底层实现原理,以便更好地使用

+ 74 - 0
py-base/README.md

@@ -0,0 +1,74 @@
+# POYEE BASE - 基础插件
+
+## 📋 项目简介
+
+> POYEE BASE 是一个提供基础功能和工具的核心插件库,为上层应用提供统一的底层支持。
+
+## 🌟 核心功能
+
+### 自定义注解 [`annotation`](src/main/java/com/poyee/annotation)
+- 数据库相关注解说明:[DB.md](DB.md)
+
+### 基础抽象方法 [`base`](src/main/java/com/poyee/base)
+> 说明:[base.md](base.md)
+- **控制层**:controller 抽象类
+- **业务层**:service 抽象类
+- **数据层**:mapper 抽象类
+  - mapper 接口
+  - provider 动态SQL工具
+    - [provider.md](provider.md) - 详细说明文档
+    - 根据实体类注解动态生成查询SQL,无需使用 mapper.xml 文件
+    - 工具内集成基础查询功能,复杂查询可自定义手写SQL
+    - 使用方式:mapper.java 继承 IBaseProvider
+    - 自定义SQL:业务模块在mapper包下创建provider包,创建*Provider类,编写自定义SQL逻辑
+    - 参考文档:[MyBatis Provider使用指南](https://blog.csdn.net/fmwind/article/details/81534357)
+- **数据模型**:entity、dto 实体类
+
+### 通用工具 [`common`](src/main/java/com/poyee/common)
+- 常量定义
+- 数据库生成工具插件
+- 业务异常处理工具 (exception)
+- JSON处理工具 (JsonMapperUtil)
+- API文档工具 (knife4j swagger2)
+
+### 配置模块 `config`
+- [自定义SQL注入器 (JoinSqlInjector)](src/main/java/com/poyee/config/JoinSqlInjector.java)
+- [Redis配置](src/main/java/com/poyee/config/RedisConfig.java)
+
+### 多数据源配置 [`dataSource`](src/main/java/com/poyee/datasource)
+
+### Excel处理工具 [`easy`](src/main/java/com/poyee/easy)
+> 说明:[easy.md](easy.md)
+
+### 枚举定义 [`enums`](src/main/java/com/poyee/enums)
+
+### 事件处理 [`handler`](src/main/java/com/poyee/handler)
+- Excel处理:
+  - [数据格式处理器 (EasyDataFormatHandler)](src/main/java/com/poyee/handler/EasyDataFormatHandler.java)
+  - [分页查询监听器 (PageReadListenerHandler)](src/main/java/com/poyee/handler/PageReadListenerHandler.java)
+- 消息队列:[`mq`](src/main/java/com/poyee/mq)
+- 缓存工具:[`redis`](src/main/java/com/poyee/redis)
+- 通用工具类:[`util`](src/main/java/com/poyee/util)
+
+### 国际化支持 [`i18n`](src/main/java/com/poyee/i18n)
+> 说明:[i18n.md](i18n.md)
+- [多语言枚举 (I18nMessageEnums)](src/main/java/com/poyee/i18n/I18nMessageEnums.java)
+- [多语言工具类 (I18nUtils)](src/main/java/com/poyee/i18n/I18nUtils.java)
+- [多语言配置 (MessageProperties)](src/main/java/com/poyee/i18n/MessageProperties.java)
+- [多语言配置文件 (message.yml)](src/main/resources/i18n/message.yml)
+
+## 🛠️ 开发环境
+
+<!-- 此处可添加开发环境相关信息,如JDK版本、构建工具等 -->
+
+## 🔧 技术架构
+
+<!-- 此处可添加技术架构相关信息,如框架版本、组件关系等 -->
+
+## 📚 使用指南
+
+<!-- 此处可添加项目使用说明、示例代码等 -->
+
+## 🔄 版本历史
+
+<!-- 此处可添加版本更新记录 -->

+ 718 - 0
py-base/base.md

@@ -0,0 +1,718 @@
+# Poyee Base 框架说明文档
+
+## 1. 概述
+
+`com.poyee.base` 包是 Poyee Checklist 项目的核心基础框架,提供了一套完整的分层架构实现,包括控制层(Controller)、服务层(Service)、数据访问层(Mapper)以及相关的数据传输对象(DTO)、请求对象(Req)和实体对象(Entity)。该框架采用了面向对象的设计思想,通过抽象基类和泛型机制,实现了代码复用和业务逻辑的统一管理。
+
+## 2. 核心组件
+
+### 2.1 基础架构
+
+#### 2.1.1 控制层(Controller)
+
+**BaseController**
+
+`BaseController` 是所有控制器的基类,提供了通用的请求处理和日志记录功能。
+
+```java
+public abstract class BaseController<S extends BaseService<T, R>, T extends BaseReq, R extends BaseDto> {
+    @Autowired
+    protected S baseService;
+    
+    // 日志记录
+    public void console(String label, Object obj) { ... }
+    
+    // 分页查询
+    public Result<R> listPage(T req, boolean checkUser) { ... }
+    
+    // 详情查询
+    public Result<R> info(Integer id) { ... }
+    
+    // 获取请求对象
+    public HttpServletRequest getRequest() { ... }
+}
+```
+
+#### 2.1.2 服务层(Service)
+
+**BaseService 接口**
+
+`BaseService` 接口定义了服务层的通用方法,包括查询、分页、计数等操作。
+
+```java
+public interface BaseService<T extends BaseReq, R extends BaseDto> extends IService<R> {
+    Result<R> listPage(T req, boolean checkUser);
+    List<R> selectJoinAllList(MPJLambdaWrapper<R> wrapper, T req);
+    QueryWrapper<R> checkWrapper(T req, boolean checkUser);
+    Long selectCount(MPJLambdaWrapper<R> wrapper);
+    List<R> getAllList(T req, boolean checkUser);
+    Result<R> info(Integer id);
+    Result<R> getOne(T req);
+    Result<R> getOneDto(T req);
+    UserInfo getUserInfo();
+    <Q extends BaseReq> IPage<R> ipage(MPJLambdaWrapper<R> wrapper, Q req);
+    <Q extends BaseReq> Result<R> ipageResult(MPJLambdaWrapper<R> wrapper, Q req);
+    MPJLambdaWrapper<R> mpjWrapper(Object req);
+    MPJLambdaWrapper<R> mpjWrapper(Object req, String alias);
+    MPJLambdaWrapper<R> mpjWrapper(MPJLambdaWrapper<R> mpjLambdaWrapper, Object req, String alias);
+}
+```
+
+**BaseServiceImpl 实现类**
+
+`BaseServiceImpl` 是 `BaseService` 接口的实现类,提供了各种通用方法的具体实现。
+
+```java
+public class BaseServiceImpl<M extends IBaseMapper<R>, T extends BaseReq, R extends BaseDto> 
+        extends ServiceImpl<M, R> implements BaseService<T, R> {
+    
+    // 检查用户权限
+    public <P extends BaseReq> void checkAndSetUserId(P p) { ... }
+    
+    // 分页查询
+    @Override
+    public Result<R> listPage(T req, boolean checkUser) { ... }
+    
+    // 获取单条数据
+    @Override
+    public Result<R> info(Integer id) { ... }
+    
+    // 构建查询条件
+    @Override
+    public QueryWrapper<R> checkWrapper(T req, boolean checkUser) { ... }
+    
+    // 构建MPJ查询条件
+    @Override
+    public MPJLambdaWrapper<R> mpjWrapper(Object req) { ... }
+}
+```
+
+#### 2.1.3 数据访问层(Mapper)
+
+**IBaseMapper 接口**
+
+`IBaseMapper` 接口继承了 MyBatis-Plus 的 `BaseMapper` 和 `MPJBaseMapper`,提供了基础的数据库操作方法。
+
+```java
+public interface IBaseMapper<T> extends BaseMapper<T>, MPJBaseMapper<T> {
+    // 基础方法由 BaseMapper 和 MPJBaseMapper 提供
+}
+```
+
+**IBaseProvider 接口**
+
+`IBaseProvider` 接口定义了动态 SQL 构建的方法,用于复杂查询场景。
+
+```java
+public interface IBaseProvider<T extends BaseReq, R extends BaseDto> {
+    List<Map<String, Object>> selectListMap(@Param("req") T req, @Param("clazz") Class<R> clazz);
+    long pageCount(String sql);
+    List<Map<String, Object>> page(String sql);
+    Map<String, Object> selectOneMap(@Param("req") T req, @Param("clazz") Class<R> clazz);
+    Map<String, Object> selectByIdMap(@Param("id") Serializable id, @Param("clazz") Class<R> clazz);
+    Map<String, Object> selectSumMap(@Param("req") T req, @Param("clazz") Class<R> clazz);
+    int save(T req);
+    int update(T req);
+}
+```
+
+**BaseProvider 实现类**
+
+`BaseProvider` 是 `IBaseProvider` 接口的抽象实现,提供了 SQL 构建的核心功能。
+
+```java
+public abstract class BaseProvider {
+    // 数据库操作类型
+    public DbMethodEnums dbMethod;
+    // 表计数器
+    public int tableCount = 0;
+    // 是否分页
+    public boolean isPage = false;
+    // 存储 insert
+    public Object insert;
+    // 记录 UPDATE
+    public Object update;
+    // 记录 SELECT
+    public Object select;
+    // 主表信息
+    public TableInfo superTable;
+    // 分页查询
+    public MPage mPage;
+    // 标记是否前端传排序字段
+    public boolean isFrontSort = false;
+    // 分页查询
+    public String pageCountSql;
+    // 记录 INSERT key-value(线程安全)
+    public final Map<String, Object> insertMap = new HashMap<>();
+    // 记录 UPDATE key-value(线程安全)
+    public final Map<String, Object> updateMap = new HashMap<>();
+    // 存储表信息的映射(线程安全)
+    public final Map<String, TableInfo> tableInfos = new ConcurrentHashMap<>();
+    // 记录 WHERE 条件(线程安全)
+    public final List<WhereInfo> whereInfos = new CopyOnWriteArrayList<>();
+    // 记录 SELECT 列(线程安全)
+    public final List<String> selectColumns = new CopyOnWriteArrayList<>();
+    // 记录 LEFT JOIN 信息(线程安全)
+    public final List<LeftJoinInfo> leftJoinInfos = new CopyOnWriteArrayList<>();
+    // 记录 ORDER BY 信息(线程安全)
+    // ...
+}
+```
+
+### 2.2 数据对象
+
+#### 2.2.1 基础请求对象(BaseReq)
+
+`BaseReq` 是所有请求对象的基类,提供了分页、排序等通用参数。
+
+```java
+@Data
+public class BaseReq {
+    @ApiModelProperty(hidden = true)
+    @TableField(exist = false)
+    public static List<String> types = Arrays.asList("pageNo", "pageSize", "sidx", "sord", "orderBy", "limit", "desensit");
+    
+    @ApiModelProperty(value = "页码,查询时使用", notes = "查询时使用", reference = "1", example = "1")
+    @TableField(exist = false)
+    private Integer pageNo = 1;
+    
+    @ApiModelProperty(value = "每页数量,查询时使用", notes = "查询时使用", reference = "10", example = "10")
+    @TableField(exist = false)
+    private Integer pageSize = 10;
+    
+    @ApiModelProperty(value = "排序字段,查询时使用", notes = "查询时使用", reference = " createTime ")
+    @TableField(exist = false)
+    private String sidx;
+    
+    @ApiModelProperty(value = "排序规则,查询时使用", notes = "查询时使用", reference = " ase ")
+    @TableField(exist = false)
+    private String sord;
+    
+    @ApiModelProperty(hidden = true)
+    @TableField(exist = false)
+    private String orderBy;
+    
+    @ApiModelProperty(hidden = true)
+    @TableField(exist = false)
+    private Integer limit;
+    
+    public Integer getLimit() {
+        return (pageNo - 1) * pageSize;
+    }
+}
+```
+
+#### 2.2.2 基础数据传输对象(BaseDto)
+
+`BaseDto` 是所有数据传输对象的基类,通常包含实体的基本属性和一些扩展属性。
+
+```java
+@Data
+public class BaseDto {
+    // 基础属性
+}
+```
+
+#### 2.2.3 基础实体对象(BaseEntity)
+
+`BaseEntity` 是所有实体对象的基类,提供了分页、排序等通用参数。
+
+```java
+@Data
+public class BaseEntity {
+    @ApiModelProperty(hidden = true)
+    @TableField(exist = false)
+    public static List<String> types = Arrays.asList("pageNo", "pageSize", "sidx", "sord", "orderBy", "limit", "desensit");
+    
+    @ApiModelProperty(value = "页码,查询时使用", notes = "查询时使用", reference = "1")
+    @TableField(exist = false)
+    private Integer pageNo = 1;
+    
+    @ApiModelProperty(value = "每页数量,查询时使用", notes = "查询时使用", reference = "10")
+    @TableField(exist = false)
+    private Integer pageSize = 10;
+    
+    @ApiModelProperty(value = "排序字段,查询时使用", notes = "查询时使用", reference = " createTime ")
+    @TableField(exist = false)
+    private String sidx;
+    
+    @ApiModelProperty(value = "排序规则,查询时使用", notes = "查询时使用", reference = " ase ")
+    @TableField(exist = false)
+    private String sord;
+    
+    @ApiModelProperty(hidden = true)
+    @TableField(exist = false)
+    private String orderBy;
+    
+    @ApiModelProperty(hidden = true)
+    @TableField(exist = false)
+    private Integer limit;
+    
+    public static boolean notIn(String type) {
+        return !types.contains(type);
+    }
+    
+    public Integer getLimit() {
+        return (pageNo - 1) * pageSize;
+    }
+}
+```
+
+#### 2.2.4 结果对象(Result)
+
+`Result` 是统一的响应结果对象,用于封装接口返回数据。
+
+```java
+@Data
+@ApiModel("通用返回结果")
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Result<T> implements Serializable {
+    @ApiModelProperty("是否成功")
+    private boolean success;
+    
+    @ApiModelProperty("状态码")
+    private Integer code;
+    
+    @ApiModelProperty("返回消息")
+    private String msg;
+    
+    @ApiModelProperty("返回数据")
+    private T data;
+    
+    @ApiModelProperty("总记录数")
+    private Long total;
+    
+    // 静态工厂方法
+    public static Result ok() { ... }
+    public static <R extends BaseDto> Result<R> ok(R data) { ... }
+    public static Result error(String msg) { ... }
+    public static Result error(Integer code, String msg) { ... }
+    public static <R extends BaseDto> Result<R> page(IPage<R> iPage) { ... }
+    // ...
+}
+```
+
+#### 2.2.5 用户信息对象(UserInfo)
+
+`UserInfo` 封装了当前登录用户的信息,用于权限控制和数据过滤。
+
+```java
+@Data
+@ToString
+public class UserInfo implements Serializable {
+    // 用户ID
+    private Integer id;
+    // 用户ID
+    private String userId;
+    // 用户头像
+    private String avatar;
+    // 角色编码
+    private String roleCode;
+    // 角色编码
+    private String role;
+    // 请求时间
+    private Long iat;
+    // 过期时间
+    private Long exp;
+    // 名称
+    private String displayName;
+    // ...
+    
+    // 商家ID
+    private Integer merchantId;
+    // 商家头像
+    private String merchantAvatar;
+    // 商家名称
+    private String merchantName;
+    // 数据权限
+    private DataAuth dataAuth;
+    // 是否是商家
+    private boolean merchant;
+    // ...
+    
+    // 判断是否有合作伙伴角色权限
+    public boolean hasPartnerRoleAuth(Roles role) { ... }
+}
+```
+
+### 2.3 工具类和注解
+
+#### 2.3.1 国际化支持
+
+**i18n 注解**
+
+`i18n` 注解用于标记需要国际化的字段、方法或类。
+
+```java
+@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface i18n {
+    boolean value() default true;
+    String sheetName() default "";
+    I18nFormat[] format() default {};
+}
+```
+
+**I18nUtils 工具类**
+
+`I18nUtils` 提供了获取国际化消息的方法。
+
+```java
+@Configuration
+public class I18nUtils {
+    // 定义占位符的正则表达式模式
+    private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(\\d+)\\}");
+    
+    private MessageProperties messageProperties;
+    
+    // 存储消息的Map,键是消息键,值是包含语言和消息的Map
+    private static Map<String, Map<String, String>> messages;
+    
+    // 初始化国际化工具类,并加载消息属性
+    @Autowired
+    public I18nUtils(MessageProperties messageProperties) { ... }
+    
+    // 根据枚举和参数获取国际化消息
+    public static String get(I18nMessageEnums i18n, Object... args) { ... }
+    
+    // ...
+}
+```
+
+**I18nMessageEnums 枚举**
+
+`I18nMessageEnums` 定义了系统中所有的国际化消息。
+
+```java
+@Getter
+public enum I18nMessageEnums {
+    // 请求成功
+    SUCCESS("success", "成功", "zh"),
+    // 请求失败
+    REQUEST_ERROR("request_error", "请求失败", "zh"),
+    // 无权操作
+    NO_PERMISSION("no_permission", "无权操作{0}", "zh"),
+    // ...
+    
+    private String code; // 多语言标识
+    private String message; // 默认信息
+    private String lang; // 默认语言
+    
+    I18nMessageEnums(String code, String message, String lang) { ... }
+}
+```
+
+#### 2.3.2 Servlet 工具类
+
+`ServletUtils` 提供了获取请求、响应、会话以及用户信息的方法。
+
+```java
+public class ServletUtils {
+    // 获取请求对象
+    public static HttpServletRequest getRequest() { ... }
+    
+    // 获取响应对象
+    public static HttpServletResponse getResponse() { ... }
+    
+    // 获取会话对象
+    public static HttpSession getSession() { ... }
+    
+    // 获取用户信息
+    public static UserInfo getUserInfo() { ... }
+    
+    // 判断是否国际版
+    public static boolean isI18n() { ... }
+    
+    // 判断接口是否支持国际版
+    public static boolean isI18nSupport() { ... }
+    
+    // ...
+}
+```
+
+#### 2.3.3 数据库注解
+
+**Select 注解**
+
+`Select` 注解用于指定查询字段。
+
+```java
+@Select(table = UserEntity.class, fieldName = "username", distinct = true)
+private String username;
+```
+
+**Where 注解**
+
+`Where` 注解用于指定查询条件。
+
+```java
+@Where(fieldName = "status", operator = EQ)
+private String status;
+```
+
+**LeftJoin 注解**
+
+`LeftJoin` 注解用于指定左连接查询。
+
+```java
+@LeftJoin(table = RoleEntity.class, leftTable = UserEntity.class, fieldName = "id", leftFieldName = "roleId")
+private String roleName;
+```
+
+**OrderBy 注解**
+
+`OrderBy` 注解用于指定排序字段。
+
+```java
+@OrderBy(fieldName = "createTime", sort = "DESC")
+private String createTime;
+```
+
+## 3. 使用示例
+
+### 3.1 创建控制器
+
+```java
+@Slf4j
+@Api(value = "球队基础信息管理", tags = "球队基础信息管理")
+@RestController
+@RequestMapping("/api/v1/teamInfo")
+public class TeamBaseInfoController extends BaseController<TeamBaseInfoService, TeamBaseInfoReq, TeamBaseInfoDto> {
+    
+    @ApiOperation(value = "详情", notes = "详情")
+    @ApiResponses({
+        @ApiResponse(code = 200, message = "返回结果:")
+    })
+    @Log(title = "详情", businessType = BusinessType.UPDATE)
+    @UserLoginToken(faceVerify = false, roles = {Roles.ADMIN, Roles.SHIPPING, Roles.CUSTOMER})
+    @i18n(format = {I18nFormat.SEARCH})  // 支持国际化
+    @PostMapping("/detail/{id}")
+    public Result<TeamBaseInfoDto> detail(@PathVariable("id") Long id) {
+        return baseService.detail(id);
+    }
+}
+```
+
+### 3.2 创建服务
+
+```java
+public interface TeamBaseInfoService extends BaseService<TeamBaseInfoReq, TeamBaseInfoDto> {
+    Page<TeamInfoPageDto> page(TeamInfoPageReq req);
+    Result add(TeamInfoCreateReq req);
+    Result update(TeamInfoUpdateReq req);
+    Result delete(RemoveInfoReq req);
+    List<TeamBaseInfoDto> searchTeamInfosByTeamSimpleName(TeamBaseInfoReq req);
+    List<TeamBaseInfoDto> getByIds(List<Long> ids);
+}
+```
+
+### 3.3 实现服务
+
+```java
+@Slf4j
+@Service
+public class TeamBaseInfoServiceImpl extends BaseServiceImpl<TeamBaseInfoMapper, TeamBaseInfoReq, TeamBaseInfoDto> implements TeamBaseInfoService {
+    
+    @Override
+    public Result add(TeamInfoCreateReq req) {
+        // 格式化简称
+        String simpleName = ObjectUtil.getSimpleName(req.getNameEn());
+        // 查询是否已经存在该名称
+        TeamBaseInfoDto dto = (TeamBaseInfoDto) baseMapper.selectOne(
+            new TeamBaseInfoReq(req.getSport(), simpleName), TeamBaseInfoDto.class);
+        if (Objects.nonNull(dto) && Objects.nonNull(dto.getId())) {
+            return Result.error(I18nUtils.get(DATA_EXIST, req.getNameEn()));
+        }
+        req.setSimpleName(simpleName);
+        return Result.ret(baseMapper.insert(req));
+    }
+    
+    @Override
+    public Result update(TeamInfoUpdateReq req) {
+        // 实现更新逻辑
+        // ...
+        return Result.ret(baseMapper.updateById(dto));
+    }
+    
+    // 其他方法实现
+    // ...
+}
+```
+
+### 3.4 创建 Mapper
+
+```java
+public interface TeamBaseInfoMapper extends IBaseMapper<TeamBaseInfoDto>, IBaseProvider<TeamBaseInfoReq, TeamBaseInfoDto> {
+    // 可以添加自定义的查询方法
+}
+```
+
+### 3.5 创建请求对象
+
+```java
+@Data
+@ApiModel(value = "球队基础信息请求参数", description = "球队基础信息请求参数")
+@TableName("tzy_team_info")
+public class TeamBaseInfoReq extends BaseReq {
+    @ApiModelProperty(value = "运动")
+    @TableField(value = "sport")
+    @Where(fieldName = "sport", operator = EQ)
+    private String sport;
+    
+    @ApiModelProperty(value = "简称")
+    @TableField(value = "simple_name")
+    @Where(fieldName = "simple_name", operator = EQ)
+    private String simpleName;
+    
+    // 构造方法
+    public TeamBaseInfoReq() {}
+    
+    public TeamBaseInfoReq(String sport, String simpleName) {
+        this.sport = sport;
+        this.simpleName = simpleName;
+    }
+}
+```
+
+### 3.6 创建数据传输对象
+
+```java
+@Data
+@ApiModel(value = "球队基础信息返回参数", description = "球队基础信息返回参数")
+@TableName("tzy_team_info")
+public class TeamBaseInfoDto extends BaseDto {
+    @ApiModelProperty(value = "ID")
+    @TableId(value = "id")
+    private Long id;
+    
+    @ApiModelProperty(value = "区分是否商家创建:merchant=商家创建")
+    @TableField(value = "code")
+    private String code;
+    
+    @ApiModelProperty(value = "中文名")
+    @TableField(value = "display_name")
+    private String displayName;
+    
+    @ApiModelProperty(value = "英文名")
+    @TableField(value = "display_name_en")
+    private String displayNameEn;
+    
+    @ApiModelProperty(value = "运动")
+    @TableField(value = "sport")
+    private String sport;
+    
+    @ApiModelProperty(value = "头像")
+    @TableField(value = "head_url")
+    private String headUrl;
+    
+    @ApiModelProperty(value = "国际化:其他语言翻译:json 字符串")
+    @TableField(value = "translations")
+    private String translations;
+    
+    @ApiModelProperty(value = "简称")
+    @TableField(value = "simple_name")
+    private String simpleName;
+}
+```
+
+## 4. 最佳实践
+
+### 4.1 分层架构
+
+- **控制层(Controller)**:负责处理请求、参数校验和返回结果,不包含业务逻辑。
+- **服务层(Service)**:负责业务逻辑处理,包括数据校验、业务规则和事务管理。
+- **数据访问层(Mapper)**:负责数据库操作,包括 CRUD 和复杂查询。
+
+### 4.2 代码生成
+
+框架提供了代码生成模板,可以快速生成基础的 Controller、Service、ServiceImpl、Mapper 等类。
+
+```
+// controller.java.vm
+package ${package.Controller};
+
+import ${package.Parent}.base.controller.BaseController;
+import ${package.Parent}.dto.${entity}Dto;
+import ${package.Entity}.${entity};
+import ${package.Service}.${table.serviceName};
+// ...
+
+@RestController
+@RequestMapping("#if(${package.ModuleName})/${package.ModuleName}#end/#if(${controllerMappingHyphenStyle})${controllerMappingHyphen}#else${table.entityPath}#end")
+public class ${table.controllerName} extends BaseController<${table.serviceName}, ${entity}, ${entity}Dto> {
+}
+```
+
+### 4.3 国际化支持
+
+使用 `i18n` 注解和 `I18nUtils` 工具类可以轻松实现国际化支持。
+
+```java
+@i18n(format = {I18nFormat.SEARCH})  // 支持国际化
+@PostMapping("/detail/{id}")
+public Result<TeamBaseInfoDto> detail(@PathVariable("id") Long id) {
+    return baseService.detail(id);
+}
+```
+
+### 4.4 异常处理
+
+使用全局异常处理器 `GlobalExceptionHandler` 统一处理异常,并返回友好的错误信息。
+
+```java
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+    @ExceptionHandler(AuthException.class)
+    public Result handleAuthException(AuthException e) {
+        return Result.error(e.getCode(), e.getMessage());
+    }
+    
+    @ExceptionHandler(BusinessException.class)
+    public Result handleBusinessException(BusinessException e) {
+        return Result.error(e.getCode(), e.getMessage());
+    }
+    
+    // 其他异常处理方法
+    // ...
+}
+```
+
+### 4.5 数据权限控制
+
+通过 `UserInfo` 和 `DataAuth` 实现数据权限控制,确保用户只能访问有权限的数据。
+
+```java
+// 在 BaseServiceImpl 中的 checkWrapper 方法中实现数据权限控制
+UserInfo userInfo = checkUser ? ServletUtils.getUserInfo() : null;
+if (null != userInfo && checkUser) {
+    switch (userInfo.getDataAuth()) {
+        case DEPT: // 部门[商家]级
+            if ("merchantId".equals(name) || "merId".equals(name)) {
+                wrapper = wrapper.eq(column, userInfo.getMerchantId());
+            }
+            break;
+        case PERSON: // 个人
+            if ("userId".equals(name) || "accountId".equals(name) || "createBy".equals(name)) {
+                wrapper = wrapper.eq(column, userInfo.getId());
+            }
+            break;
+        default:
+            if (null != value) {
+                wrapper = wrapper.eq(column, value);
+            }
+    }
+}
+```
+
+## 5. 总结
+
+Poyee Base 框架提供了一套完整的分层架构实现,通过抽象基类和泛型机制,实现了代码复用和业务逻辑的统一管理。该框架具有以下特点:
+
+1. **分层架构**:清晰的控制层、服务层、数据访问层分离,职责明确。
+2. **代码复用**:通过抽象基类和泛型机制,减少重复代码。
+3. **统一响应**:使用 `Result` 对象统一封装接口返回数据。
+4. **国际化支持**:通过 `i18n` 注解和 `I18nUtils` 工具类实现国际化。
+5. **数据权限控制**:通过 `UserInfo` 和 `DataAuth` 实现数据权限控制。
+6. **异常处理**:使用全局异常处理器统一处理异常。
+
+通过使用 Poyee Base 框架,可以快速构建高质量的企业级应用,提高开发效率和代码质量。

+ 192 - 0
py-base/easy.md

@@ -0,0 +1,192 @@
+# Poyee Easy 包功能说明
+
+## 概述
+
+`com.poyee.easy` 包是 Poyee 基础框架中的一个重要组件,主要提供了基于 EasyExcel 的 Excel 导入导出功能,并且特别支持国际化(i18n)处理。该包通过封装 Alibaba EasyExcel 库,提供了更高级的功能和更简洁的 API,使得 Excel 的导入导出操作更加便捷,特别是在需要多语言支持的国际化应用场景中。
+
+## 核心组件
+
+### 1. EasyExcelUtil
+
+这是一个用于 Excel 导出的工具类,主要功能包括:
+
+- 支持普通 Excel 导出和国际化 Excel 导出
+- 提供链式调用 API,如 `init()`, `i18nConfig()`, `build()` 等
+- 支持文件导出和流导出两种方式
+- 支持多 Sheet 导出,特别是国际化场景下的多语言 Sheet
+
+**主要方法:**
+
+- `init(Class<T> clazz, List<T> data)`: 初始化导出工具
+- `init(Class<T> clazz, List<T> data, String sheet)`: 初始化导出工具并指定 sheet 名称
+- `i18nConfig(EasyI18nConfig i18nConfig)`: 设置国际化配置
+- `build()`: 构建导出工具
+- `doWrite(String path)`: 导出到文件
+- `doWriteStream(OutputStream out)`: 导出到输出流
+
+**使用示例:**
+
+```java
+EasyExcelUtil<ChecklistExcelDto, Object> easyExcelUtil = EasyExcelUtil.init(ChecklistExcelDto.class, checklistExcelDtos, baseInfo.getDisplayName())
+    .i18nConfig(i18nConfig)
+    .build();
+easyExcelUtil.doWriteStream(outputStream);
+```
+
+### 2. EasyExcelImportUtil
+
+这是一个用于 Excel 导入的工具类,主要功能包括:
+
+- 支持普通 Excel 导入和国际化 Excel 导入
+- 提供链式调用 API,如 `create()`, `i18nConfig()`, `batch()`, `async()` 等
+- 支持批量处理和异步处理导入数据
+- 支持从 URL 下载 Excel 文件并解析
+
+**主要方法:**
+
+- `create(Class<T> clazz, Consumer<List<R>> consumer, Function<T, R> rowConverter)`: 创建导入工具
+- `i18nConfig(boolean isI18n, EasyI18nConfig i18nConfig)`: 设置国际化配置
+- `batch()`: 启用批量处理
+- `batch(int batchCount)`: 启用批量处理并设置批处理数量
+- `async(ThreadPoolExecutor executor)`: 启用异步处理
+- `doRead(InputStream inputStream)`: 从输入流读取
+- `doRead(String fileUrl)`: 从 URL 读取
+
+**使用示例:**
+
+```java
+List<PaniniChecklistImport> list = new ArrayList<>();
+EasyExcelImportUtil<PaniniChecklistImport, PaniniChecklistImport> importer = 
+    EasyExcelImportUtil.create(PaniniChecklistImport.class, list::addAll, Function.identity())
+    .i18nConfig(true, i18nConfig)
+    .batch();
+importer.doRead(url);
+```
+
+### 3. EasyExcelReadSheetListener
+
+这是一个 EasyExcel 的事件监听器,用于处理 Excel 读取过程中的事件,特别是国际化数据的处理:
+
+- 继承自 `AnalysisEventListener`,用于监听 Excel 读取过程
+- 支持多 Sheet 读取,特别是国际化场景下的多语言 Sheet
+- 提供数据转换和处理功能
+
+**主要方法:**
+
+- `EasyExcelReadSheetListener(EasyI18nConfig config, Function<T, R> rowConverter, Consumer<List<R>> consumer)`: 构造函数
+- `init()`: 初始化监听器
+- `invoke(T data, AnalysisContext context)`: 处理每行数据
+- `doAfterAllAnalysed(AnalysisContext context)`: 所有数据解析完成后的处理
+
+### 4. EasyI18nConfig
+
+这是一个国际化配置类,用于配置国际化 Excel 的导入导出:
+
+- 定义了 Sheet 的结构,包括 key、name、header 和 data 等属性
+- 支持多语言配置
+
+**主要属性:**
+
+- `sheet`: Sheet 配置列表
+
+**Sheet 类属性:**
+
+- `index`: Sheet 索引
+- `key`: Sheet 映射的字段
+- `name`: Sheet 名称
+- `header`: Sheet 头部映射
+- `data`: Sheet 数据
+
+**使用示例:**
+
+```java
+EasyI18nConfig i18nConfig = new EasyI18nConfig();
+List<EasyI18nConfig.Sheet> sheetList = new ArrayList<>();
+sheetList.add(new EasyI18nConfig.Sheet("playerTranslations", "player", Language.getLangEnMap()));
+sheetList.add(new EasyI18nConfig.Sheet("teamTranslations", "team", Language.getLangEnMap()));
+i18nConfig.setSheet(sheetList);
+```
+
+## 国际化支持
+
+该包特别强调了对国际化的支持,主要体现在:
+
+1. **多语言 Sheet**:支持在一个 Excel 文件中包含多个语言的 Sheet,如主数据 Sheet 和语言翻译 Sheet
+
+2. **国际化注解**:使用 `@ExcelI18nFromat` 和 `@i18n` 注解来标记需要国际化处理的字段
+
+3. **动态语言处理**:根据 `ServletUtils.isI18nSupport()` 判断是否需要进行国际化处理
+
+4. **JSON 处理**:使用 fastjson 库处理国际化数据的 JSON 格式化和解析
+
+## 业务应用
+
+在项目中,这些工具类主要用于:
+
+1. **Checklist 导入导出**:如 `CardChecklistBaseInfoServiceImpl` 中的 `downloadAndImport` 和 `downloadCheckList` 方法
+
+2. **国际化数据处理**:如 `doI18nImportChecklist` 和 `downloadI18nExcelAndParseExcel` 方法
+
+## 最佳实践
+
+### 导出 Excel
+
+```java
+// 1. 准备数据
+List<YourDto> dataList = ...
+
+// 2. 创建国际化配置(如果需要)
+EasyI18nConfig i18nConfig = new EasyI18nConfig();
+List<EasyI18nConfig.Sheet> sheetList = new ArrayList<>();
+sheetList.add(new EasyI18nConfig.Sheet("fieldName", "sheetName", languageMap));
+i18nConfig.setSheet(sheetList);
+
+// 3. 初始化导出工具
+EasyExcelUtil<YourDto, Object> easyExcelUtil = EasyExcelUtil.init(YourDto.class, dataList, "Sheet名称")
+    .i18nConfig(i18nConfig)  // 如果需要国际化
+    .build();
+
+// 4. 导出到文件或流
+easyExcelUtil.doWrite("path/to/file.xlsx");  // 导出到文件
+// 或
+easyExcelUtil.doWriteStream(outputStream);  // 导出到流
+```
+
+### 导入 Excel
+
+```java
+// 1. 准备接收数据的集合
+List<YourDto> list = new ArrayList<>();
+
+// 2. 创建国际化配置(如果需要)
+EasyI18nConfig i18nConfig = new EasyI18nConfig();
+List<EasyI18nConfig.Sheet> sheetList = new ArrayList<>();
+sheetList.add(new EasyI18nConfig.Sheet("fieldName", "sheetName", languageMap));
+i18nConfig.setSheet(sheetList);
+
+// 3. 创建导入工具
+EasyExcelImportUtil<YourDto, YourDto> importer = 
+    EasyExcelImportUtil.create(YourDto.class, list::addAll, Function.identity())
+    .i18nConfig(true, i18nConfig)  // 如果需要国际化
+    .batch(500)  // 批量处理,每500条处理一次
+    .async(executor);  // 如果需要异步处理
+
+// 4. 从文件或URL读取
+importer.doRead(inputStream);  // 从输入流读取
+// 或
+importer.doRead("http://example.com/file.xlsx");  // 从URL读取
+```
+
+## 注意事项
+
+1. 在使用国际化功能时,需要确保 DTO 类中有正确的注解标记
+
+2. 导入大量数据时,建议使用批量处理和异步处理功能
+
+3. 导出国际化 Excel 时,需要正确配置 EasyI18nConfig
+
+4. 使用 URL 导入时,需要确保 URL 可访问且格式正确
+
+## 总结
+
+`com.poyee.easy` 包是一个专注于 Excel 处理的工具包,它基于 EasyExcel 库,提供了更高级的功能,特别是国际化支持。它通过链式 API 和注解的方式,简化了 Excel 导入导出的开发工作,并且特别适合需要多语言支持的国际化应用场景。

+ 346 - 0
py-base/i18n.md

@@ -0,0 +1,346 @@
+# Poyee 国际化(i18n)功能说明
+
+## 1. 概述
+
+Poyee 框架提供了完善的国际化(i18n)支持,使应用能够根据用户的语言偏好自动切换界面文本、错误消息和业务数据。国际化功能主要由 `com.poyee.i18n` 包和相关注解、工具类组成,支持多语言消息配置、动态语言切换和国际化数据处理。
+
+## 2. 核心组件
+
+### 2.1 `i18n` 注解
+
+`i18n` 注解用于标记需要国际化的字段、方法或类。
+
+```java
+@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface i18n {
+    // 是否启用国际化
+    boolean value() default true;
+    // Excel导出时的sheet名称
+    String sheetName() default "";
+    // 国际化格式
+    I18nFormat[] format() default {};
+}
+```
+
+### 2.2 `I18nUtils` 工具类
+
+`I18nUtils` 是国际化功能的核心工具类,提供了获取国际化消息的方法。
+
+```java
+@Configuration
+public class I18nUtils {
+    // 定义占位符的正则表达式模式
+    private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(\\d+)\\}");
+    
+    private MessageProperties messageProperties;
+    
+    // 存储消息的Map,键是消息键,值是包含语言和消息的Map
+    private static Map<String, Map<String, String>> messages;
+    
+    // 初始化国际化工具类,并加载消息属性
+    @Autowired
+    public I18nUtils(MessageProperties messageProperties) {
+        this.messageProperties = messageProperties;
+        messages = messageProperties.init();
+    }
+    
+    // 根据枚举和参数获取国际化消息
+    public static String get(I18nMessageEnums i18n, Object... args) {
+        String key = i18n.getCode();
+        Map<String, String> translations = messages.get(key);
+        String message;
+        if (Objects.isNull(translations)) {
+            // 如果没有找到对应的翻译,使用枚举中的默认消息
+            message = i18n.getMessage();
+        } else {
+            // 根据当前的locale获取对应的翻译
+            Locale locale = LocaleContextHolder.getLocale();
+            message = translations.getOrDefault(locale.getLanguage(), i18n.getLang());
+        }
+
+        // 替换消息中的占位符
+        return replacePlaceholders(message, args);
+    }
+    
+    // 其他辅助方法...
+}
+```
+
+### 2.3 `MessageProperties` 配置类
+
+`MessageProperties` 负责从配置文件中加载国际化消息。
+
+```java
+@Component
+public class MessageProperties {
+
+    @Autowired
+    private ResourceLoader resourceLoader;
+
+    public Map<String, Map<String, String>> init() {
+        return loadMessagesFromYaml();
+    }
+
+    public Map<String, Map<String, String>> loadMessagesFromYaml() {
+        Resource resource = resourceLoader.getResource("classpath:I18n/message.yml");
+        try (InputStream input = resource.getInputStream()) {
+            Yaml yaml = new Yaml();
+            Map map = yaml.loadAs(input, Map.class);
+            return map;
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to load message.yml", e);
+        }
+    }
+}
+```
+
+### 2.4 `I18nMessageEnums` 枚举
+
+`I18nMessageEnums` 定义了系统中所有的国际化消息。
+
+```java
+@Getter
+public enum I18nMessageEnums {
+    // 请求成功
+    SUCCESS("success", "成功", "zh"),
+    // 请求失败
+    REQUEST_ERROR("request_error", "请求失败", "zh"),
+    // 无权操作
+    NO_PERMISSION("no_permission", "无权操作{0}", "zh"),
+    // 更多消息...
+    
+    private String code; // 多语言标识
+    private String message; // 默认信息
+    private String lang; // 默认语言
+    
+    I18nMessageEnums(String code, String message, String lang) {
+        this.code = code;
+        this.message = message;
+        this.lang = lang;
+    }
+}
+```
+
+### 2.5 `i18nAspect` 切面类
+
+`i18nAspect` 是一个切面类,用于处理国际化相关的逻辑。
+
+```java
+@Slf4j
+@Aspect
+@Component
+public class i18nAspect {
+
+    // 配置织入点,针对使用了i18n注解的方法
+    @Pointcut("@annotation(com.poyee.annotation.I18n)")
+    public void logPointCut() {
+    }
+
+    // 在控制器的方法执行前,处理国际化逻辑
+    @Before("execution(public * com.poyee.controller.*.*(..))")
+    public void doBefore(JoinPoint joinPoint) {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+        checkI18n(method);
+    }
+
+    // 判断是否支持国际化,并根据情况设置会话属性
+    private void checkI18n(Method method) {
+        if (method.isAnnotationPresent(i18n.class)) {
+            i18n i18n = method.getAnnotation(i18n.class);
+            HttpServletRequest request = ServletUtils.getRequest();
+            String language = request.getHeader("language");
+            boolean isI18n = true;
+            if(StringUtils.isBlank(language)) {
+                language = LocaleContextHolder.getLocale().getLanguage();
+                isI18n = false;
+            }
+
+            try {
+                HttpSession session = ServletUtils.getSession(false);
+                if (session == null) {
+                    log.warn("Session is null, cannot set I18n attributes.");
+                    return;
+                }
+
+                // 根据方法上的i18n注解和请求头中的语言信息,决定是否启用国际化
+                if(i18n.value() && isI18n) {
+                    session.setAttribute("language", language);
+                    I18nFormat[] format = i18n.format();
+                    if(format.length > 0) {
+                        session.setAttribute("i18nFormat", i18n.format());
+                        if (log.isInfoEnabled()) {
+                            log.info("i18nFormat:{}", format[0].getMsg());
+                        }
+                    }
+                    session.setAttribute("i18n", true);
+                } else {
+                    // 如果 I18n.value() 为 false,但 isI18n 为 true,仍不启用国际化
+                    session.setAttribute("i18n", false);
+                }
+            } catch (Exception e) {
+                log.error("Failed to set I18n attributes", e);
+            }
+        }
+    }
+}
+```
+
+### 2.6 `I18nFormat` 枚举
+
+`I18nFormat` 定义了国际化格式的类型。
+
+```java
+@Getter
+public enum I18nFormat {
+
+    INSERT("新增"),
+    UPDATE("编辑"),
+    DELETE("删除"),
+    SEARCH("查询"),
+    EXPORT("导出"),
+    IMPORT("导入")
+    ;
+
+    private String msg;
+
+    I18nFormat(String msg) {
+        this.msg = msg;
+    }
+}
+```
+
+## 3. 国际化配置文件
+
+国际化消息配置文件位于 `classpath:i18n/message.yml`,采用 YAML 格式,结构如下:
+
+```yaml
+success:
+  en: "Success"
+  zh: "成功"
+  
+request_error:
+  en: "Request failed"
+  zh: "请求失败"
+  
+no_permission:
+  en: "No permission to operate {0}"
+  zh: "无权操作{0}"
+
+# 更多消息...
+```
+
+## 4. 使用方法
+
+### 4.1 在控制器中启用国际化
+
+在控制器方法上添加 `@i18n` 注解,启用国际化支持:
+
+```java
+@RestController
+@RequestMapping("/api/users")
+public class UserController {
+
+    @GetMapping("/list")
+    @i18n(format = {I18nFormat.SEARCH})
+    public Result<List<UserDto>> listUsers() {
+        // 业务逻辑...
+        return Result.success(userList);
+    }
+    
+    @PostMapping("/create")
+    @i18n(format = {I18nFormat.INSERT})
+    public Result<Void> createUser(@RequestBody UserCreateReq req) {
+        // 业务逻辑...
+        return Result.success();
+    }
+}
+```
+
+### 4.2 获取国际化消息
+
+使用 `I18nUtils.get()` 方法获取国际化消息:
+
+```java
+// 简单消息
+String successMsg = I18nUtils.get(SUCCESS);
+
+// 带参数的消息
+String noPermissionMsg = I18nUtils.get(NO_PERMISSION, "用户管理");
+
+// 在异常中使用
+throw new ServiceException(I18nUtils.get(DATA_NOT_EXIST, "用户"));
+
+// 在返回结果中使用
+return Result.error(I18nUtils.get(PARAM_ERROR));
+```
+
+### 4.3 标记国际化字段
+
+在实体类或DTO中,使用 `@i18n` 注解标记需要国际化的字段:
+
+```java
+@Data
+public class ProductDto extends BaseDto {
+    private Integer id;
+    private String code;
+    
+    @TableField("name")
+    private String name;
+    
+    @i18n
+    @TableField("name_i18n")
+    private String nameI18n;
+    
+    // 其他字段...
+}
+```
+
+### 4.4 在服务层处理国际化字段
+
+在服务实现类中,可以根据是否启用国际化来决定是否查询国际化字段:
+
+```java
+public class ProductServiceImpl extends BaseServiceImpl<ProductMapper, ProductReq, ProductDto> implements ProductService {
+
+    @Override
+    public Result<Page<ProductDto>> page(ProductPageReq req) {
+        MPJLambdaWrapper<ProductDto> wrapper = new MPJLambdaWrapper<>();
+        wrapper.selectAll(ProductDto.class);
+        
+        // 根据是否启用国际化,决定是否查询国际化字段
+        checkI18n(wrapper, ProductDto.class);
+        
+        // 其他查询条件...
+        
+        return Result.success(baseMapper.selectPage(req.getPage(), wrapper));
+    }
+}
+```
+
+## 5. 最佳实践
+
+### 5.1 统一使用枚举定义消息
+
+所有国际化消息都应该在 `I18nMessageEnums` 中定义,避免硬编码消息字符串。
+
+### 5.2 合理使用占位符
+
+消息中的可变部分应使用占位符 `{0}`, `{1}` 等,而不是字符串拼接。
+
+### 5.3 控制器方法添加国际化注解
+
+所有需要支持国际化的控制器方法都应添加 `@i18n` 注解,并指定适当的 `format`。
+
+### 5.4 国际化字段命名规范
+
+国际化字段通常以原字段名加 `I18n` 后缀命名,如 `name` 和 `nameI18n`。
+
+## 6. 注意事项
+
+1. 国际化功能依赖于 Session,确保在无状态环境(如API网关)中正确传递语言信息。
+2. 国际化消息配置文件的修改需要重启应用才能生效。
+3. 默认语言为中文(zh),如需更改默认语言,需修改 `I18nMessageEnums` 中的定义。
+4. 在前端请求中,可通过 `language` 请求头指定语言,如 `language: en`。

+ 299 - 0
py-base/pom.xml

@@ -0,0 +1,299 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         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>com.poyee</groupId>
+        <artifactId>poyee-ecology</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>py-base</artifactId>
+    <name>基础模块</name>
+    <description>基础模块</description>
+    <properties>
+        <baomidou.version>3.5.2</baomidou.version>
+        <mybatis.version>2.3.0</mybatis.version>
+        <redis.version>2.1.13.RELEASE</redis.version>
+        <jedis.version>3.1.0</jedis.version>
+        <redission.version>3.5.0</redission.version>
+    </properties>
+    <dependencies>
+        <!--spring-boot-starter-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <!-- SpringBoot 拦截器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+            <version>${springboot.version}</version>
+        </dependency>
+        <!--spring-->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+        </dependency>
+        <!--热更新-->
+        <!--<dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+        </dependency>-->
+        <!--aspectj-->
+        <dependency>
+            <groupId>org.aspectj</groupId>
+            <artifactId>aspectjweaver</artifactId>
+        </dependency>
+
+        <!-- redis 环境 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.session</groupId>
+            <artifactId>spring-session-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson</artifactId>
+            <version>3.5.0</version>
+        </dependency>
+        <dependency>
+            <groupId>redis.clients</groupId>
+            <artifactId>jedis</artifactId>
+            <version>3.5.0</version>
+        </dependency>
+
+        <!--七牛云存储-->
+        <!--<dependency>
+            <groupId>com.qiniu</groupId>
+            <artifactId>qiniu-java-sdk</artifactId>
+            <version>[7.8.0, 7.8.99]</version>
+        </dependency>-->
+        <!--google-->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>29.0-jre</version>
+        </dependency>
+        <!--fluent-log-->
+        <dependency>
+            <groupId>org.codehaus.janino</groupId>
+            <artifactId>janino</artifactId>
+            <version>3.0.12</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sndyuk</groupId>
+            <artifactId>logback-more-appenders</artifactId>
+            <version>1.8.5</version>
+        </dependency>
+        <dependency>
+            <groupId>org.fluentd</groupId>
+            <artifactId>fluent-logger</artifactId>
+            <version>0.3.4</version>
+        </dependency>
+        <!--lombok-->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+        <!--fastjson-->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>${fastjson.version}</version>
+        </dependency>
+        <!--io常用工具类 -->
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>${commons.io.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-beanutils</groupId>
+            <artifactId>commons-beanutils</artifactId>
+            <version>1.9.4</version>
+        </dependency>
+        <!--spring-jdbc-->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-jdbc</artifactId>
+        </dependency>
+        <!--阿里数据库连接池 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+            <version>${druid.version}</version>
+        </dependency>
+        <!--druid-->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid</artifactId>
+            <version>${druid.version}</version>
+            <scope>compile</scope>
+        </dependency>
+        <!--mybatis-spring-->
+        <dependency>
+            <groupId>org.mybatis.spring.boot</groupId>
+            <artifactId>mybatis-spring-boot-autoconfigure</artifactId>
+            <version>${mybatis.version}</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mybatis</groupId>
+            <artifactId>mybatis-spring</artifactId>
+            <version>2.0.7</version>
+            <scope>compile</scope>
+        </dependency>
+        <!--mybatis-baomidou-->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-extension</artifactId>
+            <version>${baomidou.version}</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-generator</artifactId>
+            <version>${baomidou.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-core</artifactId>
+            <version>${baomidou.version}</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+            <version>${baomidou.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+            <version>${baomidou.version}</version>
+        </dependency>
+        <!--mybatis-plus-->
+        <dependency>
+            <groupId>com.github.yulichang</groupId>
+            <artifactId>mybatis-plus-join-boot-starter</artifactId>
+            <version>1.4.2.2</version>
+        </dependency>
+        <!--postgresql-->
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.velocity</groupId>
+            <artifactId>velocity</artifactId>
+            <version>1.7</version>
+        </dependency>
+        <!--yaml-->
+        <!--<dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+        </dependency>-->
+        <!--httpClient-->
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpcore</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpmime</artifactId>
+        </dependency>
+        <!--jwt 鉴权-->
+        <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+            <version>3.3.0</version>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+            <version>0.9.0</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>5.8.11</version>
+        </dependency>
+        <!--easyexcel-->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>3.3.4</version>
+        </dependency>
+        <!-- knife4j 接口管理 -->
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-spring-boot-starter</artifactId>
+            <version>2.0.9</version>
+        </dependency>
+        <!-- swagger2-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+            <version>2.10.5</version>
+            <!--<exclusions>
+                <exclusion>
+                    <groupId>io.swagger</groupId>
+                    <artifactId>swagger-annotations</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>io.swagger</groupId>
+                    <artifactId>swagger-models</artifactId>
+                </exclusion>
+            </exclusions>-->
+        </dependency>
+        <!--防止进入swagger页面报类型转换错误,排除2.9.2中的引用,手动增加1.5.21版本-->
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-annotations</artifactId>
+            <version>1.5.21</version>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-models</artifactId>
+            <version>1.5.21</version>
+        </dependency>
+        <!-- swagger2-UI-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+            <version>2.9.2</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>swagger-bootstrap-ui</artifactId>
+            <version>1.9.5</version>
+        </dependency>
+        <!-- Spring Cloud OpenFeign -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+        </dependency>
+        <!-- Spring Cloud LoadBalancer -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 593 - 0
py-base/provider.md

@@ -0,0 +1,593 @@
+# Provider 包说明文档
+
+## 概述
+
+`com.poyee.base.mapper.provider` 包是一个用于动态构建 SQL 语句的工具包,它基于注解驱动,通过解析实体类和 DTO 上的注解来自动生成 SQL 查询语句。该包主要用于简化数据库操作,支持复杂的查询条件、连接查询、排序、分页等功能,使开发者能够以更加面向对象的方式编写数据库访问代码。
+
+## 核心类
+
+### BaseProvider
+
+`BaseProvider` 是整个包的核心抽象类,提供了基础的 SQL 构建功能,包括左连接和 WHERE 条件的处理。它维护了一系列线程安全的集合来存储 SQL 构建过程中的各种信息:
+
+```java
+public abstract class BaseProvider {
+    // 数据库操作类型
+    public DbMethodEnums dbMethod;
+    // 表计数器
+    public int tableCount = 0;
+    // 是否分页
+    public boolean isPage = false;
+    // 存储 insert
+    public Object insert;
+    // 记录 UPDATE
+    public Object update;
+    // 记录 SELECT
+    public Object select;
+    // 主表信息
+    public TableInfo superTable;
+    // 分页查询
+    public MPage mPage;
+    // 标记是否前端传排序字段
+    public boolean isFrontSort = false;
+    // 分页查询SQL
+    public String pageCountSql;
+    // 记录 INSERT key-value(线程安全)
+    public final Map<String, Object> insertMap = new HashMap<>();
+    // 记录 UPDATE key-value(线程安全)
+    public final Map<String, Object> updateMap = new HashMap<>();
+    // 存储表信息的映射(线程安全)
+    public final Map<String, TableInfo> tableInfos = new ConcurrentHashMap<>();
+    // 记录 WHERE 条件(线程安全)
+    public final List<WhereInfo> whereInfos = new CopyOnWriteArrayList<>();
+    // 记录 SELECT 列(线程安全)
+    public final List<String> selectColumns = new CopyOnWriteArrayList<>();
+    // 记录 LEFT JOIN 信息(线程安全)
+    public final List<LeftJoinInfo> leftJoinInfos = new CopyOnWriteArrayList<>();
+    // 记录 ORDER BY 信息(线程安全)
+    public final Map<Integer,String> orderByMap = new HashMap<>();
+    
+    // 各种SQL构建方法...
+}
+```
+
+`BaseProvider` 提供了以下主要方法:
+
+- `selectById`: 根据ID查询单条记录
+- `selectOne`: 查询单条记录
+- `selectSum`: 聚合查询(求和)
+- `save`: 插入记录
+- `update`: 更新记录
+- `pageWrapper`: 分页查询
+- `leftJoinWrapper`: 左连接查询
+- `mPage`: 分页查询(返回MPage对象)
+
+### IBaseProvider
+
+`IBaseProvider` 是一个接口,定义了基础的数据库操作方法,并通过 MyBatis 的 `@SelectProvider`、`@InsertProvider` 和 `@UpdateProvider` 注解将这些方法与 `BaseProvider` 的实现关联起来。
+
+```java
+public interface IBaseProvider<T extends BaseReq, R extends BaseDto> {
+    @SelectProvider(type = FinalProviderSql.class, method = "leftJoinWrapper")
+    List<Map<String, Object>> selectListMap(@Param("req") T req, @Param("clazz") Class<R> clazz);
+    
+    @SelectProvider(type = FinalProviderSql.class, method = "pageCountSql")
+    long pageCount(String sql);
+    
+    @SelectProvider(type = FinalProviderSql.class, method = "pageSql")
+    List<Map<String, Object>> page(String sql);
+    
+    @SelectProvider(type = FinalProviderSql.class, method = "selectOne")
+    Map<String, Object> selectOneMap(@Param("req") T req, @Param("clazz") Class<R> clazz);
+    
+    @SelectProvider(type = FinalProviderSql.class, method = "selectById")
+    Map<String, Object> selectByIdMap(@Param("id") Serializable id, @Param("clazz") Class<R> clazz);
+    
+    @SelectProvider(type = FinalProviderSql.class, method = "selectSum")
+    Map<String, Object> selectSumMap(@Param("req") T req, @Param("clazz") Class<R> clazz);
+    
+    @InsertProvider(type = FinalProviderSql.class, method = "save")
+    @Options(useGeneratedKeys = true, keyProperty = "id")
+    int save(T req);
+    
+    @UpdateProvider(type = FinalProviderSql.class, method = "update")
+    int update(T req);
+    
+    // 默认方法实现...
+}
+```
+
+`IBaseProvider` 还提供了一些默认方法,如 `selectByPrimaryKey`、`selectOne`、`sum`、`selectList` 和 `selectPage` 等,这些方法封装了对 MyBatis 返回结果的处理逻辑。
+
+### 工具类
+
+#### SqlUtil
+
+`SqlUtil` 是 SQL 生成工具类,负责将 `BaseProvider` 中收集的各种信息转换为最终的 SQL 字符串。它提供了以下主要方法:
+
+- `buildPage`: 构建分页查询
+- `buildOne`: 构建单条记录查询
+- `buildSum`: 构建聚合查询
+- `toSql`: 生成最终的 SQL 字符串
+
+#### SelectUtil
+
+`SelectUtil` 负责处理 SELECT 语句的构建,包括解析实体类上的注解、处理字段映射、构建查询条件等。
+
+#### UpdateUtil
+
+`UpdateUtil` 负责处理 UPDATE 语句的构建,包括解析实体类上的注解、处理字段映射等。
+
+#### InsertUtil
+
+`InsertUtil` 负责处理 INSERT 语句的构建,包括解析实体类上的注解、处理字段映射等。
+
+#### JoinUtil
+
+`JoinUtil` 负责处理 JOIN 语句的构建,包括解析 `@LeftJoin` 注解、构建连接条件等。
+
+#### WhereUtil
+
+`WhereUtil` 负责处理 WHERE 条件的构建,包括解析 `@Where` 注解、构建查询条件等。
+
+#### TableInfoUtil
+
+`TableInfoUtil` 负责处理表信息的初始化,包括解析 `@TableName` 注解、构建表信息对象等。
+
+### 领域模型
+
+#### TableInfo
+
+`TableInfo` 表示数据库表的信息,包括表名、别名、主键等。
+
+```java
+public class TableInfo {
+    // 实体类名
+    private String className;
+    // 实体类
+    private Class<?> clazz;
+    // 表名
+    private String tableName;
+    // 别名
+    private String alias;
+    // 索引
+    private Integer idx;
+    // 主键
+    private String primaryKey;
+}
+```
+
+#### WhereInfo
+
+`WhereInfo` 表示 WHERE 条件的信息,包括表信息、字段名、操作符、值等。
+
+```java
+public class WhereInfo {
+    // 表信息
+    private TableInfo tableInfo;
+    // 字段
+    private String column;
+    // or 条件
+    private List<String> orColumn;
+    // 值
+    private Object value;
+    // 结束值 (between 时使用)
+    private Object endValue;
+    // 操作符
+    private FieldOperator operator;
+    // 字段类型
+    private FieldType fieldType;
+}
+```
+
+#### LeftJoinInfo
+
+`LeftJoinInfo` 表示左连接的信息,包括表信息、关联表信息、关联条件等。
+
+```java
+public class LeftJoinInfo {
+    // 表信息
+    private TableInfo tableInfo;
+    // 关联表信息
+    private TableInfo leftTableInfo;
+    // 关联条件
+    private String fieldName;
+    // 关联字段
+    private String leftFieldName;
+    // 别名
+    private String alias;
+}
+```
+
+#### MPage
+
+`MPage` 表示分页查询的信息,包括当前页、每页大小、总记录数等。
+
+```java
+public class MPage<T> {
+    // 当前页
+    private long current;
+    // 每页大小
+    private long size;
+    // 总记录数
+    private long total;
+    // 记录列表
+    private List<T> records;
+    // 分页SQL
+    private String pageSql;
+    // 计数SQL
+    private String countSql;
+    // 排序信息
+    private List<OrderItem> orders;
+    // 是否优化计数SQL
+    private boolean optimizeCountSql;
+    // 是否查询总记录数
+    private boolean searchCount;
+    // 是否优化连接查询的计数SQL
+    private boolean optimizeJoinOfCountSql;
+}
+```
+
+## 注解
+
+### @Select
+
+`@Select` 注解用于指定查询字段,可以应用在实体类或 DTO 的字段上。
+
+```java
+public @interface Select {
+    // 表名
+    Class<?> table();
+    // 别名
+    String alias() default "";
+    // 字段名
+    String fieldName() default "";
+    // 聚合等条件取值
+    CaseWhen[] caseWhen() default {};
+    // 是否去重
+    boolean distinct() default false;
+}
+```
+
+### @Where
+
+`@Where` 注解用于指定查询条件,可以应用在实体类或 DTO 的字段上。
+
+```java
+public @interface Where {
+    // 表名
+    Class<?> table();
+    // 别名
+    String alias() default "";
+    // 字段名
+    String field() default "";
+    // 字段名数组(用于 OR 条件)
+    String[] orFields() default {};
+    // 字段类型
+    FieldType columnType() default FieldType.STRING;
+    // 操作符
+    FieldOperator operator() default FieldOperator.EQ;
+    // 是否为空
+    boolean isNull() default false;
+    // 是否结束(用于 BETWEEN 条件)
+    boolean isEnd() default false;
+}
+```
+
+### @LeftJoin
+
+`@LeftJoin` 注解用于指定左连接,可以应用在实体类或 DTO 的字段上。
+
+```java
+public @interface LeftJoin {
+    // 关联表
+    Class<?> table();
+    // 关联表(左表,主表)
+    Class<?> leftTable() default void.class;
+    // 关联字段
+    String fieldName() default "";
+    // 关联字段(左表关联字段)
+    String leftFieldName() default "";
+}
+```
+
+### @OrderBy
+
+`@OrderBy` 注解用于指定排序,可以应用在实体类或 DTO 的字段上。
+
+```java
+public @interface OrderBy {
+    // 排序方式
+    OrderBySortEnums sortType() default OrderBySortEnums.ASC;
+    // 顺序
+    int index() default 1;
+}
+```
+
+### @Count
+
+`@Count` 注解用于指定计数查询,可以应用在实体类或 DTO 的字段上。
+
+```java
+public @interface Count {
+    // 是否去重
+    boolean distinct() default false;
+    // 条件
+    CaseWhen[] caseWhen() default {};
+}
+```
+
+### @Sum
+
+`@Sum` 注解用于指定求和查询,可以应用在实体类或 DTO 的字段上。
+
+```java
+public @interface Sum {
+    // 条件
+    CaseWhen[] caseWhen() default {};
+}
+```
+
+## 使用示例
+
+### 1. 定义实体类和DTO
+
+```java
+// 实体类
+@Data
+@TableName("user")
+public class User {
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+    
+    @TableField("name")
+    private String name;
+    
+    @TableField("age")
+    private Integer age;
+    
+    @TableField("email")
+    private String email;
+    
+    @TableField("create_time")
+    private Date createTime;
+}
+
+// 请求DTO
+@Data
+public class UserReq extends BaseReq {
+    @Where(table = User.class, field = "name")
+    private String name;
+    
+    @Where(table = User.class, field = "age", operator = FieldOperator.GE)
+    private Integer minAge;
+    
+    @Where(table = User.class, field = "age", operator = FieldOperator.LE)
+    private Integer maxAge;
+    
+    @Where(table = User.class, field = "email", operator = FieldOperator.LIKE)
+    private String email;
+    
+    @LeftJoin(table = Department.class, leftTable = User.class, fieldName = "id", leftFieldName = "dept_id")
+    private String departmentJoin;
+}
+
+// 响应DTO
+@Data
+public class UserDto extends BaseDto {
+    private Long id;
+    private String name;
+    private Integer age;
+    private String email;
+    private Date createTime;
+    
+    @Select(table = Department.class, fieldName = "name")
+    private String departmentName;
+    
+    @Count(distinct = true)
+    @TableField("id")
+    private Long userCount;
+    
+    @Sum
+    @TableField("age")
+    private Long totalAge;
+}
+```
+
+### 2. 定义Mapper接口
+
+```java
+@Mapper
+public interface UserMapper extends IBaseMapper<UserDto> {
+    // 可以添加自定义方法
+}
+```
+
+### 3. 使用示例
+
+```java
+@Service
+public class UserService extends BaseServiceImpl<UserReq, User, UserDto> {
+    @Autowired
+    private UserMapper userMapper;
+    
+    // 查询单条记录
+    public UserDto getUser(Long id) {
+        return userMapper.selectByPrimaryKey(id, UserDto.class);
+    }
+    
+    // 条件查询
+    public List<UserDto> getUserList(UserReq req) {
+        return userMapper.selectList(req, UserDto.class);
+    }
+    
+    // 分页查询
+    public Page<UserDto> getUserPage(UserReq req) {
+        return userMapper.selectPage(req, UserDto.class);
+    }
+    
+    // 统计查询
+    public UserDto getUserStats(UserReq req) {
+        return userMapper.sum(req, UserDto.class);
+    }
+    
+    // 保存
+    public int saveUser(UserReq req) {
+        return userMapper.save(req);
+    }
+    
+    // 更新
+    public int updateUser(UserReq req) {
+        return userMapper.update(req);
+    }
+}
+```
+
+## 高级用法
+
+### 1. 复杂条件查询
+
+```java
+@Data
+public class ComplexUserReq extends BaseReq {
+    // 多字段OR条件
+    @Where(table = User.class, orFields = {"name", "email"}, operator = FieldOperator.LIKE)
+    private String keyword;
+    
+    // BETWEEN条件
+    @Where(table = User.class, field = "create_time", operator = FieldOperator.BETWEEN)
+    private Date startTime;
+    
+    @Where(table = User.class, field = "create_time", operator = FieldOperator.BETWEEN, isEnd = true)
+    private Date endTime;
+    
+    // IN条件
+    @Where(table = User.class, field = "status", operator = FieldOperator.IN)
+    private List<Integer> statusList;
+}
+```
+
+### 2. 多表关联查询
+
+```java
+@Data
+public class UserWithDeptReq extends BaseReq {
+    @Where(table = User.class, field = "name", operator = FieldOperator.LIKE)
+    private String userName;
+    
+    @LeftJoin(table = Department.class, leftTable = User.class, fieldName = "id", leftFieldName = "dept_id")
+    private String deptJoin;
+    
+    @Where(table = Department.class, field = "name", operator = FieldOperator.LIKE)
+    private String deptName;
+    
+    @LeftJoin(table = Company.class, leftTable = Department.class, fieldName = "id", leftFieldName = "company_id")
+    private String companyJoin;
+    
+    @Where(table = Company.class, field = "name", operator = FieldOperator.LIKE)
+    private String companyName;
+}
+
+@Data
+public class UserWithDeptDto extends BaseDto {
+    private Long id;
+    private String name;
+    private Integer age;
+    
+    @Select(table = Department.class, fieldName = "name")
+    private String departmentName;
+    
+    @Select(table = Company.class, fieldName = "name")
+    private String companyName;
+}
+```
+
+### 3. 聚合查询
+
+```java
+@Data
+public class UserStatsDto extends BaseDto {
+    @Count(distinct = true)
+    @TableField("id")
+    private Long userCount;
+    
+    @Sum
+    @TableField("age")
+    private Long totalAge;
+    
+    @Select(table = Department.class, fieldName = "name")
+    private String departmentName;
+    
+    // CASE WHEN 条件
+    @Sum(caseWhen = {
+        @CaseWhen(when = "age > 30", then = "1", elseThen = "0")
+    })
+    @TableField("id")
+    private Long seniorUserCount;
+}
+```
+
+## 注意事项
+
+1. 所有的查询都需要传入 `Class<R> clazz` 参数,用于指定返回的 DTO 类型。
+2. 使用 `@Where` 注解时,需要指定 `table` 属性,用于确定查询条件所属的表。
+3. 使用 `@LeftJoin` 注解时,需要指定 `table` 和 `leftTable` 属性,用于确定连接的两个表。
+4. 使用 `@Select` 注解时,需要指定 `table` 和 `fieldName` 属性,用于确定查询的字段。
+5. 使用 `@OrderBy` 注解时,可以指定 `sortType` 和 `index` 属性,用于确定排序方式和顺序。
+6. 使用 `@Count` 和 `@Sum` 注解时,需要与 `@TableField` 注解一起使用,用于确定聚合的字段。
+7. 使用 `@CaseWhen` 注解时,需要指定 `when` 和 `then` 属性,用于构建 CASE WHEN 语句。
+
+## 总结
+
+`com.poyee.base.mapper.provider` 包提供了一种基于注解的方式来构建 SQL 语句,使开发者能够以更加面向对象的方式编写数据库访问代码。通过使用这些注解,开发者可以轻松实现复杂的查询条件、连接查询、排序、分页等功能,而无需手动编写 SQL 语句。
+
+这种方式的优点是:
+
+1. 代码更加简洁、易读、易维护
+2. 减少了 SQL 注入的风险
+3. 支持复杂的查询条件和连接查询
+4. 与 MyBatis 和 MyBatis-Plus 无缝集成
+
+缺点是:
+
+1. 学习成本较高
+2. 对于非常复杂的查询,可能不如直接编写 SQL 语句灵活
+3. 性能可能略低于手写 SQL 语句
+
+总体来说,这个包是一个非常有用的工具,特别适合于需要频繁进行数据库操作的项目。
+
+
+## 注意事项
+
+1. **学习成本**:使用该包需要了解各种注解的用法和 SQL 构建的原理,学习成本较高。
+2. **性能考虑**:动态构建 SQL 可能会带来一定的性能开销,对于性能要求极高的场景,可能需要直接使用原生 SQL。
+3. **复杂查询**:对于非常复杂的查询,可能不如直接编写 SQL 语句灵活。
+4. **调试难度**:动态构建的 SQL 可能难以调试,建议在开发环境中开启 SQL 日志。
+5. **版本兼容性**:在升级 MyBatis 或数据库版本时,需要注意兼容性问题。
+
+## 性能优化建议
+
+1. **表信息缓存**:实现表结构信息的缓存,避免重复解析表结构。
+2. **SQL片段缓存**:对于频繁使用的SQL片段,可以实现缓存机制。
+3. **批量操作**:使用批量插入和更新操作,减少数据库交互次数。
+4. **索引优化**:确保在WHERE条件和JOIN条件中使用的字段上创建了适当的索引。
+5. **查询优化**:
+    - 只查询需要的列,避免使用`SELECT *`
+    - 使用分页查询时,将COUNT查询和数据查询分离
+    - 对于大数据量的表,考虑使用分区表或分表策略
+6. **连接查询优化**:
+    - 减少连接表的数量,必要时使用子查询或分批查询
+    - 确保连接条件字段上有索引
+7. **代码优化**:
+    - 减少反射操作,可以使用缓存存储字段和注解信息
+    - 优化字符串拼接操作,使用StringBuilder而不是String连接
+    - 使用线程安全的集合类,避免并发问题
+8. **SQL执行计划分析**:
+    - 定期分析生成的SQL执行计划,找出性能瓶颈
+    - 对于复杂查询,考虑使用数据库视图或存储过程
+9. **监控和日志**:
+    - 实现SQL执行时间的监控和日志记录
+    - 对于执行时间超过阈值的SQL,进行告警和优化
+10. **使用连接池**:
+    - 配置合适的数据库连接池参数
+    - 监控连接池的使用情况,避免连接泄漏

+ 47 - 0
py-base/src/main/java/com/poyee/annotation/ApiAuthenticationToken.java

@@ -0,0 +1,47 @@
+package com.poyee.annotation;
+
+import com.poyee.common.enums.CheckAuthTypeEnums;
+import com.poyee.common.enums.Roles;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * api token验证注解
+ * 含有此注解的接口可支持 外网,内网访问,根据不同的访问规则验证不同类型的token
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApiAuthenticationToken {
+
+    /**
+     * 是否需要验证
+     * @return
+     */
+    boolean required() default true;
+
+    /**
+     * 验证类型:authorization、private(内部访问时使用密钥对验证)
+     */
+    CheckAuthTypeEnums type() default CheckAuthTypeEnums.AUTHORIZATION;
+
+    /**
+     * token的key
+     * @return
+     */
+    String type_key() default "X-USER-BASE64";
+
+    /**
+     * @return
+     */
+    Roles[] roles() default {Roles.ADMIN};
+
+    /**
+     * 非验证来源
+     * @return
+     */
+    String pass_aud() default "partner";
+
+}

+ 14 - 0
py-base/src/main/java/com/poyee/annotation/CheckDataNull.java

@@ -0,0 +1,14 @@
+package com.poyee.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface CheckDataNull {
+}

+ 21 - 0
py-base/src/main/java/com/poyee/annotation/Desensit.java

@@ -0,0 +1,21 @@
+package com.poyee.annotation;
+
+import com.poyee.common.enums.DesensitType;
+
+import java.lang.annotation.*;
+
+/**
+ * 脱敏规则
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface Desensit {
+    //脱敏字段
+    String field() default "";
+
+    //脱敏类型
+    DesensitType type() default DesensitType.DEFAULT;
+
+
+}

+ 16 - 0
py-base/src/main/java/com/poyee/annotation/DesensitMethod.java

@@ -0,0 +1,16 @@
+package com.poyee.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 脱敏规则
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface DesensitMethod {
+
+    boolean desensit() default true;
+
+
+}

+ 13 - 0
py-base/src/main/java/com/poyee/annotation/DictTypeFormat.java

@@ -0,0 +1,13 @@
+package com.poyee.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 字典表转换注解
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DictTypeFormat {
+    String value();
+}

+ 14 - 0
py-base/src/main/java/com/poyee/annotation/ExcelI18nFromat.java

@@ -0,0 +1,14 @@
+package com.poyee.annotation;
+
+import java.lang.annotation.*;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+@Documented
+public @interface ExcelI18nFromat {
+    String value();
+
+    boolean exists() default true;
+
+    String fieldName() default "";
+}

+ 38 - 0
py-base/src/main/java/com/poyee/annotation/I18n.java

@@ -0,0 +1,38 @@
+package com.poyee.annotation;
+
+import com.poyee.common.enums.I18nFormat;
+import com.poyee.i18n.I18nSourcesEnum;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 国际化
+ */
+@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface I18n {
+
+    /**
+     * @return
+     */
+    boolean value() default true;
+    /**
+     * @return
+     */
+    String sheetName() default "";
+
+    /**
+     * @return
+     */
+    I18nFormat[] format() default {};
+
+    /**
+     * 国际化源
+     * @return
+     */
+    I18nSourcesEnum dataSource() default I18nSourcesEnum.NONE;
+
+}

+ 34 - 0
py-base/src/main/java/com/poyee/annotation/I18nToken.java

@@ -0,0 +1,34 @@
+package com.poyee.annotation;
+
+import com.poyee.common.enums.Roles;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 国际化注解
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface I18nToken {
+    /**
+     * @return
+     */
+    boolean local() default false;
+    /**
+     * @return
+     */
+    boolean required() default true;
+    /**
+     * @return
+     */
+    Roles[] roles() default {Roles.ADMIN};
+    /**
+     * 通过验证的来源
+     * @return
+     */
+    String pass_aub() default "";
+
+}

+ 19 - 0
py-base/src/main/java/com/poyee/annotation/JsonMapper.java

@@ -0,0 +1,19 @@
+package com.poyee.annotation;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.annotation.*;
+
+/**
+ * json映射
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface JsonMapper {
+
+    @NotNull
+    String name() default "";
+    //类型举例
+    String readConverterExp() default "" ;
+}

+ 37 - 0
py-base/src/main/java/com/poyee/annotation/Log.java

@@ -0,0 +1,37 @@
+package com.poyee.annotation;
+
+
+import com.poyee.common.enums.BusinessType;
+import com.poyee.common.enums.OperatorType;
+
+import java.lang.annotation.*;
+
+/**
+ * 自定义操作日志记录注解
+ *
+ * @author zheng
+ */
+@Target({ElementType.PARAMETER, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Log {
+    /**
+     * 模块
+     */
+    String title() default "";
+
+    /**
+     * 功能
+     */
+    BusinessType businessType() default BusinessType.OTHER;
+
+    /**
+     * 操作人类别
+     */
+    OperatorType operatorType() default OperatorType.MANAGE;
+
+    /**
+     * 是否保存请求的参数
+     */
+    boolean isSaveRequestData() default true;
+}

+ 51 - 0
py-base/src/main/java/com/poyee/annotation/MpjWapper.java

@@ -0,0 +1,51 @@
+package com.poyee.annotation;
+
+
+import java.lang.annotation.*;
+
+/**
+ * 类属性注解
+ * 描述:搜索时使用
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface MpjWapper {
+    //字段
+    String value() ;
+    //字段类型
+    Type columnType() default Type.STRING;
+    //操作符
+    Operator operator() default Operator.EQ;
+    //数据
+    String data() default "";
+
+    enum Type {
+        STRING,
+        NUMBER,
+        DATE,
+        BOOLEAN,
+        ENUM,
+    }
+
+    enum Operator{
+        EQ,
+        NE,
+        GT,
+        GE,
+        LT,
+        LE,
+        LIKE,
+        NOTLIKE,
+        IN,
+        NOTIN,
+        BETWEEN,
+        NOTBETWEEN,
+        ISNULL,
+        ISNOTNULL,
+        ISNOTEMPTY,
+        ISEMPTY,
+        ISNOT
+    }
+
+}

+ 13 - 0
py-base/src/main/java/com/poyee/annotation/PassToken.java

@@ -0,0 +1,13 @@
+package com.poyee.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+// 用来跳过验证的PassToken
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface PassToken {
+    boolean required() default true;
+}

+ 24 - 0
py-base/src/main/java/com/poyee/annotation/Property.java

@@ -0,0 +1,24 @@
+package com.poyee.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 字段属性
+ */
+@Documented
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Property {
+    /**
+     * class , field , map
+     *
+     * @return
+     */
+    String fieldType() default "field";
+
+    String defaultName() default "";
+
+    String defaultValue() default "";
+
+}
+

+ 13 - 0
py-base/src/main/java/com/poyee/annotation/ReadConverterExpFormat.java

@@ -0,0 +1,13 @@
+package com.poyee.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 列举数据转换注解
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface ReadConverterExpFormat {
+    String value();
+}

+ 60 - 0
py-base/src/main/java/com/poyee/annotation/RedisCache.java

@@ -0,0 +1,60 @@
+package com.poyee.annotation;
+
+import com.poyee.enums.CacheExpirePolicy;
+
+import java.lang.annotation.*;
+
+/**
+ * Redis 缓存注解,用于方法级别缓存
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RedisCache {
+
+    /**
+     * 缓存的 key,支持 SpEL 表达式,例如:a_b_#{merId}
+     */
+    String key();
+
+    /**
+     * 占位符数据源,例如:userContext、request、merchant、args 等
+     * 用于从指定源中提取 key 中的变量值(如 merId)
+     */
+    String keySource() default "userContext";
+
+    /**
+     * 缓存过期策略
+     * @see com.poyee.enums.CacheExpirePolicy
+     */
+    CacheExpirePolicy expirePolicy() default CacheExpirePolicy.TODAY;
+
+    /**
+     * 自定义过期时间(秒),当 expirePolicy = CUSTOM 时生效
+     */
+    long expireTime() default 3600;
+
+    /**
+     * 是否分组缓存,默认为 false
+     * @return
+     */
+    boolean group() default false;
+
+    /**
+     * 缓存分组名称,当 group = true 时生效
+     * @return
+     */
+    String groupName() default "default";
+
+    /**
+     * 是否记录缓存key
+     */
+    boolean logKey() default false;
+
+    /**
+     * 记录缓存key规则
+     */
+    String logKeyRule() default "key";
+
+
+}

+ 25 - 0
py-base/src/main/java/com/poyee/annotation/RedisLock.java

@@ -0,0 +1,25 @@
+package com.poyee.annotation;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * redis 分布式锁注解
+ *
+ * @author
+ */
+@Target({ ElementType.PARAMETER, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RedisLock {
+     /**锁的key,支持SpEL表达式*/
+     String lockKey();
+     /**时间单位*/
+     TimeUnit unit() default TimeUnit.SECONDS;
+     /**锁等待时间*/
+     long waitTime() ;
+     /**锁持续时间,-1:自动续锁*/
+     long leaseTime();
+     /**是否使用事务*/
+     boolean isUseTran() default false;
+}

+ 36 - 0
py-base/src/main/java/com/poyee/annotation/UserLoginToken.java

@@ -0,0 +1,36 @@
+package com.poyee.annotation;
+
+
+import com.poyee.common.enums.Roles;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+// 需要登录才能进行操作的注解UserLoginToken
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UserLoginToken {
+    /**
+     * 是否需要登录
+     * @return
+     */
+    boolean required() default true;
+    /**
+     * 是否需要实名
+     * @return
+     */
+    boolean faceVerify() default false;
+    /**
+     * @return
+     */
+    Roles[] roles() default {Roles.ADMIN};
+
+    /**
+     * 非验证来源
+     * @return
+     */
+    String pass_aud() default "partner";
+
+}

+ 23 - 0
py-base/src/main/java/com/poyee/annotation/db/CaseWhen.java

@@ -0,0 +1,23 @@
+package com.poyee.annotation.db;
+
+import com.poyee.enums.CaseWhenValueTypeEnums;
+
+import java.lang.annotation.*;
+
+/**
+ * 聚合等条件取值
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface CaseWhen {
+    //  条件
+    String when() default "";
+    //  值
+    String then() default "";
+    //  是否是else
+    boolean isElse() default false;
+    //  值类型
+    CaseWhenValueTypeEnums valueType() default CaseWhenValueTypeEnums.CONSTANT;
+
+}

+ 20 - 0
py-base/src/main/java/com/poyee/annotation/db/Count.java

@@ -0,0 +1,20 @@
+package com.poyee.annotation.db;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+
+import java.lang.annotation.*;
+
+/**
+ * 统计字段  计数
+ * 和 {@link TableField TableField}一起使用
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Count {
+
+    boolean distinct() default false;
+
+    CaseWhen[] caseWhen() default {};
+
+}

+ 13 - 0
py-base/src/main/java/com/poyee/annotation/db/Distinct.java

@@ -0,0 +1,13 @@
+package com.poyee.annotation.db;
+
+import java.lang.annotation.*;
+
+/**
+ * Created by poyi
+ * 去重
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Distinct {
+}

+ 18 - 0
py-base/src/main/java/com/poyee/annotation/db/Join.java

@@ -0,0 +1,18 @@
+package com.poyee.annotation.db;
+
+import java.lang.annotation.*;
+
+/**
+ * 外连接
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Join {
+    // 关联表
+    String table();
+    // 关联条件
+    String on();
+    // 别名
+    String alias();
+}

+ 21 - 0
py-base/src/main/java/com/poyee/annotation/db/LeftJoin.java

@@ -0,0 +1,21 @@
+package com.poyee.annotation.db;
+
+import java.lang.annotation.*;
+
+/**
+ * 左连接
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface LeftJoin {
+    // 关联表
+    Class<?> table();
+    // 关联表(左表。主表)
+    Class<?> leftTable() default void.class;
+    // 关联字段
+    String fieldName() default "";
+    /* 关联字段(左表关联字段)*/
+    String leftFieldName() default "";
+
+}

+ 15 - 0
py-base/src/main/java/com/poyee/annotation/db/OrderBy.java

@@ -0,0 +1,15 @@
+package com.poyee.annotation.db;
+
+import com.poyee.enums.OrderBySortEnums;
+
+import java.lang.annotation.*;
+
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface OrderBy {
+    // 排序方式
+    OrderBySortEnums sortType() default OrderBySortEnums.ASC;
+    // 顺序
+    int index() default 1;
+}

+ 19 - 0
py-base/src/main/java/com/poyee/annotation/db/RightJoin.java

@@ -0,0 +1,19 @@
+package com.poyee.annotation.db;
+
+import java.lang.annotation.*;
+
+/**
+ * 右连接
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RightJoin {
+    // 表名
+    String table();
+    // 关联条件
+    String on();
+    // 别名
+    String alias();
+
+}

+ 23 - 0
py-base/src/main/java/com/poyee/annotation/db/Select.java

@@ -0,0 +1,23 @@
+package com.poyee.annotation.db;
+
+import java.lang.annotation.*;
+
+/**
+ * 查询
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Select {
+    // 表名
+    Class<?> table();
+    // 别名
+    String alias() default "";
+    // 字段名
+    String fieldName() default "";
+    // 聚合等条件取值
+    CaseWhen[] caseWhen() default {};
+
+    boolean distinct() default false;
+
+}

+ 17 - 0
py-base/src/main/java/com/poyee/annotation/db/Sum.java

@@ -0,0 +1,17 @@
+package com.poyee.annotation.db;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+
+import java.lang.annotation.*;
+
+/**
+ * 统计字段  记和
+ * 和 {@link TableField TableField}一起使用
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Sum {
+
+    CaseWhen[] caseWhen() default {};
+}

+ 88 - 0
py-base/src/main/java/com/poyee/annotation/db/Where.java

@@ -0,0 +1,88 @@
+package com.poyee.annotation.db;
+
+import com.poyee.enums.FieldOperator;
+import com.poyee.enums.FieldType;
+
+import java.lang.annotation.*;
+
+/**
+ * 条件
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Where {
+    // 表名、别名 2选一
+    Class<?> table() ;
+    // 别名 、表名 2选一
+    String alias() default "";
+    // 字段名 或字段名 2选一
+    String field() default "";
+    // 字段名 或字段名 2选一
+    String[] orFields() default {};
+    //字段类型
+    FieldType columnType() default FieldType.STRING;
+    //操作符
+    FieldOperator operator() default FieldOperator.EQ;
+    // 是否为空
+    boolean isNull() default false;
+    // 是否为结束条件
+    boolean isEnd() default false;
+    // 自定义查询条件数组,用于组合多个查询条件
+    CustomConditions[] customConditions() default {};
+    // 是否使用自定义的 or 条件
+    // 如果为 true,则 customConditions 中的条件将使用 OR 连接,而不是默认的 AND 连接
+    // 注意:如果 isCustomOr 为 true,则 customConditions 中的条件必须是独立的查询条件,不能依赖于其他条件
+    // 例如:
+    // @Where(table = User.class, isCustomOr = true,
+    //         customConditions = {
+    //             @CustomCondition(field = "status", operator = FieldOperator.EQ, defaultValue = "active"),
+    //             @CustomCondition(field = "role", operator = FieldOperator.IN, orFields = {"admin", "user"})
+    //         },{
+    //             @CustomCondition(field = "status", operator = FieldOperator.EQ, defaultValue = "logOff")
+    //         })
+    // 上述条件将生成 SQL 语句:WHERE (status = 'active' AND role IN ('admin', 'user')) OR (status = 'logOff')
+    // 如果为 false,则 customConditions 中的条件将使用 AND 连接
+    // 例如:
+    // @Where(table = User.class, isCustomOr = false,
+    //         customConditions = {
+    //             @CustomCondition(field = "status", operator = FieldOperator.EQ, defaultValue = "active"),
+    //             @CustomCondition(field = "role", operator = FieldOperator.IN, orFields = {"admin", "user"})
+    //         },{
+    //             @CustomCondition(field = "status", operator = FieldOperator.EQ, defaultValue = "logOff")
+    //         })
+    // 上述条件将生成 SQL 语句:WHERE status = 'active' AND role IN ('admin', 'user') AND status = 'logOff'
+    // 注意:如果 isCustomOr 为 false,则 customConditions 中的条件必须是可以同时成立的条件,否则将导致查询结果为空
+    // 注意:customConditions 中的条件每组条件之间是 AND 关系,而组与组之间 根据 isCustomOr 的值决定是 AND 还是 OR 关系
+    // 默认值为 false,表示使用 AND 连接
+    // 如果需要使用 OR 连接,则需要显式设置 isCustomOr 为 true
+    boolean isCustomOr() default false;
+
+    /**
+     * 用于替代 Where[] 的嵌套注解,避免循环引用
+     */
+    @Target({})
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface CustomConditions {
+        CustomCondition[] value() default {};
+    }
+
+    @Target({})
+    @Retention(RetentionPolicy.RUNTIME)
+    @interface CustomCondition {
+        // 字段名 或字段名 2选一
+        String field() default "";
+        // 字段名 或字段名 2选一
+        String[] orFields() default {};
+        //字段类型
+        FieldType columnType() default FieldType.STRING;
+        //操作符
+        FieldOperator operator() default FieldOperator.EQ;
+        // 是否为空
+        boolean isNull() default false;
+        // 是否为结束条件
+        boolean isEnd() default false;
+        //默认值
+        String defaultValue() default "";
+    }
+}

+ 30 - 0
py-base/src/main/java/com/poyee/annotation/ds/DataSource.java

@@ -0,0 +1,30 @@
+package com.poyee.annotation.ds;
+
+
+import com.poyee.datasource.enums.DataSourceType;
+
+import java.lang.annotation.*;
+
+/**
+ * 自定义多数据源切换注解
+ * <p>
+ * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
+ *
+ * @author zheng
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface DataSource {
+    /**
+     * 切换数据源名称
+     */
+    DataSourceType value() default DataSourceType.MASTER;
+    // 事务管理器
+    String txManager() default "dynamicTransactionManager"; // 事务管理器
+    // 读写分离
+    boolean readOnly() default false;
+    // 抛出指定异常后回滚
+    Class<? extends Throwable>[] rollbackFor() default {};
+}

+ 28 - 0
py-base/src/main/java/com/poyee/annotation/ds/Transactional.java

@@ -0,0 +1,28 @@
+package com.poyee.annotation.ds;
+
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.annotation.Isolation;
+import org.springframework.transaction.annotation.Propagation;
+
+import java.lang.annotation.*;
+
+/**
+ *  数据源切换注解
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Transactional {
+
+    Propagation propagation() default Propagation.REQUIRED;
+
+    Isolation isolation() default Isolation.DEFAULT;
+
+    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
+
+    boolean readOnly() default false;
+
+    Class<? extends Throwable>[] rollbackFor() default {};
+
+    Class<? extends Throwable>[] noRollbackFor() default {};
+}

+ 35 - 0
py-base/src/main/java/com/poyee/annotation/i18nSource.java

@@ -0,0 +1,35 @@
+package com.poyee.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *  国际化 数据来源
+ */
+@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface i18nSource {
+
+    /**
+     * 多语言数据来源 表
+     * @return
+     */
+    Class<?> sourceTable() default void.class;
+
+    /**
+     * 多语言数据来源 字段
+     * @return
+     */
+    String sourceField() default "";
+
+    /**
+     * key:value
+     * 多语言数据来源 字段值
+     * @return
+     */
+    String key() default "";
+
+
+}

+ 154 - 0
py-base/src/main/java/com/poyee/aspectj/ApiAuthenticationTokenAspect.java

@@ -0,0 +1,154 @@
+package com.poyee.aspectj;
+
+import com.alibaba.fastjson.JSONException;
+import com.alibaba.fastjson.JSONObject;
+import com.poyee.annotation.ApiAuthenticationToken;
+import  com.poyee.common.auth.UserRoleUtil;
+import com.poyee.common.exception.AuthException;
+import com.poyee.common.exception.BusinessException;
+import com.poyee.common.exception.ServiceException;
+import com.poyee.framework.jwt.JwtUtils;
+import com.poyee.i18n.I18nUtils;
+import com.poyee.util.ServletUtils;
+import com.poyee.util.StringUtils;
+import com.poyee.util.security.Base64Util;
+import com.poyee.util.security.RsaUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+import static com.poyee.i18n.I18nMessageEnums.AUTH_USER_ERROR;
+
+/**
+ * @Description: 登录拦截
+ * 可支持 外网,内网访问,根据不同的访问规则验证不同类型的token
+ * Created by poyee on 2025/10/31.
+ */
+@Slf4j
+@Aspect
+@Component
+public class ApiAuthenticationTokenAspect {
+
+    @Value("${whitelist.id:0}")
+    private String whitelistId;
+    // 配置织入点
+    @Pointcut("@annotation(com.poyee.annotation.ApiAuthenticationToken)")
+    public void logPointCut() {
+    }
+    /**
+     * @param
+     */
+    @Before("logPointCut()")
+    public void doBefore(JoinPoint joinPoint) {
+        // 接收到请求,记录请求内容
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+        // 检查用户权限和参数
+        checkAuthTokenByType(method);
+    }
+
+    /**
+     * 根据注解类型判断是否需要验证token
+     * @param method
+     */
+    private void checkAuthTokenByType(Method method) {
+        // 判断方法是否需要登录
+        if (method.isAnnotationPresent(ApiAuthenticationToken.class)) {
+            //如果存在注解 则获取注解配置进行对不同类型的token进行验证
+            ApiAuthenticationToken apiAuthenticationToken = method.getAnnotation(ApiAuthenticationToken.class);
+            log.info("apiAuthenticationToken:{}",apiAuthenticationToken);
+            if (apiAuthenticationToken.required()) {
+                //判断类型
+                switch (apiAuthenticationToken.type()) {
+                    case AUTHORIZATION:
+                        //验证 token
+                        checkAuthorization(apiAuthenticationToken);
+                        break;
+                    case PRIVATE:
+                        //验证token
+                        checkPrivate(apiAuthenticationToken);
+                        break;
+                    default:
+                        //无对应验证类型则返回无权限
+                        throw new AuthException("无访问权限");
+                }
+            }else{
+                log.info("不需要验证");
+            }
+        }else{
+            log.info("不需要验证");
+        }
+    }
+
+    /**
+     * @param apiAuthenticationToken
+     */
+    private void checkAuthorization(ApiAuthenticationToken apiAuthenticationToken) {
+        String authKey = apiAuthenticationToken.type_key();
+        HttpServletRequest request = ServletUtils.getRequest();
+        String userInfoStr = request.getHeader(authKey);
+        if(StringUtils.isBlank(userInfoStr)){
+            throw new BusinessException(402, I18nUtils.get(AUTH_USER_ERROR, ",请重新登录[402]"));
+        }
+        //保存用户信息
+        com.poyee.util.ServletUtils.getSession().setAttribute("x-user-base64", userInfoStr);
+        String user = "";
+        try {
+            user = new String(Base64Util.decode(userInfoStr), StandardCharsets.UTF_8);
+            JSONObject.parseObject(user);
+        } catch (JSONException jsonExe) {
+            log.error(" 转换json 异常 {} ", userInfoStr);
+            throw new BusinessException(402, I18nUtils.get(AUTH_USER_ERROR, ",请重新登录[402]"));
+        } catch (IllegalArgumentException ie) {
+            log.error(" token bease64解析 异常 {} ", userInfoStr);
+            throw new BusinessException(402, I18nUtils.get(AUTH_USER_ERROR, ",请重新登录[402]"));
+        }
+        if (StringUtils.isBlank(user)) {
+            throw new BusinessException(402, I18nUtils.get(AUTH_USER_ERROR, ",请重新登录[402]"));
+        }
+        request.getSession().setAttribute("userInfo", user);
+        //权限判断
+        if (!UserRoleUtil.builder(whitelistId).checkRole(apiAuthenticationToken.pass_aud(), Arrays.asList(apiAuthenticationToken.roles()))) {
+            throw new BusinessException(403, I18nUtils.get(AUTH_USER_ERROR, "[403]"));
+        }
+    }
+
+    /**
+     * @param apiAuthenticationToken
+     */
+    private void checkPrivate(ApiAuthenticationToken apiAuthenticationToken) {
+        //验证token
+        try {
+            String userInfoStr = apiAuthenticationToken.type_key();
+            String token = ServletUtils.getRequest().getHeader(userInfoStr);
+            //保存用户信息
+            ServletUtils.getSession().setAttribute("x-user-base64", userInfoStr);
+            String user = RsaUtil.decrypt(RsaUtil.getPrivateKeyFromResource(RsaUtil.private_path), token);
+            if(StringUtils.isBlank(user)){
+                throw new BusinessException(402, I18nUtils.get(AUTH_USER_ERROR, ",请重新登录[402]"));
+            }
+            ServletUtils.getRequest().getSession().setAttribute("userInfo", user);
+            //权限判断
+            if (!UserRoleUtil.builder(whitelistId).checkRole(apiAuthenticationToken.pass_aud(), Arrays.asList(apiAuthenticationToken.roles()))) {
+                throw new BusinessException(403, I18nUtils.get(AUTH_USER_ERROR, "[403]"));
+            }
+        }catch (Exception e) {
+            throw new BusinessException(402, I18nUtils.get(AUTH_USER_ERROR, ",请重新登录[402]"));
+        }
+//        throw new ServiceException("暂不支持");
+    }
+
+
+}

+ 111 - 0
py-base/src/main/java/com/poyee/aspectj/DataSourceAspect.java

@@ -0,0 +1,111 @@
+package com.poyee.aspectj;
+
+import com.poyee.annotation.ds.DataSource;
+import com.poyee.datasource.DynamicDataSource;
+import com.poyee.datasource.holder.DataSourceContextHolder;
+import com.poyee.util.StringUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import java.util.Objects;
+
+/**
+ * 多数据源切换切面类
+ * <p>
+ * 该类通过 AOP 实现方法或类级别的数据源动态切换。
+ * 使用 @DataSource 注解指定数据源名称,支持在方法或类级别使用。
+ * <p>
+ * 示例:
+ * <pre>
+ * @DataSource(DataSourceEnum.MASTER)
+ * public void queryMasterData() {
+ *     // 数据库操作将使用 master 数据源
+ * }
+ * </pre>
+ * <pre>
+ * @DataSource(DataSourceEnum.SLAVE)
+ * @Service
+ * public class SlaveService {
+ *     // 该类中所有方法默认使用 slave 数据源
+ * }
+ * </pre>
+ */
+@Slf4j
+@Aspect
+@Order(0)
+@Component
+public class DataSourceAspect {
+
+    protected Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Autowired
+    private DynamicDataSource dynamicDataSource;
+
+    /**
+     * 定义切点:匹配带有 @DataSource 注解的方法 或 类
+     * <p>
+     * - `@annotation(com.poyee.annotation.DataSource)`:方法上带有 @DataSource 注解
+     * - `@within(com.poyee.annotation.DataSource)`:类上带有 @DataSource 注解
+     */
+    @Pointcut("@annotation(com.poyee.annotation.ds.DataSource)" + "|| @within(com.poyee.annotation.ds.DataSource)")
+    public void dsPointCut() {
+        // 切点方法,用于绑定切点表达式
+    }
+
+    /**
+     * 环绕通知:在目标方法执行前后切换数据源
+     * <p>
+     * 1. 获取目标方法或类上的 @DataSource 注解
+     * 2. 根据注解值设置当前线程的数据源
+     * 3. 执行目标方法
+     * 4. 清理数据源上下文,防止线程复用导致数据源混乱
+     *
+     * @param point 切面连接点(包含目标方法信息)
+     * @return 方法执行结果
+     * @throws Throwable 抛出异常
+     */
+    @Around("dsPointCut()")
+    public Object around(ProceedingJoinPoint point) throws Throwable {
+        DataSource dataSource = getDataSource(point);
+
+        if (StringUtils.isNotNull(dataSource)) {
+            String name = dataSource.value().name();
+            log.info("Switching to data source: {}", name);
+
+            dynamicDataSource.initDataSource(name);
+            DataSourceContextHolder.setDataSource(name);
+        }
+
+        try {
+            return point.proceed();
+        } finally {
+            DataSourceContextHolder.clearDataSource();
+        }
+    }
+
+    /**
+     * 获取目标方法或类上的 @DataSource 注解
+     * <p>
+     * 先从方法上查找注解,如果不存在则从类上查找
+     *
+     * @param point 切面连接点
+     * @return DataSource 注解对象(如果存在),否则为 null
+     */
+    public DataSource getDataSource(ProceedingJoinPoint point) {
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
+        if (Objects.nonNull(dataSource)) {
+            return dataSource;
+        }
+        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
+    }
+}

+ 126 - 0
py-base/src/main/java/com/poyee/aspectj/DesensitAspect.java

@@ -0,0 +1,126 @@
+package com.poyee.aspectj;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.poyee.annotation.Desensit;
+import com.poyee.annotation.DesensitMethod;
+import com.poyee.base.dto.Result;
+import com.poyee.base.entity.BaseEntity;
+import com.poyee.common.enums.DesensitType;
+import com.poyee.common.exception.BaseException;
+import com.poyee.util.StringUtils;
+import com.poyee.util.security.Md5Utils;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ *
+ */
+@Slf4j
+@Aspect
+@Component
+public class DesensitAspect {
+
+    private static Object setDesensitData(Object obj) {
+        JSONObject json = new JSONObject();
+        try {
+            if (null == obj) {
+                return null;
+            }
+            if (obj.getClass().getName().indexOf("ArrayList") > 0) {
+                List<Object> objlist = (List<Object>) obj;
+                JSONArray jsonArray = new JSONArray();
+                if (!objlist.isEmpty()) {
+                    objlist.forEach(item -> {
+                        jsonArray.add(setDesensitData(item));
+                    });
+                    return jsonArray;
+                }
+            } else {
+                json = (JSONObject) JSONObject.toJSON(obj);
+                JSONObject finalJson = json;
+                Arrays.stream(obj.getClass().getDeclaredFields()).forEach(field -> {
+                    field.setAccessible(true);
+                    Desensit annotation = field.getAnnotation(Desensit.class);
+                    if (null != annotation) {
+                        DesensitType type = annotation.type();
+                        if (type.equals(DesensitType.NULL)) {
+                            finalJson.remove(field.getName());
+                        } else if (type.equals(DesensitType.DEFAULT)) {
+                            //脱敏操作
+                            try {
+                                String value = StringUtils.desenstiseStr((String) field.get(obj), 4);
+                                finalJson.put(field.getName(), value);
+                            } catch (IllegalAccessException e) {
+                                throw new RuntimeException(e);
+                            }
+                        } else if (type.equals(DesensitType.MD5)) {
+                            //MD5加密
+                            try {
+                                finalJson.put(field.getName(), Md5Utils.hash((String) field.get(obj)));
+                            } catch (IllegalAccessException e) {
+                                throw new RuntimeException(e);
+                            }
+                        }
+                    }
+                });
+                BaseEntity.types.forEach(s -> {
+                    finalJson.remove(s);
+                });
+                json = finalJson;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new BaseException("数据转换异常[1001]!");
+        }
+        return json;
+    }
+
+    // 配置织入点
+    @Pointcut("@annotation(com.poyee.annotation.DesensitMethod)")
+    public void logPointCut() {
+    }
+
+    /**
+     * @param
+     */
+//    @AfterReturning("execution(public * com.poyee.*.service.impl.*(..))")
+    @AfterReturning(pointcut = "logPointCut()", returning = "result")
+    public Object doAfter(JoinPoint joinPoint, Result result) {
+        try {
+            DesensitMethod desensitMethod = getAnnotationLog(joinPoint);
+            if (desensitMethod.desensit()) {
+                Object obj = result.getData();
+                result.setData(setDesensitData(obj));
+            }
+            log.info(" 返回参数:{} ", result);
+            return result;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private DesensitMethod getAnnotationLog(JoinPoint joinPoint) throws Exception {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null) {
+            return method.getAnnotation(DesensitMethod.class);
+        }
+        return null;
+    }
+}

+ 45 - 0
py-base/src/main/java/com/poyee/aspectj/FeignRequestInterceptor.java

@@ -0,0 +1,45 @@
+package com.poyee.aspectj;
+
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Enumeration;
+
+/**
+ * Feign请求拦截器
+ * 用于传递请求头信息
+ */
+@Slf4j
+@Component
+public class FeignRequestInterceptor implements RequestInterceptor {
+
+    @Override
+    public void apply(RequestTemplate template) {
+        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attributes != null) {
+            HttpServletRequest request = attributes.getRequest();
+            Enumeration<String> headerNames = request.getHeaderNames();
+            if (headerNames != null) {
+                while (headerNames.hasMoreElements()) {
+                    String name = headerNames.nextElement();
+                    String values = request.getHeader(name);
+                    // 跳过content-length等系统头
+                    if (!"content-length".equalsIgnoreCase(name) && 
+                        !"host".equalsIgnoreCase(name) &&
+                        !name.startsWith("x-application")) {
+                        template.header(name, values);
+                    }
+                }
+            }
+            
+            // 添加自定义请求头
+            template.header("X-Service-Call", "feign");
+            template.header("X-Request-Time", String.valueOf(System.currentTimeMillis()));
+        }
+    }
+}

+ 134 - 0
py-base/src/main/java/com/poyee/aspectj/I18nTokenAspect.java

@@ -0,0 +1,134 @@
+package com.poyee.aspectj;
+
+import com.alibaba.fastjson.JSONException;
+import com.alibaba.fastjson.JSONObject;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.poyee.annotation.I18nToken;
+import com.poyee.base.dto.UserInfo;
+import com.poyee.common.exception.BusinessException;
+import com.poyee.framework.jwt.JwtUtils;
+import com.poyee.i18n.I18nUtils;
+import com.poyee.util.ServletUtils;
+import com.poyee.util.security.Base64Util;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.Enumeration;
+import java.util.Objects;
+
+import static com.poyee.i18n.I18nMessageEnums.AUTH_USER_ERROR;
+
+@Slf4j
+@Aspect
+@Component
+public class I18nTokenAspect {
+
+    @Value("${user.info-url:https://coresvc-dev.hobbystocks.cn/user/}")
+    private String userInfoUrl;
+    
+    @Value("${spring.profiles.active:dev}")
+    private String activeProfile;
+    
+    private final ObjectMapper objectMapper = new ObjectMapper();
+    
+    // 配置织入点
+    @Pointcut("@annotation(com.poyee.annotation.I18nToken)")
+    public void logPointCut() {
+    }
+    
+    /**
+     * 前置通知:处理I18nToken注解的权限检查和参数验证
+     */
+    @Before("logPointCut()")
+    public void doBefore(JoinPoint joinPoint) {
+        HttpServletRequest request = ServletUtils.getRequest();
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+        
+        // 头部信息打印(仅在开发环境)
+        if ("dev".equals(activeProfile) || "local".equals(activeProfile)) {
+            printHeaders(request);
+        }
+
+        // 检查用户权限和参数
+        validateUserPermissionAndParams(request, method );
+    }
+    
+    /**
+     * 打印请求头信息
+     */
+    private void printHeaders(HttpServletRequest request) {
+        try {
+            log.info(" ---- 头部信息打印开始 ---- ");
+            Enumeration<String> headerNames = request.getHeaderNames();
+            while (headerNames.hasMoreElements()) {
+                String name = headerNames.nextElement();
+                log.info(" -- 头部信息-- {} 值:{} ", name, request.getHeader(name));
+            }
+            log.info(" ---- 头部信息打印结束 ---- ");
+        } catch (Exception e) {
+            log.warn("打印头部信息失败: {}", e.getMessage());
+        }
+    }
+    
+
+    /**
+     * 验证用户权限和参数
+     */
+    private void validateUserPermissionAndParams(HttpServletRequest request, Method method) {
+        //获取I18nToken注解
+        I18nToken i18nToken = method.getAnnotation(I18nToken.class);
+        if (Objects.nonNull(i18nToken) && i18nToken.required()) {
+            //验证用户信息
+            String userInfoStr = request.getHeader("X-USER-BASE64");
+            userInfoStr = StringUtils.isNotEmpty(userInfoStr) ? userInfoStr : request.getHeader("x-user-base64");
+            boolean isAuthorization = false;
+            String authorization = "";
+            if(StringUtils.isEmpty(userInfoStr)){
+                log.error(" 未获取到用户信息  X-USER-BASE64  ");
+                authorization = request.getHeader("Authorization");
+                if (StringUtils.isNotBlank(authorization) && authorization.startsWith("Bearer ")) {
+                    isAuthorization = true;
+                    authorization = authorization.replaceAll("Bearer ", "");
+                }
+                //未获取用户信息
+//                throw new AuthException(I18nUtils.get(RELOGIN));
+            }
+            //保存用户信息
+            ServletUtils.getSession().setAttribute("x-user-base64", userInfoStr);
+            String user = "";
+            try {
+                if(isAuthorization && StringUtils.isNotBlank(authorization)){
+                    user = JSONObject.toJSONString(JwtUtils.getTokenUserInfo(authorization));
+                }else {
+                    user = new String(Base64Util.decode(userInfoStr), StandardCharsets.UTF_8);
+                }
+                UserInfo userInfo = JSONObject.parseObject(user, UserInfo.class);
+                if(Objects.nonNull(userInfo) && Objects.nonNull(userInfo.getMerchantId())){
+                    userInfo.setMerchant(true);
+                }
+                //保存用户信息到session
+                ServletUtils.setUserInfo(userInfo);
+            }catch (JSONException jsonExe) {
+                log.error(" 转换json 异常 {} ", userInfoStr);
+                throw new BusinessException(402, I18nUtils.get(AUTH_USER_ERROR, ",请重新登录[402]"));
+            }
+            if (StringUtils.isEmpty(user)) {
+                throw new BusinessException(402, I18nUtils.get(AUTH_USER_ERROR, ",请重新登录[402]"));
+            }
+        }
+
+    }
+}

+ 207 - 0
py-base/src/main/java/com/poyee/aspectj/LogAspect.java

@@ -0,0 +1,207 @@
+package com.poyee.aspectj;
+
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.support.spring.PropertyPreFilters;
+import com.poyee.annotation.Log;
+import com.poyee.common.enums.BusinessStatus;
+import com.poyee.framework.domain.SysOperLog;
+import com.poyee.framework.service.LocationService;
+import com.poyee.util.ObjectUtil;
+import com.poyee.util.ServletUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.*;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+
+/**
+ * 操作日志记录处理
+ *
+ * @author zheng
+ */
+@Aspect
+@Component
+public class LogAspect {
+
+    @Autowired
+    private LocationService locationService;
+    // 存储请求开始时间
+    private static final ThreadLocal<Long> startTimeThreadLocal = new ThreadLocal<>();
+
+    /**
+     * 排除敏感属性字段
+     */
+    public static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"};
+    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
+
+    // 配置织入点
+    @Pointcut("@annotation(com.poyee.annotation.Log)")
+    public void logPointCut() {
+    }
+    @Before("logPointCut()")
+    public void doBefore() {
+        startTimeThreadLocal.set(System.currentTimeMillis());
+    }
+
+    /**
+     * 处理完请求后执行
+     *
+     * @param joinPoint 切点
+     */
+    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
+    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
+        handleLog(joinPoint, null, jsonResult);
+    }
+
+    /**
+     * 拦截异常操作
+     *
+     * @param joinPoint 切点
+     * @param e         异常
+     */
+    @AfterThrowing(value = "logPointCut()", throwing = "e")
+    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
+        handleLog(joinPoint, e, null);
+    }
+
+    /**
+     * 日志记录
+     * @param joinPoint
+     * @param e
+     * @param jsonResult
+     */
+    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
+        try {
+            // 获得注解
+            Log controllerLog = getAnnotationLog(joinPoint);
+            if (controllerLog == null) {
+                return;
+            }
+            // *========数据库日志=========*//
+            SysOperLog operLog = new SysOperLog();
+            operLog.setLanguage(LocaleContextHolder.getLocale().getLanguage());
+            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
+            // 请求的地址
+            String ip = ServletUtils.getIpAddress();
+            operLog.setOperIp(ip);
+            // 返回参数
+            operLog.setJsonResult(StringUtils.substring(JSONObject.toJSONString(jsonResult), 0, 2000));
+            operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
+            if (e != null) {
+                operLog.setStatus(BusinessStatus.FAIL.ordinal());
+                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
+            }
+            // 设置方法名称
+            String className = joinPoint.getTarget().getClass().getName();
+            String methodName = joinPoint.getSignature().getName();
+            operLog.setMethod(className + "." + methodName + "()");
+            // 设置请求方式
+            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
+            // 处理设置注解上的参数
+            getControllerMethodDescription(controllerLog, operLog);
+            log.info("<<<< 接口请求 参数 >>>>>>> {}", operLog);
+            // 计算耗时
+            long duration = System.currentTimeMillis() - startTimeThreadLocal.get();
+            operLog.setDuration(duration);
+            // 打印日志
+            log.info("接口请求: [{}] 方法: [{}] 耗时: [{}ms] IP: [{}] 地点: [{}] 参数: [{}]",
+                     operLog.getOperUrl(),
+                     operLog.getMethod(),
+                     operLog.getDuration(),
+                     operLog.getOperIp(),
+                     operLog.getOperLocation(),
+                     operLog.getOperParam()
+            );
+            //异步执行
+            log.info("<<<< 异步执行获取ip开始 >>>>>>>");
+            long getIpStartTimestamp = System.currentTimeMillis();
+            asyncRecordLocation(operLog);
+            log.info("<<<< 异步执行获取ip耗时: [{}]ms >>>>>>>", System.currentTimeMillis() - getIpStartTimestamp);
+        } catch (Exception exp) {
+            // 记录本地异常日志
+            log.error("==前置通知异常==");
+            log.error("异常信息:{}", exp.getMessage());
+            exp.printStackTrace();
+        }
+    }
+
+    /**
+     * 获取注解中对方法的描述信息 用于Controller层注解
+     *
+     * @param log     日志
+     * @param operLog 操作日志
+     * @throws Exception
+     */
+    public void getControllerMethodDescription(Log log, SysOperLog operLog) throws Exception {
+        // 设置action动作
+        operLog.setBusinessType(log.businessType().ordinal());
+        // 设置标题
+        operLog.setTitle(log.title());
+        // 设置操作人类别
+        operLog.setOperatorType(log.operatorType().ordinal());
+        // 是否需要保存request,参数和值
+        if (log.isSaveRequestData()) {
+            // 获取参数的信息,传入到数据库中。
+            setRequestValue(operLog);
+        }
+    }
+
+    /**
+     * 获取请求的参数,放到log中
+     *
+     * @param operLog 操作日志
+     * @throws Exception 异常
+     */
+    private void setRequestValue(SysOperLog operLog) throws Exception {
+        Map<String, String[]> map = ServletUtils.getRequest().getParameterMap();
+        if (!ObjectUtil.isEmpty(map)) {
+            PropertyPreFilters.MySimplePropertyPreFilter excludefilter = new PropertyPreFilters().addFilter();
+            excludefilter.addExcludes(EXCLUDE_PROPERTIES);
+            String params = JSONObject.toJSONString(map, excludefilter);
+            operLog.setOperParam(StringUtils.substring(params, 0, 2000));
+            log.info(" >>>请求日志<<< {}", operLog);
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private Log getAnnotationLog(JoinPoint joinPoint) throws Exception {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null) {
+            return method.getAnnotation(Log.class);
+        }
+        return null;
+    }
+
+
+    /**
+     * 异步记录操作地点
+     * @param operLog
+     */
+    @Async("logTaskExecutor")
+    public void asyncRecordLocation(SysOperLog operLog) {
+        try {
+            String address = locationService.getLocationByIp(operLog.getOperIp());
+            operLog.setOperLocation(address);
+            log.info("<<<< 远程查询操作地点 >>>>>>> {}", address);
+
+            // 可选:保存操作日志到数据库
+            // logService.saveOperLog(operLog);
+        } catch (Exception e) {
+            log.warn("记录操作地点失败", e);
+        }
+    }
+}

+ 38 - 0
py-base/src/main/java/com/poyee/aspectj/MqConsumerAOP.java

@@ -0,0 +1,38 @@
+package com.poyee.aspectj;
+
+import org.slf4j.MDC;
+
+import java.util.UUID;
+
+/**
+ * 自定义aop
+ *
+ * @author Administrator
+ *
+ */
+//@Component
+//@Aspect
+//@Slf4j
+public class MqConsumerAOP {
+
+    private static final String TRACE_ID = "TRACE_ID";
+
+//    @Pointcut("@annotation(org.springframework.amqp.rabbit.annotation.RabbitListener)")
+    public void mqPointCut() {
+    }
+
+//    @Before("mqPointCut()")
+    public void mqConsumerBefore() {
+        String tid = UUID.randomUUID().toString().replace("-", "");
+        if(MDC.get(TRACE_ID)==null){
+            MDC.put(TRACE_ID, tid);
+        }
+    }
+
+//    @After("mqPointCut()")
+    public void mqConsumerAfter() {
+        MDC.remove(TRACE_ID);
+    }
+
+
+}

+ 305 - 0
py-base/src/main/java/com/poyee/aspectj/RedisCacheAspect.java

@@ -0,0 +1,305 @@
+package com.poyee.aspectj;
+
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.parser.Feature;
+import com.poyee.annotation.RedisCache;
+import com.poyee.base.dto.BaseReq;
+import com.poyee.base.dto.Page;
+import com.poyee.base.dto.Result;
+import com.poyee.base.dto.UserInfo;
+import com.poyee.enums.CacheExpirePolicy;
+import com.poyee.util.DateUtils;
+import com.poyee.util.ServletUtils;
+import com.poyee.util.SpElUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.annotation.Order;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ *  Redis 缓存切面
+ */
+@Slf4j
+@Aspect
+@Component
+@Order(1)
+public class RedisCacheAspect {
+
+    private final RedisTemplate<String, Object> redisTemplate;
+
+    public RedisCacheAspect(RedisTemplate<String, Object> redisTemplate) {
+        this.redisTemplate = redisTemplate;
+    }
+
+    @Around("@annotation(redisCache)")
+    public Object doAround(ProceedingJoinPoint joinPoint, RedisCache redisCache) throws Throwable {
+        String keyExpression = redisCache.key();
+        String keySource = redisCache.keySource();
+        CacheExpirePolicy expirePolicy = redisCache.expirePolicy();
+        long customExpireTime = redisCache.expireTime();
+
+        if (expirePolicy == CacheExpirePolicy.CUSTOM) {
+            expirePolicy.setExpireTime(customExpireTime);
+        }
+
+        // 构建上下文变量
+        Map<String, Object> contextVars = new HashMap<>();
+
+        //如果获取不到用户信息, 判断方法参数中 doCache 是否为 true,如果是则解析方法参数中数据 进行缓存, 否则直接返回结果
+        Object[] args = joinPoint.getArgs();
+        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
+        Method method = methodSignature.getMethod();
+        DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
+        String[] paramNames = nameDiscoverer.getParameterNames(method);
+
+        // 检查方法参数中是否有 doCache 参数且为 true
+        boolean doCache = false;
+        Object arg = null;
+        for (int i = 0; i < args.length; i++) {
+            if (paramNames != null && i < paramNames.length) {
+                arg = args[i];
+                if(arg instanceof BaseReq && ((BaseReq) arg).isDoRedis()){
+                    doCache = true;
+                }
+            }
+        }
+
+        // 从指定 source 获取变量值(如 userContext)
+        switch (keySource) {
+            case "userContext":
+                //根据
+                List<String> strings = SpElUtils.extractLabels(keyExpression);
+                String keyWord = strings.get(0);
+                if(doCache && Objects.nonNull(arg)){
+                    Field declaredField = arg.getClass().getDeclaredField(keyWord);
+                    declaredField.setAccessible(true);
+                    Object valueObj = declaredField.get(arg);
+                    contextVars.put(keyWord, valueObj);
+                } else {
+                    //获取当前用户信息
+                    UserInfo userInfo = ServletUtils.getUserInfo();
+                    try {
+                        Class<?> aClass = userInfo.getClass();
+                        Field declaredField = aClass.getDeclaredField(keyWord);
+                        declaredField.setAccessible(true);
+                        Object valueObj = declaredField.get(userInfo);
+                        contextVars.put(keyWord, valueObj);
+                    } catch (Exception e) {
+
+                    }
+                }
+                break;
+            case "args":
+//                Object[] args = joinPoint.getArgs();
+//                MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
+//                Method method = methodSignature.getMethod();
+//                DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
+//                String[] paramNames = nameDiscoverer.getParameterNames(method);
+                for (int i = 0; i < args.length; i++) {
+                    if (paramNames != null && i < paramNames.length) {
+                        contextVars.put(paramNames[i], args[i]);
+                    }
+                }
+                break;
+            default:
+                // 自定义 source,可扩展
+                break;
+        }
+
+        // 解析 key
+        String finalKey = SpElUtils.parseKey(keyExpression, contextVars);
+
+        // 查询缓存
+        Object cachedValue = redisTemplate.opsForValue().get(finalKey);
+        if (cachedValue != null) {
+            //判断是否为JSONObject 进行转换
+            if(cachedValue instanceof String){
+                //获取过期时间
+                long expireTime = redisTemplate.getExpire(finalKey);
+                log.debug("缓存命中[剩余时间:{} s],返回结果:{}", expireTime,(cachedValue.toString().length() > 200 ? cachedValue.toString().substring(0, 200) +"..." : cachedValue.toString()));
+                //解析为json 并 取得 type 区分是 page 还是result
+                String type = "result";
+                if(cachedValue.toString().startsWith("{")){
+                    JSONObject jsonObject = JSONObject.parseObject((String) cachedValue);
+                    type = jsonObject.getString("type");
+                }
+                if(type.equals("page")){
+                    Page page = JSONObject.parseObject((String) cachedValue, Page.class, Feature.OrderedField);
+                    //设置缓存过期时间
+                    page.setRedisCacheTimes(expireTime);
+                    //如果缓存key 为空 则设置  key
+                    if(StringUtils.isBlank(page.getRedisCacheKey())){
+                        page.setRedisCacheKey(finalKey);
+                    }
+                    //如果是分页 则判断 结果是否排序 排序字段和规则, 对结果重新排序
+//                    doCheckSortByPageOrderBy(page);
+                    return page;
+                }else{
+                    Result result = JSONObject.parseObject((String) cachedValue, Result.class, Feature.OrderedField);
+                    //设置缓存过期时间
+                    result.setRedisCacheTimes(expireTime);
+                    //如果缓存key 为空 则设置  key
+                    if(StringUtils.isBlank(result.getRedisCacheKey())){
+                        result.setRedisCacheKey(finalKey);
+                    }
+                    return result;
+                }
+            }
+            return cachedValue;
+        }
+
+        // 执行目标方法
+        Object result = joinPoint.proceed();
+
+        // 判断返回值是否有效(非空、非异常)
+        if (result == null || result instanceof Boolean && !(Boolean) result) {
+            return result;
+        }
+        //处理 返回 code
+        if((result.getClass().getName().equals("com.poyee.base.dto.Result") || result.getClass().getName().equals("com.poyee.base.dto.Page") )
+                && result.getClass().getDeclaredField("code") != null){
+            try{
+                Field codeField = result.getClass().getDeclaredField("code");
+                codeField.setAccessible(true);
+                Object codeObj = codeField.get(result);
+                if(codeObj != null && Integer.parseInt(codeObj.toString()) != 200 && Integer.parseInt(codeObj.toString()) != 0 ){
+                    return result;
+                }
+            }catch (Exception e){
+                //忽略异常
+            }
+        }
+        // 设置缓存
+        long expireSeconds = expirePolicy.getExpireSeconds();
+        //设置 查询数据库操作时间
+        try{
+            Field codeField = result.getClass().getDeclaredField("lastOptTime");
+            codeField.setAccessible(true);
+            Object codeObj = codeField.get(result);
+            //设置当前时间为最后一次操作时间
+            if(codeObj == null){
+                codeField.set(result, DateUtils.dateTimeNow(DateUtils.YYYY_MM_DD_HH_MM_SS));
+            }
+            //设置缓存key
+            codeField = result.getClass().getDeclaredField("redisCacheKey");
+            codeField.setAccessible(true);
+            codeField.set(result, finalKey);
+            //设置缓存时间
+            codeField = result.getClass().getDeclaredField("redisCacheTimes");
+            codeField.setAccessible(true);
+            codeField.set(result, expirePolicy.getExpireSeconds());
+
+        }catch (Exception e){
+            log.error("设置 查询数据库操作时间 异常",e);
+        }
+        String jsonString = JSONObject.toJSONString(result);
+        if (expireSeconds > 0) {
+
+        } else {
+            // 默认设置缓存时间为1小时 (3600秒)
+            expireSeconds = 3600;
+        }
+        redisTemplate.opsForValue().set(finalKey, jsonString, expireSeconds, java.util.concurrent.TimeUnit.SECONDS);
+        //判断是否需要缓存key集合
+        if(redisCache.logKey()
+                && StringUtils.isNotBlank(redisCache.logKeyRule())
+                && !Objects.equals(redisCache.logKeyRule(),"key")){
+            String logKeyExpression = redisCache.logKeyRule();
+            // 解析 key
+            String finalLogKey = SpElUtils.parseKey(logKeyExpression, contextVars);
+            // redis 缓存 设置 list add 值
+            redisTemplate.opsForList()
+                         .leftPush(finalLogKey, finalKey);
+            // 设置列表过期时间,与缓存过期时间一致
+            redisTemplate.expire(finalLogKey, expireSeconds, java.util.concurrent.TimeUnit.SECONDS);
+        }
+        log.debug("缓存结果:{}", jsonString.length() > 200 ? jsonString.substring(0, 200) + "..." : jsonString);
+        return result;
+    }
+
+    private void doCheckSortByPageOrderBy(Page page) {
+        String orderByField = page.getOrderBy();
+        if(StringUtils.isNotBlank(orderByField)) {
+            List<?> rows = page.getRows();
+            boolean isDesc = page.isDesc(); // 是否降序
+
+            try {
+                rows.sort((o1, o2) -> {
+                    if (o1 == null || o2 == null) return 0;
+
+                    // 使用反射获取 orderByField 的值
+                    Object v1 = getFieldValue(o1, orderByField);
+                    Object v2 = getFieldValue(o2, orderByField);
+
+                    if (v1 == null && v2 == null) return 0;
+                    if (v1 == null) return isDesc ? 1 : -1;
+                    if (v2 == null) return isDesc ? -1 : 1;
+
+                    // 按类型安全比较
+                    int result;
+                    if (v1 instanceof Comparable && v2.getClass()
+                                                      .isAssignableFrom(v1.getClass())) {
+                        result = ((Comparable) v1).compareTo(v2);
+                    } else {
+                        // 转为字符串比较(兜底)
+                        result = v1.toString()
+                                   .compareTo(v2.toString());
+                    }
+
+                    return isDesc ? -result : result; // 反转符号实现降序
+                });
+            } catch (Exception e) {
+                log.warn("缓存分页数据排序失败,orderByField={}", orderByField, e);
+            }
+        }
+    }
+
+    // 使用反射获取字段值
+    private Object getFieldValue(Object obj, String fieldName) {
+        if (obj == null || StringUtils.isBlank(fieldName)) return null;
+
+        Class<?> clazz = obj.getClass();
+        String[] fields = fieldName.split("\\."); // 支持嵌套属性 a.b.c
+
+        try {
+            for (String field : fields) {
+                Field declaredField = findField(clazz, field);
+                if (declaredField == null) return null;
+                declaredField.setAccessible(true);
+                obj = declaredField.get(obj);
+                if (obj == null) break;
+                clazz = obj.getClass();
+            }
+            return obj;
+        } catch (Exception e) {
+            log.debug("获取对象 {} 字段 {} 值失败", obj.getClass().getSimpleName(), fieldName, e);
+            return null;
+        }
+    }
+
+    // 支持父类字段查找
+    private Field findField(Class<?> clazz, String name) {
+        while (clazz != null) {
+            try {
+                return clazz.getDeclaredField(name);
+            } catch (NoSuchFieldException e) {
+                clazz = clazz.getSuperclass();
+            }
+        }
+        return null;
+    }
+
+}

+ 211 - 0
py-base/src/main/java/com/poyee/aspectj/RedisLockAspect.java

@@ -0,0 +1,211 @@
+package com.poyee.aspectj;
+
+import com.poyee.annotation.RedisLock;
+import com.poyee.common.exception.ServiceException;
+import com.poyee.redis.util.RedisUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+
+import javax.annotation.Resource;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 分布式
+ * key 用例:@RedisLock(lockKey = "update_group_info_+#{groupInfoId}", waitTime = 30, leaseTime = -1)
+ * @author zheng
+ */
+@Slf4j
+@Aspect
+@Component
+public class RedisLockAspect {
+
+    @Resource(name = "masterTransactionManager")
+    private DataSourceTransactionManager transactionManager;
+    @Autowired
+    private RedisUtils redisUtils;
+
+    public static final String PREFIX = "#{";
+    public static final String SUBFIX = "}";
+
+    // 配置织入点
+    @Pointcut("@annotation(com.poyee.annotation.RedisLock)")
+    public void lockPointCut() {
+    }
+
+    @Around("com.poyee.aspectj.RedisLockAspect.lockPointCut()")
+    public Object lockAround(ProceedingJoinPoint pjp) throws Throwable {
+        Signature signature = pjp.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method targetMethod = methodSignature.getMethod();
+        RedisLock annotation = targetMethod.getAnnotation(RedisLock.class);
+        String lockKey = annotation.lockKey();
+        lockKey= generateKeyBySpEL(lockKey,pjp);
+        log.debug("redis key:{}",lockKey);
+        TimeUnit unit = annotation.unit();
+        long waitTime = annotation.waitTime();
+        long leaseTime = annotation.leaseTime();
+        boolean useTran = annotation.isUseTran();
+
+        TransactionStatus status=null;
+        boolean isLock=false;
+        try {
+            isLock = redisUtils.tryLock(lockKey, unit, waitTime, leaseTime);
+            if (isLock) {
+                Object proceed;
+                if(useTran) {
+                    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
+                    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
+                    def.setIsolationLevel(2);
+                    status = transactionManager.getTransaction(def);
+                    proceed = pjp.proceed();
+                    transactionManager.commit(status);
+                }else {
+                     proceed = pjp.proceed();
+                }
+                redisUtils.unlock(lockKey);
+                isLock=false;
+                return proceed;
+            }
+        }catch (ServiceException e) {
+            log.error("业务处理异常,{}", e);
+            if(status!=null){
+                transactionManager.rollback(status);
+            }
+            throw e;
+        } catch (Exception e) {
+            log.error("分布式锁执行异常,{}", e);
+            if(status!=null){
+                transactionManager.rollback(status);
+            }
+        } finally {
+            try {
+                if(isLock){
+                    redisUtils.unlock(lockKey);
+                }
+            } catch (Exception e) {
+                log.error("分布式锁释放异常,{}", e);
+            }
+        }
+        throw new ServiceException(500,"lock失败");
+    }
+
+
+    /**
+     * @param contextEl 带表达式的内容
+     *                  举例: "用户名:"+#{user.name}+用户年龄:+#{user.age}
+     *                  文字+#{map.name} ,不支持对象中嵌套map 如:{user.map.name}
+     *                  最多支持三个变量 #{user.data.phone}
+     * @param joinPoint 环绕切面 pjp
+     * @return
+     */
+    public static String generateKeyBySpEL(String contextEl, JoinPoint joinPoint) {
+        try {
+            StringBuilder context = new StringBuilder();
+            if (contextEl.contains(PREFIX) && contextEl.contains(SUBFIX) && contextEl.contains("+")) {
+                String[] strings = (contextEl.split("\\+"));
+                for (String s : strings) {
+                    if (s.contains(PREFIX) && s.contains(SUBFIX)) {
+                        String spel = s.substring(s.indexOf(PREFIX) + PREFIX.length(), s.lastIndexOf(SUBFIX));
+                        context.append((resolverExpression(spel, joinPoint)));
+                    } else {
+                        context.append(s);
+                    }
+                }
+            }
+            return context.toString();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static String resolverExpression(String expression, JoinPoint joinPoint) throws Exception {
+        String first = null;
+        String second = null;
+        String third = null;
+        if (expression.contains(".")) {
+            List<String> list = Arrays.asList((expression.split("\\.")));
+            if (list.size() > 3) {
+                throw new ServiceException(500, "不支持三个变量以上表达式" + expression);
+            }
+            for (int i = 0; i < list.size(); i++) {
+                if (i == 0) {
+                    first = list.get(0);
+                }
+                if (i == 1) {
+                    second = list.get(1);
+                }
+                if (i == 2) {
+                    third = list.get(2);
+                }
+            }
+        } else {
+            first = expression;
+        }
+
+        Object[] args = joinPoint.getArgs();
+        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
+        Method method = methodSignature.getMethod();
+        Class<?>[] parameterTypes = method.getParameterTypes();
+        DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
+        String[] paramNames = nameDiscoverer.getParameterNames(method);
+        for (int i = 0; i < paramNames.length; i++) {
+            Class<?> parameterType = parameterTypes[i];
+            String paramName = paramNames[i];
+            Object paramValue = args[i];
+            if (first.equals(paramName)) {
+                if (second == null) {
+                    return paramValue.toString();
+                }
+                if (second != null) {
+                    if (paramValue instanceof Map<?, ?>) {
+                        return (String) (((Map<?, ?>) paramValue).get(second));
+                    } else {
+                        return String.valueOf(getFieldValue(paramValue, second + (third != null ? "." + third : "")));
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    private static Object getFieldValue(Object target, String fieldName) throws Exception {
+        Class<?> clazz = target.getClass();
+        String[] fs = fieldName.split("\\.");
+        try {
+            for (int i = 0; i < fs.length-1; i++) {
+                Field f = clazz.getDeclaredField(fs[i]);
+                f.setAccessible(true);
+                target = f.get(target);
+                if (target != null) {
+                    return null;
+                }
+                clazz = target.getClass();
+            }
+            Field f = clazz.getDeclaredField(fs[fs.length - 1]);
+            f.setAccessible(true);
+            return f.get(target);
+        } catch (Exception e) {
+            throw e;
+        }
+    }
+
+
+}

+ 166 - 0
py-base/src/main/java/com/poyee/aspectj/UserLoginTokenAspect.java

@@ -0,0 +1,166 @@
+package com.poyee.aspectj;
+
+import com.alibaba.fastjson.JSONException;
+import com.alibaba.fastjson.JSONObject;
+import com.poyee.base.dto.AppBaseUser;
+import com.poyee.base.dto.UserInfo;
+import com.poyee.common.auth.UserRoleUtil;
+import com.poyee.common.enums.DataAuth;
+import com.poyee.common.enums.Roles;
+import com.poyee.common.exception.AuthException;
+import com.poyee.common.exception.BusinessException;
+import com.poyee.framework.jwt.JwtUtils;
+import com.poyee.annotation.UserLoginToken;
+import com.poyee.i18n.I18nUtils;
+import com.poyee.util.ServletUtils;
+import com.poyee.util.http.HttpClient;
+import com.poyee.util.security.Base64Util;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang.StringUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static com.poyee.i18n.I18nMessageEnums.*;
+
+/**
+ * 用户登录切面
+ */
+@Slf4j
+@Aspect
+@Component
+public class UserLoginTokenAspect {
+
+    @Value("${user.info-url:https://coresvc-dev.hobbystocks.cn/user/}")
+    private String userInfoUrl;
+    @Value("${whitelist.id:0}")
+    private String whitelistId;
+    // 配置织入点
+    @Pointcut("@annotation(com.poyee.annotation.UserLoginToken)")
+    public void logPointCut() {
+    }
+
+
+    /**
+     * @param
+     */
+    @Before("execution(public * com.poyee.*controller.*.*(..))")
+    public void doBefore(JoinPoint joinPoint) {
+        HttpServletRequest request = ServletUtils.getRequest();
+        String userInfoStr = request.getHeader("X-USER-BASE64");
+        userInfoStr = StringUtils.isNotEmpty(userInfoStr) ? userInfoStr : request.getHeader("x-user-base64");
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+        log.info(" X-USER-BASE64 >>> {}",userInfoStr);
+        String authorization = request.getHeader("Authorization");
+        log.info(" authorization >>> {} ",authorization);
+
+        //设置 国际语言
+        com.poyee.util.ServletUtils.getSession().setAttribute("i18n", false);
+        //检查是否需要 UserLoginToken 认证用户信息
+        //检查用户权限
+        checkUserLoginToken(request, userInfoStr, method);
+    }
+    /**
+     * 检查用户权限
+     * @param request
+     * @param userInfoStr
+     * @param method
+     */
+    private void checkUserLoginToken(HttpServletRequest request, String userInfoStr, Method method) {
+        if (method.isAnnotationPresent(UserLoginToken.class)) {
+            UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
+            if (userLoginToken.required()) {
+                boolean isAuthorization = false;
+                if (org.apache.commons.lang3.StringUtils.isEmpty(userInfoStr)) {
+                    try {
+                        userInfoStr = request.getHeader("authorization");
+                        if (userInfoStr.startsWith("Bearer ")) {
+                            isAuthorization = true;
+                            userInfoStr = userInfoStr.replaceAll("Bearer ", "");
+                        }
+                    }catch (Exception e){
+                        log.error(" 获取token 异常 {} ", userInfoStr);
+                        throw new BusinessException(401, I18nUtils.get(AUTH_USER_ERROR, "401"));
+                    }
+                }
+                //保存用户信息
+                com.poyee.util.ServletUtils.getSession().setAttribute("x-user-base64", userInfoStr);
+                String user = "";
+                try {
+                    if(isAuthorization){
+                        user = JSONObject.toJSONString(JwtUtils.getTokenUserInfo(userInfoStr));
+                    }else {
+                        user = new String(Base64Util.decode(userInfoStr), StandardCharsets.UTF_8);
+                        JSONObject.parseObject(user);
+                    }
+                } catch (JSONException jsonExe) {
+                    log.error(" 转换json 异常 {} ", userInfoStr);
+                    //使用jwt进行获取用户信息
+                    user = JSONObject.toJSONString(JwtUtils.getTokenUserInfo(userInfoStr));
+                } catch (IllegalArgumentException ie) {
+                    log.error(" token bease64解析 异常 {} ", userInfoStr);
+                    try {
+                        user = new String(Base64Util.uriDecode(userInfoStr));
+                        log.error(" bease64解析 异常>> 使用uri 解析 》》》{}", user);
+                    } catch (IllegalArgumentException ie1) {
+                        log.error(" uriBease64解析 异常>> 使用uri 解析 》》》 {}", userInfoStr);
+                        throw new BusinessException(402, I18nUtils.get(AUTH_USER_ERROR, ",请重新登录[402]"));
+                    }
+                }
+                if (org.apache.commons.lang3.StringUtils.isEmpty(user)) {
+                    throw new BusinessException(402, I18nUtils.get(AUTH_USER_ERROR, ",请重新登录[402]"));
+                }
+                request.getSession().setAttribute("userInfo", user);
+            }
+            if (userLoginToken.faceVerify()) {
+                if (!getAppUserInfo()) {
+                    throw new BusinessException(403, I18nUtils.get(AUTH_USER_NOT_REAL_NAME, ",请先进行实名认证[403]"));
+                }
+            }
+            //权限判断
+            if (!UserRoleUtil.builder(whitelistId).checkRole(userLoginToken.pass_aud(), Arrays.asList(userLoginToken.roles()))) {
+                throw new BusinessException(403, I18nUtils.get(AUTH_USER_ERROR, "[403]"));
+            }
+        }
+    }
+
+    /**
+     * @return
+     */
+    private boolean getAppUserInfo() {
+        com.poyee.base.dto.UserInfo userInfo = com.poyee.util.ServletUtils.getUserInfo();
+        if (null == userInfo || null == userInfo.getId()) {
+            throw new BusinessException( I18nUtils.get(RELOGIN, "[405]"));
+        }
+        String str = HttpClient.get(userInfoUrl + userInfo.getId());
+        log.info(" 调用接口查询用户详情信息》》》{} ", str);
+        try {
+            AppBaseUser appBaseUser = JSONObject.parseObject(str, AppBaseUser.class);
+            if (1 != appBaseUser.getFaceVerify()) {
+                throw new BusinessException(403, I18nUtils.get(AUTH_USER_NOT_REAL_NAME, "[403]"));
+            }
+            com.poyee.util.ServletUtils.getSession().setAttribute("appBaseUser", appBaseUser);
+        } catch (Exception e) {
+//            throw new BusinessException(403, "请先进行实名认证[403]!");
+        }
+        return true;
+    }
+
+}

+ 90 - 0
py-base/src/main/java/com/poyee/aspectj/i18nAspect.java

@@ -0,0 +1,90 @@
+package com.poyee.aspectj;
+
+import com.poyee.annotation.I18n;
+import com.poyee.common.enums.I18nFormat;
+import com.poyee.util.ServletUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import java.lang.reflect.Method;
+
+/**
+ * 国际化切面类,用于处理国际化相关的逻辑
+ */
+@Slf4j
+@Aspect
+@Component
+public class i18nAspect {
+
+    // 配置织入点,针对使用了i18n注解的方法
+    @Pointcut("@annotation(com.poyee.annotation.I18n)")
+    public void logPointCut() {
+    }
+
+    /**
+     * 在控制器的方法执行前,处理国际化逻辑
+     * @param joinPoint 切入点对象,包含被拦截的方法信息
+     */
+    @Before("execution(public * com.poyee.controller.*.*(..))")
+//    @Before("logPointCut()")
+    public void doBefore(JoinPoint joinPoint) {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+        checkI18n(method);
+    }
+
+    /**
+     * 判断是否支持国际化,并根据情况设置会话属性
+     * @param method 被拦截的方法对象,用于获取注解信息
+     */
+    private void checkI18n(Method method) {
+        if (method.isAnnotationPresent(I18n.class)) {
+            I18n i18n = method.getAnnotation(I18n.class);
+            HttpServletRequest request = ServletUtils.getRequest();
+            String language = request.getHeader("Accept-Language");
+            boolean isI18n = true;
+            if(StringUtils.isBlank(language)) {
+                language = LocaleContextHolder.getLocale().getLanguage();
+                isI18n = false;
+            }
+
+            try {
+                HttpSession session = ServletUtils.getSession(false);
+                if (session == null) {
+                    log.warn("Session is null, cannot set I18n attributes.");
+                    return;
+                }
+
+                // 根据方法上的i18n注解和请求头中的语言信息,决定是否启用国际化
+                if(i18n.value() && isI18n) {
+                    session.setAttribute("language", language);
+                    I18nFormat[] format = i18n.format();
+                    if(format.length > 0) {
+                        session.setAttribute("i18nFormat", i18n.format());
+                        if (log.isInfoEnabled()) {
+                            log.info("i18nFormat:{}", format[0].getMsg());
+                        }
+                    }
+                    session.setAttribute("i18n", true);
+                } else {
+                    // 如果 I18n.value() 为 false,但 isI18n 为 true,仍不启用国际化
+                    session.setAttribute("i18n", false);
+                }
+            } catch (Exception e) {
+                log.error("Failed to set I18n attributes", e);
+            }
+        }
+    }
+
+}

+ 15 - 0
py-base/src/main/java/com/poyee/base/controller/ActuatorController.java

@@ -0,0 +1,15 @@
+package com.poyee.base.controller;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/actuator")
+public class ActuatorController {
+
+    @RequestMapping("/health")
+    public String health() {
+        return "ok";
+    }
+
+}

+ 98 - 0
py-base/src/main/java/com/poyee/base/controller/BaseController.java

@@ -0,0 +1,98 @@
+package com.poyee.base.controller;
+
+import com.poyee.base.dto.BaseDto;
+import com.poyee.base.dto.BaseReq;
+import com.poyee.base.dto.UserInfo;
+import com.poyee.base.service.BaseService;
+import com.poyee.common.exception.AuthException;
+import com.poyee.i18n.I18nUtils;
+import com.poyee.util.DateUtils;
+import com.poyee.util.ServletUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Field;
+import java.util.Date;
+import java.util.Objects;
+
+import static com.poyee.i18n.I18nMessageEnums.GET_USER_INFO_ERROR;
+import static com.poyee.i18n.I18nMessageEnums.NO_PERMISSION;
+
+/**
+ *
+ */
+@Slf4j
+public abstract class BaseController<S extends BaseService<T, R>, T extends BaseReq, R extends BaseDto> {
+
+    @Autowired
+    protected S baseService;
+
+    /**
+     * 日志
+     * @param label
+     * @param obj
+     */
+    public void console(String label, Object obj ){
+        //当前操作用户 及 角色 ,操作时间
+        String userName = "游客";
+        String roleCode = "游客";
+        try{
+            UserInfo userInfo = ServletUtils.getUserInfo();
+            String name = StringUtils.isNotBlank(userInfo.getDisplayName()) ? userInfo.getDisplayName() : userInfo.getSub();
+            if(StringUtils.isBlank(name)){
+                userName = StringUtils.isNotBlank(userInfo.getUserId()) ? "用户id:"+userInfo.getUserId() : userName;
+            }
+            String role = userInfo.getRoleCode();
+            if (StringUtils.isBlank(role)) {
+                roleCode = userInfo.isMerchant() ? "商户" : roleCode;
+            }
+        } catch (Exception e) {
+            log.error("当前用户[{}] 角色【{}】获取用户信息异常", userName, roleCode, e);
+        } finally {
+            log.info("{} {}\n" + " 当前用户[{}] 角色【{}】操作时间[{}] ", label, obj,
+                     userName, roleCode , DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, new Date()));
+        }
+    }
+
+    /**
+     * 检查商户权限
+     * @param p
+     * @param <P>
+     */
+    public <P extends BaseReq> void checkAndSetMerchantId(P p){
+        try {
+            UserInfo userInfo = ServletUtils.getUserInfo();
+            if(userInfo.isMerchant()) {
+                //解析 p 中的user_id
+                if (Objects.nonNull(p)) {
+                    Field merchantId = p.getClass().getDeclaredField("merchantId");
+                    if (Objects.nonNull(merchantId)) {
+                        merchantId.setAccessible(true);
+                        String typeName = merchantId.getType().getName();
+                        if (Objects.equals("java.lang.Long", typeName)) {
+                            merchantId.set(p, Long.valueOf(userInfo.getMerchantId()));
+                        } else if (Objects.equals("java.lang.Integer", typeName)) {
+                            merchantId.set(p, userInfo.getMerchantId());
+                        }
+                    }
+                }
+            }
+        } catch (NoSuchFieldException ne) {
+            log.error("请求参数中无userId",ne);
+            throw new AuthException(402, I18nUtils.get(NO_PERMISSION, "【权限不足】"));
+        } catch (Exception e) {
+            log.error("获取用户信息失败",e);
+            throw new AuthException(I18nUtils.get(GET_USER_INFO_ERROR));
+        }
+    }
+    /**
+     * 获取request
+     */
+    public HttpServletRequest getRequest() {
+        return ServletUtils.getRequest();
+    }
+
+
+}

+ 154 - 0
py-base/src/main/java/com/poyee/base/dto/AppBaseUser.java

@@ -0,0 +1,154 @@
+package com.poyee.base.dto;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * <p>
+ *
+ * </p>
+ *
+ * @author lsz
+ * @since 2022-12-09
+ */
+@Data
+@ApiModel(value = "AppBaseUser对象", description = "")
+public class AppBaseUser implements Serializable {
+
+    @ApiModelProperty("id")
+    private Integer id;
+
+    @ApiModelProperty("所属程序")
+    private String appid;
+
+    @ApiModelProperty("真实姓名")
+    private String realname;
+
+    @ApiModelProperty("昵称")
+    private String nickname;
+
+    @ApiModelProperty("头像")
+    private String avatar;
+
+    @ApiModelProperty("积分")
+    private Long point;
+
+    @ApiModelProperty("会员等级")
+    private Integer level;
+
+    @ApiModelProperty("生日")
+    private Date birthday;
+
+    @ApiModelProperty("性别")
+    private Integer sex;
+
+    @ApiModelProperty("openid")
+    private String openid;
+
+    @ApiModelProperty("unionid")
+    private String unionid;
+
+    @ApiModelProperty("注册渠道")
+    private String registerChannel;
+
+    @ApiModelProperty("状态")
+    private Integer status;
+
+    @ApiModelProperty("删除标记")
+    private Integer delFlg;
+
+    @ApiModelProperty("备注")
+    private String remark;
+
+    @ApiModelProperty("创建人")
+    private String createBy;
+
+    @ApiModelProperty("创建时间")
+    private Date createTime;
+
+    @ApiModelProperty("更新人")
+    private String updateBy;
+
+    @ApiModelProperty("更新时间")
+    private Date updateTime;
+
+    @ApiModelProperty("账号")
+    private String username;
+
+    @ApiModelProperty("会员成长值")
+    private Integer growthNum;
+
+    @ApiModelProperty("会员码")
+    @TableField("code")
+    private String code;
+
+    @ApiModelProperty("是否接受通知消息,1接受,0拒绝")
+    private Integer notifyFlag;
+
+    @ApiModelProperty("app极光注册id")
+    private String smsRegisterId;
+
+    @TableField("user_id")
+    private Integer userId;
+
+    @TableField("notify_type")
+    private String notifyType;
+
+    @ApiModelProperty("通过人脸识别标志:1")
+    private Integer faceVerify;
+
+    @ApiModelProperty("支付开关")
+    private Integer openPsd;
+
+    @ApiModelProperty("支付密码")
+    private String payPsd;
+
+    @ApiModelProperty("登陆密码")
+    private String loginPsd;
+
+    @ApiModelProperty("是否拒绝自提")
+    private Integer refusePickUp;
+
+    @ApiModelProperty("备用")
+    private String prop1;
+
+    @ApiModelProperty("备用")
+    private String prop2;
+
+    @ApiModelProperty("备用")
+    private String prop3;
+
+    @ApiModelProperty("备用")
+    private String prop4;
+
+    @ApiModelProperty("悬浮窗口开关")
+    private Integer windowOpen;
+
+    @ApiModelProperty("身份证姓名")
+    private String certName;
+
+    @ApiModelProperty("支付宝账号")
+    private String alipayAccount;
+
+    @ApiModelProperty("开票权限")
+    private Integer openInvoice;
+
+    @ApiModelProperty("编辑黑名单")
+    private Integer blacklist;
+
+    @ApiModelProperty("身份证号")
+    private String idCard;
+
+    /**
+     * 用户身份证信息
+     **/
+    @ApiModelProperty("用户身份证信息")
+    private String userCertData;
+    private String phone;
+
+}

+ 19 - 0
py-base/src/main/java/com/poyee/base/dto/BaseDto.java

@@ -0,0 +1,19 @@
+package com.poyee.base.dto;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 出参
+ */
+@Data
+@ApiModel(description = "基础数据输出对象")
+public class BaseDto {
+
+    @ApiModelProperty(value = "data_source" ,hidden = true)
+    @TableField(value = "data_source",exist = false)
+    private String dataSource;
+
+}

+ 71 - 0
py-base/src/main/java/com/poyee/base/dto/BaseReq.java

@@ -0,0 +1,71 @@
+package com.poyee.base.dto;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 入参
+ */
+@ApiModel(description = "基础数据输入对象")
+@Data
+public class BaseReq {
+
+    @ApiModelProperty(value = "数据源",example = "slave", required = true,hidden = true)
+    @TableField(exist = false)
+    private String dbVersion;
+    @ApiModelProperty(hidden = true)
+    @TableField(exist = false)
+    public static List<String> types = Arrays.asList("pageNo", "pageSize", "sidx", "sord", "orderBy", "limit", "desensit");
+    @ApiModelProperty(value = "页码,查询时使用", notes = "查询时使用", reference = "1",example = "1")
+    @TableField(exist = false)
+    private Integer pageNo = 1;
+    @ApiModelProperty(value = "每页数量,查询时使用", notes = "查询时使用", reference = "10", example = "10")
+    @TableField(exist = false)
+    private Integer pageSize = 10;
+    @ApiModelProperty(value = "排序字段,查询时使用", notes = "查询时使用", reference = " createTime ")
+    @TableField(exist = false)
+    private String sidx;
+    @ApiModelProperty(value = "排序规则,查询时使用", notes = "查询时使用", reference = " ase ")
+    @TableField(exist = false)
+    private String sord;
+    @ApiModelProperty(value = "排序方式",hidden = true)
+    @TableField(exist = false)
+    private String orderBy;
+    @ApiModelProperty(value = "时间戳",hidden = true)
+    @TableField(exist = false)
+    private Long timestamp;
+    @ApiModelProperty(hidden = true)
+    @TableField(exist = false)
+    private Integer limit;
+
+    @ApiModelProperty(value = "创建时间", hidden = true)
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    private Date createTime;
+    @ApiModelProperty(value = "创建人", hidden = true)
+    @TableField(value = "create_by", fill = FieldFill.INSERT)
+    private String createBy;
+    @ApiModelProperty(value = "更新时间", hidden = true)
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    private Date updateTime;
+    @ApiModelProperty(value = "更新人", hidden = true)
+    @TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
+    private String updateBy;
+    @ApiModelProperty(value = "是否执行redis缓存:定时缓存处理标记不验证用户信息",hidden = true)
+    @TableField(exist = false)
+    private boolean doRedis;
+    @ApiModelProperty(value = "服务方法名称:定时缓存处理时记录方法",hidden = true)
+    @TableField(exist = false)
+    private String serviceMethodName;
+
+    public Integer getLimit() {
+        return (pageNo - 1) * pageSize;
+    }
+
+}

+ 28 - 0
py-base/src/main/java/com/poyee/base/dto/MpjWrapper.java

@@ -0,0 +1,28 @@
+package com.poyee.base.dto;
+
+import com.poyee.annotation.MpjWapper;
+import com.poyee.util.ObjectUtil;
+import lombok.Data;
+
+/**
+ *
+ */
+@Data
+public class MpjWrapper {
+
+    private MpjWapper field;
+
+    private String data;
+
+    public MpjWrapper builder(MpjWapper field,Object data) {
+        if(this.data!=null){
+            //data 使用 逗号拼接
+            this.data = ObjectUtil.trim(this.data+","+data);
+        }else{
+            this.data = ObjectUtil.trim(data.toString());
+        }
+        this.field = field;
+        return this;
+    }
+
+}

+ 91 - 0
py-base/src/main/java/com/poyee/base/dto/Page.java

@@ -0,0 +1,91 @@
+package com.poyee.base.dto;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.metadata.OrderItem;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ *
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Page<T>  {
+    @ApiModelProperty(value = "返回结果 封装类")
+    private String type = "page";
+
+    @ApiModelProperty(value = "状态码:0=正常")
+    private Integer code;
+    @ApiModelProperty(value = "总记录数")
+    private Long total;
+    @ApiModelProperty(value = "数据列表")
+    private List<T> rows;
+    @ApiModelProperty(value = "当前页码")
+    private Long pageNo;
+    @ApiModelProperty(value = "当前页条数")
+    private Long size;
+
+    @ApiModelProperty(value = "最后操作时间")
+    private String lastOptTime;
+    //缓存key
+    @ApiModelProperty(value = "缓存key")
+    private String redisCacheKey;
+    //缓存时间
+    @ApiModelProperty(value = "缓存时间:s")
+    private Long redisCacheTimes;
+    //排序字段
+    @ApiModelProperty(value = "排序字段",hidden = true)
+    private String orderBy;
+    @ApiModelProperty(value = "排序字段是否降序",hidden = true)
+    private boolean  isDesc;
+
+    public Page(Long total, List<T> rows) {
+        this.code = 200;
+        this.rows = rows;
+        if(Objects.nonNull(total)){
+            this.total = total;
+        }else if (Objects.nonNull(rows)) {
+            this.total = (long) rows.size();
+        }
+    }
+
+    public Page(List<T> rows) {
+        this.rows = rows;
+        if (Objects.nonNull(rows)) {
+            this.total = (long) rows.size();
+        }
+    }
+
+    public Page<T> setRows(List<T> rows) {
+        this.rows = rows;
+        return this;
+    }
+
+    public Page<T> setTotal(Long total) {
+        this.total = total;
+        return this;
+    }
+
+    public Page<T> setPage(Long pageNo) {
+        this.pageNo = pageNo;
+        this.size = Objects.nonNull(rows)?(long) rows.size():0;
+        return this;
+    }
+
+    public static <R extends BaseDto> Page<R> iPage(IPage<R> iPage) {
+        Page<R> r = new Page<>();
+        r.setCode(200);
+        r.setTotal(iPage.getTotal());
+        if(iPage.getTotal() > 0){
+            r.setRows(iPage.getRecords());
+        }
+        r.setTotal(iPage.getTotal());
+        return r;
+    }
+}

+ 208 - 0
py-base/src/main/java/com/poyee/base/dto/Result.java

@@ -0,0 +1,208 @@
+package com.poyee.base.dto;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.poyee.constant.ResultCode;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ *
+ */
+@ApiModel(description = "返回结果 封装类")
+@AllArgsConstructor
+@Data
+public class Result<T> {
+
+    @ApiModelProperty(value = "返回结果 封装类")
+    private String type = "result";
+    /**
+     * @see ResultCode
+     */
+    @ApiModelProperty(value = "状态码",example = "200=成功,400=失败,401=未认证,404=接口不存在,500=服务器内部错误")
+    private Integer code;
+
+    @ApiModelProperty(value = "状态描述")
+    private String msg;
+
+    @ApiModelProperty(value = "返回对象数据")
+    private Object data;
+
+    @ApiModelProperty(value = "分页数据")
+    private Page<T> page;
+    //最后一次操作时间
+    @ApiModelProperty(value = "最后一次操作时间")
+    private String lastOptTime;
+    //缓存key
+    @ApiModelProperty(value = "缓存key")
+    private String redisCacheKey;
+    //缓存时间
+    @ApiModelProperty(value = "缓存时间:s")
+    private Long redisCacheTimes;
+
+    public Result(){
+        this.code = ResultCode.SUCCESS.getCode();
+        this.msg = ResultCode.SUCCESS.getMsg();
+    }
+
+    /**
+     * @param <T>
+     * @return
+     */
+    public static <T> Result<T> success() {
+        Result<T> result = new Result<>();
+        result.setCode(ResultCode.SUCCESS.getCode());
+        result.setMsg(ResultCode.SUCCESS.getMsg());
+        return result;
+    }
+    /**
+     * @param <T>
+     * @return
+     */
+    public static <T> Result<T> success( String msg) {
+        Result<T> result = new Result<>();
+        result.setCode(ResultCode.SUCCESS.getCode());
+        result.setMsg(msg);
+        return result;
+    }
+
+    /**
+     * @param data
+     * @param <T>
+     * @return
+     */
+    public static <T> Result<T> success(Object data) {
+        Result<T> result = new Result<>();
+        result.setCode(ResultCode.SUCCESS.getCode());
+        result.setMsg(ResultCode.SUCCESS.getMsg());
+        result.setData(data);
+        return result;
+    }
+
+    /**
+     * @param rows
+     * @param <T>
+     * @return
+     */
+    public static <T> Result<T> page(List<T> rows){
+        Result result = new Result();
+        result.setPage(new Page(rows));
+        return result;
+    }
+
+    /**
+     * @param page
+     * @param <T>
+     * @return
+     */
+    public static <T> Result<T> page(Page<T> page){
+        Result result = new Result();
+        result.setPage(page);
+        return result;
+    }
+
+
+    /**
+     * @param iPage
+     * @param <T>
+     * @return
+     */
+    public static <T> Result<T> iPage(IPage<T> iPage){
+        Result result = new Result();
+        if(Objects.nonNull(iPage)){
+            result.setPage(new Page(iPage.getTotal(),iPage.getRecords()));
+        }
+        return result;
+    }
+    /**
+     * @param total
+     * @param rows
+     * @param <T>
+     * @return
+     */
+    public static <T> Result<T> page(Long total,List<T> rows) {
+        Result result = new Result();
+        result.setPage(new Page(total,rows));
+        return result;
+    }
+
+    /**
+     * @param total
+     * @return
+     */
+    public Result<T> setTotal(Long total){
+        this.page = Optional.ofNullable(this.page).orElse(new Page<T>());
+        this.page.setTotal(total);
+        return this;
+    }
+
+    /**
+     * @return
+     */
+    public static Result error(){
+        Result result = new Result<>();
+        result.setCode(ResultCode.FAIL.getCode());
+        result.setMsg(ResultCode.FAIL.getMsg());
+        return result;
+    }
+
+    /**
+     * @param msg
+     * @return
+     */
+    public static Result error(String msg){
+        Result result = new Result<>();
+        result.setCode(ResultCode.FAIL.getCode());
+        result.setMsg(msg);
+        return result;
+    }
+
+    /**
+     * @param code
+     * @param msg
+     * @return
+     */
+    public static Result error(Integer code, String msg){
+        Result result = new Result<>();
+        result.setCode(code);
+        result.setMsg(msg);
+        return result;
+    }
+
+    /**
+     * @param data
+     * @return
+     */
+    public Result setData(Object data){
+        this.data = data;
+        return this;
+    }
+
+    /**
+     * @return
+     */
+    public static Result fail() {
+        Result<Object> result = new Result<>();
+        result.setCode(ResultCode.FAIL.getCode());
+        result.setMsg(ResultCode.FAIL.getMsg());
+        return result
+                ;
+    }
+    /**
+     * @return
+     */
+    public static Result fail(String msg) {
+        Result<Object> result = new Result<>();
+        result.setCode(ResultCode.FAIL.getCode());
+        result.setMsg(msg);
+        return result
+                ;
+    }
+
+
+}

+ 73 - 0
py-base/src/main/java/com/poyee/base/dto/UserInfo.java

@@ -0,0 +1,73 @@
+package com.poyee.base.dto;
+
+import com.poyee.common.enums.DataAuth;
+import com.poyee.common.enums.Roles;
+import lombok.Data;
+import lombok.ToString;
+import org.apache.commons.collections.CollectionUtils;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 用户信息
+ */
+@Data
+@ToString
+public class UserInfo implements Serializable {
+
+    //id
+    private Integer id;
+    //用户id
+    private String userId;
+    //用户头像
+    private String avatar;
+    //角色编码
+    private String roleCode;
+    //角色编码
+    private String role;
+    //请求时间
+    private Long iat;
+    //过期时间
+    private Long exp;
+    //名称
+    private String displayName;
+
+    private String sub;
+
+    private String iss;
+    //来源
+    private String aud;
+
+    private String nbf;
+
+    private String isFaceVerify;
+
+    private List<String> scope;
+    //商家id
+    private Integer merchantId;
+    //商家头像
+    private String merchantAvatar;
+    //商家名称
+    private String merchantName;
+    /**
+     * 数据权限
+     */
+    private DataAuth dataAuth;
+
+    /**
+     * 是否是商家
+     */
+    private boolean merchant;
+    private String nickname;
+
+    private List<String> partnerRoles;
+
+    public boolean hasPartnerRoleAuth(Roles role){
+        if(CollectionUtils.isEmpty(partnerRoles)){
+            return false;
+        }
+        return partnerRoles.contains(role.getRoleCode());
+    }
+
+}

+ 43 - 0
py-base/src/main/java/com/poyee/base/entity/BaseEntity.java

@@ -0,0 +1,43 @@
+package com.poyee.base.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Data
+public class BaseEntity  {
+
+    @ApiModelProperty(hidden = true)
+    @TableField(exist = false)
+    public static List<String> types = Arrays.asList("pageNo", "pageSize", "sidx", "sord", "orderBy", "limit", "desensit");
+    @ApiModelProperty(value = "页码,查询时使用", notes = "查询时使用", reference = "1")
+    @TableField(exist = false)
+    private Integer pageNo = 1;
+    @ApiModelProperty(value = "每页数量,查询时使用", notes = "查询时使用", reference = "10")
+    @TableField(exist = false)
+    private Integer pageSize = 10;
+    @ApiModelProperty(value = "排序字段,查询时使用", notes = "查询时使用", reference = " createTime ")
+    @TableField(exist = false)
+    private String sidx;
+    @ApiModelProperty(value = "排序规则,查询时使用", notes = "查询时使用", reference = " ase ")
+    @TableField(exist = false)
+    private String sord;
+    @ApiModelProperty(hidden = true)
+    @TableField(exist = false)
+    private String orderBy;
+    @ApiModelProperty(hidden = true)
+    @TableField(exist = false)
+    private Integer limit;
+
+    public static boolean notIn(String type) {
+        return !types.contains(type);
+    }
+
+    public Integer getLimit() {
+        return (pageNo - 1) * pageSize;
+    }
+
+}

+ 14 - 0
py-base/src/main/java/com/poyee/base/mapper/IBaseMapper.java

@@ -0,0 +1,14 @@
+package com.poyee.base.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.github.yulichang.base.MPJBaseMapper;
+import com.poyee.base.dto.BaseDto;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ *
+ */
+@Mapper
+public interface IBaseMapper<D extends BaseDto> extends BaseMapper<D>, MPJBaseMapper<D> {
+
+}

+ 201 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/BaseProvider.java

@@ -0,0 +1,201 @@
+package com.poyee.base.mapper.provider;
+
+import com.poyee.base.dto.BaseDto;
+import com.poyee.base.dto.BaseReq;
+import com.poyee.base.mapper.provider.domain.LeftJoinInfo;
+import com.poyee.base.mapper.provider.domain.MPage;
+import com.poyee.base.mapper.provider.domain.TableInfo;
+import com.poyee.base.mapper.provider.domain.WhereInfo;
+import com.poyee.base.mapper.provider.util.InsertUtil;
+import com.poyee.base.mapper.provider.util.SelectUtil;
+import com.poyee.base.mapper.provider.util.SqlUtil;
+import com.poyee.base.mapper.provider.util.UpdateUtil;
+import com.poyee.common.exception.ServiceException;
+import com.poyee.enums.DbMethodEnums;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * 提供基础的 SQL 构建功能,包括左连接和 WHERE 条件的处理。
+ */
+@Slf4j
+public abstract class BaseProvider {
+
+    // 数据库操作类型
+    public DbMethodEnums dbMethod;
+    // 表计数器
+    public int tableCount = 0;
+    //
+    public boolean isPage = false;
+    // 存储 insert
+    public Object insert;
+    // 记录 UPDATE
+    public Object update;
+    // 记录 SELECT
+    public Object select;
+    // 主表信息
+    public TableInfo superTable;
+    //分页查询
+    public MPage mPage;
+    //标记是否前端传 排序字段
+    public boolean isFrontSort = false;
+    //分页查询
+    public String pageCountSql;
+    // 记录 INSERT key-value(线程安全)
+    public final Map<String, Object> insertMap = new HashMap<>();
+    // 记录 UPDATE key-value(线程安全)
+    public final Map<String, Object> updateMap = new HashMap<>();
+    // 存储表信息的映射(线程安全)
+    public final Map<String, TableInfo> tableInfos = new ConcurrentHashMap<>();
+    // 记录 WHERE 条件(线程安全)
+    public final List<WhereInfo> whereInfos = new CopyOnWriteArrayList<>();
+    // 记录 SELECT 列(线程安全)
+    public final List<String> selectColumns = new CopyOnWriteArrayList<>();
+    // 记录 LEFT JOIN 信息(线程安全)
+    public final List<LeftJoinInfo> leftJoinInfos = new CopyOnWriteArrayList<>();
+    // 记录 ORDER BY 信息(线程安全)
+    public final Map<Integer,String> orderByMap = new HashMap<>();
+
+    /**
+     * @param req
+     * @param clazz
+     * @param <T>
+     * @param <R>
+     * @return
+     */
+    public <T extends BaseReq,R extends BaseDto> MPage<R> mPage(T req, Class<R> clazz){
+        MPage mPage = new MPage();
+        Map<String,Object> map = new HashMap<>();
+        map.put("req",req);
+        map.put("clazz",clazz);
+        String pageSql = pageWrapper(map);
+        mPage.setCountSql(pageCountSql);
+        mPage.setPageSql(pageSql);
+        return mPage;
+    }
+
+    /**
+     * @param object
+     * @return
+     */
+    public String selectById(Object object) {
+        // 检查传入的对象是否非空
+        if (Objects.nonNull(object)) {
+            dbMethod = DbMethodEnums.SELECT_BY_ID;
+            // 组装 SELECT 语句
+            SelectUtil.init(this).checkSelectBean(object).initSelect(select);
+            // 生成最终的 SQL 字符串
+            return SqlUtil.init(this).buildOne().toSql();
+        }
+        throw new ServiceException("传入对象为空!");
+    }
+
+    /**
+     * @param object
+     * @return
+     */
+    public String selectOne(Object object) {
+        // 检查传入的对象是否非空
+        if (Objects.nonNull(object)) {
+            dbMethod = DbMethodEnums.SELECT;
+            // 组装 SELECT 语句
+            SelectUtil.init(this).checkSelectBean(object).initSelect(select);
+            // 生成最终的 SQL 字符串
+            return SqlUtil.init(this).buildOne().toSql();
+        }
+        throw new ServiceException("传入对象为空!");
+    }
+
+    /**
+     * 处理查询对象 生成 SQL 字符串。
+     * @param object
+     * @return
+     */
+    public String selectSum(Object object){
+        // 检查传入的对象是否非空
+        if (Objects.nonNull(object)) {
+            dbMethod = DbMethodEnums.SELECT;
+            // 组装 SELECT 语句
+            SelectUtil.init(this).checkSelectBean(object).initSelect(select);
+            // 生成最终的 SQL 字符串
+            return SqlUtil.init(this).buildSum().toSql();
+        }
+        throw new ServiceException("传入对象为空!");
+    }
+
+    /**
+     * 处理新增对象 生成 SQL 字符串。
+     * @param object
+     * @return
+     */
+    public String save(Object object) {
+        // 检查传入的对象是否非空
+        if (Objects.nonNull(object)) {
+            dbMethod = DbMethodEnums.INSERT;
+            InsertUtil.init(this).checkInsertBean(object).initInsert();
+            // 生成最终的 SQL 字符串
+            String sql = SqlUtil.init(this).toSql();
+            log.info("SQL:{}",sql);
+            return sql;
+        }
+        // 如果传入的对象为空,则返回 null
+        throw new ServiceException("传入的对象为空!");
+    }
+
+    public String update(Object object) {
+        // 检查传入的对象是否非空
+        if (Objects.nonNull(object)) {
+            dbMethod = DbMethodEnums.UPDATE;
+            UpdateUtil.init(this).checkUpdateBean(object).initUpdate();
+            // 生成最终的 SQL 字符串
+            return SqlUtil.init(this).toSql();
+        }
+        throw new ServiceException("传入的对象为空!");
+    }
+
+    /**
+     *
+     * @param object
+     * @return
+     */
+    public String pageWrapper(Object object) {
+        // 检查传入的对象是否非空
+        if (Objects.nonNull(object)) {
+            dbMethod = DbMethodEnums.SELECT;
+            isPage = true;
+            // 组装 SELECT 语句
+            SelectUtil.init(this).checkSelectBean(object).initSelect(select);
+            // 生成最终的 SQL 字符串
+            return SqlUtil.init(this).buildPage().toSql();
+        }
+        // 如果传入的对象为空,则返回 null
+        throw new ServiceException("传入的对象为空!");
+    }
+
+    /**
+     * 处理对象的左连接和 WHERE 条件,生成 SQL 字符串。
+     *
+     * @param object 需要处理的对象
+     * @return 生成的 SQL 字符串
+     */
+    public String leftJoinWrapper(Object object) {
+        // 检查传入的对象是否非空
+        if (Objects.nonNull(object)) {
+            dbMethod = DbMethodEnums.SELECT;
+            // 组装 SELECT 语句
+            SelectUtil.init(this).checkSelectBean(object).initSelect(select);
+            // 生成最终的 SQL 字符串
+            return SqlUtil.init(this).toSql();
+        }
+        // 如果传入的对象为空,则返回 null
+        throw new ServiceException("传入的对象为空!");
+    }
+
+
+}

+ 29 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/ErrMessageContent.java

@@ -0,0 +1,29 @@
+package com.poyee.base.mapper.provider;
+
+public class ErrMessageContent {
+    //"对象不能为空"
+    public static final String ERROR_MESSAGE_OBJECT_NULL = "{0}对象不能为空";
+    //"构建{0}SQL时发生错误"
+    public static final String ERROR_MESSAGE_TABLE_NAME_MISSING = "{0}对象必须包含@TableName注解";
+    //"构建{0}SQL时发生错误"
+    public static final String ERROR_MESSAGE_BUILD_SQL = "构建{0}SQL时发生错误";
+    //"没有有效的{0}字段"
+    public static final String ERROR_MESSAGE_BEAN_NULL = "{0}对象或映射为空";
+    //"没有有效的{0}字段"
+    public static final String ERROR_MESSAGE_OPERA_NULL = "没有有效的{0}字段";
+    //"访问字段时发生错误"
+    public static final String ERROR_MESSAGE_FIELD_ACCESS = "访问字段时发生错误";
+    //非法参数错误
+    public static final String ERROR_MESSAGE_ILLEGAL_ARGUMENT = "非法参数";
+    //"传入的对象为空!"
+    public static final String ERROR_MESSAGE_OBJECT_IS_NULL = "传入的对象为空!";
+    //"表名注解缺失!"
+    public static final String ERROR_MESSAGE_TABLE_NAME_MISSING_2 = "表名注解缺失!";
+    //"表名不能为空!"
+    public static final String ERROR_MESSAGE_TABLE_NAME_IS_NULL = "表名不能为空!";
+
+    public static String getErrorMessage(String message, Object... args) {
+        return String.format(message, args);
+    }
+
+}

+ 273 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/IBaseProvider.java

@@ -0,0 +1,273 @@
+package com.poyee.base.mapper.provider;
+
+import com.poyee.base.dto.BaseDto;
+import com.poyee.base.dto.BaseReq;
+import com.poyee.base.dto.Page;
+import com.poyee.base.mapper.provider.domain.MPage;
+import com.poyee.base.mapper.provider.util.InsertUtil;
+import com.poyee.common.exception.ServiceException;
+import com.poyee.util.BeanUtil;
+import com.poyee.util.ObjectUtil;
+import org.apache.ibatis.annotations.*;
+import org.apache.ibatis.annotations.Param;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static com.poyee.enums.DbMethodEnums.SELECT_BY_ID;
+
+/**
+ * @param <T>
+ * @param <R>
+ */
+public interface IBaseProvider<T extends BaseReq, R extends BaseDto> {
+    /**
+     *  查询列表
+     * @param req
+     * @param clazz
+     * @return
+     */
+    @SelectProvider(type = FinalProviderSql.class, method = "leftJoinWrapper")
+    List<Map<String, Object>> selectListMap(@Param("req") T req, @Param("clazz") Class<R> clazz);
+
+    /**
+     * 查询列表
+     * @param req
+     * @param clazz
+     * @return
+     */
+    @SelectProvider(type = FinalProviderSql.class, method = "leftJoinWrapper")
+    List<Map<String, Object>> selectListMapByReq(@Param("req") T req, @Param("clazz") Class<T> clazz);
+
+    /**
+     * 获取总记录数
+     * @param sql
+     * @return
+     */
+    @SelectProvider(type = FinalProviderSql.class, method = "pageCountSql")
+    long pageCount(String sql);
+
+    /**
+     * 分页查询
+     * @param sql
+     * @return
+     */
+    @SelectProvider(type = FinalProviderSql.class, method = "pageSql")
+    List<Map<String, Object>> page(String sql);
+
+    /**
+     * 查询单个
+     * @param req
+     * @param clazz
+     * @return
+     */
+    @SelectProvider(type = FinalProviderSql.class, method = "selectOne")
+    Map<String, Object> selectOneMap(@Param("req") T req, @Param("clazz") Class<R> clazz);
+
+    /**
+     * 根据id查询
+     * @param id
+     * @param clazz
+     * @return
+     */
+    @SelectProvider(type = FinalProviderSql.class, method = "selectById")
+    Map<String, Object> selectByIdMap(@Param("id") Serializable id, @Param("clazz") Class<R> clazz);
+
+    /**
+     * 求和
+     * @param req
+     * @param clazz
+     * @return
+     */
+    @SelectProvider(type = FinalProviderSql.class, method = "selectSum")
+    Map<String, Object> selectSumMap(@Param("req") T req, @Param("clazz") Class<R> clazz);
+
+    /**
+     * 保存
+     * @param req
+     * @return
+     */
+    @InsertProvider(type = FinalProviderSql.class, method = "save")
+    @Options(useGeneratedKeys = true, keyProperty = "id")
+//    @SelectKey(statement = "select max(id) from promotion_share_info", keyProperty = "id", before = false, resultType = Long.class)
+    int save(T req);
+
+    /**
+     * 批量保存
+     * @param reqList
+     * @return
+     */
+    @InsertProvider(type = FinalProviderSql.class, method = "saveBatch")
+    @Options(useGeneratedKeys = true, keyProperty = "id")
+    int saveBatch(@Param("reqList") List<T> reqList);
+
+    /**
+     * 更新
+     * @param req
+     * @return
+     */
+    @UpdateProvider(type = FinalProviderSql.class, method = "update")
+    int update(T req);
+
+    /**
+     * 根据id查询
+     * @param id
+     * @return
+     */
+    default R selectByPrimaryKey(Serializable id, Class<R> clazz) {
+        Map<String, Object> resultMaps = selectByIdMap(id, clazz);
+        if (resultMaps != null && !resultMaps.isEmpty()) {
+            return ObjectUtil.mapToBeanProvider(resultMaps, clazz);
+        }
+        try {
+            return clazz.newInstance();
+        } catch (InstantiationException | IllegalAccessException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+    /**
+     * 查询单个
+     * @param clazz
+     * @param req
+     * @return
+     */
+    default R selectOne(T req, Class<R> clazz) {
+        Map<String, Object> resultMaps = selectOneMap(req, clazz);
+        if (resultMaps != null && !resultMaps.isEmpty()) {
+            return ObjectUtil.mapToBeanProvider(resultMaps, clazz);
+        }
+        try {
+            return clazz.newInstance();
+        } catch (InstantiationException | IllegalAccessException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 求和
+     * @param req
+     * @param clazz
+     * @return
+     */
+    default R sum(T req, Class<R> clazz){
+        Map<String, Object> resultMaps = selectSumMap(req, clazz);
+        if (resultMaps != null && !resultMaps.isEmpty()) {
+            return ObjectUtil.mapToBeanProvider(resultMaps, clazz);
+        }
+        try {
+            return clazz.newInstance();
+        } catch (InstantiationException | IllegalAccessException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 查询列表
+     * @param req
+     * @param clazz
+     * @return
+     */
+    default List<R> selectList(T req, Class<R> clazz) {
+        List<Map<String, Object>> resultMaps = selectListMap(req, clazz);
+        if (resultMaps != null && resultMaps.size() > 0) {
+            return ObjectUtil.mapToListProvider(resultMaps, clazz);
+        }
+        return new ArrayList<>();
+    }
+
+    /**
+     * 分页查询
+     * @param req
+     * @param clazz
+     * @return
+     */
+    default Page<R> selectPage(T req, Class<R> clazz) {
+        MPage<R> mPage = new FinalProviderSql().mPage(req, clazz);
+        long total = pageCount(mPage.getCountSql());
+        List<R> records = null;
+        if (total > 0) {
+            List<Map<String, Object>> pageMaps = page(mPage.getPageSql());
+            records = ObjectUtil.mapToListProvider(pageMaps, clazz);
+        }
+        return new Page<>(total, records).setPage((long) req.getPageNo());
+    }
+
+    /**
+     * 批量保存
+     * @param reqList
+     * @param i
+     * @return
+     */
+    default int saveBatch(List<T> reqList, int i) {
+        //根据 i 进行分批次新增
+        final int[] success = {0};
+        List<List<T>> lists = BeanUtil.batchList(reqList, i);
+        lists.forEach(list -> success[0] = success[0] + saveBatch(list));
+        return success[0];
+    }
+
+    /**
+     * 拼接sql
+     */
+    class FinalProviderSql extends BaseProvider {
+        /**
+         * @param sql
+         * @return
+         */
+        public String pageCountSql(String sql) {
+            return sql;
+        }
+
+        /**
+         * @param sql
+         * @return
+         */
+        public String pageSql(String sql) {
+            return sql;
+        }
+
+        /**
+         * 批量保存
+         * @param object MyBatis传递的参数对象数组,第一个参数是实体对象列表,第二个参数是每批保存的数量
+         * @return 生成的批量插入SQL语句
+         */
+        public String saveBatch(Object object){
+            try {
+                if (object == null) {
+                    throw new ServiceException("批量保存对象不能为空");
+                }
+                List<?> reqList = new ArrayList<>();
+                if (object instanceof Map) {
+                    Map<String, Object> map = (Map<String, Object>) object;
+                    reqList = (List<?>) map.get("reqList");
+                } else {
+                    reqList = (List<?>) object;
+                }
+
+
+                if (reqList.size() == 0) {
+                    throw new ServiceException("参数列表为空");
+                }
+
+                if (reqList == null || reqList.isEmpty()) {
+                    throw new ServiceException("批量保存数据列表不能为空");
+                }
+
+                // 获取列表中的第一个对象来确定实体类
+                Object firstItem = reqList.get(0);
+                Class<?> entityClass = firstItem.getClass();
+
+                // 使用已有的insertAll方法生成批量插入SQL
+                return InsertUtil.insertAll(entityClass, (List) reqList, false);
+            } catch (Exception e) {
+                throw new ServiceException("生成批量保存SQL异常: " + e.getMessage(), e);
+            }
+        }
+    }
+}

+ 27 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/domain/LeftJoinInfo.java

@@ -0,0 +1,27 @@
+package com.poyee.base.mapper.provider.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class LeftJoinInfo {
+
+    public LeftJoinInfo(TableInfo tableInfo, TableInfo leftTableInfo) {
+        this.tableInfo = tableInfo;
+        this.leftTableInfo = leftTableInfo;
+    }
+    // 表信息
+    private TableInfo tableInfo;
+    // 关联表信息
+    private TableInfo leftTableInfo;
+    //关联条件
+    private String fieldName;
+    // 关联字段
+    private String leftFieldName;
+    // 别名
+    private String alias;
+
+}

+ 120 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/domain/MPage.java

@@ -0,0 +1,120 @@
+package com.poyee.base.mapper.provider.domain;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.metadata.OrderItem;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@Data
+public class MPage<T> implements IPage<T> {
+
+    private static final long serialVersionUID = 1L;
+    protected List<T> records;
+    protected long total;
+    protected long size;
+    protected long current;
+    protected List<OrderItem> orders;
+    protected boolean optimizeCountSql;
+    protected boolean searchCount;
+    protected boolean optimizeJoinOfCountSql;
+    protected String countId;
+    protected Long maxLimit;
+
+    protected String countSql;
+
+    protected String pageSql;
+
+    public MPage() {
+        this.records = Collections.emptyList();
+        this.total = 0L;
+        this.size = 10L;
+        this.current = 1L;
+        this.orders = new ArrayList();
+        this.optimizeCountSql = true;
+        this.searchCount = true;
+        this.optimizeJoinOfCountSql = true;
+    }
+    public MPage(long current, long size) {
+        this(current, size, 0L);
+    }
+
+    public MPage(long current, long size, long total) {
+        this(current, size, total, true);
+    }
+
+    public MPage(long current, long size, boolean searchCount) {
+        this(current, size, 0L, searchCount);
+    }
+
+    public MPage(long current, long size, long total, boolean searchCount) {
+        this.records = Collections.emptyList();
+        this.total = 0L;
+        this.size = 10L;
+        this.current = 1L;
+        this.orders = new ArrayList();
+        this.optimizeCountSql = true;
+        this.searchCount = true;
+        this.optimizeJoinOfCountSql = true;
+        if (current > 1L) {
+            this.current = current;
+        }
+
+        this.size = size;
+        this.total = total;
+        this.searchCount = searchCount;
+    }
+
+    @Override
+    public List<OrderItem> orders() {
+        return this.orders;
+    }
+
+    @Override
+    public List<T> getRecords() {
+        return this.records;
+    }
+
+    @Override
+    public IPage<T> setRecords(List<T> records) {
+        this.records = records;
+        return this;
+    }
+
+    @Override
+    public long getTotal() {
+        return this.total;
+    }
+
+    @Override
+    public IPage<T> setTotal(long total) {
+        this.total = total;
+        return this;
+    }
+
+    @Override
+    public long getSize() {
+        return this.size;
+    }
+
+    @Override
+    public IPage<T> setSize(long size) {
+        this.size = size;
+        return this;
+    }
+
+    @Override
+    public long getCurrent() {
+        return this.current;
+    }
+
+    @Override
+    public IPage<T> setCurrent(long current) {
+        this.current = current;
+        return this;
+    }
+
+
+}

+ 16 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/domain/OrderByInfo.java

@@ -0,0 +1,16 @@
+package com.poyee.base.mapper.provider.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class OrderByInfo {
+
+    private Integer index;
+    private String column;
+    private String sortType;
+
+}

+ 24 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/domain/TableInfo.java

@@ -0,0 +1,24 @@
+package com.poyee.base.mapper.provider.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class TableInfo {
+    // 实体类名
+    private String className;
+    // 实体类
+    private Class<?> clazz;
+    // 表名
+    private String tableName;
+    // 别名
+    private String alias;
+    // 索引
+    private Integer idx;
+    // 主键
+    private String primaryKey;
+
+}

+ 99 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/domain/WhereInfo.java

@@ -0,0 +1,99 @@
+package com.poyee.base.mapper.provider.domain;
+
+import com.poyee.enums.FieldOperator;
+import com.poyee.enums.FieldType;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class WhereInfo {
+
+    public WhereInfo(TableInfo tableInfo){
+        this.tableInfo = tableInfo;
+    }
+    // 表信息
+    private TableInfo tableInfo;
+    // 字段
+    private String column;
+    // or 条件
+    private List<String> orColumn;
+    // 值
+    private Object value;
+    // 结束值 (between 时使用)
+    private Object endValue;
+    /**
+     * 操作符
+     * @see FieldOperator
+     */
+    private FieldOperator operator;
+    /**
+     * 字段类型
+     * @see FieldType
+     */
+    private FieldType fieldType;
+    // 是否自定义条件
+    private Boolean isCustom;
+    // 是否是 or 条件
+    private boolean isCustomOr;
+    // 自定义条件
+    private List<CustomConditions> customConditions;
+
+
+    /**
+     *  自定义条件集合
+     */
+    @Data
+    public static class CustomConditions{
+        List<CustomCondition> customConditionList;
+        public void add(CustomCondition customCondition){
+            if(Objects.isNull(customConditionList)){
+                customConditionList = new ArrayList<>();
+            }
+            customConditionList.add(customCondition);
+        }
+    }
+
+    /**
+     *  自定义条件
+     */
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class CustomCondition{
+
+        public CustomCondition(String column, String defaultValue, FieldOperator operator, FieldType fieldType) {
+            this.column = column;
+            this.defaultValue = defaultValue;
+            this.operator = operator;
+            this.fieldType = fieldType;
+        }
+        // 字段
+        private String column;
+        // or 条件
+        private List<String> orColumn;
+        // 值
+        private Object value;
+        // 结束值 (between 时使用)
+        private Object endValue;
+        /**
+         * 操作符
+         * @see FieldOperator
+         */
+        private FieldOperator operator;
+        /**
+         * 字段类型
+         * @see FieldType
+         */
+        private FieldType fieldType;
+        // 默认值
+        private String defaultValue;
+    }
+
+}

+ 154 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/util/FormatValueUtil.java

@@ -0,0 +1,154 @@
+package com.poyee.base.mapper.provider.util;
+
+import com.poyee.base.mapper.provider.domain.WhereInfo;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
+import static com.poyee.annotation.MpjWapper.Operator.ISNOTNULL;
+
+/**
+ *  格式化值工具类
+ */
+@Slf4j
+public class FormatValueUtil {
+
+    /**
+     * 构建 IN 或 NOT IN 子句。
+     *
+     * @param valueObj WHERE 信息
+     * @return 构建的 IN 或 NOT IN 子句
+     */
+    private static String buildInClause(Object valueObj) {
+        // 如果值为 null,则返回 "NULL"
+        if (valueObj == null) {
+            return "NULL";
+        }
+        StringBuilder inClause = new StringBuilder("(");
+        // 如果值是字符串类型,则按逗号分割成多个值
+        if (valueObj instanceof String) {
+            String[] values = ((String) valueObj).split(",");
+            for (String value : values) {
+                // 对每个值进行空值和空字符串检查,并构建 IN 子句
+                if (value != null && !value.trim().isEmpty()) {
+                    inClause.append("'").append(escapeValue(value.trim())).append("',");
+                }
+            }
+        } else if (valueObj instanceof List) {
+            // 如果值是 List 类型,则遍历列表中的每个值
+            for (Object value : (List<?>) valueObj) {
+                // 对每个值进行空值检查,并构建 IN 子句
+                if (value != null) {
+                    inClause.append("'").append(escapeValue(value)).append("',");
+                }
+            }
+        } else if (valueObj instanceof Object[]) {
+            // 如果值是数组类型,则遍历数组中的每个值
+            for (Object value : (Object[]) valueObj) {
+                // 对每个值进行空值检查,并构建 IN 子句
+                if (value != null) {
+                    inClause.append("'").append(escapeValue(value)).append("',");
+                }
+            }
+        }
+        // 如果 IN 子句长度大于 1,则移除最后的逗号
+        if (inClause.length() > 1) {
+            inClause.setLength(inClause.length() - 1);
+        }
+        inClause.append(")");
+        return inClause.toString();
+    }
+
+    /**
+     * @param value
+     * @param endValue
+     * @return
+     */
+    private static String buildBetweenClause(Object value, Object endValue) {
+        return "'" + escapeValue(value) + "' AND '" + escapeValue(endValue) + "'";
+    }
+    /**
+     * 根据操作符格式化字符串值。
+     *
+     * @param whereInfo WHERE 信息
+     * @return 格式化后的字符串值
+     */
+    public static String formatStringValue(WhereInfo whereInfo) {
+        // 根据不同的操作符格式化字符串值
+        switch (whereInfo.getOperator()) {
+            case LIKE:
+                // 对于LIKE操作符,前后添加百分号并转义值
+                return "'%" + escapeValue(whereInfo.getValue()) + "%'";
+            case ILIKE:
+                // 对于ILIKE操作符,前后添加百分号并转义值
+                return "'%" + escapeValue(whereInfo.getValue()) + "%'";
+            case LLIKE:
+                // 对于LLIKE操作符,前面添加百分号并转义值
+                return "'%" + escapeValue(whereInfo.getValue()) + "'";
+            case RLIKE:
+                // 对于RLIKE操作符,后面添加百分号并转义值
+                return "'" + escapeValue(whereInfo.getValue()) + "%'";
+            case IN:
+            case NOTIN:
+                // 对于IN和NOTIN操作符,调用buildInClause方法构建IN子句
+                return buildInClause(whereInfo.getValue());
+            case BETWEEN:
+            case NOTBETWEEN:
+                // 对于IN和NOTIN操作符,调用buildInClause方法构建IN子句
+                return buildBetweenClause(whereInfo.getValue(),whereInfo.getEndValue());
+            default:
+                // 对于其他操作符,直接转义值并添加单引号
+                return "'" + escapeValue(whereInfo.getValue()) + "'";
+        }
+    }
+    /**
+     * 根据操作符格式化字符串值。
+     *
+     * @param condition WHERE 信息
+     * @return 格式化后的字符串值
+     */
+    public static String formatStringValue(WhereInfo.CustomCondition condition) {
+        // 根据不同的操作符格式化字符串值
+        switch (condition.getOperator()) {
+            case LIKE:
+                // 对于LIKE操作符,前后添加百分号并转义值
+                return "'%" + escapeValue(condition.getValue()) + "%'";
+            case ILIKE:
+                // 对于ILIKE操作符,前后添加百分号并转义值
+                return "'%" + escapeValue(condition.getValue()) + "%'";
+            case LLIKE:
+                // 对于LLIKE操作符,前面添加百分号并转义值
+                return "'%" + escapeValue(condition.getValue()) + "'";
+            case RLIKE:
+                // 对于RLIKE操作符,后面添加百分号并转义值
+                return "'" + escapeValue(condition.getValue()) + "%'";
+            case IN:
+            case NOTIN:
+                // 对于IN和NOTIN操作符,调用buildInClause方法构建IN子句
+                return buildInClause(condition.getValue());
+            case BETWEEN:
+            case NOTBETWEEN:
+                // 对于IN和NOTIN操作符,调用buildInClause方法构建IN子句
+                return buildBetweenClause(condition.getValue(), condition.getEndValue());
+            case ISNOTNULL:
+            case ISNULL:
+                return "";
+            default:
+                // 对于其他操作符,直接转义值并添加单引号
+                return "'" + escapeValue(condition.getValue()) + "'";
+        }
+    }
+    /**
+     * 转义特殊字符以防止 SQL 注入。
+     *
+     * @param value 值
+     * @return 转义后的值
+     */
+    public static String escapeValue(Object value) {
+        if (value == null) {
+            return "NULL";
+        }
+        return String.valueOf(value).replace("'", "''");
+    }
+
+}

+ 224 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/util/InsertUtil.java

@@ -0,0 +1,224 @@
+package com.poyee.base.mapper.provider.util;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.poyee.base.mapper.provider.BaseProvider;
+import com.poyee.common.exception.ServiceException;
+import com.poyee.enums.DbMethodEnums;
+import com.poyee.util.DateUtils;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.Field;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static com.poyee.base.mapper.provider.ErrMessageContent.*;
+
+@Slf4j
+public class InsertUtil {
+
+    public static final Integer maxBaseCount = 200;
+
+    private BaseProvider parent;
+
+    private InsertUtil(BaseProvider parent) {
+        this.parent = parent;
+    }
+
+    public static InsertUtil init(BaseProvider parent) {
+        return new InsertUtil(parent);
+    }
+
+   /**
+      * 生成批量插入SQL
+      * @param clazz 实体类
+      * @param blist 实体对象列表
+      * @param b 是否忽略空字段(可根据实际需求调整)
+      * @param <T> 泛型
+      * @return 批量插入SQL
+      */
+     public static <T> String insertAll(Class<T> clazz, List<T> blist, boolean b) {
+         StringBuilder sql = new StringBuilder();
+         if (blist == null || blist.isEmpty()) {
+             log.warn("批量插入数据不能为空");
+             throw new ServiceException("批量插入数据不能为空");
+         }
+
+         // 获取表名
+         TableName tableNameAnnotation = clazz.getAnnotation(TableName.class);
+         if (tableNameAnnotation == null || tableNameAnnotation.value().trim().isEmpty()) {
+             throw new ServiceException("实体类缺少@TableName注解或表名为空");
+         }
+         String tableName = tableNameAnnotation.value();
+
+         // 获取所有字段
+         Field[] fields = clazz.getDeclaredFields();
+         StringBuilder columns = new StringBuilder();
+         List<String> columnList = new java.util.ArrayList<>();
+         for (Field field : fields) {
+             if (field.isAnnotationPresent(TableField.class)) {
+                 TableField tableField = field.getAnnotation(TableField.class);
+                 if (tableField.exist()) {
+                     columnList.add(tableField.value());
+                 }
+             }
+         }
+         if (columnList.isEmpty()) {
+             throw new ServiceException("未找到可插入的字段");
+         }
+         columns.append(String.join(", ", columnList));
+
+         sql.append("INSERT INTO ").append(tableName)
+                 .append(" (").append(columns).append(") VALUES ");
+
+         for (int i = 0; i < blist.size(); i++) {
+             T obj = blist.get(i);
+             sql.append("(");
+             for (int j = 0; j < columnList.size(); j++) {
+                 String col = columnList.get(j);
+                 try {
+                     Field field = null;
+                     for (Field f : fields) {
+                         if (f.isAnnotationPresent(TableField.class)) {
+                             TableField tf = f.getAnnotation(TableField.class);
+                             if (tf.value().equals(col)) {
+                                 field = f;
+                                 break;
+                             }
+                         }
+                     }
+                     if (field == null) {
+                         sql.append("NULL");
+                     } else {
+                         field.setAccessible(true);
+                         Object value = field.get(obj);
+                         if (value == null) {
+                             sql.append("NULL");
+                         } else if (value instanceof String || value instanceof java.sql.Date || value instanceof Date) {
+                             sql.append("'").append(value.toString().replace("'", "''")).append("'");
+                         } else {
+                             sql.append(value);
+                         }
+                     }
+                 } catch (IllegalAccessException e) {
+                     throw new ServiceException("字段访问异常", e);
+                 }
+                 if (j < columnList.size() - 1) {
+                     sql.append(", ");
+                 }
+             }
+             sql.append(")");
+             if (i < blist.size() - 1) {
+                 sql.append(", ");
+             }
+         }
+         return sql.toString();
+     }
+
+    /**
+     * 检查插入对象是否合法
+     *
+     * @param object 插入对象
+     * @return 自身实例
+     */
+    public InsertUtil checkInsertBean(Object object) {
+        if (object == null) {
+            log.warn("对象不能为空");
+            throw new ServiceException(ERROR_MESSAGE_OBJECT_NULL);
+        }
+        Class<?> aClass = object.getClass();
+        if (!aClass.isAnnotationPresent(TableName.class)) {
+            log.warn("对象必须包含@TableName注解");
+            throw new ServiceException(ERROR_MESSAGE_TABLE_NAME_MISSING);
+        }
+        TableName tableNameAnnotation = aClass.getAnnotation(TableName.class);
+        String tableName = tableNameAnnotation.value();
+        if (tableName == null || tableName.trim().isEmpty()) {
+            log.warn("表名不能为空");
+            throw new ServiceException(ERROR_MESSAGE_TABLE_NAME_IS_NULL);
+        }
+
+        // 初始化表信息
+        TableInfoUtil.init(parent).initTableInfo(aClass);
+        parent.superTable = parent.tableInfos.get(aClass.getName());
+
+        parent.insert = object;
+        return this;
+    }
+
+    /**
+     * 初始化插入操作
+     */
+    public void initInsert() {
+        Object insert = parent.insert;
+        if (insert == null || parent.insertMap == null) {
+            log.warn("插入对象或映射为空,无法生成SQL。对象: {}", insert);
+            throw new ServiceException(getErrorMessage(ERROR_MESSAGE_BEAN_NULL,"插入"));
+        }
+
+        try {
+            Map<String, Object> insertMap = parent.insertMap;
+            insertMap.clear(); // 清空旧数据
+            processFields(insert, insertMap);
+
+            if (insertMap.isEmpty()) {
+                log.warn("没有有效的插入字段,对象: {}", insert);
+                throw new ServiceException(getErrorMessage(ERROR_MESSAGE_OPERA_NULL,"插入"));
+            }
+        } catch (IllegalAccessException e) {
+            log.error("访问字段时发生错误", e);
+            throw new ServiceException(ERROR_MESSAGE_FIELD_ACCESS);
+        } catch (IllegalArgumentException e) {
+            log.error("非法参数错误", e);
+            throw new ServiceException(ERROR_MESSAGE_ILLEGAL_ARGUMENT);
+        } catch (Exception e) {
+            log.error(ERROR_MESSAGE_BUILD_SQL, e);
+            throw new ServiceException(ERROR_MESSAGE_BUILD_SQL);
+        }
+    }
+
+    /**
+     * 处理字段并填充到映射中
+     *
+     * @param object    对象
+     * @param insertMap 插入映射
+     * @throws IllegalAccessException 如果字段访问失败
+     */
+    private void processFields(Object object, Map<String, Object> insertMap) throws IllegalAccessException {
+        Class<?> aClass = object.getClass();
+        Field[] declaredFields = aClass.getDeclaredFields();
+
+        for (Field field : declaredFields) {
+            field.setAccessible(true);
+            if (field.isAnnotationPresent(TableField.class)) {
+                TableField tableField = field.getAnnotation(TableField.class);
+                if (tableField.exist()) {
+                    Object value = field.get(object);
+                    if ( Objects.nonNull(value)) {
+                        if(value instanceof Date){
+                            insertMap.put(tableField.value(), DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, (Date) value));
+                        }else{
+                            insertMap.put(tableField.value(), value);
+                        }
+                    }
+                }
+                //判断是否需要设置 创建,更新时间
+                if ( Objects.equals(parent.dbMethod, DbMethodEnums.INSERT)
+                        && Objects.equals(tableField.fill() , FieldFill.INSERT_UPDATE)
+                        && Objects.equals(field.getName(), "updateTime")
+                ) {
+                    insertMap.put(tableField.value(), new Date());
+                }
+                if ( Objects.equals(parent.dbMethod, DbMethodEnums.INSERT)
+                        && Objects.equals(tableField.fill() , FieldFill.INSERT)
+                        && Objects.equals(field.getName(), "createTime")
+                ) {
+                    insertMap.put(tableField.value(), new Date());
+                }
+            }
+        }
+    }
+}

+ 76 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/util/JoinUtil.java

@@ -0,0 +1,76 @@
+package com.poyee.base.mapper.provider.util;
+
+import com.poyee.annotation.db.LeftJoin;
+import com.poyee.base.mapper.provider.BaseProvider;
+import com.poyee.base.mapper.provider.domain.LeftJoinInfo;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Objects;
+
+@Slf4j
+public class JoinUtil {
+
+    public static JoinUtil init(BaseProvider baseProvider){
+        return new JoinUtil(baseProvider);
+    }
+
+    private JoinUtil(BaseProvider baseProvider){
+        this.parent = baseProvider;
+    }
+    private BaseProvider parent;
+
+
+    /**
+     * 初始化左连接信息
+     *
+     * @param aClass 实体类类对象,用于获取表信息
+     * @param leftJoinInfos 左连接信息列表,存储处理后的左连接信息
+     * @param declaredField 声明字段,检查是否包含LeftJoin注解
+     */
+    public void initLeftJoin(Class<?> aClass, List<LeftJoinInfo> leftJoinInfos, Field declaredField) {
+        // 检查字段是否包含LeftJoin注解
+        if (declaredField.isAnnotationPresent(LeftJoin.class)) {
+            // 获取字段上的LeftJoin注解
+            LeftJoin leftJoin = declaredField.getAnnotation(LeftJoin.class);
+            // 创建一个新的左连接信息对象
+            LeftJoinInfo leftJoinInfo = new LeftJoinInfo();
+
+            // 获取注解中指定的连接表类
+            Class<?> table = leftJoin.table();
+            // 如果父对象的表信息映射中不包含此表,初始化该表的信息
+            if (!parent.tableInfos.containsKey(table.getName())) {
+                TableInfoUtil.init(parent).initTableInfo(table);
+            }
+            // 设置左连接信息中的表信息和别名
+            leftJoinInfo.setTableInfo(parent.tableInfos.get(table.getName()));
+            leftJoinInfo.setAlias(parent.tableInfos.get(table.getName()).getAlias());
+            //判断 左连接 表中是否已存在 leftJoinInfos
+            if (CollectionUtils.isNotEmpty(leftJoinInfos)) {
+                boolean b = leftJoinInfos.stream()
+                                         .anyMatch(item -> Objects.equals(item.getTableInfo().getAlias(), leftJoinInfo.getTableInfo().getAlias()));
+                if(b){ //如果已存在则返回
+                    return ;
+                }
+            }
+            // 确定左表,如果注解中未指定则使用传入的实体类
+            Class<?> leftTable = leftJoin.leftTable() != void.class ? leftJoin.leftTable() : aClass;
+            // 如果父对象的表信息映射中不包含左表,初始化左表的信息
+            if (!parent.tableInfos.containsKey(leftTable.getName())) {
+                TableInfoUtil.init(parent).initTableInfo(leftTable);
+            }
+            // 设置左连接信息中的左表信息
+            leftJoinInfo.setLeftTableInfo(parent.tableInfos.get(leftTable.getName()));
+
+            // 设置左连接信息中的字段名和左表字段名
+            leftJoinInfo.setFieldName(leftJoin.fieldName());
+            leftJoinInfo.setLeftFieldName(leftJoin.leftFieldName());
+
+            // 将处理后的左连接信息添加到列表中
+            leftJoinInfos.add(leftJoinInfo);
+        }
+    }
+
+}

+ 368 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/util/SelectUtil.java

@@ -0,0 +1,368 @@
+package com.poyee.base.mapper.provider.util;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.poyee.annotation.I18n;
+import com.poyee.annotation.db.*;
+import com.poyee.base.mapper.provider.BaseProvider;
+import com.poyee.base.mapper.provider.domain.MPage;
+import com.poyee.base.mapper.provider.domain.TableInfo;
+import com.poyee.base.mapper.provider.domain.WhereInfo;
+import com.poyee.common.exception.ServiceException;
+import com.poyee.enums.FieldOperator;
+import com.poyee.util.Convert;
+import com.poyee.util.ServletUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static com.poyee.enums.DbMethodEnums.SELECT_BY_ID;
+
+@Slf4j
+public class SelectUtil {
+
+    private BaseProvider parent;
+
+    private SelectUtil(BaseProvider baseProvider) {
+        this.parent = baseProvider;
+    }
+
+    public static SelectUtil init(BaseProvider baseProvider) {
+        return new SelectUtil(baseProvider);
+    }
+    /**
+     * @param object
+     * @return
+     */
+    @NotNull
+    public SelectUtil checkSelectBean(Object object) {
+        Object param;
+        Object select = null; // return class
+        // 判断 object 是否是 Map
+        if (object instanceof Map) {
+            Map<String, Object> map = (Map<String, Object>) object;
+            select = map.get("clazz");
+            if(Objects.equals(parent.dbMethod , SELECT_BY_ID)){
+                param = map.get("id");
+            }else{
+                param = map.get("req");
+            }
+        } else {
+            param = object;
+        }
+        // 如果无法定位返回值,则抛出异常
+        if (Objects.isNull(select)) {
+            throw new ServiceException("无法定位返回值!");
+        }
+        if(Objects.equals(parent.dbMethod , SELECT_BY_ID)){
+            //组装 select by id
+            return this.checkSelectOneBean(param, select);
+        }
+        // 获取参数对象的类信息
+        Class<?> aClass = param.getClass();
+        // 初始化表信息
+        TableInfoUtil.init(parent).initTableInfo(aClass);
+        parent.superTable = parent.tableInfos.get(aClass.getName());
+        // 获取超类的所有字段处理分页信息
+        if(parent.isPage) {
+            long page = 0;
+            long size = 10;
+            Class<?> superclass = aClass.getSuperclass();
+            try {
+                Field pageNoFields = superclass.getDeclaredField("pageNo");
+                pageNoFields.setAccessible(true);
+                page = Optional.ofNullable(Convert.toLong(pageNoFields.get(param)))
+                               .orElse(0L);
+                Field pageSizeFields = superclass.getDeclaredField("pageSize");
+                pageSizeFields.setAccessible(true);
+                size = Optional.ofNullable(Convert.toLong(pageSizeFields.get(param)))
+                               .orElse(10L);
+            } catch (NoSuchFieldException | IllegalAccessException e) {
+                throw new RuntimeException(e);
+            }
+            //设置 分页信息
+            parent.mPage = new MPage(page, size);
+            try {
+                //处理排序字段
+                Field sidxField = superclass.getDeclaredField("sidx");
+                sidxField.setAccessible(true);
+                Object sidxObj = sidxField.get(param);
+                if(Objects.nonNull(sidxObj) && StringUtils.isNotBlank(String.valueOf(sidxObj))){
+                    parent.isFrontSort = true;
+                    Field sortField = superclass.getDeclaredField("sord");
+                    sortField.setAccessible(true);
+                    String sort = "asc";
+                    if(Objects.nonNull(sortField.get(param))){
+                        sort = sortField.get(param).toString();
+                    }
+                    String alias = parent.superTable.getAlias();
+                    parent.orderByMap.put(Objects.equals(sort,"desc") ? -1 : 0 ,alias +"."+ String.valueOf(sidxObj));
+                }
+            }catch (NoSuchFieldException | IllegalAccessException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        //设置 排序信息
+        Field[] declaredFields = aClass.getDeclaredFields();
+        for (Field declaredField : declaredFields) {
+            declaredField.setAccessible(true);
+            // 构建 WHERE 条件
+            WhereUtil.init(parent).build(param, aClass, parent.whereInfos, declaredField);
+            // 构建 LEFT JOIN 信息
+            JoinUtil.init(parent).initLeftJoin(aClass, parent.leftJoinInfos, declaredField);
+        }
+        parent.select = select;
+        return this;
+    }
+
+    /**
+     * 检查选择对象
+     * @param param
+     * @return
+     */
+    private SelectUtil checkSelectOneBean(Object param, Object select) {
+        // 获取参数对象的类信息
+        Class<?> aClass;
+        if(select instanceof Class<?>){
+            aClass = (Class<?>) select;
+        }else{
+            aClass = select.getClass();
+        }
+        // 初始化表信息
+        TableInfoUtil.init(parent).initTableInfo(aClass);
+        parent.superTable = parent.tableInfos.get(aClass.getName());
+        parent.select = select;
+        //获取 主键
+        String column = "id";
+        Field[] declaredFields = aClass.getDeclaredFields();
+        for (Field declaredField : declaredFields) {
+            declaredField.setAccessible(true);
+            if(declaredField.isAnnotationPresent(TableId.class)){
+                TableId tableId = declaredField.getAnnotation(TableId.class);
+                column = tableId.value();
+            }else{
+                // 构建 LEFT JOIN 信息
+                JoinUtil.init(parent).initLeftJoin(aClass, parent.leftJoinInfos, declaredField);
+            }
+        }
+        //设置条件
+        WhereInfo whereInfo = new WhereInfo(parent.superTable);
+        whereInfo.setColumn(column);
+        whereInfo.setValue(param);
+        whereInfo.setOperator(FieldOperator.EQ);
+        parent.whereInfos.add(whereInfo);
+        return this;
+    }
+
+    /**
+     * 初始化选择对象的方法
+     * 该方法用于处理传入的选择对象,提取其字段信息,并根据注解确定查询列
+     *
+     * @param select 选择对象,可以是任何类,但通常用于具有@TableField和@Select注解的类
+     */
+    public void initSelect(Object select) {
+        // 检查选择对象是否非空
+        if (Objects.isNull(select)) {
+            log.warn("初始化选择对象失败,传入的对象为 null");
+            throw new ServiceException("初始化选择对象失败,传入的对象为 null");
+        }
+
+        try {
+            // 获取选择对象的实际类信息
+            Class<?> clazz = (Class<?>) select;
+            // 获取类的所有声明的字段
+            Field[] declaredFields = clazz.getDeclaredFields();
+            // 遍历字段
+            for (Field field : declaredFields) {
+                // 设置字段可访问,以处理私有字段
+                field.setAccessible(true);
+                //根据国际语言判断字段是否进行查询
+                if (field.isAnnotationPresent(I18n.class) && !ServletUtils.isI18nSupport()) {
+                    continue;
+                }
+                if (!field.isAnnotationPresent(Select.class)
+                        && (field.isAnnotationPresent(TableField.class) || field.isAnnotationPresent(TableId.class))) {
+                    // 检查字段是否包含TableField注解
+                    processTableFieldAnnotation(field);
+                } else if (field.isAnnotationPresent(Select.class)) {
+                    processSelectAnnotation(field);
+                }
+            }
+        } catch (Exception e) {
+            log.error("初始化选择对象时发生异常", e);
+        }
+    }
+
+    /**
+     * 处理 TableField 注解的逻辑
+     *
+     * @param field 字段对象
+     */
+    private void processTableFieldAnnotation(Field field) {
+        try {
+            boolean isDistinct = field.isAnnotationPresent(Distinct.class);
+            TableInfo superTable = parent.superTable;
+            TableId tableId = field.getAnnotation(TableId.class);
+            if (Objects.nonNull(tableId)) {
+                String selectSql = "";
+                if(isDistinct){
+                    selectSql = "DISTINCT " + superTable.getAlias() + "." + tableId.value() + " AS " + field.getName();
+                }else{
+                    selectSql = superTable.getAlias() + "." + tableId.value() + " AS " + field.getName();
+                }
+                checkSelectSql(selectSql, field, superTable, tableId.value());
+            }
+            TableField tableField = field.getAnnotation(TableField.class);
+            if (Objects.nonNull(tableField) && tableField.exist()) {
+                String selectSql;
+                if(field.isAnnotationPresent(Sum.class)){
+                    Sum sum = field.getAnnotation(Sum.class);
+                    CaseWhen[] caseWhens = sum.caseWhen();
+                    String sumFieldSql = "";
+                    if(caseWhens.length > 0){
+                        sumFieldSql = buildCaseWhen( superTable, caseWhens) ;
+                    } else {
+                        sumFieldSql = superTable.getAlias() + "." + tableField.value();
+                    }
+                    selectSql = "COALESCE(SUM(" + sumFieldSql + "),0)" + " AS " + field.getName();
+                } else if(field.isAnnotationPresent(Count.class)) {
+                    Count count = field.getAnnotation(Count.class);
+                    CaseWhen[] caseWhens = count.caseWhen();
+                    String countFieldSql = "";
+                    if(caseWhens.length > 0){
+                        countFieldSql = (count.distinct()?"DISTINCT":"")+" "+buildCaseWhen( superTable, caseWhens);
+                    }else{
+                        countFieldSql = (count.distinct()?"DISTINCT":"")+" "+superTable.getAlias() + "." + tableField.value();
+                    }
+                    selectSql = "COALESCE(COUNT( "+countFieldSql+"),0)" + " AS " + field.getName();
+                } else {
+                    if(isDistinct){
+                        selectSql = "DISTINCT " + superTable.getAlias() + "." + tableField.value() + " AS " + field.getName();
+                    }else{
+                        selectSql = superTable.getAlias() + "." + tableField.value() + " AS " + field.getName();
+                    }
+//                    selectSql = superTable.getAlias() + "." + tableField.value() + " AS " + field.getName();
+                }
+                checkSelectSql(selectSql, field, superTable, tableField.value());
+            }
+        } catch (Exception e) {
+            log.error("处理 TableField 注解时发生异常", e);
+        }
+    }
+
+    /**
+     * @param superTable
+     * @param caseWhens
+     * @return
+     */
+    private String buildCaseWhen(TableInfo superTable, CaseWhen[] caseWhens) {
+        StringBuilder caseSql = new StringBuilder();
+        StringBuilder elseStr = new StringBuilder();
+        AtomicBoolean isFirst = new AtomicBoolean(true);
+        Arrays.asList(caseWhens).forEach(caseWhen -> {
+            if (caseWhen.isElse()) {
+                elseStr.append(" ELSE ").append(caseWhen.then());
+            }else{
+                // 判断是否是第一个条件
+                if(isFirst.get()){
+                    caseSql.append(" CASE WHEN ");
+                    isFirst.set(false);
+                } else {
+                    caseSql.append(" WHEN ");
+                }
+                //  WHEN 条件
+                // 判断条件 - key
+                caseSql.append(superTable.getAlias() + "." + caseWhen.when());
+                //  THEN 值
+                caseSql.append(" THEN ");
+                // 判断值类型
+                switch (caseWhen.valueType()){
+                    case TABLE_COLUMN:
+                        caseSql.append(superTable.getAlias() + "." + caseWhen.then());
+                        break;
+                    case CONSTANT:
+                    case SQL_EXPRESSION:
+                        caseSql.append(caseWhen.then());
+                        break;
+                }
+            }
+        });
+        caseSql.append(elseStr.append(" END "));
+        String selectSql = caseSql.toString();
+        log.info("构建CASE WHEN SQL: {}", caseSql);
+        return selectSql;
+    }
+
+    /**
+     * 检查选择SQL
+     * @param selectSql
+     * @param field
+     * @param tableInfo
+     * @param fieldName
+     */
+    private void checkSelectSql(String selectSql, Field field, TableInfo tableInfo, String fieldName) {
+        parent.selectColumns.add(selectSql);
+        if (!parent.isFrontSort && field.isAnnotationPresent(OrderBy.class)) {
+            OrderBy orderBy = field.getAnnotation(OrderBy.class);
+            parent.orderByMap.put(orderBy.index(), tableInfo.getAlias() + "." + fieldName + " " + orderBy.sortType().getValue());
+        }
+        if (parent.isFrontSort && (parent.orderByMap.containsKey(-1) || parent.orderByMap.containsKey(0))) {
+            int kk = parent.orderByMap.containsKey(-1) ? -1 : 0;
+            if (Objects.equals(field.getName(), parent.orderByMap.get(kk))
+                    || Objects.equals(tableInfo.getAlias() + "." + field.getName(), parent.orderByMap.get(kk))) {
+                parent.orderByMap.put(kk, tableInfo.getAlias() + "." + fieldName + " " + (kk == -1 ? "desc" : "asc"));
+            }
+        }
+    }
+
+    /**
+     * 处理 Select 注解的逻辑
+     *
+     * @param field 字段对象
+     */
+    private void processSelectAnnotation(Field field) {
+        try {
+            Select selectAnnotation = field.getAnnotation(Select.class);
+            boolean isDistinct = field.isAnnotationPresent(Distinct.class) || selectAnnotation.distinct() ;
+            if (Objects.nonNull(selectAnnotation.table())) {
+                String tableName = selectAnnotation.table()
+                                                   .getName();
+                TableInfo tableInfo = parent.tableInfos.get(tableName);
+                if(Objects.isNull(tableInfo)){
+                    //如果未查到 但有别名则使用别名进行匹配
+                    String alias = selectAnnotation.alias();
+                    if(StringUtils.isNotBlank(alias)){
+                        tableInfo = TableInfoUtil.init(parent).initTableInfoByAlias(selectAnnotation.table(), alias);
+                    } else {
+                        TableInfoUtil.init(parent).initTableInfo(selectAnnotation.table());
+                        tableInfo = parent.tableInfos.get(tableName);
+                    }
+                }
+                if (Objects.nonNull(tableInfo)) {
+                    String selectSql = "";
+                    //判断 distinct
+                    if(isDistinct){
+                        selectSql = "DISTINCT " + tableInfo.getAlias() + "." + selectAnnotation.fieldName() + " AS " + field.getName();
+                    }else{
+                        selectSql = tableInfo.getAlias() + "." + selectAnnotation.fieldName() + " AS " + field.getName();
+                    }
+                    checkSelectSql(selectSql, field, tableInfo, selectAnnotation.fieldName());
+//                    parent.selectColumns.add(selectSql);
+                } else {
+                    log.warn("未找到表名 {} 对应的 TableInfo 对象", tableName);
+                }
+            }
+        } catch (Exception e) {
+            log.error("处理 Select 注解时发生异常", e);
+        }
+    }
+
+}

+ 641 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/util/SqlUtil.java

@@ -0,0 +1,641 @@
+package com.poyee.base.mapper.provider.util;
+
+import com.poyee.base.mapper.provider.BaseProvider;
+import com.poyee.base.mapper.provider.domain.LeftJoinInfo;
+import com.poyee.base.mapper.provider.domain.WhereInfo;
+import com.poyee.common.exception.ServiceException;
+import com.poyee.enums.DbMethodEnums;
+import com.poyee.enums.FieldOperator;
+import com.poyee.enums.FieldType;
+import com.poyee.enums.SqlEnums;
+import com.poyee.util.ObjectUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ *  SQL 生成工具类
+ */
+@Slf4j
+public class SqlUtil {
+    // SQL 类型
+    private SqlEnums type = SqlEnums.all;
+    // 父对象
+    private BaseProvider parent;
+
+    public static SqlUtil init(BaseProvider baseProvider){
+        return new SqlUtil( baseProvider);
+    }
+
+    /**
+     * @return
+     */
+    public SqlUtil buildPage(){
+        this.type = SqlEnums.page;
+        return this;
+    }
+
+    /**
+     * @param baseProvider
+     */
+    private SqlUtil(BaseProvider baseProvider){
+        this.parent = baseProvider;
+    }
+
+    /**
+     * 根据已收集的信息生成最终的 SQL 字符串。
+     *
+     * @return 生成的 SQL 字符串
+     */
+    public String toSql() {
+        String sqlString = "";
+        switch (parent.dbMethod){
+            case SELECT_BY_ID:
+            case SELECT:
+                sqlString = buildSelectSql();
+                break;
+            case INSERT:
+                sqlString = buildInsertSql();
+                break;
+            case UPDATE:
+                sqlString = buildUpdateSql();
+                break;
+            default:
+                log.warn("No select columns or parent is null, returning empty SQL.");
+                throw new ServiceException("No select columns or parent is null.");
+        }
+        return sqlString;
+    }
+
+    /**
+     * 构建插入 SQL 语句
+     *
+     * @return 插入 SQL 语句
+     */
+    private String buildInsertSql() {
+        // 非空检查
+        if (parent == null || parent.insertMap == null || parent.insertMap.isEmpty()) {
+            log.warn("Parent or insert map is null or empty, returning empty SQL.");
+            throw new ServiceException("Parent or insert map is null or empty.");
+        }
+
+        if (parent.superTable == null || parent.superTable.getTableName() == null || parent.superTable.getTableName().isEmpty()) {
+            log.warn("Super table or table name is null or empty, returning empty SQL.");
+            throw new ServiceException("Super table or table name is null or empty.");
+        }
+
+        // 获取键和值列表并进行安全检查
+        List<String> columns = new ArrayList<>(parent.insertMap.keySet());
+        List<Object> values = new ArrayList<>(parent.insertMap.values());
+
+        if (columns.isEmpty() || values.isEmpty()) {
+            log.warn("Insert map contains no valid columns or values, returning empty SQL.");
+            throw new ServiceException("Insert map contains no valid columns or values.");
+        }
+
+        // 校验列名和值是否合法
+        for (String column : columns) {
+            if (column == null || column.isEmpty()) {
+                log.warn("Invalid column name: {}", column);
+                throw new ServiceException("Invalid column name in insert map.");
+            }
+        }
+
+        try {
+            // 构建 SQL 字符串
+            StringBuilder sql = new StringBuilder();
+            sql.append("INSERT INTO ")
+               .append(parent.superTable.getTableName())
+               .append(" (")
+               .append(columns.stream().map(ObjectUtil::escapeIdentifier).collect(Collectors.joining(", "))) // 转义列名
+               .append(") VALUES (")
+               .append(ObjectUtil.join(values)) // 转义值
+               .append(")");
+
+            // 格式化并记录日志
+            String sqlString = sql.toString().trim();
+            log.info("Generated SQL: {}", sqlString); // 记录生成的 SQL
+            return sqlString;
+
+        } catch (Exception e) {
+            log.error("Error building insert SQL. Parent: {}, Columns: {}, Values: {}", parent, columns, values, e);
+            throw new ServiceException("Error building insert SQL.", e);
+        }
+    }
+
+    /**
+     * 构建更新 SQL 语句
+     * @return
+     */
+    @NotNull
+    private String buildUpdateSql(){
+        // 非空检查
+        if (parent == null || parent.updateMap == null || parent.updateMap.isEmpty()) {
+            log.warn("Parent or update map is null or empty, returning empty SQL.");
+            throw new ServiceException("Parent or update map is null or empty.");
+        }
+
+        if (parent.superTable == null || parent.superTable.getTableName() == null || parent.superTable.getTableName().isEmpty()) {
+            log.warn("Super table or table name is null or empty, returning empty SQL.");
+            throw new ServiceException("Super table or table name is null or empty.");
+        }
+        //取出 id
+        Object value = parent.updateMap.get("id");
+        Object idValue = parent.updateMap.remove("id");
+
+        // 获取键和值列表并进行安全检查
+        List<String> columns = new ArrayList<>(parent.updateMap.keySet());
+        List<Object> values = new ArrayList<>(parent.updateMap.values());
+
+        if (columns.isEmpty() || values.isEmpty()) {
+            log.warn("Update map contains no valid columns or values, returning empty SQL.");
+            throw new ServiceException("Update map contains no valid columns or values.");
+        }
+
+        for (String column : columns) {
+            if (column == null || column.isEmpty()) {
+                log.warn("Invalid column name: {}", column);
+                throw new ServiceException("Invalid column name in update map.");
+            }
+        }
+
+        try {
+            // 构建 SQL 字符串
+            StringBuilder sql = new StringBuilder();
+            sql.append("UPDATE ")
+               .append(parent.superTable.getTableName())
+               .append(" SET ");
+            for (int i = 0; i < columns.size(); i++) {
+                sql.append(columns.get(i)).append(" = ");
+                if((values.get(i) instanceof Date)){
+                    sql.append("'").append(ObjectUtil.escapeValue(values.get(i))).append("'");
+                }else{
+                    sql.append(ObjectUtil.escapeValue(values.get(i))); // 转义值
+                }
+                 sql.append(i < columns.size() - 1 ? ", " : " ");
+            }
+            //判断where 是否有值
+            if (!parent.whereInfos.isEmpty()) {
+                appendWhereClauses(sql);
+            } else {
+                sql.append("WHERE ")
+                   .append(parent.superTable.getPrimaryKey())
+                   .append(" = ")
+                   .append(ObjectUtil.escapeValue(value));
+            }
+            //判断是否有条件
+            if(!sql.toString().contains(" WHERE ")){
+                log.warn("No where condition found.");
+                throw new ServiceException("No where condition found.");
+            }
+            return sql.toString();
+        } catch (Exception e) {
+            log.error("Error building update SQL. Parent: {}, Columns: {}, Values: {}", parent, columns, values, e);
+            throw new ServiceException("Error building update SQL.", e);
+        }
+    }
+
+    /**
+     * 构建 SELECT 语句。
+     * @return
+     */
+    @NotNull
+    private String buildSelectSql() {
+
+        // 检查是否有选择的列和父对象是否存在,如果不存在,则记录警告并抛出异常
+        if (parent == null || CollectionUtils.isEmpty(parent.selectColumns)) {
+            log.warn("No select columns or parent is null, returning empty SQL.");
+            throw new ServiceException("No select columns or parent is null.");
+        }
+        // 开始构建 SQL 语句的 SELECT 部分
+        StringBuilder sql = new StringBuilder();
+        sql.append("SELECT ")
+           .append(String.join(", ", parent.selectColumns))
+           .append(" ");
+
+        // 依次调用方法以补充 SQL 语句的 FROM、JOIN 和 WHERE 子句
+        appendFromClause(sql);
+        appendJoins(sql);
+        appendWhereClauses(sql);
+
+        // 将构建好的 SQL 字符串进行格式化并记录日志
+        String sqlString = sql.toString().trim();
+        switch (type) {
+            case page:
+                //生成count sql
+                String[] split = sqlString.split("FROM");
+                //获取 表字段 中 distinct
+                String countCount = "0";
+                if(CollectionUtils.isNotEmpty(parent.selectColumns)){
+                    List<String> distinctColumns = parent.selectColumns.stream()
+                                                                       .filter(column -> column.contains("DISTINCT"))
+                                                                       //移除 单字段 as 及之后的字符
+                                                                       .map(column -> column.replaceAll("\\s+AS\\s+.*", ""))
+                                                                       .collect(Collectors.toList());
+                    if(CollectionUtils.isNotEmpty(distinctColumns)){
+                        // 使用逗号分隔
+                        countCount = String.join(",", distinctColumns);
+                        //末位 为逗号 则删除
+                        if(countCount.endsWith(",")){
+                            countCount = countCount.substring(0, countCount.length() - 1);
+                        }
+                    }
+                }
+                //生成count sql
+                parent.pageCountSql = "select count("+countCount+") from " + split[1];
+                //拼接排序
+                if (!parent.orderByMap.isEmpty()) {
+                    sql.append(" ORDER BY ");
+                    List<Integer> list = new ArrayList<>(parent.orderByMap.keySet());
+                    Collections.sort(list);
+                    for (Integer integer : list) {
+                        sql.append(parent.orderByMap.get(integer)).append(",");
+                    }
+                    sql.deleteCharAt(sql.length() - 1);
+                }
+                //生成分页sql
+                sqlString = sql.append(" LIMIT ")
+                               .append(parent.mPage.getSize())
+                               .append(" OFFSET ")
+                               .append((parent.mPage.getCurrent() - 1) * parent.mPage.getSize())
+                               .append(" ")
+                               .toString();
+                break;
+            case selectOne:
+                sqlString = sql.append(" LIMIT 1 ").toString();
+                break;
+            default:
+                break;
+        }
+        // 返回构建好的 SQL 字符串
+        //sql 格式化打印
+        log.info("SQL: {}", sqlString);
+        return sqlString;
+    }
+
+    /**
+     * 拼接 FROM 子句。
+     *
+     * @param sql SQL 字符串构建器
+     */
+    private void appendFromClause(StringBuilder sql) {
+        if (Objects.nonNull(parent.superTable)) {
+            sql.append("FROM ")
+               .append(parent.superTable.getTableName())
+               .append(" ")
+               .append(parent.superTable.getAlias())
+               .append(" ");
+        }
+    }
+    /**
+     * 将 LEFT JOIN 信息拼接到 SQL 字符串中。
+     *
+     * @param sql SQL 字符串构建器
+     */
+    private void appendJoins(StringBuilder sql) {
+        // 检查是否有左连接信息需要添加
+        if (CollectionUtils.isNotEmpty(parent.leftJoinInfos)) {
+            for (LeftJoinInfo leftJoinInfo : parent.leftJoinInfos) {
+                // 跳过空的 LeftJoinInfo
+                if (leftJoinInfo == null) {
+                    continue;
+                }
+                // 拼接 LEFT JOIN 子句
+                sql.append("LEFT JOIN ")
+                   .append(leftJoinInfo.getTableInfo().getTableName())
+                   .append(" ")
+                   .append(leftJoinInfo.getTableInfo().getAlias())
+                   .append(" ON ")
+                   .append(leftJoinInfo.getLeftTableInfo().getAlias())
+                   .append(".")
+                   .append(leftJoinInfo.getLeftFieldName())
+                   .append(" = ")
+                   .append(leftJoinInfo.getTableInfo().getAlias())
+                   .append(".")
+                   .append(leftJoinInfo.getFieldName())
+                   .append(" ");
+            }
+        }
+    }
+
+    /**
+     * 将 WHERE 条件拼接到 SQL 字符串中。
+     *
+     * @param sql SQL 字符串构建器
+     */
+    private void appendWhereClauses(StringBuilder sql) {
+        // 检查是否有 WHERE 信息需要添加
+        if (CollectionUtils.isNotEmpty(parent.whereInfos)) {
+            StringBuilder whereSql = new StringBuilder("WHERE ");
+            boolean first = true;
+            // 遍历所有 WHERE 信息并构建 WHERE 子句
+            for (WhereInfo whereInfo : parent.whereInfos) {
+                // 除第一个 WHERE 条件外,其他条件前添加 "AND"
+                if (!first) {
+                    whereSql.append("AND ");
+                }
+                // 构建并添加 WHERE 条件
+                String formatSql = buildWhereClause(whereInfo);
+                if(first && StringUtils.isNotBlank(formatSql)){
+                    first = false;
+                }
+                whereSql.append(formatSql+" ");
+            }
+            //如果最后语句为 where 则删除
+            if(whereSql.toString().endsWith("AND ")){
+                whereSql.delete(whereSql.length()-4,whereSql.length());
+            }
+            //如果最后语句为 where 则删除
+            if(whereSql.toString().endsWith("WHERE ")){
+                whereSql.delete(whereSql.length()-6,whereSql.length());
+            }
+            // 将构建好的 WHERE 子句添加到 SQL 字符串中
+            sql.append(whereSql);
+        }
+    }
+
+    /**
+     * 根据WhereInfo对象构建SQL查询的WHERE子句
+     * 此方法用于根据提供的条件信息生成SQL语句中的WHERE部分它检查WhereInfo对象及其属性的有效性,
+     * 然后根据这些属性构建WHERE子句字符串
+     *
+     * @param whereInfo 包含WHERE子句构建所需信息的对象,包括表信息、列名、操作符和值等
+     * @return 返回构建的WHERE子句字符串如果提供的信息不足以构建WHERE子句,则返回空字符串
+     */
+    private String buildWhereClause(WhereInfo whereInfo) {
+        // 检查whereInfo及其关键属性是否存在,如果任一关键属性为空,则返回空字符串
+        if (Objects.isNull(whereInfo) || Objects.isNull(whereInfo.getTableInfo())
+                || (Objects.isNull(whereInfo.getColumn()) && Objects.isNull(whereInfo.getOrColumn())
+                            && Objects.isNull(whereInfo.getIsCustom()))
+                || (Objects.nonNull(whereInfo.getIsCustom()) && CollectionUtils.isEmpty(whereInfo.getCustomConditions()))
+        ) {
+            return "";
+        }
+
+        if(Objects.isNull(whereInfo.getFieldType()) && !FieldOperator.checkEmptyValue(whereInfo.getOperator())
+                && (Objects.isNull(whereInfo.getValue()) || Objects.equals("", whereInfo.getValue()))
+                && (Objects.nonNull(whereInfo.getIsCustom()) && CollectionUtils.isEmpty(whereInfo.getCustomConditions()))){
+            return "";
+        }
+        if(Objects.isNull(whereInfo.getFieldType()) && !FieldOperator.checkEmptyValue(whereInfo.getOperator())){
+            whereInfo.setFieldType(FieldType.STRING);
+        }
+        //判断是否是自定义条件  如果是自定义条件 则 拼接自定义条件 并返回
+        if(Objects.nonNull(whereInfo.getIsCustom()) && whereInfo.getIsCustom()) {
+            return buildCustomConditionsWhere(whereInfo);
+        }
+        // 使用StringBuilder来构建WHERE子句
+        StringBuilder clause = new StringBuilder();
+        // 添加表别名和列名到WHERE子句中
+        //如果是更新 则不加别名
+        if(parent.dbMethod == DbMethodEnums.UPDATE){
+            clause.append(whereInfo.getColumn())
+                  .append(" ")
+                  .append(whereInfo.getOperator().getOperator())
+                  .append(" ")
+            ;
+            extractedValue(whereInfo, clause);
+        } else {
+            //判断 column 或 orColumn
+            String alias = whereInfo.getTableInfo().getAlias();
+            if (CollectionUtils.isNotEmpty(whereInfo.getOrColumn())) {
+                clause.append("(");
+                whereInfo.getOrColumn().forEach(column -> {
+                    clause.append(alias)
+                          .append(".")
+                          .append(column)
+                          .append(" ")
+                          .append(whereInfo.getOperator().getOperator())
+                          .append(" ")
+                    ;
+                    // 添加操作符到WHERE子句中
+                    extractedValue(whereInfo, clause);
+                    // 判断是否是最后一个列
+                    clause.append(whereInfo.getOrColumn().indexOf(column) == whereInfo.getOrColumn().size()-1 ? ")" : " OR ");
+                });
+            }else {
+                clause.append(alias)
+                      .append(".")
+                      .append(whereInfo.getColumn())
+                      .append(" ")
+                      // 添加操作符到WHERE子句中
+                      .append(whereInfo.getOperator().getOperator())
+                      .append(" ");
+                extractedValue(whereInfo, clause);
+            }
+        }
+        // 返回构建完成的WHERE子句
+        return clause.toString();
+    }
+
+    // 如果为 true,则 customConditions 中的条件将使用 OR 连接,而不是默认的 AND 连接
+    // 注意:如果 isCustomOr 为 true,则 customConditions 中的条件必须是独立的查询条件,不能依赖于其他条件
+    // 例如:
+    // @Where(table = User.class, isCustomOr = true,
+    //         customConditions = {
+    //             @CustomCondition(field = "status", operator = FieldOperator.EQ, defaultValue = "active"),
+    //             @CustomCondition(field = "role", operator = FieldOperator.IN, orFields = {"admin", "user"})
+    //         },{
+    //             @CustomCondition(field = "status", operator = FieldOperator.EQ, defaultValue = "logOff")
+    //         })
+    // 上述条件将生成 SQL 语句:WHERE (status = 'active' AND role IN ('admin', 'user')) OR (status = 'logOff')
+    // 如果为 false,则 customConditions 中的条件将使用 AND 连接
+    // 例如:
+    // @Where(table = User.class, isCustomOr = false,
+    //         customConditions = {
+    //             @CustomCondition(field = "status", operator = FieldOperator.EQ, defaultValue = "active"),
+    //             @CustomCondition(field = "role", operator = FieldOperator.IN, orFields = {"admin", "user"})
+    //         },{
+    //             @CustomCondition(field = "status", operator = FieldOperator.EQ, defaultValue = "logOff")
+    //         })
+    // 上述条件将生成 SQL 语句:WHERE status = 'active' AND role IN ('admin', 'user') AND status = 'logOff'
+    // 注意:如果 isCustomOr 为 false,则 customConditions 中的条件必须是可以同时成立的条件,否则将导致查询结果为空
+    // 注意:customConditions 中的条件每组条件之间是 AND 关系,而组与组之间 根据 isCustomOr 的值决定是 AND 还是 OR 关系
+    // 默认值为 false,表示使用 AND 连接
+    // 如果需要使用 OR 连接,则需要显式设置 isCustomOr 为 true
+    // 检查参数有效性
+    private String buildCustomConditionsWhere(WhereInfo whereInfo) {
+        if (whereInfo == null || CollectionUtils.isEmpty(whereInfo.getCustomConditions())) {
+            return "";
+        }
+        List<WhereInfo.CustomConditions> customConditions = whereInfo.getCustomConditions();
+        boolean isCustomOr = whereInfo.isCustomOr(); // 获取 isCustomOr 设置
+
+        StringBuilder result = new StringBuilder();
+        String connector = isCustomOr ? " OR " : " AND ";
+        String groupWrapperStart = isCustomOr ? "(" : "";
+        String groupWrapperEnd = isCustomOr ? ")" : "";
+
+        for (int i = 0; i < customConditions.size(); i++) {
+            WhereInfo.CustomConditions conditionsGroup = customConditions.get(i);
+            List<WhereInfo.CustomCondition> conditions = conditionsGroup.getCustomConditionList();
+
+            if (CollectionUtils.isEmpty(conditions)) {
+                continue;
+            }
+
+            // 构建当前组的条件
+            StringBuilder groupClause = new StringBuilder();
+            for (int j = 0; j < conditions.size(); j++) {
+                WhereInfo.CustomCondition condition = conditions.get(j);
+                // 构建单个条件
+                String conditionClause = buildSingleCustomCondition(whereInfo, condition);
+
+                if (StringUtils.isNotBlank(conditionClause)) {
+                    groupClause.append(conditionClause);
+                    if (j < conditions.size() - 1) {
+                        groupClause.append(" AND ");
+                    }
+                }
+            }
+
+            if (groupClause.length() > 0) {
+                if (i > 0) {
+                    result.append(connector);
+                }
+                result.append(groupWrapperStart).append(groupClause).append(groupWrapperEnd);
+            }
+        }
+
+        return result.length() > 0 ? "("+ result +")" : "";
+    }
+
+    /**
+     * 构建单个自定义条件的 SQL 子句。
+     *
+     * @param whereInfo 用于获取表别名等上下文信息
+     * @param condition 自定义条件对象
+     * @return 单个条件的 SQL 字符串
+     */
+    private String buildSingleCustomCondition(WhereInfo whereInfo, WhereInfo.CustomCondition condition) {
+        if (condition == null || StringUtils.isBlank(condition.getColumn())) {
+            return "";
+        }
+
+        StringBuilder clause = new StringBuilder();
+        String alias = whereInfo.getTableInfo().getAlias();
+
+        String field = condition.getColumn();
+        String operator = condition.getOperator().getOperator();
+        String defaultValue = condition.getDefaultValue();
+
+        // 获取值列表,用于替换占位符
+        Object value = whereInfo.getValue();
+        String valStr = String.valueOf(value);
+        String[] values = valStr.split(",");
+
+        if (StringUtils.isNotBlank(defaultValue)) {
+            // 使用正则表达式匹配 #{0}, #{1} 等形式的占位符
+            java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("#\\{(\\d+)\\}");
+            java.util.regex.Matcher matcher = pattern.matcher(defaultValue);
+
+            StringBuffer sb = new StringBuffer();
+
+            while (matcher.find()) {
+                String indexStr = matcher.group(1);
+                int index = Integer.parseInt(indexStr);
+
+                // 替换占位符为实际值,若索引越界则保留原值
+                if (index >= 0 && index < values.length) {
+                    String replacement = values[index].trim();
+                    matcher.appendReplacement(sb, replacement);
+                } else {
+                    matcher.appendReplacement(sb, matcher.group(0)); // 保留原占位符
+                }
+            }
+            matcher.appendTail(sb);
+
+            // 使用替换后的值
+            defaultValue = sb.toString();
+            condition.setValue(defaultValue);
+        }else{
+            condition.setValue("");
+        }
+
+        clause.append(alias).append(".").append(field).append(" ").append(operator).append(" ");
+        //拼接值
+        extractedValue(condition,clause);
+
+        return clause.toString();
+    }
+
+    /**
+     * 提取值
+     * @param whereInfo
+     * @param clause
+     */
+    private static void extractedValue(WhereInfo whereInfo, StringBuilder clause) {
+        //特殊处理 如果是between 或 not between
+        if (whereInfo.getOperator() == FieldOperator.BETWEEN || whereInfo.getOperator() == FieldOperator.NOTBETWEEN) {
+            // 如果是between 或 not between,则添加起始值和结束值到WHERE子句中
+            clause.append(FormatValueUtil.formatStringValue(whereInfo));
+        }else {
+            // 根据字段类型添加相应的值到WHERE子句中
+            if (whereInfo.getFieldType() == FieldType.STRING) {
+                // 对字符串类型进行特殊格式化处理
+                clause.append(FormatValueUtil.formatStringValue(whereInfo));
+            } else if (whereInfo.getFieldType() == FieldType.NUMBER) {
+                // 直接添加数值类型到WHERE子句中,无需额外格式化
+                clause.append(whereInfo.getValue());
+            } else if (whereInfo.getFieldType() == FieldType.DATE) {
+                // 对日期类型进行转义处理,防止SQL注入
+                clause.append("'").append(FormatValueUtil.escapeValue(whereInfo.getValue())).append("'");
+            } else if (whereInfo.getFieldType() == FieldType.JSONB) {
+                // 对jsonb进行转义处理,防止SQL注入
+                clause.append("'[\"").append(FormatValueUtil.escapeValue(whereInfo.getValue())).append("\"]'::jsonb");
+            } else {
+                // 直接添加到WHERE子句中,无需额外格式化
+                clause.append(whereInfo.getValue());
+            }
+        }
+    }
+
+    /**
+     * 提取值
+     * @param condition
+     * @param clause
+     */
+    private static void extractedValue(WhereInfo.CustomCondition condition, StringBuilder clause) {
+        //特殊处理 如果是between 或 not between
+        if (condition.getOperator() == FieldOperator.BETWEEN || condition.getOperator() == FieldOperator.NOTBETWEEN) {
+            // 如果是between 或 not between,则添加起始值和结束值到WHERE子句中
+            clause.append(FormatValueUtil.formatStringValue(condition));
+        }else {
+            // 根据字段类型添加相应的值到WHERE子句中
+            if (condition.getFieldType() == FieldType.STRING) {
+                // 对字符串类型进行特殊格式化处理
+                clause.append(FormatValueUtil.formatStringValue(condition));
+            } else if (condition.getFieldType() == FieldType.NUMBER) {
+                // 直接添加数值类型到WHERE子句中,无需额外格式化
+                clause.append(condition.getValue());
+            } else if (condition.getFieldType() == FieldType.DATE) {
+                // 对日期类型进行转义处理,防止SQL注入
+                clause.append("'").append(FormatValueUtil.escapeValue(condition.getValue())).append("'");
+            } else {
+                // 直接添加到WHERE子句中,无需额外格式化
+                clause.append(condition.getValue());
+            }
+        }
+    }
+
+    /**
+     * @return
+     */
+    public SqlUtil buildOne() {
+        this.type = SqlEnums.selectOne;
+        return this;
+    }
+
+    /**
+     * @return
+     */
+    public SqlUtil buildSum() {
+        this.type = SqlEnums.sum;
+        return this;
+    }
+}
+

+ 106 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/util/TableInfoUtil.java

@@ -0,0 +1,106 @@
+package com.poyee.base.mapper.provider.util;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.poyee.base.mapper.provider.BaseProvider;
+import com.poyee.base.mapper.provider.domain.TableInfo;
+import com.poyee.common.exception.ServiceException;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class TableInfoUtil {
+
+    public static TableInfoUtil init(BaseProvider baseProvider) {
+        return new TableInfoUtil(baseProvider);
+    }
+
+    private TableInfoUtil(BaseProvider baseProvider) {
+        this.parent = baseProvider;
+    }
+
+    private final BaseProvider parent;
+
+    /**
+     * 初始化表信息
+     * 该方法用于根据给定的类获取其对应的数据库表名,并生成一个TableInfo对象,最终将该对象存入parent的tableInfos中
+     * 主要功能包括:
+     * 1. 检查给定类是否使用了@TableName注解,如果没有,则抛出异常
+     * 2. 获取@TableName注解的值,作为表名
+     * 3. 根据parent.tableCount的值生成表的别名
+     * 4. 创建TableInfo对象,并将其存入parent.tableInfos中
+     * 5. 增加parent.tableCount的值,为下一个表生成别名做准备
+     *
+     * @param aClass 要初始化表信息的类,该类应使用@TableName注解来指定表名
+     */
+    public TableInfo initTableInfo(Class<?> aClass) {
+        // 检查给定类是否使用了@TableName注解,如果没有,则抛出异常
+        if (!aClass.isAnnotationPresent(TableName.class)) {
+            throw new ServiceException(aClass.getName() + "未设置表名");
+        }
+        // 获取@TableName注解的值,作为表名
+        TableName tableName = aClass.getAnnotation(TableName.class);
+        String firstTableName = tableName.value();
+        //判断是否已经存在表
+        if(parent.tableInfos.containsKey(aClass.getName())){
+            return parent.tableInfos.get(aClass.getName());
+        }
+        // 根据parent.tableCount的值生成表的别名
+        String thisAlisa = "_" + parent.tableCount;
+        if (parent.tableCount == 0) {
+            thisAlisa = "";
+        }
+        String primaryKey = "id";
+        //获取主键
+        if(aClass.isAnnotationPresent(TableId.class)){
+            primaryKey = aClass.getAnnotation(TableId.class).value();
+        }
+        // 创建TableInfo对象,并将其存入parent.tableInfos中
+        parent.tableInfos.put(aClass.getName(), new TableInfo(aClass.getName(), aClass, firstTableName, "t" + thisAlisa, parent.tableCount, primaryKey));
+        // 增加parent.tableCount的值,为下一个表生成别名做准备
+        parent.tableCount++;
+        return parent.tableInfos.get(aClass.getName());
+    }
+
+    /**
+     *  根据别名初始化表信息
+     * @param aClass
+     * @param alias
+     */
+    public TableInfo initTableInfoByAlias(Class<?> aClass, String alias) {
+        // 检查给定类是否使用了@TableName注解,如果没有,则抛出异常
+        if (!aClass.isAnnotationPresent(TableName.class)) {
+            throw new ServiceException(aClass.getName() + "未设置表名");
+        }
+        //判断是否已经存在表
+        if(parent.tableInfos.containsKey(aClass.getName())){
+            return parent.tableInfos.get(aClass.getName());
+        }
+        // 获取@TableName注解的值,作为表名
+        TableName tableName = aClass.getAnnotation(TableName.class);
+        String firstTableName = tableName.value();
+        //根据别名获取表信息
+        TableInfo tableInfo = parent.tableInfos.values().stream()
+                .filter(tableInfo1 -> tableInfo1.getAlias().equals(alias))
+                .findFirst()
+                .orElse(null);
+        if(tableInfo != null){
+           return tableInfo;
+        }
+        // 根据parent.tableCount的值生成表的别名
+        String thisAlisa = "_" + parent.tableCount;
+        if (parent.tableCount == 0) {
+            thisAlisa = "";
+        }
+        String primaryKey = "id";
+        //获取主键
+        if(aClass.isAnnotationPresent(TableId.class)){
+            primaryKey = aClass.getAnnotation(TableId.class).value();
+        }
+        // 创建TableInfo对象,并将其存入parent.tableInfos中
+        parent.tableInfos.put(aClass.getName(), new TableInfo(aClass.getName(), aClass, firstTableName, alias + thisAlisa, parent.tableCount, primaryKey));
+        // 增加parent.tableCount的值,为下一个表生成别名做准备
+        parent.tableCount++;
+        return parent.tableInfos.get(aClass.getName());
+    }
+
+}

+ 167 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/util/UpdateUtil.java

@@ -0,0 +1,167 @@
+package com.poyee.base.mapper.provider.util;
+
+import com.alibaba.excel.util.StringUtils;
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.poyee.annotation.db.Where;
+import com.poyee.base.mapper.provider.BaseProvider;
+import com.poyee.base.mapper.provider.domain.TableInfo;
+import com.poyee.base.mapper.provider.domain.WhereInfo;
+import com.poyee.common.exception.ServiceException;
+import com.poyee.enums.DbMethodEnums;
+import com.poyee.enums.FieldOperator;
+import com.poyee.util.DateUtils;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.Field;
+import java.util.Date;
+import java.util.Map;
+import java.util.Objects;
+
+import static com.poyee.base.mapper.provider.ErrMessageContent.*;
+
+@Slf4j
+public class UpdateUtil {
+
+    private BaseProvider parent;
+
+    private UpdateUtil(BaseProvider parent) {
+        this.parent = parent;
+    }
+
+    public static UpdateUtil init(BaseProvider parent) {
+        return new UpdateUtil(parent);
+    }
+
+    public UpdateUtil checkUpdateBean(Object object) {
+        if(Objects.isNull(object)){
+            log.warn("传入的对象为空!");
+            throw new ServiceException(ERROR_MESSAGE_OBJECT_IS_NULL);
+        }
+        Class<?> aClass = object.getClass();
+        if(!aClass.isAnnotationPresent(TableName.class)){
+            log.warn("对象必须包含@TableName注解");
+            throw new ServiceException(ERROR_MESSAGE_TABLE_NAME_MISSING_2);
+        }
+        TableName tableNameAnnotation = aClass.getAnnotation(TableName.class);
+        if(Objects.isNull(tableNameAnnotation.value()) || tableNameAnnotation.value().trim().isEmpty()){
+            log.warn("表名不能为空");
+            throw new ServiceException(ERROR_MESSAGE_TABLE_NAME_IS_NULL);
+        }
+
+        // 初始化表信息
+        TableInfo tableInfo = TableInfoUtil.init(parent)
+                                           .initTableInfo(aClass);
+        parent.superTable = tableInfo;
+//        parent.superTable = parent.tableInfos.get(aClass.getName());
+        parent.update = object;
+        return this;
+    }
+
+    /**
+     * 初始化更新条件
+     */
+    public void initUpdate() {
+        Object update = parent.update;
+        if (update == null || parent.updateMap == null) {
+            log.warn("更新对象或映射为空,无法生成SQL。对象: {}", update);
+            throw new ServiceException(getErrorMessage(ERROR_MESSAGE_BEAN_NULL,"更新"));
+        }
+
+        try {
+            Map<String, Object> updateMap = parent.updateMap;
+            updateMap.clear(); // 清空旧数据
+            processFields(update, updateMap);
+
+            if (updateMap.isEmpty()) {
+                log.warn("没有有效的更新字段,对象: {}", update);
+                throw new ServiceException(getErrorMessage(ERROR_MESSAGE_OPERA_NULL,"更新"));
+            }
+        } catch (IllegalAccessException e) {
+            log.error("访问字段时发生错误", e);
+            throw new ServiceException(ERROR_MESSAGE_FIELD_ACCESS);
+        } catch (IllegalArgumentException e) {
+            log.error("非法参数错误", e);
+            throw new ServiceException(ERROR_MESSAGE_ILLEGAL_ARGUMENT);
+        } catch (Exception e) {
+            log.error(getErrorMessage(ERROR_MESSAGE_BUILD_SQL,"更新"), e);
+            throw new ServiceException(getErrorMessage(ERROR_MESSAGE_BUILD_SQL,"更新"));
+        }
+    }
+
+
+    /**
+     * 处理字段并填充到映射中
+     *
+     * @param object    对象
+     * @param updateMap 插入映射
+     * @throws IllegalAccessException 如果字段访问失败
+     */
+    private void processFields(Object object, Map<String, Object> updateMap) throws IllegalAccessException {
+        Class<?> aClass = object.getClass();
+        Field[] declaredFields = aClass.getDeclaredFields();
+
+        for (Field field : declaredFields) {
+            field.setAccessible(true);
+            // 获取字段注解
+            if(!field.isAnnotationPresent(Where.class)) {
+                if (field.isAnnotationPresent(TableField.class)) {
+                    TableField tableField = field.getAnnotation(TableField.class);
+                    if (tableField.exist()) {
+                        Object value = field.get(object);
+                        if (Objects.nonNull(value)) {
+                            if (value instanceof Date) {
+                                updateMap.put(tableField.value(), DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, (Date) value));
+                            } else {
+                                updateMap.put(tableField.value(), value);
+                            }
+                        }
+                    }
+                    //判断是否需要设置 创建,更新时间
+                    if ( Objects.equals(parent.dbMethod, DbMethodEnums.UPDATE)
+                        && Objects.equals(tableField.fill() , FieldFill.INSERT_UPDATE)
+                        && Objects.equals(field.getName(), "updateTime")
+                        ) {
+                        updateMap.put(tableField.value(), new Date());
+                    }
+                }else if (field.isAnnotationPresent(TableId.class)) {
+                    TableId tableId = field.getAnnotation(TableId.class);
+                    Object value = field.get(object);
+                    if (Objects.nonNull(value)) {
+                        if (value instanceof Date) {
+                            updateMap.put(tableId.value(), DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, (Date) value));
+                        } else {
+                            updateMap.put(tableId.value(), value);
+                        }
+                    }
+                }
+            }else{
+                //设置where 条件
+                Where where = field.getAnnotation(Where.class);
+                try {
+                    Object value = field.get(object);
+                    // 根据 Where 注解配置和字段值,决定是否创建 WHERE 信息
+                    if ( (value instanceof String && StringUtils.isNotBlank(String.valueOf(value)))
+                            || (!(value instanceof String) && Objects.nonNull(value))
+                            || FieldOperator.checkEmptyValue(where)) {
+                        Class<?> table = where.table();
+                        // 确保表信息已初始化
+                        if (!parent.tableInfos.containsKey(table.getName())) {
+                            TableInfoUtil.init(parent).initTableInfo(table);
+                        }
+                        WhereInfo whereInfo = new WhereInfo(parent.tableInfos.get(table.getName()));
+                        whereInfo.setColumn(where.field());
+                        whereInfo.setValue(value);
+                        whereInfo.setOperator(where.operator());
+                        whereInfo.setFieldType(where.columnType());
+                        parent.whereInfos.add(whereInfo);
+                    }
+                } catch (IllegalAccessException e) {
+                    throw new ServiceException("无法访问字段:" + field.getName(), e);
+                }
+            }
+        }
+    }
+}

+ 184 - 0
py-base/src/main/java/com/poyee/base/mapper/provider/util/WhereUtil.java

@@ -0,0 +1,184 @@
+package com.poyee.base.mapper.provider.util;
+
+import com.alibaba.excel.util.StringUtils;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.poyee.annotation.db.Where;
+import com.poyee.base.mapper.provider.BaseProvider;
+import com.poyee.base.mapper.provider.domain.WhereInfo;
+import com.poyee.common.exception.ServiceException;
+import com.poyee.enums.FieldOperator;
+import com.poyee.enums.FieldType;
+import com.poyee.util.DateUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.lang.reflect.Field;
+import java.util.*;
+
+@Slf4j
+public class WhereUtil {
+
+    public static WhereUtil init(BaseProvider baseProvider){
+        return new WhereUtil(baseProvider);
+    }
+
+    private WhereUtil(BaseProvider baseProvider) {
+        this.parent = baseProvider;
+    }
+    private BaseProvider parent;
+
+    /**
+     * 处理 WHERE 条件。
+     *
+     * @param object 对象实例
+     * @param aClass 类对象
+     * @param whereInfos WHERE 信息列表
+     * @param field 字段对象
+     */
+    public void build(Object object, Class<?> aClass, List<WhereInfo> whereInfos, Field field) {
+        // 检查字段是否用 Where 注解标记
+        if (field.isAnnotationPresent(Where.class)) {
+            Where where = field.getAnnotation(Where.class);
+            try {
+                Object value = field.get(object);
+                // 根据 Where 注解配置和字段值,决定是否创建 WHERE 信息
+                if ( (value instanceof String && StringUtils.isNotBlank(String.valueOf(value)))
+                        || (!(value instanceof String) && Objects.nonNull(value))
+                        || FieldOperator.checkEmptyValue(where)) {
+                    Class<?> table = where.table();
+                    // 确保表信息已初始化
+                    if (!parent.tableInfos.containsKey(table.getName())) {
+                        TableInfoUtil.init(parent).initTableInfo(table);
+                    }
+                    WhereInfo whereInfo = new WhereInfo(parent.tableInfos.get(table.getName()));
+                    // 处理自定义查询条件
+                    if (checkCustomConditions(where, whereInfo,value)){
+                        whereInfos.add(whereInfo);
+                        return;
+                    }
+                    //如果 where orField 有值
+                    String[] orFields = where.orFields();
+                    if(Objects.nonNull(orFields) && orFields.length > 0){
+                        // 数组转集合
+                        whereInfo.setOrColumn(Arrays.asList(orFields));
+                    }else {
+                        if ((Objects.equals(where.operator(), FieldOperator.BETWEEN) || Objects.equals(where.operator(), FieldOperator.NOTBETWEEN)) && where.isEnd()) {
+                            whereInfo = whereInfos.stream()
+                                                  .filter(whereInfo1 -> Objects.equals(whereInfo1.getColumn(), where.field()))
+                                                  .findFirst()
+                                                  .orElse(whereInfo);
+                        }
+                        whereInfo.setColumn(where.field());
+                    }
+                    if((Objects.equals(where.operator(),FieldOperator.BETWEEN)
+                            || Objects.equals(where.operator(),FieldOperator.NOTBETWEEN)) && where.isEnd() ){
+                        whereInfo.setEndValue(valueCase(value,field));
+                    } else {
+                        whereInfo.setValue(valueCase(value,field));
+                    }
+                    whereInfo.setOperator(where.operator());
+                    whereInfo.setFieldType(where.columnType());
+                    whereInfos.add(whereInfo);
+                }
+            } catch (IllegalAccessException e) {
+                throw new ServiceException("无法访问字段:" + field.getName(), e);
+            }
+        }else {
+            // 检查字段是否用 TableField 注解标记
+            if (field.isAnnotationPresent(TableField.class)) {
+                TableField tableField = field.getAnnotation(TableField.class);
+                // 如果字段在数据库中存在,则尝试构建 WHERE 信息
+                if (tableField.exist()) {
+                    try {
+                        Object value = field.get(object);
+                        // 如果字段值非空,则创建 WHERE 信息并添加到列表中
+                        if ((value instanceof String && StringUtils.isNotBlank(String.valueOf(value))) || (!(value instanceof String) && Objects.nonNull(value))) {
+                            WhereInfo whereInfo = new WhereInfo(parent.tableInfos.get(aClass.getName()));
+                            whereInfo.setColumn(tableField.value());
+                            whereInfo.setValue(valueCase(value,field));
+                            whereInfo.setOperator(FieldOperator.EQ);
+                            whereInfo.setFieldType(valueCaseType(value));
+                            whereInfos.add(whereInfo);
+                        }
+                    } catch (IllegalAccessException e) {
+                        throw new ServiceException("无法访问字段:" + field.getName(), e);
+                    }
+                }
+            }else if(  field.isAnnotationPresent(TableId.class)){
+                TableId tableId = field.getAnnotation(TableId.class);
+                try {
+                    Object value = field.get(object);
+                    // 如果字段值非空,则创建 WHERE 信息并添加到列表中
+                    if ((value instanceof String && StringUtils.isNotBlank(String.valueOf(value))) || (!(value instanceof String) && Objects.nonNull(value))) {
+                        WhereInfo whereInfo = new WhereInfo(parent.tableInfos.get(aClass.getName()));
+                        whereInfo.setColumn(tableId.value());
+                        whereInfo.setValue(valueCase(value,field));
+                        whereInfo.setOperator(FieldOperator.EQ);
+                        whereInfo.setFieldType(valueCaseType(value));
+                        whereInfos.add(whereInfo);
+                    }
+                } catch (IllegalAccessException e) {
+                    throw new ServiceException("无法访问字段:" + field.getName(), e);
+                }
+            }
+        }
+    }
+
+    /**
+     * 检查自定义条件。
+     * @param where
+     * @param whereInfo
+     * @return
+     */
+    private static boolean checkCustomConditions(Where where, WhereInfo whereInfo, Object value) {
+        List<WhereInfo.CustomConditions> customConditionList = new ArrayList<>();
+        //判断customConditions 是否有 自定义
+        Where.CustomConditions[] customConditions = where.customConditions();
+        if (CollectionUtils.isNotEmpty(Arrays.asList(customConditions))) {
+            whereInfo.setIsCustom(true);
+            whereInfo.setCustomOr(where.isCustomOr());
+            whereInfo.setValue(value);
+            Arrays.asList(customConditions)
+                  .forEach(items -> {
+                      WhereInfo.CustomConditions customCondition = new WhereInfo.CustomConditions();
+                      Arrays.asList(items.value())
+                            .forEach(item -> {
+                                customCondition.add(new WhereInfo.CustomCondition(item.field(), item.defaultValue(), item.operator(), item.columnType()));
+                            });
+                      customConditionList.add(customCondition);
+                  });
+            whereInfo.setCustomConditions(customConditionList);
+            return true;
+        }
+        return false;
+    }
+
+    private FieldType valueCaseType(Object value) {
+        if (value instanceof String) {
+            return FieldType.STRING;
+        } else if (value instanceof Integer) {
+            return FieldType.NUMBER;
+        }else if (value instanceof Boolean) {
+            return FieldType.BOOLEAN;
+        }else if (value instanceof Date) {
+            return FieldType.DATE;
+        }
+        return FieldType.STRING;
+    }
+
+    /**
+     * @param value
+     * @return
+     */
+    private Object valueCase(Object value,Field field) {
+        //如果 是 时间类型  转正格式化的 字符串
+        if (value instanceof Date && field.isAnnotationPresent(DateTimeFormat.class)) {
+            DateTimeFormat dateFormat = field.getAnnotation(DateTimeFormat.class);
+            return DateUtils.parseDateToStr(dateFormat.pattern(), (Date) value);
+        }
+        return value;
+    }
+
+}

+ 14 - 0
py-base/src/main/java/com/poyee/base/service/BaseService.java

@@ -0,0 +1,14 @@
+package com.poyee.base.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import com.poyee.base.dto.BaseDto;
+import com.poyee.base.dto.BaseReq;
+import com.poyee.base.dto.UserInfo;
+
+public interface BaseService<R extends BaseReq, D extends BaseDto> extends IService<D> {
+    UserInfo getUserInfo();
+    public MPJLambdaWrapper<D> mpjWrapper(Object req);
+    public MPJLambdaWrapper<D> mpjWrapper(MPJLambdaWrapper<D> mpjWrapper,Object req,String alias);
+
+}

+ 295 - 0
py-base/src/main/java/com/poyee/base/service/impl/BaseServiceImpl.java

@@ -0,0 +1,295 @@
+package com.poyee.base.service.impl;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import com.poyee.base.dto.UserInfo;
+import com.poyee.common.exception.AuthException;
+import com.poyee.annotation.MpjWapper;
+import com.poyee.base.dto.BaseDto;
+import com.poyee.base.dto.BaseReq;
+import com.poyee.base.mapper.IBaseMapper;
+import com.poyee.base.service.BaseService;
+import com.poyee.framework.domain.MpjWrapper;
+import com.poyee.common.exception.ServiceException;
+import com.poyee.i18n.I18nUtils;
+import com.poyee.redis.util.RedisUtils;
+import com.poyee.util.DateUtils;
+import com.poyee.util.ObjectUtil;
+import com.poyee.util.ServletUtils;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.lang.reflect.Field;
+import java.util.*;
+
+import static com.poyee.i18n.I18nMessageEnums.*;
+
+/**
+ * @param <E>
+ * @param <R>
+ * @param <D>
+ */
+@Service
+public abstract class BaseServiceImpl<E extends IBaseMapper<D>, R extends BaseReq, D extends BaseDto> extends ServiceImpl<E, D> implements BaseService<R, D> {
+
+    @Autowired
+    protected RedisUtils redisUtils;
+
+
+    /**
+     * 获取当前用户ID
+     * @return
+     */
+    public Long getUserId(){
+        UserInfo userInfo = ServletUtils.getUserInfo();
+        return StringUtils.isNotBlank(userInfo.getUserId())?Long.parseLong(userInfo.getUserId()): Long.valueOf(userInfo.getId());
+    }
+    /**
+     * 获取当前用户ID
+     * @return
+     */
+    public String getLoginName(){
+        try {
+            UserInfo userInfo = ServletUtils.getUserInfo();
+            return StringUtils.isNotBlank(userInfo.getDisplayName()) ? userInfo.getDisplayName() : userInfo.getUserId();
+        }catch (Exception e) {
+            log.error("获取用户信息失败",e);
+//            throw new AuthException(I18nUtils.get(GET_USER_INFO_ERROR));
+        }
+        return "游客";
+    }
+    /**
+     * 检查用户权限
+     * @param p
+     * @param <P>
+     */
+    public <P extends BaseReq> void checkAndSetUserId(P p){
+        try {
+            UserInfo userInfo = ServletUtils.getUserInfo();
+            //解析 p 中的user_id
+            if (Objects.nonNull(p)) {
+                Field userId = p.getClass().getDeclaredField("userId");
+                if (Objects.nonNull(userId)) {
+                    userId.setAccessible(true);
+                    String typeName = userId.getType().getName();
+                    if(Objects.equals("java.lang.Long",typeName)){
+                        userId.set(p, Long.valueOf(userInfo.getId()));
+                    }else if(Objects.equals("java.lang.Integer",typeName)){
+                        userId.set(p, userInfo.getId());
+                    }
+                }
+            }
+        } catch (NoSuchFieldException ne) {
+            log.error("请求参数中无userId",ne);
+            throw new AuthException(402, I18nUtils.get(NO_PERMISSION, "【需指定用户】") );
+        } catch (Exception e) {
+            log.error("获取用户信息失败",e);
+            throw new AuthException(I18nUtils.get(GET_USER_INFO_ERROR));
+        }
+    }
+    /**
+     * 检查商户权限
+     * @param p
+     * @param <P>
+     */
+    public <P extends BaseReq> void checkAndSetMerchantId(P p){
+        try {
+            if(p.isDoRedis()){
+                log.debug(DateUtils.dateTime()+" 定时任务【"+p.getServiceMethodName()+"】缓存");
+                return ;
+            }
+            UserInfo userInfo = ServletUtils.getUserInfo();
+            if(userInfo.isMerchant()) {
+                //解析 p 中的user_id
+                if (Objects.nonNull(p)) {
+                    Field merchantId = p.getClass().getDeclaredField("merchantId");
+                    if (Objects.nonNull(merchantId)) {
+                        merchantId.setAccessible(true);
+                        String typeName = merchantId.getType().getName();
+                        if (Objects.equals("java.lang.Long", typeName)) {
+                            merchantId.set(p, Long.valueOf(userInfo.getMerchantId()));
+                        } else if (Objects.equals("java.lang.Integer", typeName)) {
+                            merchantId.set(p, userInfo.getMerchantId());
+                        }
+                    }
+                }
+            }
+        } catch (NoSuchFieldException ne) {
+            log.error("请求参数中无userId",ne);
+            throw new AuthException(402, I18nUtils.get(NO_PERMISSION,"【权限不足】"));
+        } catch (Exception e) {
+            log.error("获取用户信息失败",e);
+            throw new AuthException(I18nUtils.get(GET_USER_INFO_ERROR));
+        }
+    }
+    /**
+     * 检查商户权限
+     * @param p
+     * @param <P>
+     */
+    public <P extends BaseReq> void checkAndPutMerchantId(P p){
+        try {
+            UserInfo userInfo = ServletUtils.getUserInfo();
+            if(userInfo.isMerchant()) {
+                //解析 p 中的merchantId
+                if (Objects.nonNull(p)) {
+                    Field merchantId = p.getClass().getDeclaredField("merchantId");
+                    if (Objects.nonNull(merchantId)) {
+                        merchantId.setAccessible(true);
+                        String typeName = merchantId.getType().getName();
+                        if (Objects.equals("java.lang.Long", typeName)) {
+                            merchantId.set(p, Long.valueOf(userInfo.getMerchantId()));
+                        } else if (Objects.equals("java.lang.Integer", typeName)) {
+                            merchantId.set(p, userInfo.getMerchantId());
+                        }
+                    }
+                }
+            }
+        } catch (NoSuchFieldException | IllegalAccessException ne) {
+            log.error("请求参数中无merchantId",ne);
+            throw new AuthException(402, I18nUtils.get(NO_PERMISSION, "【权限不足】"));
+        } catch (AuthException e) {
+            log.error("获取用户信息失败",e);
+        }
+    }
+    /**
+     * 获取请求用户的信息
+     *
+     * @return
+     */
+    @Override
+    public UserInfo getUserInfo() {
+        return ServletUtils.getUserInfo();
+    }
+
+    /**
+     * @param req
+     * @return
+     */
+    public MPJLambdaWrapper<D> mpjWrapper(Object req) {
+        return mpjWrapper(new MPJLambdaWrapper<>(),req,"t");
+    }
+
+    /**
+     * @param req
+     * @return
+     */
+    public MPJLambdaWrapper<D> mpjWrapper(MPJLambdaWrapper<D> mpjLambdaWrapper,Object req,String alias) {
+        try {
+            if (null != req) {
+                Field[] declaredFields = req.getClass().getDeclaredFields();
+                Map<String, MpjWrapper> mpjWapperMap = new HashMap<>();
+                for (Field field : declaredFields) {
+                    field.setAccessible(true);
+                    String name = field.getName();
+                    //判断是否有注解 MpjWapper
+                    MpjWapper mpjWapper = field.getAnnotation(MpjWapper.class);
+                    if (Objects.nonNull(mpjWapper)) {
+                        if (Objects.nonNull(mpjWapper.value()) ) {
+                            MpjWrapper mpjWrapper = Optional.ofNullable(mpjWapperMap.get(mpjWapper.value()))
+                                                            .orElse(new MpjWrapper());
+                            if(Objects.nonNull(field.get(req))) {
+                                mpjWrapper = mpjWrapper.builder(mpjWapper,alias, field.get(req));
+                                mpjWapperMap.put(mpjWapper.value(), mpjWrapper);
+                            }
+                        }
+                    }
+                    //判断非使用属性
+                    TableField tableField = field.getAnnotation(TableField.class);
+                    if (Objects.isNull(tableField) || ( !Objects.isNull(tableField) && !tableField.exist()) ) {
+                        continue;
+                    }
+                    Object value = field.get(req);
+                    if(Objects.isNull(value)){
+                        continue;
+                    }
+                    String column = alias+"."+ (Objects.isNull(tableField) ? ObjectUtil.toUnderScoreCase(name) : tableField.value());
+                    if (Objects.equals("java.lang.String",field.getType().getName())
+                            && StringUtils.isNotBlank((String) value)) {
+                        mpjLambdaWrapper = mpjLambdaWrapper.like(column, value);
+                    } else if ( Objects.equals("java.lang.Integer",field.getType() .getName())
+                            || Objects.equals("java.lang.Long",field.getType() .getName())
+                    ) {
+                        mpjLambdaWrapper = mpjLambdaWrapper.eq(column, value);
+                    }
+                }
+                if( mpjWapperMap.size() > 0){
+                    //处理 非空条件
+                    MPJLambdaWrapper<D> finalMpjLambdaWrapper = mpjLambdaWrapper;
+                    mpjWapperMap.forEach((key, value) -> checkMpjWrapper(finalMpjLambdaWrapper, value));
+                }
+            }
+        }catch (Exception ex ){
+            ex.printStackTrace();
+        }
+        return mpjLambdaWrapper;
+    }
+    /**
+     * @param mpjWrapper
+     * @param wrapper
+     */
+    private void checkMpjWrapper(MPJLambdaWrapper<D> mpjWrapper ,MpjWrapper wrapper  ){
+        try {
+            MpjWapper field = wrapper.getField();
+            String alias = wrapper.getAlias();
+            String value =(StringUtils.isNotBlank(alias)?alias+".":"")+ field.value();
+            String data = wrapper.getData();
+            if(StringUtils.isBlank(data) || data.replaceAll(",","").length() == 0){
+                return ;
+            }
+            switch (field.operator()) {
+                case EQ:
+                    Object eqVal = checkColumnType(wrapper, data);
+                    mpjWrapper.eq(value, eqVal);
+                    break;
+                case NE:
+                    Object neVal = checkColumnType(wrapper, data);
+                    mpjWrapper.ne(value, neVal);
+                    break;
+                case BETWEEN:
+                    String[] split = data.split(",");
+                    if (split.length != 2) {
+                        throw new ServiceException("参数错误");
+                    }
+                    Object val1 = checkColumnType(wrapper, split[0]);
+                    Object val2 = checkColumnType(wrapper, split[1]);
+                    mpjWrapper.between(value, val1, val2);
+                    break;
+            }
+        }catch (Exception ex){
+            throw new ServiceException("参数错误");
+        }
+    }
+
+    /**
+     * @param wrapper
+     * @param data
+     * @return
+     */
+    private Object checkColumnType(MpjWrapper wrapper ,Object data){
+        Object obj = data;
+        boolean isSame = data.getClass().getName().contains(wrapper.getField().columnType().name());
+        if(isSame){
+            return obj;
+        }
+        switch (wrapper.getField().columnType()) {
+            case STRING:
+                obj = data.toString();
+                break;
+            case NUMBER:
+                obj = Double.parseDouble(data.toString());
+                break;
+            case DATE:
+                obj = DateUtils.parseDate(data.toString());
+                break;
+            case BOOLEAN:
+                obj = Boolean.parseBoolean(data.toString());
+                break;
+        }
+        return obj;
+    }
+
+}

+ 13 - 0
py-base/src/main/java/com/poyee/common/CanonicalHostNamePropertyDefiner.java

@@ -0,0 +1,13 @@
+package com.poyee.common;
+
+import ch.qos.logback.core.PropertyDefinerBase;
+import com.poyee.util.IpUtils;
+
+public class CanonicalHostNamePropertyDefiner extends PropertyDefinerBase {
+
+    @Override
+    public String getPropertyValue() {
+        return IpUtils.getHostName();
+    }
+
+}

+ 64 - 0
py-base/src/main/java/com/poyee/common/Result.java

@@ -0,0 +1,64 @@
+package com.poyee.common;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * result
+ */
+@Data
+@ToString
+@NoArgsConstructor
+public class Result implements Serializable {
+
+    private Integer code = 0;
+
+    private String msg = "SUCCESS";
+
+    private boolean success = true;
+
+    private Map<String, Object> data = new HashMap<>();
+
+    public static Result ok() {
+        return new Result();
+    }
+
+    public static Result ok(String msg) {
+        Result result = new Result();
+        result.setMsg(msg);
+        return result;
+    }
+
+    public static Result ok(Integer code, String msg) {
+        Result result = new Result();
+        result.setCode(code);
+        result.setMsg(msg);
+        return result;
+    }
+
+    public static Result error(Integer code, String msg) {
+        Result result = new Result();
+        result.setCode(code);
+        result.setMsg(msg);
+        result.setSuccess(false);
+        return result;
+    }
+
+    public Result put(String key, Object value) {
+        if (null == this.data) this.data = new HashMap<>();
+        this.data.put(key, value);
+        return this;
+    }
+
+    public Object get(String key) {
+        if (null == this.data) return null;
+        return this.data.get(key);
+    }
+
+
+}

+ 97 - 0
py-base/src/main/java/com/poyee/common/auth/UserRoleUtil.java

@@ -0,0 +1,97 @@
+package com.poyee.common.auth;
+
+import com.alibaba.fastjson.JSONObject;
+import com.poyee.base.dto.UserInfo;
+import com.poyee.common.enums.DataAuth;
+import com.poyee.common.enums.Roles;
+import com.poyee.common.exception.AuthException;
+import com.poyee.util.ServletUtils;
+import com.poyee.util.StringUtils;
+import lombok.Data;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @Author: poy
+ * @Description:
+ */
+@Slf4j
+@Data
+public class UserRoleUtil {
+
+    private String whitelistId;
+
+    public static UserRoleUtil builder(String whitelistId){
+        UserRoleUtil userRoleUtil = new UserRoleUtil();
+        userRoleUtil.whitelistId = whitelistId;
+        return userRoleUtil;
+    }
+
+    /**
+     * 权限判断
+     * @param pass_aud
+     * @param methodRoles
+     * @return
+     */
+    public boolean checkRole(String pass_aud, List<Roles> methodRoles){
+        UserInfo userInfo = ServletUtils.getUserInfo();
+        if (null == userInfo || null == userInfo.getId()) {
+            throw new AuthException(405, "请重新登录[405]!");
+        }
+        if(checkUserForWhiteList(userInfo.getId())){
+            return true;
+        }
+        //判断用户是否属于非验证来源
+        if(StringUtils.isNotBlank(pass_aud)){
+            if(Objects.equals(pass_aud, userInfo.getAud())){
+                return true;
+            }
+        }
+        String roleCode = userInfo.getRoleCode();
+        if(StringUtils.isBlank(roleCode) && StringUtils.isBlank(userInfo.getRole())){
+            throw new AuthException(401,"无权操作[401]");
+        }
+        roleCode = StringUtils.isBlank(roleCode) ? userInfo.getRole() : roleCode;
+        //用户角色
+        List<Roles> myRoles = Roles.checkMyRoles(roleCode);
+        //符合角色
+        List<Roles> hasRole = null;
+        if (myRoles != null) {
+            hasRole = methodRoles.stream().filter(myRoles::contains).collect(Collectors.toList());
+        }
+        log.info(" 当前用户存在的角色[{}] ",hasRole);
+        if (hasRole != null && hasRole.contains(Roles.ADMIN)) {
+            userInfo.setDataAuth(DataAuth.ALL);
+            return CollectionUtils.isNotEmpty(hasRole);
+        }
+        if (hasRole != null && hasRole.contains(Roles.SHIPPING)) {
+            //如果有商家权限 则设置 商家id
+            userInfo.setDataAuth(DataAuth.DEPT);
+            userInfo.setMerchant(true);
+            String merchantName = StringUtils.isBlank(userInfo.getMerchantName()) ? userInfo.getDisplayName() : userInfo.getMerchantName();
+            merchantName = StringUtils.isNotBlank(merchantName) ? merchantName : userInfo.getNickname();
+            userInfo.setMerchantName(merchantName);
+        }
+        ServletUtils.getSession().setAttribute("userInfo", JSONObject.toJSONString(userInfo));
+        return CollectionUtils.isNotEmpty(hasRole);
+    }
+
+    /**
+     * 检查用户是否在白名单中
+     * @param id
+     * @return
+     */
+    private boolean checkUserForWhiteList(@NonNull Integer id ){
+        if(StringUtils.isNotBlank(whitelistId)){
+            List<String> whitelistIds = Arrays.asList(whitelistId.split(","));
+            return whitelistIds.contains(id.toString());
+        }
+        return false;
+    }
+}

+ 18 - 0
py-base/src/main/java/com/poyee/common/enums/BusinessStatus.java

@@ -0,0 +1,18 @@
+package com.poyee.common.enums;
+
+/**
+ * 操作状态
+ *
+ * @author zheng
+ */
+public enum BusinessStatus {
+    /**
+     * 成功
+     */
+    SUCCESS,
+
+    /**
+     * 失败
+     */
+    FAIL,
+}

+ 68 - 0
py-base/src/main/java/com/poyee/common/enums/BusinessType.java

@@ -0,0 +1,68 @@
+package com.poyee.common.enums;
+
+/**
+ * 业务操作类型
+ *
+ * @author zheng
+ */
+public enum BusinessType {
+    /**
+     * 其它
+     */
+    OTHER,
+
+    /**
+     * 新增
+     */
+    INSERT,
+
+    /**
+     * 修改
+     */
+    UPDATE,
+
+    /**
+     * 删除
+     */
+    DELETE,
+
+    /**
+     * 授权
+     */
+    GRANT,
+
+    /**
+     * 导出
+     */
+    EXPORT,
+
+    /**
+     * 导入
+     */
+    IMPORT,
+
+    /**
+     * 强退
+     */
+    FORCE,
+
+    /**
+     * 生成代码
+     */
+    GENCODE,
+
+    /**
+     * 清空
+     */
+    CLEAN,
+    /**
+     * 查询
+     */
+    SEARCH,
+    /**
+     * 调用接口
+     */
+    INTERFACE,
+
+
+}

Vissa filer visades inte eftersom för många filer har ändrats