소스 검색

初始化

523096025 1 년 전
커밋
82153a1d5f
100개의 변경된 파일6003개의 추가작업 그리고 0개의 파일을 삭제
  1. 39 0
      .gitignore
  2. 105 0
      admin/pom.xml
  3. 22 0
      admin/src/main/java/com/your/packages/AdminApplication.java
  4. 40 0
      admin/src/main/java/com/your/packages/admin/captcha/TianaiCaptchaEndpoint.java
  5. 57 0
      admin/src/main/java/com/your/packages/admin/captcha/TianaiCaptchaResourceStore.java
  6. 41 0
      admin/src/main/java/com/your/packages/admin/captcha/TianaiCaptchaValidator.java
  7. 40 0
      admin/src/main/java/com/your/packages/admin/captcha/anji/AnjiCaptchaValidator.java
  8. 49 0
      admin/src/main/java/com/your/packages/admin/captcha/anji/CaptchaCacheServiceRedisImpl.java
  9. 35 0
      admin/src/main/java/com/your/packages/admin/captcha/tianai/TianaiCaptchaEndpoint.java
  10. 70 0
      admin/src/main/java/com/your/packages/admin/captcha/tianai/TianaiCaptchaResourceStore.java
  11. 41 0
      admin/src/main/java/com/your/packages/admin/captcha/tianai/TianaiCaptchaValidator.java
  12. 21 0
      admin/src/main/java/com/your/packages/admin/config/MailNotifyConfig.java
  13. 40 0
      admin/src/main/java/com/your/packages/admin/config/RedisConfiguration.java
  14. 117 0
      admin/src/main/java/com/your/packages/admin/datascope/CustomDataScope.java
  15. 29 0
      admin/src/main/java/com/your/packages/admin/datascope/CustomUserInfoCoordinator.java
  16. 21 0
      admin/src/main/java/com/your/packages/admin/datascope/DataScopeProcessor.java
  17. 50 0
      admin/src/main/java/com/your/packages/admin/datascope/DataScopeTypeEnum.java
  18. 123 0
      admin/src/main/java/com/your/packages/admin/datascope/SampleDataScopeProcessor.java
  19. 35 0
      admin/src/main/java/com/your/packages/admin/datascope/UserDataScope.java
  20. 55 0
      admin/src/main/java/com/your/packages/admin/excel/ExcelCustomHeaderController.java
  21. 66 0
      admin/src/main/java/com/your/packages/admin/excel/ExcelI18nController.java
  22. 78 0
      admin/src/main/java/com/your/packages/admin/excel/ExcelMultiSheetController.java
  23. 40 0
      admin/src/main/java/com/your/packages/admin/excel/SimpleDataHeadGenerator.java
  24. 46 0
      admin/src/main/java/com/your/packages/admin/modules/test/AdminDemoJobHandler.java
  25. 58 0
      admin/src/main/java/com/your/packages/admin/modules/test/AuthTestController.java
  26. 57 0
      admin/src/main/java/com/your/packages/admin/modules/test/CacheTestController.java
  27. 28 0
      admin/src/main/java/com/your/packages/admin/modules/test/FileUploadTestController.java
  28. 51 0
      admin/src/main/java/com/your/packages/admin/modules/test/I18nTestController.java
  29. 24 0
      admin/src/main/java/com/your/packages/admin/modules/test/IdempotentTestController.java
  30. 27 0
      admin/src/main/java/com/your/packages/admin/modules/test/OAuth2ClientTestController.java
  31. 23 0
      admin/src/main/java/com/your/packages/admin/oauth2/CustomerOAuth2AuthorizationObjectMapperCustomizer.java
  32. 49 0
      admin/src/main/java/com/your/packages/admin/oauth2/jackson2/UserDataScopeDeserializer.java
  33. 19 0
      admin/src/main/java/com/your/packages/admin/oauth2/jackson2/UserDataScopeMixin.java
  34. 73 0
      admin/src/main/java/com/your/packages/admin/sample/controller/DocumentController.java
  35. 25 0
      admin/src/main/java/com/your/packages/admin/sample/converter/DocumentConverter.java
  36. 25 0
      admin/src/main/java/com/your/packages/admin/sample/listener/SampleEventListener.java
  37. 62 0
      admin/src/main/java/com/your/packages/admin/sample/mapper/DocumentMapper.java
  38. 65 0
      admin/src/main/java/com/your/packages/admin/sample/model/entity/Document.java
  39. 26 0
      admin/src/main/java/com/your/packages/admin/sample/model/qo/DocumentQO.java
  40. 67 0
      admin/src/main/java/com/your/packages/admin/sample/model/vo/DocumentPageVO.java
  41. 33 0
      admin/src/main/java/com/your/packages/admin/sample/service/DocumentService.java
  42. 55 0
      admin/src/main/java/com/your/packages/admin/sample/service/impl/DocumentServiceImpl.java
  43. 158 0
      admin/src/main/java/com/your/packages/alibaba/excel/read/processor/DefaultAnalysisEventProcessor.java
  44. 33 0
      admin/src/main/java/com/your/packages/datascope/DataScope.java
  45. 56 0
      admin/src/main/java/com/your/packages/datascope/DataScopeAutoConfiguration.java
  46. 36 0
      admin/src/main/java/com/your/packages/datascope/annotation/DataPermission.java
  47. 16 0
      admin/src/main/java/com/your/packages/datascope/function/Action.java
  48. 16 0
      admin/src/main/java/com/your/packages/datascope/function/ResultAction.java
  49. 37 0
      admin/src/main/java/com/your/packages/datascope/handler/DataPermissionHandler.java
  50. 80 0
      admin/src/main/java/com/your/packages/datascope/handler/DataPermissionRule.java
  51. 94 0
      admin/src/main/java/com/your/packages/datascope/handler/DefaultDataPermissionHandler.java
  52. 69 0
      admin/src/main/java/com/your/packages/datascope/holder/DataPermissionRuleHolder.java
  53. 60 0
      admin/src/main/java/com/your/packages/datascope/holder/DataScopeMatchNumHolder.java
  54. 58 0
      admin/src/main/java/com/your/packages/datascope/holder/MappedStatementIdsWithoutDataScope.java
  55. 34 0
      admin/src/main/java/com/your/packages/datascope/interceptor/DataPermissionAnnotationAdvisor.java
  56. 41 0
      admin/src/main/java/com/your/packages/datascope/interceptor/DataPermissionAnnotationInterceptor.java
  57. 118 0
      admin/src/main/java/com/your/packages/datascope/interceptor/DataPermissionFinder.java
  58. 92 0
      admin/src/main/java/com/your/packages/datascope/interceptor/DataPermissionInterceptor.java
  59. 108 0
      admin/src/main/java/com/your/packages/datascope/parser/JsqlParserSupport.java
  60. 553 0
      admin/src/main/java/com/your/packages/datascope/processor/DataScopeSqlProcessor.java
  61. 64 0
      admin/src/main/java/com/your/packages/datascope/util/AnnotationUtil.java
  62. 33 0
      admin/src/main/java/com/your/packages/datascope/util/CollectionUtils.java
  63. 67 0
      admin/src/main/java/com/your/packages/datascope/util/DataPermissionUtils.java
  64. 166 0
      admin/src/main/java/com/your/packages/datascope/util/PluginUtils.java
  65. 61 0
      admin/src/main/java/com/your/packages/datascope/util/SqlParseUtils.java
  66. 85 0
      admin/src/main/java/com/your/packages/hccake/common/excel/ExcelHandlerConfiguration.java
  67. 98 0
      admin/src/main/java/com/your/packages/hccake/common/excel/ResponseExcelAutoConfiguration.java
  68. 47 0
      admin/src/main/java/com/your/packages/hccake/common/excel/annotation/RequestExcel.java
  69. 97 0
      admin/src/main/java/com/your/packages/hccake/common/excel/annotation/ResponseExcel.java
  70. 39 0
      admin/src/main/java/com/your/packages/hccake/common/excel/annotation/Sheet.java
  71. 46 0
      admin/src/main/java/com/your/packages/hccake/common/excel/aop/DynamicNameAspect.java
  72. 92 0
      admin/src/main/java/com/your/packages/hccake/common/excel/aop/RequestExcelArgumentResolver.java
  73. 61 0
      admin/src/main/java/com/your/packages/hccake/common/excel/aop/ResponseExcelReturnValueHandler.java
  74. 20 0
      admin/src/main/java/com/your/packages/hccake/common/excel/config/ExcelConfigProperties.java
  75. 62 0
      admin/src/main/java/com/your/packages/hccake/common/excel/converters/LocalDateStringConverter.java
  76. 93 0
      admin/src/main/java/com/your/packages/hccake/common/excel/converters/LocalDateTimeStringConverter.java
  77. 41 0
      admin/src/main/java/com/your/packages/hccake/common/excel/domain/ErrorMessage.java
  78. 53 0
      admin/src/main/java/com/your/packages/hccake/common/excel/domain/SheetBuildProperties.java
  79. 48 0
      admin/src/main/java/com/your/packages/hccake/common/excel/enhance/DefaultWriterBuilderEnhancer.java
  80. 42 0
      admin/src/main/java/com/your/packages/hccake/common/excel/enhance/WriterBuilderEnhancer.java
  81. 257 0
      admin/src/main/java/com/your/packages/hccake/common/excel/handler/AbstractSheetWriteHandler.java
  82. 63 0
      admin/src/main/java/com/your/packages/hccake/common/excel/handler/DefaultAnalysisEventListener.java
  83. 27 0
      admin/src/main/java/com/your/packages/hccake/common/excel/handler/ListAnalysisEventListener.java
  84. 104 0
      admin/src/main/java/com/your/packages/hccake/common/excel/handler/ManySheetWriteHandler.java
  85. 44 0
      admin/src/main/java/com/your/packages/hccake/common/excel/handler/SheetWriteHandler.java
  86. 86 0
      admin/src/main/java/com/your/packages/hccake/common/excel/handler/SingleSheetWriteHandler.java
  87. 15 0
      admin/src/main/java/com/your/packages/hccake/common/excel/head/EmptyHeadGenerator.java
  88. 22 0
      admin/src/main/java/com/your/packages/hccake/common/excel/head/HeadGenerator.java
  89. 31 0
      admin/src/main/java/com/your/packages/hccake/common/excel/head/HeadMeta.java
  90. 61 0
      admin/src/main/java/com/your/packages/hccake/common/excel/head/I18nHeaderCellWriteHandler.java
  91. 15 0
      admin/src/main/java/com/your/packages/hccake/common/excel/kit/ExcelException.java
  92. 37 0
      admin/src/main/java/com/your/packages/hccake/common/excel/kit/Validators.java
  93. 20 0
      admin/src/main/java/com/your/packages/hccake/common/excel/processor/NameProcessor.java
  94. 40 0
      admin/src/main/java/com/your/packages/hccake/common/excel/processor/NameSpelExpressionProcessor.java
  95. 23 0
      admin/src/main/resources/application-dev.yml
  96. 25 0
      admin/src/main/resources/application-prod.yml
  97. 22 0
      admin/src/main/resources/application-test.yml
  98. 110 0
      admin/src/main/resources/application.yml
  99. BIN
      admin/src/main/resources/bgimages/48.jpg
  100. 0 0
      admin/src/main/resources/bgimages/a.jpg

+ 39 - 0
.gitignore

@@ -0,0 +1,39 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+**/src/main/**/target/
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+
+### custom ###
+.flattened-pom.xml
+/**/application-local.yml
+/logs/

+ 105 - 0
admin/pom.xml

@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<parent>
+		<artifactId>huimv.farm.nongkeyuan</artifactId>
+		<groupId>com.your.packages</groupId>
+		<version>${revision}</version>
+	</parent>
+	<modelVersion>4.0.0</modelVersion>
+
+	<artifactId>admin</artifactId>
+
+	<properties>
+		<knife4j.version>3.0.3</knife4j.version>
+		<tianai-captcha.version>1.4.1</tianai-captcha.version>
+	</properties>
+
+	<dependencies>
+		<!-- 基于 spring authorization server 的授权服务器 -->
+		<dependency>
+			<groupId>com.hccake</groupId>
+			<artifactId>ballcat-spring-security-oauth2-authorization-server</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.hccake</groupId>
+			<artifactId>ballcat-admin-core</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.hccake</groupId>
+			<artifactId>ballcat-admin-websocket</artifactId>
+		</dependency>
+
+		<!--mysql驱动-->
+		<dependency>
+			<groupId>com.mysql</groupId>
+			<artifactId>mysql-connector-j</artifactId>
+		</dependency>
+
+		<!-- openapi 扩展处理 -->
+		<dependency>
+			<groupId>com.hccake</groupId>
+			<artifactId>ballcat-extend-openapi</artifactId>
+		</dependency>
+		<!-- springdoc swagger-ui -->
+		<dependency>
+			<groupId>org.springdoc</groupId>
+			<artifactId>springdoc-openapi-ui</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springdoc</groupId>
+			<artifactId>springdoc-openapi-security</artifactId>
+		</dependency>
+		<!-- swagger 增强版 ui -->
+		<dependency>
+			<groupId>com.github.xiaoymin</groupId>
+			<artifactId>knife4j-springdoc-ui</artifactId>
+			<version>${knife4j.version}</version>
+		</dependency>
+
+		<!-- tianai 图形验证码 -->
+		<dependency>
+			<groupId>cloud.tianai.captcha</groupId>
+			<artifactId>tianai-captcha-springboot-starter</artifactId>
+			<version>${tianai-captcha.version}</version>
+		</dependency>
+
+		<!-- anji 图形验证码 -->
+		<dependency>
+			<groupId>com.anji-plus</groupId>
+			<artifactId>spring-boot-starter-captcha</artifactId>
+			<version>1.3.0</version>
+		</dependency>
+
+		<dependency>
+			<groupId>cn.hutool</groupId>
+			<artifactId>hutool-json</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>com.hccake</groupId>
+			<artifactId>ballcat-spring-boot-starter-easyexcel</artifactId>
+		</dependency>
+		<!-- API, java.xml.bind module -->
+		<!-- add it when jdk11 -->
+<!--		<dependency>-->
+<!--			<groupId>jakarta.xml.bind</groupId>-->
+<!--			<artifactId>jakarta.xml.bind-api</artifactId>-->
+<!--		</dependency>-->
+<!--		<dependency>-->
+<!--			<groupId>org.glassfish.jaxb</groupId>-->
+<!--			<artifactId>jaxb-runtime</artifactId>-->
+<!--		</dependency>-->
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>

+ 22 - 0
admin/src/main/java/com/your/packages/AdminApplication.java

@@ -0,0 +1,22 @@
+package com.your.packages;
+
+import org.ballcat.springsecurity.oauth2.server.authorization.annotation.EnableOauth2AuthorizationServer;
+import org.ballcat.springsecurity.oauth2.server.resource.annotation.EnableOauth2ResourceServer;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author Hccake
+ */
+@EnableOauth2AuthorizationServer
+@EnableOauth2ResourceServer
+@MapperScan({ "com.your.packages.**.mapper" })
+@SpringBootApplication
+public class AdminApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(AdminApplication.class, args);
+	}
+
+}

+ 40 - 0
admin/src/main/java/com/your/packages/admin/captcha/TianaiCaptchaEndpoint.java

@@ -0,0 +1,40 @@
+package com.your.packages.admin.captcha;
+
+import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
+import cloud.tianai.captcha.spring.application.ImageCaptchaApplication;
+import cloud.tianai.captcha.spring.vo.CaptchaResponse;
+import cloud.tianai.captcha.spring.vo.ImageCaptchaVO;
+import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/captcha/tianai")
+@RequiredArgsConstructor
+public class TianaiCaptchaEndpoint {
+
+	private final ImageCaptchaApplication imageCaptchaApplication;
+
+	@GetMapping("/gen")
+	@ResponseBody
+	public CaptchaResponse<ImageCaptchaVO> genCaptcha(@RequestParam(value = "type", required = false) String type) {
+		if (StringUtils.isBlank(type)) {
+			type = CaptchaTypeConstant.SLIDER;
+		}
+		return imageCaptchaApplication.generateCaptcha(type);
+	}
+
+	@PostMapping("/check")
+	@ResponseBody
+	public boolean checkCaptcha(@RequestParam("id") String id, @RequestBody ImageCaptchaTrack imageCaptchaTrack) {
+		return imageCaptchaApplication.matching(id, imageCaptchaTrack).isSuccess();
+	}
+
+}

+ 57 - 0
admin/src/main/java/com/your/packages/admin/captcha/TianaiCaptchaResourceStore.java

@@ -0,0 +1,57 @@
+package com.your.packages.admin.captcha;
+
+import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
+import cloud.tianai.captcha.generator.common.constant.SliderCaptchaConstant;
+import cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator;
+import cloud.tianai.captcha.resource.common.model.dto.Resource;
+import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
+import cloud.tianai.captcha.resource.impl.DefaultResourceStore;
+import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
+import org.springframework.stereotype.Component;
+
+import static cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH;
+
+@Component
+public class TianaiCaptchaResourceStore extends DefaultResourceStore {
+
+	public TianaiCaptchaResourceStore() {
+
+		// 滑块验证码 模板 (系统内置)
+		ResourceMap template1 = new ResourceMap("default",4);
+		template1.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+				DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/active.png")));
+		template1.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+				DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/fixed.png")));
+		ResourceMap template2 = new ResourceMap("default",4);
+		template2.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+				DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/active.png")));
+		template2.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+				DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/fixed.png")));
+		// 旋转验证码 模板 (系统内置)
+		ResourceMap template3 = new ResourceMap("default",4);
+		template3.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+				StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/active.png")));
+		template3.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+				StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/fixed.png")));
+
+		// 1. 添加一些模板
+
+		addTemplate(CaptchaTypeConstant.SLIDER, template1);
+		addTemplate(CaptchaTypeConstant.SLIDER, template2);
+		addTemplate(CaptchaTypeConstant.ROTATE, template3);
+		// 2. 添加自定义背景图片
+		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/a.jpg", "default"));
+		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/b.jpg", "default"));
+		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/c.jpg", "default"));
+		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/d.jpg", "default"));
+		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/e.jpg", "default"));
+		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/g.jpg", "default"));
+		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/h.jpg", "default"));
+		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/i.jpg", "default"));
+		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/j.jpg", "default"));
+		addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/48.jpg", "default"));
+		addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/48.jpg", "default"));
+		addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/c.jpg", "default"));
+	}
+
+}

+ 41 - 0
admin/src/main/java/com/your/packages/admin/captcha/TianaiCaptchaValidator.java

@@ -0,0 +1,41 @@
+package com.your.packages.admin.captcha;
+
+import cloud.tianai.captcha.spring.application.ImageCaptchaApplication;
+import cloud.tianai.captcha.spring.plugins.secondary.SecondaryVerificationApplication;
+import cn.hutool.core.text.CharSequenceUtil;
+import lombok.RequiredArgsConstructor;
+import org.ballcat.security.captcha.CaptchaValidateResult;
+import org.ballcat.security.captcha.CaptchaValidator;
+import org.springframework.context.annotation.Primary;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * tianai 验证码的校验器
+ *
+ * @author whace
+ */
+@Component
+@Primary
+@RequiredArgsConstructor
+public class TianaiCaptchaValidator implements CaptchaValidator {
+
+	private final ImageCaptchaApplication sca;
+
+	@Override
+	public CaptchaValidateResult validate(HttpServletRequest request) {
+		String captchaId = request.getParameter("captchaId");
+		if (CharSequenceUtil.isBlank(captchaId)) {
+			return CaptchaValidateResult.failure("captcha id can not be null");
+		}
+
+		if (!(sca instanceof SecondaryVerificationApplication)) {
+			return CaptchaValidateResult.failure("captcha must enable secondary verification");
+		}
+
+		boolean match = ((SecondaryVerificationApplication) sca).secondaryVerification(captchaId);
+		return match ? CaptchaValidateResult.success() : CaptchaValidateResult.failure("captcha validate failure");
+	}
+
+}

+ 40 - 0
admin/src/main/java/com/your/packages/admin/captcha/anji/AnjiCaptchaValidator.java

@@ -0,0 +1,40 @@
+package com.your.packages.admin.captcha.anji;
+
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.service.CaptchaService;
+import lombok.RequiredArgsConstructor;
+import org.ballcat.security.captcha.CaptchaValidateResult;
+import org.ballcat.security.captcha.CaptchaValidator;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * anji-plus 的验证码校验器
+ *
+ * @author hccake
+ */
+// @Primary
+@Component
+@RequiredArgsConstructor
+public class AnjiCaptchaValidator implements CaptchaValidator {
+
+	private static final String CAPTCHA_VERIFICATION_PARAM = "captchaVerification";
+
+	private final CaptchaService captchaService;
+
+	@Override
+	public CaptchaValidateResult validate(HttpServletRequest request) {
+		// 获取验证码参数
+		String captchaVerification = request.getParameter(CAPTCHA_VERIFICATION_PARAM);
+
+		// anji 的校验处理
+		CaptchaVO captchaVO = new CaptchaVO();
+		captchaVO.setCaptchaVerification(captchaVerification);
+		ResponseModel responseModel = captchaService.verification(captchaVO);
+
+		return new CaptchaValidateResult(responseModel.isSuccess(), responseModel.getRepMsg());
+	}
+
+}

+ 49 - 0
admin/src/main/java/com/your/packages/admin/captcha/anji/CaptchaCacheServiceRedisImpl.java

@@ -0,0 +1,49 @@
+package com.your.packages.admin.captcha.anji;
+
+import com.anji.captcha.service.CaptchaCacheService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 对于分布式部署的应用,我们建议应用自己实现CaptchaCacheService,比如用Redis,参考service/spring-boot代码示例。
+ * 如果应用是单点的,也没有使用redis,那默认使用内存。 内存缓存只适合单节点部署的应用,否则验证码生产与验证在节点之间信息不同步,导致失败。
+ *
+ * ☆☆☆ SPI: 在resources目录新建META-INF.services文件夹(两层),参考当前服务resources。
+ *
+ * @Title: 使用redis缓存
+ * @author lide1202@hotmail.com
+ * @date 2020-05-12
+ */
+public class CaptchaCacheServiceRedisImpl implements CaptchaCacheService {
+
+	@Override
+	public String type() {
+		return "redis";
+	}
+
+	@Autowired
+	private StringRedisTemplate stringRedisTemplate;
+
+	@Override
+	public void set(String key, String value, long expiresInSeconds) {
+		stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS);
+	}
+
+	@Override
+	public boolean exists(String key) {
+		return stringRedisTemplate.hasKey(key);
+	}
+
+	@Override
+	public void delete(String key) {
+		stringRedisTemplate.delete(key);
+	}
+
+	@Override
+	public String get(String key) {
+		return stringRedisTemplate.opsForValue().get(key);
+	}
+
+}

+ 35 - 0
admin/src/main/java/com/your/packages/admin/captcha/tianai/TianaiCaptchaEndpoint.java

@@ -0,0 +1,35 @@
+//package com.your.packages.admin.captcha.tianai;
+//
+//import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
+//import cloud.tianai.captcha.common.response.ApiResponse;
+//import cloud.tianai.captcha.spring.application.ImageCaptchaApplication;
+//import cloud.tianai.captcha.spring.vo.CaptchaResponse;
+//import cloud.tianai.captcha.spring.vo.ImageCaptchaVO;
+//import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
+//import lombok.RequiredArgsConstructor;
+//import org.apache.commons.lang3.StringUtils;
+//import org.springframework.web.bind.annotation.*;
+//
+//@RestController
+//@RequestMapping("/captcha/tianai")
+//@RequiredArgsConstructor
+//public class TianaiCaptchaEndpoint {
+//
+//	private final ImageCaptchaApplication imageCaptchaApplication;
+//
+//	@GetMapping("/gen")
+//	@ResponseBody
+//	public CaptchaResponse<ImageCaptchaVO> genCaptcha(@RequestParam(value = "type", required = false) String type) {
+//		if (StringUtils.isBlank(type)) {
+//			type = CaptchaTypeConstant.SLIDER;
+//		}
+//		return imageCaptchaApplication.generateCaptcha(type);
+//	}
+//
+//	@PostMapping("/check")
+//	@ResponseBody
+//	public ApiResponse<?> checkCaptcha(@RequestParam("id") String id, @RequestBody ImageCaptchaTrack imageCaptchaTrack) {
+//		return imageCaptchaApplication.matching(id, imageCaptchaTrack);
+//	}
+//
+//}

+ 70 - 0
admin/src/main/java/com/your/packages/admin/captcha/tianai/TianaiCaptchaResourceStore.java

@@ -0,0 +1,70 @@
+//package com.your.packages.admin.captcha.tianai;
+//
+//import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
+//import cloud.tianai.captcha.generator.common.constant.SliderCaptchaConstant;
+//import cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator;
+//import cloud.tianai.captcha.resource.common.model.dto.Resource;
+//import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
+//import cloud.tianai.captcha.resource.impl.DefaultResourceStore;
+//import cloud.tianai.captcha.resource.impl.provider.ClassPathResourceProvider;
+//import org.springframework.stereotype.Component;
+//
+//import java.util.HashMap;
+//import java.util.Map;
+//
+//import static cloud.tianai.captcha.generator.impl.StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH;
+//
+//@Component
+//public class TianaiCaptchaResourceStore extends DefaultResourceStore {
+//
+//	public TianaiCaptchaResourceStore() {
+//
+//		// 滑块验证码 模板 (系统内置)
+////		Map<String, Resource> template1 = new HashMap<>(4);
+//		ResourceMap template1 = new ResourceMap();
+//		template1.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+//				DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/active.png")));
+//		template1.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+//				DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/fixed.png")));
+//		template1.put(SliderCaptchaConstant.OBFUSCATE_TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+//				DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/1/matrix.png")));
+////		Map<String, Resource> template2 = new HashMap<>(4);
+//		ResourceMap template2 = new ResourceMap();
+//		template2.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+//				DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/active.png")));
+//		template2.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+//				DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/fixed.png")));
+//		template2.put(SliderCaptchaConstant.OBFUSCATE_TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+//				DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/2/matrix.png")));
+//		// 旋转验证码 模板 (系统内置)
+////		Map<String, Resource> template3 = new HashMap<>(4);
+//		ResourceMap template3 = new ResourceMap();
+//		template3.put(SliderCaptchaConstant.TEMPLATE_ACTIVE_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+//				StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/active.png")));
+//		template3.put(SliderCaptchaConstant.TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+//				StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/fixed.png")));
+//		template3.put(SliderCaptchaConstant.OBFUSCATE_TEMPLATE_FIXED_IMAGE_NAME, new Resource(ClassPathResourceProvider.NAME,
+//				StandardSliderImageCaptchaGenerator.DEFAULT_SLIDER_IMAGE_TEMPLATE_PATH.concat("/3/matrix.png")));
+//
+//		// 1. 添加一些模板
+//
+//
+//		addTemplate(CaptchaTypeConstant.SLIDER, template1);
+//		addTemplate(CaptchaTypeConstant.SLIDER, template2);
+//		addTemplate(CaptchaTypeConstant.ROTATE, template3);
+//		// 2. 添加自定义背景图片
+//		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/a.jpg"));
+//		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/b.jpg"));
+//		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/c.jpg"));
+//		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/d.jpg"));
+//		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/e.jpg"));
+//		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/g.jpg"));
+//		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/h.jpg"));
+//		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/i.jpg"));
+//		addResource(CaptchaTypeConstant.SLIDER, new Resource("classpath", "bgimages/j.jpg"));
+//		addResource(CaptchaTypeConstant.ROTATE, new Resource("classpath", "bgimages/48.jpg"));
+//		addResource(CaptchaTypeConstant.CONCAT, new Resource("classpath", "bgimages/48.jpg"));
+//		addResource(CaptchaTypeConstant.WORD_IMAGE_CLICK, new Resource("classpath", "bgimages/c.jpg"));
+//	}
+//
+//}

+ 41 - 0
admin/src/main/java/com/your/packages/admin/captcha/tianai/TianaiCaptchaValidator.java

@@ -0,0 +1,41 @@
+//package com.your.packages.admin.captcha.tianai;
+//
+//import cloud.tianai.captcha.spring.application.ImageCaptchaApplication;
+//import cloud.tianai.captcha.spring.plugins.secondary.SecondaryVerificationApplication;
+//import cn.hutool.core.util.StrUtil;
+//import lombok.RequiredArgsConstructor;
+//import org.ballcat.security.captcha.CaptchaValidateResult;
+//import org.ballcat.security.captcha.CaptchaValidator;
+//import org.springframework.context.annotation.Primary;
+//import org.springframework.stereotype.Component;
+//
+//import javax.servlet.http.HttpServletRequest;
+//
+///**
+// * tianai 验证码的校验器
+// *
+// * @author whace
+// */
+//@Primary
+//@Component
+//@RequiredArgsConstructor
+//public class TianaiCaptchaValidator implements CaptchaValidator {
+//
+//	private final ImageCaptchaApplication sca;
+//
+//	@Override
+//	public CaptchaValidateResult validate(HttpServletRequest request) {
+//		String captchaId = request.getParameter("captchaId");
+//		if (StrUtil.isBlank(captchaId)) {
+//			return CaptchaValidateResult.failure("captcha id can not be null");
+//		}
+//
+//		if (!(sca instanceof SecondaryVerificationApplication)) {
+//			return CaptchaValidateResult.failure("captcha must enable secondary verification");
+//		}
+//
+//		boolean match = ((SecondaryVerificationApplication) sca).secondaryVerification(captchaId);
+//		return match ? CaptchaValidateResult.success() : CaptchaValidateResult.failure("captcha validate failure");
+//	}
+//
+//}

+ 21 - 0
admin/src/main/java/com/your/packages/admin/config/MailNotifyConfig.java

@@ -0,0 +1,21 @@
+//package com.your.packages.admin.config;
+//
+//import com.hccake.ballcat.common.mail.sender.MailSender;
+//import com.hccake.ballcat.notify.push.MailNotifyPusher;
+//import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+//import org.springframework.context.annotation.Bean;
+//import org.springframework.context.annotation.Configuration;
+//
+///**
+// * @author hccake
+// */
+//@Configuration(proxyBeanMethods = false)
+//public class MailNotifyConfig {
+//
+//	@Bean
+//	@ConditionalOnBean(MailSender.class)
+//	public MailNotifyPusher mailNotifyPusher(MailSender mailSender) {
+//		return new MailNotifyPusher(mailSender);
+//	}
+//
+//}

+ 40 - 0
admin/src/main/java/com/your/packages/admin/config/RedisConfiguration.java

@@ -0,0 +1,40 @@
+package com.your.packages.admin.config;
+
+import com.hccake.ballcat.common.redis.prefix.IRedisPrefixConverter;
+import com.hccake.ballcat.common.redis.serialize.PrefixJdkRedisSerializer;
+import com.hccake.ballcat.common.redis.serialize.PrefixStringRedisSerializer;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ * Redis 配置类,临时修复 Tianai 验证码导致的 redis 全局前缀不生效的问题
+ *
+ * @author hccake
+ */
+@RequiredArgsConstructor
+@Configuration(proxyBeanMethods = false)
+public class RedisConfiguration {
+
+	private final RedisConnectionFactory redisConnectionFactory;
+
+	@Bean
+	public StringRedisTemplate stringRedisTemplate(IRedisPrefixConverter redisPrefixConverter) {
+		StringRedisTemplate template = new StringRedisTemplate();
+		template.setConnectionFactory(redisConnectionFactory);
+		template.setKeySerializer(new PrefixStringRedisSerializer(redisPrefixConverter));
+		return template;
+	}
+
+	@Bean
+	public RedisTemplate<Object, Object> redisTemplate(IRedisPrefixConverter redisPrefixConverter) {
+		RedisTemplate<Object, Object> template = new RedisTemplate<>();
+		template.setConnectionFactory(redisConnectionFactory);
+		template.setKeySerializer(new PrefixJdkRedisSerializer(redisPrefixConverter));
+		return template;
+	}
+
+}

+ 117 - 0
admin/src/main/java/com/your/packages/admin/datascope/CustomDataScope.java

@@ -0,0 +1,117 @@
+package com.your.packages.admin.datascope;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import com.hccake.ballcat.common.security.constant.UserAttributeNameConstants;
+import com.hccake.ballcat.common.security.userdetails.User;
+import com.hccake.ballcat.common.security.util.SecurityUtils;
+import com.your.packages.datascope.DataScope;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.Parenthesis;
+import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.expression.operators.relational.InExpression;
+import net.sf.jsqlparser.schema.Column;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+/**
+ * @author hccake
+ */
+@Component
+public class CustomDataScope implements DataScope {
+
+	private static final String USER_ID = "user_id";
+
+	private static final String ORGANIZATION_ID = "organization_id";
+
+	/**
+	 * 拥有 organization_id 字段的表名集合
+	 */
+	private static final Set<String> ORGANIZATION_ID_TABLE_NAMES = CollUtil.newHashSet("sample_document");
+
+	private final Set<String> tableNames;
+
+	public CustomDataScope() {
+		this.tableNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+		this.tableNames.add("sample_document");
+	}
+
+	@Override
+	public String getResource() {
+		return "userData";
+	}
+
+	@Override
+	public boolean includes(String tableName) {
+		return tableNames.contains(tableName);
+	}
+
+	@Override
+	public Expression getExpression(String tableName, Alias tableAlias) {
+		// 获取当前登录用户
+		User user = SecurityUtils.getUser();
+		if (user == null) {
+			return null;
+		}
+
+		UserDataScope userDataScope = getUserDataScope(user);
+
+		// 如果数据权限是全部,直接放行
+		if (userDataScope.isAllScope()) {
+			return null;
+		}
+
+		// 如果数据权限是仅自己
+		if (userDataScope.isOnlySelf()) {
+			// 数据权限规则,where user_id = xx
+			return userIdEqualsToExpression(tableAlias, user.getUserId());
+		}
+
+		// 如果当前表有组织id字段,则优先使用组织id字段控制范围
+		if (ORGANIZATION_ID_TABLE_NAMES.contains(tableName)) {
+			// 数据权限规则,where (user_id =xx or organization_id in ("x","y"))
+			EqualsTo equalsTo = userIdEqualsToExpression(tableAlias, user.getUserId());
+			Expression inExpression = getInExpression(tableAlias, ORGANIZATION_ID, userDataScope.getScopeDeptIds());
+			// 这里一定要加括号,否则如果有其他查询条件,or 会出问题
+			return new Parenthesis(new OrExpression(equalsTo, inExpression));
+		}
+		else {
+			// 数据权限规则,where user_id in ("x","y")
+			return getInExpression(tableAlias, USER_ID, userDataScope.getScopeUserIds());
+		}
+	}
+
+	private UserDataScope getUserDataScope(User user) {
+		Map<String, Object> attributes = user.getAttributes();
+		Object o = attributes.get(UserAttributeNameConstants.USER_DATA_SCOPE);
+		if (o instanceof UserDataScope) {
+			return (UserDataScope) o;
+		}
+		else {
+			return BeanUtil.toBean(o, UserDataScope.class);
+		}
+	}
+
+	private EqualsTo userIdEqualsToExpression(Alias tableAlias, Long userId) {
+		Column column = new Column(tableAlias == null ? USER_ID : tableAlias.getName() + "." + USER_ID);
+		return new EqualsTo(column, new LongValue(userId));
+	}
+
+	private Expression getInExpression(Alias tableAlias, String columnName, Set<Long> scopeUserIds) {
+		Column column = new Column(tableAlias == null ? columnName : tableAlias.getName() + "." + columnName);
+		ExpressionList expressionList = new ExpressionList();
+		List<Expression> list = scopeUserIds.stream().map(LongValue::new).collect(Collectors.toList());
+		expressionList.setExpressions(list);
+		return new InExpression(column, expressionList);
+	}
+
+}

+ 29 - 0
admin/src/main/java/com/your/packages/admin/datascope/CustomUserInfoCoordinator.java

@@ -0,0 +1,29 @@
+package com.your.packages.admin.datascope;
+
+import com.hccake.ballcat.common.security.constant.UserAttributeNameConstants;
+import com.hccake.ballcat.system.authentication.UserInfoCoordinator;
+import com.hccake.ballcat.system.model.dto.UserInfoDTO;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * @author hccake
+ */
+@Component
+@RequiredArgsConstructor
+public class CustomUserInfoCoordinator implements UserInfoCoordinator {
+
+	private final DataScopeProcessor dataScopeProcessor;
+
+	@Override
+	public Map<String, Object> coordinateAttribute(UserInfoDTO userInfoDTO, Map<String, Object> attribute) {
+		// 数据权限填充
+		UserDataScope userDataScope = dataScopeProcessor.mergeScopeType(userInfoDTO.getSysUser(),
+				userInfoDTO.getRoles());
+		attribute.put(UserAttributeNameConstants.USER_DATA_SCOPE, userDataScope);
+		return attribute;
+	}
+
+}

+ 21 - 0
admin/src/main/java/com/your/packages/admin/datascope/DataScopeProcessor.java

@@ -0,0 +1,21 @@
+package com.your.packages.admin.datascope;
+
+import com.hccake.ballcat.system.model.entity.SysRole;
+import com.hccake.ballcat.system.model.entity.SysUser;
+
+import java.util.Collection;
+
+/**
+ * @author hccake
+ */
+public interface DataScopeProcessor {
+
+	/**
+	 * 根据用户和角色信息,合并用户最终的数据权限
+	 * @param user 用户
+	 * @param roles 角色列表
+	 * @return UserDataScope
+	 */
+	UserDataScope mergeScopeType(SysUser user, Collection<SysRole> roles);
+
+}

+ 50 - 0
admin/src/main/java/com/your/packages/admin/datascope/DataScopeTypeEnum.java

@@ -0,0 +1,50 @@
+package com.your.packages.admin.datascope;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 数据权限范围类型
+ *
+ * @author hccake
+ */
+@Getter
+@AllArgsConstructor
+public enum DataScopeTypeEnum {
+
+	/**
+	 * 查询全部数据
+	 */
+	ALL(0),
+
+	/**
+	 * 本人
+	 */
+	SELF(1),
+
+	/**
+	 * 本人及子级
+	 */
+	SELF_CHILD_LEVEL(2),
+
+	/**
+	 * 本级
+	 */
+	LEVEL(3),
+
+	/**
+	 * 本级及子级
+	 */
+	LEVEL_CHILD_LEVEL(4),
+
+	/**
+	 * 自定义
+	 */
+	CUSTOM(5);
+
+	/**
+	 * 类型
+	 */
+	private final Integer type;
+
+}

+ 123 - 0
admin/src/main/java/com/your/packages/admin/datascope/SampleDataScopeProcessor.java

@@ -0,0 +1,123 @@
+package com.your.packages.admin.datascope;
+
+import com.hccake.ballcat.system.model.entity.SysOrganization;
+import com.hccake.ballcat.system.model.entity.SysRole;
+import com.hccake.ballcat.system.model.entity.SysUser;
+import com.hccake.ballcat.system.service.SysOrganizationService;
+import com.hccake.ballcat.system.service.SysUserService;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author hccake
+ */
+@Component
+@RequiredArgsConstructor
+public class SampleDataScopeProcessor implements DataScopeProcessor {
+
+	private final SysOrganizationService sysOrganizationService;
+
+	private final SysUserService sysUserService;
+
+	/**
+	 * 合并角色的数据权限类型,排除相同的权限后,大的权限覆盖小的
+	 * @param user 用户
+	 * @param roles 角色列表
+	 * @return List<Integer> 合并后的权限
+	 */
+	@Override
+	public UserDataScope mergeScopeType(SysUser user, Collection<SysRole> roles) {
+		UserDataScope userDataScope = new UserDataScope();
+		Set<Long> scopeUserIds = userDataScope.getScopeUserIds();
+		Set<Long> scopeOrganizationId = userDataScope.getScopeDeptIds();
+
+		// 任何用户都应该可以看到自己的数据
+		Long userId = user.getUserId();
+		scopeUserIds.add(userId);
+
+		if (CollectionUtils.isEmpty(roles)) {
+			return userDataScope;
+		}
+
+		// 根据角色的权限返回进行分组
+		Map<Integer, List<SysRole>> map = roles.stream().collect(Collectors.groupingBy(SysRole::getScopeType));
+
+		// 如果有全部权限,直接返回
+		if (map.containsKey(DataScopeTypeEnum.ALL.getType())) {
+			userDataScope.setAllScope(true);
+			return userDataScope;
+		}
+
+		// 如果有本级及子级,删除其包含的几类数据权限
+		boolean hasLevelChildLevel = map.containsKey(DataScopeTypeEnum.LEVEL_CHILD_LEVEL.getType());
+		if (hasLevelChildLevel) {
+			map.remove(DataScopeTypeEnum.SELF.getType());
+			map.remove(DataScopeTypeEnum.SELF_CHILD_LEVEL.getType());
+			map.remove(DataScopeTypeEnum.LEVEL.getType());
+		}
+
+		// 是否有本人及子级权限
+		boolean hasSelfChildLevel = map.containsKey(DataScopeTypeEnum.SELF_CHILD_LEVEL.getType());
+		// 是否有本级权限
+		boolean hasLevel = map.containsKey(DataScopeTypeEnum.LEVEL.getType());
+		if (hasSelfChildLevel || hasLevel) {
+			// 如果有本人及子级或者本级,都删除本人的数据权限
+			map.remove(DataScopeTypeEnum.SELF.getType());
+			// 如果同时拥有,则等于本级及子级权限
+			if (hasSelfChildLevel && hasLevel) {
+				map.remove(DataScopeTypeEnum.SELF_CHILD_LEVEL.getType());
+				map.remove(DataScopeTypeEnum.LEVEL.getType());
+				map.put(DataScopeTypeEnum.LEVEL_CHILD_LEVEL.getType(), new ArrayList<>());
+			}
+		}
+
+		// 这时如果仅仅只能看个人的,直接返回
+		if (map.size() == 1 && map.containsKey(DataScopeTypeEnum.SELF.getType())) {
+			userDataScope.setOnlySelf(true);
+			return userDataScope;
+		}
+
+		// 如果有 本级及子级 或者 本级,都把自己的 organizationId 加进去
+		Long organizationId = user.getOrganizationId();
+		if (hasLevelChildLevel || hasLevel) {
+			scopeOrganizationId.add(organizationId);
+		}
+		// 如果有 本级及子级 或者 本人及子级,都把下级组织的 organizationId 加进去
+		if (hasLevelChildLevel || hasSelfChildLevel) {
+			List<SysOrganization> childOrganizations = sysOrganizationService.listChildOrganization(organizationId);
+			if (CollectionUtils.isNotEmpty(childOrganizations)) {
+				List<Long> organizationIds = childOrganizations.stream()
+					.map(SysOrganization::getId)
+					.collect(Collectors.toList());
+				scopeOrganizationId.addAll(organizationIds);
+			}
+		}
+		// 自定义部门
+		List<SysRole> sysRoles = map.get(DataScopeTypeEnum.CUSTOM.getType());
+		if (CollectionUtils.isNotEmpty(sysRoles)) {
+			Set<Long> customDeptIds = sysRoles.stream()
+				.map(SysRole::getScopeResources)
+				.filter(Objects::nonNull)
+				.flatMap(x -> Arrays.stream(x.split(",")))
+				.map(Long::parseLong)
+				.collect(Collectors.toSet());
+			scopeOrganizationId.addAll(customDeptIds);
+		}
+
+		// 把部门对应的用户id都放入集合中
+		if (CollectionUtils.isNotEmpty(scopeOrganizationId)) {
+			List<SysUser> sysUserList = sysUserService.listByOrganizationIds(scopeOrganizationId);
+			if (CollectionUtils.isNotEmpty(sysUserList)) {
+				List<Long> userIds = sysUserList.stream().map(SysUser::getUserId).collect(Collectors.toList());
+				scopeUserIds.addAll(userIds);
+			}
+		}
+
+		return userDataScope;
+	}
+
+}

+ 35 - 0
admin/src/main/java/com/your/packages/admin/datascope/UserDataScope.java

@@ -0,0 +1,35 @@
+package com.your.packages.admin.datascope;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author hccake
+ */
+@Data
+public class UserDataScope implements Serializable {
+
+	/**
+	 * 是否是全部数据权限
+	 */
+	private boolean allScope = false;
+
+	/**
+	 * 是否仅能看自己
+	 */
+	private boolean onlySelf = false;
+
+	/**
+	 * 数据权限范围,用户所能查看的用户id 集合
+	 */
+	private Set<Long> scopeUserIds = new HashSet<>();
+
+	/**
+	 * 数据权限范围,用户所能查看的部门id 集合
+	 */
+	private Set<Long> scopeDeptIds = new HashSet<>();
+
+}

+ 55 - 0
admin/src/main/java/com/your/packages/admin/excel/ExcelCustomHeaderController.java

@@ -0,0 +1,55 @@
+package com.your.packages.admin.excel;
+
+import com.alibaba.excel.annotation.ExcelIgnore;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.hccake.common.excel.annotation.ResponseExcel;
+import lombok.Data;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author hccake
+ */
+@Controller
+@RequestMapping("public/excel")
+public class ExcelCustomHeaderController {
+
+	@ResponseExcel(name = "customHead", headGenerator = SimpleDataHeadGenerator.class)
+	@GetMapping("customHead")
+	public List<SimpleData> multi() {
+		List<SimpleData> list = new ArrayList<>();
+		for (int i = 0; i < 10; i++) {
+			SimpleData simpleData = new SimpleData();
+			simpleData.setString("str" + i);
+			simpleData.setNumber(i);
+			simpleData.setDate(new Date());
+			simpleData.setIgnore("Ignore" + i);
+			list.add(simpleData);
+		}
+		return list;
+	}
+
+	@Data
+	public static class SimpleData {
+
+		@ExcelProperty("字符串标题")
+		private String string;
+
+		@ExcelProperty("日期标题")
+		private Date date;
+
+		@ExcelProperty("数字标题")
+		private Integer number;
+
+		// 忽略此字段
+		@ExcelIgnore
+		private String ignore;
+
+	}
+
+}

+ 66 - 0
admin/src/main/java/com/your/packages/admin/excel/ExcelI18nController.java

@@ -0,0 +1,66 @@
+package com.your.packages.admin.excel;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.hccake.common.excel.annotation.RequestExcel;
+import com.hccake.common.excel.annotation.ResponseExcel;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.hibernate.validator.constraints.Range;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.validation.constraints.NotNull;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author hccake
+ */
+@Slf4j
+@Controller
+@RequestMapping("public/excel")
+public class ExcelI18nController {
+
+	@ResponseExcel(name = "国际化导出", i18nHeader = true)
+	@GetMapping("i18n")
+	public List<DemoData> excelExport() {
+		List<DemoData> list = new ArrayList<>();
+
+		for (int i = 0; i < 10; i++) {
+			DemoData demoData = new DemoData();
+			demoData.setUsername("username:" + i);
+			demoData.setAge(i);
+			list.add(demoData);
+		}
+
+		return list;
+	}
+
+	/**
+	 * 国际化导入测试,由于头信息是占位符,所以导入时需要使用 index 进行
+	 * @param list list
+	 * @return 导出的数据
+	 */
+	@PostMapping("i18n")
+	@ResponseBody
+	public List<DemoData> importExcel(@RequestExcel List<DemoData> list) {
+		return list;
+	}
+
+	@Data
+	public static class DemoData {
+
+		@ExcelProperty(value = "{DemoData.username}", index = 0)
+		@NotNull(message = "{DemoData.username}:{}")
+		private String username;
+
+		@ExcelProperty(value = "{DemoData.age}", index = 1)
+		@Range(min = 0, max = 150, message = "{DemoData.age}:{}")
+		private Integer age;
+
+	}
+
+}

+ 78 - 0
admin/src/main/java/com/your/packages/admin/excel/ExcelMultiSheetController.java

@@ -0,0 +1,78 @@
+package com.your.packages.admin.excel;
+
+import com.alibaba.excel.annotation.write.style.ColumnWidth;
+import com.hccake.common.excel.annotation.ResponseExcel;
+import com.hccake.common.excel.annotation.Sheet;
+import lombok.Data;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author hccake
+ */
+@Controller
+@RequestMapping("public/excel")
+public class ExcelMultiSheetController {
+
+	@ResponseExcel(name = "不同Sheet的导出",
+			sheets = { @Sheet(sheetName = "demoData", includes = { "username" }),
+					@Sheet(sheetName = "testData", excludes = { "number" }) })
+	@GetMapping("/different-sheet")
+	public List<List> multiDifferent() {
+		List<List> lists = new ArrayList<>();
+		lists.add(demoDatalist());
+		lists.add(testDatalist());
+		return lists;
+	}
+
+	private List<DemoData> demoDatalist() {
+		List<DemoData> dataList = new ArrayList<>();
+		for (int i = 0; i < 100; i++) {
+			DemoData data = new DemoData();
+			data.setUsername("tr1" + i);
+			data.setPassword("tr2" + i);
+			dataList.add(data);
+		}
+		return dataList;
+	}
+
+	private List<TestData> testDatalist() {
+		List<TestData> dataList = new ArrayList<>();
+		for (int i = 0; i < 100; i++) {
+			TestData data = new TestData();
+			data.setStr("str" + i);
+			data.setNumber(i);
+			data.setLocalDateTime(LocalDateTime.now());
+			dataList.add(data);
+		}
+		return dataList;
+	}
+
+	// 实体对象
+	@Data
+	public static class DemoData {
+
+		private String username;
+
+		private String password;
+
+	}
+
+	@Data
+	public static class TestData {
+
+		private String str;
+
+		private Integer number;
+
+		@ColumnWidth(50) // 定义宽度
+		private LocalDateTime localDateTime;
+
+	}
+
+}

+ 40 - 0
admin/src/main/java/com/your/packages/admin/excel/SimpleDataHeadGenerator.java

@@ -0,0 +1,40 @@
+package com.your.packages.admin.excel;
+
+import com.hccake.common.excel.head.HeadGenerator;
+import com.hccake.common.excel.head.HeadMeta;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * 自定义头生成器
+ *
+ * @author hccake
+ */
+@Component
+public class SimpleDataHeadGenerator implements HeadGenerator {
+
+	@Override
+	public HeadMeta head(Class<?> clazz) {
+		HeadMeta headMeta = new HeadMeta();
+		headMeta.setHead(simpleDataHead());
+		// 排除 number 属性
+		headMeta.setIgnoreHeadFields(new HashSet<>(Collections.singletonList("number")));
+		return headMeta;
+	}
+
+	private List<List<String>> simpleDataHead() {
+		List<List<String>> list = new ArrayList<>();
+		List<String> head0 = new ArrayList<>();
+		head0.add("自定义字符串标题" + System.currentTimeMillis());
+		List<String> head1 = new ArrayList<>();
+		head1.add("自定义日期标题" + System.currentTimeMillis());
+		list.add(head0);
+		list.add(head1);
+		return list;
+	}
+
+}

+ 46 - 0
admin/src/main/java/com/your/packages/admin/modules/test/AdminDemoJobHandler.java

@@ -0,0 +1,46 @@
+//package com.your.packages.admin.modules.test;
+//
+////import com.xxl.job.core.context.XxlJobHelper;
+////import com.xxl.job.core.handler.annotation.XxlJob;
+//import lombok.extern.slf4j.Slf4j;
+//import org.springframework.stereotype.Component;
+//
+//import java.time.LocalDateTime;
+//import java.util.concurrent.TimeUnit;
+//
+///**
+// * XxlJob开发示例(Bean模式)
+// *
+// * 开发步骤: 1、任务开发:在Spring Bean实例中,开发Job方法; 2、注解配置:为Job方法添加注解
+// * "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy =
+// * "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。 3、执行日志:需要通过 "XxlJobHelper.log"
+// * 打印执行日志; 4、任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过
+// * "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果;
+// *
+// * @author xuxueli 2019-12-11 21:52:51
+// */
+//@Slf4j
+//@Component
+//public class AdminDemoJobHandler {
+//
+//	@XxlJob(value = "adminDemoJobHandler")
+//	public void execute() throws Exception {
+//		// XxlJobLogger 改为使用 XxlJobHelper
+//		XxlJobHelper.log("AdminDemoJobHandler Invoke Success.");
+//
+//		// param 获取改为使用 XxlJobHelper.getJobParam 方法
+//		String param = XxlJobHelper.getJobParam();
+//		XxlJobHelper.log("AdminDemoJobHandler param: " + param);
+//
+//		for (int i = 0; i < 5; i++) {
+//			XxlJobHelper.log("beat at:" + i);
+//			TimeUnit.SECONDS.sleep(2);
+//		}
+//
+//		XxlJobHelper.log(("执行成功!:" + LocalDateTime.now()));
+//
+//		// 无异常情况下可省略
+//		XxlJobHelper.handleSuccess();
+//	}
+//
+//}

+ 58 - 0
admin/src/main/java/com/your/packages/admin/modules/test/AuthTestController.java

@@ -0,0 +1,58 @@
+package com.your.packages.admin.modules.test;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author Hccake
+ * @version 1.0
+ * @date 2019/9/27 22:29
+ */
+@RestController
+@RequestMapping("auth-test")
+public class AuthTestController {
+
+	@PreAuthorize("permitAll()")
+	@GetMapping("permitAll")
+	public String permitAll() {
+		return "permitAll request success!";
+	}
+
+	@PreAuthorize("hasRole('2')")
+	@RequestMapping("role1")
+	public String testRole1() {
+		return "role1 request success!";
+	}
+
+	@PreAuthorize("hasRole('ROLE_1')")
+	@RequestMapping("role2")
+	public String testRole2() {
+		return "role2 request success!";
+	}
+
+	@PreAuthorize("hasRole('ROLE_3')")
+	@RequestMapping("role3")
+	public String testRole3() {
+		return "role3 request success!";
+	}
+
+	@PreAuthorize("hasPermission('edit')")
+	@RequestMapping("permission2")
+	public String testPermission2() {
+		return "permission1 request success!";
+	}
+
+	@PreAuthorize("hasPermission(targetObject, 'edit')")
+	@RequestMapping("permission1")
+	public String testPermission1() {
+		return "permission1 request success!";
+	}
+
+	@RequestMapping("permission3")
+	public String testPermission3() {
+		return "permission3 request success!";
+	}
+
+}

+ 57 - 0
admin/src/main/java/com/your/packages/admin/modules/test/CacheTestController.java

@@ -0,0 +1,57 @@
+package com.your.packages.admin.modules.test;
+
+import com.hccake.ballcat.common.redis.core.annotation.CacheDel;
+import com.hccake.ballcat.common.redis.core.annotation.CachePut;
+import com.hccake.ballcat.common.redis.core.annotation.Cached;
+import com.hccake.ballcat.system.service.SysConfigService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author Hccake
+ * @version 1.0
+ * @date 2020/3/25 23:45
+ */
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/public/cache-test")
+public class CacheTestController {
+
+	private final StringRedisTemplate redisTemplate;
+
+	private final SysConfigService sysConfigService;
+
+	@GetMapping("/cachedTestKey1")
+	@Cached(key = "testKey1")
+	public String cachedTestKey1() {
+		return "testKey1 add:" + System.currentTimeMillis();
+	}
+
+	@GetMapping("/putTestKey1")
+	@CachePut(key = "testKey1")
+	public String putTestKey1() {
+		return "testKey1 update:" + System.currentTimeMillis();
+	}
+
+	@GetMapping("/delTestKey1")
+	@CacheDel(key = "testKey1")
+	public void delTestKey1() {
+		System.out.println("testKey1 del");
+	}
+
+	@GetMapping("/cachedTestKey2")
+	public String cachedTestKey2() {
+		redisTemplate.opsForValue().set("testKey2", "1");
+		return "testKey2 add success";
+	}
+
+	@GetMapping("/config")
+	public String config(@RequestParam("confKey") String confKey) {
+		return sysConfigService.getConfValueByKey(confKey);
+	}
+
+}

+ 28 - 0
admin/src/main/java/com/your/packages/admin/modules/test/FileUploadTestController.java

@@ -0,0 +1,28 @@
+package com.your.packages.admin.modules.test;
+
+import com.hccake.ballcat.file.service.FileService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+
+/**
+ * @author hccake
+ */
+@RequestMapping("/public/test")
+@RestController
+@RequiredArgsConstructor
+public class FileUploadTestController {
+
+	private final FileService fileService;
+
+	@PostMapping("file")
+	public String hello(@RequestParam("file") MultipartFile file) throws IOException {
+		return fileService.upload(file.getInputStream(), "/test/" + file.getOriginalFilename(), file.getSize());
+	}
+
+}

+ 51 - 0
admin/src/main/java/com/your/packages/admin/modules/test/I18nTestController.java

@@ -0,0 +1,51 @@
+package com.your.packages.admin.modules.test;
+
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.hibernate.validator.constraints.Range;
+import org.springframework.context.MessageSource;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+/**
+ * @author hccake
+ */
+@RequestMapping("/public/i18n")
+@RestController
+@RequiredArgsConstructor
+@Validated
+public class I18nTestController {
+
+	private final MessageSource messageSource;
+
+	@GetMapping("hello")
+	public String hello(@RequestParam("code") String code) {
+		return messageSource.getMessage(code, null, LocaleContextHolder.getLocale());
+	}
+
+	@GetMapping("paramValidate")
+	public Integer paramValidate(@Min(value = 10, message = "{validation.ageWrong}") @RequestParam("age") Integer age) {
+		return age;
+	}
+
+	@GetMapping("bodyValidate")
+	public DemoData bodyValidate(@Validated @RequestBody DemoData demoData) {
+		return demoData;
+	}
+
+	@Data
+	public static class DemoData {
+
+		@NotNull(message = "{DemoData.username}:{}")
+		private String username;
+
+		@Range(min = 0, max = 150, message = "{DemoData.age}:{}")
+		private Integer age;
+
+	}
+
+}

+ 24 - 0
admin/src/main/java/com/your/packages/admin/modules/test/IdempotentTestController.java

@@ -0,0 +1,24 @@
+//package com.your.packages.admin.modules.test;
+//
+//import com.hccake.ballcat.common.idempotent.annotation.Idempotent;
+//import org.springframework.web.bind.annotation.GetMapping;
+//import org.springframework.web.bind.annotation.RequestMapping;
+//import org.springframework.web.bind.annotation.RequestParam;
+//import org.springframework.web.bind.annotation.RestController;
+//
+///**
+// * 幂等测试
+// *
+// * @author hccake
+// */
+//@RestController
+//@RequestMapping("/public/idempotent")
+//public class IdempotentTestController {
+//
+//	@GetMapping
+//	@Idempotent(uniqueExpression = "#key")
+//	public String test(@RequestParam("key") String key) {
+//		return key;
+//	}
+//
+//}

+ 27 - 0
admin/src/main/java/com/your/packages/admin/modules/test/OAuth2ClientTestController.java

@@ -0,0 +1,27 @@
+package com.your.packages.admin.modules.test;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author hccake
+ */
+@RequestMapping("/client")
+@RestController
+@RequiredArgsConstructor
+public class OAuth2ClientTestController {
+
+	@GetMapping("hello")
+	public String hello() {
+		return "hello, oauth2 client";
+	}
+
+	@GetMapping("user")
+	public String user() {
+		return SecurityContextHolder.getContext().toString();
+	}
+
+}

+ 23 - 0
admin/src/main/java/com/your/packages/admin/oauth2/CustomerOAuth2AuthorizationObjectMapperCustomizer.java

@@ -0,0 +1,23 @@
+package com.your.packages.admin.oauth2;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.your.packages.admin.datascope.UserDataScope;
+import com.your.packages.admin.oauth2.jackson2.UserDataScopeMixin;
+import org.ballcat.springsecurity.oauth2.server.authorization.OAuth2AuthorizationObjectMapperCustomizer;
+import org.springframework.stereotype.Component;
+
+/**
+ * 对于 User attributes 中存放的数据,必须要有对应的 Mixin 类,否则会报错
+ *
+ * @link <a href="https://github.com/spring-projects/spring-security/issues/4370">...</a>
+ * @author hccake
+ */
+@Component
+public class CustomerOAuth2AuthorizationObjectMapperCustomizer implements OAuth2AuthorizationObjectMapperCustomizer {
+
+	@Override
+	public void customize(ObjectMapper objectMapper) {
+		objectMapper.addMixIn(UserDataScope.class, UserDataScopeMixin.class);
+	}
+
+}

+ 49 - 0
admin/src/main/java/com/your/packages/admin/oauth2/jackson2/UserDataScopeDeserializer.java

@@ -0,0 +1,49 @@
+package com.your.packages.admin.oauth2.jackson2;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.MissingNode;
+import com.your.packages.admin.datascope.UserDataScope;
+
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * 自定义的 UserDataScope jackson 反序列化器
+ *
+ * @author hccake
+ */
+public class UserDataScopeDeserializer extends JsonDeserializer<UserDataScope> {
+
+	private static final TypeReference<Set<Long>> LONG_SET = new TypeReference<Set<Long>>() {
+	};
+
+	@Override
+	public UserDataScope deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
+		ObjectMapper mapper = (ObjectMapper) jp.getCodec();
+		JsonNode jsonNode = mapper.readTree(jp);
+
+		boolean allScope = readJsonNode(jsonNode, "allScope").asBoolean();
+		boolean onlySelf = readJsonNode(jsonNode, "onlySelf").asBoolean();
+
+		Set<Long> scopeUserIds = mapper.convertValue(jsonNode.get("scopeUserIds"), LONG_SET);
+		Set<Long> scopeDeptIds = mapper.convertValue(jsonNode.get("scopeDeptIds"), LONG_SET);
+
+		UserDataScope userDataScope = new UserDataScope();
+		userDataScope.setAllScope(allScope);
+		userDataScope.setOnlySelf(onlySelf);
+		userDataScope.setScopeUserIds(scopeUserIds);
+		userDataScope.setScopeDeptIds(scopeDeptIds);
+
+		return userDataScope;
+	}
+
+	private JsonNode readJsonNode(JsonNode jsonNode, String field) {
+		return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
+	}
+
+}

+ 19 - 0
admin/src/main/java/com/your/packages/admin/oauth2/jackson2/UserDataScopeMixin.java

@@ -0,0 +1,19 @@
+package com.your.packages.admin.oauth2.jackson2;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.hccake.ballcat.common.security.jackson2.UserDeserializer;
+
+/**
+ * @author hccake
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+@JsonDeserialize(using = UserDeserializer.class)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
+		isGetterVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class UserDataScopeMixin {
+
+}

+ 73 - 0
admin/src/main/java/com/your/packages/admin/sample/controller/DocumentController.java

@@ -0,0 +1,73 @@
+package com.your.packages.admin.sample.controller;
+
+import com.hccake.ballcat.common.log.operation.annotation.CreateOperationLogging;
+import com.hccake.ballcat.common.log.operation.annotation.DeleteOperationLogging;
+import com.hccake.ballcat.common.model.domain.PageParam;
+import com.hccake.ballcat.common.model.domain.PageResult;
+import com.hccake.ballcat.common.model.result.BaseResultCode;
+import com.hccake.ballcat.common.model.result.R;
+import com.your.packages.admin.sample.model.entity.Document;
+import com.your.packages.admin.sample.model.qo.DocumentQO;
+import com.your.packages.admin.sample.model.vo.DocumentPageVO;
+import com.your.packages.admin.sample.service.DocumentService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+
+/**
+ * 文档表,用于演示数据权限
+ *
+ * @author hccake 2021-09-22 19:22:44
+ */
+@Validated
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/sample/document")
+@Tag(name = "用户文档", description = "用于演示数据权限管理")
+public class DocumentController {
+
+	private final DocumentService documentService;
+
+	/**
+	 * 分页查询
+	 * @param pageParam 分页参数
+	 * @param documentQO 文档表,用于演示数据权限查询对象
+	 * @return R 通用返回体
+	 */
+	@Operation(summary = "分页查询")
+	@GetMapping("/page")
+	public R<PageResult<DocumentPageVO>> getDocumentPage(@Valid PageParam pageParam, DocumentQO documentQO) {
+		return R.ok(documentService.queryPage(pageParam, documentQO));
+	}
+
+	/**
+	 * 新增文档表,用于演示数据权限
+	 * @param document 文档表,用于演示数据权限
+	 * @return R 通用返回体
+	 */
+	@Operation(summary = "新增用户文档")
+	@CreateOperationLogging(msg = "新增文档表,用于演示数据权限")
+	@PostMapping
+	public R<Void> save(@RequestBody Document document) {
+		return documentService.save(document) ? R.ok()
+				: R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "新增文档表,用于演示数据权限失败");
+	}
+
+	/**
+	 * 通过id删除文档表,用于演示数据权限
+	 * @param id id
+	 * @return R 通用返回体
+	 */
+	@Operation(summary = "通过id删除用户文档")
+	@DeleteOperationLogging(msg = "通过id删除文档表,用于演示数据权限")
+	@DeleteMapping("/{id}")
+	public R<Void> removeById(@PathVariable("id") Integer id) {
+		return documentService.removeById(id) ? R.ok()
+				: R.failed(BaseResultCode.UPDATE_DATABASE_ERROR, "通过id删除文档表,用于演示数据权限失败");
+	}
+
+}

+ 25 - 0
admin/src/main/java/com/your/packages/admin/sample/converter/DocumentConverter.java

@@ -0,0 +1,25 @@
+package com.your.packages.admin.sample.converter;
+
+import com.your.packages.admin.sample.model.entity.Document;
+import com.your.packages.admin.sample.model.vo.DocumentPageVO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * 文档表,用于演示数据权限模型转换器
+ *
+ * @author hccake 2021-09-22 19:22:44
+ */
+@Mapper
+public interface DocumentConverter {
+
+	DocumentConverter INSTANCE = Mappers.getMapper(DocumentConverter.class);
+
+	/**
+	 * PO 转 PageVO
+	 * @param document 文档表,用于演示数据权限
+	 * @return DocumentPageVO 文档表,用于演示数据权限PageVO
+	 */
+	DocumentPageVO poToPageVo(Document document);
+
+}

+ 25 - 0
admin/src/main/java/com/your/packages/admin/sample/listener/SampleEventListener.java

@@ -0,0 +1,25 @@
+package com.your.packages.admin.sample.listener;
+
+import com.hccake.ballcat.system.event.UserOrganizationChangeEvent;
+import com.your.packages.admin.sample.service.DocumentService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author hccake
+ */
+@Component
+@RequiredArgsConstructor
+public class SampleEventListener {
+
+	private final DocumentService documentService;
+
+	@EventListener(UserOrganizationChangeEvent.class)
+	public void listener(UserOrganizationChangeEvent event) {
+		// 更新用户文档的组织id
+		documentService.updateUserOrganizationId(event.getUserId(), event.getOriginOrganizationId(),
+				event.getCurrentOrganizationId());
+	}
+
+}

+ 62 - 0
admin/src/main/java/com/your/packages/admin/sample/mapper/DocumentMapper.java

@@ -0,0 +1,62 @@
+package com.your.packages.admin.sample.mapper;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Constants;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.hccake.ballcat.common.model.domain.PageParam;
+import com.hccake.ballcat.common.model.domain.PageResult;
+import com.hccake.extend.mybatis.plus.conditions.query.LambdaQueryWrapperX;
+import com.hccake.extend.mybatis.plus.mapper.ExtendMapper;
+import com.hccake.extend.mybatis.plus.toolkit.WrappersX;
+import com.your.packages.admin.sample.model.entity.Document;
+import com.your.packages.admin.sample.model.qo.DocumentQO;
+import com.your.packages.admin.sample.model.vo.DocumentPageVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 文档表,用于演示数据权限
+ *
+ * @author hccake 2021-09-22 19:22:44
+ */
+public interface DocumentMapper extends ExtendMapper<Document> {
+
+	/**
+	 * 分页查询
+	 * @param pageParam 分页参数
+	 * @param qo 查询参数
+	 * @return PageResult<DocumentPageVO> VO分页数据
+	 */
+	default PageResult<DocumentPageVO> queryPage(PageParam pageParam, DocumentQO qo) {
+		IPage<Document> page = this.prodPage(pageParam);
+		LambdaQueryWrapperX<Document> wrapper = WrappersX.lambdaQueryX(Document.class);
+		List<DocumentPageVO> records = this.selectPageVO(page, wrapper);
+		return new PageResult<>(records, page.getTotal());
+	}
+
+	/**
+	 * 分页查询
+	 * @param page 分页数据
+	 * @param wrapper 条件构造器
+	 * @return List<DocumentPageVO>
+	 */
+	List<DocumentPageVO> selectPageVO(IPage<Document> page, @Param(Constants.WRAPPER) Wrapper<Document> wrapper);
+
+	/**
+	 * 更新用户的组织id
+	 * @param userId 用户id
+	 * @param originOrganizationId 原组织id
+	 * @param currentOrganizationId 现组织id
+	 */
+	default void updateUserOrganizationId(Long userId, Long originOrganizationId, Long currentOrganizationId) {
+		LambdaUpdateWrapper<Document> wrapper = Wrappers.lambdaUpdate(Document.class)
+			.set(Document::getOrganizationId, currentOrganizationId)
+			.eq(Document::getUserId, userId)
+			.eq(Document::getOrganizationId, originOrganizationId);
+		this.update(null, wrapper);
+	}
+
+}

+ 65 - 0
admin/src/main/java/com/your/packages/admin/sample/model/entity/Document.java

@@ -0,0 +1,65 @@
+package com.your.packages.admin.sample.model.entity;
+
+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.hccake.extend.mybatis.plus.alias.TableAlias;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 文档表,用于演示数据权限
+ *
+ * @author hccake 2021-09-22 19:22:44
+ */
+@TableAlias("t")
+@Data
+@TableName("sample_document")
+@Schema(title = "用户文档")
+public class Document {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * ID
+	 */
+	@TableId
+	@Schema(title = "ID")
+	private Long id;
+
+	/**
+	 * 文档名称
+	 */
+	@Schema(title = "文档名称")
+	private String name;
+
+	/**
+	 * 所属用户ID
+	 */
+	@Schema(title = "所属用户ID")
+	private Long userId;
+
+	/**
+	 * 所属组织ID
+	 */
+	@Schema(title = "所属组织ID")
+	private Long organizationId;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(title = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.INSERT_UPDATE)
+	@Schema(title = "更新时间")
+	private LocalDateTime updateTime;
+
+}

+ 26 - 0
admin/src/main/java/com/your/packages/admin/sample/model/qo/DocumentQO.java

@@ -0,0 +1,26 @@
+package com.your.packages.admin.sample.model.qo;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springdoc.api.annotations.ParameterObject;
+
+/**
+ * 文档表,用于演示数据权限 查询对象
+ *
+ * @author hccake 2021-09-22 19:22:44
+ */
+@Data
+@Schema(title = "用户文档查询对象")
+@ParameterObject
+public class DocumentQO {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * ID
+	 */
+	@Parameter(description = "ID")
+	private Integer id;
+
+}

+ 67 - 0
admin/src/main/java/com/your/packages/admin/sample/model/vo/DocumentPageVO.java

@@ -0,0 +1,67 @@
+package com.your.packages.admin.sample.model.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 文档表,用于演示数据权限分页视图对象
+ *
+ * @author hccake 2021-09-22 19:22:44
+ */
+@Data
+@Schema(title = "用户文档分页视图对象")
+public class DocumentPageVO {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * ID
+	 */
+	@Schema(title = "ID")
+	private Integer id;
+
+	/**
+	 * 文档名称
+	 */
+	@Schema(title = "文档名称")
+	private String name;
+
+	/**
+	 * 所属用户ID
+	 */
+	@Schema(title = "所属用户ID")
+	private Integer userId;
+
+	/**
+	 * 用户名
+	 */
+	@Schema(title = "用户名")
+	private String username;
+
+	/**
+	 * 所属组织ID
+	 */
+	@Schema(title = "所属组织ID")
+	private Integer organizationId;
+
+	/**
+	 * 组织名
+	 */
+	@Schema(title = "组织名")
+	private String organizationName;
+
+	/**
+	 * 创建时间
+	 */
+	@Schema(title = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@Schema(title = "更新时间")
+	private LocalDateTime updateTime;
+
+}

+ 33 - 0
admin/src/main/java/com/your/packages/admin/sample/service/DocumentService.java

@@ -0,0 +1,33 @@
+package com.your.packages.admin.sample.service;
+
+import com.hccake.ballcat.common.model.domain.PageParam;
+import com.hccake.ballcat.common.model.domain.PageResult;
+import com.hccake.extend.mybatis.plus.service.ExtendService;
+import com.your.packages.admin.sample.model.entity.Document;
+import com.your.packages.admin.sample.model.qo.DocumentQO;
+import com.your.packages.admin.sample.model.vo.DocumentPageVO;
+
+/**
+ * 文档表,用于演示数据权限
+ *
+ * @author hccake 2021-09-22 19:22:44
+ */
+public interface DocumentService extends ExtendService<Document> {
+
+	/**
+	 * 根据QueryObeject查询分页数据
+	 * @param pageParam 分页参数
+	 * @param qo 查询参数对象
+	 * @return PageResult&lt;DocumentPageVO&gt; 分页数据
+	 */
+	PageResult<DocumentPageVO> queryPage(PageParam pageParam, DocumentQO qo);
+
+	/**
+	 * 更新用户的组织id
+	 * @param userId 用户id
+	 * @param originOrganizationId 原组织id
+	 * @param currentOrganizationId 现组织id
+	 */
+	void updateUserOrganizationId(Long userId, Long originOrganizationId, Long currentOrganizationId);
+
+}

+ 55 - 0
admin/src/main/java/com/your/packages/admin/sample/service/impl/DocumentServiceImpl.java

@@ -0,0 +1,55 @@
+package com.your.packages.admin.sample.service.impl;
+
+import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
+import com.hccake.ballcat.common.model.domain.PageParam;
+import com.hccake.ballcat.common.model.domain.PageResult;
+import com.hccake.ballcat.system.model.entity.SysUser;
+import com.hccake.ballcat.system.service.SysUserService;
+import com.hccake.extend.mybatis.plus.service.impl.ExtendServiceImpl;
+import com.your.packages.admin.sample.mapper.DocumentMapper;
+import com.your.packages.admin.sample.model.entity.Document;
+import com.your.packages.admin.sample.model.qo.DocumentQO;
+import com.your.packages.admin.sample.model.vo.DocumentPageVO;
+import com.your.packages.admin.sample.service.DocumentService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+/**
+ * 文档表,用于演示数据权限
+ *
+ * @author hccake 2021-09-22 19:22:44
+ */
+@Service
+@RequiredArgsConstructor
+public class DocumentServiceImpl extends ExtendServiceImpl<DocumentMapper, Document> implements DocumentService {
+
+	private final SysUserService sysUserService;
+
+	/**
+	 * 根据QueryObeject查询分页数据
+	 * @param pageParam 分页参数
+	 * @param qo 查询参数对象
+	 * @return PageResult<DocumentPageVO> 分页数据
+	 */
+	@Override
+	public PageResult<DocumentPageVO> queryPage(PageParam pageParam, DocumentQO qo) {
+		return baseMapper.queryPage(pageParam, qo);
+	}
+
+	@Override
+	public void updateUserOrganizationId(Long userId, Long originOrganizationId, Long currentOrganizationId) {
+		baseMapper.updateUserOrganizationId(userId, originOrganizationId, currentOrganizationId);
+	}
+
+	/**
+	 * 插入一条记录(选择字段,策略插入)
+	 * @param document 实体对象
+	 */
+	@Override
+	public boolean save(Document document) {
+		SysUser sysUser = sysUserService.getById(document.getUserId());
+		document.setOrganizationId(sysUser.getOrganizationId());
+		return SqlHelper.retBool(baseMapper.insert(document));
+	}
+
+}

+ 158 - 0
admin/src/main/java/com/your/packages/alibaba/excel/read/processor/DefaultAnalysisEventProcessor.java

@@ -0,0 +1,158 @@
+package com.your.packages.alibaba.excel.read.processor;
+
+import com.alibaba.excel.context.AnalysisContext;
+import com.alibaba.excel.enums.HeadKindEnum;
+import com.alibaba.excel.enums.RowTypeEnum;
+import com.alibaba.excel.exception.ExcelAnalysisException;
+import com.alibaba.excel.exception.ExcelAnalysisStopException;
+import com.alibaba.excel.metadata.Head;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.read.listener.ReadListener;
+import com.alibaba.excel.read.metadata.holder.ReadRowHolder;
+import com.alibaba.excel.read.metadata.property.ExcelReadHeadProperty;
+import com.alibaba.excel.read.processor.AnalysisEventProcessor;
+import com.alibaba.excel.util.ConverterUtils;
+import com.alibaba.excel.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Analysis event
+ *
+ * @author jipengfei
+ */
+public class DefaultAnalysisEventProcessor implements AnalysisEventProcessor {
+
+	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultAnalysisEventProcessor.class);
+
+	@Override
+	public void extra(AnalysisContext analysisContext) {
+		dealExtra(analysisContext);
+	}
+
+	@Override
+	public void endRow(AnalysisContext analysisContext) {
+		if (RowTypeEnum.EMPTY.equals(analysisContext.readRowHolder().getRowType())) {
+			if (LOGGER.isDebugEnabled()) {
+				LOGGER.debug("Empty row!");
+			}
+			if (analysisContext.readWorkbookHolder().getIgnoreEmptyRow()) {
+				return;
+			}
+		}
+		dealData(analysisContext);
+	}
+
+	@Override
+	public void endSheet(AnalysisContext analysisContext) {
+		for (ReadListener readListener : analysisContext.currentReadHolder().readListenerList()) {
+			readListener.doAfterAllAnalysed(analysisContext);
+		}
+	}
+
+	private void dealExtra(AnalysisContext analysisContext) {
+		for (ReadListener readListener : analysisContext.currentReadHolder().readListenerList()) {
+			try {
+				readListener.extra(analysisContext.readSheetHolder().getCellExtra(), analysisContext);
+			}
+			catch (Exception e) {
+				onException(analysisContext, e);
+				break;
+			}
+			if (!readListener.hasNext(analysisContext)) {
+				throw new ExcelAnalysisStopException();
+			}
+		}
+	}
+
+	private void onException(AnalysisContext analysisContext, Exception e) {
+		for (ReadListener readListenerException : analysisContext.currentReadHolder().readListenerList()) {
+			try {
+				readListenerException.onException(e, analysisContext);
+			}
+			catch (RuntimeException re) {
+				throw re;
+			}
+			catch (Exception e1) {
+				throw new ExcelAnalysisException(e1.getMessage(), e1);
+			}
+		}
+	}
+
+	private void dealData(AnalysisContext analysisContext) {
+		ReadRowHolder readRowHolder = analysisContext.readRowHolder();
+		Map<Integer, ReadCellData<?>> cellDataMap = (Map) readRowHolder.getCellMap();
+		readRowHolder.setCurrentRowAnalysisResult(cellDataMap);
+		int rowIndex = readRowHolder.getRowIndex();
+		int currentHeadRowNumber = analysisContext.readSheetHolder().getHeadRowNumber();
+
+		boolean isData = rowIndex >= currentHeadRowNumber;
+
+		// Now is data
+		for (ReadListener readListener : analysisContext.currentReadHolder().readListenerList()) {
+			try {
+				if (isData) {
+					readListener.invoke(readRowHolder.getCurrentRowAnalysisResult(), analysisContext);
+				}
+				else {
+					readListener.invokeHead(cellDataMap, analysisContext);
+				}
+			}
+			catch (Exception e) {
+				onException(analysisContext, e);
+				break;
+			}
+			if (!readListener.hasNext(analysisContext)) {
+				throw new ExcelAnalysisStopException();
+			}
+		}
+
+		// Last head column
+		if (!isData && currentHeadRowNumber == rowIndex + 1) {
+			buildHead(analysisContext, cellDataMap);
+		}
+	}
+
+	private void buildHead(AnalysisContext analysisContext, Map<Integer, ReadCellData<?>> cellDataMap) {
+		if (!HeadKindEnum.CLASS.equals(analysisContext.currentReadHolder().excelReadHeadProperty().getHeadKind())) {
+			return;
+		}
+		Map<Integer, String> dataMap = ConverterUtils.convertToStringMap(cellDataMap, analysisContext);
+		ExcelReadHeadProperty excelHeadPropertyData = analysisContext.readSheetHolder().excelReadHeadProperty();
+		Map<Integer, Head> headMapData = excelHeadPropertyData.getHeadMap();
+		Map<Integer, Head> tmpHeadMap = new HashMap<>(headMapData.size() * 4 / 3 + 1);
+		for (Map.Entry<Integer, Head> entry : headMapData.entrySet()) {
+			Head headData = entry.getValue();
+			if (headData.getForceIndex() || !headData.getForceName()) {
+				tmpHeadMap.put(entry.getKey(), headData);
+				continue;
+			}
+			List<String> headNameList = headData.getHeadNameList();
+			String headName = headNameList.get(headNameList.size() - 1);
+			for (Map.Entry<Integer, String> stringEntry : dataMap.entrySet()) {
+				if (stringEntry == null) {
+					continue;
+				}
+				String headString = stringEntry.getValue();
+				Integer stringKey = stringEntry.getKey();
+				if (StringUtils.isEmpty(headString)) {
+					continue;
+				}
+				if (analysisContext.currentReadHolder().globalConfiguration().getAutoTrim()) {
+					headString = headString.trim();
+				}
+				if (headName.equals(headString)) {
+					headData.setColumnIndex(stringKey);
+					tmpHeadMap.put(stringKey, headData);
+					break;
+				}
+			}
+		}
+		excelHeadPropertyData.setHeadMap(tmpHeadMap);
+	}
+
+}

+ 33 - 0
admin/src/main/java/com/your/packages/datascope/DataScope.java

@@ -0,0 +1,33 @@
+package com.your.packages.datascope;
+
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+
+/**
+ * @author Hccake 2020/9/28
+ * @version 1.0
+ */
+public interface DataScope {
+
+	/**
+	 * 数据所对应的资源
+	 * @return 资源标识
+	 */
+	String getResource();
+
+	/**
+	 * 判断当前数据权限范围是否需要管理此表
+	 * @param tableName 当前需要处理的表名
+	 * @return 如果当前数据权限范围包含当前表名,则返回 true。,否则返回 false
+	 */
+	boolean includes(String tableName);
+
+	/**
+	 * 根据表名和表别名,动态生成的 where/or 筛选条件
+	 * @param tableName 表名
+	 * @param tableAlias 表别名,可能为空
+	 * @return 数据规则表达式
+	 */
+	Expression getExpression(String tableName, Alias tableAlias);
+
+}

+ 56 - 0
admin/src/main/java/com/your/packages/datascope/DataScopeAutoConfiguration.java

@@ -0,0 +1,56 @@
+package com.your.packages.datascope;
+
+import com.your.packages.datascope.handler.DataPermissionHandler;
+import com.your.packages.datascope.handler.DefaultDataPermissionHandler;
+import com.your.packages.datascope.interceptor.DataPermissionAnnotationAdvisor;
+import com.your.packages.datascope.interceptor.DataPermissionInterceptor;
+import com.your.packages.datascope.processor.DataScopeSqlProcessor;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+
+import java.util.List;
+
+/**
+ * @author hccake
+ */
+@AutoConfiguration
+@RequiredArgsConstructor
+@ConditionalOnBean(DataScope.class)
+public class DataScopeAutoConfiguration {
+
+	/**
+	 * 数据权限处理器
+	 * @param dataScopeList 需要控制的数据范围集合
+	 * @return DataPermissionHandler
+	 */
+	@Bean
+	@ConditionalOnMissingBean
+	public DataPermissionHandler dataPermissionHandler(List<DataScope> dataScopeList) {
+		return new DefaultDataPermissionHandler(dataScopeList);
+	}
+
+	/**
+	 * 数据权限注解 Advisor,用于处理数据权限的链式调用关系
+	 * @return DataPermissionAnnotationAdvisor
+	 */
+	@Bean
+	@ConditionalOnMissingBean(DataPermissionAnnotationAdvisor.class)
+	public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
+		return new DataPermissionAnnotationAdvisor();
+	}
+
+	/**
+	 * mybatis 拦截器,用于拦截处理 sql
+	 * @param dataPermissionHandler 数据权限处理器
+	 * @return DataPermissionInterceptor
+	 */
+	@Bean
+	@ConditionalOnMissingBean
+	public DataPermissionInterceptor dataPermissionInterceptor(DataPermissionHandler dataPermissionHandler) {
+		return new DataPermissionInterceptor(new DataScopeSqlProcessor(), dataPermissionHandler);
+	}
+
+}

+ 36 - 0
admin/src/main/java/com/your/packages/datascope/annotation/DataPermission.java

@@ -0,0 +1,36 @@
+package com.your.packages.datascope.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 数据权限注解,注解在 Mapper类 或者 对应方法上 用于提供该 mapper 对应表,所需控制的实体信息
+ *
+ * @author Hccake 2020/9/27
+ * @version 1.0
+ */
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DataPermission {
+
+	/**
+	 * 当前类或方法是否忽略数据权限
+	 * @return boolean 默认返回 false
+	 */
+	boolean ignore() default false;
+
+	/**
+	 * 仅对指定资源类型进行数据权限控制,只在开启情况下有效,当该数组有值时,exclude不生效
+	 * @see DataPermission#excludeResources
+	 * @return 资源类型数组
+	 */
+	String[] includeResources() default {};
+
+	/**
+	 * 对指定资源类型跳过数据权限控制,只在开启情况下有效,当该includeResources有值时,exclude不生效
+	 * @see DataPermission#includeResources
+	 * @return 资源类型数组
+	 */
+	String[] excludeResources() default {};
+
+}

+ 16 - 0
admin/src/main/java/com/your/packages/datascope/function/Action.java

@@ -0,0 +1,16 @@
+package com.your.packages.datascope.function;
+
+/**
+ * 操作接口
+ *
+ * @author hccake
+ */
+@FunctionalInterface
+public interface Action {
+
+	/**
+	 * 执行操作
+	 */
+	void execute();
+
+}

+ 16 - 0
admin/src/main/java/com/your/packages/datascope/function/ResultAction.java

@@ -0,0 +1,16 @@
+package com.your.packages.datascope.function;
+
+/**
+ * 有返回值的操作接口
+ *
+ * @author hccake
+ */
+@FunctionalInterface
+public interface ResultAction<T> {
+
+	/**
+	 * 执行操作
+	 */
+	T execute();
+
+}

+ 37 - 0
admin/src/main/java/com/your/packages/datascope/handler/DataPermissionHandler.java

@@ -0,0 +1,37 @@
+package com.your.packages.datascope.handler;
+
+
+import com.your.packages.datascope.DataScope;
+
+import java.util.List;
+
+/**
+ * 数据权限处理器
+ *
+ * @author Hccake 2020/9/28
+ * @version 1.0
+ */
+public interface DataPermissionHandler {
+
+	/**
+	 * 系统配置的所有的数据范围
+	 * @return 数据范围集合
+	 */
+	List<DataScope> dataScopes();
+
+	/**
+	 * 根据权限注解过滤后的数据范围集合
+	 * @param mappedStatementId Mapper方法ID
+	 * @return 数据范围集合
+	 */
+	List<DataScope> filterDataScopes(String mappedStatementId);
+
+	/**
+	 * 是否忽略权限控制,用于及早的忽略控制,例如管理员直接放行,而不必等到DataScope中再进行过滤处理,提升效率
+	 * @return boolean true: 忽略,false: 进行权限控制
+	 * @param dataScopeList 当前应用的 dataScope 集合
+	 * @param mappedStatementId Mapper方法ID
+	 */
+	boolean ignorePermissionControl(List<DataScope> dataScopeList, String mappedStatementId);
+
+}

+ 80 - 0
admin/src/main/java/com/your/packages/datascope/handler/DataPermissionRule.java

@@ -0,0 +1,80 @@
+package com.your.packages.datascope.handler;
+
+
+import com.your.packages.datascope.annotation.DataPermission;
+
+/**
+ * 数据权限的规则抽象类
+ *
+ * @author hccake
+ * @since 0.7.0
+ */
+public class DataPermissionRule {
+
+	private boolean ignore = false;
+
+	private String[] includeResources = new String[0];
+
+	private String[] excludeResources = new String[0];
+
+	public DataPermissionRule() {
+	}
+
+	public DataPermissionRule(boolean ignore) {
+		this.ignore = ignore;
+	}
+
+	public DataPermissionRule(boolean ignore, String[] includeResources, String[] excludeResources) {
+		this.ignore = ignore;
+		this.includeResources = includeResources;
+		this.excludeResources = excludeResources;
+	}
+
+	public DataPermissionRule(DataPermission dataPermission) {
+		this.ignore = dataPermission.ignore();
+		this.includeResources = dataPermission.includeResources();
+		this.excludeResources = dataPermission.excludeResources();
+	}
+
+	/**
+	 * 当前类或方法是否忽略数据权限
+	 * @return boolean 默认返回 false
+	 */
+	public boolean ignore() {
+		return ignore;
+	}
+
+	/**
+	 * 仅对指定资源类型进行数据权限控制,只在开启情况下有效,当该数组有值时,exclude不生效
+	 * @see DataPermission#excludeResources
+	 * @return 资源类型数组
+	 */
+	public String[] includeResources() {
+		return includeResources;
+	}
+
+	/**
+	 * 对指定资源类型跳过数据权限控制,只在开启情况下有效,当该includeResources有值时,exclude不生效
+	 * @see DataPermission#includeResources
+	 * @return 资源类型数组
+	 */
+	public String[] excludeResources() {
+		return excludeResources;
+	}
+
+	public DataPermissionRule setIgnore(boolean ignore) {
+		this.ignore = ignore;
+		return this;
+	}
+
+	public DataPermissionRule setIncludeResources(String[] includeResources) {
+		this.includeResources = includeResources;
+		return this;
+	}
+
+	public DataPermissionRule setExcludeResources(String[] excludeResources) {
+		this.excludeResources = excludeResources;
+		return this;
+	}
+
+}

+ 94 - 0
admin/src/main/java/com/your/packages/datascope/handler/DefaultDataPermissionHandler.java

@@ -0,0 +1,94 @@
+package com.your.packages.datascope.handler;
+
+import com.your.packages.datascope.DataScope;
+import com.your.packages.datascope.holder.DataPermissionRuleHolder;
+import com.your.packages.datascope.holder.MappedStatementIdsWithoutDataScope;
+import lombok.RequiredArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 默认的数据权限控制处理器
+ *
+ * @author Hccake 2021/1/27
+ * @version 1.0
+ */
+@RequiredArgsConstructor
+public class DefaultDataPermissionHandler implements DataPermissionHandler {
+
+	private final List<DataScope> dataScopes;
+
+	/**
+	 * 系统配置的所有的数据范围
+	 * @return 数据范围集合
+	 */
+	@Override
+	public List<DataScope> dataScopes() {
+		return dataScopes;
+	}
+
+	/**
+	 * 系统配置的所有的数据范围
+	 * @param mappedStatementId Mapper方法ID
+	 * @return 数据范围集合
+	 */
+	@Override
+	public List<DataScope> filterDataScopes(String mappedStatementId) {
+		if (this.dataScopes == null || this.dataScopes.isEmpty()) {
+			return new ArrayList<>();
+		}
+		// 获取权限规则
+		DataPermissionRule dataPermissionRule = DataPermissionRuleHolder.peek();
+		return filterDataScopes(dataPermissionRule);
+	}
+
+	/**
+	 * <p>
+	 * 是否忽略权限控制
+	 * </p>
+	 * 若当前的 mappedStatementId 存在于 <Code>MappedStatementIdsWithoutDataScope<Code/>
+	 * 中,则表示无需处理
+	 * @param dataScopeList 当前需要控制的 dataScope 集合
+	 * @param mappedStatementId Mapper方法ID
+	 * @return always false
+	 */
+	@Override
+	public boolean ignorePermissionControl(List<DataScope> dataScopeList, String mappedStatementId) {
+		return MappedStatementIdsWithoutDataScope.onAllWithoutSet(dataScopeList, mappedStatementId);
+	}
+
+	/**
+	 * 根据数据权限规则过滤出 dataScope 列表
+	 * @param dataPermissionRule 数据权限规则
+	 * @return List<DataScope>
+	 */
+	protected List<DataScope> filterDataScopes(DataPermissionRule dataPermissionRule) {
+		if (dataPermissionRule == null) {
+			return dataScopes;
+		}
+
+		if (dataPermissionRule.ignore()) {
+			return new ArrayList<>();
+		}
+
+		// 当指定了只包含的资源时,只对该资源的DataScope
+		if (dataPermissionRule.includeResources().length > 0) {
+			Set<String> a = new HashSet<>(Arrays.asList(dataPermissionRule.includeResources()));
+			return dataScopes.stream().filter(x -> a.contains(x.getResource())).collect(Collectors.toList());
+		}
+
+		// 当未指定只包含的资源,且指定了排除的资源时,则排除此部分资源的 DataScope
+		if (dataPermissionRule.excludeResources().length > 0) {
+			Set<String> a = new HashSet<>(Arrays.asList(dataPermissionRule.excludeResources()));
+			return dataScopes.stream().filter(x -> !a.contains(x.getResource())).collect(Collectors.toList());
+		}
+
+		return dataScopes;
+	}
+
+}

+ 69 - 0
admin/src/main/java/com/your/packages/datascope/holder/DataPermissionRuleHolder.java

@@ -0,0 +1,69 @@
+package com.your.packages.datascope.holder;
+
+
+import com.your.packages.datascope.handler.DataPermissionRule;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+/**
+ * 数据权限规则的持有者,使用栈存储调用链中的数据权限规则
+ *
+ * 区别于{@link com.your.packages.datascope.handler.DataPermissionRule}
+ * {@link DataPermissionRule} 是编程式数据权限控制的使用,优先级高于注解
+ *
+ * @author hccake
+ */
+public final class DataPermissionRuleHolder {
+
+	private DataPermissionRuleHolder() {
+	}
+
+	/**
+	 * 使用栈存储 DataPermissionRule,便于在方法嵌套调用时使用不同的数据权限控制。
+	 */
+	private static final ThreadLocal<Deque<DataPermissionRule>> DATA_PERMISSION_RULES = ThreadLocal
+		.withInitial(ArrayDeque::new);
+
+	/**
+	 * 获取当前的 DataPermissionRule 注解
+	 * @return DataPermissionRule
+	 */
+	public static DataPermissionRule peek() {
+		Deque<DataPermissionRule> deque = DATA_PERMISSION_RULES.get();
+		return deque == null ? null : deque.peek();
+	}
+
+	/**
+	 * 入栈一个 DataPermissionRule 注解
+	 * @return DataPermissionRule
+	 */
+	public static DataPermissionRule push(DataPermissionRule dataPermissionRule) {
+		Deque<DataPermissionRule> deque = DATA_PERMISSION_RULES.get();
+		if (deque == null) {
+			deque = new ArrayDeque<>();
+		}
+		deque.push(dataPermissionRule);
+		return dataPermissionRule;
+	}
+
+	/**
+	 * 弹出最顶部 DataPermissionRule
+	 */
+	public static void poll() {
+		Deque<DataPermissionRule> deque = DATA_PERMISSION_RULES.get();
+		deque.poll();
+		// 当没有元素时,清空 ThreadLocal
+		if (deque.isEmpty()) {
+			clear();
+		}
+	}
+
+	/**
+	 * 清除 TreadLocal
+	 */
+	public static void clear() {
+		DATA_PERMISSION_RULES.remove();
+	}
+
+}

+ 60 - 0
admin/src/main/java/com/your/packages/datascope/holder/DataScopeMatchNumHolder.java

@@ -0,0 +1,60 @@
+package com.your.packages.datascope.holder;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * DataScope 匹配数
+ *
+ * @author hccake
+ */
+public final class DataScopeMatchNumHolder {
+
+	private DataScopeMatchNumHolder() {
+	}
+
+	private static final ThreadLocal<Deque<AtomicInteger>> matchNumTreadLocal = new ThreadLocal<>();
+
+	/**
+	 * 每次 SQL 执行解析前初始化匹配次数为 0
+	 */
+	public static void initMatchNum() {
+		Deque<AtomicInteger> deque = matchNumTreadLocal.get();
+		if (deque == null) {
+			deque = new ArrayDeque<>();
+			matchNumTreadLocal.set(deque);
+		}
+		deque.push(new AtomicInteger());
+	}
+
+	/**
+	 * 获取当前 SQL 解析后被数据权限匹配中的次数
+	 * @return int 次数
+	 */
+	public static Integer pollMatchNum() {
+		Deque<AtomicInteger> deque = matchNumTreadLocal.get();
+		AtomicInteger matchNum = deque.poll();
+		return matchNum == null ? null : matchNum.get();
+	}
+
+	/**
+	 * 如果存在计数器,则次数 +1
+	 */
+	public static void incrementMatchNumIfPresent() {
+		Deque<AtomicInteger> deque = matchNumTreadLocal.get();
+		Optional.ofNullable(deque).map(Deque::peek).ifPresent(AtomicInteger::incrementAndGet);
+	}
+
+	/**
+	 * 删除 matchNumTreadLocal,在 SQL 执行解析后调用
+	 */
+	public static void removeIfEmpty() {
+		Deque<AtomicInteger> deque = matchNumTreadLocal.get();
+		if (deque == null || deque.isEmpty()) {
+			matchNumTreadLocal.remove();
+		}
+	}
+
+}

+ 58 - 0
admin/src/main/java/com/your/packages/datascope/holder/MappedStatementIdsWithoutDataScope.java

@@ -0,0 +1,58 @@
+package com.your.packages.datascope.holder;
+
+
+import com.your.packages.datascope.DataScope;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 该类用于存储,不需数据权限处理的 mappedStatementId 集合
+ *
+ * @author hccake
+ */
+public final class MappedStatementIdsWithoutDataScope {
+
+	private MappedStatementIdsWithoutDataScope() {
+	}
+
+	/**
+	 * key: DataScope class,value: 该 DataScope 不需要处理的 mappedStatementId 集合
+	 */
+	private static final Map<Class<? extends DataScope>, HashSet<String>> WITHOUT_MAPPED_STATEMENT_ID_MAP = new ConcurrentHashMap<>();
+
+	/**
+	 * 给所有的 DataScope 对应的忽略列表添加对应的 mappedStatementId
+	 * @param dataScopeList 数据范围集合
+	 * @param mappedStatementId mappedStatementId
+	 */
+	public static void addToWithoutSet(List<DataScope> dataScopeList, String mappedStatementId) {
+		for (DataScope dataScope : dataScopeList) {
+			Class<? extends DataScope> dataScopeClass = dataScope.getClass();
+			HashSet<String> set = WITHOUT_MAPPED_STATEMENT_ID_MAP.computeIfAbsent(dataScopeClass,
+					key -> new HashSet<>());
+			set.add(mappedStatementId);
+		}
+	}
+
+	/**
+	 * 是否可以忽略权限控制,检查当前 mappedStatementId 是否存在于所有需要控制的 dataScope 对应的忽略列表中
+	 * @param dataScopeList 数据范围集合
+	 * @param mappedStatementId mappedStatementId
+	 * @return 忽略控制返回 true
+	 */
+	public static boolean onAllWithoutSet(List<DataScope> dataScopeList, String mappedStatementId) {
+		for (DataScope dataScope : dataScopeList) {
+			Class<? extends DataScope> dataScopeClass = dataScope.getClass();
+			HashSet<String> set = WITHOUT_MAPPED_STATEMENT_ID_MAP.computeIfAbsent(dataScopeClass,
+					key -> new HashSet<>());
+			if (!set.contains(mappedStatementId)) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+}

+ 34 - 0
admin/src/main/java/com/your/packages/datascope/interceptor/DataPermissionAnnotationAdvisor.java

@@ -0,0 +1,34 @@
+package com.your.packages.datascope.interceptor;
+
+import com.your.packages.datascope.annotation.DataPermission;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import org.aopalliance.aop.Advice;
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.support.AbstractPointcutAdvisor;
+import org.springframework.aop.support.ComposablePointcut;
+import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
+
+/**
+ * @author hccake
+ */
+@Getter
+@EqualsAndHashCode(callSuper = true)
+public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor {
+
+	private final Advice advice;
+
+	private final Pointcut pointcut;
+
+	public DataPermissionAnnotationAdvisor() {
+		this.advice = new DataPermissionAnnotationInterceptor();
+		this.pointcut = buildPointcut();
+	}
+
+	protected Pointcut buildPointcut() {
+		Pointcut cpc = new AnnotationMatchingPointcut(DataPermission.class, true);
+		Pointcut mpc = new AnnotationMatchingPointcut(null, DataPermission.class, true);
+		return new ComposablePointcut(cpc).union(mpc);
+	}
+
+}

+ 41 - 0
admin/src/main/java/com/your/packages/datascope/interceptor/DataPermissionAnnotationInterceptor.java

@@ -0,0 +1,41 @@
+package com.your.packages.datascope.interceptor;
+
+import com.your.packages.datascope.annotation.DataPermission;
+import com.your.packages.datascope.handler.DataPermissionRule;
+import com.your.packages.datascope.holder.DataPermissionRuleHolder;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+
+import java.lang.reflect.Method;
+
+/**
+ * DataPermission注解的拦截器,在执行方法前将当前方法的对应注解压栈,执行后弹出注解
+ *
+ * @author hccake
+ */
+public class DataPermissionAnnotationInterceptor implements MethodInterceptor {
+
+	@Override
+	public Object invoke(MethodInvocation methodInvocation) throws Throwable {
+		// 当前方法
+		Method method = methodInvocation.getMethod();
+		// 获取执行类
+		Object invocationThis = methodInvocation.getThis();
+		Class<?> clazz = invocationThis != null ? invocationThis.getClass() : method.getDeclaringClass();
+		// 寻找对应的 DataPermission 注解属性
+		DataPermission dataPermission = DataPermissionFinder.findDataPermission(method, clazz);
+		// 理论上这里是不会为空的
+		if (dataPermission == null) {
+			return methodInvocation.proceed();
+		}
+
+		DataPermissionRuleHolder.push(new DataPermissionRule(dataPermission));
+		try {
+			return methodInvocation.proceed();
+		}
+		finally {
+			DataPermissionRuleHolder.poll();
+		}
+	}
+
+}

+ 118 - 0
admin/src/main/java/com/your/packages/datascope/interceptor/DataPermissionFinder.java

@@ -0,0 +1,118 @@
+package com.your.packages.datascope.interceptor;
+
+import com.your.packages.datascope.annotation.DataPermission;
+import org.springframework.aop.support.AopUtils;
+import org.springframework.core.MethodClassKey;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.util.ClassUtils;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * {@link DataPermission} 注解的查找者。用于查询当前方法对应的 DataPermission 注解环境,当方法上没有找到时,会去类上寻找。
+ *
+ * @author hccake
+ */
+@DataPermission
+public final class DataPermissionFinder {
+
+	private DataPermissionFinder() {
+
+	}
+
+	private static final Map<Object, DataPermission> DATA_PERMISSION_CACHE = new ConcurrentHashMap<>(1024);
+
+	/**
+	 * 提供一个默认的空值注解,用于缓存空值占位使用
+	 */
+	private static final DataPermission EMPTY_DATA_PERMISSION = DataPermissionFinder.class
+		.getAnnotation(DataPermission.class);
+
+	/**
+	 * 缓存的 key 值
+	 * @param method 方法
+	 * @param clazz 类
+	 * @return key
+	 */
+	private static Object getCacheKey(Method method, Class<?> clazz) {
+		return new MethodClassKey(method, clazz);
+	}
+
+	/**
+	 * 从缓存中获取数据权限注解 优先获取方法上的注解,再获取类上的注解
+	 * @param method 当前方法
+	 * @param targetClass 当前类
+	 * @return 当前方法有效的数据权限注解
+	 */
+	public static DataPermission findDataPermission(Method method, Class<?> targetClass) {
+		if (method.getDeclaringClass() == Object.class) {
+			return null;
+		}
+
+		// 先查找缓存中是否存在
+		Object methodKey = getCacheKey(method, targetClass);
+		if (DATA_PERMISSION_CACHE.containsKey(methodKey)) {
+			DataPermission dataPermission = DATA_PERMISSION_CACHE.get(methodKey);
+			// 判断是否和缓存的空注解是同一个对象
+			return EMPTY_DATA_PERMISSION == dataPermission ? null : dataPermission;
+		}
+
+		// 先查方法,如果方法上没有,则使用类上
+		DataPermission dataPermission = computeDataPermissionAnnotation(method, targetClass);
+		// 添加进缓存
+		DATA_PERMISSION_CACHE.put(methodKey, dataPermission == null ? EMPTY_DATA_PERMISSION : dataPermission);
+		return dataPermission;
+	}
+
+	/**
+	 * 计算当前应用的 dataPermission 注解
+	 * @see org.springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource#computeTransactionAttribute(Method,
+	 * Class)
+	 * @param method 当前方法
+	 * @param targetClass 当前类
+	 * @return DataPermission
+	 */
+	private static DataPermission computeDataPermissionAnnotation(Method method, Class<?> targetClass) {
+		// 方法可能在接口上,但是我们需要找到对应的实现类的方法
+		// 如果 target class 是 null, 则 method 不变
+		Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
+
+		// 首先,从目标类的方法上进行查找 dataPermission 注解
+		DataPermission dataPermission = findDataPermissionAnnotation(specificMethod);
+		if (dataPermission != null) {
+			return dataPermission;
+		}
+
+		// 第二次,则从目标类上的查找 dataPermission 注解
+		dataPermission = findDataPermissionAnnotation(specificMethod.getDeclaringClass());
+		if (dataPermission != null && ClassUtils.isUserLevelMethod(method)) {
+			return dataPermission;
+		}
+
+		if (specificMethod != method) {
+			// 回退,查找原先传入的 method
+			dataPermission = findDataPermissionAnnotation(method);
+			if (dataPermission != null) {
+				return dataPermission;
+			}
+			// 最后,尝试从原始的 method 的所有类上查找
+			dataPermission = findDataPermissionAnnotation(method.getDeclaringClass());
+			if (dataPermission != null && ClassUtils.isUserLevelMethod(method)) {
+				return dataPermission;
+			}
+		}
+
+		return null;
+	}
+
+	private static DataPermission findDataPermissionAnnotation(Method method) {
+		return AnnotatedElementUtils.findMergedAnnotation(method, DataPermission.class);
+	}
+
+	private static DataPermission findDataPermissionAnnotation(Class<?> targetClass) {
+		return AnnotatedElementUtils.findMergedAnnotation(targetClass, DataPermission.class);
+	}
+
+}

+ 92 - 0
admin/src/main/java/com/your/packages/datascope/interceptor/DataPermissionInterceptor.java

@@ -0,0 +1,92 @@
+package com.your.packages.datascope.interceptor;
+
+import com.your.packages.datascope.DataScope;
+import com.your.packages.datascope.handler.DataPermissionHandler;
+import com.your.packages.datascope.holder.DataScopeMatchNumHolder;
+import com.your.packages.datascope.holder.MappedStatementIdsWithoutDataScope;
+import com.your.packages.datascope.processor.DataScopeSqlProcessor;
+import com.your.packages.datascope.util.PluginUtils;
+import lombok.RequiredArgsConstructor;
+import org.apache.ibatis.executor.statement.StatementHandler;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.mapping.SqlCommandType;
+import org.apache.ibatis.plugin.Interceptor;
+import org.apache.ibatis.plugin.Intercepts;
+import org.apache.ibatis.plugin.Invocation;
+import org.apache.ibatis.plugin.Plugin;
+import org.apache.ibatis.plugin.Signature;
+
+import java.sql.Connection;
+import java.util.List;
+
+/**
+ * 数据权限拦截器
+ *
+ * @author Hccake 2020/9/28
+ * @version 1.0
+ */
+@RequiredArgsConstructor
+@Intercepts({
+		@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
+public class DataPermissionInterceptor implements Interceptor {
+
+	private final DataScopeSqlProcessor dataScopeSqlProcessor;
+
+	private final DataPermissionHandler dataPermissionHandler;
+
+	@Override
+	public Object intercept(Invocation invocation) throws Throwable {
+		// 第一版,测试用
+		Object target = invocation.getTarget();
+		StatementHandler sh = (StatementHandler) target;
+		PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
+		MappedStatement ms = mpSh.mappedStatement();
+		SqlCommandType sct = ms.getSqlCommandType();
+		PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
+		String mappedStatementId = ms.getId();
+
+		// 获取当前需要控制的 dataScope 集合
+		List<DataScope> filterDataScopes = dataPermissionHandler.filterDataScopes(mappedStatementId);
+		if (filterDataScopes == null || filterDataScopes.isEmpty()) {
+			return invocation.proceed();
+		}
+
+		// 根据用户权限判断是否需要拦截,例如管理员可以查看所有,则直接放行
+		if (dataPermissionHandler.ignorePermissionControl(filterDataScopes, mappedStatementId)) {
+			return invocation.proceed();
+		}
+
+		// 创建 matchNumTreadLocal
+		DataScopeMatchNumHolder.initMatchNum();
+		try {
+			// 根据 DataScopes 进行数据权限的 sql 处理
+			if (sct == SqlCommandType.SELECT) {
+				mpBs.sql(dataScopeSqlProcessor.parserSingle(mpBs.sql(), filterDataScopes));
+			}
+			else if (sct == SqlCommandType.INSERT || sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
+				mpBs.sql(dataScopeSqlProcessor.parserMulti(mpBs.sql(), filterDataScopes));
+			}
+			// 如果解析后发现当前 mappedStatementId 对应的 sql,没有任何数据权限匹配,则记录下来,后续可以直接跳过不解析
+			Integer matchNum = DataScopeMatchNumHolder.pollMatchNum();
+			List<DataScope> allDataScopes = dataPermissionHandler.dataScopes();
+			if (allDataScopes.size() == filterDataScopes.size() && matchNum != null && matchNum == 0) {
+				MappedStatementIdsWithoutDataScope.addToWithoutSet(filterDataScopes, mappedStatementId);
+			}
+		}
+		finally {
+			DataScopeMatchNumHolder.removeIfEmpty();
+		}
+
+		// 执行 sql
+		return invocation.proceed();
+	}
+
+	@Override
+	public Object plugin(Object target) {
+		if (target instanceof StatementHandler) {
+			return Plugin.wrap(target, this);
+		}
+		return target;
+	}
+
+}

+ 108 - 0
admin/src/main/java/com/your/packages/datascope/parser/JsqlParserSupport.java

@@ -0,0 +1,108 @@
+package com.your.packages.datascope.parser;
+
+import lombok.extern.slf4j.Slf4j;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.Statements;
+import net.sf.jsqlparser.statement.delete.Delete;
+import net.sf.jsqlparser.statement.insert.Insert;
+import net.sf.jsqlparser.statement.select.Select;
+import net.sf.jsqlparser.statement.update.Update;
+
+/**
+ * https://github.com/JSQLParser/JSqlParser
+ *
+ * @author miemie hccake
+ * @since 2020-06-22
+ */
+@Slf4j
+public abstract class JsqlParserSupport {
+
+	public String parserSingle(String sql, Object obj) {
+		try {
+			Statement statement = CCJSqlParserUtil.parse(sql);
+			return processParser(statement, 0, sql, obj);
+		}
+		catch (JSQLParserException e) {
+			throw new RuntimeException(String.format("Failed to process, Error SQL: %s", sql), e);
+		}
+	}
+
+	public String parserMulti(String sql, Object obj) {
+		try {
+			// fixed github pull/295
+			StringBuilder sb = new StringBuilder();
+			Statements statements = CCJSqlParserUtil.parseStatements(sql);
+			int i = 0;
+			for (Statement statement : statements.getStatements()) {
+				if (i > 0) {
+					sb.append(";");
+				}
+				sb.append(processParser(statement, i, sql, obj));
+				i++;
+			}
+			return sb.toString();
+		}
+		catch (JSQLParserException e) {
+			throw new RuntimeException(String.format("Failed to process, Error SQL: %s", sql), e);
+		}
+	}
+
+	/**
+	 * 执行 SQL 解析
+	 * @param statement JsqlParser Statement
+	 * @return sql
+	 */
+	protected String processParser(Statement statement, int index, String sql, Object obj) {
+		if (log.isDebugEnabled()) {
+			log.debug("SQL to parse, SQL: " + sql);
+		}
+		if (statement instanceof Insert) {
+			this.processInsert((Insert) statement, index, sql, obj);
+		}
+		else if (statement instanceof Select) {
+			this.processSelect((Select) statement, index, sql, obj);
+		}
+		else if (statement instanceof Update) {
+			this.processUpdate((Update) statement, index, sql, obj);
+		}
+		else if (statement instanceof Delete) {
+			this.processDelete((Delete) statement, index, sql, obj);
+		}
+		sql = statement.toString();
+		if (log.isDebugEnabled()) {
+			log.debug("parse the finished SQL: " + sql);
+		}
+		return sql;
+	}
+
+	/**
+	 * 新增
+	 */
+	protected void processInsert(Insert insert, int index, String sql, Object obj) {
+		throw new UnsupportedOperationException();
+	}
+
+	/**
+	 * 删除
+	 */
+	protected void processDelete(Delete delete, int index, String sql, Object obj) {
+		throw new UnsupportedOperationException();
+	}
+
+	/**
+	 * 更新
+	 */
+	protected void processUpdate(Update update, int index, String sql, Object obj) {
+		throw new UnsupportedOperationException();
+	}
+
+	/**
+	 * 查询
+	 */
+	protected void processSelect(Select select, int index, String sql, Object obj) {
+		throw new UnsupportedOperationException();
+	}
+
+}

+ 553 - 0
admin/src/main/java/com/your/packages/datascope/processor/DataScopeSqlProcessor.java

@@ -0,0 +1,553 @@
+package com.your.packages.datascope.processor;
+
+import com.your.packages.datascope.DataScope;
+import com.your.packages.datascope.holder.DataScopeMatchNumHolder;
+import com.your.packages.datascope.parser.JsqlParserSupport;
+import com.your.packages.datascope.util.CollectionUtils;
+import com.your.packages.datascope.util.SqlParseUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.sf.jsqlparser.expression.*;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
+import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.expression.operators.relational.InExpression;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.delete.Delete;
+import net.sf.jsqlparser.statement.insert.Insert;
+import net.sf.jsqlparser.statement.select.*;
+import net.sf.jsqlparser.statement.update.Update;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 数据权限 sql 处理器 参考 mybatis-plus 租户拦截器,解析 sql where 部分,进行查询表达式注入
+ *
+ * @author Hccake 2020/9/26
+ * @version 1.0
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class DataScopeSqlProcessor extends JsqlParserSupport {
+
+	/**
+	 * select 类型SQL处理
+	 * @param select jsqlparser Statement Select
+	 */
+	@Override
+	protected void processSelect(Select select, int index, String sql, Object obj) {
+		List<DataScope> dataScopes = (List<DataScope>) obj;
+		try {
+			// dataScopes 放入 ThreadLocal 方便透传
+			DataScopeHolder.push(dataScopes);
+			processSelectBody(select.getSelectBody());
+			List<WithItem> withItemsList = select.getWithItemsList();
+			if (CollectionUtils.isNotEmpty(withItemsList)) {
+				withItemsList.forEach(this::processSelectBody);
+			}
+		}
+		finally {
+			// 必须清空 ThreadLocal
+			DataScopeHolder.poll();
+		}
+	}
+
+	protected void processSelectBody(SelectBody selectBody) {
+		if (selectBody == null) {
+			return;
+		}
+		if (selectBody instanceof PlainSelect) {
+			processPlainSelect((PlainSelect) selectBody);
+		}
+		else if (selectBody instanceof WithItem) {
+			WithItem withItem = (WithItem) selectBody;
+			processSelectBody(withItem.getSubSelect().getSelectBody());
+		}
+		else {
+			SetOperationList operationList = (SetOperationList) selectBody;
+			List<SelectBody> selectBodys = operationList.getSelects();
+			if (CollectionUtils.isNotEmpty(selectBodys)) {
+				selectBodys.forEach(this::processSelectBody);
+			}
+		}
+	}
+
+	/**
+	 * insert 类型SQL处理
+	 * @param insert jsqlparser Statement Insert
+	 */
+	@Override
+	protected void processInsert(Insert insert, int index, String sql, Object obj) {
+		// insert 暂时不处理
+	}
+
+	/**
+	 * update 类型SQL处理
+	 * @param update jsqlparser Statement Update
+	 */
+	@Override
+	protected void processUpdate(Update update, int index, String sql, Object obj) {
+		List<DataScope> dataScopes = (List<DataScope>) obj;
+		try {
+			// dataScopes 放入 ThreadLocal 方便透传
+			DataScopeHolder.push(dataScopes);
+			update.setWhere(this.injectExpression(update.getWhere(), update.getTable()));
+		}
+		finally {
+			// 必须清空 ThreadLocal
+			DataScopeHolder.poll();
+		}
+	}
+
+	/**
+	 * delete 类型SQL处理
+	 * @param delete jsqlparser Statement Delete
+	 */
+	@Override
+	protected void processDelete(Delete delete, int index, String sql, Object obj) {
+		List<DataScope> dataScopes = (List<DataScope>) obj;
+		try {
+			// dataScopes 放入 ThreadLocal 方便透传
+			DataScopeHolder.push(dataScopes);
+			delete.setWhere(this.injectExpression(delete.getWhere(), delete.getTable()));
+		}
+		finally {
+			// 必须清空 ThreadLocal
+			DataScopeHolder.poll();
+		}
+	}
+
+	/**
+	 * 处理 PlainSelect
+	 */
+	protected void processPlainSelect(PlainSelect plainSelect) {
+		// #3087 github
+		List<SelectItem> selectItems = plainSelect.getSelectItems();
+		if (CollectionUtils.isNotEmpty(selectItems)) {
+			selectItems.forEach(this::processSelectItem);
+		}
+
+		// 处理 where 中的子查询
+		Expression where = plainSelect.getWhere();
+		processWhereSubSelect(where);
+
+		// 处理 fromItem
+		FromItem fromItem = plainSelect.getFromItem();
+		List<Table> list = processFromItem(fromItem);
+		List<Table> mainTables = new ArrayList<>(list);
+
+		// 处理 join
+		List<Join> joins = plainSelect.getJoins();
+		if (CollectionUtils.isNotEmpty(joins)) {
+			mainTables = processJoins(mainTables, joins);
+		}
+
+		// 当有 mainTable 时,进行 where 条件追加
+		if (CollectionUtils.isNotEmpty(mainTables)) {
+			plainSelect.setWhere(injectExpression(where, mainTables));
+		}
+	}
+
+	private List<Table> processFromItem(FromItem fromItem) {
+		// 处理括号括起来的表达式
+		while (fromItem instanceof ParenthesisFromItem) {
+			fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
+		}
+
+		List<Table> mainTables = new ArrayList<>();
+		// 无 join 时的处理逻辑
+		if (fromItem instanceof Table) {
+			Table fromTable = (Table) fromItem;
+			mainTables.add(fromTable);
+		}
+		else if (fromItem instanceof SubJoin) {
+			// SubJoin 类型则还需要添加上 where 条件
+			List<Table> tables = processSubJoin((SubJoin) fromItem);
+			mainTables.addAll(tables);
+		}
+		else {
+			// 处理下 fromItem
+			processOtherFromItem(fromItem);
+		}
+		return mainTables;
+	}
+
+	/**
+	 * 处理where条件内的子查询
+	 * <p>
+	 * 支持如下: 1. in 2. = 3. > 4. < 5. >= 6. <= 7. <> 8. EXISTS 9. NOT EXISTS
+	 * <p>
+	 * 前提条件: 1. 子查询必须放在小括号中 2. 子查询一般放在比较操作符的右边
+	 * @param where where 条件
+	 */
+	protected void processWhereSubSelect(Expression where) {
+		if (where == null) {
+			return;
+		}
+		if (where instanceof FromItem) {
+			processOtherFromItem((FromItem) where);
+			return;
+		}
+		if (where.toString().indexOf("SELECT") > 0) {
+			// 有子查询
+			if (where instanceof BinaryExpression) {
+				// 比较符号 , and , or , 等等
+				BinaryExpression expression = (BinaryExpression) where;
+				processWhereSubSelect(expression.getLeftExpression());
+				processWhereSubSelect(expression.getRightExpression());
+			}
+			else if (where instanceof InExpression) {
+				// in
+				InExpression expression = (InExpression) where;
+				Expression inExpression = expression.getRightExpression();
+				if (inExpression instanceof SubSelect) {
+					processSelectBody(((SubSelect) inExpression).getSelectBody());
+				}
+			}
+			else if (where instanceof ExistsExpression) {
+				// exists
+				ExistsExpression expression = (ExistsExpression) where;
+				processWhereSubSelect(expression.getRightExpression());
+			}
+			else if (where instanceof NotExpression) {
+				// not exists
+				NotExpression expression = (NotExpression) where;
+				processWhereSubSelect(expression.getExpression());
+			}
+			else if (where instanceof Parenthesis) {
+				Parenthesis expression = (Parenthesis) where;
+				processWhereSubSelect(expression.getExpression());
+			}
+		}
+	}
+
+	protected void processSelectItem(SelectItem selectItem) {
+		if (selectItem instanceof SelectExpressionItem) {
+			SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
+			if (selectExpressionItem.getExpression() instanceof SubSelect) {
+				processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody());
+			}
+			else if (selectExpressionItem.getExpression() instanceof Function) {
+				processFunction((Function) selectExpressionItem.getExpression());
+			}
+		}
+	}
+
+	/**
+	 * 处理函数
+	 * <p>
+	 * 支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)
+	 * <p>
+	 * <p>
+	 * fixed gitee pulls/141
+	 * </p>
+	 * @param function
+	 */
+	protected void processFunction(Function function) {
+		ExpressionList parameters = function.getParameters();
+		if (parameters != null) {
+			parameters.getExpressions().forEach(expression -> {
+				if (expression instanceof SubSelect) {
+					processSelectBody(((SubSelect) expression).getSelectBody());
+				}
+				else if (expression instanceof Function) {
+					processFunction((Function) expression);
+				}
+			});
+		}
+	}
+
+	/**
+	 * 处理子查询等
+	 */
+	protected void processOtherFromItem(FromItem fromItem) {
+		// 去除括号
+		while (fromItem instanceof ParenthesisFromItem) {
+			fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
+		}
+
+		if (fromItem instanceof SubSelect) {
+			SubSelect subSelect = (SubSelect) fromItem;
+			if (subSelect.getSelectBody() != null) {
+				processSelectBody(subSelect.getSelectBody());
+			}
+		}
+		else if (fromItem instanceof ValuesList) {
+			log.debug("Perform a subquery, if you do not give us feedback");
+		}
+		else if (fromItem instanceof LateralSubSelect) {
+			LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;
+			if (lateralSubSelect.getSubSelect() != null) {
+				SubSelect subSelect = lateralSubSelect.getSubSelect();
+				if (subSelect.getSelectBody() != null) {
+					processSelectBody(subSelect.getSelectBody());
+				}
+			}
+		}
+	}
+
+	/**
+	 * 处理 sub join
+	 * @param subJoin subJoin
+	 * @return Table subJoin 中的主表
+	 */
+	private List<Table> processSubJoin(SubJoin subJoin) {
+		List<Table> mainTables = new ArrayList<>();
+		if (subJoin.getJoinList() != null) {
+			List<Table> list = processFromItem(subJoin.getLeft());
+			mainTables.addAll(list);
+			mainTables = processJoins(mainTables, subJoin.getJoinList());
+		}
+		return mainTables;
+	}
+
+	/**
+	 * 处理 joins
+	 * @param mainTables 可以为 null
+	 * @param joins join 集合
+	 * @return List
+	 * <Table>
+	 * 右连接查询的 Table 列表
+	 */
+	private List<Table> processJoins(List<Table> mainTables, List<Join> joins) {
+		if (mainTables == null) {
+			mainTables = new ArrayList<>();
+		}
+
+		// join 表达式中最终的主表
+		Table mainTable = null;
+		// 当前 join 的左表
+		Table leftTable = null;
+		if (mainTables.size() == 1) {
+			mainTable = mainTables.get(0);
+			leftTable = mainTable;
+		}
+
+		// 对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名
+		Deque<List<Table>> onTableDeque = new LinkedList<>();
+		for (Join join : joins) {
+			// 处理 on 表达式
+			FromItem joinItem = join.getRightItem();
+
+			// 获取当前 join 的表,subJoint 可以看作是一张表
+			List<Table> joinTables = null;
+			if (joinItem instanceof Table) {
+				joinTables = new ArrayList<>();
+				joinTables.add((Table) joinItem);
+			}
+			else if (joinItem instanceof SubJoin) {
+				joinTables = processSubJoin((SubJoin) joinItem);
+			}
+
+			if (joinTables != null) {
+
+				// 如果是隐式内连接
+				if (join.isSimple()) {
+					mainTables.addAll(joinTables);
+					continue;
+				}
+
+				// 当前表是否忽略
+				Table joinTable = joinTables.get(0);
+
+				List<Table> onTables = null;
+				// 如果不要忽略,且是右连接,则记录下当前表
+				if (join.isRight()) {
+					mainTable = joinTable;
+					if (leftTable != null) {
+						onTables = Collections.singletonList(leftTable);
+					}
+				}
+				else if (join.isLeft()) {
+					onTables = Collections.singletonList(joinTable);
+				}
+				// JOIN 等同于 INNER JOIN
+				else if (join.isInner() || join.getASTNode().jjtGetFirstToken().toString().equalsIgnoreCase("JOIN")) {
+					if (mainTable == null) {
+						onTables = Collections.singletonList(joinTable);
+					}
+					else {
+						onTables = Arrays.asList(mainTable, joinTable);
+					}
+					mainTable = null;
+				}
+
+				// TODO 参看 net.sf.jsqlparser.statement.select.Join#ToString 的逻辑,实现其他的 JOIN
+
+				mainTables = new ArrayList<>();
+				if (mainTable != null) {
+					mainTables.add(mainTable);
+				}
+
+				// 获取 join 尾缀的 on 表达式列表
+				Collection<Expression> originOnExpressions = join.getOnExpressions();
+				// 正常 join on 表达式只有一个,立刻处理
+				if (originOnExpressions.size() == 1 && onTables != null) {
+					List<Expression> onExpressions = new LinkedList<>();
+					onExpressions.add(injectExpression(originOnExpressions.iterator().next(), onTables));
+					join.setOnExpressions(onExpressions);
+					leftTable = joinTable;
+					continue;
+				}
+				// 表名压栈,忽略的表压入 null,以便后续不处理
+				onTableDeque.push(onTables);
+				// 尾缀多个 on 表达式的时候统一处理
+				if (originOnExpressions.size() > 1) {
+					Collection<Expression> onExpressions = new LinkedList<>();
+					for (Expression originOnExpression : originOnExpressions) {
+						List<Table> currentTableList = onTableDeque.poll();
+						if (CollectionUtils.isEmpty(currentTableList)) {
+							onExpressions.add(originOnExpression);
+						}
+						else {
+							onExpressions.add(injectExpression(originOnExpression, currentTableList));
+						}
+					}
+					join.setOnExpressions(onExpressions);
+				}
+				leftTable = joinTable;
+			}
+			else {
+				processOtherFromItem(joinItem);
+				leftTable = null;
+			}
+
+		}
+
+		return mainTables;
+	}
+
+	/**
+	 * 根据 DataScope ,将数据过滤的表达式注入原本的 where/or 条件
+	 * @param currentExpression Expression where/or
+	 * @param table 表信息
+	 * @return 修改后的 where/or 条件
+	 */
+	private Expression injectExpression(Expression currentExpression, Table table) {
+		return injectExpression(currentExpression, Collections.singletonList(table));
+	}
+
+	/**
+	 * 根据 DataScope ,将数据过滤的表达式注入原本的 where/or 条件
+	 * @param currentExpression Expression where/or
+	 * @param tables 表信息
+	 * @return 修改后的 where/or 条件
+	 */
+	private Expression injectExpression(Expression currentExpression, List<Table> tables) {
+		// 没有表需要处理直接返回
+		if (CollectionUtils.isEmpty(tables)) {
+			return currentExpression;
+		}
+
+		List<Expression> dataFilterExpressions = new ArrayList<>(tables.size());
+		for (Table table : tables) {
+			// 获取表名
+			String tableName = SqlParseUtils.getTableName(table.getName());
+
+			// 进行 dataScope 的表名匹配
+			List<DataScope> matchDataScopes = DataScopeHolder.peek()
+				.stream()
+				.filter(x -> x.includes(tableName))
+				.collect(Collectors.toList());
+
+			if (CollectionUtils.isEmpty(matchDataScopes)) {
+				continue;
+			}
+
+			// 匹配则计数
+			DataScopeMatchNumHolder.incrementMatchNumIfPresent();
+
+			// 获取到数据权限过滤的表达式
+			matchDataScopes.stream()
+				.map(x -> x.getExpression(tableName, table.getAlias()))
+				.filter(Objects::nonNull)
+				.reduce(AndExpression::new)
+				.ifPresent(dataFilterExpressions::add);
+		}
+
+		if (dataFilterExpressions.isEmpty()) {
+			return currentExpression;
+		}
+
+		// 注入的表达式
+		Expression injectExpression = dataFilterExpressions.get(0);
+		// 如果有多个,则用 and 连接
+		if (dataFilterExpressions.size() > 1) {
+			for (int i = 1; i < dataFilterExpressions.size(); i++) {
+				injectExpression = new AndExpression(injectExpression, dataFilterExpressions.get(i));
+			}
+		}
+
+		if (currentExpression == null) {
+			return injectExpression;
+		}
+		if (injectExpression == null) {
+			return currentExpression;
+		}
+		if (currentExpression instanceof OrExpression) {
+			return new AndExpression(new Parenthesis(currentExpression), injectExpression);
+		}
+		else {
+			return new AndExpression(currentExpression, injectExpression);
+		}
+	}
+
+	/**
+	 * DataScope 持有者。 方便解析 SQL 时的参数透传
+	 *
+	 * @author hccake
+	 */
+	private static final class DataScopeHolder {
+
+		private DataScopeHolder() {
+		}
+
+		/**
+		 * 使用栈存储 List<DataScope>,便于在方法嵌套调用时使用不同的数据权限控制。
+		 */
+		private static final ThreadLocal<Deque<List<DataScope>>> DATA_SCOPES = ThreadLocal.withInitial(ArrayDeque::new);
+
+		/**
+		 * 获取当前的 dataScopes
+		 * @return List<DataScope>
+		 */
+		public static List<DataScope> peek() {
+			Deque<List<DataScope>> deque = DATA_SCOPES.get();
+			return deque == null ? new ArrayList<>() : deque.peek();
+		}
+
+		/**
+		 * 入栈一组 dataScopes
+		 */
+		public static void push(List<DataScope> dataScopes) {
+			Deque<List<DataScope>> deque = DATA_SCOPES.get();
+			if (deque == null) {
+				deque = new ArrayDeque<>();
+			}
+			deque.push(dataScopes);
+		}
+
+		/**
+		 * 弹出最顶部 dataScopes
+		 */
+		public static void poll() {
+			Deque<List<DataScope>> deque = DATA_SCOPES.get();
+			deque.poll();
+			// 当没有元素时,清空 ThreadLocal
+			if (deque.isEmpty()) {
+				clear();
+			}
+		}
+
+		/**
+		 * 清除 TreadLocal
+		 */
+		private static void clear() {
+			DATA_SCOPES.remove();
+		}
+
+	}
+
+}

+ 64 - 0
admin/src/main/java/com/your/packages/datascope/util/AnnotationUtil.java

@@ -0,0 +1,64 @@
+package com.your.packages.datascope.util;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+
+/**
+ * @author Hccake 2021/1/27
+ * @version 1.0
+ */
+public final class AnnotationUtil {
+
+	private AnnotationUtil() {
+	}
+
+	/**
+	 * 获取数据权限注解 优先获取方法上的注解,再获取类上的注解
+	 * @param mappedStatementId 类名.方法名
+	 * @return 数据权限注解
+	 */
+	public static <A extends Annotation> A findAnnotationByMappedStatementId(String mappedStatementId,
+			Class<A> aClass) {
+		if (mappedStatementId == null || "".equals(mappedStatementId)) {
+			return null;
+		}
+		// 1.得到类路径和方法路径
+		int lastIndexOfDot = mappedStatementId.lastIndexOf(".");
+		if (lastIndexOfDot < 0) {
+			return null;
+		}
+		String className = mappedStatementId.substring(0, lastIndexOfDot);
+		String methodName = mappedStatementId.substring(lastIndexOfDot + 1);
+		if ("".equals(className) || "".equals(methodName)) {
+			return null;
+		}
+
+		// 2.字节码
+		Class<?> clazz = null;
+		try {
+			clazz = Class.forName(className);
+		}
+		catch (ClassNotFoundException e) {
+			e.printStackTrace();
+		}
+		if (clazz == null) {
+			return null;
+		}
+
+		A annotation = null;
+		// 3.得到方法上的注解
+		Method[] methods = clazz.getMethods();
+		for (Method method : methods) {
+			String name = method.getName();
+			if (methodName.equals(name)) {
+				annotation = method.getAnnotation(aClass);
+				break;
+			}
+		}
+		if (annotation == null) {
+			annotation = clazz.getAnnotation(aClass);
+		}
+		return annotation;
+	}
+
+}

+ 33 - 0
admin/src/main/java/com/your/packages/datascope/util/CollectionUtils.java

@@ -0,0 +1,33 @@
+package com.your.packages.datascope.util;
+
+import java.util.Collection;
+
+/**
+ * Collection 工具类
+ *
+ * @author hccake
+ */
+public final class CollectionUtils {
+
+	private CollectionUtils() {
+	}
+
+	/**
+	 * 校验集合是否为空
+	 * @param collection 集合
+	 * @return boolean
+	 */
+	public static boolean isEmpty(Collection<?> collection) {
+		return collection == null || collection.isEmpty();
+	}
+
+	/**
+	 * 校验集合是否不为空
+	 * @param collection 集合
+	 * @return boolean
+	 */
+	public static boolean isNotEmpty(Collection<?> collection) {
+		return !isEmpty(collection);
+	}
+
+}

+ 67 - 0
admin/src/main/java/com/your/packages/datascope/util/DataPermissionUtils.java

@@ -0,0 +1,67 @@
+package com.your.packages.datascope.util;
+
+
+import com.your.packages.datascope.function.Action;
+import com.your.packages.datascope.function.ResultAction;
+import com.your.packages.datascope.handler.DataPermissionRule;
+import com.your.packages.datascope.holder.DataPermissionRuleHolder;
+
+/**
+ * @author hccake
+ */
+public final class DataPermissionUtils {
+
+	private DataPermissionUtils() {
+
+	}
+
+	/**
+	 * 使用指定的数据权限执行任务
+	 * @param action 待执行的动作
+	 */
+	public static void executeAndIgnoreAll(Action action) {
+		DataPermissionRule ignoreAll = new DataPermissionRule(true);
+		executeWithDataPermissionRule(ignoreAll, action);
+	}
+
+	/**
+	 * 使用指定的数据权限执行任务
+	 * @param resultAction 待执行的动作
+	 */
+	public static <T> T executeAndIgnoreAll(ResultAction<T> resultAction) {
+		DataPermissionRule ignoreAll = new DataPermissionRule(true);
+		return executeWithDataPermissionRule(ignoreAll, resultAction);
+	}
+
+	/**
+	 * 使用指定的数据权限执行任务
+	 * @param dataPermissionRule 当前任务执行时使用的数据权限规则
+	 * @param action 待执行的动作
+	 */
+	public static void executeWithDataPermissionRule(DataPermissionRule dataPermissionRule, Action action) {
+		DataPermissionRuleHolder.push(dataPermissionRule);
+		try {
+			action.execute();
+		}
+		finally {
+			DataPermissionRuleHolder.poll();
+		}
+	}
+
+	/**
+	 * 使用指定的数据权限执行任务
+	 * @param dataPermissionRule 当前任务执行时使用的数据权限规则
+	 * @param resultAction 待执行的动作
+	 */
+	public static <T> T executeWithDataPermissionRule(DataPermissionRule dataPermissionRule,
+			ResultAction<T> resultAction) {
+		DataPermissionRuleHolder.push(dataPermissionRule);
+		try {
+			return resultAction.execute();
+		}
+		finally {
+			DataPermissionRuleHolder.poll();
+		}
+	}
+
+}

+ 166 - 0
admin/src/main/java/com/your/packages/datascope/util/PluginUtils.java

@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2011-2020, baomidou (jobob@qq.com).
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * <p>
+ * https://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.your.packages.datascope.util;
+
+import org.apache.ibatis.executor.Executor;
+import org.apache.ibatis.executor.parameter.ParameterHandler;
+import org.apache.ibatis.executor.statement.StatementHandler;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.mapping.ParameterMapping;
+import org.apache.ibatis.reflection.MetaObject;
+import org.apache.ibatis.reflection.SystemMetaObject;
+import org.apache.ibatis.session.Configuration;
+
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 插件工具类
+ *
+ * @author TaoYu , hubin
+ * @since 2017-06-20
+ */
+public abstract class PluginUtils {
+
+	private PluginUtils() {
+	}
+
+	public static final String DELEGATE_BOUNDSQL_SQL = "delegate.boundSql.sql";
+
+	/**
+	 * 获得真正的处理对象,可能多层代理.
+	 */
+	@SuppressWarnings("unchecked")
+	public static <T> T realTarget(Object target) {
+		if (Proxy.isProxyClass(target.getClass())) {
+			MetaObject metaObject = SystemMetaObject.forObject(target);
+			return realTarget(metaObject.getValue("h.target"));
+		}
+		return (T) target;
+	}
+
+	/**
+	 * 给 BoundSql 设置 additionalParameters
+	 * @param boundSql BoundSql
+	 * @param additionalParameters additionalParameters
+	 */
+	public static void setAdditionalParameter(BoundSql boundSql, Map<String, Object> additionalParameters) {
+		additionalParameters.forEach(boundSql::setAdditionalParameter);
+	}
+
+	public static MPBoundSql mpBoundSql(BoundSql boundSql) {
+		return new MPBoundSql(boundSql);
+	}
+
+	public static MPStatementHandler mpStatementHandler(StatementHandler statementHandler) {
+		statementHandler = realTarget(statementHandler);
+		MetaObject object = SystemMetaObject.forObject(statementHandler);
+		return new MPStatementHandler(SystemMetaObject.forObject(object.getValue("delegate")));
+	}
+
+	/**
+	 * {@link org.apache.ibatis.executor.statement.BaseStatementHandler}
+	 */
+	public static class MPStatementHandler {
+
+		private final MetaObject statementHandler;
+
+		MPStatementHandler(MetaObject statementHandler) {
+			this.statementHandler = statementHandler;
+		}
+
+		public ParameterHandler parameterHandler() {
+			return get("parameterHandler");
+		}
+
+		public MappedStatement mappedStatement() {
+			return get("mappedStatement");
+		}
+
+		public Executor executor() {
+			return get("executor");
+		}
+
+		public MPBoundSql mPBoundSql() {
+			return new MPBoundSql(boundSql());
+		}
+
+		public BoundSql boundSql() {
+			return get("boundSql");
+		}
+
+		public Configuration configuration() {
+			return get("configuration");
+		}
+
+		@SuppressWarnings("unchecked")
+		private <T> T get(String property) {
+			return (T) statementHandler.getValue(property);
+		}
+
+	}
+
+	/**
+	 * {@link BoundSql}
+	 */
+	public static class MPBoundSql {
+
+		private final MetaObject boundSql;
+
+		private final BoundSql delegate;
+
+		MPBoundSql(BoundSql boundSql) {
+			this.delegate = boundSql;
+			this.boundSql = SystemMetaObject.forObject(boundSql);
+		}
+
+		public String sql() {
+			return delegate.getSql();
+		}
+
+		public void sql(String sql) {
+			boundSql.setValue("sql", sql);
+		}
+
+		public List<ParameterMapping> parameterMappings() {
+			List<ParameterMapping> parameterMappings = delegate.getParameterMappings();
+			return new ArrayList<>(parameterMappings);
+		}
+
+		public void parameterMappings(List<ParameterMapping> parameterMappings) {
+			boundSql.setValue("parameterMappings", Collections.unmodifiableList(parameterMappings));
+		}
+
+		public Object parameterObject() {
+			return get("parameterObject");
+		}
+
+		public Map<String, Object> additionalParameters() {
+			return get("additionalParameters");
+		}
+
+		@SuppressWarnings("unchecked")
+		private <T> T get(String property) {
+			return (T) boundSql.getValue(property);
+		}
+
+	}
+
+}

+ 61 - 0
admin/src/main/java/com/your/packages/datascope/util/SqlParseUtils.java

@@ -0,0 +1,61 @@
+package com.your.packages.datascope.util;
+
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.schema.Column;
+import net.sf.jsqlparser.schema.Table;
+
+/**
+ * SQL 解析工具类
+ *
+ * @author hccake
+ */
+public final class SqlParseUtils {
+
+	private SqlParseUtils() {
+	}
+
+	private static final String MYSQL_ESCAPE_CHARACTER = "`";
+
+	/**
+	 * 兼容 mysql 转义表名 `t_xxx`
+	 * @param tableName 表名
+	 * @return 去除转移字符后的表名
+	 */
+	public static String getTableName(String tableName) {
+		if (tableName.startsWith(MYSQL_ESCAPE_CHARACTER) && tableName.endsWith(MYSQL_ESCAPE_CHARACTER)) {
+			tableName = tableName.substring(1, tableName.length() - 1);
+		}
+		return tableName;
+	}
+
+	/**
+	 * 根据当前表是否有别名,动态对字段名前添加表别名 eg. 表名: table_1 as t 原始字段:column1 返回: t.column1
+	 * @param table 表信息
+	 * @param columnName 字段名
+	 * @return 原始字段名,或者添加了表别名的字段名
+	 */
+	public Column getAliasColumn(Table table, String columnName) {
+		return getAliasColumn(table.getName(), table.getAlias(), columnName);
+	}
+
+	/**
+	 * 根据当前表是否有别名,动态对字段名前添加表别名 eg. 表名: table_1 as t 原始字段:column1 返回: t.column1
+	 * @param tableName 表名
+	 * @param tableAlias 别别名
+	 * @param columnName 字段名
+	 * @return 原始字段名,或者添加了表别名的字段名
+	 */
+	public static Column getAliasColumn(String tableName, Alias tableAlias, String columnName) {
+		StringBuilder columnBuilder = new StringBuilder();
+		// 为了兼容隐式内连接,没有别名时条件就需要加上表名
+		if (tableAlias != null) {
+			columnBuilder.append(tableAlias.getName());
+		}
+		else {
+			columnBuilder.append(tableName);
+		}
+		columnBuilder.append(".").append(columnName);
+		return new Column(columnBuilder.toString());
+	}
+
+}

+ 85 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/ExcelHandlerConfiguration.java

@@ -0,0 +1,85 @@
+package com.your.packages.hccake.common.excel;
+
+import com.alibaba.excel.converters.Converter;
+import com.hccake.common.excel.aop.ResponseExcelReturnValueHandler;
+import com.hccake.common.excel.config.ExcelConfigProperties;
+import com.hccake.common.excel.enhance.DefaultWriterBuilderEnhancer;
+import com.hccake.common.excel.enhance.WriterBuilderEnhancer;
+import com.hccake.common.excel.handler.ManySheetWriteHandler;
+import com.hccake.common.excel.handler.SheetWriteHandler;
+import com.hccake.common.excel.handler.SingleSheetWriteHandler;
+import com.hccake.common.excel.head.I18nHeaderCellWriteHandler;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+/**
+ * @author Hccake 2020/10/28
+ * @version 1.0
+ */
+@RequiredArgsConstructor
+@Configuration
+public class ExcelHandlerConfiguration {
+
+	private final ExcelConfigProperties configProperties;
+
+	private final ObjectProvider<List<Converter<?>>> converterProvider;
+
+	/**
+	 * ExcelBuild增强
+	 * @return DefaultWriterBuilderEnhancer 默认什么也不做的增强器
+	 */
+	@Bean
+	@ConditionalOnMissingBean
+	public WriterBuilderEnhancer writerBuilderEnhancer() {
+		return new DefaultWriterBuilderEnhancer();
+	}
+
+	/**
+	 * 单sheet 写入处理器
+	 */
+	@Bean
+	@ConditionalOnMissingBean
+	public SingleSheetWriteHandler singleSheetWriteHandler() {
+		return new SingleSheetWriteHandler(configProperties, converterProvider, writerBuilderEnhancer());
+	}
+
+	/**
+	 * 多sheet 写入处理器
+	 */
+	@Bean
+	@ConditionalOnMissingBean
+	public ManySheetWriteHandler manySheetWriteHandler() {
+		return new ManySheetWriteHandler(configProperties, converterProvider, writerBuilderEnhancer());
+	}
+
+	/**
+	 * 返回Excel文件的 response 处理器
+	 * @param sheetWriteHandlerList 页签写入处理器集合
+	 * @return ResponseExcelReturnValueHandler
+	 */
+	@Bean
+	@ConditionalOnMissingBean
+	public ResponseExcelReturnValueHandler responseExcelReturnValueHandler(
+			List<SheetWriteHandler> sheetWriteHandlerList) {
+		return new ResponseExcelReturnValueHandler(sheetWriteHandlerList);
+	}
+
+	/**
+	 * excel 头的国际化处理器
+	 * @param messageSource 国际化源
+	 */
+	@Bean
+	@ConditionalOnBean(MessageSource.class)
+	@ConditionalOnMissingBean
+	public I18nHeaderCellWriteHandler i18nHeaderCellWriteHandler(MessageSource messageSource) {
+		return new I18nHeaderCellWriteHandler(messageSource);
+	}
+
+}

+ 98 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/ResponseExcelAutoConfiguration.java

@@ -0,0 +1,98 @@
+package com.your.packages.hccake.common.excel;
+
+import com.hccake.common.excel.aop.DynamicNameAspect;
+import com.hccake.common.excel.aop.RequestExcelArgumentResolver;
+import com.hccake.common.excel.aop.ResponseExcelReturnValueHandler;
+import com.hccake.common.excel.config.ExcelConfigProperties;
+import com.hccake.common.excel.head.EmptyHeadGenerator;
+import com.hccake.common.excel.processor.NameProcessor;
+import com.hccake.common.excel.processor.NameSpelExpressionProcessor;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
+
+import javax.annotation.PostConstruct;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author lengleng
+ * @date 2020/3/29
+ * <p>
+ * 配置初始化
+ */
+@AutoConfiguration
+@Import(ExcelHandlerConfiguration.class)
+@RequiredArgsConstructor
+@EnableConfigurationProperties(ExcelConfigProperties.class)
+public class ResponseExcelAutoConfiguration {
+
+	private final RequestMappingHandlerAdapter requestMappingHandlerAdapter;
+
+	private final ResponseExcelReturnValueHandler responseExcelReturnValueHandler;
+
+	/**
+	 * SPEL 解析处理器
+	 * @return NameProcessor excel名称解析器
+	 */
+	@Bean
+	@ConditionalOnMissingBean
+	public NameProcessor nameProcessor() {
+		return new NameSpelExpressionProcessor();
+	}
+
+	/**
+	 * Excel名称解析处理切面
+	 * @param nameProcessor SPEL 解析处理器
+	 * @return DynamicNameAspect
+	 */
+	@Bean
+	@ConditionalOnMissingBean
+	public DynamicNameAspect dynamicNameAspect(NameProcessor nameProcessor) {
+		return new DynamicNameAspect(nameProcessor);
+	}
+
+	/**
+	 * 空的 Excel 头生成器
+	 * @return EmptyHeadGenerator
+	 */
+	@Bean
+	@ConditionalOnMissingBean
+	public EmptyHeadGenerator emptyHeadGenerator() {
+		return new EmptyHeadGenerator();
+	}
+
+	/**
+	 * 追加 Excel返回值处理器 到 springmvc 中
+	 */
+	@PostConstruct
+	public void setReturnValueHandlers() {
+		List<HandlerMethodReturnValueHandler> returnValueHandlers = requestMappingHandlerAdapter
+			.getReturnValueHandlers();
+
+		List<HandlerMethodReturnValueHandler> newHandlers = new ArrayList<>();
+		newHandlers.add(responseExcelReturnValueHandler);
+		assert returnValueHandlers != null;
+		newHandlers.addAll(returnValueHandlers);
+		requestMappingHandlerAdapter.setReturnValueHandlers(newHandlers);
+	}
+
+	/**
+	 * 追加 Excel 请求处理器 到 springmvc 中
+	 */
+	@PostConstruct
+	public void setRequestExcelArgumentResolver() {
+		List<HandlerMethodArgumentResolver> argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers();
+		List<HandlerMethodArgumentResolver> resolverList = new ArrayList<>();
+		resolverList.add(new RequestExcelArgumentResolver());
+		resolverList.addAll(argumentResolvers);
+		requestMappingHandlerAdapter.setArgumentResolvers(resolverList);
+	}
+
+}

+ 47 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/annotation/RequestExcel.java

@@ -0,0 +1,47 @@
+package com.your.packages.hccake.common.excel.annotation;
+
+import com.alibaba.excel.read.builder.AbstractExcelReaderParameterBuilder;
+import com.hccake.common.excel.handler.DefaultAnalysisEventListener;
+import com.hccake.common.excel.handler.ListAnalysisEventListener;
+
+import java.lang.annotation.*;
+
+/**
+ * 导入excel
+ *
+ * @author lengleng
+ * @author L.cm
+ * @date 2021/4/16
+ */
+@Documented
+@Target({ ElementType.PARAMETER })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RequestExcel {
+
+	/**
+	 * 前端上传字段名称 file
+	 */
+	String fileName() default "file";
+
+	/**
+	 * 读取的监听器类
+	 * @return readListener
+	 */
+	Class<? extends ListAnalysisEventListener<?>> readListener() default DefaultAnalysisEventListener.class;
+
+	/**
+	 * 是否跳过空行
+	 * @return 默认跳过
+	 */
+	boolean ignoreEmptyRow() default false;
+
+	/**
+	 * Count the number of added heads when read sheet. 0 - This Sheet has no head ,since
+	 * the first row are the data 1 - This Sheet has one row head , this is the default 2
+	 * - This Sheet has two row head ,since the third row is the data
+	 * @see AbstractExcelReaderParameterBuilder#headRowNumber
+	 * @return headRowNumber
+	 */
+	int headRowNumber() default 1;
+
+}

+ 97 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/annotation/ResponseExcel.java

@@ -0,0 +1,97 @@
+package com.your.packages.hccake.common.excel.annotation;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.support.ExcelTypeEnum;
+import com.alibaba.excel.write.handler.WriteHandler;
+import com.hccake.common.excel.head.HeadGenerator;
+
+import java.lang.annotation.*;
+
+/**
+ * `@ResponseExcel 注解`
+ *
+ * @author lengleng
+ */
+@Documented
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ResponseExcel {
+
+	/**
+	 * 文件名称
+	 * @return string
+	 */
+	String name() default "";
+
+	/**
+	 * 文件类型 (xlsx xls)
+	 * @return string
+	 */
+	ExcelTypeEnum suffix() default ExcelTypeEnum.XLSX;
+
+	/**
+	 * 文件密码
+	 * @return password
+	 */
+	String password() default "";
+
+	/**
+	 * sheet 名称,支持多个
+	 * @return String[]
+	 */
+	Sheet[] sheets() default {};
+
+	/**
+	 * 内存操作
+	 */
+	boolean inMemory() default false;
+
+	/**
+	 * excel 模板
+	 * @return String
+	 */
+	String template() default "";
+
+	/**
+	 * + 包含字段
+	 * @return String[]
+	 */
+	String[] include() default {};
+
+	/**
+	 * 排除字段
+	 * @return String[]
+	 */
+	String[] exclude() default {};
+
+	/**
+	 * 拦截器,自定义样式等处理器
+	 * @return WriteHandler[]
+	 */
+	Class<? extends WriteHandler>[] writeHandler() default {};
+
+	/**
+	 * 转换器
+	 * @return Converter[]
+	 */
+	Class<? extends Converter>[] converter() default {};
+
+	/**
+	 * 自定义Excel头生成器
+	 * @return HeadGenerator
+	 */
+	Class<? extends HeadGenerator> headGenerator() default HeadGenerator.class;
+
+	/**
+	 * excel 头信息国际化
+	 * @return boolean
+	 */
+	boolean i18nHeader() default false;
+
+	/**
+	 * 填充模式
+	 * @return boolean
+	 */
+	boolean fill() default false;
+
+}

+ 39 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/annotation/Sheet.java

@@ -0,0 +1,39 @@
+package com.your.packages.hccake.common.excel.annotation;
+
+import com.hccake.common.excel.head.HeadGenerator;
+
+import java.lang.annotation.*;
+
+/**
+ * 用于指定导入导出的 excel 的 sheet 属性
+ *
+ * @author Yakir 2021/4/29 15:03
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Sheet {
+
+	int sheetNo() default -1;
+
+	/**
+	 * sheet name
+	 */
+	String sheetName();
+
+	/**
+	 * 包含字段
+	 */
+	String[] includes() default {};
+
+	/**
+	 * 排除字段
+	 */
+	String[] excludes() default {};
+
+	/**
+	 * 头生成器
+	 */
+	Class<? extends HeadGenerator> headGenerateClass() default HeadGenerator.class;
+
+}

+ 46 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/aop/DynamicNameAspect.java

@@ -0,0 +1,46 @@
+package com.your.packages.hccake.common.excel.aop;
+
+import com.hccake.common.excel.annotation.ResponseExcel;
+import com.hccake.common.excel.processor.NameProcessor;
+import lombok.RequiredArgsConstructor;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+/**
+ * @author lengleng
+ * @date 2020/3/29
+ */
+@Aspect
+@RequiredArgsConstructor
+public class DynamicNameAspect {
+
+	public static final String EXCEL_NAME_KEY = "__EXCEL_NAME_KEY__";
+
+	private final NameProcessor processor;
+
+	@Before("@annotation(excel)")
+	public void before(JoinPoint point, ResponseExcel excel) {
+		MethodSignature ms = (MethodSignature) point.getSignature();
+
+		String name = excel.name();
+		// 当配置的 excel 名称为空时,取当前时间
+		if (!StringUtils.hasText(name)) {
+			name = LocalDateTime.now().toString();
+		}
+		else {
+			name = processor.doDetermineName(point.getArgs(), ms.getMethod(), excel.name());
+		}
+
+		RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+		Objects.requireNonNull(requestAttributes).setAttribute(EXCEL_NAME_KEY, name, RequestAttributes.SCOPE_REQUEST);
+	}
+
+}

+ 92 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/aop/RequestExcelArgumentResolver.java

@@ -0,0 +1,92 @@
+package com.your.packages.hccake.common.excel.aop;
+
+import com.alibaba.excel.EasyExcel;
+import com.hccake.common.excel.annotation.RequestExcel;
+import com.hccake.common.excel.converters.LocalDateStringConverter;
+import com.hccake.common.excel.converters.LocalDateTimeStringConverter;
+import com.hccake.common.excel.handler.ListAnalysisEventListener;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ResolvableType;
+import org.springframework.ui.ModelMap;
+import org.springframework.util.Assert;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.WebDataBinder;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.ModelAndViewContainer;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.multipart.MultipartRequest;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * 上传excel 解析注解
+ *
+ * @author lengleng
+ * @author L.cm
+ * @date 2021/4/16
+ */
+@Slf4j
+public class RequestExcelArgumentResolver implements HandlerMethodArgumentResolver {
+
+	@Override
+	public boolean supportsParameter(MethodParameter parameter) {
+		return parameter.hasParameterAnnotation(RequestExcel.class);
+	}
+
+	@SneakyThrows(Exception.class)
+	@Override
+	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer modelAndViewContainer,
+			NativeWebRequest webRequest, WebDataBinderFactory webDataBinderFactory) {
+		Class<?> parameterType = parameter.getParameterType();
+		if (!parameterType.isAssignableFrom(List.class)) {
+			throw new IllegalArgumentException(
+					"Excel upload request resolver error, @RequestExcel parameter is not List " + parameterType);
+		}
+
+		// 处理自定义 readListener
+		RequestExcel requestExcel = parameter.getParameterAnnotation(RequestExcel.class);
+		assert requestExcel != null;
+		Class<? extends ListAnalysisEventListener<?>> readListenerClass = requestExcel.readListener();
+		ListAnalysisEventListener<?> readListener = BeanUtils.instantiateClass(readListenerClass);
+
+		// 获取请求文件流
+		HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
+		assert request != null;
+		InputStream inputStream;
+		if (request instanceof MultipartRequest) {
+			MultipartFile file = ((MultipartRequest) request).getFile(requestExcel.fileName());
+			Assert.notNull(file, "excel import: file can not be null!");
+			inputStream = file.getInputStream();
+		}
+		else {
+			inputStream = request.getInputStream();
+		}
+
+		// 获取目标类型
+		Class<?> excelModelClass = ResolvableType.forMethodParameter(parameter).getGeneric(0).resolve();
+
+		// 这里需要指定读用哪个 class 去读,然后读取第一个 sheet 文件流会自动关闭
+		EasyExcel.read(inputStream, excelModelClass, readListener)
+			.registerConverter(LocalDateStringConverter.INSTANCE)
+			.registerConverter(LocalDateTimeStringConverter.INSTANCE)
+			.ignoreEmptyRow(requestExcel.ignoreEmptyRow())
+			.sheet()
+			.headRowNumber(requestExcel.headRowNumber())
+			.doRead();
+
+		// 校验失败的数据处理 交给 BindResult
+		WebDataBinder dataBinder = webDataBinderFactory.createBinder(webRequest, readListener.getErrors(), "excel");
+		ModelMap model = modelAndViewContainer.getModel();
+		model.put(BindingResult.MODEL_KEY_PREFIX + "excel", dataBinder.getBindingResult());
+
+		return readListener.getList();
+	}
+
+}

+ 61 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/aop/ResponseExcelReturnValueHandler.java

@@ -0,0 +1,61 @@
+package com.your.packages.hccake.common.excel.aop;
+
+import com.hccake.common.excel.annotation.ResponseExcel;
+import com.hccake.common.excel.handler.SheetWriteHandler;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.MethodParameter;
+import org.springframework.util.Assert;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+
+/**
+ * 处理@ResponseExcel 返回值
+ *
+ * @author lengleng
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class ResponseExcelReturnValueHandler implements HandlerMethodReturnValueHandler {
+
+	private final List<SheetWriteHandler> sheetWriteHandlerList;
+
+	/**
+	 * 只处理@ResponseExcel 声明的方法
+	 * @param parameter 方法签名
+	 * @return 是否处理
+	 */
+	@Override
+	public boolean supportsReturnType(MethodParameter parameter) {
+		return parameter.getMethodAnnotation(ResponseExcel.class) != null;
+	}
+
+	/**
+	 * 处理逻辑
+	 * @param o 返回参数
+	 * @param parameter 方法签名
+	 * @param mavContainer 上下文容器
+	 * @param nativeWebRequest 上下文
+	 * @throws Exception 处理异常
+	 */
+	@Override
+	public void handleReturnValue(Object o, MethodParameter parameter, ModelAndViewContainer mavContainer,
+			NativeWebRequest nativeWebRequest) throws Exception {
+		/* check */
+		HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);
+		Assert.state(response != null, "No HttpServletResponse");
+		ResponseExcel responseExcel = parameter.getMethodAnnotation(ResponseExcel.class);
+		Assert.state(responseExcel != null, "No @ResponseExcel");
+		mavContainer.setRequestHandled(true);
+
+		sheetWriteHandlerList.stream()
+			.filter(handler -> handler.support(o))
+			.findFirst()
+			.ifPresent(handler -> handler.export(o, response, responseExcel));
+	}
+
+}

+ 20 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/config/ExcelConfigProperties.java

@@ -0,0 +1,20 @@
+package com.your.packages.hccake.common.excel.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * @author lengleng 2020/3/29
+ */
+@Data
+@ConfigurationProperties(prefix = ExcelConfigProperties.PREFIX)
+public class ExcelConfigProperties {
+
+	static final String PREFIX = "excel";
+
+	/**
+	 * 模板路径
+	 */
+	private String templatePath = "excel";
+
+}

+ 62 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/converters/LocalDateStringConverter.java

@@ -0,0 +1,62 @@
+package com.your.packages.hccake.common.excel.converters;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+import java.text.ParseException;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * LocalDate and string converter
+ *
+ * @author L.cm
+ */
+public enum LocalDateStringConverter implements Converter<LocalDate> {
+
+	/**
+	 * 实例
+	 */
+	INSTANCE;
+
+	@Override
+	public Class supportJavaTypeKey() {
+		return LocalDate.class;
+	}
+
+	@Override
+	public CellDataTypeEnum supportExcelTypeKey() {
+		return CellDataTypeEnum.STRING;
+	}
+
+	@Override
+	public LocalDate convertToJavaData(ReadCellData cellData, ExcelContentProperty contentProperty,
+			GlobalConfiguration globalConfiguration) throws ParseException {
+		if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
+			return LocalDate.parse(cellData.getStringValue());
+		}
+		else {
+			DateTimeFormatter formatter = DateTimeFormatter
+				.ofPattern(contentProperty.getDateTimeFormatProperty().getFormat());
+			return LocalDate.parse(cellData.getStringValue(), formatter);
+		}
+	}
+
+	@Override
+	public WriteCellData<String> convertToExcelData(LocalDate value, ExcelContentProperty contentProperty,
+			GlobalConfiguration globalConfiguration) {
+		DateTimeFormatter formatter;
+		if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
+			formatter = DateTimeFormatter.ISO_LOCAL_DATE;
+		}
+		else {
+			formatter = DateTimeFormatter.ofPattern(contentProperty.getDateTimeFormatProperty().getFormat());
+		}
+		return new WriteCellData<>(value.format(formatter));
+	}
+
+}

+ 93 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/converters/LocalDateTimeStringConverter.java

@@ -0,0 +1,93 @@
+package com.your.packages.hccake.common.excel.converters;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import com.alibaba.excel.util.DateUtils;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * LocalDateTime and string converter
+ *
+ * @author L.cm
+ */
+public enum LocalDateTimeStringConverter implements Converter<LocalDateTime> {
+
+	/**
+	 * 实例
+	 */
+	INSTANCE;
+
+	private static final String MINUS = "-";
+
+	@Override
+	public Class supportJavaTypeKey() {
+		return LocalDateTime.class;
+	}
+
+	@Override
+	public CellDataTypeEnum supportExcelTypeKey() {
+		return CellDataTypeEnum.STRING;
+	}
+
+	@Override
+	public LocalDateTime convertToJavaData(ReadCellData cellData, ExcelContentProperty contentProperty,
+			GlobalConfiguration globalConfiguration) {
+		String stringValue = cellData.getStringValue();
+		String pattern;
+		if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
+			pattern = switchDateFormat(stringValue);
+		}
+		else {
+			pattern = contentProperty.getDateTimeFormatProperty().getFormat();
+		}
+		DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
+		return LocalDateTime.parse(cellData.getStringValue(), formatter);
+	}
+
+	@Override
+	public WriteCellData<String> convertToExcelData(LocalDateTime value, ExcelContentProperty contentProperty,
+			GlobalConfiguration globalConfiguration) {
+		String pattern;
+		if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) {
+			pattern = DateUtils.DATE_FORMAT_19;
+		}
+		else {
+			pattern = contentProperty.getDateTimeFormatProperty().getFormat();
+		}
+		DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
+		return new WriteCellData<>(value.format(formatter));
+	}
+
+	/**
+	 * switch date format
+	 * @param dateString dateString
+	 * @return pattern
+	 */
+	private static String switchDateFormat(String dateString) {
+		int length = dateString.length();
+		switch (length) {
+			case 19:
+				if (dateString.contains(MINUS)) {
+					return DateUtils.DATE_FORMAT_19;
+				}
+				else {
+					return DateUtils.DATE_FORMAT_19_FORWARD_SLASH;
+				}
+			case 17:
+				return DateUtils.DATE_FORMAT_17;
+			case 14:
+				return DateUtils.DATE_FORMAT_14;
+			case 10:
+				return DateUtils.DATE_FORMAT_10;
+			default:
+				throw new IllegalArgumentException("can not find date format for:" + dateString);
+		}
+	}
+
+}

+ 41 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/domain/ErrorMessage.java

@@ -0,0 +1,41 @@
+package com.your.packages.hccake.common.excel.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 校验错误信息
+ *
+ * @author lengleng
+ * @date 2021/8/4
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErrorMessage {
+
+	/**
+	 * 行号
+	 */
+	private Long lineNum;
+
+	/**
+	 * 错误信息
+	 */
+	private Set<String> errors = new HashSet<>();
+
+	public ErrorMessage(Set<String> errors) {
+		this.errors = errors;
+	}
+
+	public ErrorMessage(String error) {
+		HashSet<String> objects = new HashSet<>();
+		objects.add(error);
+		this.errors = objects;
+	}
+
+}

+ 53 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/domain/SheetBuildProperties.java

@@ -0,0 +1,53 @@
+package com.your.packages.hccake.common.excel.domain;
+
+import com.hccake.common.excel.annotation.Sheet;
+import com.hccake.common.excel.head.HeadGenerator;
+import lombok.Data;
+
+/**
+ * Sheet Build Properties
+ *
+ * @author chengbohua
+ */
+@Data
+public class SheetBuildProperties {
+
+	/**
+	 * sheet 编号
+	 */
+	private int sheetNo = -1;
+
+	/**
+	 * sheet name
+	 */
+	private String sheetName;
+
+	/**
+	 * 包含字段
+	 */
+	private String[] includes = new String[0];
+
+	/**
+	 * 排除字段
+	 */
+	private String[] excludes = new String[0];
+
+	/**
+	 * 头生成器
+	 */
+	private Class<? extends HeadGenerator> headGenerateClass = HeadGenerator.class;
+
+	public SheetBuildProperties(Sheet sheetAnnotation) {
+		this.sheetNo = sheetAnnotation.sheetNo();
+		this.sheetName = sheetAnnotation.sheetName();
+		this.includes = sheetAnnotation.includes();
+		this.excludes = sheetAnnotation.excludes();
+		this.headGenerateClass = sheetAnnotation.headGenerateClass();
+	}
+
+	public SheetBuildProperties(int index) {
+		this.sheetNo = index;
+		this.sheetName = "sheet" + (sheetNo + 1);
+	}
+
+}

+ 48 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/enhance/DefaultWriterBuilderEnhancer.java

@@ -0,0 +1,48 @@
+package com.your.packages.hccake.common.excel.enhance;
+
+import com.alibaba.excel.write.builder.ExcelWriterBuilder;
+import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
+import com.hccake.common.excel.annotation.ResponseExcel;
+import com.hccake.common.excel.head.HeadGenerator;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * @author Hccake 2020/12/18
+ * @version 1.0
+ */
+public class DefaultWriterBuilderEnhancer implements WriterBuilderEnhancer {
+
+	/**
+	 * ExcelWriterBuilder 增强
+	 * @param writerBuilder ExcelWriterBuilder
+	 * @param response HttpServletResponse
+	 * @param responseExcel ResponseExcel
+	 * @param templatePath 模板地址
+	 * @return ExcelWriterBuilder
+	 */
+	@Override
+	public ExcelWriterBuilder enhanceExcel(ExcelWriterBuilder writerBuilder, HttpServletResponse response,
+			ResponseExcel responseExcel, String templatePath) {
+		// doNothing
+		return writerBuilder;
+	}
+
+	/**
+	 * ExcelWriterSheetBuilder 增强
+	 * @param writerSheetBuilder ExcelWriterSheetBuilder
+	 * @param sheetNo sheet角标
+	 * @param sheetName sheet名,有模板时为空
+	 * @param dataClass 当前写入的数据所属类
+	 * @param template 模板文件
+	 * @param headEnhancerClass 当前指定的自定义头处理器
+	 * @return ExcelWriterSheetBuilder
+	 */
+	@Override
+	public ExcelWriterSheetBuilder enhanceSheet(ExcelWriterSheetBuilder writerSheetBuilder, Integer sheetNo,
+			String sheetName, Class<?> dataClass, String template, Class<? extends HeadGenerator> headEnhancerClass) {
+		// doNothing
+		return writerSheetBuilder;
+	}
+
+}

+ 42 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/enhance/WriterBuilderEnhancer.java

@@ -0,0 +1,42 @@
+package com.your.packages.hccake.common.excel.enhance;
+
+import com.alibaba.excel.write.builder.ExcelWriterBuilder;
+import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
+import com.hccake.common.excel.annotation.ResponseExcel;
+import com.hccake.common.excel.head.HeadGenerator;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * ExcelWriterBuilder 增强
+ *
+ * @author Hccake 2020/12/18
+ * @version 1.0
+ */
+public interface WriterBuilderEnhancer {
+
+	/**
+	 * ExcelWriterBuilder 增强
+	 * @param writerBuilder ExcelWriterBuilder
+	 * @param response HttpServletResponse
+	 * @param responseExcel ResponseExcel
+	 * @param templatePath 模板地址
+	 * @return ExcelWriterBuilder
+	 */
+	ExcelWriterBuilder enhanceExcel(ExcelWriterBuilder writerBuilder, HttpServletResponse response,
+                                    ResponseExcel responseExcel, String templatePath);
+
+	/**
+	 * ExcelWriterSheetBuilder 增强
+	 * @param writerSheetBuilder ExcelWriterSheetBuilder
+	 * @param sheetNo sheet角标
+	 * @param sheetName sheet名,有模板时为空
+	 * @param dataClass 当前写入的数据所属类
+	 * @param template 模板文件
+	 * @param headEnhancerClass 当前指定的自定义头处理器
+	 * @return ExcelWriterSheetBuilder
+	 */
+	ExcelWriterSheetBuilder enhanceSheet(ExcelWriterSheetBuilder writerSheetBuilder, Integer sheetNo, String sheetName,
+                                         Class<?> dataClass, String template, Class<? extends HeadGenerator> headEnhancerClass);
+
+}

+ 257 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/handler/AbstractSheetWriteHandler.java

@@ -0,0 +1,257 @@
+package com.your.packages.hccake.common.excel.handler;
+
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.ExcelWriter;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.write.builder.ExcelWriterBuilder;
+import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
+import com.alibaba.excel.write.handler.WriteHandler;
+import com.alibaba.excel.write.metadata.WriteSheet;
+import com.hccake.common.excel.annotation.ResponseExcel;
+import com.hccake.common.excel.aop.DynamicNameAspect;
+import com.hccake.common.excel.config.ExcelConfigProperties;
+import com.hccake.common.excel.converters.LocalDateStringConverter;
+import com.hccake.common.excel.converters.LocalDateTimeStringConverter;
+
+import com.hccake.common.excel.domain.SheetBuildProperties;
+import com.hccake.common.excel.enhance.WriterBuilderEnhancer;
+import com.hccake.common.excel.head.HeadGenerator;
+import com.hccake.common.excel.head.HeadMeta;
+import com.hccake.common.excel.head.I18nHeaderCellWriteHandler;
+import com.hccake.common.excel.kit.ExcelException;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.SneakyThrows;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.MediaTypeFactory;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Modifier;
+import java.net.URLEncoder;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * @author lengleng
+ * @author L.cm
+ * @author Hccake
+ * @date 2020/3/31
+ */
+@RequiredArgsConstructor
+public abstract class AbstractSheetWriteHandler implements SheetWriteHandler, ApplicationContextAware {
+
+	private final ExcelConfigProperties configProperties;
+
+	private final ObjectProvider<List<Converter<?>>> converterProvider;
+
+	private final WriterBuilderEnhancer excelWriterBuilderEnhance;
+
+	private ApplicationContext applicationContext;
+
+	@Getter
+	@Setter
+	@Autowired(required = false)
+	private I18nHeaderCellWriteHandler i18nHeaderCellWriteHandler;
+
+	@Override
+	public void check(ResponseExcel responseExcel) {
+		if (responseExcel.fill() && !StringUtils.hasText(responseExcel.template())) {
+			throw new ExcelException("@ResponseExcel fill 必须配合 template 使用");
+		}
+	}
+
+	@Override
+	@SneakyThrows(UnsupportedEncodingException.class)
+	public void export(Object o, HttpServletResponse response, ResponseExcel responseExcel) {
+		check(responseExcel);
+		RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+		String name = (String) Objects.requireNonNull(requestAttributes)
+			.getAttribute(DynamicNameAspect.EXCEL_NAME_KEY, RequestAttributes.SCOPE_REQUEST);
+		if (name == null) {
+			name = UUID.randomUUID().toString();
+		}
+		String fileName = String.format("%s%s", URLEncoder.encode(name, "UTF-8"), responseExcel.suffix().getValue())
+			.replaceAll("\\+", "%20");
+		// 根据实际的文件类型找到对应的 contentType
+		String contentType = MediaTypeFactory.getMediaType(fileName)
+			.map(MediaType::toString)
+			.orElse("application/vnd.ms-excel");
+		response.setContentType(contentType);
+		response.setCharacterEncoding("utf-8");
+		response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename*=utf-8''" + fileName);
+		write(o, response, responseExcel);
+	}
+
+	/**
+	 * 通用的获取ExcelWriter方法
+	 * @param response HttpServletResponse
+	 * @param responseExcel ResponseExcel注解
+	 * @return ExcelWriter
+	 */
+	@SneakyThrows(IOException.class)
+	public ExcelWriter getExcelWriter(HttpServletResponse response, ResponseExcel responseExcel) {
+		ExcelWriterBuilder writerBuilder = EasyExcel.write(response.getOutputStream())
+			.registerConverter(LocalDateStringConverter.INSTANCE)
+			.registerConverter(LocalDateTimeStringConverter.INSTANCE)
+			.autoCloseStream(true)
+			.excelType(responseExcel.suffix())
+			.inMemory(responseExcel.inMemory());
+
+		if (StringUtils.hasText(responseExcel.password())) {
+			writerBuilder.password(responseExcel.password());
+		}
+
+		if (responseExcel.include().length != 0) {
+			writerBuilder.includeColumnFieldNames(Arrays.asList(responseExcel.include()));
+		}
+
+		if (responseExcel.exclude().length != 0) {
+			writerBuilder.excludeColumnFieldNames(Arrays.asList(responseExcel.exclude()));
+		}
+
+		for (Class<? extends WriteHandler> clazz : responseExcel.writeHandler()) {
+			writerBuilder.registerWriteHandler(BeanUtils.instantiateClass(clazz));
+		}
+
+		// 开启国际化头信息处理
+		if (responseExcel.i18nHeader() && i18nHeaderCellWriteHandler != null) {
+			writerBuilder.registerWriteHandler(i18nHeaderCellWriteHandler);
+		}
+
+		// 自定义注入的转换器
+		registerCustomConverter(writerBuilder);
+
+		for (Class<? extends Converter> clazz : responseExcel.converter()) {
+			writerBuilder.registerConverter(BeanUtils.instantiateClass(clazz));
+		}
+
+		String templatePath = configProperties.getTemplatePath();
+		if (StringUtils.hasText(responseExcel.template())) {
+			ClassPathResource classPathResource = new ClassPathResource(
+					templatePath + File.separator + responseExcel.template());
+			InputStream inputStream = classPathResource.getInputStream();
+			writerBuilder.withTemplate(inputStream);
+		}
+
+		writerBuilder = excelWriterBuilderEnhance.enhanceExcel(writerBuilder, response, responseExcel, templatePath);
+
+		return writerBuilder.build();
+	}
+
+	/**
+	 * 自定义注入转换器 如果有需要,子类自己重写
+	 * @param builder ExcelWriterBuilder
+	 */
+	public void registerCustomConverter(ExcelWriterBuilder builder) {
+		converterProvider.ifAvailable(converters -> converters.forEach(builder::registerConverter));
+	}
+
+	/**
+	 * 构建一个 空的 WriteSheet 对象
+	 * @param sheetBuildProperties sheet build 属性
+	 * @param template 模板信息
+	 * @return WriteSheet
+	 */
+	public WriteSheet emptySheet(SheetBuildProperties sheetBuildProperties, String template) {
+		// Sheet 编号和名称
+		Integer sheetNo = sheetBuildProperties.getSheetNo() >= 0 ? sheetBuildProperties.getSheetNo() : null;
+		String sheetName = sheetBuildProperties.getSheetName();
+
+		// 是否模板写入
+		ExcelWriterSheetBuilder writerSheetBuilder = StringUtils.hasText(template) ? EasyExcel.writerSheet(sheetNo)
+				: EasyExcel.writerSheet(sheetNo, sheetName);
+
+		return writerSheetBuilder.build();
+	}
+
+	/**
+	 * 获取 WriteSheet 对象
+	 * @param sheetBuildProperties sheet annotation info
+	 * @param dataClass 数据类型
+	 * @param template 模板
+	 * @param bookHeadEnhancerClass 自定义头处理器
+	 * @return WriteSheet
+	 */
+	public WriteSheet emptySheet(SheetBuildProperties sheetBuildProperties, Class<?> dataClass, String template,
+			Class<? extends HeadGenerator> bookHeadEnhancerClass) {
+
+		// Sheet 编号和名称
+		Integer sheetNo = sheetBuildProperties.getSheetNo() >= 0 ? sheetBuildProperties.getSheetNo() : null;
+		String sheetName = sheetBuildProperties.getSheetName();
+
+		// 是否模板写入
+		ExcelWriterSheetBuilder writerSheetBuilder = StringUtils.hasText(template) ? EasyExcel.writerSheet(sheetNo)
+				: EasyExcel.writerSheet(sheetNo, sheetName);
+
+		// 头信息增强 1. 优先使用 sheet 指定的头信息增强 2. 其次使用 @ResponseExcel 中定义的全局头信息增强
+		Class<? extends HeadGenerator> headGenerateClass = null;
+		if (isNotInterface(sheetBuildProperties.getHeadGenerateClass())) {
+			headGenerateClass = sheetBuildProperties.getHeadGenerateClass();
+		}
+		else if (isNotInterface(bookHeadEnhancerClass)) {
+			headGenerateClass = bookHeadEnhancerClass;
+		}
+		// 定义头信息增强则使用其生成头信息,否则使用 dataClass 来自动获取
+		if (headGenerateClass != null) {
+			fillCustomHeadInfo(dataClass, bookHeadEnhancerClass, writerSheetBuilder);
+		}
+		else if (dataClass != null) {
+			writerSheetBuilder.head(dataClass);
+			if (sheetBuildProperties.getExcludes().length > 0) {
+				writerSheetBuilder.excludeColumnFieldNames(Arrays.asList(sheetBuildProperties.getExcludes()));
+			}
+			if (sheetBuildProperties.getIncludes().length > 0) {
+				writerSheetBuilder.includeColumnFieldNames(Arrays.asList(sheetBuildProperties.getIncludes()));
+			}
+		}
+
+		// sheetBuilder 增强
+		writerSheetBuilder = excelWriterBuilderEnhance.enhanceSheet(writerSheetBuilder, sheetNo, sheetName, dataClass,
+				template, headGenerateClass);
+
+		return writerSheetBuilder.build();
+	}
+
+	private void fillCustomHeadInfo(Class<?> dataClass, Class<? extends HeadGenerator> headEnhancerClass,
+			ExcelWriterSheetBuilder writerSheetBuilder) {
+		HeadGenerator headGenerator = this.applicationContext.getBean(headEnhancerClass);
+		Assert.notNull(headGenerator, "The header generated bean does not exist.");
+		HeadMeta head = headGenerator.head(dataClass);
+		writerSheetBuilder.head(head.getHead());
+		writerSheetBuilder.excludeColumnFieldNames(head.getIgnoreHeadFields());
+	}
+
+	/**
+	 * 是否为Null Head Generator
+	 * @param headGeneratorClass 头生成器类型
+	 * @return true 已指定 false 未指定(默认值)
+	 */
+	private boolean isNotInterface(Class<? extends HeadGenerator> headGeneratorClass) {
+		return !Modifier.isInterface(headGeneratorClass.getModifiers());
+	}
+
+	@Override
+	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+		this.applicationContext = applicationContext;
+	}
+
+}

+ 63 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/handler/DefaultAnalysisEventListener.java

@@ -0,0 +1,63 @@
+package com.your.packages.hccake.common.excel.handler;
+
+import com.alibaba.excel.context.AnalysisContext;
+import com.hccake.common.excel.kit.Validators;
+import com.hccake.common.excel.domain.ErrorMessage;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.validation.ConstraintViolation;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 默认的 AnalysisEventListener
+ *
+ * @author lengleng
+ * @author L.cm
+ * @date 2021/4/16
+ */
+@Slf4j
+public class DefaultAnalysisEventListener extends ListAnalysisEventListener<Object> {
+
+	private final List<Object> list = new ArrayList<>();
+
+	private final List<ErrorMessage> errorMessageList = new ArrayList<>();
+
+	@Setter
+	private Long lineNum = 1L;
+
+	@Override
+	public void invoke(Object o, AnalysisContext analysisContext) {
+		lineNum++;
+
+		Set<ConstraintViolation<Object>> violations = Validators.validate(o);
+		if (!violations.isEmpty()) {
+			Set<String> messageSet = violations.stream()
+				.map(ConstraintViolation::getMessage)
+				.collect(Collectors.toSet());
+			errorMessageList.add(new ErrorMessage(lineNum, messageSet));
+		}
+		else {
+			list.add(o);
+		}
+	}
+
+	@Override
+	public void doAfterAllAnalysed(AnalysisContext analysisContext) {
+		log.debug("Excel read analysed");
+	}
+
+	@Override
+	public List<Object> getList() {
+		return list;
+	}
+
+	@Override
+	public List<ErrorMessage> getErrors() {
+		return errorMessageList;
+	}
+
+}

+ 27 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/handler/ListAnalysisEventListener.java

@@ -0,0 +1,27 @@
+package com.your.packages.hccake.common.excel.handler;
+
+import com.alibaba.excel.event.AnalysisEventListener;
+import com.hccake.common.excel.domain.ErrorMessage;
+
+import java.util.List;
+
+/**
+ * list analysis EventListener
+ *
+ * @author L.cm
+ */
+public abstract class ListAnalysisEventListener<T> extends AnalysisEventListener<T> {
+
+	/**
+	 * 获取 excel 解析的对象列表
+	 * @return 集合
+	 */
+	public abstract List<T> getList();
+
+	/**
+	 * 获取异常校验结果
+	 * @return 集合
+	 */
+	public abstract List<ErrorMessage> getErrors();
+
+}

+ 104 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/handler/ManySheetWriteHandler.java

@@ -0,0 +1,104 @@
+package com.your.packages.hccake.common.excel.handler;
+
+import com.alibaba.excel.ExcelWriter;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.write.metadata.WriteSheet;
+import com.hccake.common.excel.annotation.ResponseExcel;
+import com.hccake.common.excel.annotation.Sheet;
+import com.hccake.common.excel.config.ExcelConfigProperties;
+import com.hccake.common.excel.domain.SheetBuildProperties;
+import com.hccake.common.excel.enhance.WriterBuilderEnhancer;
+import com.hccake.common.excel.kit.ExcelException;
+import org.springframework.beans.factory.ObjectProvider;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author lengleng
+ * @date 2020/3/29
+ */
+public class ManySheetWriteHandler extends AbstractSheetWriteHandler {
+
+	public ManySheetWriteHandler(ExcelConfigProperties configProperties,
+			ObjectProvider<List<Converter<?>>> converterProvider, WriterBuilderEnhancer excelWriterBuilderEnhance) {
+		super(configProperties, converterProvider, excelWriterBuilderEnhance);
+	}
+
+	/**
+	 * 当且仅当List不为空且List中的元素也是List 才返回true
+	 * @param obj 返回对象
+	 * @return boolean
+	 */
+	@Override
+	public boolean support(Object obj) {
+		if (obj instanceof List) {
+			List<?> objList = (List<?>) obj;
+			return !objList.isEmpty() && objList.get(0) instanceof List;
+		}
+		else {
+			throw new ExcelException("@ResponseExcel 返回值必须为List类型");
+		}
+	}
+
+	@Override
+	public void write(Object obj, HttpServletResponse response, ResponseExcel responseExcel) {
+		List<?> objList = (List<?>) obj;
+		int objListSize = objList.size();
+
+		String template = responseExcel.template();
+
+		ExcelWriter excelWriter = getExcelWriter(response, responseExcel);
+		List<SheetBuildProperties> sheetBuildPropertiesList = getSheetBuildProperties(responseExcel, objListSize);
+
+		for (int i = 0; i < sheetBuildPropertiesList.size(); i++) {
+			SheetBuildProperties sheetBuildProperties = sheetBuildPropertiesList.get(i);
+			// 创建sheet
+			WriteSheet sheet;
+			List<?> eleList;
+			if (objListSize <= i) {
+				eleList = new ArrayList<>();
+				sheet = this.emptySheet(sheetBuildProperties, template);
+			}
+			else {
+				eleList = (List<?>) objList.get(i);
+				if (eleList.isEmpty()) {
+					sheet = this.emptySheet(sheetBuildProperties, template);
+				}
+				else {
+					Class<?> dataClass = eleList.get(0).getClass();
+					sheet = this.emptySheet(sheetBuildProperties, dataClass, template, responseExcel.headGenerator());
+				}
+			}
+
+			if (responseExcel.fill()) {
+				// 填充 sheet
+				excelWriter.fill(eleList, sheet);
+			}
+			else {
+				// 写入 sheet
+				excelWriter.write(eleList, sheet);
+			}
+		}
+
+		excelWriter.finish();
+	}
+
+	private static List<SheetBuildProperties> getSheetBuildProperties(ResponseExcel responseExcel, int objListSize) {
+		List<SheetBuildProperties> sheetBuildPropertiesList = new ArrayList<>();
+		Sheet[] sheets = responseExcel.sheets();
+		if (sheets != null && sheets.length > 0) {
+			for (Sheet sheet : sheets) {
+				sheetBuildPropertiesList.add(new SheetBuildProperties(sheet));
+			}
+		}
+		else {
+			for (int i = 0; i < objListSize; i++) {
+				sheetBuildPropertiesList.add(new SheetBuildProperties(i));
+			}
+		}
+		return sheetBuildPropertiesList;
+	}
+
+}

+ 44 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/handler/SheetWriteHandler.java

@@ -0,0 +1,44 @@
+package com.your.packages.hccake.common.excel.handler;
+
+import com.hccake.common.excel.annotation.ResponseExcel;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * @author lengleng
+ * @date 2020/3/29
+ * <p>
+ * sheet 写出处理器
+ */
+public interface SheetWriteHandler {
+
+	/**
+	 * 是否支持
+	 * @param obj 返回对象
+	 * @return boolean
+	 */
+	boolean support(Object obj);
+
+	/**
+	 * 校验
+	 * @param responseExcel 注解
+	 */
+	void check(ResponseExcel responseExcel);
+
+	/**
+	 * 返回的对象
+	 * @param o obj
+	 * @param response 输出对象
+	 * @param responseExcel 注解
+	 */
+	void export(Object o, HttpServletResponse response, ResponseExcel responseExcel);
+
+	/**
+	 * 写成对象
+	 * @param o obj
+	 * @param response 输出对象
+	 * @param responseExcel 注解
+	 */
+	void write(Object o, HttpServletResponse response, ResponseExcel responseExcel);
+
+}

+ 86 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/handler/SingleSheetWriteHandler.java

@@ -0,0 +1,86 @@
+package com.your.packages.hccake.common.excel.handler;
+
+import com.alibaba.excel.ExcelWriter;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.write.metadata.WriteSheet;
+import com.hccake.common.excel.annotation.ResponseExcel;
+import com.hccake.common.excel.annotation.Sheet;
+import com.hccake.common.excel.config.ExcelConfigProperties;
+import com.hccake.common.excel.domain.SheetBuildProperties;
+import com.hccake.common.excel.enhance.WriterBuilderEnhancer;
+import com.hccake.common.excel.kit.ExcelException;
+import org.springframework.beans.factory.ObjectProvider;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+
+/**
+ * @author lengleng
+ * @date 2020/3/29
+ * <p>
+ * 处理单sheet 页面
+ */
+public class SingleSheetWriteHandler extends AbstractSheetWriteHandler {
+
+	public SingleSheetWriteHandler(ExcelConfigProperties configProperties,
+			ObjectProvider<List<Converter<?>>> converterProvider, WriterBuilderEnhancer excelWriterBuilderEnhance) {
+		super(configProperties, converterProvider, excelWriterBuilderEnhance);
+	}
+
+	/**
+	 * obj 是List 且list不为空同时list中的元素不是是List 才返回true
+	 * @param obj 返回对象
+	 * @return boolean
+	 */
+	@Override
+	public boolean support(Object obj) {
+		if (obj instanceof List) {
+			List<?> objList = (List<?>) obj;
+			return !objList.isEmpty() && !(objList.get(0) instanceof List);
+		}
+		else {
+			throw new ExcelException("@ResponseExcel 返回值必须为List类型");
+		}
+	}
+
+	@Override
+	public void write(Object obj, HttpServletResponse response, ResponseExcel responseExcel) {
+		List<?> eleList = (List<?>) obj;
+		ExcelWriter excelWriter = getExcelWriter(response, responseExcel);
+
+		// 获取 Sheet 配置
+		SheetBuildProperties sheetBuildProperties;
+		Sheet[] sheets = responseExcel.sheets();
+		if (sheets != null && sheets.length > 0) {
+			sheetBuildProperties = new SheetBuildProperties(sheets[0]);
+		}
+		else {
+			sheetBuildProperties = new SheetBuildProperties(0);
+		}
+
+		// 模板信息
+		String template = responseExcel.template();
+
+		// 创建sheet
+		WriteSheet sheet;
+		if (eleList.isEmpty()) {
+			sheet = this.emptySheet(sheetBuildProperties, template);
+		}
+		else {
+			Class<?> dataClass = eleList.get(0).getClass();
+			sheet = this.emptySheet(sheetBuildProperties, dataClass, template, responseExcel.headGenerator());
+		}
+
+		if (responseExcel.fill()) {
+			// 填充 sheet
+			excelWriter.fill(eleList, sheet);
+		}
+		else {
+			// 写入 sheet
+			excelWriter.write(eleList, sheet);
+		}
+
+		excelWriter.finish();
+	}
+
+}

+ 15 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/head/EmptyHeadGenerator.java

@@ -0,0 +1,15 @@
+package com.your.packages.hccake.common.excel.head;
+
+/**
+ * 空的 excel 头生成器,用来忽略 excel 头生成
+ *
+ * @author Hccake
+ */
+public class EmptyHeadGenerator implements HeadGenerator {
+
+	@Override
+	public HeadMeta head(Class<?> clazz) {
+		return new HeadMeta();
+	}
+
+}

+ 22 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/head/HeadGenerator.java

@@ -0,0 +1,22 @@
+package com.your.packages.hccake.common.excel.head;
+
+/**
+ * Excel头生成器,用于自定义生成头部信息
+ *
+ * @author Hccake 2020/10/27
+ * @version 1.0
+ */
+public interface HeadGenerator {
+
+	/**
+	 * <p>
+	 * 自定义头部信息
+	 * </p>
+	 * 实现类根据数据的class信息,定制Excel头<br/>
+	 * 具体方法使用参考:https://www.yuque.com/easyexcel/doc/write#b4b9de00
+	 * @param clazz 当前sheet的数据类型
+	 * @return List<List<String>> Head头信息
+	 */
+	HeadMeta head(Class<?> clazz);
+
+}

+ 31 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/head/HeadMeta.java

@@ -0,0 +1,31 @@
+package com.your.packages.hccake.common.excel.head;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * HeadMetaInfo
+ *
+ * @author Yakir 2021/4/26 10:58
+ * @version 1.0
+ */
+@Data
+public class HeadMeta {
+
+	/**
+	 * <p>
+	 * 自定义头部信息
+	 * </p>
+	 * 实现类根据数据的class信息,定制Excel头<br/>
+	 * <a href="https://www.yuque.com/easyexcel/doc/write#b4b9de00">具体方法使用参考</a>
+	 */
+	private List<List<String>> head;
+
+	/**
+	 * 忽略头对应字段名称
+	 */
+	private Set<String> ignoreHeadFields;
+
+}

+ 61 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/head/I18nHeaderCellWriteHandler.java

@@ -0,0 +1,61 @@
+package com.your.packages.hccake.common.excel.head;
+
+import com.alibaba.excel.metadata.Head;
+import com.alibaba.excel.write.handler.CellWriteHandler;
+import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
+import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.poi.ss.usermodel.Row;
+import org.springframework.context.MessageSource;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.util.PropertyPlaceholderHelper;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 对表头进行国际化处理
+ *
+ * @author hccake
+ */
+@RequiredArgsConstructor
+public class I18nHeaderCellWriteHandler implements CellWriteHandler {
+
+	/**
+	 * 国际化消息源
+	 */
+	private final MessageSource messageSource;
+
+	/**
+	 * 国际化翻译
+	 */
+	private final PropertyPlaceholderHelper.PlaceholderResolver placeholderResolver;
+
+	public I18nHeaderCellWriteHandler(MessageSource messageSource) {
+		this.messageSource = messageSource;
+		this.placeholderResolver = placeholderName -> this.messageSource.getMessage(placeholderName, null,
+				LocaleContextHolder.getLocale());
+	}
+
+	/**
+	 * 占位符处理
+	 */
+	private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("{", "}");
+
+	@Override
+	public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
+			Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {
+		if (isHead != null && isHead) {
+			List<String> originHeadNameList = head.getHeadNameList();
+			if (CollectionUtils.isNotEmpty(originHeadNameList)) {
+				// 国际化处理
+				List<String> i18nHeadNames = originHeadNameList.stream()
+					.map(headName -> propertyPlaceholderHelper.replacePlaceholders(headName, placeholderResolver))
+					.collect(Collectors.toList());
+				head.setHeadNameList(i18nHeadNames);
+			}
+		}
+	}
+
+}

+ 15 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/kit/ExcelException.java

@@ -0,0 +1,15 @@
+package com.your.packages.hccake.common.excel.kit;
+
+/**
+ * @author lengleng
+ * @date 2020/3/31
+ */
+public class ExcelException extends RuntimeException {
+
+	private static final long serialVersionUID = 1L;
+
+	public ExcelException(String message) {
+		super(message);
+	}
+
+}

+ 37 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/kit/Validators.java

@@ -0,0 +1,37 @@
+package com.your.packages.hccake.common.excel.kit;
+
+import javax.validation.*;
+import java.util.Set;
+
+/**
+ * 校验工具
+ *
+ * @author L.cm
+ */
+public final class Validators {
+
+	private Validators() {
+	}
+
+	private static final Validator VALIDATOR;
+
+	static {
+		ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+		VALIDATOR = factory.getValidator();
+	}
+
+	/**
+	 * Validates all constraints on {@code object}.
+	 * @param object object to validate
+	 * @param <T> the type of the object to validate
+	 * @return constraint violations or an empty set if none
+	 * @throws IllegalArgumentException if object is {@code null} or if {@code null} is
+	 * passed to the varargs groups
+	 * @throws ValidationException if a non recoverable error happens during the
+	 * validation process
+	 */
+	public static <T> Set<ConstraintViolation<T>> validate(T object) {
+		return VALIDATOR.validate(object);
+	}
+
+}

+ 20 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/processor/NameProcessor.java

@@ -0,0 +1,20 @@
+package com.your.packages.hccake.common.excel.processor;
+
+import java.lang.reflect.Method;
+
+/**
+ * @author lengleng
+ * @date 2020/3/29
+ */
+public interface NameProcessor {
+
+	/**
+	 * 解析名称
+	 * @param args 拦截器对象
+	 * @param method 当前拦截方法
+	 * @param key 表达式
+	 * @return String 根据表达式解析后的字符串
+	 */
+	String doDetermineName(Object[] args, Method method, String key);
+
+}

+ 40 - 0
admin/src/main/java/com/your/packages/hccake/common/excel/processor/NameSpelExpressionProcessor.java

@@ -0,0 +1,40 @@
+package com.your.packages.hccake.common.excel.processor;
+
+import org.springframework.context.expression.MethodBasedEvaluationContext;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+
+import java.lang.reflect.Method;
+
+/**
+ * @author lengleng
+ * @date 2020/3/29
+ */
+public class NameSpelExpressionProcessor implements NameProcessor {
+
+	/**
+	 * 参数发现器
+	 */
+	private static final ParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
+
+	/**
+	 * Express语法解析器
+	 */
+	private static final ExpressionParser PARSER = new SpelExpressionParser();
+
+	@Override
+	public String doDetermineName(Object[] args, Method method, String key) {
+
+		if (!key.contains("#")) {
+			return key;
+		}
+
+		EvaluationContext context = new MethodBasedEvaluationContext(null, method, args, NAME_DISCOVERER);
+		final Object value = PARSER.parseExpression(key).getValue(context);
+		return value == null ? null : value.toString();
+	}
+
+}

+ 23 - 0
admin/src/main/resources/application-dev.yml

@@ -0,0 +1,23 @@
+spring:
+  datasource:
+#    url: jdbc:mysql://ballcat-mysql:3306/ballcat?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
+    url: jdbc:mysql://192.168.1.7:3306/huimv_farm_nongkeyuan?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
+    username: root
+    password: hm123456
+  redis:
+    host: 122.112.224.199
+    port: 6379
+    password: hm123456
+
+ballcat:
+  oss:
+    endpoint: http://oss-cn-shanghai.aliyuncs.com
+    access-key: your key here
+    access-secret: your secret here
+    bucket: your-bucket-here
+
+springdoc:
+  swagger-ui:
+    urls:
+      - { name: 'admin', url: '/v3/api-docs' }
+      - { name: 'api', url: 'http://ballcat-api/v3/api-docs' }

+ 25 - 0
admin/src/main/resources/application-prod.yml

@@ -0,0 +1,25 @@
+spring:
+  datasource:
+    url: jdbc:mysql://ballcat-mysql:3306/ballcat?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
+    username: root
+    password: '123456'
+  redis:
+    host: ballcat-redis
+    password: ''
+    port: 6379
+
+# 日志文件地址,配置此属性以便 SBA 在线查看日志
+logging:
+  file:
+    path: logs/@artifactId@
+    name: ${logging.file.path}/output.log
+
+# 生产环境关闭文档
+ballcat:
+  openapi:
+    enabled: false
+  oss:
+    bucket: your-bucket-here
+    endpoint: http://oss-cn-shanghai.aliyuncs.com
+    access-key: your key here
+    access-secret: your secret here

+ 22 - 0
admin/src/main/resources/application-test.yml

@@ -0,0 +1,22 @@
+spring:
+  datasource:
+    url: jdbc:mysql://ballcat-mysql:3306/ballcat?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
+    username: root
+    password: '123456'
+  redis:
+    host: ballcat-redis
+    password: ''
+    port: 6379
+
+# 日志文件地址,配置此属性以便 SBA 在线查看日志
+logging:
+  file:
+    path: logs/@artifactId@
+    name: ${logging.file.path}/output.log
+
+ballcat:
+  oss:
+    bucket: your-bucket-here
+    endpoint: http://oss-cn-shanghai.aliyuncs.com
+    access-key: your key here
+    access-secret: your secret here

+ 110 - 0
admin/src/main/resources/application.yml

@@ -0,0 +1,110 @@
+server:
+  port: 8080
+
+spring:
+  application:
+    name: @artifactId@
+  profiles:
+    active: @profiles.active@  # 当前激活配置,默认dev
+  messages:
+    # basename 中的 . 和 / 都可以用来表示文件层级,默认的 basename 是 messages
+    # 必须注册此 basename, 否则 security 错误信息将一直都是英文
+    basename: 'ballcat-*, org.springframework.security.messages'
+
+# 天爱图形验证码
+captcha:
+  secondary:
+    enabled: true
+
+# mybatis-plus相关配置
+mybatis-plus:
+  mapper-locations: classpath*:/mapper/**/*Mapper.xml
+  global-config:
+    banner: false
+    db-config:
+      id-type: auto
+      insert-strategy: not_empty
+      update-strategy: not_empty
+      logic-delete-value: "NOW()" # 逻辑已删除值(使用当前时间标识)
+      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
+
+
+# BallCat 相关配置
+ballcat:
+  security:
+    # 前端传输密码的 AES 加密密钥
+    password-secret-key: '==BallCat-Auth=='
+    oauth2:
+      authorizationserver:
+        # 登陆验证码是否开启
+        login-captcha-enabled: true
+        # 内嵌的表单登陆页是否开启
+        login-page-enabled: false
+      resourceserver:
+        ## 忽略鉴权的 url 列表
+        ignore-urls:
+          - /public/**
+          - /actuator/**
+          - /oauth2/**
+          - /doc.html
+          - /v2/api-docs/**
+          - /v3/api-docs/**
+          - /swagger-resources/**
+          - /swagger-ui/**
+          - /webjars/**
+          - /bycdao-ui/**
+          - /favicon.ico
+          - /captcha/**
+  # 项目 redis 缓存的 key 前缀
+  redis:
+    key-prefix: 'ballcat:'
+  # actuator 加解密密钥
+  actuator:
+    auth: true
+    secret-id: 'ballcat-monitor'
+    secret-key: '=BallCat-Monitor'
+  openapi:
+    info:
+      title: BallCat-Admin Docs
+      description: BallCat 后台管理服务Api文档
+      version: ${project.version}
+      terms-of-service: http://www.ballcat.cn/
+      license:
+        name: Powered By BallCat
+        url: http://www.ballcat.cn/
+      contact:
+        name: Hccake
+        email: chengbohua@foxmail.com
+        url: https://github.com/Hccake
+    components:
+      security-schemes:
+        apiKey:
+          type: APIKEY
+          in: HEADER
+          name: 'api-key'
+        oauth2:
+          type: OAUTH2
+          flows:
+            password:
+              token-url: /oauth/token
+    security:
+      - oauth2: [ ]
+      - apiKey: [ ]
+
+springdoc:
+  # 开启 oauth2 端点显示
+  show-oauth2-endpoints: true
+  swagger-ui:
+    oauth:
+      client-id: test
+      client-secret: test
+    display-request-duration: true
+    disable-swagger-default-url: true
+    persist-authorization: true
+
+
+
+
+
+
+

BIN
admin/src/main/resources/bgimages/48.jpg


+ 0 - 0
admin/src/main/resources/bgimages/a.jpg


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.