Bläddra i källkod

初始化CMS服务项目

liyan 2 dagar sedan
incheckning
bd78100c99
100 ändrade filer med 6784 tillägg och 0 borttagningar
  1. 52 0
      .gitignore
  2. 20 0
      LICENSE
  3. 97 0
      README.md
  4. 11 0
      build-logic/java-conventions/build.gradle
  5. 41 0
      build-logic/java-conventions/src/main/groovy/com.ruoyi.java-conventions.gradle
  6. 9 0
      build-logic/java-library/build.gradle
  7. 16 0
      build-logic/java-library/src/main/groovy/com.ruoyi.java-library.gradle
  8. 11 0
      build-logic/kotlin-library/build.gradle
  9. 10 0
      build-logic/kotlin-library/src/main/groovy/com.ruoyi.kotlin-library.gradle
  10. 14 0
      build-logic/settings.gradle
  11. 11 0
      build-logic/spring-boot-application/build.gradle
  12. 25 0
      build-logic/spring-boot-application/src/main/groovy/com.ruoyi.spring-boot-application.gradle
  13. 15 0
      build.gradle
  14. 11 0
      gradle.properties
  15. BIN
      gradle/wrapper/gradle-wrapper.jar
  16. 5 0
      gradle/wrapper/gradle-wrapper.properties
  17. 240 0
      gradlew
  18. 91 0
      gradlew.bat
  19. 15 0
      platforms/plugins-platform/build.gradle
  20. 42 0
      platforms/product-platform/build.gradle
  21. 5 0
      platforms/settings.gradle
  22. 12 0
      platforms/test-platform/build.gradle
  23. 26 0
      ruoyi-admin/build.gradle
  24. 31 0
      ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java
  25. 18 0
      ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java
  26. 17 0
      ruoyi-admin/src/main/java/com/ruoyi/ServiceConfigure.java
  27. 94 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java
  28. 163 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java
  29. 167 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/file/FileManagerController.java
  30. 120 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java
  31. 27 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java
  32. 82 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java
  33. 69 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java
  34. 92 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java
  35. 134 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java
  36. 132 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java
  37. 121 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java
  38. 132 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java
  39. 29 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java
  40. 86 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
  41. 142 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java
  42. 91 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java
  43. 130 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java
  44. 144 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java
  45. 38 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java
  46. 263 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java
  47. 252 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java
  48. 24 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/SwaggerController.java
  49. 183 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java
  50. 125 0
      ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java
  51. 1 0
      ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties
  52. 57 0
      ruoyi-admin/src/main/resources/application-druid.yml
  53. 80 0
      ruoyi-admin/src/main/resources/application-prod.yml
  54. 147 0
      ruoyi-admin/src/main/resources/application.yml
  55. 24 0
      ruoyi-admin/src/main/resources/banner.txt
  56. 37 0
      ruoyi-admin/src/main/resources/i18n/messages.properties
  57. 93 0
      ruoyi-admin/src/main/resources/logback.xml
  58. 20 0
      ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml
  59. 16 0
      ruoyi-cms/build.gradle
  60. 131 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/controller/admin/CmsPostController.kt
  61. 91 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/controller/admin/CmsTermController.kt
  62. 79 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/controller/admin/CmsTermRelationshipsController.kt
  63. 180 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/controller/wxapp/CmsController.kt
  64. 47 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CMSBaseEntity.kt
  65. 17 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CmsConfig.kt
  66. 100 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CmsPost.kt
  67. 40 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CmsPostMeta.kt
  68. 21 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CmsReviewForm.kt
  69. 84 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CmsTerm.kt
  70. 40 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CmsTermMeta.kt
  71. 31 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CmsTermRelationships.kt
  72. 7 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/mapper/CmsConfigMapper.kt
  73. 32 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/mapper/CmsPostMapper.kt
  74. 16 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/mapper/CmsPostMetaMapper.kt
  75. 13 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/mapper/CmsTermMapper.kt
  76. 7 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/mapper/CmsTermMetaMapper.kt
  77. 26 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/mapper/CmsTermRelationshipsMapper.kt
  78. 4 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/service/CmsConfigService.kt
  79. 30 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/service/CmsPostMetaService.kt
  80. 395 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/service/CmsPostService.kt
  81. 30 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/service/CmsTermMetaService.kt
  82. 131 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/service/CmsTermRelationshipsService.kt
  83. 285 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/service/CmsTermService.kt
  84. 196 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/service/FileManagerService.kt
  85. 7 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/util/CmsConstant.kt
  86. 22 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/util/EntityPathVariable.kt
  87. 44 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/vo/CmsPostFormBase.kt
  88. 11 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/vo/CmsPostFormReq.kt
  89. 27 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/vo/CmsPostFormRes.kt
  90. 31 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/vo/CmsTermForm.kt
  91. 50 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/vo/FileInfo.kt
  92. 18 0
      ruoyi-cms/src/main/kotlin/com/cclotus/cms/vo/PageForm.kt
  93. 121 0
      ruoyi-cms/src/main/resources/mapper/cms/CmsPostMapper.xml
  94. 31 0
      ruoyi-cms/src/main/resources/mapper/cms/CmsPostMetaMapper.xml
  95. 48 0
      ruoyi-cms/src/main/resources/mapper/cms/CmsTermMapper.xml
  96. 95 0
      ruoyi-cms/src/main/resources/mapper/cms/CmsTermRelationshipsMapper.xml
  97. 34 0
      ruoyi-common/build.gradle
  98. 19 0
      ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java
  99. 33 0
      ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java
  100. 0 0
      ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java

+ 52 - 0
.gitignore

@@ -0,0 +1,52 @@
+######################################################################
+# Build Tools
+
+.gradle
+**/build/
+!gradle/wrapper/gradle-wrapper.jar
+
+target/
+!.mvn/wrapper/maven-wrapper.jar
+
+/dist/
+**/node_modules/
+**/package-lock.json
+
+######################################################################
+# IDE
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### JRebel ###
+rebel.xml
+
+### NetBeans ###
+nbproject/private/
+build/*
+nbbuild/
+dist/
+nbdist/
+.nb-gradle/
+
+######################################################################
+# Others
+*.log
+*.xml.versionsBackup
+*.swp
+
+!*/build/*.java
+!*/build/*.html
+!*/build/*.xml
+.sync

+ 20 - 0
LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2018 RuoYi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 97 - 0
README.md


+ 11 - 0
build-logic/java-conventions/build.gradle

@@ -0,0 +1,11 @@
+plugins {
+    id('groovy-gradle-plugin')
+}
+
+repositories {
+    // 阿里云镜像
+    maven { url = uri("https://maven.aliyun.com/repository/public") }
+    maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
+    maven { url = uri("https://maven.aliyun.com/repository/spring") }
+    maven { url = uri("https://maven.aliyun.com/repository/spring-plugin") }
+}

+ 41 - 0
build-logic/java-conventions/src/main/groovy/com.ruoyi.java-conventions.gradle

@@ -0,0 +1,41 @@
+plugins {
+    id('java')
+}
+
+java {
+    sourceCompatibility = JavaVersion.VERSION_1_8
+    targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+var lombokVersion = '1.18.24'
+dependencies {
+    implementation(platform('com.ruoyi.platform:product-platform'))
+    compileOnly "org.projectlombok:lombok:${lombokVersion}"
+    annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
+
+    testCompileOnly "org.projectlombok:lombok:${lombokVersion}"
+    testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}"
+    testImplementation(platform('com.ruoyi.platform:test-platform'))
+    testImplementation('org.junit.jupiter:junit-jupiter')
+}
+
+repositories {
+    mavenLocal()
+    // 阿里云镜像
+    maven { url = uri("https://maven.aliyun.com/repository/public") }
+    maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
+    maven { url = uri("https://maven.aliyun.com/repository/spring") }
+    maven { url = uri("https://maven.aliyun.com/repository/spring-plugin") }
+
+    maven {
+        url = uri('https://repo.maven.apache.org/maven2/')
+    }
+}
+
+tasks.withType(JavaCompile) {
+    options.encoding = 'UTF-8'
+}
+
+tasks.named("test") {
+    useJUnitPlatform()
+}

+ 9 - 0
build-logic/java-library/build.gradle

@@ -0,0 +1,9 @@
+plugins {
+    id('groovy-gradle-plugin')
+}
+
+dependencies {
+    implementation(platform('com.ruoyi.platform:plugins-platform'))
+
+    implementation(project(':java-conventions'))
+}

+ 16 - 0
build-logic/java-library/src/main/groovy/com.ruoyi.java-library.gradle

@@ -0,0 +1,16 @@
+plugins {
+    id('com.ruoyi.java-conventions')
+    id('java-library')
+    id 'maven-publish'
+}
+
+group = 'com.ruoyi'
+version = System.properties['buildVersion']
+
+//publishing {
+//    publications {
+//        maven(MavenPublication) {
+//            from(components.java)
+//        }
+//    }
+//}

+ 11 - 0
build-logic/kotlin-library/build.gradle

@@ -0,0 +1,11 @@
+plugins {
+    id('groovy-gradle-plugin')
+}
+
+dependencies {
+    implementation(platform('com.ruoyi.platform:plugins-platform'))
+
+    implementation(project(':java-conventions'))
+    implementation('org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin')
+    implementation('org.jetbrains.kotlin:kotlin-allopen')
+}

+ 10 - 0
build-logic/kotlin-library/src/main/groovy/com.ruoyi.kotlin-library.gradle

@@ -0,0 +1,10 @@
+plugins {
+    id('com.ruoyi.java-conventions')
+    id('org.jetbrains.kotlin.jvm')
+    id('org.jetbrains.kotlin.plugin.spring')
+    id('java-library')
+}
+
+dependencies {
+    implementation('org.jetbrains.kotlin:kotlin-stdlib')
+}

+ 14 - 0
build-logic/settings.gradle

@@ -0,0 +1,14 @@
+dependencyResolutionManagement {
+    repositories {
+        maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
+
+        gradlePluginPortal()
+    }
+}
+includeBuild('../platforms')
+
+rootProject.name = 'build-logic'
+include('java-conventions')
+include('java-library')
+include('kotlin-library')
+include('spring-boot-application')

+ 11 - 0
build-logic/spring-boot-application/build.gradle

@@ -0,0 +1,11 @@
+plugins {
+    id('groovy-gradle-plugin') // <1>
+}
+
+dependencies {
+    implementation(platform('com.ruoyi.platform:plugins-platform')) // <2>
+
+    implementation(project(':java-conventions')) // <3>
+
+    implementation('org.springframework.boot:org.springframework.boot.gradle.plugin')  // <4>
+}

+ 25 - 0
build-logic/spring-boot-application/src/main/groovy/com.ruoyi.spring-boot-application.gradle

@@ -0,0 +1,25 @@
+plugins {
+    id('com.ruoyi.java-conventions')
+    id('org.springframework.boot')
+}
+
+dependencies {
+    // implementation('org.springframework.boot:spring-boot-starter-web')
+    // implementation('org.springframework.boot:spring-boot-starter-thymeleaf')
+}
+
+group = 'com.ruoyi'
+version = System.properties['buildVersion']
+
+tasks.named("bootJar") {
+    archiveClassifier = 'boot'
+    archiveFileName = "${project.archivesBaseName}.jar"
+}
+
+tasks.named("jar") {
+    archiveClassifier = ''
+    exclude {
+        it.name.endsWith("Application.class") || it.name.endsWith("ApplicationKt.class")
+    }
+    exclude('application*.yml', 'application*.properties')
+}

+ 15 - 0
build.gradle

@@ -0,0 +1,15 @@
+// This is an example of a lifecycle task that crosses build boundaries defined in the umbrella build.
+
+tasks.register('info') {
+    doLast {
+        def buildVersion = System.properties['buildVersion']
+        println "RuoYi-Vue $buildVersion"
+    }
+}
+
+tasks.register('dist', Copy) {
+    into("$rootDir/dist")
+    project(':ruoyi-admin') {
+        from(tasks['bootJar'])
+    }
+}

+ 11 - 0
gradle.properties

@@ -0,0 +1,11 @@
+# local repo config
+systemProp.repoPath = /var/repo
+
+# custom build config, would replace by environment variable
+systemProp.buildVersion = 3.8.3
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+org.gradle.jvmargs=-Xmx1024m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+org.gradle.parallel=true

BIN
gradle/wrapper/gradle-wrapper.jar


+ 5 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 240 - 0
gradlew

@@ -0,0 +1,240 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+# Collect all arguments for the java command;
+#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+#     shell script including quotes and variable substitutions, so put them in
+#     double quotes to make sure that they get re-expanded; and
+#   * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"

+ 91 - 0
gradlew.bat

@@ -0,0 +1,91 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 15 - 0
platforms/plugins-platform/build.gradle

@@ -0,0 +1,15 @@
+plugins {
+    id('java-platform')
+}
+
+group = 'com.ruoyi.platform'
+
+var kotlin_version = '1.7.20'
+
+dependencies {
+    constraints {
+        api("org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin:$kotlin_version")
+        api("org.jetbrains.kotlin:kotlin-allopen:$kotlin_version")
+        api('org.springframework.boot:org.springframework.boot.gradle.plugin:2.7.3')
+    }
+}

+ 42 - 0
platforms/product-platform/build.gradle

@@ -0,0 +1,42 @@
+plugins {
+    id('java-platform')
+}
+
+group = 'com.ruoyi.platform'
+
+// allow the definition of dependencies to other platforms like the Spring Boot BOM
+javaPlatform.allowDependencies()
+
+dependencies {
+    api(platform('org.springframework.boot:spring-boot-dependencies:2.7.3'))
+    api(platform('org.springframework.cloud:spring-cloud-dependencies:2021.0.4'))
+//    api(platform('com.alibaba.cloud:spring-cloud-alibaba-dependencies:2021.0.1.0'))
+
+    constraints {
+        api('com.alibaba:druid-spring-boot-starter:1.2.11')
+        api('com.alibaba.fastjson2:fastjson2:2.0.12')
+        api('com.github.oshi:oshi-core:6.2.2')
+        api('com.github.pagehelper:pagehelper-spring-boot-starter:1.4.3')
+        api('com.github.penggle:kaptcha:2.3.2')
+        api('commons-io:commons-io:2.11.0')
+        api('commons-fileupload:commons-fileupload:1.4')
+        api('commons-collections:commons-collections:3.2.2')
+        api('org.apache.poi:poi-ooxml:4.1.2')
+        api('org.apache.velocity:velocity-engine-core:2.3')
+        api('eu.bitwalker:UserAgentUtils:1.21')
+        api('io.jsonwebtoken:jjwt:0.9.1')
+        api('io.springfox:springfox-swagger2:3.0.0')
+        api('io.springfox:springfox-swagger-ui:3.0.0')
+        api('io.springfox:springfox-boot-starter:3.0.0')
+        api('io.swagger:swagger-models:1.6.2')
+        api('javax.xml.bind:jaxb-api:2.3.1')
+        api("io.jsonwebtoken:jjwt-api:0.10.5")
+        api("io.jsonwebtoken:jjwt-impl:0.10.5")
+        api("io.jsonwebtoken:jjwt-jackson:0.10.5")
+        api("com.baomidou:mybatis-plus-boot-starter:3.5.2")
+        api("net.glxn:qrgen:1.4")
+        api("org.apache.commons:commons-imaging:1.0-alpha1")
+
+        api('mysql:mysql-connector-java:8.0.29')
+    }
+}

+ 5 - 0
platforms/settings.gradle

@@ -0,0 +1,5 @@
+rootProject.name = 'platforms'
+
+include('product-platform')
+include('test-platform')
+include('plugins-platform')

+ 12 - 0
platforms/test-platform/build.gradle

@@ -0,0 +1,12 @@
+plugins {
+    id('java-platform')
+}
+
+group = 'com.ruoyi.platform'
+
+// allow the definition of dependencies to other platforms like the JUnit 5 BOM
+javaPlatform.allowDependencies()
+
+dependencies {
+    api(platform('org.junit:junit-bom:5.8.2'))
+}

+ 26 - 0
ruoyi-admin/build.gradle

@@ -0,0 +1,26 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ *
+ * This project uses @Incubating APIs which are subject to change.
+ */
+
+plugins {
+    id 'com.ruoyi.spring-boot-application'
+}
+
+dependencies {
+    implementation 'org.springframework.boot:spring-boot-devtools'
+    implementation 'io.springfox:springfox-boot-starter'
+    implementation 'io.swagger:swagger-models'
+    implementation 'ch.qos.logback:logback-core'
+    implementation 'ch.qos.logback:logback-classic'
+    implementation project(':ruoyi-framework')
+    implementation project(':ruoyi-quartz')
+    implementation project(':ruoyi-generator')
+    implementation project(':ruoyi-ext')
+    implementation project(':ruoyi-register')
+    implementation project(':ruoyi-cms')
+    runtimeOnly 'mysql:mysql-connector-java'
+}
+
+description = 'ruoyi-admin web服务入口'

+ 31 - 0
ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java

@@ -0,0 +1,31 @@
+package com.ruoyi;
+
+import cclotus.ruoyi.ext.RuoYiExtConfiguration;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 启动程序
+ *
+ * @author ruoyi
+ */
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
+@Import({RuoYiExtConfiguration.class})
+public class RuoYiApplication {
+    public static void main(String[] args) {
+        // System.setProperty("spring.devtools.restart.enabled", "false");
+        SpringApplication.run(RuoYiApplication.class, args);
+        System.out.println("(♥◠‿◠)ノ゙  若依启动成功   ლ(´ڡ`ლ)゙  \n" +
+                " .-------.       ____     __        \n" +
+                " |  _ _   \\      \\   \\   /  /    \n" +
+                " | ( ' )  |       \\  _. /  '       \n" +
+                " |(_ o _) /        _( )_ .'         \n" +
+                " | (_,_).' __  ___(_ o _)'          \n" +
+                " |  |\\ \\  |  ||   |(_,_)'         \n" +
+                " |  | \\ `'   /|   `-'  /           \n" +
+                " |  |  \\    /  \\      /           \n" +
+                " ''-'   `'-'    `-..-'              ");
+    }
+}

+ 18 - 0
ruoyi-admin/src/main/java/com/ruoyi/RuoYiServletInitializer.java

@@ -0,0 +1,18 @@
+package com.ruoyi;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+/**
+ * web容器中进行部署
+ * 
+ * @author ruoyi
+ */
+public class RuoYiServletInitializer extends SpringBootServletInitializer
+{
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
+    {
+        return application.sources(RuoYiApplication.class);
+    }
+}

+ 17 - 0
ruoyi-admin/src/main/java/com/ruoyi/ServiceConfigure.java

@@ -0,0 +1,17 @@
+package com.ruoyi;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.FilterType;
+
+@Configuration
+@MapperScan("com.cclotus.**.mapper")
+@ComponentScan(
+        basePackages = {"com.ruoyi.**", "com.cclotus.**"},
+        excludeFilters = {
+                @ComponentScan.Filter(type = FilterType.REGEX, pattern = "^com.cclotus.[\\w+.]+controller.wxapp.[\\w+.]*\\w+")
+        }
+)
+public class ServiceConfigure {
+}

+ 94 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java

@@ -0,0 +1,94 @@
+package com.ruoyi.web.controller.common;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Resource;
+import javax.imageio.ImageIO;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.FastByteArrayOutputStream;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.google.code.kaptcha.Producer;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.utils.sign.Base64;
+import com.ruoyi.common.utils.uuid.IdUtils;
+import com.ruoyi.system.service.ISysConfigService;
+
+/**
+ * 验证码操作处理
+ * 
+ * @author ruoyi
+ */
+@RestController
+public class CaptchaController
+{
+    @Resource(name = "captchaProducer")
+    private Producer captchaProducer;
+
+    @Resource(name = "captchaProducerMath")
+    private Producer captchaProducerMath;
+
+    @Autowired
+    private RedisCache redisCache;
+    
+    @Autowired
+    private ISysConfigService configService;
+    /**
+     * 生成验证码
+     */
+    @GetMapping("/captchaImage")
+    public AjaxResult getCode(HttpServletResponse response) throws IOException
+    {
+        AjaxResult ajax = AjaxResult.success();
+        boolean captchaEnabled = configService.selectCaptchaEnabled();
+        ajax.put("captchaEnabled", captchaEnabled);
+        if (!captchaEnabled)
+        {
+            return ajax;
+        }
+
+        // 保存验证码信息
+        String uuid = IdUtils.simpleUUID();
+        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
+
+        String capStr = null, code = null;
+        BufferedImage image = null;
+
+        // 生成验证码
+        String captchaType = RuoYiConfig.getCaptchaType();
+        if ("math".equals(captchaType))
+        {
+            String capText = captchaProducerMath.createText();
+            capStr = capText.substring(0, capText.lastIndexOf("@"));
+            code = capText.substring(capText.lastIndexOf("@") + 1);
+            image = captchaProducerMath.createImage(capStr);
+        }
+        else if ("char".equals(captchaType))
+        {
+            capStr = code = captchaProducer.createText();
+            image = captchaProducer.createImage(capStr);
+        }
+
+        redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
+        // 转换流信息写出
+        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
+        try
+        {
+            ImageIO.write(image, "jpg", os);
+        }
+        catch (IOException e)
+        {
+            return AjaxResult.error(e.getMessage());
+        }
+
+        ajax.put("uuid", uuid);
+        ajax.put("img", Base64.encode(os.toByteArray()));
+        return ajax;
+    }
+}

+ 163 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java

@@ -0,0 +1,163 @@
+package com.ruoyi.web.controller.common;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.file.FileUploadUtils;
+import com.ruoyi.common.utils.file.FileUtils;
+import com.ruoyi.framework.config.ServerConfig;
+
+/**
+ * 通用请求处理
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/common")
+public class CommonController
+{
+    private static final Logger log = LoggerFactory.getLogger(CommonController.class);
+
+    @Autowired
+    private ServerConfig serverConfig;
+
+    private static final String FILE_DELIMETER = ",";
+
+    /**
+     * 通用下载请求
+     * 
+     * @param fileName 文件名称
+     * @param delete 是否删除
+     */
+    @GetMapping("/download")
+    public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
+    {
+        try
+        {
+            if (!FileUtils.checkAllowDownload(fileName))
+            {
+                throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
+            }
+            String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
+            String filePath = RuoYiConfig.getDownloadPath() + fileName;
+
+            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+            FileUtils.setAttachmentResponseHeader(response, realFileName);
+            FileUtils.writeBytes(filePath, response.getOutputStream());
+            if (delete)
+            {
+                FileUtils.deleteFile(filePath);
+            }
+        }
+        catch (Exception e)
+        {
+            log.error("下载文件失败", e);
+        }
+    }
+
+    /**
+     * 通用上传请求(单个)
+     */
+    @PostMapping("/upload")
+    public AjaxResult uploadFile(MultipartFile file) throws Exception
+    {
+        try
+        {
+            // 上传文件路径
+            String filePath = RuoYiConfig.getUploadPath();
+            // 上传并返回新文件名称
+            String fileName = FileUploadUtils.upload(filePath, file);
+            String url = serverConfig.getUrl() + fileName;
+            AjaxResult ajax = AjaxResult.success();
+            ajax.put("url", url);
+            ajax.put("fileName", fileName);
+            ajax.put("newFileName", FileUtils.getName(fileName));
+            ajax.put("originalFilename", file.getOriginalFilename());
+            return ajax;
+        }
+        catch (Exception e)
+        {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 通用上传请求(多个)
+     */
+    @PostMapping("/uploads")
+    public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception
+    {
+        try
+        {
+            // 上传文件路径
+            String filePath = RuoYiConfig.getUploadPath();
+            List<String> urls = new ArrayList<String>();
+            List<String> fileNames = new ArrayList<String>();
+            List<String> newFileNames = new ArrayList<String>();
+            List<String> originalFilenames = new ArrayList<String>();
+            for (MultipartFile file : files)
+            {
+                // 上传并返回新文件名称
+                String fileName = FileUploadUtils.upload(filePath, file);
+                String url = serverConfig.getUrl() + fileName;
+                urls.add(url);
+                fileNames.add(fileName);
+                newFileNames.add(FileUtils.getName(fileName));
+                originalFilenames.add(file.getOriginalFilename());
+            }
+            AjaxResult ajax = AjaxResult.success();
+            ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
+            ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
+            ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
+            ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
+            return ajax;
+        }
+        catch (Exception e)
+        {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 本地资源通用下载
+     */
+    @GetMapping("/download/resource")
+    public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
+            throws Exception
+    {
+        try
+        {
+            if (!FileUtils.checkAllowDownload(resource))
+            {
+                throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
+            }
+            // 本地资源路径
+            String localPath = RuoYiConfig.getProfile();
+            // 数据库资源地址
+            String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
+            // 下载名称
+            String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
+            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+            FileUtils.setAttachmentResponseHeader(response, downloadName);
+            FileUtils.writeBytes(downloadPath, response.getOutputStream());
+        }
+        catch (Exception e)
+        {
+            log.error("下载文件失败", e);
+        }
+    }
+}

+ 167 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/file/FileManagerController.java

@@ -0,0 +1,167 @@
+package com.ruoyi.web.controller.file;
+
+import com.cclotus.cms.service.FileManagerService;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.file.FileUploadUtils;
+import com.ruoyi.common.utils.file.FileUtils;
+import com.ruoyi.common.utils.file.MimeTypeUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletResponse;
+
+@RestController
+@RequestMapping("/file-manager")
+public class FileManagerController {
+
+    @Autowired
+    private FileManagerService service;
+
+    private static final Logger log = LoggerFactory.getLogger(FileManagerController.class);
+
+    /**
+     * 文件下载请求
+     *
+     * @param fileName 文件名称
+     * @param path     文件路径
+     */
+    @PostMapping("/download")
+    public void fileDownload(String fileName, String path, HttpServletResponse response) {
+        checkPath(path);
+        checkFileName(fileName);
+        try {
+            if (!FileUtils.checkAllowDownload(fileName)) {
+                throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
+            }
+            String filePath = service.getMediaPath() + path + "/" + fileName;
+            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+            FileUtils.setAttachmentResponseHeader(response, fileName);
+            FileUtils.writeBytes(filePath, response.getOutputStream());
+        } catch (Exception e) {
+            log.error("下载文件失败", e);
+        }
+    }
+
+    /**
+     * 文件上传请求(单个)
+     */
+    @PostMapping("/upload")
+    public AjaxResult uploadFile(MultipartFile file, String path) {
+        checkPath(path);
+        try {
+            // 上传文件路径
+            String filePath = service.getMediaPath() + path;
+            String newName = service.getFileName(filePath, file.getOriginalFilename());
+            // 上传并返回新文件名称
+            String fileName = FileUploadUtils.uploadWithName(filePath, newName, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
+            AjaxResult ajax = AjaxResult.success();
+            ajax.put("url", fileName);
+            ajax.put("fileName", fileName);
+            ajax.put("newFileName", FileUtils.getName(fileName));
+            ajax.put("originalFilename", file.getOriginalFilename());
+            return ajax;
+        } catch (Exception e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 打开指定文件夹
+     *
+     * @param path 文件路径
+     * @return 指定文件路径下的文件和文件夹信息
+     */
+    @GetMapping("/open")
+    public AjaxResult openDirectory(String path) {
+        checkPath(path);
+        return AjaxResult.success(service.browseDirectory(path));
+    }
+
+    /**
+     * 获取所有目录信息
+     *
+     * @return 目录信息
+     */
+    @GetMapping("/directories")
+    public AjaxResult getDirectories() {
+        return AjaxResult.success(service.directoryTree(null));
+    }
+
+    /**
+     * 创建文件夹
+     *
+     * @param path    文件路径
+     * @param dirName 创建的文件夹名称
+     * @return 创建结果
+     */
+    @PostMapping("/create")
+    public AjaxResult createDirectory(String path, String dirName) {
+        checkPath(path);
+        checkFileName(dirName);
+        service.createDirectory(path, dirName);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 修改文件或文件夹名称
+     *
+     * @param path       文件所属路径
+     * @param originName 原始名称
+     * @param newName    新名称
+     * @return 修改结果
+     */
+    @PostMapping("/rename")
+    public AjaxResult changeFileName(String path, String originName, String newName) {
+        checkPath(path);
+        checkFileName(newName);
+        service.changeFileName(path, originName, newName);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 删除文件或者文件夹
+     *
+     * @param path      当前文件所在路径
+     * @param name      文件或者文件夹名称
+     * @param mandatory 是否强制删除文件夹内容,默认为否
+     * @return 删除结果
+     */
+    @DeleteMapping("/delete")
+    public AjaxResult delete(String path, String name) {
+        checkPath(path);
+        checkFileName(name);
+        service.delete(path, name, false);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 检查路径
+     *
+     * @param path 文件路径
+     * @return
+     */
+    private Boolean checkPath(String path) {
+        if (path == null || path.isEmpty()) return true;
+        if (path.contains("..")) throw new ServiceException("文件路径不合法");
+        return true;
+    }
+
+    /**
+     * 检查文件名称
+     *
+     * @param fileName 文件名
+     * @return
+     */
+    private Boolean checkFileName(String fileName) {
+        if (fileName == null || fileName.isEmpty()) throw new ServiceException("文件名不合法");
+        if (fileName.contains("..")) throw new ServiceException("文件名不合法");
+        return true;
+    }
+
+}

+ 120 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/CacheController.java

@@ -0,0 +1,120 @@
+package com.ruoyi.web.controller.monitor;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisCallback;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.domain.SysCache;
+
+/**
+ * 缓存监控
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/cache")
+public class CacheController
+{
+    @Autowired
+    private RedisTemplate<String, String> redisTemplate;
+
+    private final static List<SysCache> caches = new ArrayList<SysCache>();
+    {
+        caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
+        caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
+        caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
+        caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
+        caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
+        caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
+        caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @GetMapping()
+    public AjaxResult getInfo() throws Exception
+    {
+        Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
+        Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats"));
+        Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
+
+        Map<String, Object> result = new HashMap<>(3);
+        result.put("info", info);
+        result.put("dbSize", dbSize);
+
+        List<Map<String, String>> pieList = new ArrayList<>();
+        commandStats.stringPropertyNames().forEach(key -> {
+            Map<String, String> data = new HashMap<>(2);
+            String property = commandStats.getProperty(key);
+            data.put("name", StringUtils.removeStart(key, "cmdstat_"));
+            data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
+            pieList.add(data);
+        });
+        result.put("commandStats", pieList);
+        return AjaxResult.success(result);
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @GetMapping("/getNames")
+    public AjaxResult cache()
+    {
+        return AjaxResult.success(caches);
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @GetMapping("/getKeys/{cacheName}")
+    public AjaxResult getCacheKeys(@PathVariable String cacheName)
+    {
+        Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
+        return AjaxResult.success(cacheKeys);
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @GetMapping("/getValue/{cacheName}/{cacheKey}")
+    public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
+    {
+        String cacheValue = redisTemplate.opsForValue().get(cacheKey);
+        SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue);
+        return AjaxResult.success(sysCache);
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @DeleteMapping("/clearCacheName/{cacheName}")
+    public AjaxResult clearCacheName(@PathVariable String cacheName)
+    {
+        Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
+        redisTemplate.delete(cacheKeys);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @DeleteMapping("/clearCacheKey/{cacheKey}")
+    public AjaxResult clearCacheKey(@PathVariable String cacheKey)
+    {
+        redisTemplate.delete(cacheKey);
+        return AjaxResult.success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
+    @DeleteMapping("/clearCacheAll")
+    public AjaxResult clearCacheAll()
+    {
+        Collection<String> cacheKeys = redisTemplate.keys("*");
+        redisTemplate.delete(cacheKeys);
+        return AjaxResult.success();
+    }
+}

+ 27 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java

@@ -0,0 +1,27 @@
+package com.ruoyi.web.controller.monitor;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.framework.web.domain.Server;
+
+/**
+ * 服务器监控
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/server")
+public class ServerController
+{
+    @PreAuthorize("@ss.hasPermi('monitor:server:list')")
+    @GetMapping()
+    public AjaxResult getInfo() throws Exception
+    {
+        Server server = new Server();
+        server.copyTo();
+        return AjaxResult.success(server);
+    }
+}

+ 82 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysLogininforController.java

@@ -0,0 +1,82 @@
+package com.ruoyi.web.controller.monitor;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.framework.web.service.SysPasswordService;
+import com.ruoyi.system.domain.SysLogininfor;
+import com.ruoyi.system.service.ISysLogininforService;
+
+/**
+ * 系统访问记录
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/logininfor")
+public class SysLogininforController extends BaseController
+{
+    @Autowired
+    private ISysLogininforService logininforService;
+
+    @Autowired
+    private SysPasswordService passwordService;
+
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysLogininfor logininfor)
+    {
+        startPage();
+        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
+        return getDataTable(list);
+    }
+
+    @Log(title = "登录日志", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysLogininfor logininfor)
+    {
+        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
+        ExcelUtil<SysLogininfor> util = new ExcelUtil<SysLogininfor>(SysLogininfor.class);
+        util.exportExcel(response, list, "登录日志");
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
+    @Log(title = "登录日志", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{infoIds}")
+    public AjaxResult remove(@PathVariable Long[] infoIds)
+    {
+        return toAjax(logininforService.deleteLogininforByIds(infoIds));
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
+    @Log(title = "登录日志", businessType = BusinessType.CLEAN)
+    @DeleteMapping("/clean")
+    public AjaxResult clean()
+    {
+        logininforService.cleanLogininfor();
+        return success();
+    }
+
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')")
+    @Log(title = "账户解锁", businessType = BusinessType.OTHER)
+    @GetMapping("/unlock/{userName}")
+    public AjaxResult unlock(@PathVariable("userName") String userName)
+    {
+        passwordService.clearLoginRecordCache(userName);
+        return success();
+    }
+}

+ 69 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysOperlogController.java

@@ -0,0 +1,69 @@
+package com.ruoyi.web.controller.monitor;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.domain.SysOperLog;
+import com.ruoyi.system.service.ISysOperLogService;
+
+/**
+ * 操作日志记录
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/operlog")
+public class SysOperlogController extends BaseController
+{
+    @Autowired
+    private ISysOperLogService operLogService;
+
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysOperLog operLog)
+    {
+        startPage();
+        List<SysOperLog> list = operLogService.selectOperLogList(operLog);
+        return getDataTable(list);
+    }
+
+    @Log(title = "操作日志", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysOperLog operLog)
+    {
+        List<SysOperLog> list = operLogService.selectOperLogList(operLog);
+        ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class);
+        util.exportExcel(response, list, "操作日志");
+    }
+
+    @Log(title = "操作日志", businessType = BusinessType.DELETE)
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
+    @DeleteMapping("/{operIds}")
+    public AjaxResult remove(@PathVariable Long[] operIds)
+    {
+        return toAjax(operLogService.deleteOperLogByIds(operIds));
+    }
+
+    @Log(title = "操作日志", businessType = BusinessType.CLEAN)
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
+    @DeleteMapping("/clean")
+    public AjaxResult clean()
+    {
+        operLogService.cleanOperLog();
+        return AjaxResult.success();
+    }
+}

+ 92 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java

@@ -0,0 +1,92 @@
+package com.ruoyi.web.controller.monitor;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.domain.SysUserOnline;
+import com.ruoyi.system.service.ISysUserOnlineService;
+
+/**
+ * 在线用户监控
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/monitor/online")
+public class SysUserOnlineController extends BaseController
+{
+    @Autowired
+    private ISysUserOnlineService userOnlineService;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @PreAuthorize("@ss.hasPermi('monitor:online:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(String ipaddr, String userName)
+    {
+        Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
+        List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
+        for (String key : keys)
+        {
+            LoginUser user = redisCache.getCacheObject(key);
+            if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName))
+            {
+                if (StringUtils.equals(ipaddr, user.getIpaddr()) && StringUtils.equals(userName, user.getUsername()))
+                {
+                    userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
+                }
+            }
+            else if (StringUtils.isNotEmpty(ipaddr))
+            {
+                if (StringUtils.equals(ipaddr, user.getIpaddr()))
+                {
+                    userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user));
+                }
+            }
+            else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser()))
+            {
+                if (StringUtils.equals(userName, user.getUsername()))
+                {
+                    userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user));
+                }
+            }
+            else
+            {
+                userOnlineList.add(userOnlineService.loginUserToUserOnline(user));
+            }
+        }
+        Collections.reverse(userOnlineList);
+        userOnlineList.removeAll(Collections.singleton(null));
+        return getDataTable(userOnlineList);
+    }
+
+    /**
+     * 强退用户
+     */
+    @PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')")
+    @Log(title = "在线用户", businessType = BusinessType.FORCE)
+    @DeleteMapping("/{tokenId}")
+    public AjaxResult forceLogout(@PathVariable String tokenId)
+    {
+        redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
+        return AjaxResult.success();
+    }
+}

+ 134 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysConfigController.java

@@ -0,0 +1,134 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.domain.SysConfig;
+import com.ruoyi.system.service.ISysConfigService;
+
+/**
+ * 参数配置 信息操作处理
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/config")
+public class SysConfigController extends BaseController
+{
+    @Autowired
+    private ISysConfigService configService;
+
+    /**
+     * 获取参数配置列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysConfig config)
+    {
+        startPage();
+        List<SysConfig> list = configService.selectConfigList(config);
+        return getDataTable(list);
+    }
+
+    @Log(title = "参数管理", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:config:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysConfig config)
+    {
+        List<SysConfig> list = configService.selectConfigList(config);
+        ExcelUtil<SysConfig> util = new ExcelUtil<SysConfig>(SysConfig.class);
+        util.exportExcel(response, list, "参数数据");
+    }
+
+    /**
+     * 根据参数编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:query')")
+    @GetMapping(value = "/{configId}")
+    public AjaxResult getInfo(@PathVariable Long configId)
+    {
+        return AjaxResult.success(configService.selectConfigById(configId));
+    }
+
+    /**
+     * 根据参数键名查询参数值
+     */
+    @GetMapping(value = "/configKey/{configKey}")
+    public AjaxResult getConfigKey(@PathVariable String configKey)
+    {
+        return AjaxResult.success(configService.selectConfigByKey(configKey));
+    }
+
+    /**
+     * 新增参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:add')")
+    @Log(title = "参数管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysConfig config)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(configService.checkConfigKeyUnique(config)))
+        {
+            return AjaxResult.error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在");
+        }
+        config.setCreateBy(getUsername());
+        return toAjax(configService.insertConfig(config));
+    }
+
+    /**
+     * 修改参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:edit')")
+    @Log(title = "参数管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysConfig config)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(configService.checkConfigKeyUnique(config)))
+        {
+            return AjaxResult.error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在");
+        }
+        config.setUpdateBy(getUsername());
+        return toAjax(configService.updateConfig(config));
+    }
+
+    /**
+     * 删除参数配置
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:remove')")
+    @Log(title = "参数管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{configIds}")
+    public AjaxResult remove(@PathVariable Long[] configIds)
+    {
+        configService.deleteConfigByIds(configIds);
+        return success();
+    }
+
+    /**
+     * 刷新参数缓存
+     */
+    @PreAuthorize("@ss.hasPermi('system:config:remove')")
+    @Log(title = "参数管理", businessType = BusinessType.CLEAN)
+    @DeleteMapping("/refreshCache")
+    public AjaxResult refreshCache()
+    {
+        configService.resetConfigCache();
+        return AjaxResult.success();
+    }
+}

+ 132 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java

@@ -0,0 +1,132 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import org.apache.commons.lang3.ArrayUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysDept;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.service.ISysDeptService;
+
+/**
+ * 部门信息
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/dept")
+public class SysDeptController extends BaseController
+{
+    @Autowired
+    private ISysDeptService deptService;
+
+    /**
+     * 获取部门列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:list')")
+    @GetMapping("/list")
+    public AjaxResult list(SysDept dept)
+    {
+        List<SysDept> depts = deptService.selectDeptList(dept);
+        return AjaxResult.success(depts);
+    }
+
+    /**
+     * 查询部门列表(排除节点)
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:list')")
+    @GetMapping("/list/exclude/{deptId}")
+    public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId)
+    {
+        List<SysDept> depts = deptService.selectDeptList(new SysDept());
+        depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + ""));
+        return AjaxResult.success(depts);
+    }
+
+    /**
+     * 根据部门编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:query')")
+    @GetMapping(value = "/{deptId}")
+    public AjaxResult getInfo(@PathVariable Long deptId)
+    {
+        deptService.checkDeptDataScope(deptId);
+        return AjaxResult.success(deptService.selectDeptById(deptId));
+    }
+
+    /**
+     * 新增部门
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:add')")
+    @Log(title = "部门管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysDept dept)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept)))
+        {
+            return AjaxResult.error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
+        }
+        dept.setCreateBy(getUsername());
+        return toAjax(deptService.insertDept(dept));
+    }
+
+    /**
+     * 修改部门
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:edit')")
+    @Log(title = "部门管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysDept dept)
+    {
+        Long deptId = dept.getDeptId();
+        deptService.checkDeptDataScope(deptId);
+        if (UserConstants.NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept)))
+        {
+            return AjaxResult.error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
+        }
+        else if (dept.getParentId().equals(deptId))
+        {
+            return AjaxResult.error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
+        }
+        else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0)
+        {
+            return AjaxResult.error("该部门包含未停用的子部门!");
+        }
+        dept.setUpdateBy(getUsername());
+        return toAjax(deptService.updateDept(dept));
+    }
+
+    /**
+     * 删除部门
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:remove')")
+    @Log(title = "部门管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{deptId}")
+    public AjaxResult remove(@PathVariable Long deptId)
+    {
+        if (deptService.hasChildByDeptId(deptId))
+        {
+            return AjaxResult.error("存在下级部门,不允许删除");
+        }
+        if (deptService.checkDeptExistUser(deptId))
+        {
+            return AjaxResult.error("部门存在用户,不允许删除");
+        }
+        deptService.checkDeptDataScope(deptId);
+        return toAjax(deptService.deleteDeptById(deptId));
+    }
+}

+ 121 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictDataController.java

@@ -0,0 +1,121 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysDictData;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.service.ISysDictDataService;
+import com.ruoyi.system.service.ISysDictTypeService;
+
+/**
+ * 数据字典信息
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/dict/data")
+public class SysDictDataController extends BaseController
+{
+    @Autowired
+    private ISysDictDataService dictDataService;
+
+    @Autowired
+    private ISysDictTypeService dictTypeService;
+
+    @PreAuthorize("@ss.hasPermi('system:dict:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysDictData dictData)
+    {
+        startPage();
+        List<SysDictData> list = dictDataService.selectDictDataList(dictData);
+        return getDataTable(list);
+    }
+
+    @Log(title = "字典数据", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:dict:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysDictData dictData)
+    {
+        List<SysDictData> list = dictDataService.selectDictDataList(dictData);
+        ExcelUtil<SysDictData> util = new ExcelUtil<SysDictData>(SysDictData.class);
+        util.exportExcel(response, list, "字典数据");
+    }
+
+    /**
+     * 查询字典数据详细
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:query')")
+    @GetMapping(value = "/{dictCode}")
+    public AjaxResult getInfo(@PathVariable Long dictCode)
+    {
+        return AjaxResult.success(dictDataService.selectDictDataById(dictCode));
+    }
+
+    /**
+     * 根据字典类型查询字典数据信息
+     */
+    @GetMapping(value = "/type/{dictType}")
+    public AjaxResult dictType(@PathVariable String dictType)
+    {
+        List<SysDictData> data = dictTypeService.selectDictDataByType(dictType);
+        if (StringUtils.isNull(data))
+        {
+            data = new ArrayList<SysDictData>();
+        }
+        return AjaxResult.success(data);
+    }
+
+    /**
+     * 新增字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:add')")
+    @Log(title = "字典数据", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysDictData dict)
+    {
+        dict.setCreateBy(getUsername());
+        return toAjax(dictDataService.insertDictData(dict));
+    }
+
+    /**
+     * 修改保存字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:edit')")
+    @Log(title = "字典数据", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysDictData dict)
+    {
+        dict.setUpdateBy(getUsername());
+        return toAjax(dictDataService.updateDictData(dict));
+    }
+
+    /**
+     * 删除字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
+    @Log(title = "字典类型", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{dictCodes}")
+    public AjaxResult remove(@PathVariable Long[] dictCodes)
+    {
+        dictDataService.deleteDictDataByIds(dictCodes);
+        return success();
+    }
+}

+ 132 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDictTypeController.java

@@ -0,0 +1,132 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysDictType;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.service.ISysDictTypeService;
+
+/**
+ * 数据字典信息
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/dict/type")
+public class SysDictTypeController extends BaseController
+{
+    @Autowired
+    private ISysDictTypeService dictTypeService;
+
+    @PreAuthorize("@ss.hasPermi('system:dict:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysDictType dictType)
+    {
+        startPage();
+        List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
+        return getDataTable(list);
+    }
+
+    @Log(title = "字典类型", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:dict:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysDictType dictType)
+    {
+        List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
+        ExcelUtil<SysDictType> util = new ExcelUtil<SysDictType>(SysDictType.class);
+        util.exportExcel(response, list, "字典类型");
+    }
+
+    /**
+     * 查询字典类型详细
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:query')")
+    @GetMapping(value = "/{dictId}")
+    public AjaxResult getInfo(@PathVariable Long dictId)
+    {
+        return AjaxResult.success(dictTypeService.selectDictTypeById(dictId));
+    }
+
+    /**
+     * 新增字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:add')")
+    @Log(title = "字典类型", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysDictType dict)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(dictTypeService.checkDictTypeUnique(dict)))
+        {
+            return AjaxResult.error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在");
+        }
+        dict.setCreateBy(getUsername());
+        return toAjax(dictTypeService.insertDictType(dict));
+    }
+
+    /**
+     * 修改字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:edit')")
+    @Log(title = "字典类型", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysDictType dict)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(dictTypeService.checkDictTypeUnique(dict)))
+        {
+            return AjaxResult.error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在");
+        }
+        dict.setUpdateBy(getUsername());
+        return toAjax(dictTypeService.updateDictType(dict));
+    }
+
+    /**
+     * 删除字典类型
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
+    @Log(title = "字典类型", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{dictIds}")
+    public AjaxResult remove(@PathVariable Long[] dictIds)
+    {
+        dictTypeService.deleteDictTypeByIds(dictIds);
+        return success();
+    }
+
+    /**
+     * 刷新字典缓存
+     */
+    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
+    @Log(title = "字典类型", businessType = BusinessType.CLEAN)
+    @DeleteMapping("/refreshCache")
+    public AjaxResult refreshCache()
+    {
+        dictTypeService.resetDictCache();
+        return AjaxResult.success();
+    }
+
+    /**
+     * 获取字典选择框列表
+     */
+    @GetMapping("/optionselect")
+    public AjaxResult optionselect()
+    {
+        List<SysDictType> dictTypes = dictTypeService.selectDictTypeAll();
+        return AjaxResult.success(dictTypes);
+    }
+}

+ 29 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java

@@ -0,0 +1,29 @@
+package com.ruoyi.web.controller.system;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 首页
+ *
+ * @author ruoyi
+ */
+@RestController
+public class SysIndexController
+{
+    /** 系统基础配置 */
+    @Autowired
+    private RuoYiConfig ruoyiConfig;
+
+    /**
+     * 访问首页,提示语
+     */
+    @RequestMapping("/")
+    public String index()
+    {
+        return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
+    }
+}

+ 86 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java

@@ -0,0 +1,86 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import java.util.Set;
+import org.springframework.beans.factory.annotation.Autowired;
+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.RestController;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysMenu;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginBody;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.framework.web.service.SysLoginService;
+import com.ruoyi.framework.web.service.SysPermissionService;
+import com.ruoyi.system.service.ISysMenuService;
+
+/**
+ * 登录验证
+ * 
+ * @author ruoyi
+ */
+@RestController
+public class SysLoginController
+{
+    @Autowired
+    private SysLoginService loginService;
+
+    @Autowired
+    private ISysMenuService menuService;
+
+    @Autowired
+    private SysPermissionService permissionService;
+
+    /**
+     * 登录方法
+     * 
+     * @param loginBody 登录信息
+     * @return 结果
+     */
+    @PostMapping("/login")
+    public AjaxResult login(@RequestBody LoginBody loginBody)
+    {
+        AjaxResult ajax = AjaxResult.success();
+        // 生成令牌
+        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
+                loginBody.getUuid());
+        ajax.put(Constants.TOKEN, token);
+        return ajax;
+    }
+
+    /**
+     * 获取用户信息
+     * 
+     * @return 用户信息
+     */
+    @GetMapping("getInfo")
+    public AjaxResult getInfo()
+    {
+        SysUser user = SecurityUtils.getLoginUser().getUser();
+        // 角色集合
+        Set<String> roles = permissionService.getRolePermission(user);
+        // 权限集合
+        Set<String> permissions = permissionService.getMenuPermission(user);
+        AjaxResult ajax = AjaxResult.success();
+        ajax.put("user", user);
+        ajax.put("roles", roles);
+        ajax.put("permissions", permissions);
+        return ajax;
+    }
+
+    /**
+     * 获取路由信息
+     * 
+     * @return 路由信息
+     */
+    @GetMapping("getRouters")
+    public AjaxResult getRouters()
+    {
+        Long userId = SecurityUtils.getUserId();
+        List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
+        return AjaxResult.success(menuService.buildMenus(menus));
+    }
+}

+ 142 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java

@@ -0,0 +1,142 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysMenu;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.service.ISysMenuService;
+
+/**
+ * 菜单信息
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/menu")
+public class SysMenuController extends BaseController
+{
+    @Autowired
+    private ISysMenuService menuService;
+
+    /**
+     * 获取菜单列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:list')")
+    @GetMapping("/list")
+    public AjaxResult list(SysMenu menu)
+    {
+        List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
+        return AjaxResult.success(menus);
+    }
+
+    /**
+     * 根据菜单编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:query')")
+    @GetMapping(value = "/{menuId}")
+    public AjaxResult getInfo(@PathVariable Long menuId)
+    {
+        return AjaxResult.success(menuService.selectMenuById(menuId));
+    }
+
+    /**
+     * 获取菜单下拉树列表
+     */
+    @GetMapping("/treeselect")
+    public AjaxResult treeselect(SysMenu menu)
+    {
+        List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
+        return AjaxResult.success(menuService.buildMenuTreeSelect(menus));
+    }
+
+    /**
+     * 加载对应角色菜单列表树
+     */
+    @GetMapping(value = "/roleMenuTreeselect/{roleId}")
+    public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId)
+    {
+        List<SysMenu> menus = menuService.selectMenuList(getUserId());
+        AjaxResult ajax = AjaxResult.success();
+        ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId));
+        ajax.put("menus", menuService.buildMenuTreeSelect(menus));
+        return ajax;
+    }
+
+    /**
+     * 新增菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:add')")
+    @Log(title = "菜单管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysMenu menu)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu)))
+        {
+            return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
+        }
+        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
+        {
+            return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
+        }
+        menu.setCreateBy(getUsername());
+        return toAjax(menuService.insertMenu(menu));
+    }
+
+    /**
+     * 修改菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:edit')")
+    @Log(title = "菜单管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysMenu menu)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu)))
+        {
+            return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
+        }
+        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
+        {
+            return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
+        }
+        else if (menu.getMenuId().equals(menu.getParentId()))
+        {
+            return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
+        }
+        menu.setUpdateBy(getUsername());
+        return toAjax(menuService.updateMenu(menu));
+    }
+
+    /**
+     * 删除菜单
+     */
+    @PreAuthorize("@ss.hasPermi('system:menu:remove')")
+    @Log(title = "菜单管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{menuId}")
+    public AjaxResult remove(@PathVariable("menuId") Long menuId)
+    {
+        if (menuService.hasChildByMenuId(menuId))
+        {
+            return AjaxResult.error("存在子菜单,不允许删除");
+        }
+        if (menuService.checkMenuExistRole(menuId))
+        {
+            return AjaxResult.error("菜单已分配,不允许删除");
+        }
+        return toAjax(menuService.deleteMenuById(menuId));
+    }
+}

+ 91 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysNoticeController.java

@@ -0,0 +1,91 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.system.domain.SysNotice;
+import com.ruoyi.system.service.ISysNoticeService;
+
+/**
+ * 公告 信息操作处理
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/notice")
+public class SysNoticeController extends BaseController
+{
+    @Autowired
+    private ISysNoticeService noticeService;
+
+    /**
+     * 获取通知公告列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysNotice notice)
+    {
+        startPage();
+        List<SysNotice> list = noticeService.selectNoticeList(notice);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据通知公告编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:query')")
+    @GetMapping(value = "/{noticeId}")
+    public AjaxResult getInfo(@PathVariable Long noticeId)
+    {
+        return AjaxResult.success(noticeService.selectNoticeById(noticeId));
+    }
+
+    /**
+     * 新增通知公告
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:add')")
+    @Log(title = "通知公告", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysNotice notice)
+    {
+        notice.setCreateBy(getUsername());
+        return toAjax(noticeService.insertNotice(notice));
+    }
+
+    /**
+     * 修改通知公告
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:edit')")
+    @Log(title = "通知公告", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysNotice notice)
+    {
+        notice.setUpdateBy(getUsername());
+        return toAjax(noticeService.updateNotice(notice));
+    }
+
+    /**
+     * 删除通知公告
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:remove')")
+    @Log(title = "通知公告", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{noticeIds}")
+    public AjaxResult remove(@PathVariable Long[] noticeIds)
+    {
+        return toAjax(noticeService.deleteNoticeByIds(noticeIds));
+    }
+}

+ 130 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysPostController.java

@@ -0,0 +1,130 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.domain.SysPost;
+import com.ruoyi.system.service.ISysPostService;
+
+/**
+ * 岗位信息操作处理
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/post")
+public class SysPostController extends BaseController
+{
+    @Autowired
+    private ISysPostService postService;
+
+    /**
+     * 获取岗位列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysPost post)
+    {
+        startPage();
+        List<SysPost> list = postService.selectPostList(post);
+        return getDataTable(list);
+    }
+    
+    @Log(title = "岗位管理", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:post:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysPost post)
+    {
+        List<SysPost> list = postService.selectPostList(post);
+        ExcelUtil<SysPost> util = new ExcelUtil<SysPost>(SysPost.class);
+        util.exportExcel(response, list, "岗位数据");
+    }
+
+    /**
+     * 根据岗位编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:query')")
+    @GetMapping(value = "/{postId}")
+    public AjaxResult getInfo(@PathVariable Long postId)
+    {
+        return AjaxResult.success(postService.selectPostById(postId));
+    }
+
+    /**
+     * 新增岗位
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:add')")
+    @Log(title = "岗位管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysPost post)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(postService.checkPostNameUnique(post)))
+        {
+            return AjaxResult.error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在");
+        }
+        else if (UserConstants.NOT_UNIQUE.equals(postService.checkPostCodeUnique(post)))
+        {
+            return AjaxResult.error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在");
+        }
+        post.setCreateBy(getUsername());
+        return toAjax(postService.insertPost(post));
+    }
+
+    /**
+     * 修改岗位
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:edit')")
+    @Log(title = "岗位管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysPost post)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(postService.checkPostNameUnique(post)))
+        {
+            return AjaxResult.error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在");
+        }
+        else if (UserConstants.NOT_UNIQUE.equals(postService.checkPostCodeUnique(post)))
+        {
+            return AjaxResult.error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在");
+        }
+        post.setUpdateBy(getUsername());
+        return toAjax(postService.updatePost(post));
+    }
+
+    /**
+     * 删除岗位
+     */
+    @PreAuthorize("@ss.hasPermi('system:post:remove')")
+    @Log(title = "岗位管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{postIds}")
+    public AjaxResult remove(@PathVariable Long[] postIds)
+    {
+        return toAjax(postService.deletePostByIds(postIds));
+    }
+
+    /**
+     * 获取岗位选择框列表
+     */
+    @GetMapping("/optionselect")
+    public AjaxResult optionselect()
+    {
+        List<SysPost> posts = postService.selectPostAll();
+        return AjaxResult.success(posts);
+    }
+}

+ 144 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java

@@ -0,0 +1,144 @@
+package com.ruoyi.web.controller.system;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.file.FileUploadUtils;
+import com.ruoyi.common.utils.file.MimeTypeUtils;
+import com.ruoyi.framework.web.service.TokenService;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 个人信息 业务处理
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/user/profile")
+public class SysProfileController extends BaseController
+{
+    @Autowired
+    private ISysUserService userService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 个人信息
+     */
+    @GetMapping
+    public AjaxResult profile()
+    {
+        LoginUser loginUser = getLoginUser();
+        SysUser user = loginUser.getUser();
+        AjaxResult ajax = AjaxResult.success(user);
+        ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername()));
+        ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername()));
+        return ajax;
+    }
+
+    /**
+     * 修改用户
+     */
+    @Log(title = "个人信息", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult updateProfile(@RequestBody SysUser user)
+    {
+        LoginUser loginUser = getLoginUser();
+        SysUser sysUser = loginUser.getUser();
+        user.setUserName(sysUser.getUserName());
+        if (StringUtils.isNotEmpty(user.getPhonenumber())
+                && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user)))
+        {
+            return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
+        }
+        if (StringUtils.isNotEmpty(user.getEmail())
+                && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user)))
+        {
+            return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在");
+        }
+        user.setUserId(sysUser.getUserId());
+        user.setPassword(null);
+        user.setAvatar(null);
+        user.setDeptId(null);
+        if (userService.updateUserProfile(user) > 0)
+        {
+            // 更新缓存用户信息
+            sysUser.setNickName(user.getNickName());
+            sysUser.setPhonenumber(user.getPhonenumber());
+            sysUser.setEmail(user.getEmail());
+            sysUser.setSex(user.getSex());
+            tokenService.setLoginUser(loginUser);
+            return AjaxResult.success();
+        }
+        return AjaxResult.error("修改个人信息异常,请联系管理员");
+    }
+
+    /**
+     * 重置密码
+     */
+    @Log(title = "个人信息", businessType = BusinessType.UPDATE)
+    @PutMapping("/updatePwd")
+    public AjaxResult updatePwd(String oldPassword, String newPassword)
+    {
+        LoginUser loginUser = getLoginUser();
+        String userName = loginUser.getUsername();
+        String password = loginUser.getPassword();
+        if (!SecurityUtils.matchesPassword(oldPassword, password))
+        {
+            return AjaxResult.error("修改密码失败,旧密码错误");
+        }
+        if (SecurityUtils.matchesPassword(newPassword, password))
+        {
+            return AjaxResult.error("新密码不能与旧密码相同");
+        }
+        if (userService.resetUserPwd(userName, SecurityUtils.encryptPassword(newPassword)) > 0)
+        {
+            // 更新缓存用户密码
+            loginUser.getUser().setPassword(SecurityUtils.encryptPassword(newPassword));
+            tokenService.setLoginUser(loginUser);
+            return AjaxResult.success();
+        }
+        return AjaxResult.error("修改密码异常,请联系管理员");
+    }
+
+    /**
+     * 头像上传
+     */
+    @Log(title = "用户头像", businessType = BusinessType.UPDATE)
+    @PostMapping("/avatar")
+    public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception
+    {
+        if (!file.isEmpty())
+        {
+            LoginUser loginUser = getLoginUser();
+            String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION);
+            if (userService.updateUserAvatar(loginUser.getUsername(), avatar))
+            {
+                AjaxResult ajax = AjaxResult.success();
+                ajax.put("imgUrl", avatar);
+                // 更新缓存用户头像
+                loginUser.getUser().setAvatar(avatar);
+                tokenService.setLoginUser(loginUser);
+                return ajax;
+            }
+        }
+        return AjaxResult.error("上传图片异常,请联系管理员");
+    }
+}

+ 38 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java

@@ -0,0 +1,38 @@
+package com.ruoyi.web.controller.system;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.model.RegisterBody;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.web.service.SysRegisterService;
+import com.ruoyi.system.service.ISysConfigService;
+
+/**
+ * 注册验证
+ * 
+ * @author ruoyi
+ */
+@RestController
+public class SysRegisterController extends BaseController
+{
+    @Autowired
+    private SysRegisterService registerService;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    @PostMapping("/register")
+    public AjaxResult register(@RequestBody RegisterBody user)
+    {
+        if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser"))))
+        {
+            return error("当前系统没有开启注册功能!");
+        }
+        String msg = registerService.register(user);
+        return StringUtils.isEmpty(msg) ? success() : error(msg);
+    }
+}

+ 263 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java

@@ -0,0 +1,263 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysDept;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.framework.web.service.SysPermissionService;
+import com.ruoyi.framework.web.service.TokenService;
+import com.ruoyi.system.domain.SysUserRole;
+import com.ruoyi.system.service.ISysDeptService;
+import com.ruoyi.system.service.ISysRoleService;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 角色信息
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/role")
+public class SysRoleController extends BaseController
+{
+    @Autowired
+    private ISysRoleService roleService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private SysPermissionService permissionService;
+
+    @Autowired
+    private ISysUserService userService;
+
+    @Autowired
+    private ISysDeptService deptService;
+
+    @PreAuthorize("@ss.hasPermi('system:role:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysRole role)
+    {
+        startPage();
+        List<SysRole> list = roleService.selectRoleList(role);
+        return getDataTable(list);
+    }
+
+    @Log(title = "角色管理", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:role:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysRole role)
+    {
+        List<SysRole> list = roleService.selectRoleList(role);
+        ExcelUtil<SysRole> util = new ExcelUtil<SysRole>(SysRole.class);
+        util.exportExcel(response, list, "角色数据");
+    }
+
+    /**
+     * 根据角色编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:query')")
+    @GetMapping(value = "/{roleId}")
+    public AjaxResult getInfo(@PathVariable Long roleId)
+    {
+        roleService.checkRoleDataScope(roleId);
+        return AjaxResult.success(roleService.selectRoleById(roleId));
+    }
+
+    /**
+     * 新增角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:add')")
+    @Log(title = "角色管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysRole role)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleNameUnique(role)))
+        {
+            return AjaxResult.error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在");
+        }
+        else if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleKeyUnique(role)))
+        {
+            return AjaxResult.error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在");
+        }
+        role.setCreateBy(getUsername());
+        return toAjax(roleService.insertRole(role));
+
+    }
+
+    /**
+     * 修改保存角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysRole role)
+    {
+        roleService.checkRoleAllowed(role);
+        roleService.checkRoleDataScope(role.getRoleId());
+        if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleNameUnique(role)))
+        {
+            return AjaxResult.error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在");
+        }
+        else if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleKeyUnique(role)))
+        {
+            return AjaxResult.error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在");
+        }
+        role.setUpdateBy(getUsername());
+        
+        if (roleService.updateRole(role) > 0)
+        {
+            // 更新缓存用户权限
+            LoginUser loginUser = getLoginUser();
+            if (StringUtils.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin())
+            {
+                loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser()));
+                loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName()));
+                tokenService.setLoginUser(loginUser);
+            }
+            return AjaxResult.success();
+        }
+        return AjaxResult.error("修改角色'" + role.getRoleName() + "'失败,请联系管理员");
+    }
+
+    /**
+     * 修改保存数据权限
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/dataScope")
+    public AjaxResult dataScope(@RequestBody SysRole role)
+    {
+        roleService.checkRoleAllowed(role);
+        roleService.checkRoleDataScope(role.getRoleId());
+        return toAjax(roleService.authDataScope(role));
+    }
+
+    /**
+     * 状态修改
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/changeStatus")
+    public AjaxResult changeStatus(@RequestBody SysRole role)
+    {
+        roleService.checkRoleAllowed(role);
+        roleService.checkRoleDataScope(role.getRoleId());
+        role.setUpdateBy(getUsername());
+        return toAjax(roleService.updateRoleStatus(role));
+    }
+
+    /**
+     * 删除角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:remove')")
+    @Log(title = "角色管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{roleIds}")
+    public AjaxResult remove(@PathVariable Long[] roleIds)
+    {
+        return toAjax(roleService.deleteRoleByIds(roleIds));
+    }
+
+    /**
+     * 获取角色选择框列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:query')")
+    @GetMapping("/optionselect")
+    public AjaxResult optionselect()
+    {
+        return AjaxResult.success(roleService.selectRoleAll());
+    }
+
+    /**
+     * 查询已分配用户角色列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:list')")
+    @GetMapping("/authUser/allocatedList")
+    public TableDataInfo allocatedList(SysUser user)
+    {
+        startPage();
+        List<SysUser> list = userService.selectAllocatedList(user);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询未分配用户角色列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:list')")
+    @GetMapping("/authUser/unallocatedList")
+    public TableDataInfo unallocatedList(SysUser user)
+    {
+        startPage();
+        List<SysUser> list = userService.selectUnallocatedList(user);
+        return getDataTable(list);
+    }
+
+    /**
+     * 取消授权用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authUser/cancel")
+    public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole)
+    {
+        return toAjax(roleService.deleteAuthUser(userRole));
+    }
+
+    /**
+     * 批量取消授权用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authUser/cancelAll")
+    public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds)
+    {
+        return toAjax(roleService.deleteAuthUsers(roleId, userIds));
+    }
+
+    /**
+     * 批量选择用户授权
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authUser/selectAll")
+    public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds)
+    {
+        roleService.checkRoleDataScope(roleId);
+        return toAjax(roleService.insertAuthUsers(roleId, userIds));
+    }
+
+    /**
+     * 获取对应角色部门树列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:role:query')")
+    @GetMapping(value = "/deptTree/{roleId}")
+    public AjaxResult deptTree(@PathVariable("roleId") Long roleId)
+    {
+        AjaxResult ajax = AjaxResult.success();
+        ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId));
+        ajax.put("depts", deptService.selectDeptTreeList(new SysDept()));
+        return ajax;
+    }
+}

+ 252 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java

@@ -0,0 +1,252 @@
+package com.ruoyi.web.controller.system;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.lang3.ArrayUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.entity.SysDept;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.service.ISysDeptService;
+import com.ruoyi.system.service.ISysPostService;
+import com.ruoyi.system.service.ISysRoleService;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 用户信息
+ * 
+ * @author ruoyi
+ */
+@RestController
+@RequestMapping("/system/user")
+public class SysUserController extends BaseController
+{
+    @Autowired
+    private ISysUserService userService;
+
+    @Autowired
+    private ISysRoleService roleService;
+
+    @Autowired
+    private ISysDeptService deptService;
+
+    @Autowired
+    private ISysPostService postService;
+
+    /**
+     * 获取用户列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(SysUser user)
+    {
+        startPage();
+        List<SysUser> list = userService.selectUserList(user);
+        return getDataTable(list);
+    }
+
+    @Log(title = "用户管理", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('system:user:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, SysUser user)
+    {
+        List<SysUser> list = userService.selectUserList(user);
+        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
+        util.exportExcel(response, list, "用户数据");
+    }
+
+    @Log(title = "用户管理", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('system:user:import')")
+    @PostMapping("/importData")
+    public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
+    {
+        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
+        List<SysUser> userList = util.importExcel(file.getInputStream());
+        String operName = getUsername();
+        String message = userService.importUser(userList, updateSupport, operName);
+        return AjaxResult.success(message);
+    }
+
+    @PostMapping("/importTemplate")
+    public void importTemplate(HttpServletResponse response)
+    {
+        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
+        util.importTemplateExcel(response, "用户数据");
+    }
+
+    /**
+     * 根据用户编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:query')")
+    @GetMapping(value = { "/", "/{userId}" })
+    public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId)
+    {
+        userService.checkUserDataScope(userId);
+        AjaxResult ajax = AjaxResult.success();
+        List<SysRole> roles = roleService.selectRoleAll();
+        ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
+        ajax.put("posts", postService.selectPostAll());
+        if (StringUtils.isNotNull(userId))
+        {
+            SysUser sysUser = userService.selectUserById(userId);
+            ajax.put(AjaxResult.DATA_TAG, sysUser);
+            ajax.put("postIds", postService.selectPostListByUserId(userId));
+            ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList()));
+        }
+        return ajax;
+    }
+
+    /**
+     * 新增用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:add')")
+    @Log(title = "用户管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody SysUser user)
+    {
+        if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(user.getUserName())))
+        {
+            return AjaxResult.error("新增用户'" + user.getUserName() + "'失败,登录账号已存在");
+        }
+        else if (StringUtils.isNotEmpty(user.getPhonenumber())
+                && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user)))
+        {
+            return AjaxResult.error("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
+        }
+        else if (StringUtils.isNotEmpty(user.getEmail())
+                && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user)))
+        {
+            return AjaxResult.error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
+        }
+        user.setCreateBy(getUsername());
+        user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
+        return toAjax(userService.insertUser(user));
+    }
+
+    /**
+     * 修改用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody SysUser user)
+    {
+        userService.checkUserAllowed(user);
+        userService.checkUserDataScope(user.getUserId());
+        if (StringUtils.isNotEmpty(user.getPhonenumber())
+                && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user)))
+        {
+            return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
+        }
+        else if (StringUtils.isNotEmpty(user.getEmail())
+                && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user)))
+        {
+            return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在");
+        }
+        user.setUpdateBy(getUsername());
+        return toAjax(userService.updateUser(user));
+    }
+
+    /**
+     * 删除用户
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:remove')")
+    @Log(title = "用户管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{userIds}")
+    public AjaxResult remove(@PathVariable Long[] userIds)
+    {
+        if (ArrayUtils.contains(userIds, getUserId()))
+        {
+            return error("当前用户不能删除");
+        }
+        return toAjax(userService.deleteUserByIds(userIds));
+    }
+
+    /**
+     * 重置密码
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:resetPwd')")
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/resetPwd")
+    public AjaxResult resetPwd(@RequestBody SysUser user)
+    {
+        userService.checkUserAllowed(user);
+        userService.checkUserDataScope(user.getUserId());
+        user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
+        user.setUpdateBy(getUsername());
+        return toAjax(userService.resetPwd(user));
+    }
+
+    /**
+     * 状态修改
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/changeStatus")
+    public AjaxResult changeStatus(@RequestBody SysUser user)
+    {
+        userService.checkUserAllowed(user);
+        userService.checkUserDataScope(user.getUserId());
+        user.setUpdateBy(getUsername());
+        return toAjax(userService.updateUserStatus(user));
+    }
+
+    /**
+     * 根据用户编号获取授权角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:query')")
+    @GetMapping("/authRole/{userId}")
+    public AjaxResult authRole(@PathVariable("userId") Long userId)
+    {
+        AjaxResult ajax = AjaxResult.success();
+        SysUser user = userService.selectUserById(userId);
+        List<SysRole> roles = roleService.selectRolesByUserId(userId);
+        ajax.put("user", user);
+        ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
+        return ajax;
+    }
+
+    /**
+     * 用户授权角色
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
+    @Log(title = "用户管理", businessType = BusinessType.GRANT)
+    @PutMapping("/authRole")
+    public AjaxResult insertAuthRole(Long userId, Long[] roleIds)
+    {
+        userService.checkUserDataScope(userId);
+        userService.insertUserAuth(userId, roleIds);
+        return success();
+    }
+
+    /**
+     * 获取部门树列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:list')")
+    @GetMapping("/deptTree")
+    public AjaxResult deptTree(SysDept dept)
+    {
+        return AjaxResult.success(deptService.selectDeptTreeList(dept));
+    }
+}

+ 24 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/SwaggerController.java

@@ -0,0 +1,24 @@
+package com.ruoyi.web.controller.tool;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import com.ruoyi.common.core.controller.BaseController;
+
+/**
+ * swagger 接口
+ * 
+ * @author ruoyi
+ */
+@Controller
+@RequestMapping("/tool/swagger")
+public class SwaggerController extends BaseController
+{
+    @PreAuthorize("@ss.hasPermi('tool:swagger:view')")
+    @GetMapping()
+    public String index()
+    {
+        return redirect("/swagger-ui.html");
+    }
+}

+ 183 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/TestController.java

@@ -0,0 +1,183 @@
+package com.ruoyi.web.controller.tool;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.utils.StringUtils;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import io.swagger.annotations.ApiOperation;
+
+/**
+ * swagger 用户测试方法
+ * 
+ * @author ruoyi
+ */
+@Api("用户信息管理")
+@RestController
+@RequestMapping("/test/user")
+public class TestController extends BaseController
+{
+    private final static Map<Integer, UserEntity> users = new LinkedHashMap<Integer, UserEntity>();
+    {
+        users.put(1, new UserEntity(1, "admin", "admin123", "15888888888"));
+        users.put(2, new UserEntity(2, "ry", "admin123", "15666666666"));
+    }
+
+    @ApiOperation("获取用户列表")
+    @GetMapping("/list")
+    public R<List<UserEntity>> userList()
+    {
+        List<UserEntity> userList = new ArrayList<UserEntity>(users.values());
+        return R.ok(userList);
+    }
+
+    @ApiOperation("获取用户详细")
+    @ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class)
+    @GetMapping("/{userId}")
+    public R<UserEntity> getUser(@PathVariable Integer userId)
+    {
+        if (!users.isEmpty() && users.containsKey(userId))
+        {
+            return R.ok(users.get(userId));
+        }
+        else
+        {
+            return R.fail("用户不存在");
+        }
+    }
+
+    @ApiOperation("新增用户")
+    @ApiImplicitParams({
+        @ApiImplicitParam(name = "userId", value = "用户id", dataType = "Integer", dataTypeClass = Integer.class),
+        @ApiImplicitParam(name = "username", value = "用户名称", dataType = "String", dataTypeClass = String.class),
+        @ApiImplicitParam(name = "password", value = "用户密码", dataType = "String", dataTypeClass = String.class),
+        @ApiImplicitParam(name = "mobile", value = "用户手机", dataType = "String", dataTypeClass = String.class)
+    })
+    @PostMapping("/save")
+    public R<String> save(UserEntity user)
+    {
+        if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
+        {
+            return R.fail("用户ID不能为空");
+        }
+        users.put(user.getUserId(), user);
+        return R.ok();
+    }
+
+    @ApiOperation("更新用户")
+    @PutMapping("/update")
+    public R<String> update(@RequestBody UserEntity user)
+    {
+        if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
+        {
+            return R.fail("用户ID不能为空");
+        }
+        if (users.isEmpty() || !users.containsKey(user.getUserId()))
+        {
+            return R.fail("用户不存在");
+        }
+        users.remove(user.getUserId());
+        users.put(user.getUserId(), user);
+        return R.ok();
+    }
+
+    @ApiOperation("删除用户信息")
+    @ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class)
+    @DeleteMapping("/{userId}")
+    public R<String> delete(@PathVariable Integer userId)
+    {
+        if (!users.isEmpty() && users.containsKey(userId))
+        {
+            users.remove(userId);
+            return R.ok();
+        }
+        else
+        {
+            return R.fail("用户不存在");
+        }
+    }
+}
+
+@ApiModel(value = "UserEntity", description = "用户实体")
+class UserEntity
+{
+    @ApiModelProperty("用户ID")
+    private Integer userId;
+
+    @ApiModelProperty("用户名称")
+    private String username;
+
+    @ApiModelProperty("用户密码")
+    private String password;
+
+    @ApiModelProperty("用户手机")
+    private String mobile;
+
+    public UserEntity()
+    {
+
+    }
+
+    public UserEntity(Integer userId, String username, String password, String mobile)
+    {
+        this.userId = userId;
+        this.username = username;
+        this.password = password;
+        this.mobile = mobile;
+    }
+
+    public Integer getUserId()
+    {
+        return userId;
+    }
+
+    public void setUserId(Integer userId)
+    {
+        this.userId = userId;
+    }
+
+    public String getUsername()
+    {
+        return username;
+    }
+
+    public void setUsername(String username)
+    {
+        this.username = username;
+    }
+
+    public String getPassword()
+    {
+        return password;
+    }
+
+    public void setPassword(String password)
+    {
+        this.password = password;
+    }
+
+    public String getMobile()
+    {
+        return mobile;
+    }
+
+    public void setMobile(String mobile)
+    {
+        this.mobile = mobile;
+    }
+}

+ 125 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/core/config/SwaggerConfig.java

@@ -0,0 +1,125 @@
+package com.ruoyi.web.core.config;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import com.ruoyi.common.config.RuoYiConfig;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.models.auth.In;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.ApiKey;
+import springfox.documentation.service.AuthorizationScope;
+import springfox.documentation.service.Contact;
+import springfox.documentation.service.SecurityReference;
+import springfox.documentation.service.SecurityScheme;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spi.service.contexts.SecurityContext;
+import springfox.documentation.spring.web.plugins.Docket;
+
+/**
+ * Swagger2的接口配置
+ * 
+ * @author ruoyi
+ */
+@Configuration
+public class SwaggerConfig
+{
+    /** 系统基础配置 */
+    @Autowired
+    private RuoYiConfig ruoyiConfig;
+
+    /** 是否开启swagger */
+    @Value("${swagger.enabled}")
+    private boolean enabled;
+
+    /** 设置请求的统一前缀 */
+    @Value("${swagger.pathMapping}")
+    private String pathMapping;
+
+    /**
+     * 创建API
+     */
+    @Bean
+    public Docket createRestApi()
+    {
+        return new Docket(DocumentationType.OAS_30)
+                // 是否启用Swagger
+                .enable(enabled)
+                // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
+                .apiInfo(apiInfo())
+                // 设置哪些接口暴露给Swagger展示
+                .select()
+                // 扫描所有有注解的api,用这种方式更灵活
+                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
+                // 扫描指定包中的swagger注解
+                // .apis(RequestHandlerSelectors.basePackage("com.ruoyi.project.tool.swagger"))
+                // 扫描所有 .apis(RequestHandlerSelectors.any())
+                .paths(PathSelectors.any())
+                .build()
+                /* 设置安全模式,swagger可以设置访问token */
+                .securitySchemes(securitySchemes())
+                .securityContexts(securityContexts())
+                .pathMapping(pathMapping);
+    }
+
+    /**
+     * 安全模式,这里指定token通过Authorization头请求头传递
+     */
+    private List<SecurityScheme> securitySchemes()
+    {
+        List<SecurityScheme> apiKeyList = new ArrayList<SecurityScheme>();
+        apiKeyList.add(new ApiKey("Authorization", "Authorization", In.HEADER.toValue()));
+        return apiKeyList;
+    }
+
+    /**
+     * 安全上下文
+     */
+    private List<SecurityContext> securityContexts()
+    {
+        List<SecurityContext> securityContexts = new ArrayList<>();
+        securityContexts.add(
+                SecurityContext.builder()
+                        .securityReferences(defaultAuth())
+                        .operationSelector(o -> o.requestMappingPattern().matches("/.*"))
+                        .build());
+        return securityContexts;
+    }
+
+    /**
+     * 默认的安全上引用
+     */
+    private List<SecurityReference> defaultAuth()
+    {
+        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
+        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
+        authorizationScopes[0] = authorizationScope;
+        List<SecurityReference> securityReferences = new ArrayList<>();
+        securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
+        return securityReferences;
+    }
+
+    /**
+     * 添加摘要信息
+     */
+    private ApiInfo apiInfo()
+    {
+        // 用ApiInfoBuilder进行定制
+        return new ApiInfoBuilder()
+                // 设置标题
+                .title("标题:若依管理系统_接口文档")
+                // 描述
+                .description("描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...")
+                // 作者信息
+                .contact(new Contact(ruoyiConfig.getName(), null, null))
+                // 版本
+                .version("版本号:" + ruoyiConfig.getVersion())
+                .build();
+    }
+}

+ 1 - 0
ruoyi-admin/src/main/resources/META-INF/spring-devtools.properties

@@ -0,0 +1 @@
+restart.include.json=/com.alibaba.fastjson.*.jar

+ 57 - 0
ruoyi-admin/src/main/resources/application-druid.yml

@@ -0,0 +1,57 @@
+# 数据源配置
+spring:
+    datasource:
+        type: com.alibaba.druid.pool.DruidDataSource
+        driverClassName: com.mysql.cj.jdbc.Driver
+        druid:
+            # 主库数据源
+            master:
+                url: jdbc:mysql://localhost:3306/ry-admin?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                username: root
+                password: 123456
+            # 从库数据源
+            slave:
+                # 从数据源开关/默认关闭
+                enabled: false
+                url:
+                username:
+                password:
+            # 初始连接数
+            initialSize: 5
+            # 最小连接池数量
+            minIdle: 10
+            # 最大连接池数量
+            maxActive: 20
+            # 配置获取连接等待超时的时间
+            maxWait: 60000
+            # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+            timeBetweenEvictionRunsMillis: 60000
+            # 配置一个连接在池中最小生存的时间,单位是毫秒
+            minEvictableIdleTimeMillis: 300000
+            # 配置一个连接在池中最大生存的时间,单位是毫秒
+            maxEvictableIdleTimeMillis: 900000
+            # 配置检测连接是否有效
+            validationQuery: SELECT 1 FROM DUAL
+            testWhileIdle: true
+            testOnBorrow: false
+            testOnReturn: false
+            webStatFilter:
+                enabled: true
+            statViewServlet:
+                enabled: true
+                # 设置白名单,不填则允许所有访问
+                allow:
+                url-pattern: /druid/*
+                # 控制台管理用户名和密码
+                login-username: ruoyi
+                login-password: 123456
+            filter:
+                stat:
+                    enabled: true
+                    # 慢SQL记录
+                    log-slow-sql: true
+                    slow-sql-millis: 1000
+                    merge-sql: true
+                wall:
+                    config:
+                        multi-statement-allow: true

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

@@ -0,0 +1,80 @@
+# 数据源配置
+spring:
+  # 文件上传
+  servlet:
+    multipart:
+      # 单个文件大小
+      max-file-size: 200MB
+      # 设置总上传的文件大小
+      max-request-size: 500MB
+  redis:
+    host: 10.7.55.11
+  datasource:
+    type: com.alibaba.druid.pool.DruidDataSource
+    driverClassName: com.mysql.cj.jdbc.Driver
+    druid:
+      # 主库数据源
+      master:
+        url: jdbc:mysql://10.7.55.11:3306/ry-admin?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+        username: root
+        password: PXB@admin123
+      # 从库数据源
+      slave:
+        # 从数据源开关/默认关闭
+        enabled: false
+        url:
+        username:
+        password:
+      # 初始连接数
+      initialSize: 5
+      # 最小连接池数量
+      minIdle: 10
+      # 最大连接池数量
+      maxActive: 20
+      # 配置获取连接等待超时的时间
+      maxWait: 60000
+      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+      timeBetweenEvictionRunsMillis: 60000
+      # 配置一个连接在池中最小生存的时间,单位是毫秒
+      minEvictableIdleTimeMillis: 300000
+      # 配置一个连接在池中最大生存的时间,单位是毫秒
+      maxEvictableIdleTimeMillis: 900000
+      # 配置检测连接是否有效
+      validationQuery: SELECT 1 FROM DUAL
+      testWhileIdle: true
+      testOnBorrow: false
+      testOnReturn: false
+      webStatFilter:
+        enabled: true
+      statViewServlet:
+        enabled: true
+        # 设置白名单,不填则允许所有访问
+        allow:
+        url-pattern: /druid/*
+        # 控制台管理用户名和密码
+        login-username: ruoyi
+        login-password: 123456
+      filter:
+        stat:
+          enabled: true
+          # 慢SQL记录
+          log-slow-sql: true
+          slow-sql-millis: 1000
+          merge-sql: true
+        wall:
+          config:
+            multi-statement-allow: true
+# 项目相关配置
+ruoyi:
+  # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
+  profile: /home/uploadPath
+# 定时自动审核意见反馈设置
+schedule:
+  feedback:
+    enable: true
+    initial-delay: 10
+    fixed-delay: 120
+
+service:
+  report:
+    service-url: http://10.7.55.1:51000/social-em/em/myEventInfo/findPageXcx

+ 147 - 0
ruoyi-admin/src/main/resources/application.yml

@@ -0,0 +1,147 @@
+# 项目相关配置
+ruoyi:
+  # 名称
+  name: RuoYi
+  # 版本
+  version: 3.8.3
+  # 版权年份
+  copyrightYear: 2022
+  # 实例演示开关
+  demoEnabled: true
+  # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
+  profile: D:/ruoyi/uploadPath
+  # 获取ip地址开关
+  addressEnabled: false
+  # 验证码类型 math 数组计算 char 字符验证
+  captchaType: math
+
+# 开发环境配置
+server:
+  # 服务器的HTTP端口,默认为8080
+  port: 8080
+  servlet:
+    # 应用的访问路径
+    context-path: /
+  tomcat:
+    # tomcat的URI编码
+    uri-encoding: UTF-8
+    # 连接数满后的排队数,默认为100
+    accept-count: 1000
+    threads:
+      # tomcat最大线程数,默认为200
+      max: 800
+      # Tomcat启动初始化的线程数,默认值10
+      min-spare: 100
+
+# 日志配置
+logging:
+  level:
+    com.ruoyi: debug
+    org.springframework: warn
+    com.cclotus: debug
+
+# 用户配置
+user:
+  password:
+    # 密码最大错误次数
+    maxRetryCount: 5
+    # 密码锁定时间(默认10分钟)
+    lockTime: 10
+
+# Spring配置
+spring:
+  # 资源信息
+  messages:
+    # 国际化资源文件路径
+    basename: i18n/messages
+  profiles:
+    active: druid
+  # 文件上传
+  servlet:
+    multipart:
+      # 单个文件大小
+      max-file-size: 200MB
+      # 设置总上传的文件大小
+      max-request-size: 500MB
+  # 服务模块
+  devtools:
+    restart:
+      # 热部署开关
+      enabled: true
+  # 处理springboot版本过高和swagger冲突的问题
+  mvc:
+    pathmatch:
+      matching-strategy: ant_path_matcher
+  # redis 配置
+  redis:
+    # 地址
+    host: localhost
+    # 端口,默认为6379
+    port: 6379
+    # 数据库索引
+    database: 0
+    # 密码
+    password:
+    # 连接超时时间
+    timeout: 10s
+    lettuce:
+      pool:
+        # 连接池中的最小空闲连接
+        min-idle: 0
+        # 连接池中的最大空闲连接
+        max-idle: 8
+        # 连接池的最大数据库连接数
+        max-active: 8
+        # #连接池最大阻塞等待时间(使用负值表示没有限制)
+        max-wait: -1ms
+
+# token配置
+token:
+  # 令牌自定义标识
+  header: Authorization
+  # 令牌密钥
+  secret: abcdefghijklmnopqrstuvwxyz
+  # 令牌有效期(默认30分钟)
+  expireTime: 30
+
+# MyBatis配置
+mybatis-plus:
+  # 搜索指定包别名
+  typeAliasesPackage: com.ruoyi.**.domain
+  # 配置mapper的扫描,找到所有的mapper.xml映射文件
+  mapperLocations: classpath*:mapper/**/*Mapper.xml
+  # 加载全局的配置文件
+  configLocation: classpath:mybatis/mybatis-config.xml
+
+# PageHelper分页插件
+pagehelper:
+  helperDialect: mysql
+  supportMethodsArguments: true
+  params: count=countSql
+
+# Swagger配置
+swagger:
+  # 是否开启swagger
+  enabled: true
+  # 请求前缀
+  pathMapping: /dev-api
+
+# 防止XSS攻击
+xss:
+  # 过滤开关
+  enabled: true
+  # 排除链接(多个用逗号分隔)
+  excludes: /system/notice
+  # 匹配链接
+  urlPatterns: /system/*,/monitor/*,/tool/*
+
+schedule:
+  feedback:
+    enable: true
+    initial-delay: 5
+    fixed-delay: 1000
+
+service:
+  report:
+    service-url: https://pingyiban.com/report-api/social-em/em/myEventInfo/findPageXcx
+

+ 24 - 0
ruoyi-admin/src/main/resources/banner.txt

@@ -0,0 +1,24 @@
+Application Version: ${ruoyi.version}
+Spring Boot Version: ${spring-boot.version}
+////////////////////////////////////////////////////////////////////
+//                          _ooOoo_                               //
+//                         o8888888o                              //
+//                         88" . "88                              //
+//                         (| ^_^ |)                              //
+//                         O\  =  /O                              //
+//                      ____/`---'\____                           //
+//                    .'  \\|     |//  `.                         //
+//                   /  \\|||  :  |||//  \                        //
+//                  /  _||||| -:- |||||-  \                       //
+//                  |   | \\\  -  /// |   |                       //
+//                  | \_|  ''\---/''  |   |                       //
+//                  \  .-\__  `-`  ___/-. /                       //
+//                ___`. .'  /--.--\  `. . ___                     //
+//              ."" '<  `.___\_<|>_/___.'  >'"".                  //
+//            | | :  `- \`.;`\ _ /`;.`/ - ` : | |                 //
+//            \  \ `-.   \_ __\ /__ _/   .-` /  /                 //
+//      ========`-.____`-.___\_____/___.-`____.-'========         //
+//                           `=---='                              //
+//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^        //
+//             佛祖保佑       永不宕机      永无BUG               //
+////////////////////////////////////////////////////////////////////

+ 37 - 0
ruoyi-admin/src/main/resources/i18n/messages.properties

@@ -0,0 +1,37 @@
+#错误消息
+not.null=* 必须填写
+user.jcaptcha.error=验证码错误
+user.jcaptcha.expire=验证码已失效
+user.not.exists=用户不存在/密码错误
+user.password.not.match=用户不存在/密码错误
+user.password.retry.limit.count=密码输入错误{0}次
+user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
+user.password.delete=对不起,您的账号已被删除
+user.blocked=用户已封禁,请联系管理员
+role.blocked=角色已封禁,请联系管理员
+user.logout.success=退出成功
+
+length.not.valid=长度必须在{min}到{max}个字符之间
+
+user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
+user.password.not.valid=* 5-50个字符
+ 
+user.email.not.valid=邮箱格式错误
+user.mobile.phone.number.not.valid=手机号格式错误
+user.login.success=登录成功
+user.register.success=注册成功
+user.notfound=请重新登录
+user.forcelogout=管理员强制退出,请重新登录
+user.unknown.error=未知错误,请重新登录
+
+##文件上传消息
+upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
+upload.filename.exceed.length=上传的文件名最长{0}个字符
+
+##权限
+no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
+no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
+no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
+no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
+no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
+no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]

+ 93 - 0
ruoyi-admin/src/main/resources/logback.xml

@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="logs" />
+    <!-- 日志输出格式 -->
+	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+
+	<!-- 控制台输出 -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+	</appender>
+
+	<!-- 系统日志输出 -->
+	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+	</appender>
+
+	<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+			<!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+			<!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+	<!-- 用户访问日志输出  -->
+    <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/sys-user.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 按天回滚 daily -->
+            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+
+	<!-- 系统模块日志级别控制  -->
+	<logger name="com.ruoyi" level="info" />
+	<!-- Spring日志级别控制  -->
+	<logger name="org.springframework" level="warn" />
+
+	<root level="info">
+		<appender-ref ref="console" />
+	</root>
+
+	<!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="file_info" />
+        <appender-ref ref="file_error" />
+    </root>
+
+	<!--系统用户操作日志-->
+    <logger name="sys-user" level="info">
+        <appender-ref ref="sys-user"/>
+    </logger>
+</configuration>

+ 20 - 0
ruoyi-admin/src/main/resources/mybatis/mybatis-config.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration
+PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-config.dtd">
+<configuration>
+    <!-- 全局参数 -->
+    <settings>
+        <!-- 使全局的映射器启用或禁用缓存 -->
+        <setting name="cacheEnabled"             value="true"   />
+        <!-- 允许JDBC 支持自动生成主键 -->
+        <setting name="useGeneratedKeys"         value="true"   />
+        <!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
+        <setting name="defaultExecutorType"      value="SIMPLE" />
+		<!-- 指定 MyBatis 所用日志的具体实现 -->
+        <setting name="logImpl"                  value="SLF4J"  />
+        <!-- 使用驼峰命名法转换字段 -->
+		<!-- <setting name="mapUnderscoreToCamelCase" value="true"/> -->
+	</settings>
+	
+</configuration>

+ 16 - 0
ruoyi-cms/build.gradle

@@ -0,0 +1,16 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ *
+ * This project uses @Incubating APIs which are subject to change.
+ */
+
+plugins {
+    id 'com.ruoyi.java-library'
+    id 'com.ruoyi.kotlin-library'
+}
+
+dependencies {
+    api project(':ruoyi-common')
+}
+group = 'cc-lotus.ruoyi'
+description = 'ruoyi-cms 内容管理模块'

+ 131 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/controller/admin/CmsPostController.kt

@@ -0,0 +1,131 @@
+package com.cclotus.cms.controller.admin
+
+import com.cclotus.cms.domain.CmsPost
+import com.cclotus.cms.domain.CmsReviewForm
+import com.cclotus.cms.service.CmsPostService
+import com.cclotus.cms.util.CmsConstant
+import com.cclotus.cms.vo.CmsPostFormReq
+import com.ruoyi.common.annotation.Log
+import com.ruoyi.common.core.controller.BaseController
+import com.ruoyi.common.core.domain.AjaxResult
+import com.ruoyi.common.core.page.TableDataInfo
+import com.ruoyi.common.enums.BusinessType
+import com.ruoyi.common.reqParam.PageParam
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.validation.annotation.Validated
+import org.springframework.web.bind.annotation.*
+
+
+@Suppress("NAME_SHADOWING")
+@RestController
+@RequestMapping("/cms")
+class CmsPostController : BaseController() {
+
+    @Autowired
+    lateinit var cmsPostService: CmsPostService
+
+    /**
+     * 查询发布内容列表
+     *
+     * @param contentType  内容数据类型
+     * @param termAlias 分类别名
+     * @param paging    是否分页,默认分页
+     * @param deep      是否显示栏目下子栏目的文章
+     * @param post      过滤条件
+     */
+    @GetMapping(path = ["/{contentType:article|page|video|picture|custom}/{termAlias}/list", "/{contentType:article|page|video|picture|custom}/list"])
+    fun list(
+        @PathVariable("contentType") contentType: String,
+        @PathVariable(name = "termAlias", required = false) termAlias: String?,
+        paging: Boolean?,
+        deep: Boolean?,
+        post: CmsPost,
+        pageParam: PageParam?,
+        content: Boolean?,
+        @RequestParam(required = false) query: Map<String, String>?,
+    ): TableDataInfo {
+        val pageParam = if (paging != null && !paging) null else pageParam
+        post.contentType = contentType
+        // 提取扩展属性查询条件
+        val meta = query?.filterKeys { it.startsWith(prefix = CmsConstant.META_QUERY_PREFIX, ignoreCase = true) }
+            ?.mapKeys { it.key.removePrefix(CmsConstant.META_QUERY_PREFIX) }
+        return cmsPostService.list(termAlias, deep ?: false, post, pageParam, content ?: false, meta)
+    }
+
+    /**
+     * 根据页面别名获取页面详情
+     *
+     * @param contentType 页面别名
+     * @return 页面详情信息
+     */
+    @GetMapping("/{contentType:article|page|video|picture|custom}/{post}")
+    fun detail(
+        @PathVariable("contentType") contentType: String,
+        @PathVariable("post") post: String,
+    ): AjaxResult {
+        return AjaxResult.success(cmsPostService.detail(contentType, post))
+    }
+
+    /**
+     * 新增发布内容
+     */
+    @Log(title = "发布内容管理", businessType = BusinessType.INSERT)
+    @PostMapping("/{contentType:article|page|video|picture|custom}")
+    fun add(
+        @PathVariable("contentType") contentType: String,
+        @Validated @RequestBody cmsPostFormReq: CmsPostFormReq,
+    ): AjaxResult {
+        cmsPostFormReq.contentType = contentType
+        cmsPostFormReq.origin = "admin"
+        // 管理员发布的文章默认为审核通过状态
+        cmsPostFormReq.review = "1"
+        return toAjax(cmsPostService.create(cmsPostFormReq, username))
+    }
+
+    /**
+     * 修改发布内容
+     */
+    @Log(title = "发布内容管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{contentType:article|page|video|picture|custom}")
+    fun edit(
+        @PathVariable("contentType") contentType: String,
+        @Validated @RequestBody cmsPostFormReq: CmsPostFormReq,
+    ): AjaxResult {
+        return toAjax(cmsPostService.update(cmsPostFormReq, username))
+    }
+
+    /**
+     * 删除发布内容
+     *
+     * @param contentType    内容类型
+     * @param postIds 内容ID列表
+     */
+    @Log(title = "发布内容管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{contentType:article|page|video|picture|custom}/{postIds}")
+    fun remove(
+        @PathVariable("contentType") contentType: String,
+        @PathVariable("postIds") postIds: Array<Int>,
+    ): AjaxResult {
+        return toAjax(cmsPostService.deleteByIds(postIds))
+    }
+
+    /**
+     * 审核内容
+     *
+     * @param contentType 内容类型
+     * @param post 内容标识
+     * @param reviewForm 审核信息
+     * @return 处理结果
+     */
+    @Log(title = "审核内容", businessType = BusinessType.UPDATE)
+    @PostMapping("/{contentType:article|page|video|picture|custom}/review/{post}")
+    fun review(
+        @PathVariable("contentType") contentType: String,
+        @PathVariable("post") post: String,
+        @RequestBody @Validated reviewForm: CmsReviewForm,
+    ): AjaxResult {
+        cmsPostService.review(contentType, post, reviewForm)
+        return AjaxResult.success()
+    }
+}
+

+ 91 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/controller/admin/CmsTermController.kt

@@ -0,0 +1,91 @@
+package com.cclotus.cms.controller.admin
+
+import com.cclotus.cms.domain.CmsTerm
+import com.cclotus.cms.service.CmsTermService
+import com.cclotus.cms.vo.CmsTermForm
+import com.ruoyi.common.annotation.Log
+import com.ruoyi.common.core.controller.BaseController
+import com.ruoyi.common.core.domain.AjaxResult
+import com.ruoyi.common.core.page.TableDataInfo
+import com.ruoyi.common.enums.BusinessType
+import com.ruoyi.common.reqParam.PageParam
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.validation.annotation.Validated
+import org.springframework.web.bind.annotation.*
+
+@Suppress("NAME_SHADOWING")
+@RestController
+@RequestMapping("/cms")
+class CmsTermController : BaseController() {
+
+    @Autowired
+    lateinit var cmsTermService: CmsTermService
+
+    /**
+     * 查询分类列表
+     */
+    @GetMapping("/{taxonomy:wxAppMenu|menu|catalog|tag}/list")
+    fun list(
+        @PathVariable("taxonomy") taxonomy: String,
+        cmsTerm: CmsTerm,
+        paging: Boolean?,
+        pageParam: PageParam?,
+    ): TableDataInfo {
+        val pageParam = if (paging != null && !paging) null else pageParam
+        cmsTerm.taxonomy = taxonomy
+        return cmsTermService.list(cmsTerm, pageParam)
+    }
+
+    /**
+     * 根据分类别名或Id获取分类详情
+     *
+     * @param taxonomy 分类系统
+     * @param term 分类标识
+     * @return 分类详细信息
+     */
+    @GetMapping("/{taxonomy:wxAppMenu|menu|catalog|tag}/{term}")
+    fun detail(
+        @PathVariable("taxonomy") taxonomy: String,
+        @PathVariable("term") term: String,
+    ): AjaxResult {
+        return AjaxResult.success(cmsTermService.detail(taxonomy, term))
+    }
+
+    /**
+     * 新增分类
+     */
+    @Log(title = "分类管理", businessType = BusinessType.INSERT)
+    @PostMapping("/{taxonomy:wxAppMenu|menu|catalog|tag}")
+    fun add(
+        @PathVariable("taxonomy") taxonomy: String,
+        @Validated @RequestBody cmsTerm: CmsTermForm,
+    ): AjaxResult {
+        cmsTerm.taxonomy = taxonomy
+        return toAjax(cmsTermService.create(cmsTerm))
+    }
+
+    /**
+     * 修改分类
+     */
+    @Log(title = "分类管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{taxonomy:wxAppMenu|menu|catalog|tag}")
+    fun edit(
+        @PathVariable("taxonomy") taxonomy: String,
+        @RequestBody cmsTerm: CmsTermForm,
+    ): AjaxResult {
+        cmsTerm.taxonomy = taxonomy
+        return toAjax(cmsTermService.update(cmsTerm))
+    }
+
+    /**
+     * 删除分类
+     */
+    @Log(title = "分类管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{taxonomy:wxAppMenu|menu|catalog|tag}/{termIds}")
+    fun remove(
+        @PathVariable("taxonomy") taxonomy: String,
+        @PathVariable termIds: Array<Int>,
+    ): AjaxResult {
+        return toAjax(cmsTermService.deleteByIds(termIds))
+    }
+}

+ 79 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/controller/admin/CmsTermRelationshipsController.kt

@@ -0,0 +1,79 @@
+package com.cclotus.cms.controller.admin
+
+import com.cclotus.cms.service.CmsTermRelationshipsService
+import com.ruoyi.common.annotation.Log
+import com.ruoyi.common.core.controller.BaseController
+import com.ruoyi.common.core.domain.AjaxResult
+import com.ruoyi.common.core.page.TableDataInfo
+import com.ruoyi.common.enums.BusinessType
+import com.ruoyi.common.reqParam.PageParam
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.web.bind.annotation.*
+
+
+@RestController
+@RequestMapping("/cms/relation")
+class CmsTermRelationshipsController : BaseController() {
+
+    @Autowired
+    lateinit var cmsTermRelationshipsService: CmsTermRelationshipsService
+
+    /**
+     * 查询termId绑定的对象列表
+     */
+    @GetMapping(path = ["/{termId}/objects"])
+    fun listObjects(
+        @PathVariable("termId") termId: Int,
+        pageParam: PageParam,
+    ): TableDataInfo {
+        return getDataTable(cmsTermRelationshipsService.getBindingObjectsByTermId(termId, pageParam))
+    }
+
+    /**
+     * 为内容绑定一组分类,如:一篇文章添加多个标签
+     */
+    @Log(title = "关联关系管理", businessType = BusinessType.INSERT)
+    @PostMapping("/p/{postId}/t/{termIds}")
+    fun bindObjectToTerms(
+        @PathVariable("postId") postId: Int,
+        @PathVariable("termIds") termIds: Array<Int>,
+    ): AjaxResult {
+        return toAjax(cmsTermRelationshipsService.bindPostToTerms(postId, termIds))
+    }
+
+    /**
+     * 为分类绑定一组对象,如:一个栏目下添加多篇文章
+     */
+    @Log(title = "关联关系管理", businessType = BusinessType.INSERT)
+    @PostMapping("/t/{termId}/o/{objectIds}")
+    fun bindTermToObjects(
+        @PathVariable("termId") termId: Int,
+        @PathVariable("objectIds") objectIds: Array<Int>,
+    ): AjaxResult {
+        return toAjax(cmsTermRelationshipsService.bindTermToObjects(termId, objectIds))
+    }
+
+    /**
+     * 从关联关系中,删除与termId关联的一组objectIds
+     */
+    @Log(title = "关联关系管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/t/{termId}/o/{objectIds}")
+    fun removeObjects(
+        @PathVariable("termId") termId: Int,
+        @PathVariable("objectIds") objectIds: Array<Int>,
+    ): AjaxResult {
+        return toAjax(cmsTermRelationshipsService.deleteObjectIds(termId, objectIds))
+    }
+
+    /**
+     * 从关联关系中,删除与objectId关联的一组termIds
+     */
+    @Log(title = "关联关系管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/o/{objectId}/t/{termIds}")
+    fun removeTerms(
+        @PathVariable("objectId") objectId: Int,
+        @PathVariable("termIds") termIds: Array<Int>,
+    ): AjaxResult {
+        return toAjax(cmsTermRelationshipsService.deleteTermIds(objectId, termIds))
+    }
+}

+ 180 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/controller/wxapp/CmsController.kt

@@ -0,0 +1,180 @@
+package com.cclotus.cms.controller.wxapp
+
+import com.cclotus.cms.domain.CmsPost
+import com.cclotus.cms.domain.CmsTerm
+import com.cclotus.cms.service.CmsPostService
+import com.cclotus.cms.service.CmsTermService
+import com.cclotus.cms.util.CmsConstant
+import com.cclotus.cms.vo.CmsPostFormReq
+import com.ruoyi.common.core.domain.AjaxResult
+import com.ruoyi.common.core.page.TableDataInfo
+import com.ruoyi.common.reqParam.PageParam
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.util.Assert
+import org.springframework.validation.annotation.Validated
+import org.springframework.web.bind.annotation.*
+
+@Suppress("NAME_SHADOWING")
+@RestController
+@RequestMapping("/cms")
+class CmsController {
+
+    @Autowired
+    lateinit var cmsTermService: CmsTermService
+
+    @Autowired
+    lateinit var cmsPostService: CmsPostService
+
+    /**
+     * 查询菜单列表
+     *
+     * @param taxonomy 分类系统
+     * @param paging    是否分页,默认分页
+     * @param cmsTerm   过滤条件
+     */
+    @GetMapping("/{taxonomy:wxAppMenu|catalog|tag}/list")
+    fun listMenu(
+        @PathVariable("taxonomy") taxonomy: String,
+        cmsTerm: CmsTerm,
+        paging: Boolean?,
+        pageParam: PageParam?,
+    ): TableDataInfo? {
+        var pageParam = pageParam
+        if (paging != null && !paging) pageParam = null
+
+        cmsTerm.taxonomy = taxonomy
+        return cmsTermService.list(cmsTerm, pageParam)
+    }
+
+    /**
+     * 根据分类别名或Id获取分类详情
+     *
+     * @param taxonomy 分类系统
+     * @param term 分类标识
+     * @return 分类详细信息
+     */
+    @GetMapping("/{taxonomy:wxAppMenu|catalog|tag}/{term}")
+    fun detail(
+        @PathVariable("taxonomy") taxonomy: String,
+        @PathVariable("term") term: String,
+    ): AjaxResult {
+        return AjaxResult.success(cmsTermService.detail(taxonomy, term))
+    }
+
+    /**
+     * 查询发布内容列表
+     *
+     * @param contentType 内容类型
+     * @param termAlias 栏目别名
+     * @param paging    是否分页,默认分页
+     * @param deep      是否显示栏目下子栏目的文章
+     * @param post      过滤条件
+     * @param scope     列表范围 null:查询所有列表,myself:查询自己创建的列表
+     * @param userId    用户ID
+     */
+    @GetMapping(path = ["/{contentType:article|page|video|picture|custom}/{termAlias}/list", "/{contentType:article|page|video|picture|custom}/list"])
+    fun listPost(
+        @PathVariable("contentType") contentType: String,
+        @PathVariable(name = "termAlias", required = false) termAlias: String?,
+        paging: Boolean?,
+        deep: Boolean?,
+        post: CmsPost,
+        pageParam: PageParam?,
+        content: Boolean?,
+        scope: String?,
+        userId: String?,
+        @RequestParam(required = false) query: Map<String, String>?,
+    ): TableDataInfo? {
+        var pageParam = pageParam
+        if (paging != null && !paging) pageParam = null
+        // 根据查询范围构建查询条件
+        when (scope) {
+            "myself" -> { // 查询自己的发布内容
+                Assert.hasText(userId, "用户ID不可为空")
+                post.createBy = userId
+            }
+
+            else -> { // 默认查询所有已审核的发布内容
+                post.review = "1"
+            }
+        }
+        // 提取扩展属性查询条件
+        val meta = query?.filterKeys { it.startsWith(prefix = CmsConstant.META_QUERY_PREFIX, ignoreCase = true) }
+            ?.mapKeys { it.key.removePrefix(CmsConstant.META_QUERY_PREFIX) }
+        // 默认查询显示和发布的文章内容
+        post.visible = "1"
+        post.status = "0"
+        post.contentType = contentType
+        return cmsPostService.list(termAlias, deep ?: false, post, pageParam, content ?: false, meta)
+    }
+
+    /**
+     * 获取文章详情信息
+     *
+     * @return
+     */
+    @GetMapping("/{contentType:article|page|video|picture|custom}/{post}")
+    fun detailPost(
+        @PathVariable("contentType") contentType: String,
+        @PathVariable("post") post: String,
+    ): AjaxResult {
+        return AjaxResult.success(cmsPostService.detail(contentType, post))
+    }
+
+    /**
+     * 新增发布内容
+     *
+     * @param contentType 内容类型 custom
+     * @param cmsPostFormReq 内容详情
+     * @param termAlias 类型别名
+     * @param userId 创建用户ID
+     * @return 创建结果
+     */
+    @PostMapping("/{contentType:custom}/{termAlias}")
+    fun add(
+        @PathVariable("contentType") contentType: String,
+        @PathVariable("termAlias") termAlias: String,
+        @Validated @RequestBody cmsPostFormReq: CmsPostFormReq,
+        userId: String?,
+    ): AjaxResult {
+        Assert.hasText(userId, "用户ID不可为空")
+        cmsPostFormReq.contentType = contentType
+        // 文章审核状态为待审核
+        cmsPostFormReq.review = "0"
+        // 处理默认状态值
+        cmsPostFormReq.status = "0"
+        cmsPostFormReq.topStatus = 0
+        cmsPostFormReq.visible = "1"
+        cmsPostFormReq.origin = "user"
+        // 创建文章
+        cmsPostService.createByTermAlias(cmsPostFormReq, userId!!, termAlias)
+        return AjaxResult.success()
+    }
+
+    /**
+     * 修改发布内容
+     *
+     * @param contentType 内容类型
+     * @param cmsPostFormReq 内容详情
+     * @param userId 用户ID
+     * @return 修改结果
+     */
+    @PutMapping("/{contentType:custom}/{termAlias}")
+    fun edit(
+        @PathVariable("contentType") contentType: String,
+        @PathVariable(name = "termAlias", required = false) termAlias: String?,
+        @Validated @RequestBody cmsPostFormReq: CmsPostFormReq,
+        userId: String?,
+    ): AjaxResult {
+        Assert.hasText(userId, "用户ID不可为空")
+        cmsPostFormReq.contentType = contentType
+        // 文章审核状态为待审核
+        cmsPostFormReq.review = "0"
+        // 处理默认状态值
+        cmsPostFormReq.status = "0"
+        cmsPostFormReq.topStatus = 0
+        cmsPostFormReq.visible = "1"
+        cmsPostService.update(cmsPostFormReq, userId!!)
+        return AjaxResult.success()
+    }
+}

+ 47 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CMSBaseEntity.kt

@@ -0,0 +1,47 @@
+package com.cclotus.cms.domain
+
+import com.baomidou.mybatisplus.annotation.FieldFill
+import com.baomidou.mybatisplus.annotation.TableField
+import com.fasterxml.jackson.annotation.JsonFormat
+import java.time.LocalDateTime
+
+abstract class CMSBaseEntity {
+
+    /**
+     * 创建人
+     */
+    @TableField("create_by")
+    var createBy: String? = null
+
+    /**
+     * 创建时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    var createTime: LocalDateTime? = null
+
+    /**
+     * 修改人
+     */
+    @TableField("update_by")
+    var updateBy: String? = null
+
+    /**
+     * 修改时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    var updateTime: LocalDateTime? = null
+
+    /**
+     * 备注
+     */
+    @TableField("remark")
+    var remark: String? = null
+
+    /**
+     * 请求参数
+     */
+    @TableField(exist = false)
+    var params: MutableMap<String, Any> = mutableMapOf()
+}

+ 17 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CmsConfig.kt

@@ -0,0 +1,17 @@
+package com.cclotus.cms.domain
+
+/**
+ * 网站配置
+ */
+class CmsConfig {
+
+    var configId: Int? = null
+
+    var label: String? = null
+
+    var configKey: String? = null
+
+    var configValue: String? = null
+
+    var dataType: String? = null
+}

+ 100 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CmsPost.kt

@@ -0,0 +1,100 @@
+package com.cclotus.cms.domain
+
+import com.baomidou.mybatisplus.annotation.*
+
+/**
+ * 内容数据
+ */
+@TableName("cms_post")
+class CmsPost : CMSBaseEntity() {
+
+    /**
+     * 内容ID
+     */
+    @TableId(value = "post_id", type = IdType.AUTO)
+    var postId: Int? = null
+
+    /**
+     * 内容标题
+     */
+    @TableField("title")
+    var title: String? = null
+
+    /**
+     * 内容首图
+     */
+    @TableField("image")
+    var image: String? = null
+
+    /**
+     * 内容摘要
+     */
+    @TableField("summary")
+    var summary: String? = null
+
+    /**
+     * 内容正文类型
+     */
+    @TableField("content_type")
+    var contentType: String? = null
+
+    /**
+     * 内容正文
+     */
+    @TableField(value = "content", updateStrategy = FieldStrategy.IGNORED)
+    var content: String? = null
+
+    /**
+     * 发布状态(0:草稿 1: 发布)
+     */
+    @TableField("status")
+    var status: String? = null
+
+    /**
+     * 置顶状态(0:否 非0: 是)
+     */
+    @TableField("top_status")
+    var topStatus: Int? = null
+
+    /**
+     * 显示隐藏(0: 隐藏 1: 显示)
+     */
+    @TableField("visible")
+    var visible: String? = null
+
+    /**
+     * 页面别名(只有页面类型内容此属性才生效)
+     */
+    @TableField("alias")
+    var alias: String? = null
+
+    /**
+     * 审核状态 0待审核 1审核通过 2审核驳回
+     */
+    @TableField("review")
+    var review: String? = null
+
+    /**
+     * 审核意见
+     */
+    @TableField("comment")
+    var comment: String? = null
+
+    /**
+     * 内容发布来源
+     */
+    @TableField("origin")
+    var origin: String? = null
+
+    /**
+     * 文章所属分类
+     */
+    @TableField(exist = false)
+    var catalogs: List<CmsTerm> = mutableListOf()
+
+    /**
+     * 文章扩展属性列表
+     */
+    @TableField(exist = false)
+    var meta: List<CmsPostMeta> = mutableListOf()
+}

+ 40 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CmsPostMeta.kt

@@ -0,0 +1,40 @@
+package com.cclotus.cms.domain
+
+import com.baomidou.mybatisplus.annotation.IdType
+import com.baomidou.mybatisplus.annotation.TableField
+import com.baomidou.mybatisplus.annotation.TableId
+import com.baomidou.mybatisplus.annotation.TableName
+import javax.validation.constraints.NotNull
+
+/**
+ * 内容与扩展属性关联关系
+ */
+@TableName("cms_post_meta")
+class CmsPostMeta {
+
+    /**
+     * 自增Id
+     */
+    @TableId(value = "meta_id", type = IdType.AUTO)
+    var metaId: Int? = null
+
+    /**
+     * 内容Id
+     */
+    @TableField(value = "post_id")
+    @NotNull(message = "内容Id不可为空")
+    var postId: Int? = null
+
+    /**
+     * 扩展属性键
+     */
+    @TableField(value = "meta_key")
+    @NotNull(message = "内容的扩展属性键不可空")
+    var metaKey: String? = null
+
+    /**
+     * 扩展属性值
+     */
+    @TableField(value = "meta_value")
+    var metaValue: String? = null
+}

+ 21 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CmsReviewForm.kt

@@ -0,0 +1,21 @@
+package com.cclotus.cms.domain
+
+import javax.validation.constraints.NotBlank
+
+/**
+ * 内容审核信息
+ *
+ */
+class CmsReviewForm {
+
+    /**
+     * 审核状态
+     */
+    @NotBlank(message = "审核状态不可为空")
+    var review: String? = null
+
+    /**
+     * 审核意见
+     */
+    var comment: String? = null
+}

+ 84 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CmsTerm.kt

@@ -0,0 +1,84 @@
+package com.cclotus.cms.domain
+
+import com.baomidou.mybatisplus.annotation.IdType
+import com.baomidou.mybatisplus.annotation.TableField
+import com.baomidou.mybatisplus.annotation.TableId
+import com.baomidou.mybatisplus.annotation.TableName
+import javax.validation.constraints.NotBlank
+
+/**
+ * CMS分类用语
+ */
+@TableName("cms_term")
+class CmsTerm {
+
+    /**
+     * 分类用语ID
+     */
+    @TableId(value = "term_id", type = IdType.AUTO)
+    var termId: Int? = null
+
+    /**
+     * 上级Id
+     */
+    @TableField("parent_id")
+    var parentId: Int? = null
+
+    /**
+     * 祖级列表
+     */
+    @TableField("ancestors")
+    var ancestors: String? = null
+
+    /**
+     * 分类名称
+     */
+    @TableField("name")
+    @NotBlank(message = "分类名称不可为空")
+    var name: String? = null
+
+    /**
+     * 分类别名
+     */
+    @TableField("alias")
+    @NotBlank(message = "分类别名不可为空")
+    var alias: String? = null
+
+    /**
+     * 简要描述
+     */
+    @TableField("description")
+    var description: String? = null
+
+    /**
+     * 分类图标
+     */
+    @TableField("image")
+    var image: String? = null
+
+    /**
+     * 分类系统
+     */
+    @TableField("taxonomy")
+    @NotBlank(message = "分类系统不可为空")
+    var taxonomy: String? = null
+
+    /**
+     * 分类关联的数据类型
+     */
+    @TableField("type")
+    @NotBlank(message = "分类关联的数据类型不可为空")
+    var type: String? = null
+
+    /**
+     * 分类序号
+     */
+    @TableField("number")
+    var number: Int? = null
+
+    /**
+     * 扩展属性列表
+     */
+    @TableField(exist = false)
+    var meta: List<CmsTermMeta> = mutableListOf()
+}

+ 40 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CmsTermMeta.kt

@@ -0,0 +1,40 @@
+package com.cclotus.cms.domain
+
+import com.baomidou.mybatisplus.annotation.IdType
+import com.baomidou.mybatisplus.annotation.TableField
+import com.baomidou.mybatisplus.annotation.TableId
+import com.baomidou.mybatisplus.annotation.TableName
+import javax.validation.constraints.NotNull
+
+/**
+ * 分类扩展属性表
+ */
+@TableName("cms_term_meta")
+class CmsTermMeta {
+
+    /**
+     * 自增Id
+     */
+    @TableId(value = "meta_id", type = IdType.AUTO)
+    var metaId: Int? = null
+
+    /**
+     * 分类Id
+     */
+    @TableField(value = "term_id")
+    @NotNull(message = "分类Id不可为空")
+    var termId: Int? = null
+
+    /**
+     * 扩展属性键
+     */
+    @TableField(value = "meta_key")
+    @NotNull(message = "分类的扩展属性键不可空")
+    var metaKey: String? = null
+
+    /**
+     * 扩展属性值
+     */
+    @TableField(value = "meta_value")
+    var metaValue: String? = null
+}

+ 31 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/domain/CmsTermRelationships.kt

@@ -0,0 +1,31 @@
+package com.cclotus.cms.domain
+
+import com.baomidou.mybatisplus.annotation.IdType
+import com.baomidou.mybatisplus.annotation.TableField
+import com.baomidou.mybatisplus.annotation.TableId
+import com.baomidou.mybatisplus.annotation.TableName
+
+/**
+ * 内容与栏目关联关系
+ */
+@TableName("cms_term_relationships")
+class CmsTermRelationships {
+
+    /**
+     * 自增Id
+     */
+    @TableId(value = "rela_id", type = IdType.AUTO)
+    var relaId: Int? = null
+
+    /**
+     * 栏目Id
+     */
+    @TableField(value = "term_id")
+    var termId: Int? = null
+
+    /**
+     * 关联对象Id (关联对象:term, post, link)
+     */
+    @TableField("object_id")
+    var objectId: Int? = null
+}

+ 7 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/mapper/CmsConfigMapper.kt

@@ -0,0 +1,7 @@
+package com.cclotus.cms.mapper
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import com.cclotus.cms.domain.CmsConfig
+
+interface CmsConfigMapper : BaseMapper<CmsConfig> {
+}

+ 32 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/mapper/CmsPostMapper.kt

@@ -0,0 +1,32 @@
+package com.cclotus.cms.mapper
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import com.cclotus.cms.domain.CmsPost
+import org.apache.ibatis.annotations.Param
+
+interface CmsPostMapper : BaseMapper<CmsPost> {
+
+    /**
+     * 根据内容ID查询内容详情
+     * @param contentType 内容类型
+     * @param postId 内容ID
+     */
+    fun selectPostByPostId(@Param("contentType") contentType: String,
+                           @Param("postId") postId: Int): CmsPost?
+
+    /**
+     * 根据页面别名查询页面内容
+     *  @param contentType 内容类型
+     * @param alias 内容别名
+     */
+    fun selectPostByAlias(@Param("contentType") contentType: String,
+                          @Param("alias") alias: String): CmsPost?
+
+    /**
+     * 根据页面类型、别名以及内容Id或别名查询页面内容
+     */
+    fun selectPostEx(@Param("catalogAlias") catalogAlias:String?,
+                     @Param("postType") postType: String,
+                     @Param("postId") postId: Int?,
+                     @Param("postAlias") postAlias: String?): CmsPost?
+}

+ 16 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/mapper/CmsPostMetaMapper.kt

@@ -0,0 +1,16 @@
+package com.cclotus.cms.mapper
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import com.cclotus.cms.domain.CmsPostMeta
+import org.apache.ibatis.annotations.Param
+
+interface CmsPostMetaMapper : BaseMapper<CmsPostMeta> {
+
+    /**
+     * 根据元数据过滤条件查询内容元数据列表
+     *
+     * @param meta 元数据过滤条件
+     * @return 符合条件的内容元数据列表
+     */
+    fun selectPostMetaList(@Param("meta") meta: Map<String, String>, @Param("size") size: Int = meta.size): List<Int>
+}

+ 13 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/mapper/CmsTermMapper.kt

@@ -0,0 +1,13 @@
+package com.cclotus.cms.mapper
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import com.cclotus.cms.domain.CmsTerm
+import org.apache.ibatis.annotations.Param
+
+interface CmsTermMapper : BaseMapper<CmsTerm> {
+    fun selectOneWithMetas(@Param("taxonomy") taxonomy: String,
+                           @Param("termId") termId: Int?,
+                           @Param("alias") alias: String?): CmsTerm?
+
+    fun selectChildrenById(termId: Int): List<CmsTerm>
+}

+ 7 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/mapper/CmsTermMetaMapper.kt

@@ -0,0 +1,7 @@
+package com.cclotus.cms.mapper
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import com.cclotus.cms.domain.CmsTermMeta
+
+interface CmsTermMetaMapper : BaseMapper<CmsTermMeta> {
+}

+ 26 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/mapper/CmsTermRelationshipsMapper.kt

@@ -0,0 +1,26 @@
+package com.cclotus.cms.mapper
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper
+import com.cclotus.cms.domain.CmsPost
+import com.cclotus.cms.domain.CmsTerm
+import com.cclotus.cms.domain.CmsTermRelationships
+import org.apache.ibatis.annotations.Param
+
+interface CmsTermRelationshipsMapper : BaseMapper<CmsTermRelationships> {
+
+    fun selectCatalogRelationsByPostId(postId: Int): List<CmsTermRelationships>
+
+    fun selectCatalogTermsByPostId(postId: Int): List<CmsTerm>
+
+    fun getBindingTermsByTermId(termId: Int):  List<CmsTerm>
+
+    fun getBindingPostsByTermId(termId: Int):  List<CmsPost>
+
+    fun deleteObjectIds(@Param("objectIds") objectIds: List<Int>): Int
+
+    fun deleteObjectIdsByTermId(@Param("termId") termId: Int,
+                                @Param("objectIds") objectIds: List<Int>): Int
+
+    fun deleteTermIdsByObjectId(@Param("objectId") objectId: Int,
+                                @Param("termIds") termIds: List<Int>): Int
+}

+ 4 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/service/CmsConfigService.kt

@@ -0,0 +1,4 @@
+package com.cclotus.cms.service
+
+class CmsConfigService {
+}

+ 30 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/service/CmsPostMetaService.kt

@@ -0,0 +1,30 @@
+package com.cclotus.cms.service
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
+import com.cclotus.cms.domain.CmsPostMeta
+import com.cclotus.cms.mapper.CmsPostMetaMapper
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Service
+
+@Service
+class CmsPostMetaService {
+
+    @Autowired
+    lateinit var cmsPostMetaMapper: CmsPostMetaMapper
+
+    /**
+     * 根据内容ID列表返回扩展属性列表
+     * @param postIds 内容ID列表
+     * @return 指定内容ID的扩展属性列表
+     */
+    fun selectListByPostIds(postIds: List<Int>): List<CmsPostMeta> {
+        if (postIds.isEmpty())
+            return emptyList()
+
+        return QueryWrapper<CmsPostMeta>()
+            .`in`("post_id", postIds)
+            .let {
+                cmsPostMetaMapper.selectList(it)
+            }
+    }
+}

+ 395 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/service/CmsPostService.kt

@@ -0,0 +1,395 @@
+package com.cclotus.cms.service
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
+import com.cclotus.cms.domain.CmsPost
+import com.cclotus.cms.domain.CmsPostMeta
+import com.cclotus.cms.domain.CmsReviewForm
+import com.cclotus.cms.domain.CmsTermRelationships
+import com.cclotus.cms.mapper.CmsPostMapper
+import com.cclotus.cms.mapper.CmsPostMetaMapper
+import com.cclotus.cms.mapper.CmsTermRelationshipsMapper
+import com.cclotus.cms.util.EntityPathVariable
+import com.cclotus.cms.vo.CmsPostFormReq
+import com.cclotus.cms.vo.CmsPostFormRes
+import com.ruoyi.common.core.page.TableDataInfo
+import com.ruoyi.common.exception.ServiceException
+import com.ruoyi.common.reqParam.PageParam
+import com.ruoyi.common.utils.PageUtils
+import com.ruoyi.common.utils.TableDataInfoUtil
+import org.springframework.beans.BeanUtils
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Service
+import org.springframework.transaction.annotation.Transactional
+
+@Service
+class CmsPostService {
+
+    @Autowired
+    lateinit var cmsTermService: CmsTermService
+
+    @Autowired
+    lateinit var cmsPostMapper: CmsPostMapper
+
+    @Autowired
+    lateinit var cmsTermRelationshipsMapper: CmsTermRelationshipsMapper
+
+    @Autowired
+    lateinit var cmsPostMetaMapper: CmsPostMetaMapper
+
+    @Autowired
+    lateinit var cmsPostMetaService: CmsPostMetaService
+
+    @Autowired
+    lateinit var cmsTermMetaService: CmsTermMetaService
+
+    /**
+     * 查询内容列表
+     * @param termAlias 分类别名
+     * @param deep      是否显示栏目下子栏目的文章,默认不查询子栏目下的文章
+     * @param post      过滤条件
+     * @param pageParam 分页条件
+     * @param content   列表页是否返回内容详情
+     * @param meta      扩展属性过滤条件
+     */
+    fun list(
+        termAlias: String?,
+        deep: Boolean,
+        post: CmsPost,
+        pageParam: PageParam?,
+        content: Boolean,
+        meta: Map<String, String>? = null,
+    ): TableDataInfo {
+
+        // 如果别名不为空,查询符合条件的栏目信息
+        val terms = termAlias?.takeIf { it.isNotBlank() }?.let {
+            cmsTermService.getTermsByAlias(termAlias, deep)
+        }
+
+        // 如果存在termAlias过滤条件,先查询与该termAlias关联的objectIds
+        val objectIds = terms?.let {
+            cmsTermService.getObjectIdsByTerms(it).also { ids ->
+                if (ids.isEmpty()) {
+                    return TableDataInfoUtil.getDataTable(emptyList<CmsPost>())
+                }
+            }
+        }
+
+        // 如果存在扩展属性过滤条件,查询内容元数据表
+        val postIdsByMeta = if (!meta.isNullOrEmpty()) {
+            cmsPostMetaMapper.selectPostMetaList(meta)
+        } else null
+
+        val queryWrapper = QueryWrapper<CmsPost>()
+        if (!content) {
+            // content为false时,不返回content字段,避免返回的数据量过大
+            queryWrapper.select(CmsPost::class.java) { info -> !info.column.equals("content") }
+        }
+
+        // 审核状态字段条件不为空时,判断所属栏目是否存在需要审核扩展属性,如果需要审核,添加审核过滤条件
+        if (!post.review.isNullOrBlank() && !deep) {
+            terms?.getOrNull(0)?.let {
+                // 查询栏目是否存在需要审核属性,并且该值为需要审核
+                cmsTermMetaService.selectListByTermIds(listOf(it.termId!!))
+                    .filter { meta -> meta.metaKey == "reviewRequired" && meta.metaValue == "1" }.getOrNull(0)?.run {
+                        queryWrapper.eq("review", post.review)
+                    }
+            }
+        }
+
+        if (!objectIds.isNullOrEmpty()) {
+            // 设置termAlias过滤条件时,只返回与alias关联的objects
+            queryWrapper.`in`("post_id", objectIds)
+        }
+
+        if (!postIdsByMeta.isNullOrEmpty()) {
+            // 过滤符合扩展字段查询条件的文章列表
+            queryWrapper.`in`("post_id", postIdsByMeta)
+        }
+
+        queryWrapper.eq(!post.contentType.isNullOrBlank(), "content_type", post.contentType)
+            .like(!post.title.isNullOrBlank(), "title", post.title)
+            .like(!post.summary.isNullOrBlank(), "summary", post.summary)
+            .eq(!post.status.isNullOrBlank(), "status", post.status)
+            .eq(post.topStatus != null, "top_status", post.topStatus)
+            .eq(!post.visible.isNullOrBlank(), "visible", post.visible)
+            .eq(!post.createBy.isNullOrBlank(), "create_by", post.createBy)
+            .orderByDesc("top_status", "create_time")
+
+        // 查询post主表,并处理分页
+        pageParam?.let {
+            it.orderByColumn = null
+            PageUtils.startPage(it)
+        }
+        val posts = cmsPostMapper.selectList(queryWrapper).also {
+            if (it.isEmpty()) return TableDataInfoUtil.getDataTable(it)
+        }
+
+        // 查询meta从表,并按postId分组
+        val postIds = posts.map { it.postId!! }.distinct()
+        val metas = cmsPostMetaService.selectListByPostIds(postIds).groupBy { it.postId!! }
+
+        // 将meta从表数据关联到post主表
+        posts.forEach { it.meta = metas[it.postId] ?: emptyList() }
+
+        // 构建分页查询结果,转换post并替换rows
+        return TableDataInfoUtil.getDataTable(posts).apply {
+            rows = posts.map { it.toCmsPostFormRes() }
+        }
+    }
+
+    fun CmsPost.toCmsPostFormRes(): CmsPostFormRes {
+        return CmsPostFormRes().apply {
+            BeanUtils.copyProperties(this@toCmsPostFormRes, this)
+            this.meta =
+                this@toCmsPostFormRes.meta.associate { it.metaKey!! to it.metaValue } as MutableMap<String, String?>
+        }
+    }
+
+    /**
+     * 根据文章别名或Id获取文章详情
+     */
+    fun detail(contentType: String, post: String): CmsPostFormRes {
+        val cmsPost = getPostByIdentity(contentType, post)
+        return cmsPost.toCmsPostFormRes()
+    }
+
+    /**
+     * 根据文章ID获取文章详情
+     */
+    fun getPostInfoEx(
+        catalogAlias: String?,
+        postType: String,
+        postId: Int?,
+        postAlias: String?,
+    ): CmsPostFormRes {
+        if (postId == null && postAlias == null) {
+            throw ServiceException("文章ID和别名不可同时为空", 500)
+        }
+
+        val cmsPost = cmsPostMapper.selectPostEx(catalogAlias, postType, postId, postAlias)
+            ?: throw ServiceException("指定的文章信息不存在", 500)
+
+        return cmsPost.toCmsPostFormRes()
+    }
+
+    /**
+     * 新增发布内容
+     * @param post 发布内容
+     * @param poster 发布者
+     * @return 影响数据条数
+     */
+    @Transactional
+    fun create(post: CmsPostFormReq, poster: String): Int {
+
+        post.alias?.let {
+            if (it.isNotBlank()) {
+                val queryWrapper = QueryWrapper<CmsPost>().eq("alias", it)
+                if (cmsPostMapper.selectCount(queryWrapper) > 0) throw ServiceException("页面别名已存在", 500)
+            }
+        }
+
+        // 插入发布内容
+        val cmsPost = CmsPost().apply {
+            BeanUtils.copyProperties(post, this)
+            this.contentType = this.contentType ?: "article"
+            this.status = this.status ?: "0"
+            this.topStatus = this.topStatus ?: 0
+            this.visible = this.visible ?: "1"
+            this.createBy = poster
+        }.also {
+            if (cmsPostMapper.insert(it) != 1) {
+                throw ServiceException("插入发布内容失败", 500)
+            }
+        }
+
+        // 插入发布内容与栏目的关联关系
+        post.catalogIds?.forEach { catalogId ->
+            CmsTermRelationships().apply {
+                this.objectId = cmsPost.postId
+                this.termId = catalogId
+            }.also {
+                if (cmsTermRelationshipsMapper.insert(it) != 1) {
+                    throw ServiceException("插入发布内容与栏目的关联关系失败", 500)
+                }
+            }
+        }
+
+        // 插入发布内容的扩展属性键值列表
+        post.meta?.forEach { meta ->
+            CmsPostMeta().apply {
+                this.postId = cmsPost.postId
+                this.metaKey = meta.key
+                this.metaValue = meta.value
+            }.also {
+                if (cmsPostMetaMapper.insert(it) != 1) {
+                    throw ServiceException("插入发布内容的扩展属性列表失败", 500)
+                }
+            }
+        }
+
+        return 1
+    }
+
+    /**
+     * 在指定的类型下创建内容
+     *
+     * @param post 内容详情
+     * @param poster 创建者
+     * @param termAlias 类型别名
+     */
+    @Transactional
+    fun createByTermAlias(post: CmsPostFormReq, poster: String, termAlias: String): Int {
+        val cmsTerm =
+            cmsTermService.getTermsByAlias(termAlias, false).getOrNull(0) ?: throw ServiceException("指定类型不存在")
+        val termId = cmsTerm.termId
+        post.catalogIds = post.catalogIds?.also {
+            if (!it.contains(termId)) it.add(termId!!)
+        } ?: mutableListOf(termId!!)
+        return create(post, poster)
+    }
+
+    /**
+     * 更新发布内容
+     * @param post 待修改发布内容
+     * @return 影响数据条数
+     */
+    @Transactional
+    fun update(post: CmsPostFormReq, poster: String): Int {
+        val postId = requireNotNull(post.postId) { "内容Id不可为空" }
+
+        // 获取待更新发布内容
+        val entity = cmsPostMapper.selectById(postId) ?: throw ServiceException("文章不存在", 500)
+
+        post.alias?.let {
+            if (it.isNotBlank()) {
+                val queryWrapper = QueryWrapper<CmsPost>().eq("alias", it).ne("post_id", entity.postId)
+                if (cmsPostMapper.selectCount(queryWrapper) > 0) throw ServiceException("页面别名已存在", 500)
+            }
+        }
+
+        // 更新发布内容与栏目类别的关联关系
+        post.catalogIds?.let {
+            updatePostCatalogsRelations(postId, it)
+        }
+
+        // 更新发布内容的扩展属性键值列表
+        post.meta?.let {
+            updatePostMetas(postId, it)
+        }
+
+        // 更新发布内容
+        entity.apply {
+            BeanUtils.copyProperties(post, this, "origin")
+            this.updateBy = poster
+        }.also {
+            cmsPostMapper.updateById(it)
+        }
+
+        return 1
+    }
+
+    /**
+     * 根据IDs删除发布内容
+     */
+    @Transactional
+    fun deleteByIds(postIds: Array<Int>): Int {
+        if (postIds.isEmpty()) return 0
+
+        cmsPostMetaMapper.delete(QueryWrapper<CmsPostMeta>().`in`("post_id", postIds.toList()))
+
+        cmsTermRelationshipsMapper.deleteObjectIds(postIds.toList())
+
+        return cmsPostMapper.deleteBatchIds(postIds.toList())
+    }
+
+    /**
+     * 审核内容
+     *
+     * @param contentType 内容类型
+     * @param post 内容标识,文章ID或者别名
+     * @param reviewForm 审核信息
+     */
+    fun review(contentType: String, post: String, reviewForm: CmsReviewForm) {
+        val cmsPost = getPostByIdentity(contentType, post)
+        cmsPost.review = reviewForm.review
+        cmsPost.comment = reviewForm.comment
+        cmsPostMapper.updateById(cmsPost)
+    }
+
+    /**
+     * 根据标识获取内容信息
+     *
+     * @param contentType 内容类型
+     * @param identity 内容标识,ID或别名
+     * @return 内容详情
+     */
+    private fun getPostByIdentity(contentType: String, identity: String): CmsPost {
+        return EntityPathVariable.parse(identity).let {
+            if (it.id != null) {
+                cmsPostMapper.selectPostByPostId(contentType, it.id) ?: throw ServiceException(
+                    "指定ID文章信息不存在", 500
+                )
+            } else if (it.alias != null) {
+                cmsPostMapper.selectPostByAlias(contentType, it.alias)
+                    ?: throw ServiceException("指定别名的文章信息不存在", 500)
+            } else {
+                throw ServiceException("指定的文章信息不存在", 500)
+            }
+        }
+    }
+
+    /**
+     * 更新发布内容的扩展属性键值列表
+     */
+    private fun updatePostMetas(postId: Int, metas: Map<String, String>) {
+        // 删除旧的属性
+        cmsPostMetaMapper.delete(QueryWrapper<CmsPostMeta>().eq("post_id", postId))
+
+        // 插入新的属性
+        metas.forEach { meta ->
+            CmsPostMeta().apply {
+                this.postId = postId
+                this.metaKey = meta.key
+                this.metaValue = meta.value
+            }.also {
+                if (cmsPostMetaMapper.insert(it) != 1) {
+                    throw ServiceException("插入发布内容的扩展属性列表失败", 500)
+                }
+            }
+        }
+    }
+
+    /**
+     * 更新发布内容与栏目类别的关联关系
+     */
+    private fun updatePostCatalogsRelations(postId: Int, catalogIds: List<Int>) {
+
+        // 获取关联的栏目类型
+        val rela = cmsTermRelationshipsMapper.selectCatalogRelationsByPostId(postId)
+        val relaCatalogIds = rela.map { r -> r.termId }.toList()
+
+        // 计算待删除栏目类别列表,和待插入栏目类别列表
+        val removeIds = relaCatalogIds.minus(catalogIds.toSet())
+        val addIds = catalogIds.minus(relaCatalogIds.toSet())
+
+        // 删除旧的关联关系
+        rela.forEach {
+            if (removeIds.contains(it.termId)) {
+                cmsTermRelationshipsMapper.deleteById(it.relaId)
+            }
+        }
+
+        // 插入新的关联关系
+        //  TODO: 需要检查新增的栏目类别是否合法
+        catalogIds.forEach {
+            if (addIds.contains(it)) {
+                CmsTermRelationships().apply {
+                    this.objectId = postId
+                    this.termId = it
+                }.also { r ->
+                    cmsTermRelationshipsMapper.insert(r)
+                }
+            }
+        }
+    }
+}

+ 30 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/service/CmsTermMetaService.kt

@@ -0,0 +1,30 @@
+package com.cclotus.cms.service
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
+import com.cclotus.cms.domain.CmsTermMeta
+import com.cclotus.cms.mapper.CmsTermMetaMapper
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Service
+
+@Service
+class CmsTermMetaService {
+
+    @Autowired
+    lateinit var cmsTermMetaMapper: CmsTermMetaMapper
+
+    /**
+     * 根据分类ID列表返回扩展属性列表
+     * @param termIds 内容ID列表
+     * @return 指定termID的扩展属性列表
+     */
+    fun selectListByTermIds(termIds: List<Int>): List<CmsTermMeta> {
+        if (termIds.isEmpty())
+            return mutableListOf()
+
+        return QueryWrapper<CmsTermMeta>()
+            .`in`("term_id", termIds)
+            .let {
+                cmsTermMetaMapper.selectList(it)
+            }
+    }
+}

+ 131 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/service/CmsTermRelationshipsService.kt

@@ -0,0 +1,131 @@
+package com.cclotus.cms.service
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
+import com.cclotus.cms.domain.CmsPost
+import com.cclotus.cms.domain.CmsTerm
+import com.cclotus.cms.domain.CmsTermRelationships
+import com.cclotus.cms.mapper.CmsPostMapper
+import com.cclotus.cms.mapper.CmsTermMapper
+import com.cclotus.cms.mapper.CmsTermRelationshipsMapper
+import com.ruoyi.common.exception.ServiceException
+import com.ruoyi.common.reqParam.PageParam
+import com.ruoyi.common.utils.PageUtils
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Service
+import org.springframework.transaction.annotation.Transactional
+
+@Service
+class CmsTermRelationshipsService {
+
+    @Autowired
+    lateinit var cmsTermMapper: CmsTermMapper
+
+    @Autowired
+    lateinit var cmsPostMapper: CmsPostMapper
+
+    @Autowired
+    lateinit var cmsTermRelationshipsMapper: CmsTermRelationshipsMapper
+
+    /**
+     * 查询分类绑定的Term对象列表
+     */
+    fun getBindingObjectsByTermId(termId: Int, pageParam: PageParam?) : List<Any>?{
+        val objectType = getObjectType(cmsTermMapper.selectById(termId).type!!)
+
+        pageParam?.let {
+            PageUtils.startPage(it)
+        }
+
+        return when (objectType) {
+            "term" -> cmsTermRelationshipsMapper.getBindingTermsByTermId(termId)
+            "post" -> cmsTermRelationshipsMapper.getBindingPostsByTermId(termId)
+
+            else -> throw ServiceException("未知的分类数据类型", 500)
+        }
+    }
+
+    /**
+     *  为内容绑定一组分类,如:一篇文章添加多个标签或关联某个栏目
+     */
+    @Transactional
+    fun bindPostToTerms(postId: Int, termIds: Array<Int>):Int {
+        if(!cmsPostMapper.exists(QueryWrapper<CmsPost>().eq("post_id", postId)))
+            throw ServiceException("内容Id不存在", 500)
+
+        termIds.forEach {
+            cmsTermMapper.selectOne(QueryWrapper<CmsTerm>().eq("term_id", it))
+                ?: throw ServiceException("分类Id不存在", 500)
+
+            CmsTermRelationships().apply {
+                this.objectId = postId
+                this.termId = it
+            }.also { r ->
+                if (cmsTermRelationshipsMapper.insert(r) != 1) {
+                    throw ServiceException("插入内容与分类的关系失败", 500)
+                }
+            }
+        }
+        return termIds.size
+    }
+
+    /**
+     *  为分类绑定一组对象,如:一个栏目或标签下添加多篇文章
+     *
+     */
+    @Transactional
+    fun bindTermToObjects(termId: Int, objectIds:Array<Int>):Int {
+        // 检查分类是否存在
+        val term = cmsTermMapper.selectOne(QueryWrapper<CmsTerm>().eq("term_id", termId))
+
+        objectIds.forEach {
+            // 检查对象是否存在
+            if(!isObjectExist(term.type!!, it))
+                throw ServiceException("指定的对象不存在", 500)
+
+            CmsTermRelationships().apply {
+                this.objectId = it
+                this.termId = termId
+            }.also { r ->
+                if (cmsTermRelationshipsMapper.insert(r) != 1) {
+                    throw ServiceException("插入分类与关联对象的关系失败", 500)
+                }
+            }
+        }
+
+        return objectIds.size
+    }
+
+    private fun isObjectExist(dataType: String, objectId: Int): Boolean {
+        return when(getObjectType(dataType)){
+            "term" -> cmsTermMapper.exists(QueryWrapper<CmsTerm>().eq("term_id", objectId))
+            "post" -> cmsPostMapper.exists(QueryWrapper<CmsPost>().eq("post_id", objectId))
+            else -> throw ServiceException("未知的分类数据类型", 500)
+        }
+    }
+
+    fun getObjectType(dataType: String): String{
+        // TODO: 建立对应的字典项, 构建数据类型集合
+        return when(dataType){
+            in setOf("term", "child", "wxAppMenu", "menu", "catalog", "tag") -> "term"
+            in setOf("post", "article", "page", "picture", "video") -> "post"
+            in setOf("link", "external", "route", "appId") -> "link"
+            else -> "unknown"
+        }
+    }
+
+    /**
+     *  从关联关系中,删除与termId关联的一组objectIds
+     */
+    @Transactional
+    fun deleteObjectIds(termId: Int, objectIds: Array<Int>): Int {
+        return cmsTermRelationshipsMapper.deleteObjectIdsByTermId(termId, objectIds.toList())
+    }
+
+    /**
+     *  从关联关系中,删除与objectId关联的一组termIds
+     */
+    @Transactional
+    fun deleteTermIds(objectId: Int, termIds: Array<Int>): Int {
+        return cmsTermRelationshipsMapper.deleteTermIdsByObjectId(objectId, termIds.toList())
+    }
+}

+ 285 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/service/CmsTermService.kt

@@ -0,0 +1,285 @@
+package com.cclotus.cms.service
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
+import com.cclotus.cms.domain.CmsTerm
+import com.cclotus.cms.domain.CmsTermMeta
+import com.cclotus.cms.domain.CmsTermRelationships
+import com.cclotus.cms.mapper.CmsTermMapper
+import com.cclotus.cms.mapper.CmsTermMetaMapper
+import com.cclotus.cms.mapper.CmsTermRelationshipsMapper
+import com.cclotus.cms.util.EntityPathVariable
+import com.cclotus.cms.vo.CmsTermForm
+import com.ruoyi.common.core.page.TableDataInfo
+import com.ruoyi.common.exception.ServiceException
+import com.ruoyi.common.reqParam.PageParam
+import com.ruoyi.common.utils.PageUtils
+import com.ruoyi.common.utils.TableDataInfoUtil
+import org.springframework.beans.BeanUtils
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Service
+import org.springframework.transaction.annotation.Transactional
+
+@Service
+class CmsTermService {
+
+    @Autowired
+    lateinit var cmsTermMapper: CmsTermMapper
+
+    @Autowired
+    lateinit var cmsTermRelationshipsMapper: CmsTermRelationshipsMapper
+
+    @Autowired
+    lateinit var cmsTermMetaMapper: CmsTermMetaMapper
+
+    @Autowired
+    lateinit var cmsTermMetaService: CmsTermMetaService
+
+    /**
+     * 获取分类列表
+     */
+    fun list(term: CmsTerm, pageParam: PageParam?): TableDataInfo {
+        // 查询term主表,并处理分页
+        pageParam?.let { PageUtils.startPage(it) }
+        val queryWrapper = QueryWrapper<CmsTerm>()
+            .eq(!term.taxonomy.isNullOrBlank(), "taxonomy", term.taxonomy)
+            .eq(!term.type.isNullOrBlank(), "type", term.type)
+            .like(!term.name.isNullOrBlank(), "name", term.name)
+            .eq(!term.alias.isNullOrEmpty(), "alias", term.alias)
+            .eq(term.parentId != null, "parent_id", term.parentId)
+            .orderByAsc("number")
+        val terms = cmsTermMapper.selectList(queryWrapper)
+
+        // 查询meta从表,并按termId分组
+        val metas = terms.map { it.termId!! }.distinct().toList().let { termIds ->
+            cmsTermMetaService.selectListByTermIds(termIds).groupBy { it.termId!! }
+        }
+
+        // 将meta从表数据关联到post主表
+        terms.forEach { it.meta = metas[it.termId] ?: emptyList() }
+
+        // 构建分页查询结果,转换post并替换rows
+        return TableDataInfoUtil.getDataTable(terms).apply {
+            rows = terms.map { it.toCmsTermForm() }
+        }
+    }
+
+    fun CmsTerm.toCmsTermForm(): CmsTermForm {
+        return CmsTermForm().apply {
+            BeanUtils.copyProperties(this@toCmsTermForm, this)
+            this.meta = this@toCmsTermForm.meta.associate { it.metaKey!! to it.metaValue!! }
+        }
+    }
+
+    /**
+     * 根据分类别名或Id获取分类详情
+     */
+    fun detail(taxonomy: String, term: String): CmsTermForm {
+        val (id, alias) = EntityPathVariable.parse(term)
+        return cmsTermMapper.selectOneWithMetas(taxonomy, id, alias)?.toCmsTermForm()
+            ?: throw ServiceException("分类不存在", 500)
+    }
+
+    /**
+     * 添加分类
+     * @param term 分类信息
+     * @return 影响数据条数
+     */
+    @Transactional
+    fun create(term: CmsTermForm): Int {
+        // 如果未设置 parentId,使用默认值 0 表示顶层栏目
+        term.parentId = term.parentId ?: 0
+
+        // 检查分类别名是否冲突
+        val queryWrapper = QueryWrapper<CmsTerm>().eq("alias", term.alias!!)
+        if (cmsTermMapper.selectCount(queryWrapper) > 0)
+            throw ServiceException("分类别名已存在", 500)
+
+        // 对于子分类
+        if (term.parentId != 0) {
+            // 检查父级分类是否存在
+            val parentTerm = cmsTermMapper.selectById(term.parentId)
+                ?: throw ServiceException("父级分类不存在", 500)
+
+            // 默认子分类自动继承父分类类别
+            term.type = term.type ?: parentTerm.type
+
+            // 设置子分类的组级列表
+            term.ancestors = parentTerm.ancestors + "," + term.parentId
+        } else {
+            term.ancestors = "0"
+        }
+
+        // 插入分类
+        val entity = CmsTerm().apply {
+            BeanUtils.copyProperties(term, this)
+            this.number = this.number ?: 0
+        }.also {
+            if (cmsTermMapper.insert(it) != 1) {
+                throw ServiceException("插入分类失败", 500)
+            }
+        }
+
+        // 插入分类与对象的关联关系
+        term.objectIds?.forEach { objectId ->
+            CmsTermRelationships().apply {
+                this.termId = entity.termId
+                this.objectId = objectId
+            }.also {
+                if (cmsTermRelationshipsMapper.insert(it) != 1) {
+                    throw ServiceException("插入分类与对象的关联关系失败", 500)
+                }
+            }
+        }
+
+        // 插入发布内容的扩展属性键值列表
+        term.meta?.forEach { meta ->
+            CmsTermMeta().apply {
+                this.termId = entity.termId
+                this.metaKey = meta.key
+                this.metaValue = meta.value
+            }.also {
+                if (cmsTermMetaMapper.insert(it) != 1) {
+                    throw ServiceException("插入分类的扩展属性列表失败", 500)
+                }
+            }
+        }
+
+        return 1
+    }
+
+    /**
+     * 更新分类
+     * @param term 分类信息
+     * @return 影响数据条数
+     */
+    @Transactional
+    fun update(term: CmsTermForm): Int {
+        val termId = requireNotNull(term.termId) { "分类Id不可为空" }
+
+        /* 获取待更新分类 */
+        val entity = cmsTermMapper.selectById(termId) ?: throw ServiceException("分类不存在", 500)
+
+        /* 检查别名是否已冲突 */
+        if (!term.alias.isNullOrBlank() && !entity.alias.equals(term.alias)) {
+            val queryWrapper = QueryWrapper<CmsTerm>()
+                .eq("alias", term.alias)
+            if (cmsTermMapper.selectCount(queryWrapper) > 0) {
+                throw ServiceException("分类别名已存在", 500)
+            }
+        }
+
+        // 更新分类的扩展属性键值列表
+        term.meta?.let {
+            updateMetas(termId, it)
+        }
+
+        /**
+         *   支持修改分类名称、别名、图标、描述、排序
+         *   不支持修改分类系统、父级分类、分类的数据类型
+         */
+        return entity.apply {
+            BeanUtils.copyProperties(
+                term,
+                entity,
+                "term_id",
+                "parent_id",
+                "ancestors",
+                "taxonomy",
+                "type"
+            )
+        }.let {
+            cmsTermMapper.updateById(it)
+        }
+    }
+
+    /**
+     * 根据IDs删除分类
+     */
+    @Transactional
+    fun deleteByIds(termIds: Array<Int>): Int {
+        if (termIds.isEmpty()) return 1
+
+        QueryWrapper<CmsTerm>().`in`("parent_id", termIds.toList()).let {
+            if (cmsTermMapper.exists(it))
+                throw ServiceException("不能删除分类:分类下有子分类", 500)
+        }
+
+        QueryWrapper<CmsTermRelationships>().`in`("term_id", termIds.toList()).let {
+            if (cmsTermRelationshipsMapper.exists(it))
+                throw ServiceException("不能删除分类:分类下有关联的发布内容", 500)
+        }
+
+        // 删除分类的扩展属性
+        QueryWrapper<CmsTermMeta>().`in`("term_id", termIds.toList()).let {
+            cmsTermMetaMapper.delete(it)
+        }
+
+        return cmsTermMapper.deleteBatchIds(termIds.toList())
+    }
+
+    /**
+     * 更新发布内容的扩展属性键值列表
+     */
+    private fun updateMetas(termId: Int, metas: Map<String, String?>) {
+        // 删除旧的属性
+        cmsTermMetaMapper.delete(QueryWrapper<CmsTermMeta>().eq("term_id", termId))
+
+        // 插入新的属性
+        metas.forEach { meta ->
+            CmsTermMeta().apply {
+                this.termId = termId
+                this.metaKey = meta.key
+                this.metaValue = meta.value
+            }.also {
+                if (cmsTermMetaMapper.insert(it) != 1) {
+                    throw ServiceException("插入分类的扩展属性列表失败", 500)
+                }
+            }
+        }
+    }
+
+    /**
+     * 根据别名获取分类列表
+     * @param alias 分类别名
+     * @param deep 是否查询子分类
+     */
+    fun getTermsByAlias(alias: String, deep: Boolean): List<CmsTerm> {
+        val terms = mutableListOf<CmsTerm>()
+
+        val term = cmsTermMapper.selectOne(QueryWrapper<CmsTerm>().eq("alias", alias))
+            ?: throw ServiceException("分类别名不存在", 500)
+
+        terms.add(term)
+
+        if (deep) {
+            terms.addAll(cmsTermMapper.selectChildrenById(term.termId!!))
+        }
+
+        return terms
+    }
+
+    /**
+     * 根据分类列表查询与分类相关的对象Ids列表
+     * @param terms 分类列表
+     */
+    fun getObjectIdsByTerms(terms: List<CmsTerm>): List<Int> {
+        val termIds = terms.mapNotNull { it.termId }
+
+        return cmsTermRelationshipsMapper.selectList(
+            QueryWrapper<CmsTermRelationships>()
+                .`in`("term_id", termIds)
+                .groupBy("object_id")
+                .having("count(*) >= ${termIds.size}")
+                .select("object_id")
+        ).mapNotNull { it.objectId }.distinct().toList()
+    }
+
+    /**
+     * 根据别名获取与分类相关的对象Ids列表
+     * @param alias 分类别名
+     * @param deep 是否查询子分类
+     */
+    fun getObjectIdsByTermAlias(alias: String, deep: Boolean): List<Int> {
+        return getObjectIdsByTerms(getTermsByAlias(alias, deep))
+    }
+}

+ 196 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/service/FileManagerService.kt

@@ -0,0 +1,196 @@
+package com.cclotus.cms.service
+
+import com.cclotus.cms.vo.FileInfo
+import com.ruoyi.common.config.RuoYiConfig
+import com.ruoyi.common.constant.Constants
+import com.ruoyi.common.exception.ServiceException
+import org.springframework.stereotype.Service
+import java.io.File
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+
+/**
+ * 文件管理服务
+ *
+ */
+@Service
+class FileManagerService {
+
+
+    // 媒体文件夹位置
+    private val MEDIA_DIR = "/media"
+
+    /**
+     * 浏览指定文件夹
+     *
+     * @param path 文件夹路径
+     * @return 指定文件夹下的所有内容
+     */
+    fun browseDirectory(path: String?): List<FileInfo> {
+        return browse(path ?: "", 1, false)
+    }
+
+    /**
+     * 获取目录结构树
+     *
+     * @param path
+     * @return
+     */
+    fun directoryTree(path: String?): List<FileInfo> {
+        return browse(path ?: "", null, true)
+    }
+
+    /**
+     * 打开指定媒体文件夹
+     *
+     * @param path 与媒体文件夹的相对路径,以'/'分隔
+     * @param maxDepth 最大打开深度
+     * @param onlyDirectory 是否只返回目录信息
+     * @return 文件及目录信息列表
+     */
+    fun browse(path: String, maxDepth: Int?, onlyDirectory: Boolean): List<FileInfo> {
+        val rootPath = getMediaPath()
+        val fileTreeWalk = File("$rootPath$path").let {
+            if (!it.exists()) throw ServiceException("指定文件夹不存在")
+            if (!it.isDirectory) throw ServiceException("打开的不是文件夹")
+            maxDepth?.run {
+                it.walkTopDown().maxDepth(this)
+            } ?: it.walkTopDown()
+        }
+        val fileInfos = fileTreeWalk.filter {
+            !(onlyDirectory && it.isFile)
+        }.map {
+            FileInfo().apply {
+                this.isDirectory = it.isDirectory
+                this.path = path.ifBlank { "/" }
+                this.name = it.name
+                this.url = if (it.isFile) {
+                    if (this.path!!.last() == '/') Constants.RESOURCE_PREFIX + MEDIA_DIR + this.path + it.name
+                    Constants.RESOURCE_PREFIX + MEDIA_DIR + this.path + "/" + it.name
+                } else null
+                this.updateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(it.lastModified()), ZoneOffset.of("+8"))
+                this.size = if (it.isFile) it.length() * 1.0 / 1000 else null
+                this.isEmpty = it.list()?.isEmpty()
+            }
+        }.toMutableList()
+        // 移除根目录信息
+        if (fileInfos.isNotEmpty()) fileInfos.removeAt(0)
+        return fileInfos
+    }
+
+    /**
+     * 创建目录
+     *
+     * @param path 源目录
+     * @param directoryName 创建目录名
+     * @return 创建结果
+     */
+    fun createDirectory(path: String?, directoryName: String) {
+        val directory = getFile(path, directoryName)
+        if (directory.exists()) throw ServiceException("当前文件夹已存在")
+        if (!directory.mkdir()) throw ServiceException("目录创建失败")
+    }
+
+
+    /**
+     * 修改文件名
+     *
+     * @param path 文件所在路径
+     * @param originName 文件原始名称
+     * @param newName 新文件名
+     */
+    fun changeFileName(path: String?, originName: String, newName: String) {
+        // 如果新旧文件名相同,直接返回
+        if (originName == newName) return
+        val newFile = getFile(path, newName)
+        if (newFile.exists()) throw ServiceException("文件名已存在")
+        val originFile = getFile(path, originName)
+        val result = originFile.renameTo(newFile)
+        if (!result) throw ServiceException("重命名失败")
+    }
+
+    /**
+     * 删除文件或文件夹
+     *
+     * @param path 文件路径
+     * @param name 文件名
+     * @param mandatory 是否强制删除,默认为否,为true则文件夹下有内容也会删除
+     */
+    fun delete(path: String?, name: String, mandatory: Boolean = false) {
+        val file = getFile(path, name)
+        if (!file.exists()) throw ServiceException("指定文件不存在")
+        if (file.isDirectory) {
+            file.list()?.let {
+                if (it.isNotEmpty() && !mandatory) throw ServiceException("文件夹下内容不为空,不允许删除")
+            }
+            // 如果为强制删除,则删除目录下所有文件及本目录
+            file.walkBottomUp().forEach {
+                it.delete()
+            }
+            return
+        }
+        file.delete()
+    }
+
+    /**
+     * 获取媒体文件根目录
+     *
+     * @return 媒体文件夹根目录
+     */
+    fun getMediaPath(): String {
+        return RuoYiConfig.getProfile() + MEDIA_DIR
+    }
+
+    /**
+     * 获取指定路径的文件信息
+     *
+     * @param path 文件所在路径
+     * @param fileName 文件名称
+     * @return 文件
+     */
+    fun getFile(path: String?, fileName: String): File {
+        val filePath = if (path.isNullOrBlank()) "/" else path
+        return if (filePath.last() == '/') {
+            File(getMediaPath() + filePath + fileName)
+        } else {
+            File(getMediaPath() + filePath + "/" + fileName)
+        }
+    }
+
+    /**
+     * 获取文件名,如果重复就会自动标号,格式:文件名(序号)
+     *
+     * @param filePath 文件路径
+     * @param fileName 文件名
+     * @return 自动生成的文件名称
+     */
+    @Suppress("NAME_SHADOWING")
+    fun getFileName(filePath: String, fileName: String?): String {
+        val fileName = fileName ?: throw ServiceException("文件名不可为空")
+        val fileNameWithExtension = fileName.substring(0, fileName.lastIndexOf("."))
+        val extension = fileName.substringAfterLast(".")
+        val files = File(filePath).listFiles() ?: emptyArray()
+        val regex = "^$fileNameWithExtension(\\([1-9][0-9]*\\))*".toRegex()
+        var max = 0
+        val sameNameFiles = files.filter {
+            it.isFile && regex.matches(it.name.substring(0, it.name.lastIndexOf(".")))
+        }
+        if (sameNameFiles.none { it.name == fileName }) return fileName
+        sameNameFiles.forEach {
+            if (it.name.contains("(")) {
+                val name = it.name
+                val num = name.substring(name.lastIndexOf("(") + 1, name.lastIndexOf(")"))
+                try {
+                    val n = num.toInt()
+                    if (n > max) {
+                        max = n
+                    }
+                } catch (_: NumberFormatException) {
+
+                }
+            }
+        }
+        return "$fileNameWithExtension(${max + 1}).$extension"
+    }
+}

+ 7 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/util/CmsConstant.kt

@@ -0,0 +1,7 @@
+package com.cclotus.cms.util
+
+object CmsConstant {
+
+    // 内容元数据查询参数前缀
+    val META_QUERY_PREFIX = "meta."
+}

+ 22 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/util/EntityPathVariable.kt

@@ -0,0 +1,22 @@
+package com.cclotus.cms.util
+
+data class EntityPathVariable(val id:Int?, val alias:String?){
+
+    companion object {
+        fun parse(pathVariable: String): EntityPathVariable{
+            return when(pathVariable[0]) {
+                '_'-> EntityPathVariable(pathVariable.substring(1).toInt(), null)
+
+                '@'-> EntityPathVariable(null, pathVariable.substring(1))
+
+                else -> {
+                    if(("^[0-9]*$").toRegex().matches(pathVariable))
+                        EntityPathVariable(pathVariable.toInt(), null)
+                    else
+                        EntityPathVariable(null, pathVariable)
+                }
+            }
+        }
+    }
+
+}

+ 44 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/vo/CmsPostFormBase.kt

@@ -0,0 +1,44 @@
+package com.cclotus.cms.vo
+
+/**
+ * 文章信息
+ */
+open class CmsPostFormBase {
+
+    var postId: Int? = null
+
+    var title: String? = null
+
+    var type: String? = null
+
+    var image: String? = null
+
+    var summary: String? = null
+
+    var contentType: String? = null
+
+    var content: String? = null
+
+    var status: String? = null
+
+    var topStatus: Int? = null
+
+    var visible: String? = null
+
+    var alias: String? = null
+
+    /**
+     * 审核状态 0待审核 1审核通过 2审核驳回
+     */
+    var review: String? = null
+
+    /**
+     * 审核意见
+     */
+    var comment: String? = null
+
+    /**
+     * 内容发布来源
+     */
+    var origin: String? = null
+}

+ 11 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/vo/CmsPostFormReq.kt

@@ -0,0 +1,11 @@
+package com.cclotus.cms.vo
+
+/**
+ * 文章信息
+ */
+class CmsPostFormReq : CmsPostFormBase() {
+
+    var catalogIds: MutableList<Int>? = null
+
+    var meta: Map<String, String>? = null
+}

+ 27 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/vo/CmsPostFormRes.kt

@@ -0,0 +1,27 @@
+package com.cclotus.cms.vo
+
+import com.cclotus.cms.domain.CmsTerm
+import com.fasterxml.jackson.annotation.JsonFormat
+import java.time.LocalDateTime
+
+/**
+ * 文章信息
+ */
+class CmsPostFormRes : CmsPostFormBase() {
+
+    var createBy: String? = null
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    var createTime: LocalDateTime? = null
+
+    var updateBy: String? = null
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    var updateTime: LocalDateTime? = null
+
+    var remark: String? = null
+
+    var catalogs: List<CmsTerm> = mutableListOf()
+
+    var meta: MutableMap<String, String?> = mutableMapOf()
+}

+ 31 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/vo/CmsTermForm.kt

@@ -0,0 +1,31 @@
+package com.cclotus.cms.vo
+
+/**
+ * CMS分类
+ */
+open class CmsTermForm {
+
+    var termId: Int? = null
+
+    var parentId: Int? = null
+
+    var ancestors: String? = null
+
+    var name: String? = null
+
+    var alias: String? = null
+
+    var description: String? = null
+
+    var image: String? = null
+
+    var taxonomy: String? = null
+
+    var type: String? = null
+
+    var number: Int? = null
+
+    var objectIds: List<Int>? = null
+
+    var meta: Map<String, String?>? = mutableMapOf()
+}

+ 50 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/vo/FileInfo.kt

@@ -0,0 +1,50 @@
+package com.cclotus.cms.vo
+
+import com.fasterxml.jackson.annotation.JsonFormat
+import com.fasterxml.jackson.annotation.JsonProperty
+import java.time.LocalDateTime
+
+/**
+ * 文件信息
+ *
+ */
+class FileInfo {
+
+    /**
+     * 当前路径
+     */
+    var path: String? = null
+
+    /**
+     * 文件名
+     */
+    var name: String? = null
+
+    /**
+     * 是否为目录
+     */
+    @JsonProperty("isDirectory")
+    var isDirectory: Boolean? = null
+
+    /**
+     * 文件url
+     */
+    var url: String? = null
+
+    /**
+     * 文件大小,单位KB
+     */
+    var size: Double? = null
+
+    /**
+     * 文件夹是否为空
+     */
+    @JsonProperty("isEmpty")
+    var isEmpty: Boolean? = null
+
+    /**
+     * 文件最后修改时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    var updateTime: LocalDateTime? = null
+}

+ 18 - 0
ruoyi-cms/src/main/kotlin/com/cclotus/cms/vo/PageForm.kt

@@ -0,0 +1,18 @@
+package com.cclotus.cms.vo
+
+import javax.validation.constraints.NotBlank
+
+
+class PageForm {
+
+    /**
+     * 页面标题
+     */
+    @NotBlank(message = "页面标题不可为空")
+    var pageTitle: String? = null
+
+    /**
+     * 页面内容
+     */
+    var content: String? = null
+}

+ 121 - 0
ruoyi-cms/src/main/resources/mapper/cms/CmsPostMapper.xml

@@ -0,0 +1,121 @@
+<?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="com.cclotus.cms.mapper.CmsPostMapper">
+
+    <resultMap type="com.cclotus.cms.domain.CmsPost" id="CmsPost">
+        <result property="postId" column="post_id"/>
+        <result property="title" column="title"/>
+        <result property="image" column="image"/>
+        <result property="summary" column="summary"/>
+        <result property="contentType" column="content_type"/>
+        <result property="content" column="content"/>
+        <result property="status" column="status"/>
+        <result property="topStatus" column="top_status"/>
+        <result property="visible" column="visible"/>
+        <result property="alias" column="alias"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="remark" column="remark"/>
+        <result property="review" column="review"/>
+        <result property="comment" column="comment"/>
+        <result property="origin" column="origin"/>
+        <collection property="catalogs" javaType="java.util.List" resultMap="CmsTerm"/>
+        <collection property="meta" javaType="java.util.List" resultMap="CmsPostMeta"/>
+    </resultMap>
+
+    <resultMap id="CmsTerm" type="com.cclotus.cms.domain.CmsTerm">
+        <result property="termId"   column="t_term_id"/>
+        <result property="parentId" column="t_parent_id"/>
+        <result property="name"     column="t_name"/>
+        <result property="alias"    column="t_alias"/>
+        <result property="description" column="t_description"/>
+        <result property="image"    column="t_image"/>
+        <result property="taxonomy" column="t_taxonomy"/>
+        <result property="type"     column="t_type"/>
+        <result property="number"   column="t_number"/>
+    </resultMap>
+
+    <resultMap id="CmsPostMeta" type="com.cclotus.cms.domain.CmsPostMeta">
+        <result property="metaId" column="meta_id"/>
+        <result property="metaKey" column="meta_key"/>
+        <result property="metaValue" column="meta_value"/>
+    </resultMap>
+
+    <select id="selectPostByPostId" resultMap="CmsPost">
+        select p.*,
+               t.term_id    as t_term_id,
+               t.parent_id  as t_parent_id,
+               t.name       as t_name,
+               t.alias      as t_alias,
+               t.description  as t_description,
+               t.image      as t_image,
+               t.taxonomy   as t_taxonomy,
+               t.type       as t_type,
+               t.number     as t_number,
+               e.meta_key,
+               e.meta_value
+        FROM cms_post p
+                LEFT JOIN cms_term_relationships r ON p.post_id = r.object_id
+                LEFT JOIN cms_term t on r.term_id = t.term_id
+                LEFT JOIN cms_post_meta e on p.post_id = e.post_id
+        where p.post_id = #{postId} and p.content_type = #{contentType}
+    </select>
+
+    <select id="selectPostByAlias" resultMap="CmsPost">
+        select p.*,
+               t.term_id    as t_term_id,
+               t.parent_id  as t_parent_id,
+               t.name       as t_name,
+               t.alias      as t_alias,
+               t.description  as t_description,
+               t.image      as t_image,
+               t.taxonomy   as t_taxonomy,
+               t.type       as t_type,
+               t.number     as t_number,
+               e.meta_key,
+               e.meta_value
+        FROM cms_post p
+                LEFT JOIN cms_term_relationships r ON p.post_id = r.object_id
+                LEFT JOIN cms_term t on r.term_id = t.term_id
+                LEFT JOIN cms_post_meta e on p.post_id = e.post_id
+        where p.alias = #{alias} and p.content_type = #{contentType}
+    </select>
+
+    <select id="selectPostEx" resultMap="CmsPost">
+        select p.*,
+            t.term_id    as t_term_id,
+            t.parent_id  as t_parent_id,
+            t.name       as t_name,
+            t.alias      as t_alias,
+            t.description  as t_description,
+            t.image      as t_image,
+            t.taxonomy   as t_taxonomy,
+            t.type       as t_type,
+            t.number     as t_number,
+            e.meta_key,
+            e.meta_value
+        FROM cms_post p
+            LEFT JOIN cms_term_relationships r ON p.post_id = r.object_id
+            LEFT JOIN cms_term t on r.term_id = t.term_id
+            LEFT JOIN cms_post_meta e on p.post_id = e.post_id
+        <where>
+            <if test="catalogAlias != null and catalogAlias != ''">
+                AND t.alias = #{catalogAlias}
+            </if>
+            <if test="postType != null and postType != ''">
+                AND p.content_type = #{postType}
+            </if>
+            <if test="postId != null and postId != ''">
+                AND p.post_id = #{postId}
+            </if>
+            <if test="postAlias != null and postAlias != ''">
+                AND p.alias = #{postAlias}
+            </if>
+        </where>
+    </select>
+
+</mapper>

+ 31 - 0
ruoyi-cms/src/main/resources/mapper/cms/CmsPostMetaMapper.xml

@@ -0,0 +1,31 @@
+<?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="com.cclotus.cms.mapper.CmsPostMetaMapper">
+
+    <resultMap id="CmsPostMeta" type="com.cclotus.cms.domain.CmsPostMeta">
+        <result property="metaId" column="meta_id"/>
+        <result property="postId" column="post_id"/>
+        <result property="metaKey" column="meta_key"/>
+        <result property="metaValue" column="meta_value"/>
+    </resultMap>
+
+    <select id="selectPostMetaList" resultType="Int">
+        select post_id
+        from
+        (select
+        post_id,
+        count(post_id) as `count`
+        from cms_post_meta
+        where (meta_key, meta_value) in
+        <foreach collection="meta" item="value" index="key" open="(" close=")" separator=",">
+            (#{key},#{value})
+        </foreach>
+        GROUP BY post_id
+        ) as stat
+        where stat.`count`= #{size}
+    </select>
+
+
+</mapper>

+ 48 - 0
ruoyi-cms/src/main/resources/mapper/cms/CmsTermMapper.xml

@@ -0,0 +1,48 @@
+<?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="com.cclotus.cms.mapper.CmsTermMapper">
+
+    <resultMap id="CmsTerm" type="com.cclotus.cms.domain.CmsTerm">
+        <result property="termId" column="term_id"/>
+        <result property="parentId" column="parent_id"/>
+        <result property="ancestors" column="ancestors"/>
+        <result property="name" column="name"/>
+        <result property="alias" column="alias"/>
+        <result property="description" column="description"/>
+        <result property="image" column="image"/>
+        <result property="taxonomy" column="taxonomy"/>
+        <result property="type" column="type"/>
+        <result property="number" column="number"/>
+        <collection property="meta" javaType="java.util.List" resultMap="CmsTermMeta"/>
+    </resultMap>
+
+    <resultMap id="CmsTermMeta" type="com.cclotus.cms.domain.CmsTermMeta">
+        <result property="metaId" column="meta_id"/>
+        <result property="metaKey" column="meta_key"/>
+        <result property="metaValue" column="meta_value"/>
+    </resultMap>
+
+    <select id="selectOneWithMetas" resultMap="CmsTerm">
+        select t.*, m.meta_id, m.meta_key, m.meta_value
+        from cms_term t
+        left join cms_term_meta m on t.term_id = m.term_id
+        <where>
+            <if test="taxonomy != null and taxonomy != ''">
+                and t.taxonomy = #{taxonomy}
+            </if>
+            <if test="alias != null and alias != ''">
+                and t.alias = #{alias}
+            </if>
+            <if test="termId != null">
+                and t.term_id = #{termId}
+            </if>
+        </where>
+    </select>
+
+    <select id="selectChildrenById" resultMap="CmsTerm">
+        select * from cms_term where find_in_set(#{termId}, ancestors)
+    </select>
+
+</mapper>

+ 95 - 0
ruoyi-cms/src/main/resources/mapper/cms/CmsTermRelationshipsMapper.xml

@@ -0,0 +1,95 @@
+<?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="com.cclotus.cms.mapper.CmsTermRelationshipsMapper">
+
+    <resultMap id="CmsTerm" type="com.cclotus.cms.domain.CmsTerm">
+        <result property="termId" column="term_id"/>
+        <result property="parentId" column="parent_id"/>
+        <result property="ancestors" column="ancestors"/>
+        <result property="name" column="name"/>
+        <result property="alias" column="alias"/>
+        <result property="description" column="description"/>
+        <result property="image" column="image"/>
+        <result property="taxonomy" column="taxonomy"/>
+        <result property="type" column="type"/>
+        <result property="number" column="number"/>
+    </resultMap>
+
+    <resultMap id="CmsTermRelationships" type="com.cclotus.cms.domain.CmsTermRelationships">
+        <result property="relaId"    column="rela_id"/>
+        <result property="termId"   column="term_id"/>
+        <result property="objectId" column="object_id"/>
+    </resultMap>
+
+    <resultMap type="com.cclotus.cms.domain.CmsPost" id="CmsPost">
+        <result property="postId" column="post_id"/>
+        <result property="title" column="title"/>
+        <result property="image" column="image"/>
+        <result property="summary" column="summary"/>
+        <result property="contentType" column="content_type"/>
+        <result property="content" column="content"/>
+        <result property="status" column="status"/>
+        <result property="topStatus" column="top_status"/>
+        <result property="visible" column="visible"/>
+        <result property="alias" column="alias"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="remark" column="remark"/>
+    </resultMap>
+
+    <select id="selectCatalogRelationsByPostId" parameterType="Int" resultMap="CmsTermRelationships">
+        select r.* from cms_term_relationships r
+        left join cms_term t on r.term_id = t.term_id and t.taxonomy = 'catalog'
+        where r.object_id= #{postId}
+    </select>
+
+    <select id="selectCatalogTermsByPostId" parameterType="Int" resultMap="CmsTerm">
+        select t.* from cms_term t
+        left join cms_term_relationships r on r.term_id = t.term_id and t.taxonomy = 'catalog'
+        where r.object_id = #{postId}
+    </select>
+
+    <select id="getBindingTermsByTermId" resultMap="CmsTerm">
+        select * from cms_term where term_id in
+        (
+            select r.object_id from cms_term_relationships r
+            where r.term_id = #{termId}
+        )
+    </select>
+
+    <select id="getBindingPostsByTermId" resultMap="CmsPost">
+        select * from cms_post where post_id in
+        (
+            select r.object_id from cms_term_relationships r where r.term_id = #{termId}
+        )
+    </select>
+
+    <delete id="deleteObjectIds">
+        delete from cms_term_relationships
+        where object_id in
+        <foreach collection="objectIds" item="objectId" open="(" separator="," close=")">
+            #{objectId}
+        </foreach>
+    </delete>
+
+    <delete id="deleteObjectIdsByTermId">
+        delete from cms_term_relationships
+        where term_id = #{termId} and object_id in
+        <foreach collection="objectIds" item="objectId" open="(" separator="," close=")">
+            #{objectId}
+        </foreach>
+    </delete>
+
+    <delete id="deleteTermIdsByObjectId">
+        delete from cms_term_relationships
+        where object_id = #{ObjectId} and term_id in
+        <foreach collection="termIds" item="termId" open="(" separator="," close=")">
+            #{termId}
+        </foreach>
+    </delete>
+
+</mapper>

+ 34 - 0
ruoyi-common/build.gradle

@@ -0,0 +1,34 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ *
+ * This project uses @Incubating APIs which are subject to change.
+ */
+
+plugins {
+    id 'com.ruoyi.java-library'
+}
+
+dependencies {
+    api 'org.springframework:spring-web'
+    api 'org.springframework.boot:spring-boot-starter-security'
+    api 'org.springframework.boot:spring-boot-starter-validation'
+    api 'org.springframework.boot:spring-boot-starter-data-redis'
+
+    api 'com.fasterxml.jackson.core:jackson-databind'
+    api 'com.alibaba.fastjson2:fastjson2'
+    api('com.github.pagehelper:pagehelper-spring-boot-starter') {
+        exclude group: 'org.mybatis', module: 'mybatis'
+        exclude group: 'org.mybatis', module: 'mybatis-spring'
+    }
+    api 'com.baomidou:mybatis-plus-boot-starter'
+    api 'commons-io:commons-io'
+    api 'commons-fileupload:commons-fileupload'
+    api 'eu.bitwalker:UserAgentUtils'
+    api 'io.jsonwebtoken:jjwt'
+    api 'javax.servlet:javax.servlet-api'
+    api 'javax.xml.bind:jaxb-api'
+    api 'org.apache.commons:commons-lang3'
+    api 'org.apache.poi:poi-ooxml'
+}
+
+description = 'ruoyi-common 通用工具'

+ 19 - 0
ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java

@@ -0,0 +1,19 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 匿名访问不鉴权注解
+ * 
+ * @author ruoyi
+ */
+@Target({ ElementType.METHOD, ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Anonymous
+{
+}

+ 33 - 0
ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java

@@ -0,0 +1,33 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 数据权限过滤注解
+ * 
+ * @author ruoyi
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DataScope
+{
+    /**
+     * 部门表的别名
+     */
+    public String deptAlias() default "";
+
+    /**
+     * 用户表的别名
+     */
+    public String userAlias() default "";
+
+    /**
+     * 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取,多个权限用逗号分隔开来
+     */
+    public String permission() default "";
+}

+ 0 - 0
ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java


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