husj 1 nedēļu atpakaļ
revīzija
a82aeb942d
100 mainītis faili ar 4447 papildinājumiem un 0 dzēšanām
  1. 17 0
      .gitignore
  2. 11 0
      Dockerfile
  3. 193 0
      pom.xml
  4. 74 0
      startup.sh
  5. 51 0
      templates/controller.java.ftl
  6. 151 0
      templates/entity.java.ftl
  7. 114 0
      templates/entity.kt.ftl
  8. 19 0
      templates/mapper.java.ftl
  9. 39 0
      templates/mapper.xml.ftl
  10. 19 0
      templates/service.java.ftl
  11. 25 0
      templates/serviceImpl.java.ftl
  12. 48 0
      tj-api/pom.xml
  13. 4 0
      tj-api/src/main/java/com/tianji/api/annotations/EnableCategoryCache.java
  14. 93 0
      tj-api/src/main/java/com/tianji/api/cache/CategoryCache.java
  15. 36 0
      tj-api/src/main/java/com/tianji/api/cache/RoleCache.java
  16. 18 0
      tj-api/src/main/java/com/tianji/api/client/auth/AuthClient.java
  17. 23 0
      tj-api/src/main/java/com/tianji/api/client/course/CatalogueClient.java
  18. 18 0
      tj-api/src/main/java/com/tianji/api/client/course/CategoryClient.java
  19. 68 0
      tj-api/src/main/java/com/tianji/api/client/course/CourseClient.java
  20. 15 0
      tj-api/src/main/java/com/tianji/api/client/course/SubjectClient.java
  21. 36 0
      tj-api/src/main/java/com/tianji/api/client/exam/ExamClient.java
  22. 36 0
      tj-api/src/main/java/com/tianji/api/client/learning/LearningClient.java
  23. 31 0
      tj-api/src/main/java/com/tianji/api/client/learning/fallback/LearningClientFallback.java
  24. 15 0
      tj-api/src/main/java/com/tianji/api/client/search/SearchClient.java
  25. 46 0
      tj-api/src/main/java/com/tianji/api/client/trade/TradeClient.java
  26. 41 0
      tj-api/src/main/java/com/tianji/api/client/trade/fallback/TradeClientFallback.java
  27. 57 0
      tj-api/src/main/java/com/tianji/api/client/user/UserClient.java
  28. 45 0
      tj-api/src/main/java/com/tianji/api/client/user/fallback/UserClientFallback.java
  29. 33 0
      tj-api/src/main/java/com/tianji/api/config/CategoryCacheConfig.java
  30. 26 0
      tj-api/src/main/java/com/tianji/api/config/FallbackConfig.java
  31. 22 0
      tj-api/src/main/java/com/tianji/api/config/RequestIdRelayConfiguration.java
  32. 31 0
      tj-api/src/main/java/com/tianji/api/config/RoleCacheConfig.java
  33. 34 0
      tj-api/src/main/java/com/tianji/api/constants/CourseStatus.java
  34. 5 0
      tj-api/src/main/java/com/tianji/api/constants/SmsConstants.java
  35. 27 0
      tj-api/src/main/java/com/tianji/api/dto/IdAndNumDTO.java
  36. 44 0
      tj-api/src/main/java/com/tianji/api/dto/auth/RoleDTO.java
  37. 19 0
      tj-api/src/main/java/com/tianji/api/dto/course/CataSimpleInfoDTO.java
  38. 41 0
      tj-api/src/main/java/com/tianji/api/dto/course/CatalogueDTO.java
  39. 16 0
      tj-api/src/main/java/com/tianji/api/dto/course/CategoryBasicDTO.java
  40. 40 0
      tj-api/src/main/java/com/tianji/api/dto/course/CategoryDTO.java
  41. 56 0
      tj-api/src/main/java/com/tianji/api/dto/course/CourseDTO.java
  42. 49 0
      tj-api/src/main/java/com/tianji/api/dto/course/CourseFullInfoDTO.java
  43. 26 0
      tj-api/src/main/java/com/tianji/api/dto/course/CoursePurchaseInfoDTO.java
  44. 49 0
      tj-api/src/main/java/com/tianji/api/dto/course/CourseSearchDTO.java
  45. 45 0
      tj-api/src/main/java/com/tianji/api/dto/course/CourseSimpleInfoDTO.java
  46. 24 0
      tj-api/src/main/java/com/tianji/api/dto/course/MediaQuoteDTO.java
  47. 22 0
      tj-api/src/main/java/com/tianji/api/dto/course/SectionInfoDTO.java
  48. 28 0
      tj-api/src/main/java/com/tianji/api/dto/course/SubNumAndCourseNumDTO.java
  49. 47 0
      tj-api/src/main/java/com/tianji/api/dto/course/SubjectDTO.java
  50. 23 0
      tj-api/src/main/java/com/tianji/api/dto/exam/QuestionBizDTO.java
  51. 40 0
      tj-api/src/main/java/com/tianji/api/dto/exam/QuestionDTO.java
  52. 18 0
      tj-api/src/main/java/com/tianji/api/dto/leanring/LearningLessonDTO.java
  53. 16 0
      tj-api/src/main/java/com/tianji/api/dto/leanring/LearningRecordDTO.java
  54. 30 0
      tj-api/src/main/java/com/tianji/api/dto/leanring/LearningRecordFormDTO.java
  55. 14 0
      tj-api/src/main/java/com/tianji/api/dto/sms/SmsInfoDTO.java
  56. 28 0
      tj-api/src/main/java/com/tianji/api/dto/trade/OrderBasicDTO.java
  57. 24 0
      tj-api/src/main/java/com/tianji/api/dto/user/LoginFormDTO.java
  58. 53 0
      tj-api/src/main/java/com/tianji/api/dto/user/UserDTO.java
  59. 5 0
      tj-api/src/main/resources/META-INF/spring.factories
  60. 26 0
      tj-auth/pom.xml
  61. 26 0
      tj-auth/tj-auth-common/pom.xml
  62. 27 0
      tj-auth/tj-auth-common/src/main/java/com/tianji/auth/common/constants/AuthErrorInfo.java
  63. 30 0
      tj-auth/tj-auth-common/src/main/java/com/tianji/auth/common/constants/JwtConstants.java
  64. 13 0
      tj-auth/tj-auth-common/src/main/java/com/tianji/auth/common/domain/PrivilegeRoleDTO.java
  65. 34 0
      tj-auth/tj-auth-gateway-sdk/pom.xml
  66. 24 0
      tj-auth/tj-auth-gateway-sdk/src/main/java/com/tianji/authsdk/gateway/config/AuthAutoConfiguration.java
  67. 176 0
      tj-auth/tj-auth-gateway-sdk/src/main/java/com/tianji/authsdk/gateway/util/AuthUtil.java
  68. 110 0
      tj-auth/tj-auth-gateway-sdk/src/main/java/com/tianji/authsdk/gateway/util/JwtSignerHolder.java
  69. 19 0
      tj-auth/tj-auth-gateway-sdk/src/main/resources/META-INF/spring-configuration-metadata.json
  70. 2 0
      tj-auth/tj-auth-gateway-sdk/src/main/resources/META-INF/spring.factories
  71. 41 0
      tj-auth/tj-auth-resource-sdk/pom.xml
  72. 17 0
      tj-auth/tj-auth-resource-sdk/src/main/java/com/tianji/authsdk/resource/config/FeignRelayUserAutoConfiguration.java
  73. 14 0
      tj-auth/tj-auth-resource-sdk/src/main/java/com/tianji/authsdk/resource/config/ResourceAuthProperties.java
  74. 52 0
      tj-auth/tj-auth-resource-sdk/src/main/java/com/tianji/authsdk/resource/config/ResourceInterceptorConfiguration.java
  75. 17 0
      tj-auth/tj-auth-resource-sdk/src/main/java/com/tianji/authsdk/resource/interceptors/FeignRelayUserInterceptor.java
  76. 27 0
      tj-auth/tj-auth-resource-sdk/src/main/java/com/tianji/authsdk/resource/interceptors/LoginAuthInterceptor.java
  77. 38 0
      tj-auth/tj-auth-resource-sdk/src/main/java/com/tianji/authsdk/resource/interceptors/UserInfoInterceptor.java
  78. 31 0
      tj-auth/tj-auth-resource-sdk/src/main/resources/META-INF/spring-configuration-metadata.json
  79. 3 0
      tj-auth/tj-auth-resource-sdk/src/main/resources/META-INF/spring.factories
  80. 92 0
      tj-auth/tj-auth-service/pom.xml
  81. 41 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/AuthApplication.java
  82. 41 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/config/AuthConfig.java
  83. 6 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/constants/AuthConstants.java
  84. 62 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/controller/AccountController.java
  85. 30 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/controller/JwkController.java
  86. 152 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/controller/MenuController.java
  87. 192 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/controller/PrivilegeController.java
  88. 99 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/controller/RoleController.java
  89. 27 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/dto/MenuDTO.java
  90. 32 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/dto/PrivilegeDTO.java
  91. 41 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/po/AccountRole.java
  92. 72 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/po/LoginRecord.java
  93. 119 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/po/Menu.java
  94. 116 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/po/Privilege.java
  95. 111 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/po/Role.java
  96. 47 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/po/RoleMenu.java
  97. 48 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/po/RolePrivilege.java
  98. 39 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/vo/LoginRecordVO.java
  99. 45 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/vo/MenuOptionVO.java
  100. 30 0
      tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/vo/PrivilegeOptionVO.java

+ 17 - 0
.gitignore

@@ -0,0 +1,17 @@
+# Created by .ignore support plugin (hsz.mobi)
+### Example user template template
+### Example user template
+
+# IntelliJ project files
+.idea
+*.iml
+out
+gen
+target
+*.class
+
+*.log
+.settings
+.project
+logs
+job

+ 11 - 0
Dockerfile

@@ -0,0 +1,11 @@
+FROM openjdk:11.0-jre-buster
+LABEL maintainer="研究院研发组 <research-maint@itcast.cn>"
+ENV JAVA_OPTS=""
+# 设定时区
+ENV TZ=Asia/Shanghai
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+
+WORKDIR /app
+ADD app.jar /app/app.jar
+
+ENTRYPOINT ["sh","-c","java  -jar $JAVA_OPTS /app/app.jar"]

+ 193 - 0
pom.xml

@@ -0,0 +1,193 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.tianji</groupId>
+    <artifactId>tjxt</artifactId>
+    <version>1.0.0</version>
+    <modules>
+        <module>tj-common</module>
+        <module>tj-auth</module>
+        <module>tj-api</module>
+        <module>tj-gateway</module>
+        <module>tj-user</module>
+        <module>tj-message</module>
+        <module>tj-media</module>
+        <module>tj-course</module>
+        <module>tj-search</module>
+        <module>tj-learning</module>
+        <module>tj-pay</module>
+        <module>tj-trade</module>
+        <module>tj-exam</module>
+        <module>tj-data</module>
+    </modules>
+    <packaging>pom</packaging>
+
+    <!-- 继承 Spring Boot 父工程 -->
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.7.2</version>
+    </parent>
+
+
+    <properties>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <tjxt.version>1.0</tjxt.version>
+        <org.projectlombok.version>1.18.20</org.projectlombok.version>
+        <spring-cloud.version>2021.0.3</spring-cloud.version>
+        <spring-cloud-alibaba.version>2021.0.1.0</spring-cloud-alibaba.version>
+        <mybatis-plus.version>3.4.3</mybatis-plus.version>
+        <hutool.version>5.7.17</hutool.version>
+        <swagger.version>3.0.3</swagger.version>
+        <mysql.version>8.0.23</mysql.version>
+        <ali.sdk.core.version>4.6.0</ali.sdk.core.version>
+        <ali.sdk.kms.version>2.10.1</ali.sdk.kms.version>
+        <ali.sdk.oss.version>3.10.2</ali.sdk.oss.version>
+        <ali.sdk.pay.version>4.33.12.ALL</ali.sdk.pay.version>
+        <tencent.cloud.version>3.1.515</tencent.cloud.version>
+        <redisson.version>3.13.6</redisson.version>
+        <elasticsearch.version>7.12.1</elasticsearch.version>
+        <tencent.sdk.cos.version>5.6.89</tencent.sdk.cos.version>
+        <tencent.sdk.vod.version>2.1.5</tencent.sdk.vod.version>
+        <xxl-job-version>2.3.1</xxl-job-version>
+        <seata-version>1.5.1</seata-version>
+    </properties>
+    <!-- 对依赖包进行管理 -->
+    <dependencyManagement>
+        <dependencies>
+            <!-- Spring Cloud 依赖包管理 -->
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>${spring-cloud.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
+                <version>${spring-cloud-alibaba.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <!-- 数据库驱动包管理 -->
+            <dependency>
+                <groupId>mysql</groupId>
+                <artifactId>mysql-connector-java</artifactId>
+                <version>${mysql.version}</version>
+            </dependency>
+
+            <!-- mybatis plus 管理 -->
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-boot-starter</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+            <!--knife4j-->
+            <dependency>
+                <groupId>com.github.xiaoymin</groupId>
+                <artifactId>knife4j-spring-boot-starter</artifactId>
+                <version>${swagger.version}</version>
+            </dependency>
+            <!--腾讯云-->
+            <dependency>
+                <groupId>com.tencentcloudapi</groupId>
+                <artifactId>tencentcloud-sdk-java</artifactId>
+                <version>${tencent.cloud.version}</version>
+            </dependency>
+            <!--ali sdk-->
+            <dependency>
+                <groupId>com.aliyun</groupId>
+                <artifactId>aliyun-java-sdk-core</artifactId>
+                <version>${ali.sdk.core.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.aliyun</groupId>
+                <artifactId>aliyun-java-sdk-kms</artifactId>
+                <version>${ali.sdk.kms.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.alipay.sdk</groupId>
+                <artifactId>alipay-sdk-java</artifactId>
+                <version>${ali.sdk.pay.version}</version>
+            </dependency>
+            <!--阿里云OSS的SDK-->
+            <dependency>
+                <groupId>com.aliyun.oss</groupId>
+                <artifactId>aliyun-sdk-oss</artifactId>
+                <version>${ali.sdk.oss.version}</version>
+            </dependency>
+            <!--redisson-->
+            <dependency>
+                <groupId>org.redisson</groupId>
+                <artifactId>redisson</artifactId>
+                <version>${redisson.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.aspectj</groupId>
+                <artifactId>aspectjweaver</artifactId>
+                <version>${aspectj.version}</version>
+            </dependency>
+            <!--腾讯cos-->
+            <dependency>
+                <groupId>com.qcloud</groupId>
+                <artifactId>cos_api</artifactId>
+                <version>${tencent.sdk.cos.version}</version>
+            </dependency>
+            <!--腾讯vod-->
+            <dependency>
+                <groupId>com.qcloud</groupId>
+                <artifactId>vod_api</artifactId>
+                <version>${tencent.sdk.vod.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.xuxueli</groupId>
+                <artifactId>xxl-job-core</artifactId>
+                <version>${xxl-job-version}</version>
+            </dependency>
+
+        </dependencies>
+    </dependencyManagement>
+    <dependencies>
+        <!-- lombok 管理 -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>${org.projectlombok.version}</version>
+        </dependency>
+        <!--单元测试-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <!--开启bootstrap文件读取-->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-bootstrap</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <version>3.8.1</version>
+                    <configuration>
+                        <source>11</source> <!-- depending on your project -->
+                        <target>11</target> <!-- depending on your project -->
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+    </build>
+</project>

+ 74 - 0
startup.sh

@@ -0,0 +1,74 @@
+#! /bin/sh
+cd /usr/local/src/script || exit 1
+BASE_PATH='/usr/local/src/jenkins/workspace/tjxt-dev-build'
+PROJECT_NAME=""
+PROJECT_PATH=''
+CONTAINER_NAME=""
+JAVA_OPTS="-Xms300m -Xmx300m"
+PORT=8080
+DEBUG_PORT=0
+while getopts "c:n:d:p:o:a:" opt; do
+    case $opt in
+         c)
+            CONTAINER_NAME=$OPTARG
+          ;;
+         n)
+            PROJECT_NAME=$OPTARG
+          ;;
+         d)
+            PROJECT_PATH=$OPTARG
+          ;;
+         p)
+            PORT=$OPTARG
+          ;;
+         o)
+            [ -n "$OPTARG" ] && JAVA_OPTS=$OPTARG
+          ;;
+         a)
+            [ -n "$OPTARG" ] && DEBUG_PORT=$OPTARG
+          ;;
+         ?)
+            echo "unkonw argument"
+            exit 1
+          ;;
+    esac
+done
+if [ "$DEBUG_PORT" = "0" ]; then
+  JAVA_OPTS=$JAVA_OPTS
+else
+  JAVA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
+fi
+IMAGE_NAME="${CONTAINER_NAME}:latest"
+echo "copy xx.jar from ${BASE_PATH}/${PROJECT_PATH}"
+rm -f app.jar
+cp ${BASE_PATH}/${PROJECT_PATH}/target/${PROJECT_NAME}.jar ./app.jar ||  exit 1
+
+echo "begin to build ${PROJECT_NAME} image !!"
+
+[ -n "`docker ps -a | grep ${CONTAINER_NAME}`" ] && docker rm -f ${CONTAINER_NAME}
+[ -n "`docker images | grep ${CONTAINER_NAME}`" ] && docker rmi ${IMAGE_NAME}
+
+docker build -t ${IMAGE_NAME} . || exit 1
+echo "${PROJECT_NAME} image build success,java_opts = $JAVA_OPTS !!^_^"
+
+echo "begin to create container ${CONTAINER_NAME},port: ${PORT} !!"
+
+if [ "$DEBUG_PORT" = "0" ]; then
+  echo "run in normal mode"
+  docker run -d --name ${CONTAINER_NAME} \
+   -p "${PORT}:${PORT}" \
+   -e JAVA_OPTS="${JAVA_OPTS}" \
+   --memory 300m --memory-swap -1 \
+   --network heima-net ${IMAGE_NAME} \
+  || exit 1
+else
+  echo "run in debug mode"
+  docker run -d --name ${CONTAINER_NAME} \
+   -p "${PORT}:${PORT}" \
+   -p ${DEBUG_PORT}:5005 \
+   -e JAVA_OPTS="${JAVA_OPTS}" \
+   --network heima-net ${IMAGE_NAME} \
+  || exit 1
+fi
+echo "container is running now !! ^_^"
+exit 0

+ 51 - 0
templates/controller.java.ftl

@@ -0,0 +1,51 @@
+package ${package.Controller};
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import ${package.Service}.${table.serviceName};
+import ${package.Entity}.${table.entityName};
+<#if swagger2>
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.Api;
+</#if>
+import lombok.RequiredArgsConstructor;
+<#if restControllerStyle>
+import org.springframework.web.bind.annotation.RestController;
+<#else>
+import org.springframework.stereotype.Controller;
+</#if>
+<#if superControllerClassPackage??>
+import ${superControllerClassPackage};
+</#if>
+
+/**
+ * <p>
+ * ${table.comment!} 控制器
+ * </p>
+ *
+ * @author ${author}
+ */
+<#if swagger2>
+@Api(tags = "${entity}管理")
+</#if>
+<#if restControllerStyle>
+@RestController
+<#else>
+@Controller
+</#if>
+@RequiredArgsConstructor
+@RequestMapping("<#if package.ModuleName??>/${package.ModuleName}</#if>/<#if camelTableNameStyle??>${camelTableName}<#else>${table.entityPath}</#if>")
+<#if kotlin>
+class ${table.controllerName}<#if superControllerClass??> : ${superControllerClass}()</#if>
+<#else>
+  <#if superControllerClass??>
+public class ${table.controllerName} extends ${superControllerClass} {
+  <#else>
+public class ${table.controllerName} {
+  </#if>
+
+    private final ${table.serviceName} ${cfg.camelTableName}Service;
+
+
+}
+</#if>

+ 151 - 0
templates/entity.java.ftl

@@ -0,0 +1,151 @@
+package ${package.Entity};
+
+<#list table.importPackages as pkg>
+import ${pkg};
+</#list>
+<#if swagger2>
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+</#if>
+<#if entityLombokModel>
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+</#if>
+
+/**
+ * <p>
+ * ${table.comment!}
+ * </p>
+ *
+ * @author ${author}
+ */
+<#if entityLombokModel>
+@Data
+    <#if superEntityClass??>
+@EqualsAndHashCode(callSuper = true)
+    <#else>
+@EqualsAndHashCode(callSuper = false)
+    </#if>
+@Accessors(chain = true)
+</#if>
+<#if table.convert>
+@TableName("${table.name}")
+</#if>
+<#if swagger2>
+@ApiModel(value="${entity}对象", description="${table.comment!}")
+</#if>
+<#if superEntityClass??>
+public class ${entity} extends ${superEntityClass}<#if activeRecord><${entity}></#if> {
+<#elseif activeRecord>
+public class ${entity} extends Model<${entity}> {
+<#else>
+public class ${entity} implements Serializable {
+</#if>
+
+<#if entitySerialVersionUID>
+    private static final long serialVersionUID = 1L;
+</#if>
+<#-- ----------  BEGIN 字段循环遍历  ---------->
+<#list table.fields as field>
+    <#if field.keyFlag>
+        <#assign keyPropertyName="${field.propertyName}"/>
+    </#if>
+
+    <#if field.comment!?length gt 0>
+        <#if swagger2>
+    @ApiModelProperty(value = "${field.comment}")
+        <#else>
+    /**
+     * ${field.comment}
+     */
+        </#if>
+    </#if>
+    <#if field.keyFlag>
+        <#-- 主键 -->
+        <#if field.keyIdentityFlag>
+    @TableId(value = "${field.name}", type = IdType.AUTO)
+        <#elseif idType??>
+    @TableId(value = "${field.name}", type = IdType.${idType})
+        <#elseif field.convert>
+    @TableId("${field.name}")
+        </#if>
+        <#-- 普通字段 -->
+    <#elseif field.fill??>
+    <#-- -----   存在字段填充设置   ----->
+        <#if field.convert>
+    @TableField(value = "${field.name}", fill = FieldFill.${field.fill})
+        <#else>
+    @TableField(fill = FieldFill.${field.fill})
+        </#if>
+    <#elseif field.convert>
+    @TableField("${field.name}")
+    </#if>
+    <#-- 乐观锁注解 -->
+    <#if (versionFieldName!"") == field.name>
+    @Version
+    </#if>
+    <#-- 逻辑删除注解 -->
+    <#if (logicDeleteFieldName!"") == field.name>
+    @TableLogic
+    </#if>
+    private ${field.propertyType} ${field.propertyName};
+</#list>
+<#------------  END 字段循环遍历  ---------->
+
+<#if !entityLombokModel>
+    <#list table.fields as field>
+        <#if field.propertyType == "boolean">
+            <#assign getprefix="is"/>
+        <#else>
+            <#assign getprefix="get"/>
+        </#if>
+    public ${field.propertyType} ${getprefix}${field.capitalName}() {
+        return ${field.propertyName};
+    }
+
+    <#if entityBuilderModel>
+    public ${entity} set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
+    <#else>
+    public void set${field.capitalName}(${field.propertyType} ${field.propertyName}) {
+    </#if>
+        this.${field.propertyName} = ${field.propertyName};
+        <#if entityBuilderModel>
+        return this;
+        </#if>
+    }
+    </#list>
+</#if>
+
+<#if entityColumnConstant>
+    <#list table.fields as field>
+    public static final String ${field.name?upper_case} = "${field.name}";
+
+    </#list>
+</#if>
+<#if activeRecord>
+    @Override
+    protected Serializable pkVal() {
+    <#if keyPropertyName??>
+        return this.${keyPropertyName};
+    <#else>
+        return null;
+    </#if>
+    }
+
+</#if>
+<#if !entityLombokModel>
+    @Override
+    public String toString() {
+        return "${entity}{" +
+    <#list table.fields as field>
+        <#if field_index==0>
+            "${field.propertyName}=" + ${field.propertyName} +
+        <#else>
+            ", ${field.propertyName}=" + ${field.propertyName} +
+        </#if>
+    </#list>
+        "}";
+    }
+</#if>
+}

+ 114 - 0
templates/entity.kt.ftl

@@ -0,0 +1,114 @@
+package ${package.Entity}
+
+<#list table.importPackages as pkg>
+import ${pkg}
+</#list>
+<#if swagger2>
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+</#if>
+/**
+ * <p>
+ * ${table.comment}
+ * </p>
+ *
+ * @author ${author}
+ */
+<#if table.convert>
+@TableName("${table.name}")
+</#if>
+<#if swagger2>
+    @ApiModel(value="${entity}对象", description="${table.comment!}")
+</#if>
+<#if superEntityClass??>
+class ${entity} : ${superEntityClass}<#if activeRecord><${entity}></#if> {
+<#elseif activeRecord>
+class ${entity} : Model<${entity}>() {
+<#else>
+class ${entity} : Serializable {
+</#if>
+
+<#-- ----------  BEGIN 字段循环遍历  ---------->
+<#list table.fields as field>
+<#if field.keyFlag>
+    <#assign keyPropertyName="${field.propertyName}"/>
+</#if>
+
+<#if field.comment!?length gt 0>
+<#if swagger2>
+        @ApiModelProperty(value = "${field.comment}")
+<#else>
+    /**
+     * ${field.comment}
+     */
+</#if>
+</#if>
+<#if field.keyFlag>
+<#-- 主键 -->
+<#if field.keyIdentityFlag>
+    @TableId(value = "${field.name}", type = IdType.AUTO)
+<#elseif idType ??>
+    @TableId(value = "${field.name}", type = IdType.${idType})
+<#elseif field.convert>
+    @TableId("${field.name}")
+</#if>
+<#-- 普通字段 -->
+<#elseif field.fill??>
+<#-- -----   存在字段填充设置   ----->
+<#if field.convert>
+    @TableField(value = "${field.name}", fill = FieldFill.${field.fill})
+<#else>
+    @TableField(fill = FieldFill.${field.fill})
+</#if>
+<#elseif field.convert>
+    @TableField("${field.name}")
+</#if>
+<#-- 乐观锁注解 -->
+<#if (versionFieldName!"") == field.name>
+    @Version
+</#if>
+<#-- 逻辑删除注解 -->
+<#if (logicDeleteFieldName!"") == field.name>
+    @TableLogic
+</#if>
+    <#if field.propertyType == "Integer">
+    var ${field.propertyName}: Int? = null
+    <#else>
+    var ${field.propertyName}: ${field.propertyType}? = null
+    </#if>
+</#list>
+<#-- ----------  END 字段循环遍历  ---------->
+
+
+<#if entityColumnConstant>
+    companion object {
+<#list table.fields as field>
+
+        const val ${field.name.toUpperCase()} : String = "${field.name}"
+
+</#list>
+    }
+
+</#if>
+<#if activeRecord>
+    override fun pkVal(): Serializable? {
+<#if keyPropertyName??>
+        return ${keyPropertyName}
+<#else>
+        return null
+</#if>
+    }
+
+</#if>
+    override fun toString(): String {
+        return "${entity}{" +
+<#list table.fields as field>
+<#if field_index==0>
+        "${field.propertyName}=" + ${field.propertyName} +
+<#else>
+        ", ${field.propertyName}=" + ${field.propertyName} +
+</#if>
+</#list>
+        "}"
+    }
+}

+ 19 - 0
templates/mapper.java.ftl

@@ -0,0 +1,19 @@
+package ${package.Mapper};
+
+import ${package.Entity}.${entity};
+import ${superMapperClassPackage};
+
+/**
+ * <p>
+ * ${table.comment!} Mapper 接口
+ * </p>
+ *
+ * @author ${author}
+ */
+<#if kotlin>
+interface ${table.mapperName} : ${superMapperClass}<${entity}>
+<#else>
+public interface ${table.mapperName} extends ${superMapperClass}<${entity}> {
+
+}
+</#if>

+ 39 - 0
templates/mapper.xml.ftl

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="${package.Mapper}.${table.mapperName}">
+
+<#if enableCache>
+    <!-- 开启二级缓存 -->
+    <cache type="org.mybatis.caches.ehcache.LoggingEhcache"/>
+
+</#if>
+<#if baseResultMap>
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="${package.Entity}.${entity}">
+<#list table.fields as field>
+<#if field.keyFlag><#--生成主键排在第一位-->
+        <id column="${field.name}" property="${field.propertyName}" />
+</#if>
+</#list>
+<#list table.commonFields as field><#--生成公共字段 -->
+    <result column="${field.name}" property="${field.propertyName}" />
+</#list>
+<#list table.fields as field>
+<#if !field.keyFlag><#--生成普通字段 -->
+        <result column="${field.name}" property="${field.propertyName}" />
+</#if>
+</#list>
+    </resultMap>
+
+</#if>
+<#if baseColumnList>
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+<#list table.commonFields as field>
+        ${field.name},
+</#list>
+        ${table.fieldNames}
+    </sql>
+
+</#if>
+</mapper>

+ 19 - 0
templates/service.java.ftl

@@ -0,0 +1,19 @@
+package ${package.Service};
+
+import ${package.Entity}.${entity};
+import ${superServiceClassPackage};
+
+/**
+ * <p>
+ * ${table.comment!} 服务类
+ * </p>
+ *
+ * @author ${author}
+ */
+<#if kotlin>
+interface ${table.serviceName} : ${superServiceClass}<${entity}>
+<#else>
+public interface ${table.serviceName} extends ${superServiceClass}<${entity}> {
+
+}
+</#if>

+ 25 - 0
templates/serviceImpl.java.ftl

@@ -0,0 +1,25 @@
+package ${package.ServiceImpl};
+
+import ${package.Entity}.${entity};
+import ${package.Mapper}.${table.mapperName};
+import ${package.Service}.${table.serviceName};
+import ${superServiceImplClassPackage};
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * ${table.comment!} 服务实现类
+ * </p>
+ *
+ * @author ${author}
+ */
+@Service
+<#if kotlin>
+open class ${table.serviceImplName} : ${superServiceImplClass}<${table.mapperName}, ${entity}>(), ${table.serviceName} {
+
+}
+<#else>
+public class ${table.serviceImplName} extends ${superServiceImplClass}<${table.mapperName}, ${entity}> implements ${table.serviceName} {
+
+}
+</#if>

+ 48 - 0
tj-api/pom.xml

@@ -0,0 +1,48 @@
+<?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>tjxt</artifactId>
+        <groupId>com.tianji</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tj-api</artifactId>
+
+    <properties>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+        </dependency>
+        <!--httpClient的依赖 -->
+        <dependency>
+            <groupId>io.github.openfeign</groupId>
+            <artifactId>feign-httpclient</artifactId>
+        </dependency>
+        <!--common-->
+        <dependency>
+            <groupId>com.tianji</groupId>
+            <artifactId>tj-common</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+		<dependency>
+            <groupId>org.hibernate.validator</groupId>
+            <artifactId>hibernate-validator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+        </dependency>
+        <!--sentinel-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 4 - 0
tj-api/src/main/java/com/tianji/api/annotations/EnableCategoryCache.java

@@ -0,0 +1,4 @@
+package com.tianji.api.annotations;
+
+public @interface EnableCategoryCache {
+}

+ 93 - 0
tj-api/src/main/java/com/tianji/api/cache/CategoryCache.java

@@ -0,0 +1,93 @@
+package com.tianji.api.cache;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.tianji.api.client.course.CategoryClient;
+import com.tianji.api.dto.course.CategoryBasicDTO;
+import com.tianji.common.utils.CollUtils;
+import lombok.RequiredArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@RequiredArgsConstructor
+public class CategoryCache {
+
+    private final Cache<String, Map<Long, CategoryBasicDTO>> categoryCaches;
+
+    private final CategoryClient categoryClient;
+
+    public Map<Long, CategoryBasicDTO> getCategoryMap() {
+        return categoryCaches.get("CATEGORY", key -> {
+            // 1.从CategoryClient查询
+            List<CategoryBasicDTO> list = categoryClient.getAllOfOneLevel();
+            if (list == null || list.isEmpty()) {
+                return CollUtils.emptyMap();
+            }
+            // 2.转换数据
+            return list.stream().collect(Collectors.toMap(CategoryBasicDTO::getId, Function.identity()));
+        });
+    }
+
+    public String getCategoryNames(List<Long> ids) {
+        if (ids == null || ids.size() == 0) {
+            return "";
+        }
+        // 1.读取分类缓存
+        Map<Long, CategoryBasicDTO> map = getCategoryMap();
+        // 2.根据id查询分类名称并组装
+        StringBuilder sb = new StringBuilder();
+        for (Long id : ids) {
+            sb.append(map.get(id).getName()).append("/");
+        }
+        // 3.返回结果
+        return sb.deleteCharAt(sb.length() - 1).toString();
+    }
+
+    public List<String> getCategoryNameList(List<Long> ids) {
+        if (ids == null || ids.size() == 0) {
+            return CollUtils.emptyList();
+        }
+        // 1.读取分类缓存
+        Map<Long, CategoryBasicDTO> map = getCategoryMap();
+        // 2.根据id查询分类名称并组装
+        List<String> list = new ArrayList<>(ids.size());
+        for (Long id : ids) {
+            list.add(map.get(id).getName());
+        }
+        // 3.返回结果
+        return list;
+    }
+
+    public List<CategoryBasicDTO> queryCategoryByIds(List<Long> ids) {
+        if (ids == null || ids.size() == 0) {
+            return CollUtils.emptyList();
+        }
+        Map<Long, CategoryBasicDTO> map = getCategoryMap();
+        return ids.stream()
+                .map(map::get)
+                .collect(Collectors.toList());
+    }
+
+    public List<String> getNameByLv3Ids(List<Long> lv3Ids) {
+        Map<Long, CategoryBasicDTO> map = getCategoryMap();
+        List<String> list = new ArrayList<>(lv3Ids.size());
+        for (Long lv3Id : lv3Ids) {
+            CategoryBasicDTO lv3 = map.get(lv3Id);
+            CategoryBasicDTO lv2 = map.get(lv3.getParentId());
+            CategoryBasicDTO lv1 = map.get(lv2.getParentId());
+            list.add(lv1.getName() + "/" + lv2.getName() + "/" + lv3.getName());
+        }
+        return list;
+    }
+
+    public String getNameByLv3Id(Long lv3Id) {
+        Map<Long, CategoryBasicDTO> map = getCategoryMap();
+        CategoryBasicDTO lv3 = map.get(lv3Id);
+        CategoryBasicDTO lv2 = map.get(lv3.getParentId());
+        CategoryBasicDTO lv1 = map.get(lv2.getParentId());
+        return lv1.getName() + "/" + lv2.getName() + "/" + lv3.getName();
+    }
+}

+ 36 - 0
tj-api/src/main/java/com/tianji/api/cache/RoleCache.java

@@ -0,0 +1,36 @@
+package com.tianji.api.cache;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.tianji.api.client.auth.AuthClient;
+import com.tianji.api.dto.auth.RoleDTO;
+import com.tianji.api.dto.user.UserDTO;
+import com.tianji.common.enums.UserType;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public class RoleCache {
+
+    private final Cache<Long, RoleDTO> roleCaches;
+    private final AuthClient authClient;
+
+    public String getRoleName(Long roleId) {
+        RoleDTO roleDTO = roleCaches.get(roleId, authClient::queryRoleById);
+        if (roleDTO == null) {
+            return null;
+        }
+        return roleDTO.getName();
+    }
+
+    public String exchangeRoleName(UserDTO u) {
+        if (u == null) {
+            return "--";
+        }
+        if (UserType.STUDENT.equalsValue(u.getType())) {
+            // 学生,直接返回角色名称
+            return u.getName();
+        } else {
+            // 管理员需要拼接角色名称
+            return getRoleName(u.getRoleId()) + "-" + u.getName();
+        }
+    }
+}

+ 18 - 0
tj-api/src/main/java/com/tianji/api/client/auth/AuthClient.java

@@ -0,0 +1,18 @@
+package com.tianji.api.client.auth;
+
+import com.tianji.api.dto.auth.RoleDTO;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+import java.util.List;
+
+@FeignClient("auth-service")
+public interface AuthClient {
+
+    @GetMapping("/roles/{id}")
+    RoleDTO queryRoleById(@PathVariable("id") Long id);
+
+    @GetMapping("/roles/list")
+    List<RoleDTO> listAllRoles();
+}

+ 23 - 0
tj-api/src/main/java/com/tianji/api/client/course/CatalogueClient.java

@@ -0,0 +1,23 @@
+package com.tianji.api.client.course;
+
+import com.tianji.api.dto.course.CataSimpleInfoDTO;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
+
+@FeignClient(contextId = "catalogue", value = "course-service",path = "catalogues")
+public interface CatalogueClient {
+
+    /**
+     * 根据目录id列表查询目录信息
+     *
+     * @param ids 目录id列表
+     * @return id列表中对应的目录基础信息
+     */
+    @GetMapping("/batchQuery")
+    List<CataSimpleInfoDTO> batchQueryCatalogue(@RequestParam("ids") Iterable<Long> ids);
+
+
+}

+ 18 - 0
tj-api/src/main/java/com/tianji/api/client/course/CategoryClient.java

@@ -0,0 +1,18 @@
+package com.tianji.api.client.course;
+
+import com.tianji.api.dto.course.CategoryBasicDTO;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+
+import java.util.List;
+
+@FeignClient(contextId = "category",value = "course-service",path = "categorys")
+public interface CategoryClient {
+
+    /**
+     * 获取所有课程及课程分类
+     * @return  所有课程及课程分类
+     */
+    @GetMapping("getAllOfOneLevel")
+    List<CategoryBasicDTO> getAllOfOneLevel();
+}

+ 68 - 0
tj-api/src/main/java/com/tianji/api/client/course/CourseClient.java

@@ -0,0 +1,68 @@
+package com.tianji.api.client.course;
+
+import com.tianji.api.dto.course.*;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
+
+@FeignClient(contextId = "course", value = "course-service")
+public interface CourseClient {
+
+    /**
+     * 根据老师id列表获取老师出题数据和讲课数据
+     * @param teacherIds 老师id列表
+     * @return 老师id和老师对应的出题数和教课数
+     */
+    @GetMapping("/course/infoByTeacherIds")
+    List<SubNumAndCourseNumDTO> infoByTeacherIds(@RequestParam("teacherIds") Iterable<Long> teacherIds);
+
+    /**
+     * 根据小节id获取小节对应的mediaId和课程id
+     *
+     * @param sectionId 小节id
+     * @return 小节对应的mediaId和课程id
+     */
+    @GetMapping("/course/section/{id}")
+    SectionInfoDTO sectionInfo(@PathVariable("id") Long sectionId);
+
+    /**
+     * 根据媒资Id列表查询媒资被引用的次数
+     *
+     * @param mediaIds 媒资id列表
+     * @return 媒资id和媒资被引用的次数的列表
+     */
+    @GetMapping("/course/media/useInfo")
+    List<MediaQuoteDTO> mediaUserInfo(@RequestParam("mediaIds") Iterable<Long> mediaIds);
+
+    /**
+     * 根据课程id查询索引库需要的数据
+     *
+     * @param id 课程id
+     * @return 索引库需要的数据
+     */
+    @GetMapping("/course/{id}/searchInfo")
+    CourseSearchDTO getSearchInfo(@PathVariable("id") Long id);
+
+    /**
+     * 根据课程id集合查询课程简单信息
+     * @param ids id集合
+     * @return 课程简单信息的列表
+     */
+    @GetMapping("/courses/simpleInfo/list")
+    List<CourseSimpleInfoDTO> getSimpleInfoList(@RequestParam("ids") Iterable<Long> ids);
+
+    /**
+     * 根据课程id,获取课程、目录、教师信息
+     * @param id 课程id
+     * @return 课程信息、目录信息、教师信息
+     */
+    @GetMapping("/course/{id}")
+    CourseFullInfoDTO getCourseInfoById(
+            @PathVariable("id") Long id,
+            @RequestParam(value = "withCatalogue", required = false) boolean withCatalogue,
+            @RequestParam(value = "withTeachers", required = false) boolean withTeachers
+    );
+}

+ 15 - 0
tj-api/src/main/java/com/tianji/api/client/course/SubjectClient.java

@@ -0,0 +1,15 @@
+package com.tianji.api.client.course;
+
+import com.tianji.api.dto.course.SubjectDTO;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
+
+@FeignClient(value = "course-service", path = "subjects")
+public interface SubjectClient {
+
+    @GetMapping("list")
+    List<SubjectDTO> queryByIds(@RequestParam("ids") Iterable<Long> ids);
+}

+ 36 - 0
tj-api/src/main/java/com/tianji/api/client/exam/ExamClient.java

@@ -0,0 +1,36 @@
+package com.tianji.api.client.exam;
+
+import com.tianji.api.dto.exam.QuestionBizDTO;
+import com.tianji.api.dto.exam.QuestionDTO;
+import io.swagger.annotations.ApiParam;
+import org.springframework.cloud.openfeign.FeignClient;
+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.RequestParam;
+
+import java.util.List;
+import java.util.Map;
+
+@FeignClient("exam-service")
+public interface ExamClient {
+
+    @PostMapping("/question-biz/list")
+    void saveQuestionBizInfoBatch(@RequestBody Iterable<QuestionBizDTO> qbs);
+
+    @GetMapping("/question-biz/biz/list")
+    List<QuestionBizDTO> queryQuestionIdsByBizIds(@RequestParam("ids") Iterable<Long> bizIds);
+
+    @GetMapping("/question-biz/scores")
+    Map<Long, Integer> queryQuestionScoresByBizIds(@RequestParam("ids") Iterable<Long> bizIds);
+
+    @GetMapping("/questions/list")
+    List<QuestionDTO> queryQuestionByIds(@ApiParam("要查询的题目的id集合") @RequestParam("ids") Iterable<Long> ids);
+
+    @GetMapping("/questions/numOfTeacher")
+    Map<Long, Integer> countSubjectNumOfTeacher(@RequestParam("ids") Iterable<Long> createrIds);
+
+    @GetMapping("/questions//scores")
+    Map<Long, Integer> queryQuestionScores(
+            @ApiParam("要查询的题目的id集合") @RequestParam("ids") Iterable<Long> ids);
+}

+ 36 - 0
tj-api/src/main/java/com/tianji/api/client/learning/LearningClient.java

@@ -0,0 +1,36 @@
+package com.tianji.api.client.learning;
+
+import com.tianji.api.client.learning.fallback.LearningClientFallback;
+import com.tianji.api.dto.leanring.LearningLessonDTO;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+@FeignClient(value = "learning-service", fallbackFactory = LearningClientFallback.class)
+public interface LearningClient {
+
+    /**
+     * 统计课程学习人数
+     * @param courseId 课程id
+     * @return 学习人数
+     */
+    @GetMapping("/lessons/{courseId}/count")
+    Integer countLearningLessonByCourse(@PathVariable("courseId") Long courseId);
+
+    /**
+     * 校验当前用户是否可以学习当前课程
+     * @param courseId 课程id
+     * @return lessonId,如果是报名了则返回lessonId,否则返回空
+     */
+    @GetMapping("/lessons/{courseId}/valid")
+    Long isLessonValid(@PathVariable("courseId") Long courseId);
+
+    /**
+     * 查询当前用户指定课程的学习进度
+     * @param courseId 课程id
+     * @return 课表信息、学习记录及进度信息
+     */
+    @GetMapping("/learning-records/course/{courseId}")
+    LearningLessonDTO queryLearningRecordByCourse(@PathVariable("courseId") Long courseId);
+
+}

+ 31 - 0
tj-api/src/main/java/com/tianji/api/client/learning/fallback/LearningClientFallback.java

@@ -0,0 +1,31 @@
+package com.tianji.api.client.learning.fallback;
+
+import com.tianji.api.client.learning.LearningClient;
+import com.tianji.api.dto.leanring.LearningLessonDTO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.openfeign.FallbackFactory;
+
+@Slf4j
+public class LearningClientFallback implements FallbackFactory<LearningClient> {
+
+    @Override
+    public LearningClient create(Throwable cause) {
+        log.error("查询学习服务异常", cause);
+        return new LearningClient() {
+            @Override
+            public Integer countLearningLessonByCourse(Long courseId) {
+                return 0;
+            }
+
+            @Override
+            public Long isLessonValid(Long courseId) {
+                return null;
+            }
+
+            @Override
+            public LearningLessonDTO queryLearningRecordByCourse(Long courseId) {
+                return null;
+            }
+        };
+    }
+}

+ 15 - 0
tj-api/src/main/java/com/tianji/api/client/search/SearchClient.java

@@ -0,0 +1,15 @@
+package com.tianji.api.client.search;
+
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
+
+@FeignClient("search-service")
+public interface SearchClient {
+
+    @GetMapping("/courses/name")
+    List<Long> queryCoursesIdByName(
+            @RequestParam(value = "keyword", required = false) String keyword);
+}

+ 46 - 0
tj-api/src/main/java/com/tianji/api/client/trade/TradeClient.java

@@ -0,0 +1,46 @@
+package com.tianji.api.client.trade;
+
+import com.tianji.api.client.trade.fallback.TradeClientFallback;
+import com.tianji.api.dto.course.CoursePurchaseInfoDTO;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
+import java.util.Map;
+
+@FeignClient(value = "trade-service", fallbackFactory = TradeClientFallback.class)
+public interface TradeClient {
+    /**
+     * 统计指定课程的报名人数
+     * @param courseIdList 课程id集合
+     * @return 统计结果
+     */
+    @GetMapping("/order-details/enrollNum")
+    Map<Long, Integer> countEnrollNumOfCourse(@RequestParam("courseIdList") List<Long> courseIdList);
+
+    /**
+     * 统计指定学生的报名课程数量
+     * @param studentIds 学生id集合
+     * @return 统计结果
+     */
+    @GetMapping("/order-details/enrollCourse")
+    Map<Long, Integer> countEnrollCourseOfStudent(@RequestParam("studentIds") List<Long> studentIds);
+
+    /**
+     * 检查当前用户是否报名指定课程
+     * @param id 课程id
+     * @return 是否报名
+     */
+    @GetMapping("/order-details/course/{id}")
+    Boolean checkMyLesson(@PathVariable("id") Long id);
+
+    /**
+     * 统计课程购买、退款状态
+     * @param courseId 课程id
+     * @return 统计结果
+     */
+    @GetMapping("/order-details/purchaseInfo")
+    CoursePurchaseInfoDTO getPurchaseInfoOfCourse(@RequestParam("courseId") Long courseId);
+}

+ 41 - 0
tj-api/src/main/java/com/tianji/api/client/trade/fallback/TradeClientFallback.java

@@ -0,0 +1,41 @@
+package com.tianji.api.client.trade.fallback;
+
+import com.tianji.api.client.trade.TradeClient;
+import com.tianji.api.dto.course.CoursePurchaseInfoDTO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.openfeign.FallbackFactory;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+public class TradeClientFallback implements FallbackFactory<TradeClient> {
+
+    @Override
+    public TradeClient create(Throwable cause) {
+        log.error("查询交易服务异常", cause);
+        return new TradeClient() {
+
+            @Override
+            public Map<Long, Integer> countEnrollNumOfCourse(List<Long> courseIdList) {
+                return new HashMap<>();
+            }
+
+            @Override
+            public Map<Long, Integer> countEnrollCourseOfStudent(List<Long> studentIds) {
+                return new HashMap<>();
+            }
+
+            @Override
+            public Boolean checkMyLesson(Long id) {
+                return false;
+            }
+
+            @Override
+            public CoursePurchaseInfoDTO getPurchaseInfoOfCourse(Long courseId) {
+                return new CoursePurchaseInfoDTO();
+            }
+        };
+    }
+}

+ 57 - 0
tj-api/src/main/java/com/tianji/api/client/user/UserClient.java

@@ -0,0 +1,57 @@
+package com.tianji.api.client.user;
+
+
+import com.tianji.api.client.user.fallback.UserClientFallback;
+import com.tianji.api.dto.user.LoginFormDTO;
+import com.tianji.api.dto.user.UserDTO;
+import com.tianji.common.domain.dto.LoginUserDTO;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@FeignClient(value = "user-service", fallbackFactory = UserClientFallback.class)
+public interface UserClient {
+
+    /**
+     * 根据手机号查询用户id
+     * @param phone 手机号
+     * @return 用户id
+     */
+    @GetMapping("/users/ids")
+    Long exchangeUserIdWithPhone(@RequestParam("phone") String phone);
+
+    /**
+     * 登录接口
+     * @param loginDTO 登录信息
+     * @param isStaff 是否是员工
+     * @return 用户详情
+     */
+    @PostMapping("/users/detail/{isStaff}")
+    LoginUserDTO queryUserDetail(@RequestBody LoginFormDTO loginDTO, @PathVariable("isStaff") boolean isStaff);
+
+    /**
+     * 查询用户类型
+     * @param id 用户id
+     * @return 用户类型,0-普通学员,1-老师,2-其他员工
+     */
+    @GetMapping("/users/{id}/type")
+    Integer queryUserType(@PathVariable("id") Long id);
+
+    /**
+     * <h1>根据id批量查询用户信息</h1>
+     * @param ids 用户id集合
+     * @return 用户集合
+     */
+    @GetMapping("/users/list")
+    List<UserDTO> queryUserByIds(@RequestParam("ids") Iterable<Long> ids);
+
+
+    /**
+     * 根据id查询单个学生信息
+     * @param id 用户id
+     * @return 学生
+     */
+    @GetMapping("/users/{id}")
+    UserDTO queryUserById(@PathVariable("id") Long id);
+}

+ 45 - 0
tj-api/src/main/java/com/tianji/api/client/user/fallback/UserClientFallback.java

@@ -0,0 +1,45 @@
+package com.tianji.api.client.user.fallback;
+
+import com.tianji.api.client.user.UserClient;
+import com.tianji.api.dto.user.LoginFormDTO;
+import com.tianji.api.dto.user.UserDTO;
+import com.tianji.common.domain.dto.LoginUserDTO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.openfeign.FallbackFactory;
+
+import java.util.Collections;
+import java.util.List;
+
+@Slf4j
+public class UserClientFallback implements FallbackFactory<UserClient> {
+    @Override
+    public UserClient create(Throwable cause) {
+        log.error("查询用户服务出现异常", cause);
+        return new UserClient() {
+            @Override
+            public Long exchangeUserIdWithPhone(String phone) {
+                return null;
+            }
+
+            @Override
+            public LoginUserDTO queryUserDetail(LoginFormDTO loginDTO, boolean isStaff) {
+                return null;
+            }
+
+            @Override
+            public Integer queryUserType(Long id) {
+                return null;
+            }
+
+            @Override
+            public List<UserDTO> queryUserByIds(Iterable<Long> ids) {
+                return Collections.emptyList();
+            }
+
+            @Override
+            public UserDTO queryUserById(Long id) {
+                return null;
+            }
+        };
+    }
+}

+ 33 - 0
tj-api/src/main/java/com/tianji/api/config/CategoryCacheConfig.java

@@ -0,0 +1,33 @@
+package com.tianji.api.config;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.tianji.api.cache.CategoryCache;
+import com.tianji.api.client.course.CategoryClient;
+import com.tianji.api.dto.course.CategoryBasicDTO;
+import org.springframework.context.annotation.Bean;
+
+import java.time.Duration;
+import java.util.Map;
+
+public class CategoryCacheConfig {
+    /**
+     * 课程分类的caffeine缓存
+     */
+    @Bean
+    public Cache<String, Map<Long, CategoryBasicDTO>> categoryCaches(){
+        return Caffeine.newBuilder()
+                .initialCapacity(1) // 容量限制
+                .maximumSize(10_000) // 最大内存限制
+                .expireAfterWrite(Duration.ofMinutes(30)) // 有效期
+                .build();
+    }
+    /**
+     * 课程分类的缓存工具类
+     */
+    @Bean
+    public CategoryCache categoryCache(
+            Cache<String, Map<Long, CategoryBasicDTO>> categoryCaches, CategoryClient categoryClient){
+        return new CategoryCache(categoryCaches, categoryClient);
+    }
+}

+ 26 - 0
tj-api/src/main/java/com/tianji/api/config/FallbackConfig.java

@@ -0,0 +1,26 @@
+package com.tianji.api.config;
+
+import com.tianji.api.client.learning.fallback.LearningClientFallback;
+import com.tianji.api.client.trade.fallback.TradeClientFallback;
+import com.tianji.api.client.user.fallback.UserClientFallback;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class FallbackConfig {
+    @Bean
+    public LearningClientFallback learningClientFallback(){
+        return new LearningClientFallback();
+    }
+
+    @Bean
+    public TradeClientFallback tradeClientFallback(){
+        return new TradeClientFallback();
+    }
+
+    @Bean
+    public UserClientFallback userClientFallback(){
+        return new UserClientFallback();
+    }
+
+}

+ 22 - 0
tj-api/src/main/java/com/tianji/api/config/RequestIdRelayConfiguration.java

@@ -0,0 +1,22 @@
+package com.tianji.api.config;
+
+
+import feign.RequestInterceptor;
+import org.slf4j.MDC;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static com.tianji.common.constants.Constant.*;
+
+@Configuration
+@EnableFeignClients(basePackages = "com.tianji.api.client")
+public class RequestIdRelayConfiguration {
+
+    @Bean
+    public RequestInterceptor requestIdInterceptor(){
+        return template -> template
+                .header(REQUEST_ID_HEADER, MDC.get(REQUEST_ID_HEADER))
+                .header(REQUEST_FROM_HEADER, FEIGN_ORIGIN_NAME);
+    }
+}

+ 31 - 0
tj-api/src/main/java/com/tianji/api/config/RoleCacheConfig.java

@@ -0,0 +1,31 @@
+package com.tianji.api.config;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.tianji.api.cache.RoleCache;
+import com.tianji.api.client.auth.AuthClient;
+import com.tianji.api.dto.auth.RoleDTO;
+import org.springframework.context.annotation.Bean;
+
+import java.time.Duration;
+
+public class RoleCacheConfig {
+    /**
+     * 角色的caffeine缓存
+     */
+    @Bean
+    public Cache<Long, RoleDTO> roleCaches(){
+        return Caffeine.newBuilder()
+                .initialCapacity(1)
+                .maximumSize(10_000)
+                .expireAfterWrite(Duration.ofMinutes(30))
+                .build();
+    }
+    /**
+     * 角色的缓存工具
+     */
+    @Bean
+    public RoleCache roleCache(Cache<Long, RoleDTO> roleCaches, AuthClient authClient){
+        return new RoleCache(roleCaches, authClient);
+    }
+}

+ 34 - 0
tj-api/src/main/java/com/tianji/api/constants/CourseStatus.java

@@ -0,0 +1,34 @@
+package com.tianji.api.constants;
+
+import com.tianji.common.enums.BaseEnum;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author wusongsong
+ * @since 2022/7/18 16:07
+ * @version 1.0.0
+ **/
+@Getter
+@AllArgsConstructor
+public enum CourseStatus implements BaseEnum {
+    NO_UP_SHELF(1, "待上架"),
+    SHELF(2, "已上架"),
+    DOWN_SHELF(3, "下架"),
+    FINISHED(4, "已完结");
+
+    private final int value;
+    private final String desc;
+
+    public static String desc(Integer status) {
+        if (status == null) {
+            return "";
+        }
+        for (CourseStatus courseStatus : values()) {
+            if (courseStatus.getValue() == status) {
+                return courseStatus.getDesc();
+            }
+        }
+        return null;
+    }
+}

+ 5 - 0
tj-api/src/main/java/com/tianji/api/constants/SmsConstants.java

@@ -0,0 +1,5 @@
+package com.tianji.api.constants;
+
+public interface SmsConstants {
+    String VERIFY_CODE_PARAM_NAME = "code";
+}

+ 27 - 0
tj-api/src/main/java/com/tianji/api/dto/IdAndNumDTO.java

@@ -0,0 +1,27 @@
+package com.tianji.api.dto;
+
+import com.tianji.common.utils.CollUtils;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * id和nun模型,一个id对应的数量可以用与查询id和num的关系
+ * @author wusongsong
+ * @since 2022/8/3 9:27
+ * @version 1.0.0
+ **/
+@Data
+public class IdAndNumDTO {
+    private Long id;
+    private Integer num;
+
+    public static Map<Long, Integer> toMap(List<IdAndNumDTO> list){
+        if (CollUtils.isEmpty(list)) {
+            return CollUtils.emptyMap();
+        }
+        return list.stream().collect(Collectors.toMap(IdAndNumDTO::getId, IdAndNumDTO::getNum));
+    }
+}

+ 44 - 0
tj-api/src/main/java/com/tianji/api/dto/auth/RoleDTO.java

@@ -0,0 +1,44 @@
+package com.tianji.api.dto.auth;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * 角色表
+ * </p>
+ *
+ * @author 虎哥
+ * @since 2022-06-16
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@ApiModel(description = "角色实体")
+public class RoleDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @ApiModelProperty(value = "主键", example = "1")
+    private Long id;
+
+    /**
+     * 角色代号,例如:admin
+     */
+    @ApiModelProperty(value = "角色代号", example = "admin")
+    private String code;
+
+    /**
+     * 角色描述
+     */
+    @ApiModelProperty(value = "角色名称", example = "教师")
+    private String name;
+}

+ 19 - 0
tj-api/src/main/java/com/tianji/api/dto/course/CataSimpleInfoDTO.java

@@ -0,0 +1,19 @@
+package com.tianji.api.dto.course;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * @author wusongsong
+ * @since 2022/7/27 14:22
+ * @version 1.0.0
+ **/
+@Data
+public class CataSimpleInfoDTO {
+    @ApiModelProperty("目录id")
+    private Long id;
+    @ApiModelProperty("目录名称")
+    private String name;
+    @ApiModelProperty("数字序号,不包含章序号")
+    private Integer cIndex;
+}

+ 41 - 0
tj-api/src/main/java/com/tianji/api/dto/course/CatalogueDTO.java

@@ -0,0 +1,41 @@
+package com.tianji.api.dto.course;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author wusongsong
+ * @since 2022/7/11 16:42
+ * @version 1.0.0
+ **/
+@Data
+@ApiModel(description = "课程目录")
+public class CatalogueDTO {
+    @ApiModelProperty("章、节、练习id")
+    private Long id;
+    @ApiModelProperty("序号")
+    private Integer index;
+    @ApiModelProperty("章节练习名称")
+    private String name;
+    @ApiModelProperty("课程总时长,单位秒")
+    private Integer mediaDuration;
+    @ApiModelProperty("是否支持免费试看")
+    private Boolean trailer;
+    @ApiModelProperty("媒资名称")
+    private String mediaName;
+    @ApiModelProperty("媒资id")
+    private Long mediaId;
+    @ApiModelProperty("目录类型1:章,2:节,3:测试")
+    private Integer type;
+    @ApiModelProperty("题目数量")
+    private Integer subjectNum;
+    @ApiModelProperty("题目总分")
+    private Integer totalScore;
+    @ApiModelProperty("是否可以修改,默认不能修改")
+    private Boolean canUpdate = false;
+    @ApiModelProperty("该章的所有小节和练习")
+    private List<CatalogueDTO> sections;
+}

+ 16 - 0
tj-api/src/main/java/com/tianji/api/dto/course/CategoryBasicDTO.java

@@ -0,0 +1,16 @@
+package com.tianji.api.dto.course;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel(description = "分类id和名称信息")
+public class CategoryBasicDTO {
+    @ApiModelProperty(value = "分类id", example = "1")
+    private Long id;
+    @ApiModelProperty(value = "分类名称", example = "Java")
+    private String name;
+    @ApiModelProperty(value = "父分类id", example = "0")
+    private Long parentId;
+}

+ 40 - 0
tj-api/src/main/java/com/tianji/api/dto/course/CategoryDTO.java

@@ -0,0 +1,40 @@
+package com.tianji.api.dto.course;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * @ClassName CategoryDTO
+ * @author wusongsong
+ * @since 2022/7/21 14:51
+ * @version 1.0.0
+ **/
+@Data
+@ApiModel("课程分类")
+public class CategoryDTO {
+    @ApiModelProperty("课程分类id")
+    private Long id;
+    @ApiModelProperty("课程分类名称")
+    private String name;
+    @ApiModelProperty("三级分类数量")
+    private Integer thirdCategoryNum;
+    @ApiModelProperty("课程数量")
+    private Integer courseNum;
+    @ApiModelProperty("状态:1:正常,2:禁用")
+    private Integer status;
+    @ApiModelProperty("状态描述")
+    private String statusDesc;
+    @ApiModelProperty("创建时间")
+    private LocalDateTime createTime;
+    @ApiModelProperty("更新时间")
+    private LocalDateTime updateTime;
+    @ApiModelProperty("排序")
+    private Integer index;
+    @ApiModelProperty("父id")
+    private Long parentId;
+    @ApiModelProperty("级别")
+    private Integer level;
+}

+ 56 - 0
tj-api/src/main/java/com/tianji/api/dto/course/CourseDTO.java

@@ -0,0 +1,56 @@
+package com.tianji.api.dto.course;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@ApiModel("课程信息")
+@Data
+public class CourseDTO {
+    @ApiModelProperty("课程id")
+    private Long id;
+    @ApiModelProperty("课程名称")
+    private String name;
+    @ApiModelProperty("一级课程分类id")
+    private Long categoryIdLv1;
+    @ApiModelProperty("二级课程分类id")
+    private Long categoryIdLv2;
+    @ApiModelProperty("三级课程分类id")
+    private Long categoryIdLv3;
+    @ApiModelProperty("课程封面")
+    private String coverUrl;
+    @ApiModelProperty("创建时间")
+    private LocalDateTime createTime;
+    @ApiModelProperty("更新时间")
+    private LocalDateTime updateTime;
+    @ApiModelProperty("价格")
+    private Integer price;
+    @ApiModelProperty("视频播放时长")
+    private Integer duration;
+    @ApiModelProperty("课程有效期天数")
+    private Integer validDuration;
+    @ApiModelProperty("是否免费")
+    private Boolean free;
+    @ApiModelProperty("发布时间")
+    private LocalDateTime publishTime;
+    @ApiModelProperty("章节数")
+    private Integer sections;
+    @ApiModelProperty("课程状态")
+    private  Byte status;
+    @ApiModelProperty("老师id")
+    private Long teacher;
+    @ApiModelProperty("课程类型,1:直播课程,2:录播课程")
+    private Integer courseType;
+    @ApiModelProperty("更新时间")
+    private Long updater;
+    @ApiModelProperty("课程进行到的步骤,1:基本信息,2:目录,3:课程视频,4:课程题目,5:课程老师")
+    private Integer step;
+    @ApiModelProperty(value = "课程报名人数(销量)", example = "3920")
+    private Integer sold = 0;
+    @ApiModelProperty(value = "课程评价得分,45代表4.5星", example = "35")
+    private Integer score = 0;
+    @ApiModelProperty("课程是否禁用,0:禁用,1:启用")
+    private Integer enable;
+}

+ 49 - 0
tj-api/src/main/java/com/tianji/api/dto/course/CourseFullInfoDTO.java

@@ -0,0 +1,49 @@
+package com.tianji.api.dto.course;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 课程信息
+ *
+ * @author wusongsong
+ * @since 2022/8/5 16:54
+ * @version 1.0.0
+ **/
+@Data
+@ApiModel(description = "课程详细信息,包含课程、目录、教师")
+public class CourseFullInfoDTO {
+    @ApiModelProperty("课程id")
+    private Long id;
+    @ApiModelProperty("课程名称")
+    private String name;
+    @ApiModelProperty("封面链接")
+    private String coverUrl;
+    @ApiModelProperty("价格")
+    private Integer price;
+    @ApiModelProperty("一级课程分类id")
+    private Long firstCateId;
+    @ApiModelProperty("二级课程分类id")
+    private Long secondCateId;
+    @ApiModelProperty("三级课程分类id")
+    private Long thirdCateId;
+    @ApiModelProperty("课程总节数")
+    private Integer sectionNum;
+    @ApiModelProperty("课程购买有效期结束时间")
+    private LocalDateTime purchaseEndTime;
+    @ApiModelProperty("课程学习有效期")
+    private Integer validDuration;
+    @ApiModelProperty("课程章信息")
+    private List<CatalogueDTO> chapters;
+    @ApiModelProperty("老师列表")
+    private List<Long> teacherIds;
+    @JsonIgnore
+    public List<Long> getCategoryIds(){
+        return List.of(firstCateId, secondCateId, thirdCateId);
+    }
+}

+ 26 - 0
tj-api/src/main/java/com/tianji/api/dto/course/CoursePurchaseInfoDTO.java

@@ -0,0 +1,26 @@
+package com.tianji.api.dto.course;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 课程支付相关信息 课程状态
+ * @author wusongsong
+ * @since 2022/7/26 20:41
+ * @version 1.0.0
+ **/
+@Data
+@ApiModel("课程购买信息")
+@NoArgsConstructor
+@AllArgsConstructor
+public class CoursePurchaseInfoDTO {
+    @ApiModelProperty("报名人数")
+    private Integer enrollNum;
+    @ApiModelProperty("退款人数")
+    private Integer refundNum;
+    @ApiModelProperty("实付总金额")
+    private Integer realPayAmount;
+}

+ 49 - 0
tj-api/src/main/java/com/tianji/api/dto/course/CourseSearchDTO.java

@@ -0,0 +1,49 @@
+package com.tianji.api.dto.course;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 课程信息
+ * @ClassName CourseDTO
+ * @author wusongsong
+ * @since 2022/7/18 13:12
+ * @version 1.0.0
+ **/
+@ApiModel(description = "课程信息")
+@Data
+public class CourseSearchDTO {
+    @ApiModelProperty("课程id")
+    private Long id;
+    @ApiModelProperty("课程名称")
+    private String name;
+    @ApiModelProperty("一级课程分类id")
+    private Long categoryIdLv1;
+    @ApiModelProperty("二级课程分类id")
+    private Long categoryIdLv2;
+    @ApiModelProperty("三级课程分类id")
+    private Long categoryIdLv3;
+    @ApiModelProperty("课程封面")
+    private String coverUrl;
+    @ApiModelProperty("价格")
+    private Integer price;
+    @ApiModelProperty("是否免费")
+    private Boolean free;
+    @ApiModelProperty("发布时间")
+    private LocalDateTime publishTime;
+    @ApiModelProperty("章节数")
+    private Integer sections;
+    @ApiModelProperty("课程时长")
+    private Integer duration;
+    @ApiModelProperty("老师id")
+    private Long teacher;
+    @ApiModelProperty("课程类型,1:直播课程,2:录播课程")
+    private Integer courseType;
+    @ApiModelProperty(value = "课程报名人数(销量)", example = "3920")
+    private Integer sold = 0;
+    @ApiModelProperty(value = "课程评价得分,45代表4.5星", example = "35")
+    private Integer score = 0;
+}

+ 45 - 0
tj-api/src/main/java/com/tianji/api/dto/course/CourseSimpleInfoDTO.java

@@ -0,0 +1,45 @@
+package com.tianji.api.dto.course;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * @author wusongsong
+ * @since 2022/7/27 14:32
+ * @version 1.0.0
+ **/
+@Data
+public class CourseSimpleInfoDTO {
+    @ApiModelProperty("课程id")
+    private Long id;
+    @ApiModelProperty("课程名称")
+    private String name;
+    @ApiModelProperty("封面url")
+    private String coverUrl;
+    @ApiModelProperty("价格")
+    private Integer price;
+    @ApiModelProperty("课程状态")
+    private Integer status;
+    @ApiModelProperty("是否是免费课程")
+    private Boolean free;
+    @ApiModelProperty("一级分类id")
+    private Long firstCateId;
+    @ApiModelProperty("二级分类id")
+    private Long secondCateId;
+    @ApiModelProperty("三级分类id")
+    private Long thirdCateId;
+    @ApiModelProperty("小节数量")
+    private Integer sectionNum;
+    @ApiModelProperty("课程购买有效期结束时间")
+    private LocalDateTime purchaseEndTime;
+    @ApiModelProperty("课程学习有效期,单位:月")
+    private Integer validDuration;
+    @JsonIgnore
+    public List<Long> getCategoryIds(){
+        return List.of(firstCateId, secondCateId, thirdCateId);
+    }
+}

+ 24 - 0
tj-api/src/main/java/com/tianji/api/dto/course/MediaQuoteDTO.java

@@ -0,0 +1,24 @@
+package com.tianji.api.dto.course;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @ClassName MediaQuoteDTO
+ * @author wusongsong
+ * @since 2022/7/18 17:43
+ * @version 1.0.0
+ **/
+@ApiModel("媒资被引用情况")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class MediaQuoteDTO {
+    @ApiModelProperty("媒资id")
+    private Long mediaId;
+    @ApiModelProperty("引用数")
+    private Integer quoteNum;
+}

+ 22 - 0
tj-api/src/main/java/com/tianji/api/dto/course/SectionInfoDTO.java

@@ -0,0 +1,22 @@
+package com.tianji.api.dto.course;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@ApiModel("小节信息,包含课程id和媒资id")
+@AllArgsConstructor
+@NoArgsConstructor
+public class SectionInfoDTO {
+    @ApiModelProperty("课程id")
+    private Long courseId;
+    @ApiModelProperty("媒资id")
+    private Long mediaId;
+    @ApiModelProperty("是否支持免费试看")
+    private Boolean trailer;
+    @ApiModelProperty("免费时长,不免费为0,单位分钟")
+    private Integer freeDuration;
+}

+ 28 - 0
tj-api/src/main/java/com/tianji/api/dto/course/SubNumAndCourseNumDTO.java

@@ -0,0 +1,28 @@
+package com.tianji.api.dto.course;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 老师负责的课程数和出题数目的集合
+ * @ClassName SubNumAndCourseNumDTO
+ * @author wusongsong
+ * @since 2022/7/18 15:12
+ * @version 1.0.0
+ **/
+@Data
+@AllArgsConstructor
+@NotNull
+@ApiModel("老师id和老师对应的课程数,出题数")
+public class SubNumAndCourseNumDTO {
+    @ApiModelProperty("老师id")
+    private Long teacherId;
+    @ApiModelProperty("老师负责的课程数")
+    private Integer courseNum;
+    @ApiModelProperty("老师出题数")
+    private Integer subjectNum;
+}

+ 47 - 0
tj-api/src/main/java/com/tianji/api/dto/course/SubjectDTO.java

@@ -0,0 +1,47 @@
+package com.tianji.api.dto.course;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * <p>
+ * 考试记录表
+ * </p>
+ *
+ * @author 虎哥
+ * @since 2022-07-18
+ */
+@Data
+@ApiModel(description = "考试问题详情")
+public class SubjectDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("问题id")
+    private Long id;
+
+    @ApiModelProperty("题干")
+    private String name;
+
+    @ApiModelProperty("选择题的选项")
+    private List<String> options;
+
+    @ApiModelProperty("分值")
+    private Integer score;
+
+    @ApiModelProperty("问题类型,1:单选题,2:多选题,3:不定向选择题,4:判断题,5:主观题")
+    private Integer subjectType;
+
+    @ApiModelProperty("难易度,1:简单,2:中等,3:困难")
+    private Integer difficulty;
+
+    @ApiModelProperty("解析")
+    private String analysis;
+
+    @ApiModelProperty("选择题答案,0对应A,1对应B,可填多个")
+    private List<Integer> answers;
+}

+ 23 - 0
tj-api/src/main/java/com/tianji/api/dto/exam/QuestionBizDTO.java

@@ -0,0 +1,23 @@
+package com.tianji.api.dto.exam;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+@Data
+@ApiModel(description = "题目与业务关联信息")
+@Accessors(chain = true)
+@AllArgsConstructor(staticName = "of")
+@NoArgsConstructor
+public class QuestionBizDTO{
+
+    @ApiModelProperty("业务id,要关联问题的某业务id,例如小节id")
+    private Long bizId;
+
+    @ApiModelProperty("题目id")
+    private Long questionId;
+
+}

+ 40 - 0
tj-api/src/main/java/com/tianji/api/dto/exam/QuestionDTO.java

@@ -0,0 +1,40 @@
+package com.tianji.api.dto.exam;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@ApiModel(description = "题目数据")
+public class QuestionDTO {
+
+    @ApiModelProperty("题目id")
+    private Long id;
+
+    @ApiModelProperty("题目名称,题干")
+    private String name;
+
+    @ApiModelProperty("题目类型,1:单选题,2:多选题,3:不定向选择题,4:判断题,5:主观题")
+    private String type;
+
+    @ApiModelProperty("难易度,1:简单,2:中等,3:困难")
+    private Integer difficulty;
+
+    @ApiModelProperty("分值")
+    private Integer score;
+
+    @ApiModelProperty("选择题选项,json数组格式")
+    private List<String> options;
+
+    @ApiModelProperty("选择题正确答案1到10,如果有多个答案,中间使用逗号隔开,如果是判断题,1:代表正确,其他代表错误")
+    private String answer;
+
+    @ApiModelProperty("答案解析")
+    private String analysis;
+}

+ 18 - 0
tj-api/src/main/java/com/tianji/api/dto/leanring/LearningLessonDTO.java

@@ -0,0 +1,18 @@
+package com.tianji.api.dto.leanring;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@ApiModel(description = "学习课表进度信息")
+public class LearningLessonDTO {
+    @ApiModelProperty("课表id")
+    private Long id;
+    @ApiModelProperty("最近学习的小节id")
+    private Long latestSectionId;
+    @ApiModelProperty("学习过的小节的记录")
+    private List<LearningRecordDTO> records;
+}

+ 16 - 0
tj-api/src/main/java/com/tianji/api/dto/leanring/LearningRecordDTO.java

@@ -0,0 +1,16 @@
+package com.tianji.api.dto.leanring;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel(description = "小节信息及学习进度")
+public class LearningRecordDTO {
+    @ApiModelProperty("对应节的id")
+    private Long sectionId;
+    @ApiModelProperty("视频的当前观看时长,单位秒")
+    private Integer moment;
+    @ApiModelProperty("是否完成学习,默认false")
+    private Boolean finished;
+}

+ 30 - 0
tj-api/src/main/java/com/tianji/api/dto/leanring/LearningRecordFormDTO.java

@@ -0,0 +1,30 @@
+package com.tianji.api.dto.leanring;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@ApiModel(description = "学习记录表单数据")
+public class LearningRecordFormDTO {
+
+    @ApiModelProperty("小节类型:1-视频,2-考试")
+    private Integer sectionType;
+
+    @ApiModelProperty("课表id")
+    private Long lessonId;
+
+    @ApiModelProperty("对应节的id")
+    private Long sectionId;
+
+    @ApiModelProperty("视频总时长,单位秒")
+    private Integer duration;
+
+    @ApiModelProperty("视频的当前观看时长,单位秒,第一次提交填0")
+    private Integer moment;
+
+    @ApiModelProperty("提交时间")
+    private LocalDateTime commitTime;
+}

+ 14 - 0
tj-api/src/main/java/com/tianji/api/dto/sms/SmsInfoDTO.java

@@ -0,0 +1,14 @@
+package com.tianji.api.dto.sms;
+
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+@ApiModel(description = "短信发送参数")
+public class SmsInfoDTO {
+    private String templateCode;
+    private Iterable<String> phones;
+    private Map<String, String> templateParams;
+}

+ 28 - 0
tj-api/src/main/java/com/tianji/api/dto/trade/OrderBasicDTO.java

@@ -0,0 +1,28 @@
+package com.tianji.api.dto.trade;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@Builder
+public class OrderBasicDTO {
+    /**
+     * 订单id
+     */
+    private Long orderId;
+    /**
+     * 下单用户id
+     */
+    private Long userId;
+    /**
+     * 下单的课程id集合
+     */
+    private List<Long> courseIds;
+    /**
+     * 订单完成时间
+     */
+    private LocalDateTime finishTime;
+}

+ 24 - 0
tj-api/src/main/java/com/tianji/api/dto/user/LoginFormDTO.java

@@ -0,0 +1,24 @@
+package com.tianji.api.dto.user;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+@Data
+@ApiModel(description = "登录表单实体")
+public class LoginFormDTO {
+    @ApiModelProperty(value = "登录方式:1-密码登录; 2-验证码登录", example = "1", required = true)
+    @NotNull
+    private Integer type;
+    @ApiModelProperty(value = "用户名", example = "jack")
+    private String username;
+    @ApiModelProperty(value = "手机号", example = "13800010001")
+    private String cellPhone;
+    @ApiModelProperty(value = "密码", example = "123", required = true)
+    @NotNull
+    private String password;
+    @ApiModelProperty(value = "7天免密登录", example = "true")
+    private Boolean rememberMe = false;
+}

+ 53 - 0
tj-api/src/main/java/com/tianji/api/dto/user/UserDTO.java

@@ -0,0 +1,53 @@
+package com.tianji.api.dto.user;
+
+import com.tianji.common.constants.RegexConstants;
+import com.tianji.common.validate.annotations.EnumValid;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+
+@Data
+@ApiModel(description = "用户详情")
+public class UserDTO {
+    @ApiModelProperty(value = "用户id", example = "1")
+    private Long id;
+    @ApiModelProperty(value = "手机", example = "13890011009")
+    @Pattern(regexp = RegexConstants.PHONE_PATTERN, message = "手机号格式错误")
+    private String cellPhone;
+    @ApiModelProperty(value = "用户名称/昵称", example = "李四")
+    private String name;
+    @ApiModelProperty(value = "用户类型,1-其他员工,2-普通学员,3-老师", example = "2")
+    @EnumValid(enumeration = {1,2,3}, message = "用户类型错误")
+    @NotNull
+    private Integer type;
+    @ApiModelProperty(value = "角色id,老师和学生不用填", example = "5")
+    private Long roleId;
+    @ApiModelProperty(value = "头像", example = "default-user-icon.jpg")
+    private String icon;
+    @ApiModelProperty(value = "岗位", example = "讲师")
+    private String job;
+    @ApiModelProperty(value = "个人介绍", example = "黑马高级Java讲师")
+    private String intro;
+    @ApiModelProperty(value = "形象照地址", example = "default-teacher-photo.jpg")
+    private String photo;
+    @ApiModelProperty(value = "用户名", example = "13800010004")
+    private String username;
+    @ApiModelProperty(value = "邮箱")
+    @Email
+    private String email;
+    @ApiModelProperty(value = "QQ号码")
+    private String qq;
+    @ApiModelProperty(value = "省")
+    private String province;
+    @ApiModelProperty(value = "市")
+    private String city;
+    @ApiModelProperty(value = "区")
+    private String district;
+    @ApiModelProperty(value = "性别:0-男性,1-女性", example = "0")
+    @EnumValid(enumeration = {0, 1}, message = "性别格式不正确")
+    private Integer gender;
+}

+ 5 - 0
tj-api/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,5 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  com.tianji.api.config.RequestIdRelayConfiguration, \
+  com.tianji.api.config.RoleCacheConfig, \
+  com.tianji.api.config.FallbackConfig, \
+  com.tianji.api.config.CategoryCacheConfig

+ 26 - 0
tj-auth/pom.xml

@@ -0,0 +1,26 @@
+<?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>tjxt</artifactId>
+        <groupId>com.tianji</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tj-auth</artifactId>
+    <packaging>pom</packaging>
+    <modules>
+        <module>tj-auth-common</module>
+        <module>tj-auth-service</module>
+        <module>tj-auth-resource-sdk</module>
+        <module>tj-auth-gateway-sdk</module>
+    </modules>
+
+    <properties>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+    </properties>
+
+</project>

+ 26 - 0
tj-auth/tj-auth-common/pom.xml

@@ -0,0 +1,26 @@
+<?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>tj-auth</artifactId>
+        <groupId>com.tianji</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tj-auth-common</artifactId>
+
+    <properties>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+    </properties>
+    <dependencies>
+        <!--domain-->
+        <dependency>
+            <groupId>com.tianji</groupId>
+            <artifactId>tj-common</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+    </dependencies>
+</project>

+ 27 - 0
tj-auth/tj-auth-common/src/main/java/com/tianji/auth/common/constants/AuthErrorInfo.java

@@ -0,0 +1,27 @@
+package com.tianji.auth.common.constants;
+
+public interface AuthErrorInfo {
+
+    interface Msg {
+        String INVALID_STAFF_TYPE = "无效的账户类型";
+
+
+        String INVALID_ROLE_ID = "绑定的角色不存在";
+        String PRIVILEGE_EXISTS = "权限信息已存在";
+        String PRIVILEGE_NOT_FOUND = "权限数据不存在";
+        String ROLE_NOT_FOUND = "角色数据不存在";
+        String MENU_NOT_FOUND = "菜单数据不存在";
+        String UNAUTHORIZED = "未登录";
+        String FORBIDDEN = "无访问权限";
+        String INVALID_TOKEN = "无效的token";
+        String EXPIRED_TOKEN = "token已过期";
+        String INVALID_TOKEN_PAYLOAD = "token参数格式错误";
+    }
+
+    interface Code {
+        // 过期token
+        int EXPIRED_TOKEN_CODE = 40101;
+        // 无效token
+        int INVALID_TOKEN_CODE = 40102;
+    }
+}

+ 30 - 0
tj-auth/tj-auth-common/src/main/java/com/tianji/auth/common/constants/JwtConstants.java

@@ -0,0 +1,30 @@
+package com.tianji.auth.common.constants;
+
+import java.time.Duration;
+
+public class JwtConstants {
+    public static final String PAYLOAD_USER_KEY = "user";
+    public static final String PAYLOAD_JTI_KEY = "jti";
+
+    public static final String JWT_REDIS_KEY_PREFIX = "jwt:uid:";
+    // token过期时间,测试期间改为 1天,正常是5分钟
+    public static final Duration JWT_TOKEN_TTL = Duration.ofMinutes(60*5);
+    // public static final Duration JWT_TOKEN_TTL = Duration.ofMinutes(60 * 24);
+    public static final Duration JWT_REFRESH_TTL = Duration.ofMinutes(30);
+
+    public static final Duration JWT_REMEMBER_ME_TTL = Duration.ofDays(7);
+
+    public static final String JWT_ALGORITHM = "rs256";
+    public static final String AUTHORIZATION_HEADER = "authorization";
+    public static final String REFRESH_HEADER = "refresh";
+    public static final String ADMIN_REFRESH_HEADER = "admin-refresh";
+
+    public static final String USER_HEADER = "user-info";
+
+    /* 权限缓存 KEY  begin */
+    public static final String AUTH_PRIVILEGE_KEY = "auth:privileges";
+    public static final String AUTH_PRIVILEGE_VERSION_KEY = "version";
+    public static final String LOCK_AUTH_PRIVILEGE_KEY = "lock:auth:privileges";
+    /* 权限缓存 KEY  end */
+
+}

+ 13 - 0
tj-auth/tj-auth-common/src/main/java/com/tianji/auth/common/domain/PrivilegeRoleDTO.java

@@ -0,0 +1,13 @@
+package com.tianji.auth.common.domain;
+
+import lombok.Data;
+
+import java.util.Set;
+
+@Data
+public class PrivilegeRoleDTO {
+    private Long id;
+    private String antPath;
+    private Boolean internal;
+    private Set<Long> roles;
+}

+ 34 - 0
tj-auth/tj-auth-gateway-sdk/pom.xml

@@ -0,0 +1,34 @@
+<?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>tj-auth</artifactId>
+        <groupId>com.tianji</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tj-auth-gateway-sdk</artifactId>
+
+    <properties>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.tianji</groupId>
+            <artifactId>tj-auth-common</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-commons</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 24 - 0
tj-auth/tj-auth-gateway-sdk/src/main/java/com/tianji/authsdk/gateway/config/AuthAutoConfiguration.java

@@ -0,0 +1,24 @@
+package com.tianji.authsdk.gateway.config;
+
+import com.tianji.authsdk.gateway.util.AuthUtil;
+import com.tianji.authsdk.gateway.util.JwtSignerHolder;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+@Configuration
+public class AuthAutoConfiguration {
+
+    @Bean
+    @ConditionalOnClass(DiscoveryClient.class)
+    public JwtSignerHolder jwtSignerHolder(DiscoveryClient discoveryClient){
+        return new JwtSignerHolder(discoveryClient);
+    }
+
+    @Bean
+    public AuthUtil authUtil(JwtSignerHolder jwtSignerHolder, StringRedisTemplate stringRedisTemplate){
+        return new AuthUtil(jwtSignerHolder, stringRedisTemplate);
+    }
+}

+ 176 - 0
tj-auth/tj-auth-gateway-sdk/src/main/java/com/tianji/authsdk/gateway/util/AuthUtil.java

@@ -0,0 +1,176 @@
+package com.tianji.authsdk.gateway.util;
+
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.exceptions.ValidateException;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import cn.hutool.jwt.JWT;
+import cn.hutool.jwt.JWTValidator;
+import com.tianji.auth.common.domain.PrivilegeRoleDTO;
+import com.tianji.common.domain.R;
+import com.tianji.common.domain.dto.LoginUserDTO;
+import com.tianji.common.exceptions.ForbiddenException;
+import com.tianji.common.exceptions.UnauthorizedException;
+import com.tianji.common.utils.StringUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.BoundHashOperations;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.util.AntPathMatcher;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static com.tianji.auth.common.constants.AuthErrorInfo.Code.EXPIRED_TOKEN_CODE;
+import static com.tianji.auth.common.constants.AuthErrorInfo.Code.INVALID_TOKEN_CODE;
+import static com.tianji.auth.common.constants.AuthErrorInfo.Msg.*;
+import static com.tianji.auth.common.constants.JwtConstants.*;
+
+@Slf4j
+public class AuthUtil {
+    // 缓存权限信息
+    private Map<String, PrivilegeRoleDTO> privileges = new HashMap<>();
+    // 要拦截的路径匹配符的集合
+    private Set<String> paths = new HashSet<>();
+    // 权限版本信息,减少不必要的缓存处理
+    private int privilegeVersion;
+
+    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
+    private final JwtSignerHolder jwtSignerHolder;
+    private final StringRedisTemplate stringRedisTemplate;
+    private final BoundHashOperations<String, String, String> hashOps;
+
+    public AuthUtil(JwtSignerHolder jwtSignerHolder, StringRedisTemplate stringRedisTemplate) {
+        this.jwtSignerHolder = jwtSignerHolder;
+        this.stringRedisTemplate = stringRedisTemplate;
+        this.hashOps = stringRedisTemplate.boundHashOps(AUTH_PRIVILEGE_KEY);
+    }
+
+    public R<LoginUserDTO> parseToken(String token) {
+        // 1.校验token是否为空
+        if(StringUtils.isBlank(token)){
+            return R.error(INVALID_TOKEN_CODE, INVALID_TOKEN);
+        }
+        JWT jwt = null;
+        try {
+            jwt = JWT.of(token).setSigner(jwtSignerHolder.getJwtSigner());
+        } catch (Exception e) {
+            return R.error(INVALID_TOKEN_CODE, INVALID_TOKEN);
+        }
+        // 2.校验jwt是否有效
+        if (!jwt.verify()) {
+            // 验证失败,返回空
+            return R.error(INVALID_TOKEN_CODE, INVALID_TOKEN);
+        }
+        // 3.校验是否过期
+        try {
+            JWTValidator.of(jwt).validateDate();
+        } catch (ValidateException e) {
+            return R.error(EXPIRED_TOKEN_CODE, EXPIRED_TOKEN);
+        }
+        // 4.数据格式校验
+        Object userPayload = jwt.getPayload(PAYLOAD_USER_KEY);
+        if (userPayload == null) {
+            // 数据为空
+            return R.error(INVALID_TOKEN_CODE, INVALID_TOKEN_PAYLOAD);
+        }
+
+        // 5.数据解析
+        LoginUserDTO userDTO;
+        try {
+            userDTO = ((JSONObject)userPayload).toBean(LoginUserDTO.class);
+        } catch (RuntimeException e) {
+            // token格式有误
+            return R.error(INVALID_TOKEN_CODE, INVALID_TOKEN_PAYLOAD);
+        }
+
+        // 6.返回
+        return R.ok(userDTO);
+    }
+
+    public void checkAuth(String antPath, R<LoginUserDTO> r){
+        // 1.判断是否是需要权限的路径
+        String matchPath = findMatchPath(antPath);
+        if(matchPath == null){
+            // 没有权限限制,直接放行
+            return;
+        }
+        // 2.判断是否登录成功
+        if(!r.success()){
+            // 未登录,直接报错
+            throw new UnauthorizedException(r.getCode(), r.getMsg());
+        }
+        // 3.获取当前路径所需权限
+        PrivilegeRoleDTO pathPrivilege = findPathPrivilege(matchPath);
+
+        // 4.权限判断
+        Set<Long> requiredRoles = pathPrivilege.getRoles();
+        if (!CollectionUtil.contains(requiredRoles, r.getData().getRoleId())) {
+            // 没有访问权限
+            throw new ForbiddenException(FORBIDDEN);
+        }
+    }
+
+    private String findMatchPath(String antPath){
+        String matchPath = null;
+        for (String pathPattern : paths) {
+            if(antPathMatcher.match(pathPattern, antPath)){
+                matchPath = pathPattern;
+                break;
+            }
+        }
+        return matchPath;
+    }
+
+    private PrivilegeRoleDTO findPathPrivilege(String path){
+        return privileges.get(path);
+    }
+
+    private List<PrivilegeRoleDTO> loadPrivileges(){
+        List<String> values = hashOps.values();
+        if(CollUtil.isEmpty(values)){
+            return Collections.emptyList();
+        }
+        return values.stream()
+                .map(json -> JSONUtil.toBean(json, PrivilegeRoleDTO.class))
+                .collect(Collectors.toList());
+    }
+
+    private int currentVersion() {
+        String version = stringRedisTemplate.opsForValue().get(AUTH_PRIVILEGE_VERSION_KEY);
+        if(StrUtil.isEmpty(version)){
+            return 0;
+        }
+        return Integer.parseInt(version);
+    }
+
+
+    @Scheduled(fixedDelay = 20000)
+    public void refreshTask(){
+        // 1.获取版本号
+        int currentVersion = currentVersion();
+        if (currentVersion == this.privilegeVersion) {
+            // 版本一致,说明数据没有更新,直接结束任务
+            return;
+        }
+        // 2.获取最新权限信息
+        List<PrivilegeRoleDTO> privilegeRoleDTOS = loadPrivileges();
+        if(CollUtil.isEmpty(privilegeRoleDTOS)){
+            // 更新版本
+            this.privilegeVersion = currentVersion;
+            return;
+        }
+        // 3.数据处理
+        Map<String, PrivilegeRoleDTO> map = new HashMap<>();
+        for (PrivilegeRoleDTO p : privilegeRoleDTOS) {
+            map.put(p.getAntPath(), p);
+            this.privileges = map;
+        }
+        this.paths = map.keySet();
+        // 4.更新版本
+        this.privilegeVersion = currentVersion;
+    }
+}

+ 110 - 0
tj-auth/tj-auth-gateway-sdk/src/main/java/com/tianji/authsdk/gateway/util/JwtSignerHolder.java

@@ -0,0 +1,110 @@
+package com.tianji.authsdk.gateway.util;
+
+import cn.hutool.crypto.KeyUtil;
+import cn.hutool.crypto.SecureUtil;
+import cn.hutool.crypto.asymmetric.AsymmetricAlgorithm;
+import cn.hutool.http.HttpUtil;
+import cn.hutool.jwt.signers.JWTSigner;
+import cn.hutool.jwt.signers.JWTSignerUtil;
+import com.tianji.auth.common.constants.JwtConstants;
+import com.tianji.common.utils.CollUtils;
+import com.tianji.common.utils.MarkedRunnable;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.client.ServiceInstance;
+import org.springframework.cloud.client.discovery.DiscoveryClient;
+
+import javax.annotation.PostConstruct;
+import java.nio.charset.StandardCharsets;
+import java.security.PublicKey;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+@Data
+@Slf4j
+public class JwtSignerHolder {
+
+    private volatile JWTSigner jwtSigner;
+
+    private DiscoveryClient discoveryClient;
+
+    public JwtSignerHolder(DiscoveryClient discoveryClient) {
+        this.discoveryClient = discoveryClient;
+    }
+
+    private final ExecutorService ses = new ThreadPoolExecutor(
+            1,
+            1,
+            10,
+            TimeUnit.SECONDS,
+            new ArrayBlockingQueue<>(1),
+            r -> new Thread(r, "AuthFetchJwkThread")
+    );
+
+    @PostConstruct
+    public void init(){
+        // 尝试获取jwk秘钥
+        ses.submit(new MarkedRunnable(new JwkTask(discoveryClient)));
+    }
+
+    public void shutdown(){
+        ses.shutdown();
+        log.debug("销毁加载秘钥线程 AuthFetchJwkThread");
+    }
+    public static void sleep(long time){
+        try {
+            Thread.sleep(time);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+    class JwkTask implements Runnable{
+        private final DiscoveryClient discoveryClient;
+
+        public JwkTask(DiscoveryClient discoveryClient) {
+            this.discoveryClient = discoveryClient;
+        }
+
+        @Override
+        public void run() {
+            while (jwtSigner == null) {
+                try {
+                    log.info("尝试加载auth服务地址");
+                    List<ServiceInstance> instances = discoveryClient.getInstances("auth-service");
+                    if(CollUtils.isEmpty(instances)){
+                        log.error("加载auth服务地址失败,原因:数据为空");
+                        sleep(10000);
+                        continue;
+                    }
+                    ServiceInstance instance = instances.get(0);
+                    String jwkUri = String.format("http://%s:%d/jwks", instance.getHost(), instance.getPort());
+                    log.info("加载auth服务地址成功,{}", jwkUri);
+
+                    log.info("尝试加载jwk秘钥");
+                    // 请求获取jwk
+                    String result = HttpUtil.get(jwkUri, StandardCharsets.UTF_8);
+                    if(result == null){
+                        log.error("加载jwk秘钥失败,原因:数据为空");
+                        sleep(10000);
+                        continue;
+                    }
+                    // 解析
+                    PublicKey publicKey = KeyUtil.generatePublicKey(
+                            AsymmetricAlgorithm.RSA_ECB_PKCS1.getValue(),
+                            SecureUtil.decode(result)
+                    );
+                    jwtSigner = JWTSignerUtil.createSigner(JwtConstants.JWT_ALGORITHM, publicKey);
+                    log.info("加载jwk秘钥成功!");
+                } catch (Exception e) {
+                    log.error("加载jwk秘钥失败,原因:{}", e.getMessage());
+                    sleep(10000);
+                }
+            }
+            // 关闭线程池
+            shutdown();
+        }
+    }
+}

+ 19 - 0
tj-auth/tj-auth-gateway-sdk/src/main/resources/META-INF/spring-configuration-metadata.json

@@ -0,0 +1,19 @@
+{
+  "groups": [
+    {
+      "name": "tj.auth.gateway",
+      "type": "com.tianji.authsdk.gateway.config.AuthAutoConfiguration",
+      "sourceType": "com.tianji.authsdk.gateway.config.AuthProperties"
+    }
+  ],
+  "properties": [
+    {
+      "name": "tj.auth.excludePath",
+      "type": "java.util.Set",
+      "description": "不用登录就能访问的路径,ant风格通配符",
+      "sourceType": "com.tianji.authsdk.gateway.config.AuthProperties",
+      "defaultValue": ""
+    }
+  ],
+  "hints": []
+}

+ 2 - 0
tj-auth/tj-auth-gateway-sdk/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  com.tianji.authsdk.gateway.config.AuthAutoConfiguration

+ 41 - 0
tj-auth/tj-auth-resource-sdk/pom.xml

@@ -0,0 +1,41 @@
+<?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>tj-auth</artifactId>
+        <groupId>com.tianji</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tj-auth-resource-sdk</artifactId>
+
+    <properties>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webmvc</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+            <version>2.5</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.tianji</groupId>
+            <artifactId>tj-auth-common</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>io.github.openfeign</groupId>
+            <artifactId>feign-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>

+ 17 - 0
tj-auth/tj-auth-resource-sdk/src/main/java/com/tianji/authsdk/resource/config/FeignRelayUserAutoConfiguration.java

@@ -0,0 +1,17 @@
+package com.tianji.authsdk.resource.config;
+
+import feign.Feign;
+import com.tianji.authsdk.resource.interceptors.FeignRelayUserInterceptor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ConditionalOnClass(Feign.class)
+public class FeignRelayUserAutoConfiguration {
+
+    @Bean
+    public FeignRelayUserInterceptor feignRelayUserInterceptor(){
+        return new FeignRelayUserInterceptor();
+    }
+}

+ 14 - 0
tj-auth/tj-auth-resource-sdk/src/main/java/com/tianji/authsdk/resource/config/ResourceAuthProperties.java

@@ -0,0 +1,14 @@
+package com.tianji.authsdk.resource.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.List;
+
+@Data
+@ConfigurationProperties(prefix = "tj.auth.resource")
+public class ResourceAuthProperties {
+    private Boolean enable = false;
+    private List<String> includeLoginPaths;
+    private List<String> excludeLoginPaths;
+}

+ 52 - 0
tj-auth/tj-auth-resource-sdk/src/main/java/com/tianji/authsdk/resource/config/ResourceInterceptorConfiguration.java

@@ -0,0 +1,52 @@
+package com.tianji.authsdk.resource.config;
+
+import cn.hutool.core.collection.CollUtil;
+import com.tianji.authsdk.resource.interceptors.LoginAuthInterceptor;
+import com.tianji.authsdk.resource.interceptors.UserInfoInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+@EnableConfigurationProperties(ResourceAuthProperties.class)
+public class ResourceInterceptorConfiguration implements WebMvcConfigurer {
+
+    private final ResourceAuthProperties authProperties;
+
+    @Autowired
+    public ResourceInterceptorConfiguration(ResourceAuthProperties resourceAuthProperties) {
+        this.authProperties = resourceAuthProperties;
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 1.添加用户信息拦截器
+        registry.addInterceptor(new UserInfoInterceptor()).order(0);
+        // 2.是否需要做登录拦截
+        if(!authProperties.getEnable()){
+            // 无需登录拦截
+            return;
+        }
+        // 2.添加登录拦截器
+        InterceptorRegistration registration = registry.addInterceptor(new LoginAuthInterceptor()).order(1);
+        // 2.1.添加拦截器路径
+        if(CollUtil.isNotEmpty(authProperties.getIncludeLoginPaths())){
+            registration.addPathPatterns(authProperties.getIncludeLoginPaths());
+        }
+        // 2.2.添加排除路径
+        if(CollUtil.isNotEmpty(authProperties.getExcludeLoginPaths())){
+            registration.excludePathPatterns(authProperties.getExcludeLoginPaths());
+        }
+        // 2.3.排除swagger路径
+        registration.excludePathPatterns(
+                "/v2/**",
+                "/v3/**",
+                "/swagger-resources/**",
+                "/webjars/**",
+                "/doc.html"
+        );
+    }
+}

+ 17 - 0
tj-auth/tj-auth-resource-sdk/src/main/java/com/tianji/authsdk/resource/interceptors/FeignRelayUserInterceptor.java

@@ -0,0 +1,17 @@
+package com.tianji.authsdk.resource.interceptors;
+
+import com.tianji.auth.common.constants.JwtConstants;
+import com.tianji.common.utils.UserContext;
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+
+public class FeignRelayUserInterceptor implements RequestInterceptor {
+    @Override
+    public void apply(RequestTemplate template) {
+        Long userId = UserContext.getUser();
+        if (userId == null) {
+            return;
+        }
+        template.header(JwtConstants.USER_HEADER, userId.toString());
+    }
+}

+ 27 - 0
tj-auth/tj-auth-resource-sdk/src/main/java/com/tianji/authsdk/resource/interceptors/LoginAuthInterceptor.java

@@ -0,0 +1,27 @@
+package com.tianji.authsdk.resource.interceptors;
+
+import com.tianji.common.utils.UserContext;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Slf4j
+public class LoginAuthInterceptor implements HandlerInterceptor {
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        // 1.尝试获取用户信息
+        Long userId = UserContext.getUser();
+        // 2.判断是否登录
+        if (userId == null) {
+            response.setStatus(401);
+            response.sendError(401, "未登录用户无法访问!");
+            // 2.3.未登录,直接拦截
+            return false;
+        }
+        // 3.登录则放行
+        return true;
+    }
+}

+ 38 - 0
tj-auth/tj-auth-resource-sdk/src/main/java/com/tianji/authsdk/resource/interceptors/UserInfoInterceptor.java

@@ -0,0 +1,38 @@
+package com.tianji.authsdk.resource.interceptors;
+
+import com.tianji.auth.common.constants.JwtConstants;
+import com.tianji.common.utils.UserContext;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Slf4j
+public class UserInfoInterceptor implements HandlerInterceptor {
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        // 1.尝试获取头信息中的用户信息
+        String authorization = request.getHeader(JwtConstants.USER_HEADER);
+        // 2.判断是否为空
+        if (authorization == null) {
+            return true;
+        }
+        // 3.转为用户id并保存
+        try {
+            Long userId = Long.valueOf(authorization);
+            UserContext.setUser(userId);
+            return true;
+        } catch (NumberFormatException e) {
+            log.error("用户身份信息格式不正确,{}, 原因:{}", authorization, e.getMessage());
+            return true;
+        }
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
+        // 清理用户信息
+        UserContext.removeUser();
+    }
+}

+ 31 - 0
tj-auth/tj-auth-resource-sdk/src/main/resources/META-INF/spring-configuration-metadata.json

@@ -0,0 +1,31 @@
+{
+  "groups": [
+    {
+      "name": "tj.auth.resource",
+      "type": "com.tianji.authsdk.resource.config.ResourceInterceptorConfiguration",
+      "sourceType": "com.tianji.authsdk.resource.config.ResourceAuthProperties"
+    }
+  ],
+  "properties": [
+    {
+      "name": "tj.auth.resource.enable",
+      "type": "java.lang.Boolean",
+      "description": "是否开启登录拦截功能,如果开启则需要指定拦截路径,默认拦截所有",
+      "sourceType": "com.tianji.authsdk.resource.config.ResourceAuthProperties",
+      "defaultValue": false
+    },
+    {
+      "name": "tj.auth.resource.includeLoginPaths",
+      "type": "java.util.List",
+      "description": "要拦截的路径,例如:/user/**",
+      "sourceType": "com.tianji.authsdk.resource.config.ResourceAuthProperties"
+    },
+    {
+      "name": "tj.auth.resource.excludeLoginPaths",
+      "type": "java.util.List",
+      "description": "不拦截的路径,例如:/user/**",
+      "sourceType": "com.tianji.authsdk.resource.config.ResourceAuthProperties"
+    }
+  ],
+  "hints": []
+}

+ 3 - 0
tj-auth/tj-auth-resource-sdk/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,3 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  com.tianji.authsdk.resource.config.ResourceInterceptorConfiguration, \
+  com.tianji.authsdk.resource.config.FeignRelayUserAutoConfiguration

+ 92 - 0
tj-auth/tj-auth-service/pom.xml

@@ -0,0 +1,92 @@
+<?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>tj-auth</artifactId>
+        <groupId>com.tianji</groupId>
+        <version>1.0.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tj-auth-service</artifactId>
+
+    <properties>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>com.tianji</groupId>
+            <artifactId>tj-api</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+        <!--Redis-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <!--redisson-->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson</artifactId>
+        </dependency>
+        <!--commons-pool2-->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+        </dependency>
+        <!--discovery-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+        </dependency>
+        <!--config-->
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>
+        <!--loadbalancer-->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+        </dependency>
+        <!--sdk-->
+        <dependency>
+            <groupId>com.tianji</groupId>
+            <artifactId>tj-auth-resource-sdk</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+    </dependencies>
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>build-info</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <mainClass>com.tianji.auth.AuthApplication</mainClass>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 41 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/AuthApplication.java

@@ -0,0 +1,41 @@
+package com.tianji.auth;
+
+import lombok.extern.slf4j.Slf4j;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.core.env.Environment;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+@MapperScan("com.tianji.auth.mapper")
+@SpringBootApplication
+@EnableScheduling
+@Slf4j
+public class AuthApplication {
+    public static void main(String[] args) throws UnknownHostException {
+        SpringApplication app = new SpringApplicationBuilder(AuthApplication.class).build(args);
+        Environment env = app.run(args).getEnvironment();
+        String protocol = "http";
+        if (env.getProperty("server.ssl.key-store") != null) {
+            protocol = "https";
+        }
+        log.info("--/\n---------------------------------------------------------------------------------------\n\t" +
+                        "Application '{}' is running! Access URLs:\n\t" +
+                        "Local: \t\t{}://localhost:{}\n\t" +
+                        "External: \t{}://{}:{}\n\t" +
+                        "Profile(s): \t{}" +
+                        "\n---------------------------------------------------------------------------------------",
+                env.getProperty("spring.application.name"),
+                protocol,
+                env.getProperty("server.port"),
+                protocol,
+                InetAddress.getLocalHost().getHostAddress(),
+                env.getProperty("server.port"),
+                env.getActiveProfiles());
+        log.info("--/\n------------------------------------------------------------------------------\n\t" );
+    }
+}

+ 41 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/config/AuthConfig.java

@@ -0,0 +1,41 @@
+package com.tianji.auth.config;
+
+import org.apache.tomcat.util.http.LegacyCookieProcessor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer;
+import org.springframework.cloud.bootstrap.encrypt.KeyProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
+
+import java.security.KeyPair;
+
+@Configuration
+@EnableConfigurationProperties(KeyProperties.class)
+public class AuthConfig {
+
+    @Bean
+    @ConfigurationProperties(prefix = "encrypt")
+    public KeyProperties keyProperties(){
+        return new KeyProperties();
+    }
+
+    @Bean
+    public KeyPair keyPair(KeyProperties keyProperties){
+        // 获取秘钥工厂
+        KeyStoreKeyFactory keyStoreKeyFactory =
+                new KeyStoreKeyFactory(
+                        keyProperties.getKeyStore().getLocation(),
+                        keyProperties.getKeyStore().getPassword().toCharArray());
+        //读取钥匙对
+        return keyStoreKeyFactory.getKeyPair(
+                keyProperties.getKeyStore().getAlias(),
+                keyProperties.getKeyStore().getSecret().toCharArray());
+    }
+
+    @Bean
+    public TomcatContextCustomizer cookieTomcatContextCustomizer(){
+        return context -> context.setCookieProcessor(new LegacyCookieProcessor());
+    }
+}

+ 6 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/constants/AuthConstants.java

@@ -0,0 +1,6 @@
+package com.tianji.auth.constants;
+
+public abstract class AuthConstants {
+    /*管理员的角色ID*/
+    public static final Long ADMIN_ROLE_ID = 1L;
+}

+ 62 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/controller/AccountController.java

@@ -0,0 +1,62 @@
+package com.tianji.auth.controller;
+
+
+import com.tianji.api.dto.user.LoginFormDTO;
+import com.tianji.auth.common.constants.JwtConstants;
+import com.tianji.auth.service.IAccountService;
+import com.tianji.common.exceptions.BadRequestException;
+import com.tianji.common.utils.WebUtils;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 账户登录相关接口
+ */
+@RestController
+@RequestMapping("/accounts")
+@Api(tags = "账户管理")
+@RequiredArgsConstructor
+public class AccountController {
+
+    private final IAccountService accountService;
+
+    @ApiOperation("登录并获取token")
+    @PostMapping(value = "/login")
+    public String loginByPw(@RequestBody LoginFormDTO loginFormDTO) {
+        return accountService.login(loginFormDTO, false);
+    }
+
+    @ApiOperation("管理端登录并获取token")
+    @PostMapping(value = "/admin/login")
+    public String adminLoginByPw(@RequestBody LoginFormDTO loginFormDTO) {
+        return accountService.login(loginFormDTO, true);
+    }
+
+    @ApiOperation("退出登录")
+    @PostMapping(value = "/logout")
+    public void logout() {
+        accountService.logout();
+    }
+
+    @ApiOperation("刷新token")
+    @GetMapping(value = "/refresh")
+    public String refreshToken(
+            @CookieValue(value = JwtConstants.REFRESH_HEADER, required = false) String studentToken,
+            @CookieValue(value = JwtConstants.ADMIN_REFRESH_HEADER, required = false) String adminToken
+    ) {
+        if (studentToken == null && adminToken == null) {
+            throw new BadRequestException("登录超时");
+        }
+        String host = WebUtils.getHeader("origin");
+        if (host == null) {
+            throw new BadRequestException("登录超时");
+        }
+        String token = host.startsWith("www", 7) ? studentToken : adminToken;
+        if (token == null) {
+            throw new BadRequestException("登录超时");
+        }
+        return accountService.refreshToken(WebUtils.cookieBuilder().decode(token));
+    }
+}

+ 30 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/controller/JwkController.java

@@ -0,0 +1,30 @@
+package com.tianji.auth.controller;
+
+import cn.hutool.core.codec.Base64;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import springfox.documentation.annotations.ApiIgnore;
+
+import java.security.KeyPair;
+
+@RestController
+@RequestMapping("jwks")
+@ApiIgnore
+public class JwkController {
+
+    private final KeyPair keyPair;
+
+    @Autowired
+    public JwkController(KeyPair keyPair) {
+        this.keyPair = keyPair;
+    }
+
+    @GetMapping
+    public String getJwk(){
+        // TODO 可以加入clientId和clientSecret校验
+        // 获取公钥并转码
+        return Base64.encode(keyPair.getPublic().getEncoded());
+    }
+}

+ 152 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/controller/MenuController.java

@@ -0,0 +1,152 @@
+package com.tianji.auth.controller;
+
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.tianji.auth.domain.dto.MenuDTO;
+import com.tianji.auth.domain.po.Menu;
+import com.tianji.auth.domain.vo.MenuOptionVO;
+import com.tianji.auth.service.IMenuService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * <p>
+ * 权限表,包括菜单权限和访问路径权限 前端控制器
+ * </p>
+ *
+ * @author 虎哥
+ * @since 2022-06-16
+ */
+@RestController
+@RequestMapping("/menus")
+@Api(tags = "菜单管理")
+@RequiredArgsConstructor
+public class MenuController {
+
+    private final IMenuService menuService;
+
+    /**
+     * 根据父菜单id查询子菜单
+     * @param pid 父菜单id,如果给 0 就是查询1级菜单
+     * @return 菜单集合
+     */
+    @GetMapping("/parent/{pid}")
+    @ApiOperation("根据父菜单id查询子菜单")
+    public List<MenuOptionVO> listMenusByParent(
+            @ApiParam(value = "父菜单id", example = "0") @PathVariable("pid") Long pid){
+        // 1.根据父id查询
+        List<Menu> list = menuService.lambdaQuery().eq(Menu::getParentId, pid).list();
+        // 2.非空判断
+        if (CollectionUtil.isEmpty(list)) {
+            return Collections.emptyList();
+        }
+        // 3.数据转换
+        return list.stream().map(MenuOptionVO::new).collect(Collectors.toList());
+    }
+
+    @GetMapping("{id}")
+    @ApiOperation("根据id查询菜单")
+    public MenuOptionVO getMenuById(@ApiParam(value = "菜单id", example = "1") @PathVariable("id") Long id) {
+        Menu menu = menuService.getById(id);
+        if (menu == null) {
+            return null;
+        }
+        return new MenuOptionVO(menu);
+    }
+
+    /**
+     * 查询菜单,按照多级菜单组成树结构
+     * @return 菜单列表,组成树结构
+     */
+    @GetMapping
+    @ApiOperation("查询菜单,按照多级菜单组成树结构")
+    public List<MenuOptionVO> listMenuTree(){
+        // 1.查询所有菜单
+        List<Menu> menus = menuService.list();
+        return convert2MenuDTOs(menus);
+    }
+
+    private List<MenuOptionVO> convert2MenuDTOs(List<Menu> menus) {
+        if (CollectionUtil.isEmpty(menus)) {
+            return Collections.emptyList();
+        }
+        // 2.按照父菜单id分组
+        Map<Long, List<MenuOptionVO>> menuMap = menus.stream()
+                .map(MenuOptionVO::new)
+                .collect(Collectors.groupingBy(MenuOptionVO::getParentId));
+        // 3.组合
+        // 3.1.获取1级菜单
+        List<MenuOptionVO> parents = menuMap.get(0L);
+        // 3.2.获取2级菜单
+        for (MenuOptionVO parent : parents) {
+            List<MenuOptionVO> subMenus = menuMap.get(parent.getId());
+            subMenus.sort(Comparator.comparingInt(MenuOptionVO::getPriority));
+            parent.setSubMenus(subMenus);
+        }
+        // 3.3.排序
+        parents.sort(Comparator.comparingInt(MenuOptionVO::getPriority));
+        return parents;
+    }
+
+    /**
+     * 根据当前登录用户的权限查询菜单选项,按照多级菜单组成树结构
+     * @return 菜单列表,组成树结构
+     */
+    @GetMapping("me")
+    @ApiOperation("查询我的菜单,按照多级菜单组成树结构")
+    public List<MenuOptionVO> listMenuTreeByUser(){
+        // 1.查询所有菜单
+        List<Menu> menus = menuService.listMenuByUser();
+        return convert2MenuDTOs(menus);
+    }
+
+    @PostMapping
+    @ApiOperation("新增菜单")
+    public void saveMenu(@RequestBody MenuDTO menuDTO){
+        // 1.数据转换
+        Menu menu = new Menu(menuDTO);
+        // 2.保存
+        menuService.saveMenu(menu);
+    }
+
+    @PutMapping("{id}")
+    @ApiOperation("更新菜单")
+    public void updateMenu(
+            @RequestBody MenuDTO menuDTO,
+            @ApiParam(value = "菜单id", example = "1")@PathVariable("id") Long id) {
+        menuDTO.setId(id);
+        menuService.updateById(new Menu(menuDTO));
+    }
+
+    @DeleteMapping("{id}")
+    @ApiOperation("根据id删除菜单")
+    public void deleteMenu(
+            @ApiParam(value = "菜单id", example = "1") @PathVariable("id") Long id) {
+        menuService.deleteMenu(id);
+    }
+
+    @PostMapping("/role/{roleId}")
+    @ApiOperation("绑定角色与菜单权限")
+    public void bindRoleMenus(
+            @ApiParam(value = "角色id", example = "1") @PathVariable("roleId") Long roleId,
+            @ApiParam(value = "菜单id集合") List<Long> menuIds){
+        menuService.bindRoleMenus(roleId, menuIds);
+    }
+
+    @DeleteMapping("/role/{roleId}")
+    @ApiOperation("解除角色的菜单权限")
+    public void deleteRoleMenus(
+            @ApiParam(value = "角色id", example = "1") @PathVariable("roleId") Long roleId,
+            @ApiParam(value = "菜单id集合") List<Long> menuIds){
+        menuService.deleteRoleMenus(roleId, menuIds);
+    }
+}

+ 192 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/controller/PrivilegeController.java

@@ -0,0 +1,192 @@
+package com.tianji.auth.controller;
+
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.tianji.auth.domain.dto.PrivilegeDTO;
+import com.tianji.auth.domain.po.Privilege;
+import com.tianji.auth.domain.vo.PrivilegeOptionVO;
+import com.tianji.auth.service.IPrivilegeService;
+import com.tianji.common.domain.dto.PageDTO;
+import com.tianji.common.domain.query.PageQuery;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * <p>
+ * 权限表,包括菜单权限和访问路径权限 前端控制器
+ * </p>
+ *
+ * @author 虎哥
+ * @since 2022-06-15
+ */
+@RestController
+@RequestMapping("/privileges")
+@Api(tags = "权限管理接口")
+@RequiredArgsConstructor
+public class PrivilegeController {
+
+    private final IPrivilegeService privilegesService;
+
+    /**
+     * 分页查询所有权限
+     *
+     * @param pageQuery 分页查询条件
+     * @return 分页结果
+     */
+    @ApiOperation("分页查询所有权限")
+    @GetMapping
+    public PageDTO<PrivilegeDTO> listAllPrivileges(PageQuery pageQuery) {
+        // 1.分页查询
+        Page<Privilege> page = privilegesService.listPrivilegesByPage(pageQuery);
+        // 2.非空判断
+        List<Privilege> list = page.getRecords();
+        if (CollectionUtil.isEmpty(list)) {
+            // 结果为空,返回空结果 添加总页码数
+            return new PageDTO<>(page.getTotal(), page.getPages(), Collections.emptyList());
+        }
+        // 3.数据转换
+        List<PrivilegeDTO> dtoList = list.stream().map(Privilege::toDTO).collect(Collectors.toList());
+        // 4.封装返回
+        return new PageDTO<>(page.getTotal(), page.getPages(), dtoList);
+    }
+
+    /**
+     * 查询所有权限,作为下拉选框菜单
+     *
+     * @return 分页结果
+     */
+    @ApiOperation("查询菜单下的所有权限,作为下拉选框菜单")
+    @GetMapping("options/{menuId}")
+    public List<PrivilegeOptionVO> listAllPrivilegesOptionsByMenuId(
+            @ApiParam(value = "菜单id", example = "1") @PathVariable("menuId") Long menuId
+    ) {
+        // 1.查询菜单下的权限
+        List<Privilege> list = privilegesService.lambdaQuery()
+                .eq(Privilege::getMenuId, menuId)
+                .eq(Privilege::getInternal, false)
+                .list();
+        // 2.非空判断
+        if (CollectionUtil.isEmpty(list)) {
+            // 结果为空,返回空结果
+            return Collections.emptyList();
+        }
+        // 3.数据转换
+        return list.stream()
+                .map(PrivilegeOptionVO::new).collect(Collectors.toList());
+    }
+
+    /**
+     * 查询某个角色的权限
+     *
+     * @return 某个角色的权限列表
+     */
+    @ApiOperation("查询菜单下的权限列表,某个角色的权限")
+    @GetMapping("/roles/{roleId}/{menuId}")
+    public List<PrivilegeOptionVO> listPrivilegeByRoleId(
+            @ApiParam(value = "角色id", required = true, example = "1") @PathVariable("roleId") Long roleId,
+            @ApiParam(value = "菜单id", required = true, example = "1") @PathVariable("menuId") Long menuId
+    ) {
+        // 1.查询角色对应的权限id
+        Set<Long> privilegeIds = privilegesService.listPrivilegeByRoleId(roleId);
+        if (CollectionUtil.isEmpty(privilegeIds)) {
+            return Collections.emptyList();
+        }
+        // 2.查询菜单下所有权限
+        List<PrivilegeOptionVO> vos = listAllPrivilegesOptionsByMenuId(menuId);
+        // 3.标记
+        for (PrivilegeOptionVO vo : vos) {
+            vo.setChecked(privilegeIds.contains(vo.getId()));
+        }
+        return vos;
+    }
+
+    /**
+     * 新增权限
+     *
+     * @param privilegeDTO 权限数据
+     * @return 新增成功的权限数据
+     */
+    @ApiOperation("新增权限")
+    @PostMapping
+    public PrivilegeDTO savePrivilege(@Validated @RequestBody PrivilegeDTO privilegeDTO) {
+        // 域对象转换
+        Privilege privilege = new Privilege(privilegeDTO);
+        // 新增
+        privilegesService.savePrivilege(privilege);
+        // 返回
+        return privilege.toDTO();
+    }
+
+    /**
+     * 修改权限
+     *
+     * @param privilegeDTO 权限数据
+     * @param id           要修改的权限的id
+     * @return 修改后的权限结果
+     */
+    @ApiOperation("修改权限")
+    @PutMapping("{id}")
+    public PrivilegeDTO updatePrivilege(
+            @RequestBody PrivilegeDTO privilegeDTO,
+            @ApiParam(value = "要修改的权限id", required = true, example = "1") @PathVariable("id") Long id) {
+        // 域对象转换
+        Privilege privilege = new Privilege(privilegeDTO);
+        privilege.setId(id);
+        // 修改
+        privilegesService.updateById(privilege);
+        // 返回
+        return privilege.toDTO();
+    }
+
+    /**
+     * 删除权限
+     *
+     * @param id 权限id
+     */
+    @ApiOperation("删除权限")
+    @DeleteMapping("{id}")
+    public void removePrivilegeById(
+            @ApiParam(value = "要删除的权限id", required = true, example = "1") @PathVariable("id") Long id) {
+        privilegesService.removePrivilegeById(id);
+    }
+
+    /**
+     * 绑定角色与API权限
+     *
+     * @param roleId       角色id
+     * @param privilegeIds 权限id集合
+     */
+    @PostMapping("/role/{roleId}")
+    @ApiOperation("绑定角色与API权限")
+    public void bindRolePrivileges(
+            @ApiParam(value = "角色id", example = "1") @PathVariable("roleId") Long roleId,
+            @ApiParam(value = "API权限的id集合") List<Long> privilegeIds) {
+        privilegesService.bindRolePrivileges(roleId, privilegeIds);
+    }
+
+    /**
+     * 解除角色的API权限
+     *
+     * @param roleId       角色id
+     * @param privilegeIds 权限id集合
+     */
+    @DeleteMapping("/role/{roleId}")
+    @ApiOperation("解除角色的API权限")
+    public void deleteRolePrivileges(
+            @ApiParam(value = "角色id", example = "1") @PathVariable("roleId") Long roleId,
+            @ApiParam(value = "API权限的id集合") List<Long> privilegeIds) {
+        privilegesService.deleteRolePrivileges(roleId, privilegeIds);
+    }
+
+
+}

+ 99 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/controller/RoleController.java

@@ -0,0 +1,99 @@
+package com.tianji.auth.controller;
+
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.tianji.api.dto.auth.RoleDTO;
+import com.tianji.auth.domain.po.Role;
+import com.tianji.auth.service.IRoleService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+
+/**
+ * @author 虎哥
+ * @since 2022-06-16
+ */
+@Api(tags = "角色管理")
+@RestController
+@RequestMapping("/roles")
+@RequiredArgsConstructor
+public class RoleController {
+
+    private final IRoleService roleService;
+
+    @ApiOperation("查询员工角色列表")
+    @GetMapping("/list")
+    public List<RoleDTO> listAllRoles(){
+        // 1.查询
+        List<Role> list = roleService.list();
+        if (CollectionUtil.isEmpty(list)) {
+            return Collections.emptyList();
+        }
+        // 3.数据转换
+        return list.stream().map(Role::toDTO).collect(Collectors.toList());
+    }
+
+    @ApiOperation("查询员工角色列表")
+    @GetMapping
+    public List<RoleDTO> listStaffRoles(){
+        // 1.查询
+        List<Role> list = roleService.lambdaQuery().eq(Role::getType, Role.RoleType.CUSTOM).list();
+        if (CollectionUtil.isEmpty(list)) {
+            return Collections.emptyList();
+        }
+        // 3.数据转换
+        return list.stream().map(Role::toDTO).collect(Collectors.toList());
+    }
+
+    @ApiOperation("根据id查询角色")
+    @GetMapping("/{id}")
+    public RoleDTO queryRoleById(@PathVariable("id") Long id){
+        // 1.查询
+        Role role = roleService.getById(id);
+        if (role == null) {
+            return null;
+        }
+        // 2.数据转换
+        return role.toDTO();
+    }
+
+
+
+    @ApiOperation("新增角色")
+    @PostMapping
+    public RoleDTO saveRole(@RequestBody RoleDTO roleDTO) {
+        Role role = new Role(roleDTO);
+        role.setType(Role.RoleType.CUSTOM);
+        // 1.新增
+        roleService.save(role);
+        // 2.返回
+        roleDTO.setId(role.getId());
+        return roleDTO;
+    }
+
+    @ApiOperation("修改角色信息")
+    @PutMapping("{id}")
+    public void updateRole(
+            @RequestBody RoleDTO roleDTO,
+            @ApiParam(value = "角色id", example = "1") @PathVariable("id") Long id
+    ) {
+        // 1.数据转换
+        Role role = new Role(roleDTO);
+        role.setId(id);
+        // 2.修改
+        roleService.updateById(role);
+    }
+
+    @ApiOperation("删除角色信息")
+    @DeleteMapping("{id}")
+    public void deleteRole(@ApiParam(value = "角色id", example = "1") @PathVariable("id") Long id) {
+        roleService.deleteRole(id);
+    }
+}

+ 27 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/dto/MenuDTO.java

@@ -0,0 +1,27 @@
+package com.tianji.auth.domain.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel(description = "菜单表单实体")
+public class MenuDTO {
+    @ApiModelProperty(value = "菜单id", example = "1")
+    private Long id;
+
+    @ApiModelProperty(value = "父菜单id", example = "0")
+    private Long parentId;
+
+    @ApiModelProperty(value = "菜单文本", example = "系统管理")
+    private String label;
+
+    @ApiModelProperty(value = "菜单路径", example = "/sys/index")
+    private String path;
+
+    @ApiModelProperty(value = "菜单图标", example = "el-icon-sys")
+    private String icon;
+
+    @ApiModelProperty(value = "菜单顺序", example = "1")
+    private Integer priority;
+}

+ 32 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/dto/PrivilegeDTO.java

@@ -0,0 +1,32 @@
+package com.tianji.auth.domain.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import java.io.Serializable;
+
+@Data
+@ApiModel(description = "API权限")
+public class PrivilegeDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "权限id", example = "1")
+    private Long id;
+    @ApiModelProperty(value = "权限所属菜单id", example = "1")
+    private Long menuId;
+    @ApiModelProperty(value = "权限说明", example = "新增员工")
+    @NotNull(message = "权限说明不能为空")
+    private String intro;
+    @ApiModelProperty(value = "API请求方式", example = "GET")
+    @Pattern(regexp = "^GET|POST|PUT|DELETE$", message = "请求方式必须是大写")
+    private String method;
+    @ApiModelProperty(value = "API请求路径", example = "/account/staff")
+    @NotNull(message = "uri不能为空")
+    private String uri;
+    @ApiModelProperty("是否是内部权限")
+    private Boolean internal;
+}

+ 41 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/po/AccountRole.java

@@ -0,0 +1,41 @@
+package com.tianji.auth.domain.po;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * 账户、角色关联表
+ * </p>
+ *
+ * @author 虎哥
+ * @since 2022-06-16
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("account_role")
+public class AccountRole implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 账户id
+     */
+    private Long accountId;
+
+    /**
+     * 角色id
+     */
+    private Long roleId;
+
+}

+ 72 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/po/LoginRecord.java

@@ -0,0 +1,72 @@
+package com.tianji.auth.domain.po;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 登录信息记录表
+ * </p>
+ *
+ * @author 虎哥
+ * @since 2022-07-12
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("login_record")
+public class LoginRecord implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 用户id
+     */
+    private Long userId;
+
+    /**
+     * 用户id
+     */
+    private String cellPhone;
+
+    /**
+     * 登录时间
+     */
+    private LocalDateTime loginTime;
+
+    /**
+     * 登出时间
+     */
+    private LocalDateTime logoutTime;
+
+    /**
+     * 登录日期
+     */
+    private LocalDate loginDate;
+
+    /**
+     * 登录时长,单位是秒
+     */
+    private Long duration;
+
+    /**
+     * ip地址
+     */
+    private String ipv4;
+
+
+}

+ 119 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/po/Menu.java

@@ -0,0 +1,119 @@
+package com.tianji.auth.domain.po;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.tianji.auth.domain.dto.MenuDTO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 权限表,包括菜单权限和访问路径权限
+ * </p>
+ *
+ * @author 虎哥
+ * @since 2022-07-12
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("menu")
+@NoArgsConstructor
+public class Menu implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId
+    private Long id;
+
+    /**
+     * 父菜单id,默认0代表没有父菜单
+     */
+    private Long parentId;
+
+    /**
+     * 是否有子菜单,默认false
+     */
+    private Boolean hasChildren;
+
+    /**
+     * 菜单文本
+     */
+    private String label;
+
+    /**
+     * 菜单路径
+     */
+    private String path;
+
+    /**
+     * 菜单图标
+     */
+    private String icon;
+
+    /**
+     * 顺序优先级,默认127
+     */
+    private Integer priority;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建者id
+     */
+
+    private Long creater;
+
+    /**
+     * 更新者id
+     */
+
+    private Long updater;
+
+    /**
+     * 部门id
+     */
+    private Long depId;
+
+    /**
+     * 逻辑删除,默认0
+     */
+    private Integer deleted;
+
+
+    public Menu(MenuDTO dto) {
+        this.id = dto.getId();
+        this.parentId = dto.getParentId();
+        this.label = dto.getLabel();
+        this.path = dto.getPath();
+        this.icon = dto.getIcon();
+        this.priority = dto.getPriority();
+    }
+
+    public MenuDTO toDTO(){
+        MenuDTO dto = new MenuDTO();
+        dto.setId(id);
+        dto.setPath(path);
+        dto.setParentId(parentId);
+        dto.setLabel(label);
+        dto.setIcon(icon);
+        dto.setPriority(priority);
+        return dto;
+    }
+}

+ 116 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/po/Privilege.java

@@ -0,0 +1,116 @@
+package com.tianji.auth.domain.po;
+
+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.tianji.auth.domain.dto.PrivilegeDTO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 权限表,包括菜单权限和访问路径权限
+ * </p>
+ *
+ * @author 虎哥
+ * @since 2022-07-12
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("`privilege`")
+public class Privilege implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId
+    private Long id;
+
+    /**
+     * 菜单id
+     */
+    private Long menuId;
+
+    /**
+     * 说明
+     */
+    private String intro;
+
+    /**
+     * API权限的请求方式
+     */
+    private String method;
+
+    /**
+     * API权限的请求路径
+     */
+    private String uri;
+
+    /**
+     * 是否是内部接口
+     */
+    private Boolean internal;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建者id
+     */
+
+    private Long creater;
+
+    /**
+     * 更新者id
+     */
+
+    private Long updater;
+
+    /**
+     * 部门id
+     */
+    private Long depId;
+
+    /**
+     * 逻辑删除,默认0
+     */
+    private Integer deleted;
+
+    public Privilege() {
+    }
+
+    public Privilege(PrivilegeDTO dto) {
+        this.id = dto.getId();
+        this.menuId = dto.getMenuId();
+        this.intro = dto.getIntro();
+        this.method = dto.getMethod();
+        this.uri = dto.getUri();
+        this.internal = dto.getInternal();
+    }
+
+    public PrivilegeDTO toDTO(){
+        PrivilegeDTO dto = new PrivilegeDTO();
+        dto.setId(id);
+        dto.setMenuId(menuId);
+        dto.setIntro(intro);
+        dto.setMethod(method);
+        dto.setUri(uri);
+        dto.setInternal(internal);
+        return dto;
+    }
+}

+ 111 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/po/Role.java

@@ -0,0 +1,111 @@
+package com.tianji.auth.domain.po;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.tianji.api.dto.auth.RoleDTO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 角色表
+ * </p>
+ *
+ * @author 虎哥
+ * @since 2022-07-12
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("role")
+@NoArgsConstructor
+public class Role implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId
+    private Long id;
+
+    /**
+     * 角色代号,例如:admin
+     */
+    private String code;
+
+    /**
+     * 角色名称
+     */
+    private String name;
+
+    /**
+     * 角色类型:0-固定角色(不可选)1-自定义角色
+     */
+    private RoleType type;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建者id
+     */
+
+    private Long creater;
+
+    /**
+     * 更新者id
+     */
+
+    private Long updater;
+    /**
+     * 部门id
+     */
+    private Long depId;
+
+    /**
+     * 逻辑删除,默认0
+     */
+    private Integer deleted;
+
+    public Role(RoleDTO dto) {
+        this.id = dto.getId();
+        this.code = dto.getCode();
+        this.name = dto.getName();
+    }
+
+    public RoleDTO toDTO(){
+        RoleDTO dto = new RoleDTO();
+        dto.setId(id);
+        dto.setCode(code);
+        dto.setName(name);
+        return dto;
+    }
+
+    @Getter
+    public enum RoleType{
+        CONSTANT(0, "固定角色"),
+        CUSTOM(1, "自定义角色"),
+        ;
+        @EnumValue
+        int value;
+        String desc;
+
+        RoleType(int value, String desc) {
+            this.value = value;
+            this.desc = desc;
+        }
+    }
+}

+ 47 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/po/RoleMenu.java

@@ -0,0 +1,47 @@
+package com.tianji.auth.domain.po;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * 账户、角色关联表
+ * </p>
+ *
+ * @author 虎哥
+ * @since 2022-06-16
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("role_menu")
+@NoArgsConstructor
+public class RoleMenu implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 角色id
+     */
+    private Long roleId;
+
+    /**
+     * 菜单id
+     */
+    private Long menuId;
+
+    public RoleMenu(Long roleId, Long menuId) {
+        this.roleId = roleId;
+        this.menuId = menuId;
+    }
+}

+ 48 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/po/RolePrivilege.java

@@ -0,0 +1,48 @@
+package com.tianji.auth.domain.po;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * 账户、角色关联表
+ * </p>
+ *
+ * @author 虎哥
+ * @since 2022-06-16
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("role_privilege")
+@NoArgsConstructor
+public class RolePrivilege implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 角色id
+     */
+    private Long roleId;
+
+    /**
+     * 权限id
+     */
+    private Long privilegeId;
+
+
+    public RolePrivilege(Long roleId, Long privilegeId) {
+        this.roleId = roleId;
+        this.privilegeId = privilegeId;
+    }
+}

+ 39 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/vo/LoginRecordVO.java

@@ -0,0 +1,39 @@
+package com.tianji.auth.domain.vo;
+
+import lombok.Data;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Data
+public class LoginRecordVO {
+    /**
+     * 用户id
+     */
+    private Long userId;
+
+    /**
+     * 登录时间
+     */
+    private LocalDateTime loginTime;
+
+    /**
+     * 登出时间
+     */
+    private LocalDateTime logoutTime;
+
+    /**
+     * 登录日期
+     */
+    private LocalDate loginDate;
+
+    /**
+     * 登录时长,单位是秒
+     */
+    private Long duration;
+
+    /**
+     * ip地址
+     */
+    private String ipv4;
+}

+ 45 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/vo/MenuOptionVO.java

@@ -0,0 +1,45 @@
+package com.tianji.auth.domain.vo;
+
+import com.tianji.auth.domain.po.Menu;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@ApiModel(description = "菜单选项实体")
+public class MenuOptionVO {
+    @ApiModelProperty(value = "菜单id", example = "1")
+    private Long id;
+
+    @ApiModelProperty(value = "父菜单id", example = "0")
+    private Long parentId;
+
+    @ApiModelProperty(value = "菜单文本", example = "系统管理")
+    private String label;
+
+    @ApiModelProperty(value = "菜单图标", example = "el-icon-sys")
+    private String icon;
+
+    @ApiModelProperty(value = "是否有子菜单", example = "false")
+    private Boolean hasChildren;
+
+    @ApiModelProperty(value = "菜单顺序", example = "1")
+    private Integer priority;
+
+    @ApiModelProperty(value = "子菜单集合")
+    private List<MenuOptionVO> subMenus;
+
+    public MenuOptionVO() {
+    }
+
+    public MenuOptionVO(Menu menu) {
+        this.id = menu.getId();
+        this.parentId = menu.getParentId();
+        this.label = menu.getLabel();
+        this.icon = menu.getIcon();
+        this.hasChildren = menu.getHasChildren();
+        this.priority = menu.getPriority();
+    }
+}

+ 30 - 0
tj-auth/tj-auth-service/src/main/java/com/tianji/auth/domain/vo/PrivilegeOptionVO.java

@@ -0,0 +1,30 @@
+package com.tianji.auth.domain.vo;
+
+import com.tianji.auth.domain.po.Privilege;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+@ApiModel(description = "API权限选项实体")
+public class PrivilegeOptionVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "权限id", example = "1")
+    private Long id;
+    @ApiModelProperty(value = "权限说明", example = "新增员工")
+    private String intro;
+    @ApiModelProperty(value = "是否选中", example = "true")
+    private Boolean checked;
+
+    public PrivilegeOptionVO() {
+    }
+
+    public PrivilegeOptionVO(Privilege privilege) {
+        this.id = privilege.getId();
+        this.intro = privilege.getIntro();
+    }
+}

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels