Просмотр исходного кода

添加 oss-web 模块, 作为第三方调用的后端接口, 逐步取消第三方直接引入 oss-api

reghao 2 лет назад
Родитель
Сommit
7eccc8276d
100 измененных файлов с 5293 добавлено и 0 удалено
  1. 24 0
      oss-web/bin/restart.sh
  2. 22 0
      oss-web/bin/shutdown.sh
  3. 5 0
      oss-web/bin/start.sh
  4. 233 0
      oss-web/pom.xml
  5. 15 0
      oss-web/src/main/java/cn/reghao/oss/web/OssWebApplication.java
  6. 92 0
      oss-web/src/main/java/cn/reghao/oss/web/account/controller/AccountAuthController.java
  7. 47 0
      oss-web/src/main/java/cn/reghao/oss/web/account/controller/AccountCodeController.java
  8. 106 0
      oss-web/src/main/java/cn/reghao/oss/web/account/controller/AccountProfileController.java
  9. 53 0
      oss-web/src/main/java/cn/reghao/oss/web/account/controller/FileController.java
  10. 85 0
      oss-web/src/main/java/cn/reghao/oss/web/account/controller/MenuController.java
  11. 77 0
      oss-web/src/main/java/cn/reghao/oss/web/account/controller/RoleController.java
  12. 98 0
      oss-web/src/main/java/cn/reghao/oss/web/account/controller/page/MenuPageController.java
  13. 85 0
      oss-web/src/main/java/cn/reghao/oss/web/account/controller/page/RolePageController.java
  14. 136 0
      oss-web/src/main/java/cn/reghao/oss/web/account/controller/page/UserPageController.java
  15. 15 0
      oss-web/src/main/java/cn/reghao/oss/web/account/db/query/MenuQuery.java
  16. 54 0
      oss-web/src/main/java/cn/reghao/oss/web/account/db/query/MenuQueryImpl.java
  17. 30 0
      oss-web/src/main/java/cn/reghao/oss/web/account/db/query/RoleQuery.java
  18. 107 0
      oss-web/src/main/java/cn/reghao/oss/web/account/db/query/RoleQueryImpl.java
  19. 28 0
      oss-web/src/main/java/cn/reghao/oss/web/account/db/query/UserQuery.java
  20. 84 0
      oss-web/src/main/java/cn/reghao/oss/web/account/db/query/UserQueryImpl.java
  21. 15 0
      oss-web/src/main/java/cn/reghao/oss/web/account/db/repository/DiskFileRepository.java
  22. 15 0
      oss-web/src/main/java/cn/reghao/oss/web/account/db/repository/MenuRepository.java
  23. 13 0
      oss-web/src/main/java/cn/reghao/oss/web/account/db/repository/RoleRepository.java
  24. 13 0
      oss-web/src/main/java/cn/reghao/oss/web/account/db/repository/UserRepository.java
  25. 9 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/constant/DataStatus.java
  26. 11 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/constant/MenuType.java
  27. 25 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/constant/RoleType.java
  28. 34 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/constant/UserGender.java
  29. 33 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/AccountLoginDto.java
  30. 21 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/AccountProfile.java
  31. 20 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/AccountRole.java
  32. 31 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/CreateAccountDto.java
  33. 44 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/MenuAddDTO.java
  34. 46 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/MenuDTO.java
  35. 52 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/MenuUpdateDTO.java
  36. 34 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/RoleDTO.java
  37. 19 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/RsaPubkey.java
  38. 19 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/UpdatePasswordDto.java
  39. 24 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/UserUpdateDTO.java
  40. 57 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/po/DiskFile.java
  41. 52 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/po/Menu.java
  42. 43 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/po/Role.java
  43. 135 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/po/User.java
  44. 49 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/po/UserAuthority.java
  45. 26 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/vo/MenuVO.java
  46. 28 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/vo/RoleVO.java
  47. 35 0
      oss-web/src/main/java/cn/reghao/oss/web/account/model/vo/UserVO.java
  48. 28 0
      oss-web/src/main/java/cn/reghao/oss/web/account/security/ExceptionAuthenticationEntryPoint.java
  49. 203 0
      oss-web/src/main/java/cn/reghao/oss/web/account/security/WebSecurityConfig.java
  50. 36 0
      oss-web/src/main/java/cn/reghao/oss/web/account/security/encoder/Md5PasswordEncoder.java
  51. 13 0
      oss-web/src/main/java/cn/reghao/oss/web/account/security/exceptioin/AccountLoginException.java
  52. 33 0
      oss-web/src/main/java/cn/reghao/oss/web/account/security/filter/LoginRedirectFilter.java
  53. 48 0
      oss-web/src/main/java/cn/reghao/oss/web/account/security/form/AccountAuthFilter.java
  54. 42 0
      oss-web/src/main/java/cn/reghao/oss/web/account/security/form/AccountAuthProvider.java
  55. 126 0
      oss-web/src/main/java/cn/reghao/oss/web/account/security/form/AccountAuthToken.java
  56. 36 0
      oss-web/src/main/java/cn/reghao/oss/web/account/security/handler/AuthFailHandlerImpl.java
  57. 55 0
      oss-web/src/main/java/cn/reghao/oss/web/account/security/handler/AuthSuccessHandlerImpl.java
  58. 27 0
      oss-web/src/main/java/cn/reghao/oss/web/account/security/handler/LogoutHandlerImpl.java
  59. 27 0
      oss-web/src/main/java/cn/reghao/oss/web/account/security/handler/LogoutSuccessHandlerImpl.java
  60. 34 0
      oss-web/src/main/java/cn/reghao/oss/web/account/security/session/SecuritySessionConfig.java
  61. 33 0
      oss-web/src/main/java/cn/reghao/oss/web/account/security/session/SessionExpiredStrategy.java
  62. 19 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/AccountAuthService.java
  63. 20 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/AccountService.java
  64. 56 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/AccountSessionService.java
  65. 42 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/CodeService.java
  66. 15 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/FileService.java
  67. 70 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/HomeService.java
  68. 15 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/MenuService.java
  69. 58 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/PubkeyService.java
  70. 17 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/RoleService.java
  71. 51 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/UserContext.java
  72. 155 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/impl/AccountAuthServiceImpl.java
  73. 154 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/impl/AccountServiceImpl.java
  74. 157 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/impl/FileServiceImpl.java
  75. 237 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/impl/MenuServiceImpl.java
  76. 77 0
      oss-web/src/main/java/cn/reghao/oss/web/account/service/impl/RoleServiceImpl.java
  77. 62 0
      oss-web/src/main/java/cn/reghao/oss/web/app/controller/AppConfigController.java
  78. 53 0
      oss-web/src/main/java/cn/reghao/oss/web/app/controller/AppStatusController.java
  79. 45 0
      oss-web/src/main/java/cn/reghao/oss/web/app/controller/AudioFileController.java
  80. 68 0
      oss-web/src/main/java/cn/reghao/oss/web/app/controller/BuildDeployController.java
  81. 72 0
      oss-web/src/main/java/cn/reghao/oss/web/app/controller/ImageFileController.java
  82. 82 0
      oss-web/src/main/java/cn/reghao/oss/web/app/controller/MediaController.java
  83. 56 0
      oss-web/src/main/java/cn/reghao/oss/web/app/controller/OssController.java
  84. 57 0
      oss-web/src/main/java/cn/reghao/oss/web/app/controller/VideoFileController.java
  85. 113 0
      oss-web/src/main/java/cn/reghao/oss/web/app/controller/page/AppConfigPageController.java
  86. 85 0
      oss-web/src/main/java/cn/reghao/oss/web/app/controller/page/BuildDeployPageController.java
  87. 51 0
      oss-web/src/main/java/cn/reghao/oss/web/app/controller/page/StatusPageController.java
  88. 19 0
      oss-web/src/main/java/cn/reghao/oss/web/app/db/query/AppConfigQuery.java
  89. 63 0
      oss-web/src/main/java/cn/reghao/oss/web/app/db/query/impl/AppConfigQueryImpl.java
  90. 21 0
      oss-web/src/main/java/cn/reghao/oss/web/app/db/repository/AppConfigRepository.java
  91. 15 0
      oss-web/src/main/java/cn/reghao/oss/web/app/model/constant/AppType.java
  92. 30 0
      oss-web/src/main/java/cn/reghao/oss/web/app/model/constant/BuildStatus.java
  93. 30 0
      oss-web/src/main/java/cn/reghao/oss/web/app/model/constant/DeployStatus.java
  94. 15 0
      oss-web/src/main/java/cn/reghao/oss/web/app/model/constant/build/CompileType.java
  95. 13 0
      oss-web/src/main/java/cn/reghao/oss/web/app/model/constant/build/RepoAuthType.java
  96. 13 0
      oss-web/src/main/java/cn/reghao/oss/web/app/model/constant/build/RepoType.java
  97. 43 0
      oss-web/src/main/java/cn/reghao/oss/web/app/model/dto/AppConfigDto.java
  98. 33 0
      oss-web/src/main/java/cn/reghao/oss/web/app/model/dto/AppConfigUpdateDto.java
  99. 42 0
      oss-web/src/main/java/cn/reghao/oss/web/app/model/dto/CopyAppDto.java
  100. 29 0
      oss-web/src/main/java/cn/reghao/oss/web/app/model/dto/DeployConfigDto.java

+ 24 - 0
oss-web/bin/restart.sh

@@ -0,0 +1,24 @@
+#!/bin/bash
+
+app_dir=`pwd`
+app_name='oss-web.jar'
+
+pid=`ps aux | grep ${app_name} | grep -v 'grep' | tr -s ' '| cut -d ' ' -f 2`
+echo "process id: "${pid}
+if [[ -z ${pid} ]];
+then
+  echo "process killed"
+else
+  kill -15 ${pid}
+fi
+
+echo "sleep 10s and wait process killed"
+sleep 10
+pid=`ps aux | grep ${app_name} | grep -v 'grep' | tr -s ' '| cut -d ' ' -f 2`
+if [[ -z ${pid} ]];
+then
+  echo "${app_name} has killed, restart now"
+  nohup java -jar ${app_dir}"/"${app_name} > console.log 2>&1 &
+else
+  echo "process ${pid} not killed"
+fi

+ 22 - 0
oss-web/bin/shutdown.sh

@@ -0,0 +1,22 @@
+#!/bin/bash
+
+app_name='oss-web.jar'
+
+pid=`ps aux | grep ${app_name} | grep -v 'grep' | tr -s ' '| cut -d ' ' -f 2`
+echo "process id: "${pid}
+if [[ -z ${pid} ]];
+then
+  echo "process killed"
+else
+  kill -15 ${pid}
+fi
+
+echo "sleep 10s and wait process killed"
+sleep 10
+pid=`ps aux | grep ${app_name} | grep -v 'grep' | tr -s ' '| cut -d ' ' -f 2`
+if [[ -z ${pid} ]];
+then
+  echo "${app_name} has killed"
+else
+  echo "process ${pid} not killed"
+fi

+ 5 - 0
oss-web/bin/start.sh

@@ -0,0 +1,5 @@
+#!/bin/bash
+
+app_dir=`pwd`
+app_name='oss-web.jar'
+nohup java -jar ${app_dir}"/"${app_name} > console.log 2>&1 &

+ 233 - 0
oss-web/pom.xml

@@ -0,0 +1,233 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>cn.reghao.oss</groupId>
+    <artifactId>oss-web</artifactId>
+    <version>1.0.0</version>
+    <packaging>jar</packaging>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>2.3.9.RELEASE</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.reghao.jutil</groupId>
+            <artifactId>jdk</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.reghao.jutil</groupId>
+            <artifactId>tool</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.18.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+            <version>1.2.3</version>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>1.2.3</version>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.reghao.jutil</groupId>
+            <artifactId>web</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.validation</groupId>
+            <artifactId>validation-api</artifactId>
+            <version>2.0.1.Final</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <version>8.0.17</version>
+        </dependency>
+        <dependency>
+            <groupId>com.zaxxer</groupId>
+            <artifactId>HikariCP</artifactId>
+            <version>3.3.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-thymeleaf</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.thymeleaf.extras</groupId>
+            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-cache</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+            <version>2.9.2</version>
+        </dependency>
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+            <version>2.9.2</version>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.reghao.oss</groupId>
+            <artifactId>oss-api</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.dubbo</groupId>
+            <artifactId>dubbo-spring-boot-starter</artifactId>
+            <version>2.7.8</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.curator</groupId>
+            <artifactId>curator-recipes</artifactId>
+            <version>2.12.0</version>
+        </dependency>
+    </dependencies>
+
+    <profiles>
+        <profile>
+            <id>dev</id>
+            <properties>
+                <profile.active>dev</profile.active>
+            </properties>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+        </profile>
+        <profile>
+            <id>test</id>
+            <properties>
+                <profile.active>test</profile.active>
+            </properties>
+        </profile>
+    </profiles>
+
+    <build>
+        <finalName>oss-web</finalName>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+                <includes>
+                    <include>application.yml</include>
+                    <include>application-${profile.active}.yml</include>
+                    <include>logback-spring.xml</include>
+                    <!-- 前端静态资源 -->
+                    <include>static/**</include>
+                    <include>templates/**</include>
+                </includes>
+            </resource>
+        </resources>
+
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>11</source>
+                    <target>11</target>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-resources-plugin</artifactId>
+                <version>2.4.3</version>
+                <configuration>
+                    <!-- 让 maven 不处理字体 -->
+                    <nonFilteredFileExtensions>
+                        <nonFilteredFileExtension>ttf</nonFilteredFileExtension>
+                        <nonFilteredFileExtension>woff</nonFilteredFileExtension>
+                        <nonFilteredFileExtension>woff2</nonFilteredFileExtension>
+                    </nonFilteredFileExtensions>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.3.9.RELEASE</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 15 - 0
oss-web/src/main/java/cn/reghao/oss/web/OssWebApplication.java

@@ -0,0 +1,15 @@
+package cn.reghao.oss.web;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+@SpringBootApplication
+@EnableJpaRepositories
+@EntityScan({"cn.reghao.oss.web"})
+public class OssWebApplication {
+	public static void main(String[] args) {
+		SpringApplication.run(OssWebApplication.class, args);
+	}
+}

+ 92 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/controller/AccountAuthController.java

@@ -0,0 +1,92 @@
+package cn.reghao.oss.web.account.controller;
+
+import cn.reghao.oss.web.account.model.constant.RoleType;
+import cn.reghao.oss.web.account.model.po.Menu;
+import cn.reghao.oss.web.account.model.po.User;
+import cn.reghao.oss.web.account.service.HomeService;
+import cn.reghao.oss.web.account.service.UserContext;
+import cn.reghao.oss.web.sys.db.repository.SysMessageRepository;
+import io.swagger.annotations.Api;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2021-04-04 21:24:18
+ */
+@Slf4j
+@Api(tags = "登录页和首页")
+@Controller
+public class AccountAuthController {
+    private final HomeService homeService;
+    private final SysMessageRepository sysMessageRepository;
+
+    public AccountAuthController(HomeService homeService, SysMessageRepository sysMessageRepository) {
+        this.homeService = homeService;
+        this.sysMessageRepository = sysMessageRepository;
+    }
+    
+    @GetMapping("/login")
+    public String toLogin(Model model) {
+        model.addAttribute("isCaptcha", false);
+        return "/login";
+    }
+
+    @GetMapping("/")
+    public String index(Model model) throws Exception {
+        User user = UserContext.getUser();
+        if (user == null) {
+            throw new Exception("未登录");
+        }
+
+        boolean isAdmin = user.getRole().contains(RoleType.ROLE_ADMIN.name());
+        if (isAdmin) {
+            int unreadTotal = sysMessageRepository.countByUnread(true);
+            String unreadMessage;
+            if (unreadTotal > 9) {
+                unreadMessage = "9+";
+            } else {
+                unreadMessage = String.valueOf(unreadTotal);
+            }
+            model.addAttribute("unreadMessage", unreadMessage);
+        }
+
+        List<Menu> menus = homeService.userMenus(user.getRole());
+        Map<Integer, Menu> treeMenu = homeService.treeMenu(menus);
+        model.addAttribute("user", user);
+        model.addAttribute("treeMenu", treeMenu);
+        return "/main";
+    }
+
+    @GetMapping("/home")
+    public String home(Model model) {
+        /*String commitId = NotAvailable.na.getDesc();
+        if (appVersion != null) {
+            commitId = appVersion.getCommitId();
+        }
+
+        JvmInfo jvmInfo = jvm.info();
+        //JvmStat jvmStat = jvm.stat();
+        String osInfo = String.format("%s %s", jvmInfo.getOsName(), jvmInfo.getOsVersion());
+        String jvmInfo1 = String.format("%s %s", jvmInfo.getJvmName(), jvmInfo.getJvmVersion());
+        int pid = jvmInfo.getJvmPid();
+        String startAt = jvmInfo.getJvmStartTime();
+        String ipv4 = network.detail().get(0).getIpv4();*/
+
+        model.addAttribute("managerVersion", "commitId");
+        model.addAttribute("hostAddr", "ipv4");
+        model.addAttribute("osInfo", "osInfo");
+        model.addAttribute("jvmInfo", "jvmInfo1");
+        model.addAttribute("pid", "pid");
+        model.addAttribute("startAt", "startAt");
+        //model.addAttribute("jvmStat", jvmStat);
+        String template = "/home/index";
+        template = "/home/index1";
+        return template;
+    }
+}

+ 47 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/controller/AccountCodeController.java

@@ -0,0 +1,47 @@
+package cn.reghao.oss.web.account.controller;
+
+import cn.reghao.oss.web.account.model.dto.RsaPubkey;
+import cn.reghao.oss.web.account.service.CodeService;
+import cn.reghao.oss.web.account.service.PubkeyService;
+import cn.reghao.jutil.jdk.result.WebResult;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+
+/**
+ * @author reghao
+ * @date 2022-02-18 16:05:30
+ */
+@RestController
+@RequestMapping("/api/account/code")
+public class AccountCodeController {
+    private final CodeService codeService;
+    private final PubkeyService pubkeyService;
+
+    public AccountCodeController(CodeService codeService, PubkeyService pubkeyService) {
+        this.codeService = codeService;
+        this.pubkeyService = pubkeyService;
+    }
+
+    @GetMapping(value = "/pubkey", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getPubkey() throws NoSuchAlgorithmException {
+        RsaPubkey rsaPubkey = pubkeyService.getPubkey();
+        return WebResult.success(rsaPubkey);
+    }
+
+    @GetMapping(value = "/captcha", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getCaptcha() throws IOException {
+        InputStream in = codeService.generateCaptcha();
+        byte[] base64Bytes = Base64.getEncoder().encode(in.readAllBytes());
+        String base64Str = new String(base64Bytes, StandardCharsets.UTF_8);
+        String imageStr = String.format("data:image/jpeg;base64,%s", base64Str);
+        return WebResult.success(imageStr);
+    }
+}

+ 106 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/controller/AccountProfileController.java

@@ -0,0 +1,106 @@
+package cn.reghao.oss.web.account.controller;
+
+import cn.reghao.oss.web.account.model.dto.UpdatePasswordDto;
+import cn.reghao.oss.web.account.model.dto.CreateAccountDto;
+import cn.reghao.jutil.jdk.result.Result;
+import cn.reghao.jutil.jdk.result.WebResult;
+import cn.reghao.oss.web.account.model.dto.AccountProfile;
+import cn.reghao.oss.web.account.model.dto.AccountRole;
+import cn.reghao.oss.web.account.service.AccountService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.MediaType;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2021-04-04 21:24:18
+ */
+@Api(tags = "用户接口")
+@RequestMapping("/api/rbac/user")
+@RestController
+public class AccountProfileController {
+    private final AccountService accountService;
+
+    public AccountProfileController(AccountService accountService) {
+        this.accountService = accountService;
+    }
+
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    @ApiOperation(value = "创建用户")
+    @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+    public String createUser(@Validated CreateAccountDto createAccountDto) {
+        Result result = accountService.createAccount(createAccountDto);
+        return WebResult.result(result);
+    }
+
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    @ApiOperation(value = "批量创建用户")
+    @PostMapping(value = "/batch", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String batchAdd(MultipartFile file) {
+        return WebResult.success();
+    }
+
+    @PostMapping(value = "/avatar", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String userAvatar(MultipartFile file) {
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "修改用户信息")
+    @PostMapping(value = "/modify", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Deprecated
+    public String updateAccountProfile(@Validated AccountProfile accountProfile) {
+        accountService.updateAccountProfile(accountProfile);
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "修改用户信息")
+    @PostMapping(value = "/edit", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String editUserInfo(@Validated AccountProfile accountProfile) {
+        accountService.updateAccountProfile(accountProfile);
+        return WebResult.success();
+    }
+
+    @PreAuthorize("hasRole('ROLE_ADMIN')")
+    @ApiOperation(value = "删除用户")
+    @DeleteMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deleteAccount(@PathVariable("id") Integer userId) {
+        accountService.deleteAccount(userId);
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "修改用户密码")
+    @PostMapping(value = "/passwd", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String modifyPassword(@NotNull Integer id, @NotNull String newPassword) {
+        accountService.updateAccountPassword(id, newPassword);
+        return WebResult.success();
+    }
+
+    @PostMapping(value = "/passwd/update", produces = MediaType.APPLICATION_JSON_VALUE)
+    @ResponseBody
+    public String editPasswd(UpdatePasswordDto updatePasswordDto) {
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "分配用户角色")
+    @PostMapping(value = "/role", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String assignRole(@Validated AccountRole accountRole) {
+        accountService.updateAccountRole(accountRole);
+        return WebResult.success();
+    }
+
+    // TODO 暂不启用本功能
+    @ApiOperation(value = "启用/禁用用户")
+    @PostMapping(value = "/status/{enable}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String updateAccountStatus(@PathVariable("enable") Boolean enable,
+                                @RequestParam(value = "ids") List<Integer> userIds) {
+        userIds.forEach(userId -> accountService.updateAccountStatus(userId, enable));
+        return WebResult.success();
+    }
+}

+ 53 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/controller/FileController.java

@@ -0,0 +1,53 @@
+package cn.reghao.oss.web.account.controller;
+
+import cn.reghao.oss.web.account.service.FileService;
+import cn.reghao.jutil.jdk.result.WebResult;
+import cn.reghao.jutil.web.ServletUtil;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2024-01-20 17:24:58
+ */
+@Controller
+@RequestMapping
+public class FileController {
+    private final FileService fileService;
+
+    public FileController(FileService fileService) {
+        this.fileService = fileService;
+    }
+
+    @PostMapping(value = "/api/file/upload", produces = MediaType.APPLICATION_JSON_VALUE)
+    @ResponseBody
+    public String imageUpload(MultipartFile file) throws Exception {
+        String url = fileService.putFile(file);
+        Map<String, String> map = new HashMap<>();
+        map.put("name", file.getOriginalFilename());
+        map.put("url", url);
+        return WebResult.success(map);
+    }
+
+    @GetMapping("/file/**")
+    @ResponseBody
+    public void getFile() throws IOException {
+        HttpServletRequest servletRequest = ServletUtil.getRequest();
+        String uri = servletRequest.getRequestURI();
+        String uri1 = URLDecoder.decode(uri, StandardCharsets.UTF_8);
+        String objectName =  uri1.replaceFirst("/", "");
+        fileService.getFile(objectName);
+    }
+}

+ 85 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/controller/MenuController.java

@@ -0,0 +1,85 @@
+package cn.reghao.oss.web.account.controller;
+
+import cn.reghao.jutil.jdk.result.Result;
+import cn.reghao.oss.web.account.db.query.MenuQuery;
+import cn.reghao.oss.web.account.model.dto.MenuDTO;
+import cn.reghao.oss.web.account.model.po.Menu;
+import cn.reghao.oss.web.account.service.impl.MenuServiceImpl;
+import cn.reghao.jutil.jdk.result.WebResult;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2021-04-04 21:24:18
+ */
+@Api(tags = "资源接口")
+@RequestMapping("/api/rbac/menu")
+@RestController
+public class MenuController {
+    private final MenuServiceImpl menuServiceImpl;
+    private final MenuQuery menuQuery;
+
+    public MenuController(MenuServiceImpl menuServiceImpl, MenuQuery menuQuery) {
+        this.menuServiceImpl = menuServiceImpl;
+        this.menuQuery = menuQuery;
+    }
+
+    @ApiOperation(value = "添加资源")
+    @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+    public String addMenu(@Validated Menu menu) {
+        Result result = menuServiceImpl.addMenu(menu);
+        return WebResult.result(result);
+    }
+
+    @ApiOperation(value = "修改资源")
+    @PostMapping(value = "/edit", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String modifyMenu(@Validated MenuDTO menuDTO) {
+        Result result = menuServiceImpl.updateMenu(menuDTO);
+        return WebResult.result(result);
+    }
+
+    @ApiOperation(value = "修改资源状态")
+    @PostMapping(value = "/status/{enabled}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String menuStatus(@PathVariable("enabled") boolean enabled, @RequestParam("ids") List<Integer> ids) {
+        menuServiceImpl.updateMenusStatus(enabled, ids);
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "删除资源")
+    @DeleteMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deleteMenu(@PathVariable("id") Integer menuId) {
+        Result result = menuServiceImpl.deleteMenu(menuId);
+        return WebResult.result(result);
+    }
+
+    @ApiOperation(value = "获取指定状态的菜单")
+    @GetMapping(value = "/{isEnabled}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String list(@PathVariable(value = "isEnabled") Boolean isEnabled) {
+        List<Menu> list = menuQuery.getSortedMenusByStatus(isEnabled).stream()
+                .peek(menu -> menu.getRoles().forEach(role -> {
+                    role.setMenus(null);
+                }))
+                .collect(Collectors.toList());
+        return WebResult.success(list);
+    }
+
+    @ApiOperation(value = "对同一 pid 组内的资源进行排序")
+    @GetMapping(value = "/sorted/{pid}/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String sortList(@PathVariable(value = "pid") int pid,
+                           @PathVariable(value = "id", required = false) Menu menu) {
+        Map<Integer, String> map = menuQuery.getSortedChildGroupByPid(pid);
+        // 排除当前 menu
+        if (menu != null) {
+            map.remove(menu.getPos());
+        }
+        return WebResult.success(map);
+    }
+}

+ 77 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/controller/RoleController.java

@@ -0,0 +1,77 @@
+package cn.reghao.oss.web.account.controller;
+
+import cn.reghao.oss.web.account.db.query.MenuQuery;
+import cn.reghao.oss.web.account.model.po.Menu;
+import cn.reghao.oss.web.account.model.po.Role;
+import cn.reghao.oss.web.account.service.RoleService;
+import cn.reghao.jutil.jdk.result.WebResult;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2021-04-04 21:24:18
+ */
+@Api(tags = "角色接口")
+@RequestMapping("/api/rbac/role")
+@RestController
+public class RoleController {
+    private final MenuQuery menuQuery;
+    private final RoleService roleService;
+
+    public RoleController(MenuQuery menuQuery, RoleService roleService) {
+        this.menuQuery = menuQuery;
+        this.roleService = roleService;
+    }
+
+    @ApiOperation("添加或修改角色")
+    @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+    public String addOrModifyRole(@Validated Role role) {
+        roleService.addOrModify(role);
+        return WebResult.success();
+    }
+
+    @ApiOperation("删除角色")
+    @DeleteMapping(value = "/{roleId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deleteRole(@PathVariable("roleId") Integer roleId) {
+        roleService.delete(roleId);
+        return WebResult.success();
+    }
+
+    @ApiOperation("获取角色可访问的资源")
+    @GetMapping(value = "/menus/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getRoleMenus(@PathVariable("id") Role role) {
+        List<Menu> allMenus = menuQuery.findAll();
+        allMenus.forEach(menu -> {
+            // TODO 序列化时会递归 roles 中的 Role 导致 StackOverflow, 因此在序列化时应该把 Role 的 menus 设置为 null
+            Set<Role> roles = menu.getRoles().stream()
+                    .peek(role1 -> role1.setMenus(null))
+                    .collect(Collectors.toSet());
+            if (roles.contains(role)) {
+                // 对应前端的 checked 复选框
+                menu.setRemark("auth:true");
+            }
+        });
+
+        return WebResult.success(allMenus);
+    }
+
+    @ApiOperation("设置角色可访问的资源")
+    @PostMapping(value = "/menus", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String setRoleMenus(@RequestParam(value = "id") Integer roleId,
+                               @RequestParam(value = "menuId", required = false) Set<Menu> menus) {
+        if (menus == null) {
+            menus = Collections.emptySet();
+        }
+        roleService.setRoleMenus(roleId, menus);
+        return WebResult.success();
+    }
+}

+ 98 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/controller/page/MenuPageController.java

@@ -0,0 +1,98 @@
+package cn.reghao.oss.web.account.controller.page;
+
+import cn.reghao.oss.web.account.db.query.MenuQuery;
+import cn.reghao.oss.web.account.db.query.RoleQuery;
+import cn.reghao.oss.web.account.model.dto.MenuDTO;
+import cn.reghao.oss.web.account.model.po.Role;
+import cn.reghao.oss.web.account.model.po.Menu;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * @author reghao
+ * @date 2021-04-04 21:24:18
+ */
+@Api(tags = "资源页面")
+@RequestMapping("/rbac/menu")
+@Controller
+public class MenuPageController {
+    private final RoleQuery roleQuery;
+    private final MenuQuery menuQuery;
+
+    public MenuPageController(RoleQuery roleQuery, MenuQuery menuQuery) {
+        this.roleQuery = roleQuery;
+        this.menuQuery = menuQuery;
+    }
+
+    @ApiOperation(value = "资源列表页面")
+    @GetMapping
+    public String menuPage(@RequestParam(value = "enabled", required = false) Boolean enabled, Model model) {
+        if (enabled == null) {
+            enabled = true;
+        }
+
+        model.addAttribute("enabled", enabled);
+        return "/rbac/menu/index";
+    }
+
+    @ApiOperation(value = "资源添加页面")
+    @GetMapping({"/add", "/add/{pid}"})
+    public String toAdd(@PathVariable(value = "pid", required = false) Integer pid, Model model) {
+        // 父级菜单
+        Menu pMenu = null;
+        if (pid != null) {
+            pMenu = menuQuery.findById(pid);
+        }
+
+        Set<Role> allRoles = new HashSet<>(roleQuery.findAll());
+        Set<Role> menuRoles = Collections.emptySet();
+
+        model.addAttribute("allRoles", allRoles);
+        model.addAttribute("menuRoles", menuRoles);
+        model.addAttribute("pMenu", pMenu);
+        return "/rbac/menu/add";
+    }
+
+    @ApiOperation(value = "资源编辑页面")
+    @GetMapping("/edit/{id}")
+    public String toEdit(@PathVariable("id") Menu menu, Model model) {
+        int pid = menu.getPid();
+        Menu pMenu;
+        if (pid == 0) {
+            pMenu = new Menu(pid, "根菜单");
+        } else {
+            pMenu = menuQuery.findById(pid);
+        }
+
+        Set<Role> allRoles = new HashSet<>(roleQuery.findAll());
+        //Set<Role> menuRoles = menu.getRoles();
+        Set<Role> menuRoles = Collections.emptySet();
+
+        model.addAttribute("allRoles", allRoles);
+        model.addAttribute("menuRoles", menuRoles);
+        model.addAttribute("menu", new MenuDTO(menu));
+        model.addAttribute("pMenu", pMenu);
+        return "/rbac/menu/edit";
+    }
+
+    // TODO Hibernate 会根据传入的 id 自动查找相应的 Menu
+    @ApiOperation(value = "可访问资源的角色列表页面")
+    @GetMapping("/roleList/{id}")
+    public String roleListWithResource(@PathVariable("id") Menu menu, Model model) {
+        List<Role> list = new ArrayList<>(menu.getRoles());
+        model.addAttribute("list", list);
+        return "/rbac/menu/roles";
+    }
+
+    @ApiOperation(value = "资源详细信息页面")
+    @GetMapping("/detail/{id}")
+    public String toDetail(@PathVariable("id") Menu menu, Model model) {
+        model.addAttribute("menu", menu);
+        return "/rbac/menu/detail";
+    }
+}

+ 85 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/controller/page/RolePageController.java

@@ -0,0 +1,85 @@
+package cn.reghao.oss.web.account.controller.page;
+
+import cn.reghao.oss.web.account.db.query.RoleQuery;
+import cn.reghao.oss.web.account.model.po.User;
+import cn.reghao.oss.web.account.model.vo.RoleVO;
+import cn.reghao.oss.web.util.db.PageSort;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2021-04-04 21:24:18
+ */
+@Api(tags = "角色页面")
+@RequestMapping("/rbac/role")
+@Controller
+public class RolePageController {
+    private RoleQuery roleQuery;
+
+    public RolePageController(RoleQuery roleQuery) {
+        this.roleQuery = roleQuery;
+    }
+
+    @ApiOperation("角色列表页面")
+    @GetMapping
+    public String rolePage(@RequestParam(value = "name", required = false) String name, Model model) {
+        Page<RoleVO> page;
+        if (name != null) {
+            List<RoleVO> list = roleQuery.getByMatchName(name);
+            page = new PageImpl<>(list);
+        } else {
+            PageRequest pageRequest = PageSort.pageRequest();
+            page = roleQuery.getRoleVOByPage(pageRequest);
+        }
+
+        model.addAttribute("page", page);
+        model.addAttribute("list", page.getContent());
+        return "/rbac/role/index";
+    }
+
+    @ApiOperation("角色新增页面")
+    @GetMapping("/add")
+    public String addRolePage() {
+        return "/rbac/role/add";
+    }
+
+    @ApiOperation("角色编辑页面")
+    @GetMapping("/edit/{id}")
+    public String editRolePage(@PathVariable("id") int id, Model model) {
+        RoleVO vo = roleQuery.getRoleVOById(id);
+        model.addAttribute("role", vo);
+        return "/rbac/role/add";
+    }
+
+    @ApiOperation("角色详细信息页面")
+    @GetMapping("/detail/{id}")
+    public String roleDetailPage(@PathVariable("id") int id, Model model) {
+        RoleVO vo = roleQuery.getRoleVOById(id);
+        model.addAttribute("role", vo);
+        return "/rbac/role/detail";
+    }
+
+    @ApiOperation("设置角色可访问的资源页面")
+    @GetMapping("/menus/{id}")
+    public String menusPage(@PathVariable(value = "id") Integer id, Model model){
+        model.addAttribute("roleId", id);
+        return "/rbac/role/menus";
+    }
+
+    @ApiOperation("拥有角色的所有用户页面")
+    @GetMapping("/users/{id}")
+    public String userListWithRole(@PathVariable("id") Integer roleId, Model model) {
+        List<User> list = roleQuery.getUsersByRoleId(roleId);
+        model.addAttribute("list", list);
+        return "/rbac/role/users";
+    }
+}

+ 136 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/controller/page/UserPageController.java

@@ -0,0 +1,136 @@
+package cn.reghao.oss.web.account.controller.page;
+
+import cn.reghao.oss.web.account.db.query.RoleQuery;
+import cn.reghao.oss.web.account.db.query.UserQuery;
+import cn.reghao.oss.web.account.model.po.Role;
+import cn.reghao.oss.web.account.model.po.User;
+import cn.reghao.oss.web.account.model.vo.UserVO;
+import cn.reghao.oss.web.account.service.AccountSessionService;
+import cn.reghao.oss.web.account.service.UserContext;
+import cn.reghao.oss.web.util.db.PageSort;
+import cn.reghao.jutil.jdk.converter.DateTimeConverter;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2021-04-04 21:24:18
+ */
+@Api(tags = "用户页面")
+@RequestMapping("/rbac/user")
+@Controller
+public class UserPageController {
+    private final UserQuery userQuery;
+    private final RoleQuery roleQuery;
+    private final AccountSessionService accountSessionService;
+
+    public UserPageController(UserQuery userQuery, RoleQuery roleQuery, AccountSessionService accountSessionService) {
+        this.userQuery = userQuery;
+        this.roleQuery = roleQuery;
+        this.accountSessionService = accountSessionService;
+    }
+
+    @ApiOperation(value = "用户列表页面")
+    @GetMapping
+    public String userPage(@RequestParam(value = "screenName", required = false) String screenName, Model model) {
+        Page<UserVO> page;
+        if (screenName != null) {
+            List<UserVO> list = userQuery.getByMatchScreenName(screenName);
+            page = new PageImpl<>(list);
+        } else {
+            PageRequest pageRequest = PageSort.pageRequest();
+            page = userQuery.getUserVOByPage(pageRequest);
+        }
+
+        Map<Integer, LocalDateTime> map = accountSessionService.getLastRequest();
+        page.getContent().forEach(userVO -> {
+            int userId = userVO.getUserId();
+            LocalDateTime lastAccess = map.get(userId);
+            if (lastAccess != null) {
+                userVO.setLastAccess(DateTimeConverter.format(lastAccess));
+            }
+        });
+
+        model.addAttribute("page", page);
+        model.addAttribute("list", page.getContent());
+        return "/rbac/user/index";
+    }
+
+    @ApiOperation(value = "新增用户页面")
+    @GetMapping("/add")
+    public String addUserPage(Model model) {
+        Set<Role> allRoles = new HashSet<>(new HashSet<>(roleQuery.findAll()));
+        Set<Role> userRoles = Collections.emptySet();
+
+        model.addAttribute("allRoles", allRoles);
+        model.addAttribute("userRoles", userRoles);
+        return "/rbac/user/add";
+    }
+
+    @ApiOperation(value = "用户信息编辑页面")
+    @GetMapping("/edit/{id}")
+    @Deprecated
+    public String editUserPage(@PathVariable("id") User user, Model model) {
+        Set<Role> allRoles = new HashSet<>(roleQuery.findAll());
+        Set<Role> userRoles = userQuery.getUserRoles(user);
+
+        model.addAttribute("allRoles", allRoles);
+        model.addAttribute("userRoles", userRoles);
+        model.addAttribute("user", user);
+        return "/rbac/user/edit";
+    }
+
+    @ApiOperation(value = "用户详细信息页面")
+    @GetMapping("/detail/{id}")
+    public String userDetailPage(@PathVariable("id") int id, Model model) {
+        User user = userQuery.findById(id);
+        Set<Role> roles = userQuery.getUserRoles(user);
+        List<String> names = roles.stream().map(Role::getName).collect(Collectors.toList());
+
+        model.addAttribute("roles", names.toString());
+        model.addAttribute("user", user);
+        return "/rbac/user/detail";
+    }
+
+    @GetMapping("/profile")
+    public String userInfoPage(Model model) {
+        User user = UserContext.getUser();
+        model.addAttribute("user", user);
+        return "/rbac/user/userinfo";
+    }
+
+    @ApiOperation(value = "用户修改密码页面")
+    @GetMapping("/passwd/{id}")
+    public String modifyPasswordPage(@PathVariable("id") Integer id, Model model) {
+        model.addAttribute("id", id);
+        return "/rbac/user/passwd";
+    }
+
+    @GetMapping("/passwd/edit")
+    public String editPasswdPage(Model model) {
+        return "/rbac/user/editpasswd";
+    }
+
+    @ApiOperation(value = "用户角色分配页面")
+    @GetMapping("/role/{id}")
+    public String assignRolePage(@PathVariable("id") User user, Model model) {
+        Set<Role> roles = new HashSet<>(roleQuery.findAll());
+        int userId = user.getId();
+        Set<Role> authRoles = userQuery.getUserRoles(userId);
+
+        model.addAttribute("id", userId);
+        model.addAttribute("list", roles);
+        model.addAttribute("authRoles", authRoles);
+        return "/rbac/user/role";
+    }
+}

+ 15 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/db/query/MenuQuery.java

@@ -0,0 +1,15 @@
+package cn.reghao.oss.web.account.db.query;
+
+import cn.reghao.oss.web.account.model.po.Menu;
+import cn.reghao.jutil.jdk.db.BaseQuery;
+
+import java.util.*;
+
+/**
+ * @author reghao
+ * @date 2021-07-12 15:32:26
+ */
+public interface MenuQuery extends BaseQuery<Menu> {
+    List<Menu> getSortedMenusByStatus(Boolean isEnabled);
+    Map<Integer, String> getSortedChildGroupByPid(int pid);
+}

+ 54 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/db/query/MenuQueryImpl.java

@@ -0,0 +1,54 @@
+package cn.reghao.oss.web.account.db.query;
+
+import cn.reghao.oss.web.account.db.repository.MenuRepository;
+import cn.reghao.oss.web.account.model.po.Menu;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2021-07-12 15:32:26
+ */
+@Service
+public class MenuQueryImpl implements MenuQuery {
+    private final MenuRepository menuRepository;
+
+    public MenuQueryImpl(MenuRepository menuRepository) {
+        this.menuRepository = menuRepository;
+    }
+
+    @Override
+    public List<Menu> findAll() {
+        return menuRepository.findAll();
+    }
+
+    @Override
+    public Menu findById(int id) {
+        return menuRepository.getOne(id);
+    }
+
+    @Override
+    public List<Menu> getSortedMenusByStatus(Boolean isEnabled) {
+        List<Menu> menuList = menuRepository.findByEnabled(isEnabled);
+        Map<Integer, List<Menu>> map =  menuList.stream().collect(Collectors.groupingBy(Menu::getPid));
+        List<Menu> list = new ArrayList<>();
+        map.forEach((pid, menus) -> {
+            list.addAll(menus.stream()
+                    .sorted(Comparator.comparing(Menu::getPos))
+                    .collect(Collectors.toList()));
+        });
+
+        return list;
+    }
+
+    @Override
+    public Map<Integer, String> getSortedChildGroupByPid(int pid) {
+        List<Menu> menus = menuRepository.findByPid(pid);
+        menus.sort(Comparator.comparingInt(Menu::getPos));
+        Map<Integer, String> map = new HashMap<>();
+        menus.forEach(menu -> map.put(menu.getPos(), menu.getName()));
+        return map;
+    }
+}

+ 30 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/db/query/RoleQuery.java

@@ -0,0 +1,30 @@
+package cn.reghao.oss.web.account.db.query;
+
+import cn.reghao.oss.web.account.model.po.Role;
+import cn.reghao.oss.web.account.model.po.User;
+import cn.reghao.oss.web.account.model.vo.RoleVO;
+import cn.reghao.jutil.jdk.db.BaseQuery;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2021-07-12 15:32:26
+ */
+public interface RoleQuery extends BaseQuery<Role> {
+    Role findByName(String name);
+    Page<RoleVO> getRoleVOByPage(PageRequest pageRequest);
+    List<RoleVO> getByMatchName(String name);
+    RoleVO getRoleVOById(Integer id);
+    /**
+     * 获取拥有角色的用户
+     *
+     * @param
+     * @return
+     * @date 2021-07-14 下午2:57
+     */
+    List<User> getUsersByRoleId(Integer id);
+    List<User> getUsersByRole(Role role);
+}

+ 107 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/db/query/RoleQueryImpl.java

@@ -0,0 +1,107 @@
+package cn.reghao.oss.web.account.db.query;
+
+import cn.reghao.oss.web.account.db.repository.RoleRepository;
+import cn.reghao.oss.web.account.db.repository.UserRepository;
+import cn.reghao.oss.web.account.model.po.Role;
+import cn.reghao.oss.web.account.model.po.User;
+import cn.reghao.oss.web.account.model.vo.RoleVO;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+
+import javax.persistence.criteria.Predicate;
+import javax.persistence.criteria.SetJoin;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2021-07-12 15:32:26
+ */
+@Service
+public class RoleQueryImpl implements RoleQuery {
+    private final RoleRepository roleRepository;
+    private final UserRepository userRepository;
+
+    public RoleQueryImpl(RoleRepository roleRepository, UserRepository userRepository) {
+        this.roleRepository = roleRepository;
+        this.userRepository = userRepository;
+    }
+
+    @Override
+    public List<Role> findAll() {
+        return roleRepository.findAll();
+    }
+
+    @Override
+    public Role findById(int id) {
+        return roleRepository.findById(id).orElse(null);
+    }
+
+    @Override
+    public Role findByName(String name) {
+        return roleRepository.findByName(name);
+    }
+
+    @Override
+    public Page<RoleVO> getRoleVOByPage(PageRequest pageRequest) {
+        return roleRepository.findAll(pageRequest).map(RoleVO::new);
+    }
+
+    @Override
+    public List<RoleVO> getByMatchName(String name) {
+        Specification<Role> specification = (root, query, cb) -> {
+            String likeQuery = String.format("%%%s%%", name);
+            Predicate predicate = cb.like(root.get("name"), likeQuery);
+            return cb.and(predicate);
+        };
+        return roleRepository.findAll(specification).stream().map(RoleVO::new).collect(Collectors.toList());
+    }
+
+    @Override
+    public RoleVO getRoleVOById(Integer id) {
+        Role role = findById(id);
+        return role != null ? new RoleVO(role) : null;
+    }
+
+    /**
+     * 获取拥有角色的用户
+     *
+     * @param
+     * @return
+     * @date 2021-07-14 下午2:57
+     */
+    @Override
+    public List<User> getUsersByRoleId(Integer id) {
+        Role role = findById(id);
+        return getUsersByRole(role);
+    }
+
+    /**
+     * SQL 语句
+     * select u.*
+     * from role r
+     * inner join user_role ur
+     * inner join `user` u
+     * on r.title=ur.role and ur.user_id=u.id and r.title='ROLE_FRONTEND'
+     *
+     * @param
+     * @return
+     * @date 2023-03-02 17:54:48
+     */
+    @Override
+    public List<User> getUsersByRole(Role role) {
+        String name = role.getName();
+        Specification<User> specification = ((root, query, cb) -> {
+            SetJoin<User, String> setJoin = root.joinSet("role");
+            // User 中 Set 的元素是原始类型
+            Predicate predicate = setJoin.as(String.class).in(name);
+            // User 中的 Set 的元素是某个对象, fieldName 表示对象的某个字段名
+            //Predicate predicate = cb.equal(setJoin.get("fieldName"), title);
+            return cb.and(predicate);
+        });
+
+        return userRepository.findAll(specification);
+    }
+}

+ 28 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/db/query/UserQuery.java

@@ -0,0 +1,28 @@
+package cn.reghao.oss.web.account.db.query;
+
+import cn.reghao.oss.web.account.model.po.Role;
+import cn.reghao.oss.web.account.model.po.User;
+import cn.reghao.oss.web.account.model.vo.UserVO;
+import cn.reghao.jutil.jdk.db.BaseQuery;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+
+import java.util.*;
+
+/**
+ * @author reghao
+ * @date 2021-07-12 15:32:26
+ */
+public interface UserQuery extends BaseQuery<User> {
+    Page<UserVO> getUserVOByPage(PageRequest pageRequest);
+    List<UserVO> getByMatchScreenName(String screenName);
+    /**
+     * 获取用户拥有的角色
+     *
+     * @param
+     * @return
+     * @date 2021-09-10 下午11:12
+     */
+    Set<Role> getUserRoles(User user);
+    Set<Role> getUserRoles(Integer userId);
+}

+ 84 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/db/query/UserQueryImpl.java

@@ -0,0 +1,84 @@
+package cn.reghao.oss.web.account.db.query;
+
+import cn.reghao.oss.web.account.db.repository.RoleRepository;
+import cn.reghao.oss.web.account.db.repository.UserRepository;
+import cn.reghao.oss.web.account.model.po.Role;
+import cn.reghao.oss.web.account.model.po.User;
+import cn.reghao.oss.web.account.model.po.UserAuthority;
+import cn.reghao.oss.web.account.model.vo.UserVO;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+
+import javax.persistence.criteria.Predicate;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2021-07-12 15:32:26
+ */
+@Service
+public class UserQueryImpl implements UserQuery {
+    private final UserRepository userRepository;
+    private final RoleRepository roleRepository;
+
+    public UserQueryImpl(UserRepository userRepository, RoleRepository roleRepository) {
+        this.userRepository = userRepository;
+        this.roleRepository = roleRepository;
+    }
+
+    @Override
+    public User findById(int id) {
+        return userRepository.getOne(id);
+    }
+
+    @Override
+    public Page<UserVO> getUserVOByPage(PageRequest pageRequest) {
+        return userRepository.findAll(pageRequest).map(UserVO::new);
+    }
+
+    @Override
+    public List<UserVO> getByMatchScreenName(String screenName) {
+        Specification<User> specification = (root, query, cb) -> {
+            String likeQuery = String.format("%%%s%%", screenName);
+            Predicate predicate = cb.like(root.get("screenName"), likeQuery);
+            return cb.and(predicate);
+        };
+        return userRepository.findAll(specification).stream().map(UserVO::new).collect(Collectors.toList());
+    }
+
+    /**
+     * 获取用户拥有的角色
+     *
+     * @param
+     * @return
+     * @date 2021-09-10 下午11:12
+     */
+    @Override
+    public Set<Role> getUserRoles(User user) {
+        Set<UserAuthority> set = user.getAuthorities().stream()
+                .map(grantedAuthority -> new UserAuthority(grantedAuthority.getAuthority()))
+                .collect(Collectors.toSet());
+
+        List<String> roles = set.stream()
+                .map(UserAuthority::getAuthority)
+                .collect(Collectors.toList());
+        Specification<Role> spec = ((root, query, criteriaBuilder) -> root.get("name").in(roles));
+        return new HashSet<>(roleRepository.findAll(spec));
+    }
+
+    @Override
+    public Set<Role> getUserRoles(Integer userId) {
+        User user = findById(userId);
+        if (user == null) {
+            return Collections.emptySet();
+        }
+
+        return getUserRoles(user);
+    }
+}

+ 15 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/db/repository/DiskFileRepository.java

@@ -0,0 +1,15 @@
+package cn.reghao.oss.web.account.db.repository;
+
+import cn.reghao.oss.web.account.model.po.DiskFile;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2024-01-29 09:24:29
+ */
+public interface DiskFileRepository extends JpaRepository<DiskFile, Integer> {
+    DiskFile findByObjectName(String objectName);
+    List<DiskFile> findBySha256sum(String sha256sum);
+}

+ 15 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/db/repository/MenuRepository.java

@@ -0,0 +1,15 @@
+package cn.reghao.oss.web.account.db.repository;
+
+import cn.reghao.oss.web.account.model.po.Menu;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2019-08-29 10:52:14
+ */
+public interface MenuRepository extends JpaRepository<Menu, Integer> {
+    List<Menu> findByPid(int pid);
+    List<Menu> findByEnabled(boolean isEnabled);
+}

+ 13 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/db/repository/RoleRepository.java

@@ -0,0 +1,13 @@
+package cn.reghao.oss.web.account.db.repository;
+
+import cn.reghao.oss.web.account.model.po.Role;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+/**
+ * @author reghao
+ * @date 2019-08-29 10:52:14
+ */
+public interface RoleRepository extends JpaRepository<Role, Integer>, JpaSpecificationExecutor<Role> {
+    Role findByName(String name);
+}

+ 13 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/db/repository/UserRepository.java

@@ -0,0 +1,13 @@
+package cn.reghao.oss.web.account.db.repository;
+
+import cn.reghao.oss.web.account.model.po.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+/**
+ * @author reghao
+ * @date 2019-08-29 10:52:14
+ */
+public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecificationExecutor<User> {
+    User findByUsername(String username);
+}

+ 9 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/constant/DataStatus.java

@@ -0,0 +1,9 @@
+package cn.reghao.oss.web.account.model.constant;
+
+/**
+ * @author reghao
+ * @date 2021-05-17 13:50:14
+ */
+public enum DataStatus {
+    ENABLE, DISABLE
+}

+ 11 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/constant/MenuType.java

@@ -0,0 +1,11 @@
+package cn.reghao.oss.web.account.model.constant;
+
+/**
+ * 菜单类型
+ *
+ * @author reghao
+ * @date 2021-04-05 02:22:44
+ */
+public enum MenuType {
+    dir, page
+}

+ 25 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/constant/RoleType.java

@@ -0,0 +1,25 @@
+package cn.reghao.oss.web.account.model.constant;
+
+/**
+ * 角色类型
+ *
+ * @author reghao
+ * @date 2021-04-05 02:22:44
+ */
+public enum RoleType {
+    ROLE_ADMIN("管理员"),
+    ROLE_USER("普通用户"),
+    ROLE_BACKEND("后端"),
+    ROLE_FRONTEND("前端"),
+    ROLE_TEST("测试");
+
+    private final String desc;
+
+    RoleType(String desc) {
+        this.desc = desc;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+}

+ 34 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/constant/UserGender.java

@@ -0,0 +1,34 @@
+package cn.reghao.oss.web.account.model.constant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 用户性别
+ *
+ * @author reghao
+ * @date 2021-07-14 10:23:46
+ */
+public enum UserGender {
+    female(0, "女"), male(1, "男"), unknown(2, "未知");
+
+    private final Integer code;
+    private final String desc;
+
+    private static Map<Integer, String> descMap = new HashMap<>();
+    static {
+        for (UserGender gender : UserGender.values()) {
+            descMap.put(gender.code, gender.desc);
+        }
+    }
+
+    UserGender(Integer code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    // TODO 第一次调用时会初始化 descMap
+    public static String getDescByCode(int code) {
+        return descMap.get(code);
+    }
+}

+ 33 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/AccountLoginDto.java

@@ -0,0 +1,33 @@
+package cn.reghao.oss.web.account.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+/**
+ * 用户登录提交的数据
+ *
+ * @author reghao
+ * @date 2021-11-17 16:36:05
+ */
+@Setter
+@Getter
+public class AccountLoginDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @NotBlank
+    private String principal;
+    @NotBlank
+    private String credential;
+    @Length(min = 5, max = 5, message = "图形验证码应是 5 个字符")
+    private String captchaCode;
+    private Boolean rememberMe;
+
+    public AccountLoginDto() {
+        this.rememberMe = false;
+    }
+}

+ 21 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/AccountProfile.java

@@ -0,0 +1,21 @@
+package cn.reghao.oss.web.account.model.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2021-07-12 16:06:11
+ */
+@Data
+public class AccountProfile implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private Integer userId;
+    @NotBlank(message = "用户名不能为空白字符串")
+    private String screenName;
+    private String mobile;
+    private String email;
+    private Integer gender;
+}

+ 20 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/AccountRole.java

@@ -0,0 +1,20 @@
+package cn.reghao.oss.web.account.model.dto;
+
+import cn.reghao.oss.web.account.model.po.Role;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+import java.util.Set;
+
+/**
+ * @author reghao
+ * @date 2021-07-14 09:37:16
+ */
+@Data
+public class AccountRole implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private Integer userId;
+    @NotNull(message = "用户角色不能为 NULL")
+    private Set<Role> roles;
+}

+ 31 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/CreateAccountDto.java

@@ -0,0 +1,31 @@
+package cn.reghao.oss.web.account.model.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * @author reghao
+ * @date 2023-10-23 17:38:19
+ */
+@Setter
+@Getter
+public class CreateAccountDto {
+    @NotBlank(message = "必须指定登录名")
+    private String username;
+    @NotBlank(message = "必须指定用户名")
+    private String screenName;
+    @NotBlank(message = "必须指定登录密码")
+    private String password;
+    @NotNull(message = "必须选择用户角色")
+    private Integer roleId;
+
+    public CreateAccountDto(String username, String password, int roleId) {
+        this.username = username;
+        this.screenName = username;
+        this.password = password;
+        this.roleId = roleId;
+    }
+}

+ 44 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/MenuAddDTO.java

@@ -0,0 +1,44 @@
+package cn.reghao.oss.web.account.model.dto;
+
+import cn.reghao.oss.web.account.model.po.Menu;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2021-04-04 22:42:14
+ */
+@NoArgsConstructor
+@Data
+public class MenuAddDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    // Menu 类型
+    @NotBlank(message = "菜单类型不能为空白字符串")
+    private String type;
+    @NotBlank(message = "菜单名不能为空白字符串")
+    private String name;
+    @NotBlank(message = "URL 地址不能为空白字符串,目录类型使用可使用 # 字符")
+    private String url;
+    // 父级菜单 ID
+    @NotNull(message = "父级菜单不能为 NULL")
+    private Integer pid;
+    // 在同一个 pid 组内的位置,作为排序使用
+    private Integer pos;
+
+    public Menu to() {
+        Menu menu = new Menu();
+        menu.setType(this.getType());
+        menu.setName(this.getName());
+        menu.setUrl(this.getUrl());
+        menu.setIcon("layui-icon layui-icon-face-smile");
+        menu.setPid(this.getPid());
+        menu.setPos(this.getPos());
+        menu.setEnabled(true);
+        return menu;
+    }
+}

+ 46 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/MenuDTO.java

@@ -0,0 +1,46 @@
+package cn.reghao.oss.web.account.model.dto;
+
+import cn.reghao.oss.web.account.model.po.Menu;
+import cn.reghao.oss.web.account.model.po.Role;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+import java.util.Set;
+
+/**
+ * @author reghao
+ * @date 2021-07-15 14:06:13
+ */
+@NoArgsConstructor
+@Data
+public class MenuDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @NotNull(message = "菜单 ID 不能为 NULL")
+    private Integer menuId;
+    // 父级菜单 ID
+    @NotNull(message = "父级菜单不能为 NULL")
+    private Integer pid;
+    // 在同一个 pid 组内的位置,作为排序使用
+    private Integer pos;
+    @NotBlank(message = "菜单名不能为空白字符串")
+    private String name;
+    @NotBlank(message = "URL 地址不能为空白字符串,目录类型使用可使用 # 字符")
+    private String url;
+    private String icon;
+    @NotNull(message = "角色不能为 NULL")
+    @JsonIgnore
+    private Set<Role> roles;
+
+    public MenuDTO(Menu menu) {
+        this.menuId = menu.getId();
+        this.pid = menu.getPid();
+        this.pos = menu.getPos();
+        this.name = menu.getName();
+        this.url = menu.getUrl();
+        this.icon = menu.getIcon();
+    }
+}

+ 52 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/MenuUpdateDTO.java

@@ -0,0 +1,52 @@
+package cn.reghao.oss.web.account.model.dto;
+
+import cn.reghao.oss.web.account.model.po.Menu;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2021-07-15 14:06:13
+ */
+@NoArgsConstructor
+@Data
+public class MenuUpdateDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @NotNull(message = "菜单 ID 不能为 NULL")
+    private Integer menuId;
+    @NotBlank(message = "菜单名不能为空白字符串")
+    private String name;
+    @NotBlank(message = "URL 地址不能为空白字符串,目录类型使用可使用 # 字符")
+    private String url;
+    private String icon;
+    // 父级菜单 ID
+    @NotNull(message = "父级菜单不能为 NULL")
+    private Integer pid;
+    // 在同一个 pid 组内的位置,作为排序使用
+    private Integer pos;
+
+    public MenuUpdateDTO(Menu menu) {
+        this.menuId = menu.getId();
+        this.name = menu.getName();
+        this.url = menu.getUrl();
+        this.icon = menu.getIcon();
+        this.pid = menu.getPid();
+        this.pos = menu.getPos();
+    }
+
+    public Menu to() {
+        Menu menu = new Menu();
+        menu.setId(this.getMenuId());
+        menu.setName(this.getName());
+        menu.setUrl(this.getUrl());
+        menu.setIcon(this.getIcon());
+        menu.setPid(this.getPid());
+        menu.setPos(this.getPos());
+        return menu;
+    }
+}

+ 34 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/RoleDTO.java

@@ -0,0 +1,34 @@
+package cn.reghao.oss.web.account.model.dto;
+
+import cn.reghao.oss.web.account.model.po.Role;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.Pattern;
+
+/**
+ * @author reghao
+ * @date 2021-07-14 14:14:07
+ */
+@NoArgsConstructor
+@Data
+public class RoleDTO {
+    private Integer roleId;
+    @Pattern(regexp = "^\\w+$", message = "角色只能是数字、英文字符和下划线")
+    private String name;
+    @Length(max = 100, message = "描述的长度不超过 100 个中文字符")
+    private String description;
+    private String createTime;
+
+    public Role to() {
+        Role role = new Role();
+        if (this.getRoleId() != null) {
+            role.setId(this.getRoleId());
+        }
+
+        role.setName("ROLE_" + this.getName());
+        role.setDescription(this.getDescription());
+        return role;
+    }
+}

+ 19 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/RsaPubkey.java

@@ -0,0 +1,19 @@
+package cn.reghao.oss.web.account.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * @author reghao
+ * @date 2024-01-29 13:49:33
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Setter
+@Getter
+public class RsaPubkey {
+    private String pubkey;
+    private String r;
+}

+ 19 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/UpdatePasswordDto.java

@@ -0,0 +1,19 @@
+package cn.reghao.oss.web.account.model.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2023-02-22 11:04:24
+ */
+@Setter
+@Getter
+public class UpdatePasswordDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String original;
+    private String password;
+}

+ 24 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/dto/UserUpdateDTO.java

@@ -0,0 +1,24 @@
+package cn.reghao.oss.web.account.model.dto;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2021-07-12 16:06:11
+ */
+@Data
+public class UserUpdateDTO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @NotNull(message = "用户 ID 不能为 NULL")
+    private Integer userId;
+    @NotBlank(message = "用户名不能为空白字符串")
+    private String screenName;
+    // TODO 验证邮箱和手机号码是否有效
+    private String mobile;
+    private String email;
+}

+ 57 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/po/DiskFile.java

@@ -0,0 +1,57 @@
+package cn.reghao.oss.web.account.model.po;
+
+import cn.reghao.oss.web.util.db.BaseEntity;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+
+/**
+ * @author reghao
+ * @date 2023-11-11 00:06:21
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@Setter
+@Getter
+@Entity
+public class DiskFile extends BaseEntity {
+    @Column(nullable = false, unique = true)
+    private String objectName;
+    @Column(nullable = false, unique = true)
+    private String objectId;
+    @Column(nullable = false)
+    private String absolutePath;
+    @Column(nullable = false)
+    private String sha256sum;
+    @Column(nullable = false)
+    private String filename;
+    @Column(nullable = false)
+    private String contentType;
+    @Column(nullable = false)
+    private Long size;
+    private Long owner;
+
+    public DiskFile(String objectName, String objectId, DiskFile diskFile, String filename) {
+        this.objectName = objectName;
+        this.objectId = objectId;
+        this.absolutePath = diskFile.getAbsolutePath();
+        this.sha256sum = diskFile.getSha256sum();
+        this.filename = filename;
+        this.contentType = diskFile.getContentType();
+        this.size = diskFile.getSize();
+    }
+
+    public DiskFile(String objectName, String objectId, String absolutePath, String sha256sum, String filename, String contentType, long size) {
+        this.objectName = objectName;
+        this.objectId = objectId;
+        this.absolutePath = absolutePath;
+        this.sha256sum = sha256sum;
+        this.filename = filename;
+        this.contentType = contentType;
+        this.size = size;
+    }
+}

+ 52 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/po/Menu.java

@@ -0,0 +1,52 @@
+package cn.reghao.oss.web.account.model.po;
+
+import cn.reghao.oss.web.util.db.BaseEntity;
+import lombok.*;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.util.*;
+
+/**
+ * Layui 菜单, 作为 RBAC 中的 resource
+ *
+ * @author reghao
+ * @date 2021-04-04 22:42:14
+ */
+@NoArgsConstructor
+@Setter
+@Getter
+@Table(name = "sys_menu")
+@Entity
+public class Menu extends BaseEntity {
+    // Menu 类型
+    @NotBlank(message = "菜单类型不能为空白字符串")
+    private String type;
+    @NotBlank(message = "菜单名不能为空白字符串")
+    private String name;
+    @NotBlank(message = "URL 地址不能为空白字符串,目录类型使用可使用 # 字符")
+    private String url;
+    @NotNull(message = "菜单图标不能为 NULL")
+    private String icon;
+    // 父级菜单 ID
+    @NotNull(message = "父级菜单不能为 NULL")
+    private Integer pid;
+    // 在同一个 pid 组内的位置,作为排序使用
+    @NotNull
+    private Integer pos;
+    private Boolean enabled;
+    // Menu 拥有的所有子 Menu(按排序顺序, 不持久化)
+    @Transient
+    private Map<Integer, Menu> children;
+    @ManyToMany(mappedBy = "menus")
+    @NotNull(message = "角色不能为 NULL")
+    private Set<Role> roles;
+    @Transient
+    private String remark;
+
+    public Menu(int id, String name) {
+        this.id = id;
+        this.name = name;
+    }
+}

+ 43 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/po/Role.java

@@ -0,0 +1,43 @@
+package cn.reghao.oss.web.account.model.po;
+
+import cn.reghao.oss.web.util.db.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import org.hibernate.validator.constraints.Length;
+
+import javax.persistence.*;
+import javax.validation.constraints.Pattern;
+import java.util.Set;
+
+/**
+ * @author reghao
+ * @date 2019/03/14 21:46:13
+ */
+@NoArgsConstructor
+@Data
+@EqualsAndHashCode(callSuper = false, exclude = {"description", "menus"})
+@ToString(exclude = {"menus"})
+@Table(name = "sys_role")
+@Entity
+public class Role extends BaseEntity {
+    // TODO 匹配小写英文字符报错
+    // @Pattern(regexp = "^[a-z]+$", message = "角色只能是英文字符")
+    @Pattern(regexp = "^\\w+$", message = "角色只能是英文字符")
+    @Column(unique = true, nullable = false)
+    private String name;
+    @Length(max = 100, message = "对角色描述的长度不超过 100 个中文字符")
+    private String description;
+    // Role 端维护 Role 和 Menu 之间的关系
+    @ManyToMany
+    @JoinTable(name = "sys_role_menu",
+            joinColumns = @JoinColumn(name = "role_id"),
+            inverseJoinColumns = @JoinColumn(name = "menu_id"))
+    private Set<Menu> menus;
+
+    public Role(String name, String description) {
+        this.name = name;
+        this.description = description;
+    }
+}

+ 135 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/po/User.java

@@ -0,0 +1,135 @@
+package cn.reghao.oss.web.account.model.po;
+
+import cn.reghao.oss.web.util.db.BaseEntity;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.*;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotBlank;
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 用户
+ *
+ * @author reghao
+ * @date 2019/03/14 19:12:48
+ */
+@NoArgsConstructor
+@Getter
+@Setter
+@Table(name = "sys_user")
+@Entity
+public class User extends BaseEntity implements UserDetails {
+    // 用户名和密码
+    @Column(nullable = false, unique = true)
+    @NotBlank(message = "用户名不能为空白字符串")
+    private String username;
+    @Column(nullable = false)
+    @JsonIgnore
+    @NotBlank(message = "密码不能为空白字符串")
+    private String encodedPassword;
+    @Column(nullable = false)
+    private String salt;
+    private LocalDateTime createAt;
+
+    @ElementCollection(fetch = FetchType.EAGER)
+    @CollectionTable(name = "sys_user_role")
+    private Set<String> role;
+    private Boolean enabled = true;
+    private Boolean locked = false;
+
+    @NotBlank(message = "用户名不能为空白字符串")
+    private String screenName;
+    private String avatarUrl;
+    private String mobile;
+    private String email;
+    private Integer gender;
+
+    public User(String username, String encodedPassword, String salt, Set<String> roles) {
+        this.username = username;
+        this.encodedPassword = encodedPassword;
+        this.salt = salt;
+        this.createAt = LocalDateTime.now();
+        this.role = roles;
+        this.enabled = true;
+        this.locked = false;
+        this.screenName = username;
+        this.gender = 3;
+        this.avatarUrl = "/imgs/avatar.jpg";
+    }
+
+    @Override
+    public String getUsername() {
+        return username;
+    }
+
+    @Override
+    public String getPassword() {
+        return encodedPassword;
+    }
+
+    /**
+     * 帐号是否未过期
+     *
+     * @param
+     * @return
+     * @date 2019-05-29 下午2:05
+     */
+    @Override
+    public boolean isAccountNonExpired() {
+        return true;
+    }
+
+    /**
+     * 密码是否未过期
+     *
+     * @param
+     * @return
+     * @date 2019-05-29 下午2:05
+     */
+    @Override
+    public boolean isCredentialsNonExpired() {
+        return true;
+    }
+
+    /**
+     * 帐号是否可用
+     *
+     * @param
+     * @return
+     * @date 2019-05-29 下午2:05
+     */
+    @Override
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    /**
+     * 帐号是否锁定
+     *
+     * @param
+     * @return
+     * @date 2019-05-29 下午2:05
+     */
+    @Override
+    public boolean isAccountNonLocked() {
+        return locked;
+    }
+
+    /**
+     * 权限列表
+     *
+     * @param
+     * @return
+     * @date 2019-05-29 下午2:22
+     */
+    @Override
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        return role.stream().map(UserAuthority::new).collect(Collectors.toSet());
+    }
+}

+ 49 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/po/UserAuthority.java

@@ -0,0 +1,49 @@
+package cn.reghao.oss.web.account.model.po;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.util.Assert;
+
+/**
+ * SimpleGrantedAuthority 的重写, 仅用于 SpringSecurity
+ *
+ * @author reghao
+ * @date 2020-06-24 14:44:25
+ */
+public class UserAuthority implements GrantedAuthority {
+    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+    private final String role;
+
+    public UserAuthority(String role) {
+        Assert.hasText(role, "A granted authority textual representation is required");
+        this.role = role;
+    }
+
+    @Override
+    public String getAuthority() {
+        return role;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj instanceof UserAuthority) {
+            return role.equals(((UserAuthority) obj).role);
+        }
+
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return this.role.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return this.role;
+    }
+}

+ 26 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/vo/MenuVO.java

@@ -0,0 +1,26 @@
+package cn.reghao.oss.web.account.model.vo;
+
+import cn.reghao.oss.web.account.model.po.Menu;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2021-07-15 14:06:13
+ */
+@Data
+public class MenuVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private Integer menuId;
+    private String name;
+    private String url;
+    private String type;
+
+    public MenuVO(Menu menu) {
+        this.menuId = menu.getId();
+        this.name = menu.getName();
+        this.url = menu.getUrl();
+        this.type = menu.getType();
+    }
+}

+ 28 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/vo/RoleVO.java

@@ -0,0 +1,28 @@
+package cn.reghao.oss.web.account.model.vo;
+
+import cn.reghao.oss.web.account.model.po.Role;
+import cn.reghao.jutil.jdk.converter.DateTimeConverter;
+import lombok.Data;
+
+import java.util.Locale;
+
+/**
+ * @author reghao
+ * @date 2021-07-14 14:14:07
+ */
+@Data
+public class RoleVO {
+    private Integer roleId;
+    private String name;
+    private String description;
+    private String createTime;
+    private String updateTime;
+
+    public RoleVO(Role role) {
+        this.roleId = role.getId();
+        this.name = role.getName().split("ROLE_")[1].toLowerCase(Locale.ROOT);
+        this.description = role.getDescription();
+        this.createTime = DateTimeConverter.format(role.getCreateTime());
+        this.updateTime = DateTimeConverter.format(role.getUpdateTime());
+    }
+}

+ 35 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/model/vo/UserVO.java

@@ -0,0 +1,35 @@
+package cn.reghao.oss.web.account.model.vo;
+
+import cn.reghao.oss.web.account.model.constant.UserGender;
+import cn.reghao.oss.web.account.model.po.User;
+import cn.reghao.jutil.jdk.converter.DateTimeConverter;
+import lombok.Data;
+
+/**
+ * @author reghao
+ * @date 2021-07-14 10:28:47
+ */
+@Data
+public class UserVO {
+    private Integer userId;
+    private String username;
+    private String screenName;
+    private String gender;
+    private String mobile;
+    private String email;
+    private String createTime;
+    private String lastAccess;
+    private String status;
+
+    public UserVO(User user) {
+        this.userId = user.getId();
+        this.username = user.getUsername();
+        this.screenName = user.getScreenName();
+        this.gender = UserGender.getDescByCode(user.getGender());
+        this.mobile = user.getMobile();
+        this.email = user.getEmail();
+        this.createTime = DateTimeConverter.format(user.getCreateTime());
+        this.status = user.getEnabled() ? "启用" : "禁用";
+        this.lastAccess = "N/A";
+    }
+}

+ 28 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/security/ExceptionAuthenticationEntryPoint.java

@@ -0,0 +1,28 @@
+package cn.reghao.oss.web.account.security;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 在 WebSecurityConfig 中配置的需要认证的接口没有获取到请求中的认证信息时会转到此处进行处理
+ *
+ * @author reghao
+ * @date 2023-07-28 16:43:59
+ */
+@Slf4j
+public class ExceptionAuthenticationEntryPoint implements AuthenticationEntryPoint {
+    @Override
+    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
+            throws IOException, ServletException {
+        String uri = request.getRequestURI();
+        String errMsg = exception.getMessage();
+        log.error("请求 {} 接口认证失败: {}, 重定向到登录页面", uri, errMsg);
+        response.sendRedirect("/login");
+    }
+}

+ 203 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/security/WebSecurityConfig.java

@@ -0,0 +1,203 @@
+package cn.reghao.oss.web.account.security;
+
+import cn.reghao.oss.web.account.security.filter.LoginRedirectFilter;
+import cn.reghao.oss.web.account.security.form.AccountAuthFilter;
+import cn.reghao.oss.web.account.security.form.AccountAuthProvider;
+import cn.reghao.oss.web.account.service.AccountAuthService;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
+import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.session.SessionRegistry;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import org.springframework.security.web.authentication.session.*;
+import org.springframework.security.web.context.SecurityContextPersistenceFilter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Web 应用安全配置
+ *
+ * @author reghao
+ * @date 2019-05-20 10:41:10
+ */
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true) // 调用方法时检查权限
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+    private final String loginPage = "/login";
+    private final String loginApi = "/login";
+    private final String logoutApi = "/logout";
+
+    private final AccountAuthProvider userAuthProvider;
+    private final AccountAuthService accountAuthService;
+    private final AuthenticationSuccessHandler successHandler;
+    private final AuthenticationFailureHandler failureHandler;
+    private final LogoutHandler logoutHandler;
+    private final LogoutSuccessHandler logoutSuccessHandler;
+    private final SessionRegistry sessionRegistry;
+
+    public WebSecurityConfig(AccountAuthProvider userAuthProvider, AccountAuthService accountAuthService,
+                             AuthenticationSuccessHandler successHandler, AuthenticationFailureHandler failureHandler,
+                             LogoutHandler logoutHandler, LogoutSuccessHandler logoutSuccessHandler,
+                             SessionRegistry sessionRegistry) {
+        this.userAuthProvider = userAuthProvider;
+        this.accountAuthService = accountAuthService;
+        this.successHandler = successHandler;
+        this.failureHandler = failureHandler;
+        this.logoutHandler = logoutHandler;
+        this.logoutSuccessHandler = logoutSuccessHandler;
+        this.sessionRegistry = sessionRegistry;
+    }
+
+    /**
+     * 放行静态资源
+     *
+     * @param
+     * @return
+     * @date 2021-04-04 下午9:06
+     */
+    @Override
+    public void configure(WebSecurity web) {
+        web.ignoring()
+                .antMatchers("/ws/**")
+                .antMatchers("/css/**")
+                .antMatchers("/imgs/**")
+                .antMatchers("/js/**")
+                .antMatchers("/lib/**");
+    }
+
+    @Override
+    protected void configure(HttpSecurity http) throws Exception {
+        http.authorizeRequests()
+                .antMatchers("/login").permitAll()
+                // 放行匹配前缀的请求
+                .antMatchers("/api/account/code/**").permitAll()
+                .antMatchers("/api/oss/**").permitAll()
+                .antMatchers("/api/video/**").permitAll()
+                .antMatchers("/api/audio/**").permitAll()
+                .antMatchers("/api/image/**").permitAll()
+                .anyRequest().authenticated();
+
+        // 配置 FilterChainProxy 过滤器链
+        http.addFilterAfter(new LoginRedirectFilter(), SecurityContextPersistenceFilter.class);
+        http.addFilterBefore(accountAuthFilter(sessionRegistry), UsernamePasswordAuthenticationFilter.class);
+
+        // 禁用 UsernamePasswordAuthenticationFilter, 使用自定义的 AccountAuthFilter
+        http.formLogin().disable();
+
+        // 配置 LogoutFilter
+        http.logout()
+                .logoutUrl(logoutApi)
+                .addLogoutHandler(logoutHandler)
+                .logoutSuccessHandler(logoutSuccessHandler);
+
+        // 配置 ExceptionTranslationFilter, 登录认证接口失败时的处理, 不会重定向到 loginPage
+        http.exceptionHandling()
+                .authenticationEntryPoint(new ExceptionAuthenticationEntryPoint());
+
+        // 配置 SessionManagementFilter 和 ConcurrentSessionFilter
+        http.sessionManagement()
+                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
+                //.sessionAuthenticationStrategy(new MySessionAuthenticationStrategy(sessionRegistry()));
+                .maximumSessions(1)
+                .expiredUrl(loginPage);
+
+        // 配置 RememberMeAuthenticationFilter, 禁用 RememberMeAuthenticationFilter
+        http.rememberMe().disable();
+                /*.key("DExNzAyNTQ2Nzo3NDI3MTNhYmM5MGE5")
+                .rememberMeParameter("rememberMe");*/
+
+        // 配置 CorsFilter, 禁用 CorsFilter
+        http.cors().disable();
+
+        // 配置 CsrfFilter, 禁用 CsrfFilter
+        http.csrf().disable();
+
+        // 配置 HeaderWriterFilter, 禁用 HeaderWriterFilter
+        http.headers().disable();
+    }
+
+    /**
+     * 配置认证管理器
+     *
+     * @param
+     * @return
+     * @date 2021-07-25 下午2:28
+     */
+    @Bean
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception {
+        return super.authenticationManagerBean();
+    }
+
+    /**
+     * 配置认证使用的 provider
+     *
+     * @param
+     * @return
+     * @date 2021-07-25 下午2:31
+     */
+    @Override
+    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+        auth.authenticationProvider(userAuthProvider);
+    }
+
+    /**
+     * 配置账号密码登录 filter
+     *
+     * @param
+     * @return
+     * @date 2022-07-06 上午9:54
+     */
+    @Bean
+    public AccountAuthFilter accountAuthFilter(SessionRegistry sessionRegistry) throws Exception {
+        AccountAuthFilter filter = new AccountAuthFilter(loginApi, "POST", accountAuthService);
+        filter.setAuthenticationManager(super.authenticationManager());
+        filter.setAuthenticationSuccessHandler(successHandler);
+        filter.setAuthenticationFailureHandler(failureHandler);
+        filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy(sessionRegistry));
+        return filter;
+    }
+
+    public CompositeSessionAuthenticationStrategy sessionAuthenticationStrategy(SessionRegistry sessionRegistry){
+        ConcurrentSessionControlAuthenticationStrategy controlAuthenticationStrategy =
+                new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
+        // 设置帐号同时登录的最大数量
+        controlAuthenticationStrategy.setMaximumSessions(1);
+        //controlAuthenticationStrategy.setExceptionIfMaximumExceeded(true);
+
+        List<SessionAuthenticationStrategy> authenticationStrategies = new ArrayList<>();
+        authenticationStrategies.add(controlAuthenticationStrategy);
+        authenticationStrategies.add(new SessionFixationProtectionStrategy());
+        authenticationStrategies.add(new RegisterSessionAuthenticationStrategy(sessionRegistry));
+
+        return new CompositeSessionAuthenticationStrategy(authenticationStrategies);
+    }
+
+    /**
+     * 角色继承
+     * ADMIN 可以访问 USER 的权限,反之不可
+     *
+     * @date 2019-07-05 上午11:18
+     */
+    @Bean
+    public RoleHierarchy roleHierarchy() {
+        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
+        roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
+        return roleHierarchy;
+    }
+}

+ 36 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/security/encoder/Md5PasswordEncoder.java

@@ -0,0 +1,36 @@
+package cn.reghao.oss.web.account.security.encoder;
+
+import cn.reghao.jutil.jdk.security.Md5Cryptor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Component;
+
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * @author reghao
+ * @date 2019-03-26 14:46:57
+ */
+@Component
+public class Md5PasswordEncoder implements PasswordEncoder {
+    private final Md5Cryptor md5Cryptor;
+
+    public Md5PasswordEncoder() throws NoSuchAlgorithmException {
+        this.md5Cryptor = new Md5Cryptor();
+    }
+
+    @Override
+    public String encode(CharSequence rawPassword) {
+        // rawPassword 带盐值
+        return md5Cryptor.encrypt(rawPassword.toString());
+    }
+
+    private byte[] encode(CharSequence rawPassword, byte[] salt) {
+        return null;
+    }
+
+    @Override
+    public boolean matches(CharSequence rawPassword, String encodedPassword) {
+        String password = md5Cryptor.encrypt(rawPassword.toString());
+        return encodedPassword.equals(password);
+    }
+}

+ 13 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/security/exceptioin/AccountLoginException.java

@@ -0,0 +1,13 @@
+package cn.reghao.oss.web.account.security.exceptioin;
+
+import org.springframework.security.core.AuthenticationException;
+
+/**
+ * @author reghao
+ * @date 2020-10-09 22:26:09
+ */
+public class AccountLoginException extends AuthenticationException {
+    public AccountLoginException(String msg) {
+        super(msg);
+    }
+}

+ 33 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/security/filter/LoginRedirectFilter.java

@@ -0,0 +1,33 @@
+package cn.reghao.oss.web.account.security.filter;
+
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 已登录状态下,访问 /login 页面自动跳转到 /
+ *
+ * @author reghao
+ * @date 2019-06-04 23:06:50
+ */
+public class LoginRedirectFilter implements Filter {
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+        SecurityContext securityContext = SecurityContextHolder.getContext();
+        String url = httpRequest.getRequestURI().substring(httpRequest.getContextPath().length());
+        if (securityContext != null && securityContext.getAuthentication() != null && "/login".equals(url)) {
+            // 将登录页面重定向到主页
+            httpResponse.sendRedirect("/");
+        } else {
+            chain.doFilter(request, response);
+        }
+    }
+}

+ 48 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/security/form/AccountAuthFilter.java

@@ -0,0 +1,48 @@
+package cn.reghao.oss.web.account.security.form;
+
+import cn.reghao.oss.web.account.model.dto.AccountLoginDto;
+import cn.reghao.oss.web.account.service.AccountAuthService;
+import cn.reghao.jutil.web.ServletUtil;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 账号密码登录 filter
+ * 替换 UsernamePasswordAuthenticationFilter,用于认证用户,匹配指定 URL 才会进行处理
+ *
+ * @author reghao
+ * @date 2022-07-06 15:55:59
+ */
+public class AccountAuthFilter extends AbstractAuthenticationProcessingFilter {
+    private final AccountAuthService accountAuthService;
+
+    public AccountAuthFilter(String authUrl, String httpMethod, AccountAuthService accountAuthService) {
+        super(new AntPathRequestMatcher(authUrl, httpMethod));
+        this.accountAuthService = accountAuthService;
+    }
+
+    /**
+     * 构造 Authentication 对象
+     * 参照 UsernamePasswordAuthenticationFilter
+     *
+     * @param
+     * @return
+     * @date 2020-05-06 上午11:16
+     */
+    @Override
+    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
+            throws AuthenticationException, IOException, ServletException {
+        // json payload 中的 username 和 password 参数
+        AccountLoginDto accountLoginDto = (AccountLoginDto) ServletUtil.getBody(request, AccountLoginDto.class);
+        AccountAuthToken preAuthToken = accountAuthService.getPreAuthentication(accountLoginDto);
+        // 调用 UserAuthProvider.authenticate()
+        return this.getAuthenticationManager().authenticate(preAuthToken);
+    }
+}

+ 42 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/security/form/AccountAuthProvider.java

@@ -0,0 +1,42 @@
+package cn.reghao.oss.web.account.security.form;
+
+import cn.reghao.oss.web.account.service.AccountAuthService;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.stereotype.Component;
+
+/**
+ * 认证用户名/密码登录的 provider
+ *
+ * @author reghao
+ * @date 2019-04-09 09:07:40
+ */
+@Component
+public class AccountAuthProvider implements AuthenticationProvider {
+    private final AccountAuthService accountAuthService;
+
+    public AccountAuthProvider(AccountAuthService accountAuthService) {
+        this.accountAuthService = accountAuthService;
+    }
+
+    /**
+     * 对 Authentication 对象进行认证
+     * 参照 DaoAuthenticationProvider
+     *
+     * @param
+     * @return
+     * @date 2020-05-06 上午11:16
+     */
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+        AccountAuthToken preAuthToken = (AccountAuthToken) authentication;
+        AccountAuthToken authToken = accountAuthService.authByPassword(preAuthToken);
+        return authToken;
+    }
+
+    @Override
+    public boolean supports(Class<?> authentication) {
+        return (AccountAuthToken.class.isAssignableFrom(authentication));
+    }
+}

+ 126 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/security/form/AccountAuthToken.java

@@ -0,0 +1,126 @@
+package cn.reghao.oss.web.account.security.form;
+
+import cn.reghao.oss.web.account.model.po.User;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.SpringSecurityCoreVersion;
+
+import java.util.List;
+
+/**
+ * 参考 org.springframework.security.authentication.UsernamePasswordAuthenticationToken 实现
+ *
+ * @author reghao
+ * @date 2022-07-06 15:59:02
+ */
+public class AccountAuthToken extends AbstractAuthenticationToken {
+    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+
+    // ~ Instance fields
+    // ================================================================================================
+    private final String loginId;
+    private final boolean rememberMe;
+    private final Integer userId;
+    private final Object principal;
+    private Object credentials;
+
+    // ~ Constructors
+    // ===================================================================================================
+
+    /**
+     * 等待认证时创建的 Authentication 对象
+     *
+     * This constructor can be safely used by any code that wishes to create a
+     * <code>PasswordAuthToken</code>, as the {@link #isAuthenticated()}
+     * will return <code>false</code>.
+     *
+     */
+    public AccountAuthToken(String loginId, Boolean rememberMe, Object principal, Object credentials) {
+        super(null);
+        super.setAuthenticated(false);
+        this.loginId = loginId;
+        this.rememberMe = rememberMe;
+        this.userId = null;
+        this.principal = principal;
+        this.credentials = credentials;
+    }
+
+    /**
+     * 认证通过时创建的 Authentication 对象
+     *
+     * This constructor should only be used by <code>AuthenticationManager</code> or
+     * <code>AuthenticationProvider</code> implementations that are satisfied with
+     * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
+     * authentication token.
+     *
+     * @param authToken
+     * @param user
+     */
+    public AccountAuthToken(AccountAuthToken authToken, User user) {
+        super(user.getAuthorities());
+        super.setAuthenticated(true); // must use super, as we override
+        super.setDetails(user);
+        this.loginId = authToken.getLoginId();
+        this.rememberMe = authToken.isRememberMe();
+        this.userId = user.getId();
+        this.principal = authToken.getPrincipal();
+        this.credentials = authToken.getCredentials();
+    }
+
+    /**
+     * 从 AccessToken 中恢复的已认证 Authentication 对象
+     *
+     * @param
+     * @return
+     * @date 2023-02-17 17:42:27
+     */
+    public AccountAuthToken(int plat, String loginId, int loginType, Object principal, List<GrantedAuthority> authorities) {
+        super(authorities);
+        super.setAuthenticated(true);
+        this.loginId = loginId;
+        this.rememberMe = false;
+        this.userId = Integer.parseInt((String) principal);
+        this.principal = principal;
+        this.credentials = null;
+    }
+
+    // ~ Methods
+    // ========================================================================================================
+    public String getLoginId() {
+        return loginId;
+    }
+
+    public boolean isRememberMe() {
+        return rememberMe;
+    }
+
+    public Integer getUserId() {
+        return this.userId;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return this.credentials;
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return this.principal;
+    }
+
+    /*@Override
+    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
+        if (isAuthenticated) {
+            throw new IllegalArgumentException(
+                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
+        }
+
+        super.setAuthenticated(false);
+    }*/
+
+    @Override
+    public void eraseCredentials() {
+        super.eraseCredentials();
+        credentials = null;
+    }
+}

+ 36 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/security/handler/AuthFailHandlerImpl.java

@@ -0,0 +1,36 @@
+package cn.reghao.oss.web.account.security.handler;
+
+import cn.reghao.jutil.jdk.result.WebResult;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * 认证失败后的处理
+ *
+ * @author reghao
+ * @date 2019-04-09 00:02:34
+ */
+@Component
+public class AuthFailHandlerImpl implements AuthenticationFailureHandler {
+    @Override
+    public void onAuthenticationFailure(HttpServletRequest request,
+                                        HttpServletResponse response,
+                                        AuthenticationException exception) throws IOException, ServletException {
+        String errMsg = exception.getMessage();
+        String body = WebResult.failWithMsg(errMsg);
+        writeResponse(response, body);
+    }
+
+    private void writeResponse(HttpServletResponse response, String body) throws IOException {
+        response.setContentType("application/json; charset=utf-8");
+        PrintWriter printWriter = response.getWriter();
+        printWriter.write(body);
+    }
+}

+ 55 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/security/handler/AuthSuccessHandlerImpl.java

@@ -0,0 +1,55 @@
+package cn.reghao.oss.web.account.security.handler;
+
+import cn.reghao.jutil.jdk.result.WebResult;
+import cn.reghao.jutil.web.ServletUtil;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.savedrequest.SavedRequest;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * 认证成功后跳转到登录前的地址
+ *
+ * @author reghao
+ * @date 2019-04-08 23:50:51
+ */
+@Component
+public class AuthSuccessHandlerImpl implements AuthenticationSuccessHandler {
+    @Override
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth)
+            throws IOException {
+        String redirectPath = getRedirectPath();
+        String body = WebResult.success(redirectPath);
+        writeResponse(response, body);
+    }
+
+    /**
+     * 认证成功后, 重定向到登录前的地址, 需要 session 的支持
+     *
+     * @param
+     * @return
+     * @date 2023-08-16 10:58:41
+     */
+    private String getRedirectPath() {
+        String redirectPath = "/";
+        // 获取 spring security 在 session 中存放的变量
+        SavedRequest savedRequest =
+                (SavedRequest) ServletUtil.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST");
+        if (savedRequest != null) {
+            redirectPath = savedRequest.getRedirectUrl();
+        }
+
+        return redirectPath;
+    }
+
+    private void writeResponse(HttpServletResponse response, String body) throws IOException {
+        response.setContentType("application/json; charset=utf-8");
+        PrintWriter printWriter = response.getWriter();
+        printWriter.write(body);
+    }
+}

+ 27 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/security/handler/LogoutHandlerImpl.java

@@ -0,0 +1,27 @@
+package cn.reghao.oss.web.account.security.handler;
+
+import cn.reghao.oss.web.account.service.AccountAuthService;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * @author reghao
+ * @date 2023-08-03 14:32:07
+ */
+@Component
+public class LogoutHandlerImpl implements LogoutHandler {
+    private final AccountAuthService accountAuthService;
+
+    public LogoutHandlerImpl(AccountAuthService accountAuthService) {
+        this.accountAuthService = accountAuthService;
+    }
+
+    @Override
+    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) {
+        accountAuthService.logout();
+    }
+}

+ 27 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/security/handler/LogoutSuccessHandlerImpl.java

@@ -0,0 +1,27 @@
+package cn.reghao.oss.web.account.security.handler;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 账号注销成功后的处理, 使用此类后不再调用 logoutSuccessUrl, 在此类中直接返回给客户端
+ * 账号注销的调用顺序 LogoutHandlerImpl -> LogoutSuccessHandlerImpl
+ *
+ * @author reghao
+ * @date 2023-02-14 16:11:48
+ */
+@Component
+public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
+    @Override
+    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth)
+            throws IOException, ServletException {
+        String redirectPath = "/login";
+        response.sendRedirect(redirectPath);
+    }
+}

+ 34 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/security/session/SecuritySessionConfig.java

@@ -0,0 +1,34 @@
+package cn.reghao.oss.web.account.security.session;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.core.session.SessionRegistry;
+import org.springframework.security.core.session.SessionRegistryImpl;
+import org.springframework.security.web.session.ConcurrentSessionFilter;
+import org.springframework.security.web.session.HttpSessionEventPublisher;
+
+/**
+ * spring-security 中涉及 session 处理的相关配置
+ *
+ * @author reghao
+ * @date 2024-02-02 17:18:12
+ */
+@Configuration
+public class SecuritySessionConfig {
+    @Bean
+    public SessionRegistry sessionRegistry() {
+        return new SessionRegistryImpl();
+    }
+
+    @Bean
+    public ConcurrentSessionFilter concurrentSessionFilter(SessionRegistry sessionRegistry,
+                                                           SessionExpiredStrategy sessionExpiredStrategy){
+        // 帐号同时登录数量超过限制时的处理类
+        return new ConcurrentSessionFilter(sessionRegistry, sessionExpiredStrategy);
+    }
+
+    @Bean
+    public HttpSessionEventPublisher httpSessionEventPublisher() {
+        return new HttpSessionEventPublisher();
+    }
+}

+ 33 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/security/session/SessionExpiredStrategy.java

@@ -0,0 +1,33 @@
+package cn.reghao.oss.web.account.security.session;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.web.session.SessionInformationExpiredEvent;
+import org.springframework.security.web.session.SessionInformationExpiredStrategy;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * ConcurrentSessionFilter 中检测到过期 session 时的处理策略
+ *
+ * @author reghao
+ * @date 2024-02-02 16:32:43
+ */
+@Slf4j
+@Component
+public class SessionExpiredStrategy implements SessionInformationExpiredStrategy {
+    @Override
+    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
+        String principal = (String) event.getSessionInformation().getPrincipal();
+        log.info("{} 已在别处登录!", principal);
+
+        HttpServletResponse response = event.getResponse();
+        /*response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        response.setContentType("application/json;charset=utf-8");
+        response.getWriter().write("您的账号已经在别的地方登录!");*/
+        String redirectPath = "/login";
+        response.sendRedirect(redirectPath);
+    }
+}

+ 19 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/AccountAuthService.java

@@ -0,0 +1,19 @@
+package cn.reghao.oss.web.account.service;
+
+import cn.reghao.oss.web.account.model.dto.AccountLoginDto;
+import cn.reghao.oss.web.account.security.form.AccountAuthToken;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.validation.annotation.Validated;
+
+/**
+ * @author reghao
+ * @date 2021-11-14 14:51:17
+ */
+public interface AccountAuthService extends UserDetailsService {
+    AccountAuthToken getPreAuthentication(@Validated AccountLoginDto userLoginDto) throws AuthenticationException;
+    AccountAuthToken authByPassword(AccountAuthToken authToken);
+    void setCookie(AccountAuthToken authToken, long timeout);
+    void clearCookie();
+    void logout();
+}

+ 20 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/AccountService.java

@@ -0,0 +1,20 @@
+package cn.reghao.oss.web.account.service;
+
+import cn.reghao.oss.web.account.model.dto.CreateAccountDto;
+import cn.reghao.oss.web.account.model.dto.AccountProfile;
+import cn.reghao.oss.web.account.model.dto.AccountRole;
+import cn.reghao.jutil.jdk.result.Result;
+
+/**
+ * @author reghao
+ * @date 2020-06-19 16:36:53
+ */
+public interface AccountService {
+    void initAccount();
+    Result createAccount(CreateAccountDto createAccountDto);
+    void updateAccountPassword(Integer userId, String newPassword);
+    void updateAccountProfile(AccountProfile accountProfile);
+    void updateAccountRole(AccountRole accountRole);
+    void updateAccountStatus(Integer userId, Boolean enable);
+    void deleteAccount(Integer userId);
+}

+ 56 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/AccountSessionService.java

@@ -0,0 +1,56 @@
+package cn.reghao.oss.web.account.service;
+
+import cn.reghao.oss.web.account.db.repository.UserRepository;
+import cn.reghao.oss.web.account.model.po.User;
+import org.springframework.security.core.session.SessionInformation;
+import org.springframework.security.core.session.SessionRegistry;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.*;
+
+/**
+ * @author reghao
+ * @date 2024-02-04 16:00:01
+ */
+@Service
+public class AccountSessionService {
+    private final SessionRegistry sessionRegistry;
+    private final UserRepository userRepository;
+
+    public AccountSessionService(SessionRegistry sessionRegistry, UserRepository userRepository) {
+        this.sessionRegistry = sessionRegistry;
+        this.userRepository = userRepository;
+    }
+
+    public Map<Integer, LocalDateTime> getLastRequest() {
+        Map<Integer, LocalDateTime> map = new HashMap<>();
+        for (Object principal : sessionRegistry.getAllPrincipals()) {
+            String username = (String) principal;
+            User user = userRepository.findByUsername(username);
+            for (SessionInformation sessionInfo : sessionRegistry.getAllSessions(principal, false)) {
+                Date date = sessionInfo.getLastRequest();
+                LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
+                map.put(user.getId(), localDateTime);
+            }
+        }
+
+        return map;
+    }
+
+    public void deactiveSession(User user) {
+        int currentUserId = UserContext.getUser().getId();
+        if (currentUserId != user.getId()) {
+            String principal = user.getUsername();
+            for (Object object : sessionRegistry.getAllPrincipals()) {
+                String username = (String) object;
+                if (username.equals(principal)) {
+                    for (SessionInformation sessionInfo : sessionRegistry.getAllSessions(principal, false)) {
+                        sessionInfo.expireNow();
+                    }
+                }
+            }
+        }
+    }
+}

+ 42 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/CodeService.java

@@ -0,0 +1,42 @@
+package cn.reghao.oss.web.account.service;
+
+import cn.reghao.oss.web.util.CaptchaUtil;
+import cn.reghao.oss.web.util.CacheKeys;
+import cn.reghao.jutil.jdk.security.RandomString;
+import com.github.benmanes.caffeine.cache.Cache;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.stereotype.Service;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * @author reghao
+ * @date 2022-04-26 20:24:58
+ */
+@Slf4j
+@Service
+public class CodeService {
+    private final long sessionTimeout;
+    private final Cache<String, Object> caffeineCache;
+
+    public CodeService(ServerProperties serverProperties, Cache<String, Object> caffeineCache) {
+        this.sessionTimeout = serverProperties.getServlet().getSession().getTimeout().getSeconds();
+        this.caffeineCache = caffeineCache;
+    }
+
+    public InputStream generateCaptcha() throws IOException {
+        String captchaCode = RandomString.getString(5);
+        caffeineCache.put(CacheKeys.getCaptchaKey(), captchaCode);
+
+        ByteArrayOutputStream baos = CaptchaUtil.captchaWithDisturb(captchaCode, 130, 48);
+        return new ByteArrayInputStream(baos.toByteArray());
+    }
+
+    public String getCaptcha() {
+        return (String) caffeineCache.getIfPresent(CacheKeys.getCaptchaKey());
+    }
+}

+ 15 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/FileService.java

@@ -0,0 +1,15 @@
+package cn.reghao.oss.web.account.service;
+
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+
+/**
+ * @author reghao
+ * @date 2024-01-19 21:29:24
+ */
+public interface FileService {
+    void initLocalStore() throws IOException;
+    void getFile(String objectName) throws IOException;
+    String putFile(MultipartFile file) throws Exception;
+}

+ 70 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/HomeService.java

@@ -0,0 +1,70 @@
+package cn.reghao.oss.web.account.service;
+
+import cn.reghao.oss.web.account.db.query.RoleQuery;
+import cn.reghao.oss.web.account.model.po.Menu;
+import cn.reghao.oss.web.account.model.po.Role;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2021-04-04 23:36:10
+ */
+@Service
+public class HomeService {
+    private final RoleQuery roleQuery;
+
+    public HomeService(RoleQuery roleQuery) {
+        this.roleQuery = roleQuery;
+    }
+
+    /**
+     * 用户拥有的角色可访问的资源
+     *
+     * @param
+     * @return
+     * @date 2021-09-10 下午6:27
+     */
+    public List<Menu> userMenus(Set<String> roles) {
+        List<Menu> menus = new ArrayList<>();
+        roles.forEach(name -> {
+            Role role = roleQuery.findByName(name);
+            menus.addAll(role.getMenus().stream().filter(Menu::getEnabled).collect(Collectors.toSet()));
+        });
+
+        return menus;
+    }
+
+    /**
+     * 根据 menu 集合生成页面侧边栏的树形菜单
+     *
+     * @param
+     * @return
+     * @date 2021-09-10 下午6:24
+     */
+    public Map<Integer, Menu> treeMenu(List<Menu> menus) {
+        // id -> menu
+        Map<Integer, Menu> keyMenu = new HashMap<>(16);
+        menus.forEach(menu -> keyMenu.put(menu.getId(), menu));
+
+        // 树形菜单数据
+        Map<Integer, Menu> treeMenu = new HashMap<>(16);
+        keyMenu.forEach((id, menu) -> {
+            Menu parentMenu = keyMenu.get(menu.getPid());
+            if (parentMenu != null) {
+                Map<Integer, Menu> childMenus = parentMenu.getChildren();
+                if (childMenus == null) {
+                    childMenus = new HashMap<>();
+                    parentMenu.setChildren(childMenus);
+                }
+                childMenus.put(menu.getPos(), menu);
+            } else {
+                treeMenu.put(menu.getPos(), menu);
+            }
+        });
+
+        return treeMenu;
+    }
+}

+ 15 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/MenuService.java

@@ -0,0 +1,15 @@
+package cn.reghao.oss.web.account.service;
+
+import cn.reghao.jutil.jdk.result.Result;
+import cn.reghao.oss.web.account.model.dto.MenuDTO;
+import cn.reghao.oss.web.account.model.po.Menu;
+
+/**
+ * @author reghao
+ * @date 2021-07-21 15:17:14
+ */
+public interface MenuService {
+    Result addMenu(Menu menu);
+    Result updateMenu(MenuDTO menuDTO);
+    Result deleteMenu(Integer menuId);
+}

+ 58 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/PubkeyService.java

@@ -0,0 +1,58 @@
+package cn.reghao.oss.web.account.service;
+
+import cn.reghao.oss.web.account.model.dto.RsaPubkey;
+import cn.reghao.oss.web.util.CacheKeys;
+import cn.reghao.jutil.jdk.security.RandomString;
+import cn.reghao.jutil.jdk.security.RsaCryptor;
+import com.github.benmanes.caffeine.cache.Cache;
+import org.springframework.stereotype.Service;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2023-02-20 11:09:03
+ */
+@Service
+public class PubkeyService {
+    private final Cache<String, Object> caffeineCache;
+
+    public PubkeyService(Cache<String, Object> caffeineCache) {
+        this.caffeineCache = caffeineCache;
+    }
+
+    public RsaPubkey getPubkey() throws NoSuchAlgorithmException {
+        String pubkey = (String) caffeineCache.getIfPresent(CacheKeys.getRsaPubkeyKey());
+        if (pubkey == null) {
+            Map<String, String> rsaKeyPair = RsaCryptor.genKeyPair();
+            String prikey = rsaKeyPair.get("prikey");
+            pubkey = rsaKeyPair.get("pubkey");
+
+            caffeineCache.put(CacheKeys.getRsaPubkeyKey(), pubkey);
+            caffeineCache.put(CacheKeys.getRsaPrikeyKey(), prikey);
+        }
+
+        String r = RandomString.getString(16);
+        long timeout = 300;
+        caffeineCache.put(CacheKeys.getPubkeyRKey(), r);
+        return new RsaPubkey(pubkey, r);
+    }
+
+    public String decrypt(String cipherText) throws Exception {
+        String prikey = (String) caffeineCache.getIfPresent(CacheKeys.getRsaPrikeyKey());
+        if (prikey == null) {
+            String msg = "私钥不存在, 请重新刷新页面";
+            throw new Exception(msg);
+        }
+
+        String r = (String) caffeineCache.getIfPresent(CacheKeys.getPubkeyRKey());
+        if (r == null) {
+            String msg = "当前会话已过期, 请刷新页面后重新登录. 或者检查浏览器是否禁用了 cookie";
+            throw new Exception(msg);
+        }
+
+        String plainText = RsaCryptor.decrypt(cipherText, prikey);
+        return plainText.replace(r, "");
+    }
+}

+ 17 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/RoleService.java

@@ -0,0 +1,17 @@
+package cn.reghao.oss.web.account.service;
+
+import cn.reghao.oss.web.account.model.po.Menu;
+import cn.reghao.oss.web.account.model.po.Role;
+
+import java.util.Set;
+
+/**
+ * @author reghao
+ * @date 2020-06-19 16:36:53
+ */
+public interface RoleService {
+    void initRole();
+    void addOrModify(Role role);
+    void delete(Integer roleId);
+    void setRoleMenus(Integer roleId, Set<Menu> menus);
+}

+ 51 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/UserContext.java

@@ -0,0 +1,51 @@
+package cn.reghao.oss.web.account.service;
+
+import cn.reghao.oss.web.account.model.po.User;
+import cn.reghao.oss.web.account.security.form.AccountAuthToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * @author reghao
+ * @date 2023-12-01 10:31:52
+ */
+public class UserContext {
+    public static String getUsername() {
+        User user = getUser();
+        if (user == null) {
+            return null;
+        }
+
+        return user.getScreenName();
+    }
+
+    public static User getUser() {
+        Authentication authToken = SecurityContextHolder.getContext().getAuthentication();
+        if (authToken instanceof AccountAuthToken) {
+            return (User) authToken.getDetails();
+        }
+
+        return null;
+    }
+
+    public static String getUserRole() {
+        User user = getUser();
+        if (user != null && !user.getRole().isEmpty()) {
+            return user.getRole().iterator().next();
+        }
+
+        return "";
+    }
+
+    public static Set<String> getUserRoles() {
+        User user = getUser();
+        if (user != null) {
+            return user.getRole();
+        }
+
+        return Collections.emptySet();
+    }
+}

+ 155 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/impl/AccountAuthServiceImpl.java

@@ -0,0 +1,155 @@
+package cn.reghao.oss.web.account.service.impl;
+
+import cn.reghao.oss.web.account.db.repository.UserRepository;
+import cn.reghao.oss.web.account.model.dto.AccountLoginDto;
+import cn.reghao.oss.web.account.model.po.User;
+import cn.reghao.oss.web.account.security.exceptioin.AccountLoginException;
+import cn.reghao.oss.web.account.security.form.AccountAuthToken;
+import cn.reghao.oss.web.account.service.AccountAuthService;
+import cn.reghao.oss.web.account.service.CodeService;
+import cn.reghao.oss.web.account.service.PubkeyService;
+import cn.reghao.oss.web.util.CacheKeys;
+import cn.reghao.jutil.web.ServletUtil;
+import com.github.benmanes.caffeine.cache.Cache;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.Cookie;
+
+/**
+ * @author reghao
+ * @date 2022-07-08 14:51:17
+ */
+@Service
+public class AccountAuthServiceImpl implements AccountAuthService {
+    private final String cookieName = "USERDATA";
+    private final String domain = "";
+    private final Cache<String, Object> cache;
+    private final UserRepository userRepository;
+    private final CodeService codeService;
+    private final PubkeyService pubkeyService;
+    private final PasswordEncoder passwordEncoder;
+
+    public AccountAuthServiceImpl(Cache<String, Object> cache, UserRepository userRepository,
+                                  CodeService codeService, PubkeyService pubkeyService, PasswordEncoder passwordEncoder) {
+        this.cache = cache;
+        this.userRepository = userRepository;
+        this.codeService = codeService;
+        this.pubkeyService = pubkeyService;
+        this.passwordEncoder = passwordEncoder;
+    }
+
+    @Override
+    public AccountAuthToken getPreAuthentication(AccountLoginDto accountLoginDto) {
+        String principal = accountLoginDto.getPrincipal();
+        String credential = accountLoginDto.getCredential();
+        String captchaCode = accountLoginDto.getCaptchaCode();
+        Boolean rememberMe = accountLoginDto.getRememberMe();
+
+        /*String savedCaptchaCode = codeService.getCaptcha();
+        if (savedCaptchaCode == null) {
+            String errMsg = "当前会话已过期, 请刷新页面后重新登录. 或者检查浏览器是否禁用了 cookie";
+            throw new PreAuthenticatedCredentialsNotFoundException(errMsg);
+        } else if (!savedCaptchaCode.equalsIgnoreCase(captchaCode)) {
+            String errMsg = "图形验证码不正确...";
+            throw new PreAuthenticatedCredentialsNotFoundException(errMsg);
+        }*/
+
+        String decryptCredential;
+        try {
+            decryptCredential = pubkeyService.decrypt(credential);
+        } catch (Exception e) {
+            throw new PreAuthenticatedCredentialsNotFoundException(e.getMessage());
+        }
+
+        String loginId = ServletUtil.getSessionId();
+        return new AccountAuthToken(loginId, rememberMe, principal, decryptCredential);
+    }
+
+    @Override
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+        User user = userRepository.findByUsername(username);
+        if (user == null) {
+            String errMsg = String.format("帐号 %s 不存在", username);
+            throw new UsernameNotFoundException(errMsg);
+        }
+
+        return user;
+    }
+
+    @Override
+    public AccountAuthToken authByPassword(AccountAuthToken authToken) {
+        String username = (String) authToken.getPrincipal();
+        User user = (User) loadUserByUsername(username);
+        String password = (String) authToken.getCredentials();
+        String encodedPassword = passwordEncoder.encode(password + user.getSalt());
+        if (!user.getPassword().equals(encodedPassword)) {
+            String errMsg = "账号或密码不正确";
+            throw new AccountLoginException(errMsg);
+        }
+
+        return new AccountAuthToken(authToken, user);
+    }
+
+    @Override
+    public void setCookie(AccountAuthToken authToken, long timeout) {
+        int userId = authToken.getUserId();
+        int plat = 1;
+        String loginId = authToken.getLoginId();
+
+        String userdata = String.format("%s:%s:%s", userId, plat, loginId);
+        Cookie cookie3 = generateCookie(cookieName, userdata, timeout);
+        ServletUtil.getResponse().addCookie(cookie3);
+
+        String loginSuccessKey = CacheKeys.getLoginSuccessKey(userId, plat, loginId);
+        if (timeout == 0) {
+            timeout = 3600*24*30;
+        }
+        cache.put(loginSuccessKey, authToken);
+    }
+
+    private Cookie generateCookie(String name, String value, long timeout) {
+        String path = "/";
+
+        Cookie cookie = new Cookie(name, value);
+        if (timeout != 0) {
+            cookie.setMaxAge((int) timeout);
+        }
+        cookie.setDomain(domain);
+        cookie.setSecure(true);
+        cookie.setPath(path);
+        return cookie;
+    }
+
+    @Override
+    public void logout() {
+        // 删除 cookie 中的数据(或者是撤销 token)
+        // 删除 redis 中的 account login 数据
+        // 删除 redis 中的 session 数据
+        // 删除 SecurityContextHolder 中的数据
+
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication instanceof AccountAuthToken) {
+            SecurityContext context = SecurityContextHolder.getContext();
+            context.setAuthentication(null);
+            SecurityContextHolder.clearContext();
+        }
+    }
+
+    @Override
+    public void clearCookie() {
+        String path = "/";
+        Cookie cookie2 = new Cookie(cookieName, null);
+        cookie2.setMaxAge(0);
+        cookie2.setDomain(domain);
+        cookie2.setSecure(true);
+        cookie2.setPath(path);
+        ServletUtil.getResponse().addCookie(cookie2);
+    }
+}

+ 154 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/impl/AccountServiceImpl.java

@@ -0,0 +1,154 @@
+package cn.reghao.oss.web.account.service.impl;
+
+import cn.reghao.oss.web.account.db.repository.RoleRepository;
+import cn.reghao.oss.web.account.db.repository.UserRepository;
+import cn.reghao.oss.web.account.model.constant.RoleType;
+import cn.reghao.oss.web.account.model.dto.CreateAccountDto;
+import cn.reghao.oss.web.account.model.po.Role;
+import cn.reghao.oss.web.account.service.AccountService;
+import cn.reghao.oss.web.account.service.AccountSessionService;
+import cn.reghao.jutil.jdk.result.Result;
+import cn.reghao.jutil.jdk.security.RandomString;
+import cn.reghao.oss.web.account.model.dto.AccountProfile;
+import cn.reghao.oss.web.account.model.dto.AccountRole;
+import cn.reghao.oss.web.account.model.po.User;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2020-06-19 16:36:53
+ */
+@Slf4j
+@Service
+public class AccountServiceImpl implements AccountService {
+    private final UserRepository userRepository;
+    private final RoleRepository roleRepository;
+    private final PasswordEncoder passwordEncoder;
+    private final AccountSessionService accountSessionService;
+
+    public AccountServiceImpl(UserRepository userRepository, RoleRepository roleRepository,
+                              PasswordEncoder passwordEncoder, AccountSessionService accountSessionService) {
+        this.userRepository = userRepository;
+        this.roleRepository = roleRepository;
+        this.passwordEncoder = passwordEncoder;
+        this.accountSessionService = accountSessionService;
+    }
+
+    @Override
+    public void initAccount() {
+        List<User> list = userRepository.findAll(PageRequest.of(0, 1)).getContent();
+        if (list.isEmpty()) {
+            String username = RandomString.getString(3).toLowerCase(Locale.ROOT);
+            username = "admin";
+            String password = RandomString.getString(10);
+
+            Role role = roleRepository.findByName(RoleType.ROLE_ADMIN.name());
+            if (role != null) {
+                int roleId = role.getId();
+                CreateAccountDto createAccountDto = new CreateAccountDto(username, password, roleId);
+                createAccount(createAccountDto);
+                log.info("初始化完成, 帐号和密码分别是 {} 和 {}", username, password);
+            }
+        }
+    }
+
+    @Override
+    public Result createAccount(CreateAccountDto createAccountDto) {
+        int roleId = createAccountDto.getRoleId();
+        Role role = roleRepository.findById(roleId).orElse(null);
+        if (role == null) {
+            return Result.fail("role 不存在");
+        }
+
+        String username = createAccountDto.getUsername();
+        User user = userRepository.findByUsername(username);
+        if (user == null) {
+            String password = createAccountDto.getPassword();
+            String salt = RandomString.getSalt(64);
+            String encodedPassword = passwordEncoder.encode(password + salt);
+
+            user = new User(username, encodedPassword, salt, Set.of(role.getName()));
+            userRepository.save(user);
+            return Result.success();
+        }
+
+        String errMsg = String.format("帐号 %s 已存在", username);
+        return Result.fail(errMsg);
+
+    }
+
+    @Override
+    public void updateAccountPassword(Integer userId, String newPassword) {
+        User userEntity = userRepository.findById(userId).orElse(null);
+        if (userEntity == null) {
+            return;
+        }
+
+        String salt = RandomString.getSalt(64);
+        String encodedPassword = passwordEncoder.encode(newPassword + salt);
+        userEntity.setSalt(salt);
+        userEntity.setEncodedPassword(encodedPassword);
+        userRepository.save(userEntity);
+        accountSessionService.deactiveSession(userEntity);
+    }
+
+    @Override
+    public void updateAccountProfile(AccountProfile accountProfile) {
+        User userEntity = userRepository.findById(accountProfile.getUserId()).orElse(null);
+        if (userEntity == null) {
+            return;
+        }
+
+        userEntity.setScreenName(accountProfile.getScreenName());
+        userEntity.setMobile(accountProfile.getMobile());
+        userEntity.setEmail(accountProfile.getEmail());
+        userRepository.save(userEntity);
+    }
+
+    @Override
+    public void updateAccountRole(AccountRole accountRole) {
+        int userId = accountRole.getUserId();
+        User userEntity = userRepository.findById(userId).orElse(null);
+        if (userEntity == null) {
+            return;
+        }
+
+        Set<String> roles = accountRole.getRoles().stream().map(Role::getName).collect(Collectors.toSet());
+        userEntity.setRole(roles);
+        userRepository.save(userEntity);
+        accountSessionService.deactiveSession(userEntity);
+    }
+
+    @Override
+    public void updateAccountStatus(Integer userId, Boolean enable) {
+        User userEntity = userRepository.findById(userId).orElse(null);
+        if (userEntity == null) {
+            return;
+        }
+
+        userEntity.setEnabled(enable);
+        userRepository.save(userEntity);
+    }
+
+    @Override
+    public void deleteAccount(Integer userId) {
+        User userEntity = userRepository.findById(userId).orElse(null);
+        if (userEntity == null) {
+            return;
+        }
+
+        if (userEntity.getId() == 1) {
+            log.error("不能删除管理员帐号");
+        } else {
+            userRepository.delete(userEntity);
+        }
+    }
+}

+ 157 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/impl/FileServiceImpl.java

@@ -0,0 +1,157 @@
+package cn.reghao.oss.web.account.service.impl;
+
+import cn.reghao.oss.web.account.db.repository.DiskFileRepository;
+import cn.reghao.oss.web.account.model.po.DiskFile;
+import cn.reghao.oss.web.account.service.FileService;
+import cn.reghao.jutil.jdk.security.DigestUtil;
+import cn.reghao.jutil.web.ServletUtil;
+import org.apache.commons.io.FileUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * @author reghao
+ * @date 2024-01-19 21:29:24
+ */
+@Service
+public class FileServiceImpl implements FileService {
+    // 1MiB
+    private final int bufSize = 1024*1024;
+    private final String storeDir = "";
+    private final DiskFileRepository diskFileRepository;
+
+    public FileServiceImpl(DiskFileRepository diskFileRepository) {
+        this.diskFileRepository = diskFileRepository;
+    }
+
+    @Override
+    public void initLocalStore() throws IOException {
+        File dir1 = new File(storeDir);
+        if (!dir1.exists()) {
+            //FileUtils.forceMkdir(dir1);
+        }
+    }
+
+    @Override
+    public void getFile(String objectName) throws IOException {
+        DiskFile diskFile = diskFileRepository.findByObjectName(objectName);
+        if (diskFile == null) {
+            writeResponse(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+
+        String absolutePath = diskFile.getAbsolutePath();
+        String contentType = diskFile.getContentType();
+        long size = diskFile.getSize();
+        HttpServletResponse response = ServletUtil.getResponse();
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setContentType(contentType);
+        response.setContentLengthLong(size);
+
+        OutputStream outputStream = response.getOutputStream();
+        writeResponse(outputStream, absolutePath, 0, size);
+    }
+
+    private void writeResponse(int statusCode) throws IOException {
+        HttpServletResponse response = ServletUtil.getResponse();
+        response.setStatus(statusCode);
+        OutputStream outputStream = response.getOutputStream();
+        outputStream.flush();
+        outputStream.close();
+    }
+
+    private void writeResponse(OutputStream outputStream, String absolutePath, long start, long end) throws IOException {
+        RandomAccessFile raf = new RandomAccessFile(absolutePath, "r");
+        raf.seek(start);
+
+        long len = end-start+1;
+        if (len < bufSize) {
+            int len1 = (int) len;
+            byte[] buf1 = new byte[len1];
+            int readLen1 = raf.read(buf1, 0, len1);
+            outputStream.write(buf1, 0, readLen1);
+        } else {
+            byte[] buf = new byte[bufSize];
+            long totalRead = 0;
+            int readLen;
+            while ((readLen = raf.read(buf, 0, bufSize)) != -1) {
+                outputStream.write(buf, 0, readLen);
+                totalRead += readLen;
+
+                long left = len - totalRead;
+                if (left < bufSize) {
+                    int left1 = (int) left;
+                    byte[] buf1 = new byte[left1];
+                    int readLen1 = raf.read(buf1, 0, left1);
+                    outputStream.write(buf1, 0, readLen1);
+                    break;
+                }
+            }
+        }
+
+        outputStream.flush();
+        outputStream.close();
+        raf.close();
+    }
+
+    @Override
+    public String putFile(MultipartFile file) throws Exception {
+        long size = file.getSize();
+        String filename = file.getOriginalFilename();
+        String suffix = getSuffix(filename);
+
+        String objectId = UUID.randomUUID().toString().replace("-", "");;
+        String objectName;
+        if (suffix.isBlank()) {
+            objectName = String.format("file/%s", objectId);
+        } else {
+            objectName = String.format("file/%s%s", objectId, suffix);
+        }
+
+        String contentId = UUID.randomUUID().toString().replace("-", "");
+        File savedFile = saveFile(file.getInputStream(), contentId, suffix);
+        String contentType = Files.probeContentType(Path.of(savedFile.getAbsolutePath()));
+        String sha256sum = DigestUtil.sha256sum(savedFile.getAbsolutePath());
+
+        List<DiskFile> diskFiles = diskFileRepository.findBySha256sum(sha256sum);
+        DiskFile diskFile;
+        if (!diskFiles.isEmpty()) {
+            DiskFile existFile = diskFiles.get(0);
+            diskFile = new DiskFile(objectName, objectId, existFile, filename);
+            FileUtils.deleteQuietly(savedFile);
+        } else {
+            diskFile = new DiskFile(objectName, objectId, savedFile.getAbsolutePath(), sha256sum, filename, contentType, size);
+        }
+
+        diskFileRepository.save(diskFile);
+        return "/" + objectName;
+    }
+
+    private File saveFile(InputStream inputStream, String contentId, String suffix) throws IOException {
+        String absolutePath = String.format("%s/%s%s", storeDir, contentId, suffix);
+        File file = new File(absolutePath);
+        if (file.exists()) {
+            throw new IOException(absolutePath + " exist");
+        }
+
+        Files.copy(inputStream, Path.of(absolutePath), StandardCopyOption.REPLACE_EXISTING);
+        return file;
+    }
+
+    private String getSuffix(String filename) {
+        if (filename == null) {
+            return "";
+        }
+
+        int idx = filename.lastIndexOf(".");
+        return idx == -1 ? "" : filename.substring(idx);
+    }
+}

+ 237 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/impl/MenuServiceImpl.java

@@ -0,0 +1,237 @@
+package cn.reghao.oss.web.account.service.impl;
+
+import cn.reghao.oss.web.account.db.repository.MenuRepository;
+import cn.reghao.oss.web.account.db.repository.RoleRepository;
+import cn.reghao.oss.web.account.model.constant.MenuType;
+import cn.reghao.oss.web.account.service.MenuService;
+import cn.reghao.jutil.jdk.result.Result;
+import cn.reghao.jutil.jdk.result.ResultStatus;
+import cn.reghao.oss.web.account.model.dto.MenuDTO;
+import cn.reghao.oss.web.account.model.po.Menu;
+import cn.reghao.oss.web.account.model.po.Role;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2021-05-17 17:16:19
+ */
+@Slf4j
+@Service
+public class MenuServiceImpl implements MenuService {
+    private final MenuRepository menuRepository;
+    private final RoleRepository roleRepository;
+
+    public MenuServiceImpl(MenuRepository menuRepository, RoleRepository roleRepository) {
+        this.menuRepository = menuRepository;
+        this.roleRepository = roleRepository;
+    }
+
+    @Override
+    public synchronized Result addMenu(Menu menu) {
+        Result result = moreThanTwoParents(menu.getPid());
+        if (result.getCode() != ResultStatus.SUCCESS.getCode()) {
+            return result;
+        }
+
+        // 调整 menu 组内元素的位置
+        insertNewMenu(menu);
+        menu.setEnabled(true);
+        Menu menuEntity = menuRepository.save(menu);
+
+        //Set<Role> roles = menu.getRoles();
+        Set<Role> roles = Collections.emptySet();
+        roles.forEach(role -> role.getMenus().add(menuEntity));
+        roleRepository.saveAll(new ArrayList<>(roles));
+        return Result.result(ResultStatus.SUCCESS);
+    }
+
+    /**
+     * 检查菜单的层级, Menu 最多只能有两个 parent, 即最多只能有三级菜单
+     *
+     * @param
+     * @return
+     * @date 2021-07-15 上午11:15
+     */
+    private Result moreThanTwoParents(int pid) {
+        if (pid != 0) {
+            Menu menu1 = menuRepository.getOne(pid);
+            int pid1 = menu1.getPid();
+            if (pid1 != 0) {
+                Menu menu2 = menuRepository.getOne(pid1);
+                int pid2 = menu2.getPid();
+                if (pid2 != 0) {
+                    return Result.result(ResultStatus.FAIL, "Menu 最多只能有两个 parent,即最多只能有三级菜单");
+                }
+            }
+        }
+
+        return Result.result(ResultStatus.SUCCESS);
+    }
+
+    /**
+     * 向 menu 组插入元素时调整组内 menu 的位置
+     *
+     * @date 2021-05-18 上午9:54
+     */
+    private void insertNewMenu(Menu menu) {
+        int pid = menu.getPid();
+        if (pid != 0) {
+            Menu pMenu = menuRepository.getOne(pid);
+            if (pMenu.getType().equals(MenuType.page.name())) {
+                log.error("父级菜单的类型不能是 PAGE");
+                return;
+            }
+        }
+
+        List<Menu> menus = menuRepository.findByPid(pid);
+        menus.sort(Comparator.comparingInt(Menu::getPos));
+        // 组内没有 menu 或新增的 menu 在组内排在最后一位
+        if (menus.isEmpty()) {
+            menu.setPos(1);
+            return;
+        }
+
+        // menu 的新位置
+        int pos = menu.getPos()+1;
+        int size = menus.size();
+        if (pos == 1) {
+            // menu 的位置在 menu 组的首位, 组内的其他元素位置向后移一位
+            menu.setPos(1);
+            moveBackward(menus, 1);
+        } else if (pos >= size+1) {
+            // menu 的位置在 menu 组的末尾, 组内的其他元素位置不变
+            menu.setPos(size+1);
+        } else {
+            // menu 的位置在 menu 组的中间, pos 开始的元素位置向后移一位
+            menu.setPos(pos);
+            moveBackward(menus, pos);
+        }
+    }
+
+    /**
+     * 向后移动元素(向 menu 组新增元素时调用)
+     *
+     * @return
+     * @date 2021-07-15 下午5:07
+     */
+    private void moveBackward(List<Menu> menus, int startPos) {
+        for (int i = startPos-1; i < menus.size(); i++) {
+            Menu tmpMenu = menus.get(i);
+            int newPos = tmpMenu.getPos()+1;
+            tmpMenu.setPos(newPos);
+        }
+        menuRepository.saveAll(menus);
+    }
+
+    /**
+     * 向前移动元素(从 menu 组删除元素时调用)
+     *
+     * @param
+     * @return
+     * @date 2021-07-15 下午5:41
+     */
+    private void moveForward(List<Menu> menus, int startPos) {
+        for (int i = startPos; i < menus.size(); i++) {
+            Menu tmpMenu = menus.get(i);
+            int newPos = tmpMenu.getPos()-1;
+            tmpMenu.setPos(newPos);
+        }
+        menuRepository.saveAll(menus);
+    }
+
+    @Override
+    public synchronized Result updateMenu(MenuDTO menuDTO) {
+        int menuId = menuDTO.getMenuId();
+        Menu menuEntity = menuRepository.getOne(menuId);
+        if (menuEntity == null) {
+            return Result.result(ResultStatus.FAIL, String.format("ID 为 %s 的 Menu 不存在", menuId));
+        }
+
+        int oldPos = menuEntity.getPos();
+        // menu 的新位置
+        int newPos = menuDTO.getPos()+1;
+        int pid = menuDTO.getPid();
+        if (pid != menuEntity.getPid()) {
+            Result result = moreThanTwoParents(pid);
+            if (result.getCode() != ResultStatus.SUCCESS.getCode()) {
+                return result;
+            }
+
+            // menu 更换到了新的 menu 组
+            menuEntity.setPid(pid);
+            menuEntity.setPos(menuDTO.getPos());
+            insertNewMenu(menuEntity);
+        } else if (newPos != oldPos) {
+            reOrderMenus(pid, oldPos, newPos);
+            menuEntity.setPos(newPos);
+        }
+
+        menuEntity.setName(menuDTO.getName());
+        menuEntity.setUrl(menuDTO.getUrl());
+        menuEntity.setIcon(menuDTO.getIcon());
+        menuEntity.setUpdateTime(LocalDateTime.now());
+        menuRepository.save(menuEntity);
+
+        Set<Role> roles = menuDTO.getRoles();
+        roles.forEach(role -> role.getMenus().add(menuEntity));
+        roleRepository.saveAll(new ArrayList<>(roles));
+        return Result.result(ResultStatus.SUCCESS);
+    }
+
+    public void updateMenusStatus(boolean status, List<Integer> menuIds) {
+        menuRepository.findAllById(menuIds).forEach(menu -> {
+            menu.setEnabled(status);
+            menuRepository.save(menu);
+        });
+    }
+
+    /**
+     * 对组内的菜单进行重排序,只需调整 oldPos~newPos 之间的元素位置
+     *
+     * @param
+     * @return
+     * @date 2021-07-21 上午11:07
+     */
+    private void reOrderMenus(int pid, int oldPos, int newPos) {
+        Map<Integer, Menu> map = menuRepository.findByPid(pid).stream()
+                .collect(Collectors.toMap(Menu::getPos, menu -> menu));
+        map.remove(oldPos);
+        if (newPos < oldPos) {
+            // 向前移动
+            for (int i = newPos, j = 1; i < oldPos; i++, j++) {
+                map.get(i).setPos(newPos+j);
+            }
+        } else {
+            // 向后移动
+            for (int i = newPos-1, j = 1; i > oldPos; i--, j++) {
+                map.get(i).setPos(newPos-j);
+            }
+        }
+        menuRepository.saveAll(new ArrayList<>(map.values()));
+    }
+
+    @Override
+    public synchronized Result deleteMenu(Integer menuId) {
+        Menu menu = menuRepository.getOne(menuId);
+
+        // 删除 Role 关联的 Menu
+        for (Role role : menu.getRoles()) {
+            role.getMenus().remove(menu);
+            roleRepository.save(role);
+        }
+
+        // 重新调整组内排序
+        int pid = menu.getPid();
+        List<Menu> menus = menuRepository.findByPid(pid);
+        menus.sort(Comparator.comparingInt(Menu::getPos));
+        int pos = menu.getPos();
+        moveForward(menus, pos);
+        menuRepository.delete(menu);
+        return Result.result(ResultStatus.SUCCESS);
+    }
+}

+ 77 - 0
oss-web/src/main/java/cn/reghao/oss/web/account/service/impl/RoleServiceImpl.java

@@ -0,0 +1,77 @@
+package cn.reghao.oss.web.account.service.impl;
+
+import cn.reghao.oss.web.account.db.query.RoleQuery;
+import cn.reghao.oss.web.account.db.repository.MenuRepository;
+import cn.reghao.oss.web.account.db.repository.RoleRepository;
+import cn.reghao.oss.web.account.model.constant.RoleType;
+import cn.reghao.oss.web.account.model.po.Menu;
+import cn.reghao.oss.web.account.model.po.Role;
+import cn.reghao.oss.web.account.model.po.User;
+import cn.reghao.oss.web.account.service.RoleService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+/**
+ * @author reghao
+ * @date 2020-06-19 16:36:53
+ */
+@Slf4j
+@Service
+public class RoleServiceImpl implements RoleService {
+    private final RoleQuery roleQuery;
+    private final RoleRepository roleRepository;
+    private final MenuRepository menuRepository;
+
+    public RoleServiceImpl(RoleQuery roleQuery, RoleRepository roleRepository, MenuRepository menuRepository) {
+        this.roleQuery = roleQuery;
+        this.roleRepository = roleRepository;
+        this.menuRepository = menuRepository;
+    }
+
+    public void initRole() {
+        List<Role> roleList = roleRepository.findAll();
+        if (roleList.isEmpty()) {
+            roleList = new ArrayList<>();
+            for (RoleType roleType : RoleType.values()) {
+                roleList.add(new Role(roleType.name(), roleType.getDesc()));
+            }
+            roleRepository.saveAll(roleList);
+
+            List<Menu> menus = menuRepository.findAll();
+            if (!menus.isEmpty()) {
+                Role role = roleRepository.findByName(RoleType.ROLE_ADMIN.name());
+                role.setMenus(new HashSet<>(menus));
+                roleRepository.save(role);
+            }
+        }
+    }
+
+    @Override
+    public void addOrModify(Role role) {
+        String title = String.format("ROLE_%s", role.getName());
+        role.setName(title.toUpperCase(Locale.ROOT));
+        roleRepository.save(role);
+    }
+
+    @Override
+    public void delete(Integer roleId) {
+        Role role = roleRepository.getOne(roleId);
+        List<User> users = roleQuery.getUsersByRole(role);
+        if (!users.isEmpty()) {
+            log.error("还有用户分配有本角色");
+            return;
+        }
+
+        role.getMenus().clear();
+        roleRepository.delete(role);
+    }
+
+    @Override
+    public void setRoleMenus(Integer roleId, Set<Menu> menus) {
+        Role role = roleRepository.getOne(roleId);
+        role.setMenus(menus);
+        roleRepository.save(role);
+    }
+}

+ 62 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/controller/AppConfigController.java

@@ -0,0 +1,62 @@
+package cn.reghao.oss.web.app.controller;
+
+import cn.reghao.oss.web.app.model.dto.AppConfigDto;
+import cn.reghao.oss.web.app.model.dto.AppConfigUpdateDto;
+import cn.reghao.oss.web.app.model.dto.CopyAppDto;
+import cn.reghao.oss.web.app.model.po.AppConfig;
+import cn.reghao.jutil.jdk.result.WebResult;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2019-11-27 11:29:43
+ */
+@Slf4j
+@Api(tags = "应用配置 CRUD 接口")
+@RestController
+@RequestMapping("/api/app/config/app")
+public class AppConfigController {
+    @ApiOperation(value = "添加应用配置")
+    @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+    public String addAppConfig(@Validated AppConfigDto appConfigDto) {
+        return WebResult.success();
+    }
+
+    // TODO 使用 @PathVariable 注解时会自动填充实体
+    @ApiOperation(value = "复制应用配置")
+    @PostMapping(value = "/copy", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String copyAppConfig(@Validated CopyAppDto copyAppDto) {
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "修改应用配置")
+    @PostMapping(value = "/update", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String updateAppConfig(@Validated AppConfigUpdateDto appConfigUpdateDto) {
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "删除应用配置")
+    @DeleteMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deleteAppConfig(@PathVariable("id") AppConfig app) {
+        String appId = app.getAppId();
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "清空应用本地仓库")
+    @DeleteMapping(value = "/repo/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deleteAppLocalRepo(@PathVariable("id") AppConfig app) {
+        return WebResult.success();
+    }
+
+    @PostMapping(value = "/enable", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deleteAll(@RequestParam(value = "ids") List<String> appIds) {
+        return WebResult.success();
+    }
+}

+ 53 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/controller/AppStatusController.java

@@ -0,0 +1,53 @@
+package cn.reghao.oss.web.app.controller;
+
+import cn.reghao.jutil.jdk.result.Result;
+import cn.reghao.jutil.jdk.result.ResultStatus;
+import cn.reghao.jutil.jdk.result.WebResult;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2020-02-28 11:33:23
+ */
+@Slf4j
+@Api(tags = "应用状态管理接口")
+@RestController
+@RequestMapping("/api/app/status")
+public class AppStatusController {
+    @ApiOperation(value = "重启应用")
+    @PostMapping(value = "/restart/{appId}/{machineId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String restart(@PathVariable("appId") String appId, @PathVariable("machineId") String machineId) throws Exception {
+        return WebResult.result(Result.result(ResultStatus.SUCCESS, appId + " 正在重启,请 10s 后刷新页面查看最新状态"));
+    }
+
+    @ApiOperation(value = "停止应用")
+    @PostMapping(value = "/stop/{appId}/{machineId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String stop(@PathVariable("appId") String appId, @PathVariable("machineId") String machineId) throws Exception {
+        return WebResult.result(Result.result(ResultStatus.SUCCESS, appId + " 正在停止,请稍后刷新页面"));
+    }
+
+    @ApiOperation(value = "启动应用")
+    @PostMapping(value = "/start/{appId}/{machineId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String start(@PathVariable("appId") String appId, @PathVariable("machineId") String machineId) throws Exception {
+        return WebResult.result(Result.result(ResultStatus.SUCCESS, appId + " 正在启动,请 10s 后刷新页面查看最新状态"));
+    }
+
+    @ApiOperation(value = "应用当前状态")
+    @GetMapping(value = "/{appId}/{machineId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String status(@PathVariable("appId") String appId, @PathVariable("machineId") String machineId) {
+        String msg = String.format("正在获取 %s 的状态,请 10s 后刷新页面查看", appId);
+        return WebResult.result(Result.result(ResultStatus.SUCCESS, msg));
+    }
+
+    @ApiOperation(value = "应用历史日志")
+    @GetMapping(value = "/log", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getAppLog() {
+        return WebResult.success();
+    }
+}

+ 45 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/controller/AudioFileController.java

@@ -0,0 +1,45 @@
+package cn.reghao.oss.web.app.controller;
+
+import cn.reghao.jutil.jdk.result.WebResult;
+import cn.reghao.oss.api.dto.media.AudioInfo;
+import cn.reghao.oss.api.dto.media.AudioUrl;
+import cn.reghao.oss.api.dto.media.ImageUrlDto;
+import cn.reghao.oss.web.app.service.media.AudioService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2024-02-23 09:27:21
+ */
+@Api(tags = "音频文件接口")
+@RestController
+@RequestMapping("/api/media/audio")
+public class AudioFileController {
+    private final AudioService audioService;
+
+    public AudioFileController(AudioService audioService) {
+        this.audioService = audioService;
+    }
+
+    @ApiOperation(value = "获取音频文件信息")
+    @GetMapping(value = "/info", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getAudioInfo(@RequestParam("audioFileId") String audioFileId) {
+        AudioInfo audioInfo = audioService.getAudioInfo(audioFileId);
+        return WebResult.success(audioInfo);
+    }
+
+    @ApiOperation(value = "获取音频文件 url")
+    @GetMapping(value = "/url", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getImageUrls(@RequestParam("audioFileId") String audioFileId) {
+        List<AudioUrl> list = audioService.getAudioUrls(audioFileId, 10000);
+        return WebResult.success(list);
+    }
+}

+ 68 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/controller/BuildDeployController.java

@@ -0,0 +1,68 @@
+package cn.reghao.oss.web.app.controller;
+
+import cn.reghao.jutil.jdk.result.WebResult;
+import cn.reghao.oss.api.dto.media.VideoInfo;
+import cn.reghao.oss.api.iface.media.VideoFileService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @author reghao
+ * @date 2019-08-30 18:49:15
+ */
+@Slf4j
+@Api(tags = "应用构建部署接口")
+@RestController
+@RequestMapping("/api/app/bd")
+public class BuildDeployController {
+    @DubboReference(check = false)
+    private VideoFileService videoFileService;
+
+    @ApiOperation(value = "构建部署应用")
+    @ApiImplicitParams(@ApiImplicitParam(name="appId", value="应用 ID", paramType="path", dataType = "String"))
+    @PostMapping(value = "/update/{appId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String buildAndDeploy(@PathVariable("appId") String appId) throws Exception {
+        return WebResult.successWithMsg("构建部署请求已提交");
+    }
+
+    @ApiOperation(value = "构建应用")
+    @ApiImplicitParams(@ApiImplicitParam(name="appId", value="应用 ID", paramType="path", dataType = "String"))
+    @PostMapping(value = "/build/{appId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String build(@PathVariable("appId") String appId) throws Exception {
+        return WebResult.successWithMsg("构建请求已提交");
+    }
+
+    @ApiOperation(value = "部署应用")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name="buildLogId", value="构建 ID", paramType="path", dataType = "String"),
+            @ApiImplicitParam(name="machineId", value="机器 ID", paramType="path", dataType = "String")
+    })
+    @PostMapping(value = "/deploy/{buildLogId}/{machineId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deploy(@PathVariable("buildLogId") String buildLogId, @PathVariable("machineId") String machineId)
+            throws Exception {
+        if (buildLogId == null || buildLogId.equals("null")) {
+            return WebResult.failWithMsg("应用尚未构建");
+        }
+
+        return WebResult.successWithMsg("部署请求已提交");
+    }
+
+    @ApiOperation(value = "重置应用构建状态")
+    @PostMapping(value = "/reset", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String resetState() {
+        VideoInfo videoInfo = videoFileService.getVideoInfo("abcdefg");
+        return WebResult.successWithMsg("应用构建状态已重置");
+    }
+
+    @ApiOperation(value = "webhook 自动构建部署")
+    @PostMapping("/hook")
+    public String hook(@RequestBody String body) throws Exception {
+        return WebResult.success();
+    }
+}

+ 72 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/controller/ImageFileController.java

@@ -0,0 +1,72 @@
+package cn.reghao.oss.web.app.controller;
+
+import cn.reghao.jutil.jdk.result.WebResult;
+import cn.reghao.oss.api.dto.media.ImageUrlDto;
+import cn.reghao.oss.web.app.service.media.ImageService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author reghao
+ * @date 2024-02-23 09:27:15
+ */
+@Api(tags = "图片文件接口")
+@RestController
+@RequestMapping("/api/media/image")
+public class ImageFileController {
+    private final ImageService imageService;
+
+    public ImageFileController(ImageService imageService) {
+        this.imageService = imageService;
+    }
+
+    @ApiOperation(value = "根据 object_name 删除图片文件")
+    @PostMapping(value = "/delete/name", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deleteByObjectNames(@RequestParam("objectNames") List<String> objectNames) throws Exception {
+        imageService.deleteByObjectNames(objectNames);
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "根据 image_file_id 删除图片文件")
+    @PostMapping(value = "/delete/id", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deleteByImageIds(@RequestParam("imageFileIds") List<String> imageFileIds) throws Exception {
+        imageService.deleteByImageFileIds(imageFileIds);
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "获取图片文件 url")
+    @GetMapping(value = "/url", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getImageUrl(@RequestParam("channelId") int channelId,
+                              @RequestParam("imageFileId") String imageFileId,
+                              @RequestParam(value = "loginUser", required = false) Long loginUser) {
+        ImageUrlDto imageUrlDto;
+        if (loginUser != null) {
+            imageUrlDto = imageService.getImageUrl(imageFileId, loginUser, channelId);
+        } else {
+            imageUrlDto = imageService.getImageUrl(channelId, imageFileId);
+        }
+
+        return WebResult.success(imageUrlDto);
+    }
+
+    @ApiOperation(value = "获取图片文件 url")
+    @GetMapping(value = "/urls", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getImageUrls(@RequestParam("imageFileIds") Set<String> imageFileIds) {
+        List<ImageUrlDto> list = imageService.getImageUrls(imageFileIds);
+        return WebResult.success(list);
+    }
+
+    @ApiOperation(value = "获取图片签名 url")
+    @GetMapping(value = "/signedurl", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getImageSignedUrls(@RequestParam("channelId") int channelId,
+                                     @RequestParam("url") String url,
+                                     @RequestParam(value = "loginUser") Long loginUser) {
+        String signedUrl = imageService.getSignedUrl(url, loginUser, channelId);
+        return WebResult.success(signedUrl);
+    }
+}

+ 82 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/controller/MediaController.java

@@ -0,0 +1,82 @@
+package cn.reghao.oss.web.app.controller;
+
+import cn.reghao.jutil.jdk.result.WebResult;
+import cn.reghao.oss.web.app.service.media.MediaService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author reghao
+ * @date 2024-02-23 09:27:28
+ */
+@Api(tags = "媒体接口")
+@RestController
+@RequestMapping("/api/media")
+public class MediaController {
+    private final MediaService mediaService;
+
+    public MediaController(MediaService mediaService) {
+        this.mediaService = mediaService;
+    }
+
+    @ApiOperation(value = "视频转码")
+    @PostMapping(value = "/convert/video/{videoFileId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String convertVideo(@PathVariable("videoFileId") String videoFileId) throws Exception {
+        mediaService.convertVideo(videoFileId);
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "音频转码")
+    @PostMapping(value = "/convert/audio/{audioFileId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String convertAudio(@PathVariable("audioFileId") String audioFileId) throws Exception {
+        mediaService.convertAudio(audioFileId);
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "设置视频可见范围")
+    @PostMapping(value = "/scope/video", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String setVideoScope(@RequestParam("videoFileId") String videoFileId,
+                                @RequestParam("scope") Integer scope) {
+        mediaService.setVideoScope(videoFileId, scope);
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "设置音频可见范围")
+    @PostMapping(value = "/scope/audio", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String setAudioScope(@RequestParam("audioFileId") String audioFileId,
+                                @RequestParam("scope") Integer scope) {
+        mediaService.setAudioScope(audioFileId, scope);
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "设置图片可见范围")
+    @PostMapping(value = "/scope/image", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String setImageScope(@RequestParam("imageFileIds") Set<String> imageFileIds,
+                                @RequestParam("scope") Integer scope) {
+        mediaService.setImagesScope(imageFileIds, scope);
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "设置对象可见范围")
+    @PostMapping(value = "/scope/object", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String setObjectScope(@RequestParam("scope") Integer scope,
+                                @RequestParam("objectId") String objectId,
+                                @RequestParam("contentType") Integer contentType) {
+        mediaService.setObjectScope(scope, objectId, contentType);
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "设置对象可见范围")
+    @PostMapping(value = "/scope/objects", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String setObjectsScope(@RequestParam("scope") Integer scope,
+                                  @RequestParam("objectIds") List<String> objectIds,
+                                  @RequestParam("contentType") Integer contentType) {
+        mediaService.setObjectsScope(scope, objectIds, contentType);
+        return WebResult.success();
+    }
+}

+ 56 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/controller/OssController.java

@@ -0,0 +1,56 @@
+package cn.reghao.oss.web.app.controller;
+
+import cn.reghao.jutil.jdk.result.WebResult;
+import cn.reghao.oss.api.dto.DownloadUrl;
+import cn.reghao.oss.api.dto.ObjectInfo;
+import cn.reghao.oss.api.dto.ServerInfo;
+import cn.reghao.oss.api.dto.media.ImageUrlDto;
+import cn.reghao.oss.web.app.service.OssService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author reghao
+ * @date 2024-02-23 09:27:36
+ */
+@Api(tags = "OSS 接口")
+@RestController
+@RequestMapping("/api/oss")
+public class OssController {
+    private final OssService ossService;
+
+    public OssController(OssService ossService) {
+        this.ossService = ossService;
+    }
+
+    @ApiOperation(value = "获取 oss-store 节点")
+    @GetMapping(value = "/server/info", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getServerInfo(@RequestParam("userId") Long userId, @RequestParam("channelId") Integer channelId) {
+        ServerInfo serverInfo = ossService.getServerInfo(userId, channelId);
+        return WebResult.success(serverInfo);
+    }
+
+    @ApiOperation(value = "获取对象信息")
+    @GetMapping(value = "/object/info", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getObjectInfo(@RequestParam("objectId") String objectId) {
+        ObjectInfo objectInfo = ossService.getObjectInfo(objectId);
+        return WebResult.success(objectInfo);
+    }
+
+    @ApiOperation(value = "获取对象 url")
+    @GetMapping(value = "/object/url", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getObjectUrl(@RequestParam("objectId") String objectId,
+                               @RequestParam("channelId") Integer channelId,
+                               @RequestParam("userId") Long userId) {
+        DownloadUrl downloadUrl = ossService.getDownloadUrl(objectId, channelId, userId);
+        return WebResult.success(downloadUrl);
+    }
+}

+ 57 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/controller/VideoFileController.java

@@ -0,0 +1,57 @@
+package cn.reghao.oss.web.app.controller;
+
+import cn.reghao.jutil.jdk.result.WebResult;
+import cn.reghao.oss.api.dto.media.VideoInfo;
+import cn.reghao.oss.api.dto.media.VideoUrlDto;
+import cn.reghao.oss.web.app.service.media.VideoService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * @author reghao
+ * @date 2024-02-23 09:05:58
+ */
+@Api(tags = "视频文件接口")
+@RestController
+@RequestMapping("/api/media/video")
+public class VideoFileController {
+    private final VideoService videoService;
+
+    public VideoFileController(VideoService videoService) {
+        this.videoService = videoService;
+    }
+
+    @ApiOperation(value = "删除视频文件")
+    @DeleteMapping(value = "/delete/{videoFileId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String deleteVideoFile(@PathVariable("videoFileId") String videoFileId) throws Exception {
+        videoService.deleteVideoFile(videoFileId);
+        return WebResult.success();
+    }
+
+    @ApiOperation(value = "获取视频文件信息")
+    @GetMapping(value = "/info/{videoFileId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getVideoInfo(@PathVariable("videoFileId") String videoFileId) {
+        VideoInfo videoInfo = videoService.getVideoInfo(videoFileId);
+        return WebResult.success(videoInfo);
+    }
+
+    @ApiOperation(value = "获取视频文件URL")
+    @GetMapping(value = "/url", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getVideoUrl(@RequestParam("videoFileId") String videoFileId,
+                              @RequestParam("userId") Long userId) {
+        List<VideoUrlDto> list = videoService.getVideoUrls(videoFileId, userId);
+        return WebResult.success(list);
+    }
+
+    @ApiOperation(value = "获取视频文件创建时间")
+    @GetMapping(value = "/time/{videoFileId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    public String getVideoCreateTime(@PathVariable("videoFileId") String videoFileId) {
+        LocalDateTime localDateTime = videoService.getCreateTime(videoFileId);
+        return WebResult.success(localDateTime);
+    }
+}

+ 113 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/controller/page/AppConfigPageController.java

@@ -0,0 +1,113 @@
+package cn.reghao.oss.web.app.controller.page;
+
+import cn.reghao.oss.web.app.model.po.AppConfig;
+import cn.reghao.oss.web.app.model.vo.KeyValue;
+import cn.reghao.oss.web.util.DefaultSetting;
+import cn.reghao.oss.web.util.db.PageSort;
+import cn.reghao.oss.web.app.db.query.AppConfigQuery;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author reghao
+ * @date 2019-08-30 18:49:15
+ */
+@Slf4j
+@Api(tags = "应用配置页面")
+@Controller
+@RequestMapping("/app/config/app")
+public class AppConfigPageController {
+    private final AppConfigQuery appConfigQuery;
+
+    public AppConfigPageController(AppConfigQuery appConfigQuery) {
+        this.appConfigQuery = appConfigQuery;
+    }
+
+    @ApiOperation(value = "应用配置页面")
+    @GetMapping
+    public String appConfigPage(@RequestParam(value = "env", required = false) String env,
+                                @RequestParam(value = "type", required = false) String type,
+                                @RequestParam(value = "appName", required = false) String appName,
+                                Model model) {
+        if (env == null) {
+            env = DefaultSetting.getDefaultAppEnv();
+        }
+
+        if (type == null) {
+            type = DefaultSetting.getDefaultAppType();
+        }
+
+        if (appName != null) {
+            Map<String, String> map = new HashMap<>();
+            map.put("appName", appName);
+            List<AppConfig> list = appConfigQuery.query(map);
+            Page<AppConfig> page = new PageImpl<>(list);
+
+            model.addAttribute("env", env);
+            model.addAttribute("type", type);
+            model.addAttribute("page", page);
+            model.addAttribute("list", page.getContent());
+            return "/app/config/app/index";
+        }
+
+        PageRequest pageRequest = PageSort.pageRequest();
+        Page<AppConfig> appPage = appConfigQuery.findByEnvAndType(env, type, pageRequest);
+
+        model.addAttribute("env", env);
+        model.addAttribute("type", type);
+        model.addAttribute("page", appPage);
+        model.addAttribute("list", appPage.getContent());
+        return "/app/config/app/index";
+    }
+
+    @GetMapping("/add")
+    public String addAppConfigPage(Model model) {
+        setAppModel(model);
+        return "/app/config/app/add";
+    }
+
+    private void setAppModel(Model model) {
+        List<KeyValue> packTypes = new ArrayList<>();
+        setCommon(model);
+    }
+
+    private void setCommon(Model model) {
+    }
+
+    @GetMapping("/edit/{id}")
+    public String editAppConfigPage(@PathVariable("id") AppConfig app, Model model) {
+        setAppModel(model);
+        model.addAttribute("app", app);
+        return "/app/config/app/edit";
+    }
+
+    @GetMapping("/copy/{appId}")
+    public String copyAppConfigPage(@PathVariable("appId") String appId, Model model) {
+        List<KeyValue> envs = getEnvList();
+        model.addAttribute("environments", envs);
+        model.addAttribute("appId", appId);
+        return "/app/config/app/copy";
+    }
+
+    private List<KeyValue> getEnvList() {
+        List<KeyValue> envs = new ArrayList<>();
+        return envs;
+    }
+
+    @GetMapping("/detail/{appId}")
+    public String appConfigPage(@PathVariable("appId") String appId, Model model) {
+        AppConfig app = appConfigQuery.findByAppId(appId);
+        model.addAttribute("app", app);
+        return "/app/config/app/detail";
+    }
+}

+ 85 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/controller/page/BuildDeployPageController.java

@@ -0,0 +1,85 @@
+package cn.reghao.oss.web.app.controller.page;
+
+import cn.reghao.oss.web.util.DefaultSetting;
+import cn.reghao.oss.web.util.db.PageSort;
+import cn.reghao.jutil.jdk.db.PageList;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.*;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * @author reghao
+ * @date 2019-08-30 18:49:15
+ */
+@Slf4j
+@Api(tags = "构建部署页面")
+@Controller
+@RequestMapping("/app/bd")
+public class BuildDeployPageController {
+    @ApiOperation(value = "应用构建页面")
+    @GetMapping(value = "/build")
+    public String index(@RequestParam(value = "env", required = false) String env,
+                        @RequestParam(value = "type", required = false) String type,
+                        @RequestParam(value = "appName", required = false) String appName, Model model) throws Exception {
+        if (env == null) {
+            env = DefaultSetting.getDefaultAppEnv();
+        }
+
+        if (type == null) {
+            type = DefaultSetting.getDefaultAppType();
+        }
+
+        PageRequest pageRequest = PageSort.pageRequest();
+        int pageNumber = pageRequest.getPageNumber()+1;
+        int pageSize = pageRequest.getPageSize();
+        model.addAttribute("env", env);
+        model.addAttribute("type", type);
+        model.addAttribute("page", Page.empty());
+        model.addAttribute("list", Collections.emptyList());
+        return "/app/bd/index";
+    }
+
+    @ApiOperation(value = "应用部署页面")
+    @GetMapping("/deploy")
+    public String deployPage(@RequestParam("appId") String appId,
+                             @RequestParam("buildLogId") String buildLogId,
+                             Model model) {
+        model.addAttribute("appId", appId);
+        model.addAttribute("buildLogId", buildLogId);
+        model.addAttribute("list", Collections.emptyList());
+        return "/app/bd/deploy";
+    }
+
+    @ApiOperation(value = "构建结果页面")
+    @GetMapping("/build/result")
+    public String buildLogResultPage(@RequestParam("buildLogId") String buildLogId, Model model) {
+        List<String> list = new ArrayList<>();
+        list.add("构建结果暂不可用");
+        model.addAttribute("list", list);
+        return "/app/bd/log/buildresult";
+    }
+
+    @ApiOperation(value = "历史构建页面")
+    @GetMapping("/build/history")
+    public String buildLogPage(@RequestParam("appId") String appId, Model model) {
+        return "/app/bd/log/buildlog";
+    }
+
+    @ApiOperation(value = "构建配置页面")
+    @GetMapping("/build/config")
+    public String buildConfigPage(@RequestParam("buildLogId") String buildLogId, Model model) {
+        return "/app/bd/log/buildconfig";
+    }
+
+    @ApiOperation(value = "构建耗时页面")
+    @GetMapping("/build/consumed")
+    public String buildTimePage(@RequestParam("buildLogId") String buildLogId, Model model) {
+        return "/app/bd/log/buildtime";
+    }
+}

+ 51 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/controller/page/StatusPageController.java

@@ -0,0 +1,51 @@
+package cn.reghao.oss.web.app.controller.page;
+
+import cn.reghao.oss.web.util.DefaultSetting;
+import cn.reghao.oss.web.util.db.PageSort;
+import cn.reghao.jutil.jdk.db.PageList;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+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.RequestParam;
+
+import java.util.*;
+
+/**
+ * @author reghao
+ * @date 2019-08-30 18:49:15
+ */
+@Slf4j
+@Api(tags = "应用状态页面")
+@Controller
+@RequestMapping("/app/stat")
+public class StatusPageController {
+    @ApiOperation(value = "应用运行状态列表页面")
+    @GetMapping
+    public String statusPage1(@RequestParam(value = "env", required = false) String env,
+                              @RequestParam(value = "appType", required = false) String appType,
+                              @RequestParam(value = "status", required = false) Boolean status,
+                              @RequestParam(value = "appName", required = false) String appName,
+                              Model model) {
+        model.addAttribute("env", env);
+        model.addAttribute("type", appType);
+        model.addAttribute("status", status);
+        model.addAttribute("page", Page.empty());
+        model.addAttribute("list", Collections.emptyList());
+        return "/app/stat/index1";
+    }
+
+    @ApiOperation(value = "应用运行状态详情页面")
+    @GetMapping(value = "/detail/{appId}")
+    public String statusPage2(@PathVariable(value = "appId") String appId, Model model) {
+        model.addAttribute("list", Collections.emptyList());
+        return "/app/stat/index2";
+    }
+}

+ 19 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/db/query/AppConfigQuery.java

@@ -0,0 +1,19 @@
+package cn.reghao.oss.web.app.db.query;
+
+import cn.reghao.oss.web.app.model.po.AppConfig;
+import cn.reghao.jutil.jdk.db.BaseQuery;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2021-06-02 15:01:18
+ */
+public interface AppConfigQuery extends BaseQuery<AppConfig> {
+    List<AppConfig> query(Map<String, String> kv);
+    AppConfig findByAppId(String appId);
+    Page<AppConfig> findByEnvAndType(String env, String type, Pageable pageable);
+}

+ 63 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/db/query/impl/AppConfigQueryImpl.java

@@ -0,0 +1,63 @@
+package cn.reghao.oss.web.app.db.query.impl;
+
+import cn.reghao.oss.web.app.db.query.AppConfigQuery;
+import cn.reghao.oss.web.app.db.repository.AppConfigRepository;
+import cn.reghao.oss.web.app.model.po.AppConfig;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+
+import javax.persistence.criteria.Predicate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author reghao
+ * @date 2021-06-02 15:01:18
+ */
+@Service
+public class AppConfigQueryImpl implements AppConfigQuery {
+    private final AppConfigRepository appRepository;
+
+    public AppConfigQueryImpl(AppConfigRepository appRepository) {
+        this.appRepository = appRepository;
+    }
+
+    @Override
+    public AppConfig findById(int id) {
+        return appRepository.getOne(id);
+    }
+
+    @Override
+    public List<AppConfig> query(Map<String, String> kv) {
+        Specification<AppConfig> specification = (root, query, cb) -> {
+            List<Predicate> predicates = new ArrayList<>();
+            kv.forEach((name, value) -> {
+                predicates.add(cb.like(root.get(name), "%" + value + "%"));
+            });
+
+            // select * from app_building where app_id like '%test%' and app_name like '%测试%'
+            return cb.and(predicates.toArray(new Predicate[0]));
+        };
+        return appRepository.findAll(specification);
+    }
+
+    //@Cacheable({"appId"})
+    @Override
+    public AppConfig findByAppId(String appId) {
+        return appRepository.findByDeletedFalseAndAppId(appId);
+    }
+
+    @Override
+    public Page<AppConfig> findByEnvAndType(String env, String type, Pageable pageable) {
+        return appRepository.findByEnvAndAppType(env, type, pageable);
+    }
+
+    //@Cacheable({"appId"})
+    @Override
+    public List<AppConfig> findAll() {
+        return appRepository.findAll();
+    }
+}

+ 21 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/db/repository/AppConfigRepository.java

@@ -0,0 +1,21 @@
+package cn.reghao.oss.web.app.db.repository;
+
+import cn.reghao.oss.web.app.model.po.AppConfig;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+import java.util.List;
+
+/**
+ *
+ * @author reghao
+ * @date 2020-01-21 14:53:03
+ */
+public interface AppConfigRepository extends JpaRepository<AppConfig, Integer>, JpaSpecificationExecutor<AppConfig> {
+    AppConfig findByDeletedFalseAndAppId(String appId);
+    List<AppConfig> findByAppRepoAndRepoBranch(String repo, String branch);
+    Page<AppConfig> findByEnv(String env, Pageable pageable);
+    Page<AppConfig> findByEnvAndAppType(String env, String type, Pageable pageable);
+}

+ 15 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/model/constant/AppType.java

@@ -0,0 +1,15 @@
+package cn.reghao.oss.web.app.model.constant;
+
+/**
+ * 应用类型列表
+ *
+ * @author reghao
+ * @date 2019-10-18 14:31:29
+ */
+public enum AppType {
+    maven, dotnetCore, npm;
+
+    public String getName() {
+        return this.name();
+    }
+}

+ 30 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/model/constant/BuildStatus.java

@@ -0,0 +1,30 @@
+package cn.reghao.oss.web.app.model.constant;
+
+/**
+ * 构建状态
+ *
+ * @author reghao
+ * @date 2021-11-08 16:35:42
+ */
+public enum BuildStatus {
+    neverBuild(1, "尚未构建"),
+    onBuilding(2, "正在构建"),
+    buildSuccess(3, "构建成功"),
+    buildFail(4, "构建失败");
+
+    private final Integer code;
+    private final String desc;
+
+    BuildStatus(Integer code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+}

+ 30 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/model/constant/DeployStatus.java

@@ -0,0 +1,30 @@
+package cn.reghao.oss.web.app.model.constant;
+
+/**
+ * 部署状态
+ *
+ * @author reghao
+ * @date 2021-11-08 16:35:42
+ */
+public enum DeployStatus {
+    neverDeploy(1, "尚未部署"),
+    onDeploying(2, "正在部署"),
+    deploySuccess(3, "部署成功"),
+    deployFail(4, "部署失败");
+
+    private final Integer code;
+    private final String desc;
+
+    DeployStatus(Integer code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+}

+ 15 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/model/constant/build/CompileType.java

@@ -0,0 +1,15 @@
+package cn.reghao.oss.web.app.model.constant.build;
+
+/**
+ * 编译方式类型
+ *
+ * @author reghao
+ * @date 2019-10-18 14:31:29
+ */
+public enum CompileType {
+    none, shell, maven;
+
+    public String getName() {
+        return this.name();
+    }
+}

+ 13 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/model/constant/build/RepoAuthType.java

@@ -0,0 +1,13 @@
+package cn.reghao.oss.web.app.model.constant.build;
+
+/**
+ * @author reghao
+ * @date 2021-02-05 18:50:01
+ */
+public enum RepoAuthType {
+    http, ssh, none;
+
+    public String getName() {
+        return this.name();
+    }
+}

+ 13 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/model/constant/build/RepoType.java

@@ -0,0 +1,13 @@
+package cn.reghao.oss.web.app.model.constant.build;
+
+/**
+ * @author reghao
+ * @date 2021-02-05 22:50:41
+ */
+public enum RepoType {
+    git;
+
+    public String getName() {
+        return this.name();
+    }
+}

+ 43 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/model/dto/AppConfigDto.java

@@ -0,0 +1,43 @@
+package cn.reghao.oss.web.app.model.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Pattern;
+
+/**
+ * @author reghao
+ * @date 2023-03-10 15:00:20
+ */
+@NoArgsConstructor
+@Setter
+@Getter
+public class AppConfigDto {
+    @Pattern(regexp = "^\\S*$", message = "应用 ID 不能包含空白符")
+    @Length(max = 20, message = "应用 ID 的最大长度为 20 个字符")
+    private String appId;
+    @NotBlank(message = "应用名字不能为空白符")
+    @Length(min = 2, max = 20, message = "应用名字的长度为 2 ~ 20 个字符")
+    private String appName;
+    private String appType;
+    @NotBlank(message = "必须指定应用环境")
+    private String env;
+    @NotBlank(message = "必须指定应用仓库")
+    private String appRepo;
+    @NotBlank(message = "必须指定仓库分支")
+    private String repoBranch;
+    private String appRootPath;
+    private String bindPorts;
+
+    // buildConfig
+    @NotBlank(message = "必须指定仓库认证")
+    private String repoAuthConfig;
+    @NotBlank(message = "必须指定编译配置")
+    private String compilerConfig;
+    @NotBlank(message = "必须指定打包配置")
+    private String packerConfig;
+    private String dockerfile;
+}

+ 33 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/model/dto/AppConfigUpdateDto.java

@@ -0,0 +1,33 @@
+package cn.reghao.oss.web.app.model.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * @author reghao
+ * @date 2023-03-10 15:00:20
+ */
+@NoArgsConstructor
+@Setter
+@Getter
+public class AppConfigUpdateDto {
+    private String appId;
+    @NotBlank(message = "必须指定应用名字")
+    private String appName;
+    @NotBlank(message = "必须指定仓库分支")
+    private String repoBranch;
+    private String appRootPath;
+    private String bindPorts;
+
+    // buildConfig
+    @NotBlank(message = "必须指定仓库认证")
+    private String repoAuthConfig;
+    @NotBlank(message = "必须指定编译配置")
+    private String compilerConfig;
+    @NotBlank(message = "必须指定打包配置")
+    private String packerConfig;
+    private String dockerfile;
+}

+ 42 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/model/dto/CopyAppDto.java

@@ -0,0 +1,42 @@
+package cn.reghao.oss.web.app.model.dto;
+
+import cn.reghao.oss.web.app.model.po.AppConfig;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Pattern;
+import java.io.Serializable;
+
+/**
+ * @author reghao
+ * @date 2021-06-03 16:15:23
+ */
+@Data
+public class CopyAppDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @Pattern(regexp = "^\\S*$", message = "应用 ID 不能包含空白符")
+    @Length(max = 20, message = "应用 ID 的最大长度为 20 个字符")
+    private String appId;
+    @Pattern(regexp = "^\\S*$", message = "应用 ID 不能包含空白符")
+    @Length(max = 20, message = "应用 ID 的最大长度为 20 个字符")
+    private String newAppId;
+    @NotBlank(message = "新应用环境不能为空白字符串")
+    @Length(max = 20, message = "新应用环境最大长度为 10 个字符")
+    private String newEnv;
+    @NotBlank(message = "新仓库分支不能为空白字符串")
+    @Length(max = 20, message = "新仓库分支最大长度为 40 个字符")
+    private String newRepoBranch;
+
+    public AppConfig app(AppConfig from) {
+        from.setAppId(newAppId);
+        from.setEnv(newEnv);
+        from.setRepoBranch(newRepoBranch);
+
+        from.setId(null);
+        from.setCreateTime(null);
+        from.setUpdateTime(null);
+        return from;
+    }
+}

+ 29 - 0
oss-web/src/main/java/cn/reghao/oss/web/app/model/dto/DeployConfigDto.java

@@ -0,0 +1,29 @@
+package cn.reghao.oss.web.app.model.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+/**
+ * 应用部署配置
+ *
+ * @author reghao
+ * @date 2020-05-13 16:59:20
+ */
+@NoArgsConstructor
+@Data
+public class DeployConfigDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @NotBlank(message = "appId 不能为空白字符串")
+    private String appId;
+    @NotBlank(message = "machineId 不能为空白字符串")
+    private String machineId;
+    private String machineIpv4;
+    private String packType;
+    private String startScript;
+    private String unpackScript;
+    private String startHome;
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов