Ver código fonte

Initial commit

classic_blue 3 meses atrás
commit
c39b440ed8
100 arquivos alterados com 9823 adições e 0 exclusões
  1. 26 0
      .gitignore
  2. 15 0
      README.md
  3. 115 0
      application-webadmin/pom.xml
  4. 23 0
      application-webadmin/src/main/java/com/serbia/webadmin/WebAdminApplication.java
  5. 179 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/controller/SerbiaCountryCodeController.java
  6. 189 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/controller/SerbiaGroupMembersController.java
  7. 315 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/controller/SerbiaGroupsController.java
  8. 197 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/controller/SerbiaMessagesController.java
  9. 311 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/controller/SerbiaUsersController.java
  10. 33 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dao/SerbiaCountryCodeMapper.java
  11. 33 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dao/SerbiaGroupMembersMapper.java
  12. 33 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dao/SerbiaGroupsMapper.java
  13. 33 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dao/SerbiaMessagesMapper.java
  14. 33 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dao/SerbiaUsersMapper.java
  15. 96 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dao/mapper/SerbiaCountryCodeMapper.xml
  16. 78 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dao/mapper/SerbiaGroupMembersMapper.xml
  17. 98 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dao/mapper/SerbiaGroupsMapper.xml
  18. 101 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dao/mapper/SerbiaMessagesMapper.xml
  19. 143 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dao/mapper/SerbiaUsersMapper.xml
  20. 69 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dto/SerbiaCountryCodeDto.java
  21. 51 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dto/SerbiaGroupMembersDto.java
  22. 91 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dto/SerbiaGroupsDto.java
  23. 95 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dto/SerbiaMessagesDto.java
  24. 143 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/dto/SerbiaUsersDto.java
  25. 67 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/model/SerbiaCountryCode.java
  26. 58 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/model/SerbiaGroupMembers.java
  27. 101 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/model/SerbiaGroups.java
  28. 121 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/model/SerbiaMessages.java
  29. 151 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/model/SerbiaUsers.java
  30. 44 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/model/constant/UserStatus.java
  31. 68 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/service/SerbiaCountryCodeService.java
  32. 68 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/service/SerbiaGroupMembersService.java
  33. 68 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/service/SerbiaGroupsService.java
  34. 68 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/service/SerbiaMessagesService.java
  35. 78 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/service/SerbiaUsersService.java
  36. 101 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/service/impl/SerbiaCountryCodeServiceImpl.java
  37. 116 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/service/impl/SerbiaGroupMembersServiceImpl.java
  38. 117 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/service/impl/SerbiaGroupsServiceImpl.java
  39. 122 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/service/impl/SerbiaMessagesServiceImpl.java
  40. 132 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/service/impl/SerbiaUsersServiceImpl.java
  41. 250 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/util/FlowIdentityExtHelper.java
  42. 60 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/vo/SerbiaCountryCodeVo.java
  43. 49 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/vo/SerbiaGroupMembersVo.java
  44. 55 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/vo/SerbiaGroupsVo.java
  45. 90 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/vo/SerbiaMessagesVo.java
  46. 117 0
      application-webadmin/src/main/java/com/serbia/webadmin/app/vo/SerbiaUsersVo.java
  47. 38 0
      application-webadmin/src/main/java/com/serbia/webadmin/config/ApplicationConfig.java
  48. 49 0
      application-webadmin/src/main/java/com/serbia/webadmin/config/DataSourceType.java
  49. 58 0
      application-webadmin/src/main/java/com/serbia/webadmin/config/FilterConfig.java
  50. 21 0
      application-webadmin/src/main/java/com/serbia/webadmin/config/InterceptorConfig.java
  51. 88 0
      application-webadmin/src/main/java/com/serbia/webadmin/config/MultiDataSourceConfig.java
  52. 66 0
      application-webadmin/src/main/java/com/serbia/webadmin/config/ThirdPartyAuthConfig.java
  53. 31 0
      application-webadmin/src/main/java/com/serbia/webadmin/config/WebSocketAutoConfig.java
  54. 281 0
      application-webadmin/src/main/java/com/serbia/webadmin/interceptor/AuthenticationInterceptor.java
  55. 55 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/bo/SysMenuExtraData.java
  56. 66 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/bo/SysMenuPerm.java
  57. 340 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/GlobalDictController.java
  58. 615 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/LoginController.java
  59. 89 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/LoginUserController.java
  60. 352 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/SysDataPermController.java
  61. 428 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/SysDeptController.java
  62. 231 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/SysMenuController.java
  63. 63 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/SysOperationLogController.java
  64. 183 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/SysPostController.java
  65. 331 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/SysRoleController.java
  66. 378 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/SysUserController.java
  67. 13 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysDataPermDeptMapper.java
  68. 43 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysDataPermMapper.java
  69. 13 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysDataPermMenuMapper.java
  70. 13 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysDataPermUserMapper.java
  71. 33 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysDeptMapper.java
  72. 33 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysDeptPostMapper.java
  73. 42 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysDeptRelationMapper.java
  74. 40 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysMenuMapper.java
  75. 13 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysPermWhitelistMapper.java
  76. 52 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysPostMapper.java
  77. 25 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysRoleMapper.java
  78. 13 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysRoleMenuMapper.java
  79. 188 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysUserMapper.java
  80. 13 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysUserPostMapper.java
  81. 13 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysUserRoleMapper.java
  82. 8 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysDataPermDeptMapper.xml
  83. 86 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysDataPermMapper.xml
  84. 8 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysDataPermMenuMapper.xml
  85. 8 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysDataPermUserMapper.xml
  86. 70 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysDeptMapper.xml
  87. 46 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysDeptPostMapper.xml
  88. 32 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysDeptRelationMapper.xml
  89. 58 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysMenuMapper.xml
  90. 9 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysPermWhitelistMapper.xml
  91. 80 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysPostMapper.xml
  92. 31 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysRoleMapper.xml
  93. 8 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysRoleMenuMapper.xml
  94. 294 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysUserMapper.xml
  95. 9 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysUserPostMapper.xml
  96. 8 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysUserRoleMapper.xml
  97. 27 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dto/SysDataPermDeptDto.java
  98. 55 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dto/SysDataPermDto.java
  99. 27 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dto/SysDataPermMenuDto.java
  100. 48 0
      application-webadmin/src/main/java/com/serbia/webadmin/upms/dto/SysDeptDto.java

+ 26 - 0
.gitignore

@@ -0,0 +1,26 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+/.mvn/*
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/build/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/

+ 15 - 0
README.md

@@ -0,0 +1,15 @@
+### 服务接口文档
+---
+- Knife4j
+  - 服务启动后,Knife4j的文档入口地址 [http://localhost:8082/doc.html#/plus](http://localhost:8082/doc.html#/plus)
+
+### 服务启动环境依赖
+---
+
+执行docker-compose up -d 命令启动下面依赖的服务。
+执行docker-compose down 命令停止下面服务。
+
+- Redis
+  - 版本:4以上版本
+  - 端口: 6379
+  - 推荐客户端工具 [AnotherRedisDesktopManager](https://github.com/qishibo/AnotherRedisDesktopManager)

+ 115 - 0
application-webadmin/pom.xml

@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<parent>
+        <groupId>com.serbia</groupId>
+        <artifactId>Serbia</artifactId>
+        <version>1.0.0</version>
+	</parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>application-webadmin</artifactId>
+    <version>1.0.0</version>
+    <name>application-webadmin</name>
+    <packaging>jar</packaging>
+
+	<dependencies>
+		<!-- 业务组件依赖 -->
+        <dependency>
+            <groupId>com.serbia</groupId>
+            <artifactId>common-satoken</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.serbia</groupId>
+            <artifactId>common-ext</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.serbia</groupId>
+            <artifactId>common-redis</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.serbia</groupId>
+            <artifactId>common-mobile</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.serbia</groupId>
+            <artifactId>common-report</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.serbia</groupId>
+            <artifactId>common-online</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.serbia</groupId>
+            <artifactId>common-flow-online</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.serbia</groupId>
+            <artifactId>common-log</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.serbia</groupId>
+            <artifactId>common-huaweicloud-obs</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.serbia</groupId>
+            <artifactId>common-sequence</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.serbia</groupId>
+            <artifactId>common-datafilter</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.serbia</groupId>
+            <artifactId>common-swagger</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.serbia</groupId>
+            <artifactId>common-dict</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.binarywang</groupId>
+            <artifactId>wx-java-miniapp-spring-boot-starter</artifactId>  <!-- 微信登录(小程序) -->
+            <version>4.6.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
+
+	</dependencies>
+
+
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring-boot.version}</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+		</plugins>
+	</build>
+</project>

+ 23 - 0
application-webadmin/src/main/java/com/serbia/webadmin/WebAdminApplication.java

@@ -0,0 +1,23 @@
+package com.serbia.webadmin;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+/**
+ * 应用服务启动类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@EnableAsync
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
+@ComponentScan("com.serbia")
+public class WebAdminApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(WebAdminApplication.class, args);
+	}
+}

+ 179 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/controller/SerbiaCountryCodeController.java

@@ -0,0 +1,179 @@
+package com.serbia.webadmin.app.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.serbia.common.log.annotation.OperationLog;
+import com.serbia.common.log.model.constant.SysOperationLogType;
+import com.github.pagehelper.page.PageMethod;
+import com.serbia.webadmin.app.vo.*;
+import com.serbia.webadmin.app.dto.*;
+import com.serbia.webadmin.app.model.*;
+import com.serbia.webadmin.app.service.*;
+import com.serbia.common.core.object.*;
+import com.serbia.common.core.util.*;
+import com.serbia.common.core.constant.*;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * 国家区域编码管理操作控制器类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Tag(name = "国家区域编码管理管理接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/app/serbiaCountryCode")
+public class SerbiaCountryCodeController {
+
+    @Autowired
+    private SerbiaCountryCodeService serbiaCountryCodeService;
+
+    /**
+     * 新增国家区域编码管理数据。
+     *
+     * @param serbiaCountryCodeDto 新增对象。
+     * @return 应答结果对象,包含新增对象主键Id。
+     */
+    @ApiOperationSupport(ignoreParameters = {"serbiaCountryCodeDto.id"})
+    @SaCheckPermission("serbiaCountryCode.add")
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/add")
+    public ResponseResult<Long> add(@MyRequestBody SerbiaCountryCodeDto serbiaCountryCodeDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(serbiaCountryCodeDto, false);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SerbiaCountryCode serbiaCountryCode = MyModelUtil.copyTo(serbiaCountryCodeDto, SerbiaCountryCode.class);
+        serbiaCountryCode = serbiaCountryCodeService.saveNew(serbiaCountryCode);
+        return ResponseResult.success(serbiaCountryCode.getId());
+    }
+
+    /**
+     * 更新国家区域编码管理数据。
+     *
+     * @param serbiaCountryCodeDto 更新对象。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("serbiaCountryCode.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/update")
+    public ResponseResult<Void> update(@MyRequestBody SerbiaCountryCodeDto serbiaCountryCodeDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(serbiaCountryCodeDto, true);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SerbiaCountryCode serbiaCountryCode = MyModelUtil.copyTo(serbiaCountryCodeDto, SerbiaCountryCode.class);
+        SerbiaCountryCode originalSerbiaCountryCode = serbiaCountryCodeService.getById(serbiaCountryCode.getId());
+        if (originalSerbiaCountryCode == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (!serbiaCountryCodeService.update(serbiaCountryCode, originalSerbiaCountryCode)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除国家区域编码管理数据。
+     *
+     * @param id 删除对象主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("serbiaCountryCode.delete")
+    @OperationLog(type = SysOperationLogType.DELETE)
+    @PostMapping("/delete")
+    public ResponseResult<Void> delete(@MyRequestBody Long id) {
+        if (MyCommonUtil.existBlankArgument(id)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        return this.doDelete(id);
+    }
+
+    /**
+     * 批量删除国家区域编码管理数据。
+     *
+     * @param idList 待删除对象的主键Id列表。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("serbiaCountryCode.delete")
+    @OperationLog(type = SysOperationLogType.DELETE_BATCH)
+    @PostMapping("/deleteBatch")
+    public ResponseResult<Void> deleteBatch(@MyRequestBody List<Long> idList) {
+        if (MyCommonUtil.existBlankArgument(idList)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        for (Long id : idList) {
+            ResponseResult<Void> responseResult = this.doDelete(id);
+            if (!responseResult.isSuccess()) {
+                return responseResult;
+            }
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 列出符合过滤条件的国家区域编码管理列表。
+     *
+     * @param serbiaCountryCodeDtoFilter 过滤对象。
+     * @param orderParam 排序参数。
+     * @param pageParam 分页参数。
+     * @return 应答结果对象,包含查询结果集。
+     */
+    @SaCheckPermission("serbiaCountryCode.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<SerbiaCountryCodeVo>> list(
+            @MyRequestBody SerbiaCountryCodeDto serbiaCountryCodeDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount());
+        }
+        SerbiaCountryCode serbiaCountryCodeFilter = MyModelUtil.copyTo(serbiaCountryCodeDtoFilter, SerbiaCountryCode.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SerbiaCountryCode.class);
+        List<SerbiaCountryCode> serbiaCountryCodeList =
+                serbiaCountryCodeService.getSerbiaCountryCodeListWithRelation(serbiaCountryCodeFilter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(serbiaCountryCodeList, SerbiaCountryCodeVo.class));
+    }
+
+    /**
+     * 查看指定国家区域编码管理对象详情。
+     *
+     * @param id 指定对象主键Id。
+     * @return 应答结果对象,包含对象详情。
+     */
+    @SaCheckPermission("serbiaCountryCode.view")
+    @GetMapping("/view")
+    public ResponseResult<SerbiaCountryCodeVo> view(@RequestParam Long id) {
+        SerbiaCountryCode serbiaCountryCode = serbiaCountryCodeService.getByIdWithRelation(id, MyRelationParam.full());
+        if (serbiaCountryCode == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        SerbiaCountryCodeVo serbiaCountryCodeVo = MyModelUtil.copyTo(serbiaCountryCode, SerbiaCountryCodeVo.class);
+        return ResponseResult.success(serbiaCountryCodeVo);
+    }
+
+    private ResponseResult<Void> doDelete(Long id) {
+        String errorMessage;
+        // 验证关联Id的数据合法性
+        SerbiaCountryCode originalSerbiaCountryCode = serbiaCountryCodeService.getById(id);
+        if (originalSerbiaCountryCode == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (!serbiaCountryCodeService.remove(id)) {
+            errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+}

+ 189 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/controller/SerbiaGroupMembersController.java

@@ -0,0 +1,189 @@
+package com.serbia.webadmin.app.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.serbia.common.log.annotation.OperationLog;
+import com.serbia.common.log.model.constant.SysOperationLogType;
+import com.github.pagehelper.page.PageMethod;
+import com.serbia.webadmin.app.vo.*;
+import com.serbia.webadmin.app.dto.*;
+import com.serbia.webadmin.app.model.*;
+import com.serbia.webadmin.app.service.*;
+import com.serbia.common.core.object.*;
+import com.serbia.common.core.util.*;
+import com.serbia.common.core.constant.*;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * 聊天群组成员管理操作控制器类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Tag(name = "聊天群组成员管理管理接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/app/serbiaGroupMembers")
+public class SerbiaGroupMembersController {
+
+    @Autowired
+    private SerbiaGroupMembersService serbiaGroupMembersService;
+
+    /**
+     * 新增聊天群组成员管理数据。
+     *
+     * @param serbiaGroupMembersDto 新增对象。
+     * @return 应答结果对象,包含新增对象主键Id。
+     */
+    @ApiOperationSupport(ignoreParameters = {"serbiaGroupMembersDto.id"})
+    @SaCheckPermission("serbiaGroupMembers.add")
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/add")
+    public ResponseResult<Long> add(@MyRequestBody SerbiaGroupMembersDto serbiaGroupMembersDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(serbiaGroupMembersDto, false);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SerbiaGroupMembers serbiaGroupMembers = MyModelUtil.copyTo(serbiaGroupMembersDto, SerbiaGroupMembers.class);
+        // 验证关联Id的数据合法性
+        CallResult callResult = serbiaGroupMembersService.verifyRelatedData(serbiaGroupMembers, null);
+        if (!callResult.isSuccess()) {
+            return ResponseResult.errorFrom(callResult);
+        }
+        serbiaGroupMembers = serbiaGroupMembersService.saveNew(serbiaGroupMembers);
+        return ResponseResult.success(serbiaGroupMembers.getId());
+    }
+
+    /**
+     * 更新聊天群组成员管理数据。
+     *
+     * @param serbiaGroupMembersDto 更新对象。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("serbiaGroupMembers.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/update")
+    public ResponseResult<Void> update(@MyRequestBody SerbiaGroupMembersDto serbiaGroupMembersDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(serbiaGroupMembersDto, true);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SerbiaGroupMembers serbiaGroupMembers = MyModelUtil.copyTo(serbiaGroupMembersDto, SerbiaGroupMembers.class);
+        SerbiaGroupMembers originalSerbiaGroupMembers = serbiaGroupMembersService.getById(serbiaGroupMembers.getId());
+        if (originalSerbiaGroupMembers == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        // 验证关联Id的数据合法性
+        CallResult callResult = serbiaGroupMembersService.verifyRelatedData(serbiaGroupMembers, originalSerbiaGroupMembers);
+        if (!callResult.isSuccess()) {
+            return ResponseResult.errorFrom(callResult);
+        }
+        if (!serbiaGroupMembersService.update(serbiaGroupMembers, originalSerbiaGroupMembers)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除聊天群组成员管理数据。
+     *
+     * @param id 删除对象主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("serbiaGroupMembers.delete")
+    @OperationLog(type = SysOperationLogType.DELETE)
+    @PostMapping("/delete")
+    public ResponseResult<Void> delete(@MyRequestBody Long id) {
+        if (MyCommonUtil.existBlankArgument(id)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        return this.doDelete(id);
+    }
+
+    /**
+     * 批量删除聊天群组成员管理数据。
+     *
+     * @param idList 待删除对象的主键Id列表。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("serbiaGroupMembers.delete")
+    @OperationLog(type = SysOperationLogType.DELETE_BATCH)
+    @PostMapping("/deleteBatch")
+    public ResponseResult<Void> deleteBatch(@MyRequestBody List<Long> idList) {
+        if (MyCommonUtil.existBlankArgument(idList)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        for (Long id : idList) {
+            ResponseResult<Void> responseResult = this.doDelete(id);
+            if (!responseResult.isSuccess()) {
+                return responseResult;
+            }
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 列出符合过滤条件的聊天群组成员管理列表。
+     *
+     * @param serbiaGroupMembersDtoFilter 过滤对象。
+     * @param orderParam 排序参数。
+     * @param pageParam 分页参数。
+     * @return 应答结果对象,包含查询结果集。
+     */
+    @SaCheckPermission("serbiaGroupMembers.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<SerbiaGroupMembersVo>> list(
+            @MyRequestBody SerbiaGroupMembersDto serbiaGroupMembersDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount());
+        }
+        SerbiaGroupMembers serbiaGroupMembersFilter = MyModelUtil.copyTo(serbiaGroupMembersDtoFilter, SerbiaGroupMembers.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SerbiaGroupMembers.class);
+        List<SerbiaGroupMembers> serbiaGroupMembersList =
+                serbiaGroupMembersService.getSerbiaGroupMembersListWithRelation(serbiaGroupMembersFilter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(serbiaGroupMembersList, SerbiaGroupMembersVo.class));
+    }
+
+    /**
+     * 查看指定聊天群组成员管理对象详情。
+     *
+     * @param id 指定对象主键Id。
+     * @return 应答结果对象,包含对象详情。
+     */
+    @SaCheckPermission("serbiaGroupMembers.view")
+    @GetMapping("/view")
+    public ResponseResult<SerbiaGroupMembersVo> view(@RequestParam Long id) {
+        SerbiaGroupMembers serbiaGroupMembers = serbiaGroupMembersService.getByIdWithRelation(id, MyRelationParam.full());
+        if (serbiaGroupMembers == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        SerbiaGroupMembersVo serbiaGroupMembersVo = MyModelUtil.copyTo(serbiaGroupMembers, SerbiaGroupMembersVo.class);
+        return ResponseResult.success(serbiaGroupMembersVo);
+    }
+
+    private ResponseResult<Void> doDelete(Long id) {
+        String errorMessage;
+        // 验证关联Id的数据合法性
+        SerbiaGroupMembers originalSerbiaGroupMembers = serbiaGroupMembersService.getById(id);
+        if (originalSerbiaGroupMembers == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (!serbiaGroupMembersService.remove(id)) {
+            errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+}

+ 315 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/controller/SerbiaGroupsController.java

@@ -0,0 +1,315 @@
+package com.serbia.webadmin.app.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.hutool.core.util.ReflectUtil;
+import com.serbia.common.core.upload.BaseUpDownloader;
+import com.serbia.common.core.upload.UpDownloaderFactory;
+import com.serbia.common.core.upload.UploadResponseInfo;
+import com.serbia.common.core.upload.UploadStoreInfo;
+import com.serbia.common.log.annotation.OperationLog;
+import com.serbia.common.log.model.constant.SysOperationLogType;
+import com.github.pagehelper.page.PageMethod;
+import com.serbia.webadmin.app.vo.*;
+import com.serbia.webadmin.app.dto.*;
+import com.serbia.webadmin.app.model.*;
+import com.serbia.webadmin.app.service.*;
+import com.serbia.common.core.object.*;
+import com.serbia.common.core.util.*;
+import com.serbia.common.core.constant.*;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.serbia.common.redis.cache.SessionCacheHelper;
+import com.serbia.webadmin.config.ApplicationConfig;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * 聊天群组管理操作控制器类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Tag(name = "聊天群组管理管理接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/app/serbiaGroups")
+public class SerbiaGroupsController {
+
+    @Autowired
+    private ApplicationConfig appConfig;
+    @Autowired
+    private SessionCacheHelper cacheHelper;
+    @Autowired
+    private UpDownloaderFactory upDownloaderFactory;
+    @Autowired
+    private SerbiaGroupsService serbiaGroupsService;
+
+    /**
+     * 新增聊天群组管理数据。
+     *
+     * @param serbiaGroupsDto 新增对象。
+     * @return 应答结果对象,包含新增对象主键Id。
+     */
+    @ApiOperationSupport(ignoreParameters = {
+            "serbiaGroupsDto.groupId",
+            "serbiaGroupsDto.searchString",
+            "serbiaGroupsDto.createTimeStart",
+            "serbiaGroupsDto.createTimeEnd",
+            "serbiaGroupsDto.updateTimeStart",
+            "serbiaGroupsDto.updateTimeEnd"})
+    @SaCheckPermission("serbiaGroups.add")
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/add")
+    public ResponseResult<Long> add(@MyRequestBody SerbiaGroupsDto serbiaGroupsDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(serbiaGroupsDto, false);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SerbiaGroups serbiaGroups = MyModelUtil.copyTo(serbiaGroupsDto, SerbiaGroups.class);
+        // 验证关联Id的数据合法性
+        CallResult callResult = serbiaGroupsService.verifyRelatedData(serbiaGroups, null);
+        if (!callResult.isSuccess()) {
+            return ResponseResult.errorFrom(callResult);
+        }
+        serbiaGroups = serbiaGroupsService.saveNew(serbiaGroups);
+        return ResponseResult.success(serbiaGroups.getGroupId());
+    }
+
+    /**
+     * 更新聊天群组管理数据。
+     *
+     * @param serbiaGroupsDto 更新对象。
+     * @return 应答结果对象。
+     */
+    @ApiOperationSupport(ignoreParameters = {
+            "serbiaGroupsDto.searchString",
+            "serbiaGroupsDto.createTimeStart",
+            "serbiaGroupsDto.createTimeEnd",
+            "serbiaGroupsDto.updateTimeStart",
+            "serbiaGroupsDto.updateTimeEnd"})
+    @SaCheckPermission("serbiaGroups.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/update")
+    public ResponseResult<Void> update(@MyRequestBody SerbiaGroupsDto serbiaGroupsDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(serbiaGroupsDto, true);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SerbiaGroups serbiaGroups = MyModelUtil.copyTo(serbiaGroupsDto, SerbiaGroups.class);
+        SerbiaGroups originalSerbiaGroups = serbiaGroupsService.getById(serbiaGroups.getGroupId());
+        if (originalSerbiaGroups == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        // 验证关联Id的数据合法性
+        CallResult callResult = serbiaGroupsService.verifyRelatedData(serbiaGroups, originalSerbiaGroups);
+        if (!callResult.isSuccess()) {
+            return ResponseResult.errorFrom(callResult);
+        }
+        if (!serbiaGroupsService.update(serbiaGroups, originalSerbiaGroups)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除聊天群组管理数据。
+     *
+     * @param groupId 删除对象主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("serbiaGroups.delete")
+    @OperationLog(type = SysOperationLogType.DELETE)
+    @PostMapping("/delete")
+    public ResponseResult<Void> delete(@MyRequestBody Long groupId) {
+        if (MyCommonUtil.existBlankArgument(groupId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        return this.doDelete(groupId);
+    }
+
+    /**
+     * 批量删除聊天群组管理数据。
+     *
+     * @param groupIdList 待删除对象的主键Id列表。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("serbiaGroups.delete")
+    @OperationLog(type = SysOperationLogType.DELETE_BATCH)
+    @PostMapping("/deleteBatch")
+    public ResponseResult<Void> deleteBatch(@MyRequestBody List<Long> groupIdList) {
+        if (MyCommonUtil.existBlankArgument(groupIdList)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        for (Long groupId : groupIdList) {
+            ResponseResult<Void> responseResult = this.doDelete(groupId);
+            if (!responseResult.isSuccess()) {
+                return responseResult;
+            }
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 列出符合过滤条件的聊天群组管理列表。
+     *
+     * @param serbiaGroupsDtoFilter 过滤对象。
+     * @param orderParam 排序参数。
+     * @param pageParam 分页参数。
+     * @return 应答结果对象,包含查询结果集。
+     */
+    @SaCheckPermission("serbiaGroups.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<SerbiaGroupsVo>> list(
+            @MyRequestBody SerbiaGroupsDto serbiaGroupsDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount());
+        }
+        SerbiaGroups serbiaGroupsFilter = MyModelUtil.copyTo(serbiaGroupsDtoFilter, SerbiaGroups.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SerbiaGroups.class);
+        List<SerbiaGroups> serbiaGroupsList =
+                serbiaGroupsService.getSerbiaGroupsListWithRelation(serbiaGroupsFilter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(serbiaGroupsList, SerbiaGroupsVo.class));
+    }
+
+    /**
+     * 查看指定聊天群组管理对象详情。
+     *
+     * @param groupId 指定对象主键Id。
+     * @return 应答结果对象,包含对象详情。
+     */
+    @SaCheckPermission("serbiaGroups.view")
+    @GetMapping("/view")
+    public ResponseResult<SerbiaGroupsVo> view(@RequestParam Long groupId) {
+        SerbiaGroups serbiaGroups = serbiaGroupsService.getByIdWithRelation(groupId, MyRelationParam.full());
+        if (serbiaGroups == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        SerbiaGroupsVo serbiaGroupsVo = MyModelUtil.copyTo(serbiaGroups, SerbiaGroupsVo.class);
+        return ResponseResult.success(serbiaGroupsVo);
+    }
+
+    /**
+     * 附件文件下载。
+     * 这里将图片和其他类型的附件文件放到不同的父目录下,主要为了便于今后图片文件的迁移。
+     *
+     * @param groupId 附件所在记录的主键Id。
+     * @param fieldName 附件所属的字段名。
+     * @param filename  文件名。如果没有提供该参数,就从当前记录的指定字段中读取。
+     * @param asImage   下载文件是否为图片。
+     * @param response  Http 应答对象。
+     */
+    @SaCheckPermission("serbiaGroups.view")
+    @OperationLog(type = SysOperationLogType.DOWNLOAD, saveResponse = false)
+    @GetMapping("/download")
+    public void download(
+            @RequestParam(required = false) Long groupId,
+            @RequestParam String fieldName,
+            @RequestParam String filename,
+            @RequestParam Boolean asImage,
+            HttpServletResponse response) {
+        if (MyCommonUtil.existBlankArgument(fieldName, filename, asImage)) {
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            return;
+        }
+        // 使用try来捕获异常,是为了保证一旦出现异常可以返回500的错误状态,便于调试。
+        // 否则有可能给前端返回的是200的错误码。
+        try {
+            // 如果请求参数中没有包含主键Id,就判断该文件是否为当前session上传的。
+            if (groupId == null) {
+                if (!cacheHelper.existSessionUploadFile(filename)) {
+                    ResponseResult.output(HttpServletResponse.SC_FORBIDDEN);
+                    return;
+                }
+            } else {
+                SerbiaGroups serbiaGroups = serbiaGroupsService.getById(groupId);
+                if (serbiaGroups == null) {
+                    ResponseResult.output(HttpServletResponse.SC_NOT_FOUND);
+                    return;
+                }
+                String fieldJsonData = (String) ReflectUtil.getFieldValue(serbiaGroups, fieldName);
+                if (fieldJsonData == null && !cacheHelper.existSessionUploadFile(filename)) {
+                    ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST);
+                    return;
+                }
+                if (!BaseUpDownloader.containFile(fieldJsonData, filename)
+                        && !cacheHelper.existSessionUploadFile(filename)) {
+                    ResponseResult.output(HttpServletResponse.SC_FORBIDDEN);
+                    return;
+                }
+            }
+            UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SerbiaGroups.class, fieldName);
+            if (!storeInfo.isSupportUpload()) {
+                ResponseResult.output(HttpServletResponse.SC_NOT_IMPLEMENTED,
+                        ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD));
+                return;
+            }
+            BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
+            upDownloader.doDownload(appConfig.getUploadFileBaseDir(),
+                    SerbiaGroups.class.getSimpleName(), fieldName, filename, asImage, response);
+        } catch (Exception e) {
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            log.error(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 文件上传操作。
+     *
+     * @param fieldName  上传文件名。
+     * @param asImage    是否作为图片上传。如果是图片,今后下载的时候无需权限验证。否则就是附件上传,下载时需要权限验证。
+     * @param uploadFile 上传文件对象。
+     */
+    @SaCheckPermission("serbiaGroups.view")
+    @OperationLog(type = SysOperationLogType.UPLOAD, saveResponse = false)
+    @PostMapping("/upload")
+    public void upload(
+            @RequestParam String fieldName,
+            @RequestParam Boolean asImage,
+            @RequestParam("uploadFile") MultipartFile uploadFile) throws IOException {
+        UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SerbiaGroups.class, fieldName);
+        // 这里就会判断参数中指定的字段,是否支持上传操作。
+        if (!storeInfo.isSupportUpload()) {
+            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN,
+                    ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD));
+            return;
+        }
+        // 根据字段注解中的存储类型,通过工厂方法获取匹配的上传下载实现类,从而解耦。
+        BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
+        UploadResponseInfo responseInfo = upDownloader.doUpload(null,
+                appConfig.getUploadFileBaseDir(), SerbiaGroups.class.getSimpleName(), fieldName, asImage, uploadFile);
+        if (Boolean.TRUE.equals(responseInfo.getUploadFailed())) {
+            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN,
+                    ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage()));
+            return;
+        }
+        cacheHelper.putSessionUploadFile(responseInfo.getFilename());
+        ResponseResult.output(ResponseResult.success(responseInfo));
+    }
+
+    private ResponseResult<Void> doDelete(Long groupId) {
+        String errorMessage;
+        // 验证关联Id的数据合法性
+        SerbiaGroups originalSerbiaGroups = serbiaGroupsService.getById(groupId);
+        if (originalSerbiaGroups == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (!serbiaGroupsService.remove(groupId)) {
+            errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+}

+ 197 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/controller/SerbiaMessagesController.java

@@ -0,0 +1,197 @@
+package com.serbia.webadmin.app.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.serbia.common.log.annotation.OperationLog;
+import com.serbia.common.log.model.constant.SysOperationLogType;
+import com.github.pagehelper.page.PageMethod;
+import com.serbia.webadmin.app.vo.*;
+import com.serbia.webadmin.app.dto.*;
+import com.serbia.webadmin.app.model.*;
+import com.serbia.webadmin.app.service.*;
+import com.serbia.common.core.object.*;
+import com.serbia.common.core.util.*;
+import com.serbia.common.core.constant.*;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * 聊天信息管理操作控制器类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Tag(name = "聊天信息管理管理接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/app/serbiaMessages")
+public class SerbiaMessagesController {
+
+    @Autowired
+    private SerbiaMessagesService serbiaMessagesService;
+
+    /**
+     * 新增聊天信息管理数据。
+     *
+     * @param serbiaMessagesDto 新增对象。
+     * @return 应答结果对象,包含新增对象主键Id。
+     */
+    @ApiOperationSupport(ignoreParameters = {
+            "serbiaMessagesDto.messageId",
+            "serbiaMessagesDto.searchString",
+            "serbiaMessagesDto.createTimeStart",
+            "serbiaMessagesDto.createTimeEnd"})
+    @SaCheckPermission("serbiaMessages.add")
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/add")
+    public ResponseResult<Long> add(@MyRequestBody SerbiaMessagesDto serbiaMessagesDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(serbiaMessagesDto, false);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SerbiaMessages serbiaMessages = MyModelUtil.copyTo(serbiaMessagesDto, SerbiaMessages.class);
+        // 验证关联Id的数据合法性
+        CallResult callResult = serbiaMessagesService.verifyRelatedData(serbiaMessages, null);
+        if (!callResult.isSuccess()) {
+            return ResponseResult.errorFrom(callResult);
+        }
+        serbiaMessages = serbiaMessagesService.saveNew(serbiaMessages);
+        return ResponseResult.success(serbiaMessages.getMessageId());
+    }
+
+    /**
+     * 更新聊天信息管理数据。
+     *
+     * @param serbiaMessagesDto 更新对象。
+     * @return 应答结果对象。
+     */
+    @ApiOperationSupport(ignoreParameters = {
+            "serbiaMessagesDto.searchString",
+            "serbiaMessagesDto.createTimeStart",
+            "serbiaMessagesDto.createTimeEnd"})
+    @SaCheckPermission("serbiaMessages.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/update")
+    public ResponseResult<Void> update(@MyRequestBody SerbiaMessagesDto serbiaMessagesDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(serbiaMessagesDto, true);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SerbiaMessages serbiaMessages = MyModelUtil.copyTo(serbiaMessagesDto, SerbiaMessages.class);
+        SerbiaMessages originalSerbiaMessages = serbiaMessagesService.getById(serbiaMessages.getMessageId());
+        if (originalSerbiaMessages == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        // 验证关联Id的数据合法性
+        CallResult callResult = serbiaMessagesService.verifyRelatedData(serbiaMessages, originalSerbiaMessages);
+        if (!callResult.isSuccess()) {
+            return ResponseResult.errorFrom(callResult);
+        }
+        if (!serbiaMessagesService.update(serbiaMessages, originalSerbiaMessages)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除聊天信息管理数据。
+     *
+     * @param messageId 删除对象主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("serbiaMessages.delete")
+    @OperationLog(type = SysOperationLogType.DELETE)
+    @PostMapping("/delete")
+    public ResponseResult<Void> delete(@MyRequestBody Long messageId) {
+        if (MyCommonUtil.existBlankArgument(messageId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        return this.doDelete(messageId);
+    }
+
+    /**
+     * 批量删除聊天信息管理数据。
+     *
+     * @param messageIdList 待删除对象的主键Id列表。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("serbiaMessages.delete")
+    @OperationLog(type = SysOperationLogType.DELETE_BATCH)
+    @PostMapping("/deleteBatch")
+    public ResponseResult<Void> deleteBatch(@MyRequestBody List<Long> messageIdList) {
+        if (MyCommonUtil.existBlankArgument(messageIdList)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        for (Long messageId : messageIdList) {
+            ResponseResult<Void> responseResult = this.doDelete(messageId);
+            if (!responseResult.isSuccess()) {
+                return responseResult;
+            }
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 列出符合过滤条件的聊天信息管理列表。
+     *
+     * @param serbiaMessagesDtoFilter 过滤对象。
+     * @param orderParam 排序参数。
+     * @param pageParam 分页参数。
+     * @return 应答结果对象,包含查询结果集。
+     */
+    @SaCheckPermission("serbiaMessages.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<SerbiaMessagesVo>> list(
+            @MyRequestBody SerbiaMessagesDto serbiaMessagesDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount());
+        }
+        SerbiaMessages serbiaMessagesFilter = MyModelUtil.copyTo(serbiaMessagesDtoFilter, SerbiaMessages.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SerbiaMessages.class);
+        List<SerbiaMessages> serbiaMessagesList =
+                serbiaMessagesService.getSerbiaMessagesListWithRelation(serbiaMessagesFilter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(serbiaMessagesList, SerbiaMessagesVo.class));
+    }
+
+    /**
+     * 查看指定聊天信息管理对象详情。
+     *
+     * @param messageId 指定对象主键Id。
+     * @return 应答结果对象,包含对象详情。
+     */
+    @SaCheckPermission("serbiaMessages.view")
+    @GetMapping("/view")
+    public ResponseResult<SerbiaMessagesVo> view(@RequestParam Long messageId) {
+        SerbiaMessages serbiaMessages = serbiaMessagesService.getByIdWithRelation(messageId, MyRelationParam.full());
+        if (serbiaMessages == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        SerbiaMessagesVo serbiaMessagesVo = MyModelUtil.copyTo(serbiaMessages, SerbiaMessagesVo.class);
+        return ResponseResult.success(serbiaMessagesVo);
+    }
+
+    private ResponseResult<Void> doDelete(Long messageId) {
+        String errorMessage;
+        // 验证关联Id的数据合法性
+        SerbiaMessages originalSerbiaMessages = serbiaMessagesService.getById(messageId);
+        if (originalSerbiaMessages == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (!serbiaMessagesService.remove(messageId)) {
+            errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+}

+ 311 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/controller/SerbiaUsersController.java

@@ -0,0 +1,311 @@
+package com.serbia.webadmin.app.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.hutool.core.util.ReflectUtil;
+import com.serbia.common.core.upload.BaseUpDownloader;
+import com.serbia.common.core.upload.UpDownloaderFactory;
+import com.serbia.common.core.upload.UploadResponseInfo;
+import com.serbia.common.core.upload.UploadStoreInfo;
+import com.serbia.common.log.annotation.OperationLog;
+import com.serbia.common.log.model.constant.SysOperationLogType;
+import com.github.pagehelper.page.PageMethod;
+import com.serbia.webadmin.app.vo.*;
+import com.serbia.webadmin.app.dto.*;
+import com.serbia.webadmin.app.model.*;
+import com.serbia.webadmin.app.service.*;
+import com.serbia.common.core.object.*;
+import com.serbia.common.core.util.*;
+import com.serbia.common.core.constant.*;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.serbia.common.redis.cache.SessionCacheHelper;
+import com.serbia.webadmin.config.ApplicationConfig;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * 塞尔用户管理操作控制器类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Tag(name = "塞尔用户管理管理接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/app/serbiaUsers")
+public class SerbiaUsersController {
+
+    @Autowired
+    private ApplicationConfig appConfig;
+    @Autowired
+    private SessionCacheHelper cacheHelper;
+    @Autowired
+    private UpDownloaderFactory upDownloaderFactory;
+    @Autowired
+    private SerbiaUsersService serbiaUsersService;
+
+    /**
+     * 新增塞尔用户管理数据。
+     *
+     * @param serbiaUsersDto 新增对象。
+     * @return 应答结果对象,包含新增对象主键Id。
+     */
+    @ApiOperationSupport(ignoreParameters = {
+            "serbiaUsersDto.userId",
+            "serbiaUsersDto.searchString",
+            "serbiaUsersDto.createTimeStart",
+            "serbiaUsersDto.createTimeEnd"})
+    @SaCheckPermission("serbiaUsers.add")
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/add")
+    public ResponseResult<Long> add(@MyRequestBody SerbiaUsersDto serbiaUsersDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(serbiaUsersDto, false);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SerbiaUsers serbiaUsers = MyModelUtil.copyTo(serbiaUsersDto, SerbiaUsers.class);
+        // 验证关联Id的数据合法性
+        CallResult callResult = serbiaUsersService.verifyRelatedData(serbiaUsers, null);
+        if (!callResult.isSuccess()) {
+            return ResponseResult.errorFrom(callResult);
+        }
+        serbiaUsers = serbiaUsersService.saveNew(serbiaUsers);
+        return ResponseResult.success(serbiaUsers.getUserId());
+    }
+
+    /**
+     * 更新塞尔用户管理数据。
+     *
+     * @param serbiaUsersDto 更新对象。
+     * @return 应答结果对象。
+     */
+    @ApiOperationSupport(ignoreParameters = {
+            "serbiaUsersDto.searchString",
+            "serbiaUsersDto.createTimeStart",
+            "serbiaUsersDto.createTimeEnd"})
+    @SaCheckPermission("serbiaUsers.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/update")
+    public ResponseResult<Void> update(@MyRequestBody SerbiaUsersDto serbiaUsersDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(serbiaUsersDto, true);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SerbiaUsers serbiaUsers = MyModelUtil.copyTo(serbiaUsersDto, SerbiaUsers.class);
+        SerbiaUsers originalSerbiaUsers = serbiaUsersService.getById(serbiaUsers.getUserId());
+        if (originalSerbiaUsers == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        // 验证关联Id的数据合法性
+        CallResult callResult = serbiaUsersService.verifyRelatedData(serbiaUsers, originalSerbiaUsers);
+        if (!callResult.isSuccess()) {
+            return ResponseResult.errorFrom(callResult);
+        }
+        if (!serbiaUsersService.update(serbiaUsers, originalSerbiaUsers)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除塞尔用户管理数据。
+     *
+     * @param userId 删除对象主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("serbiaUsers.delete")
+    @OperationLog(type = SysOperationLogType.DELETE)
+    @PostMapping("/delete")
+    public ResponseResult<Void> delete(@MyRequestBody Long userId) {
+        if (MyCommonUtil.existBlankArgument(userId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        return this.doDelete(userId);
+    }
+
+    /**
+     * 批量删除塞尔用户管理数据。
+     *
+     * @param userIdList 待删除对象的主键Id列表。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("serbiaUsers.delete")
+    @OperationLog(type = SysOperationLogType.DELETE_BATCH)
+    @PostMapping("/deleteBatch")
+    public ResponseResult<Void> deleteBatch(@MyRequestBody List<Long> userIdList) {
+        if (MyCommonUtil.existBlankArgument(userIdList)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        for (Long userId : userIdList) {
+            ResponseResult<Void> responseResult = this.doDelete(userId);
+            if (!responseResult.isSuccess()) {
+                return responseResult;
+            }
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 列出符合过滤条件的塞尔用户管理列表。
+     *
+     * @param serbiaUsersDtoFilter 过滤对象。
+     * @param orderParam 排序参数。
+     * @param pageParam 分页参数。
+     * @return 应答结果对象,包含查询结果集。
+     */
+    @SaCheckPermission("serbiaUsers.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<SerbiaUsersVo>> list(
+            @MyRequestBody SerbiaUsersDto serbiaUsersDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount());
+        }
+        SerbiaUsers serbiaUsersFilter = MyModelUtil.copyTo(serbiaUsersDtoFilter, SerbiaUsers.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SerbiaUsers.class);
+        List<SerbiaUsers> serbiaUsersList =
+                serbiaUsersService.getSerbiaUsersListWithRelation(serbiaUsersFilter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(serbiaUsersList, SerbiaUsersVo.class));
+    }
+
+    /**
+     * 查看指定塞尔用户管理对象详情。
+     *
+     * @param userId 指定对象主键Id。
+     * @return 应答结果对象,包含对象详情。
+     */
+    @SaCheckPermission("serbiaUsers.view")
+    @GetMapping("/view")
+    public ResponseResult<SerbiaUsersVo> view(@RequestParam Long userId) {
+        SerbiaUsers serbiaUsers = serbiaUsersService.getByIdWithRelation(userId, MyRelationParam.full());
+        if (serbiaUsers == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        SerbiaUsersVo serbiaUsersVo = MyModelUtil.copyTo(serbiaUsers, SerbiaUsersVo.class);
+        return ResponseResult.success(serbiaUsersVo);
+    }
+
+    /**
+     * 附件文件下载。
+     * 这里将图片和其他类型的附件文件放到不同的父目录下,主要为了便于今后图片文件的迁移。
+     *
+     * @param userId 附件所在记录的主键Id。
+     * @param fieldName 附件所属的字段名。
+     * @param filename  文件名。如果没有提供该参数,就从当前记录的指定字段中读取。
+     * @param asImage   下载文件是否为图片。
+     * @param response  Http 应答对象。
+     */
+    @SaCheckPermission("serbiaUsers.view")
+    @OperationLog(type = SysOperationLogType.DOWNLOAD, saveResponse = false)
+    @GetMapping("/download")
+    public void download(
+            @RequestParam(required = false) Long userId,
+            @RequestParam String fieldName,
+            @RequestParam String filename,
+            @RequestParam Boolean asImage,
+            HttpServletResponse response) {
+        if (MyCommonUtil.existBlankArgument(fieldName, filename, asImage)) {
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            return;
+        }
+        // 使用try来捕获异常,是为了保证一旦出现异常可以返回500的错误状态,便于调试。
+        // 否则有可能给前端返回的是200的错误码。
+        try {
+            // 如果请求参数中没有包含主键Id,就判断该文件是否为当前session上传的。
+            if (userId == null) {
+                if (!cacheHelper.existSessionUploadFile(filename)) {
+                    ResponseResult.output(HttpServletResponse.SC_FORBIDDEN);
+                    return;
+                }
+            } else {
+                SerbiaUsers serbiaUsers = serbiaUsersService.getById(userId);
+                if (serbiaUsers == null) {
+                    ResponseResult.output(HttpServletResponse.SC_NOT_FOUND);
+                    return;
+                }
+                String fieldJsonData = (String) ReflectUtil.getFieldValue(serbiaUsers, fieldName);
+                if (fieldJsonData == null && !cacheHelper.existSessionUploadFile(filename)) {
+                    ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST);
+                    return;
+                }
+                if (!BaseUpDownloader.containFile(fieldJsonData, filename)
+                        && !cacheHelper.existSessionUploadFile(filename)) {
+                    ResponseResult.output(HttpServletResponse.SC_FORBIDDEN);
+                    return;
+                }
+            }
+            UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SerbiaUsers.class, fieldName);
+            if (!storeInfo.isSupportUpload()) {
+                ResponseResult.output(HttpServletResponse.SC_NOT_IMPLEMENTED,
+                        ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD));
+                return;
+            }
+            BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
+            upDownloader.doDownload(appConfig.getUploadFileBaseDir(),
+                    SerbiaUsers.class.getSimpleName(), fieldName, filename, asImage, response);
+        } catch (Exception e) {
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            log.error(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 文件上传操作。
+     *
+     * @param fieldName  上传文件名。
+     * @param asImage    是否作为图片上传。如果是图片,今后下载的时候无需权限验证。否则就是附件上传,下载时需要权限验证。
+     * @param uploadFile 上传文件对象。
+     */
+    @SaCheckPermission("serbiaUsers.view")
+    @OperationLog(type = SysOperationLogType.UPLOAD, saveResponse = false)
+    @PostMapping("/upload")
+    public void upload(
+            @RequestParam String fieldName,
+            @RequestParam Boolean asImage,
+            @RequestParam("uploadFile") MultipartFile uploadFile) throws IOException {
+        UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SerbiaUsers.class, fieldName);
+        // 这里就会判断参数中指定的字段,是否支持上传操作。
+        if (!storeInfo.isSupportUpload()) {
+            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN,
+                    ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD));
+            return;
+        }
+        // 根据字段注解中的存储类型,通过工厂方法获取匹配的上传下载实现类,从而解耦。
+        BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
+        UploadResponseInfo responseInfo = upDownloader.doUpload(null,
+                appConfig.getUploadFileBaseDir(), SerbiaUsers.class.getSimpleName(), fieldName, asImage, uploadFile);
+        if (Boolean.TRUE.equals(responseInfo.getUploadFailed())) {
+            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN,
+                    ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage()));
+            return;
+        }
+        cacheHelper.putSessionUploadFile(responseInfo.getFilename());
+        ResponseResult.output(ResponseResult.success(responseInfo));
+    }
+
+    private ResponseResult<Void> doDelete(Long userId) {
+        String errorMessage;
+        // 验证关联Id的数据合法性
+        SerbiaUsers originalSerbiaUsers = serbiaUsersService.getById(userId);
+        if (originalSerbiaUsers == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (!serbiaUsersService.remove(userId)) {
+            errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+}

+ 33 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dao/SerbiaCountryCodeMapper.java

@@ -0,0 +1,33 @@
+package com.serbia.webadmin.app.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.app.model.SerbiaCountryCode;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 国家区域编码管理数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SerbiaCountryCodeMapper extends BaseDaoMapper<SerbiaCountryCode> {
+
+    /**
+     * 批量插入对象列表。
+     *
+     * @param serbiaCountryCodeList 新增对象列表。
+     */
+    void insertList(List<SerbiaCountryCode> serbiaCountryCodeList);
+
+    /**
+     * 获取过滤后的对象列表。
+     *
+     * @param serbiaCountryCodeFilter 主表过滤对象。
+     * @param orderBy 排序字符串,order by从句的参数。
+     * @return 对象列表。
+     */
+    List<SerbiaCountryCode> getSerbiaCountryCodeList(
+            @Param("serbiaCountryCodeFilter") SerbiaCountryCode serbiaCountryCodeFilter, @Param("orderBy") String orderBy);
+}

+ 33 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dao/SerbiaGroupMembersMapper.java

@@ -0,0 +1,33 @@
+package com.serbia.webadmin.app.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.app.model.SerbiaGroupMembers;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 聊天群组成员管理数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SerbiaGroupMembersMapper extends BaseDaoMapper<SerbiaGroupMembers> {
+
+    /**
+     * 批量插入对象列表。
+     *
+     * @param serbiaGroupMembersList 新增对象列表。
+     */
+    void insertList(List<SerbiaGroupMembers> serbiaGroupMembersList);
+
+    /**
+     * 获取过滤后的对象列表。
+     *
+     * @param serbiaGroupMembersFilter 主表过滤对象。
+     * @param orderBy 排序字符串,order by从句的参数。
+     * @return 对象列表。
+     */
+    List<SerbiaGroupMembers> getSerbiaGroupMembersList(
+            @Param("serbiaGroupMembersFilter") SerbiaGroupMembers serbiaGroupMembersFilter, @Param("orderBy") String orderBy);
+}

+ 33 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dao/SerbiaGroupsMapper.java

@@ -0,0 +1,33 @@
+package com.serbia.webadmin.app.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.app.model.SerbiaGroups;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 聊天群组管理数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SerbiaGroupsMapper extends BaseDaoMapper<SerbiaGroups> {
+
+    /**
+     * 批量插入对象列表。
+     *
+     * @param serbiaGroupsList 新增对象列表。
+     */
+    void insertList(List<SerbiaGroups> serbiaGroupsList);
+
+    /**
+     * 获取过滤后的对象列表。
+     *
+     * @param serbiaGroupsFilter 主表过滤对象。
+     * @param orderBy 排序字符串,order by从句的参数。
+     * @return 对象列表。
+     */
+    List<SerbiaGroups> getSerbiaGroupsList(
+            @Param("serbiaGroupsFilter") SerbiaGroups serbiaGroupsFilter, @Param("orderBy") String orderBy);
+}

+ 33 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dao/SerbiaMessagesMapper.java

@@ -0,0 +1,33 @@
+package com.serbia.webadmin.app.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.app.model.SerbiaMessages;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 聊天信息管理数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SerbiaMessagesMapper extends BaseDaoMapper<SerbiaMessages> {
+
+    /**
+     * 批量插入对象列表。
+     *
+     * @param serbiaMessagesList 新增对象列表。
+     */
+    void insertList(List<SerbiaMessages> serbiaMessagesList);
+
+    /**
+     * 获取过滤后的对象列表。
+     *
+     * @param serbiaMessagesFilter 主表过滤对象。
+     * @param orderBy 排序字符串,order by从句的参数。
+     * @return 对象列表。
+     */
+    List<SerbiaMessages> getSerbiaMessagesList(
+            @Param("serbiaMessagesFilter") SerbiaMessages serbiaMessagesFilter, @Param("orderBy") String orderBy);
+}

+ 33 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dao/SerbiaUsersMapper.java

@@ -0,0 +1,33 @@
+package com.serbia.webadmin.app.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.app.model.SerbiaUsers;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 塞尔用户管理数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SerbiaUsersMapper extends BaseDaoMapper<SerbiaUsers> {
+
+    /**
+     * 批量插入对象列表。
+     *
+     * @param serbiaUsersList 新增对象列表。
+     */
+    void insertList(List<SerbiaUsers> serbiaUsersList);
+
+    /**
+     * 获取过滤后的对象列表。
+     *
+     * @param serbiaUsersFilter 主表过滤对象。
+     * @param orderBy 排序字符串,order by从句的参数。
+     * @return 对象列表。
+     */
+    List<SerbiaUsers> getSerbiaUsersList(
+            @Param("serbiaUsersFilter") SerbiaUsers serbiaUsersFilter, @Param("orderBy") String orderBy);
+}

+ 96 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dao/mapper/SerbiaCountryCodeMapper.xml

@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.app.dao.SerbiaCountryCodeMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.app.model.SerbiaCountryCode">
+        <id column="id" jdbcType="BIGINT" property="id"/>
+        <result column="english_name" jdbcType="VARCHAR" property="englishName"/>
+        <result column="country" jdbcType="VARCHAR" property="country"/>
+        <result column="country_code" jdbcType="VARCHAR" property="countryCode"/>
+        <result column="area_code" jdbcType="VARCHAR" property="areaCode"/>
+        <result column="pin_yin" jdbcType="VARCHAR" property="pinYin"/>
+        <result column="first_code" jdbcType="VARCHAR" property="firstCode"/>
+        <result column="data_state" jdbcType="TINYINT" property="dataState"/>
+        <result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
+        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
+        <result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
+        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
+    </resultMap>
+
+    <insert id="insertList">
+        INSERT INTO serbia_country_code
+            (id,
+            english_name,
+            country,
+            country_code,
+            area_code,
+            pin_yin,
+            first_code,
+            data_state,
+            create_user_id,
+            create_time,
+            update_user_id,
+            update_time)
+        VALUES
+        <foreach collection="list" index="index" item="item" separator="," >
+            (#{item.id},
+            #{item.englishName},
+            #{item.country},
+            #{item.countryCode},
+            #{item.areaCode},
+            #{item.pinYin},
+            #{item.firstCode},
+            #{item.dataState},
+            #{item.createUserId},
+            #{item.createTime},
+            #{item.updateUserId},
+            #{item.updateTime})
+        </foreach>
+    </insert>
+
+    <!-- 如果有逻辑删除字段过滤,请写到这里 -->
+    <sql id="filterRef">
+        <!-- 这里必须加上全包名,否则当filterRef被其他Mapper.xml包含引用的时候,就会调用Mapper.xml中的该SQL片段 -->
+        <include refid="com.serbia.webadmin.app.dao.SerbiaCountryCodeMapper.inputFilterRef"/>
+        AND serbia_country_code.data_state = ${@com.serbia.common.core.constant.GlobalDeletedFlag@NORMAL}
+    </sql>
+
+    <!-- 这里仅包含调用接口输入的主表过滤条件 -->
+    <sql id="inputFilterRef">
+        <if test="serbiaCountryCodeFilter != null">
+            <if test="serbiaCountryCodeFilter.id != null">
+                AND serbia_country_code.id = #{serbiaCountryCodeFilter.id}
+            </if>
+            <if test="serbiaCountryCodeFilter.englishName != null and serbiaCountryCodeFilter.englishName != ''">
+                AND serbia_country_code.english_name = #{serbiaCountryCodeFilter.englishName}
+            </if>
+            <if test="serbiaCountryCodeFilter.country != null and serbiaCountryCodeFilter.country != ''">
+                AND serbia_country_code.country = #{serbiaCountryCodeFilter.country}
+            </if>
+            <if test="serbiaCountryCodeFilter.countryCode != null and serbiaCountryCodeFilter.countryCode != ''">
+                AND serbia_country_code.country_code = #{serbiaCountryCodeFilter.countryCode}
+            </if>
+            <if test="serbiaCountryCodeFilter.areaCode != null and serbiaCountryCodeFilter.areaCode != ''">
+                AND serbia_country_code.area_code = #{serbiaCountryCodeFilter.areaCode}
+            </if>
+            <if test="serbiaCountryCodeFilter.pinYin != null and serbiaCountryCodeFilter.pinYin != ''">
+                AND serbia_country_code.pin_yin = #{serbiaCountryCodeFilter.pinYin}
+            </if>
+            <if test="serbiaCountryCodeFilter.firstCode != null and serbiaCountryCodeFilter.firstCode != ''">
+                AND serbia_country_code.first_code = #{serbiaCountryCodeFilter.firstCode}
+            </if>
+            <if test="serbiaCountryCodeFilter.dataState != null">
+                AND serbia_country_code.data_state = #{serbiaCountryCodeFilter.dataState}
+            </if>
+        </if>
+    </sql>
+
+    <select id="getSerbiaCountryCodeList" resultMap="BaseResultMap" parameterType="com.serbia.webadmin.app.model.SerbiaCountryCode">
+        SELECT * FROM serbia_country_code
+        <where>
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+</mapper>

+ 78 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dao/mapper/SerbiaGroupMembersMapper.xml

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.app.dao.SerbiaGroupMembersMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.app.model.SerbiaGroupMembers">
+        <id column="id" jdbcType="BIGINT" property="id"/>
+        <result column="group_id" jdbcType="BIGINT" property="groupId"/>
+        <result column="user_id" jdbcType="BIGINT" property="userId"/>
+        <result column="group_role" jdbcType="INTEGER" property="groupRole"/>
+        <result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
+        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
+        <result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
+        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
+        <result column="data_state" jdbcType="TINYINT" property="dataState"/>
+    </resultMap>
+
+    <insert id="insertList">
+        INSERT INTO serbia_group_members
+            (id,
+            group_id,
+            user_id,
+            group_role,
+            create_user_id,
+            create_time,
+            update_user_id,
+            update_time,
+            data_state)
+        VALUES
+        <foreach collection="list" index="index" item="item" separator="," >
+            (#{item.id},
+            #{item.groupId},
+            #{item.userId},
+            #{item.groupRole},
+            #{item.createUserId},
+            #{item.createTime},
+            #{item.updateUserId},
+            #{item.updateTime},
+            #{item.dataState})
+        </foreach>
+    </insert>
+
+    <!-- 如果有逻辑删除字段过滤,请写到这里 -->
+    <sql id="filterRef">
+        <!-- 这里必须加上全包名,否则当filterRef被其他Mapper.xml包含引用的时候,就会调用Mapper.xml中的该SQL片段 -->
+        <include refid="com.serbia.webadmin.app.dao.SerbiaGroupMembersMapper.inputFilterRef"/>
+        AND serbia_group_members.data_state = ${@com.serbia.common.core.constant.GlobalDeletedFlag@NORMAL}
+    </sql>
+
+    <!-- 这里仅包含调用接口输入的主表过滤条件 -->
+    <sql id="inputFilterRef">
+        <if test="serbiaGroupMembersFilter != null">
+            <if test="serbiaGroupMembersFilter.id != null">
+                AND serbia_group_members.id = #{serbiaGroupMembersFilter.id}
+            </if>
+            <if test="serbiaGroupMembersFilter.groupId != null">
+                AND serbia_group_members.group_id = #{serbiaGroupMembersFilter.groupId}
+            </if>
+            <if test="serbiaGroupMembersFilter.userId != null">
+                AND serbia_group_members.user_id = #{serbiaGroupMembersFilter.userId}
+            </if>
+            <if test="serbiaGroupMembersFilter.groupRole != null">
+                AND serbia_group_members.group_role = #{serbiaGroupMembersFilter.groupRole}
+            </if>
+            <if test="serbiaGroupMembersFilter.createUserId != null">
+                AND serbia_group_members.create_user_id = #{serbiaGroupMembersFilter.createUserId}
+            </if>
+        </if>
+    </sql>
+
+    <select id="getSerbiaGroupMembersList" resultMap="BaseResultMap" parameterType="com.serbia.webadmin.app.model.SerbiaGroupMembers">
+        SELECT * FROM serbia_group_members
+        <where>
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+</mapper>

+ 98 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dao/mapper/SerbiaGroupsMapper.xml

@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.app.dao.SerbiaGroupsMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.app.model.SerbiaGroups">
+        <id column="group_id" jdbcType="BIGINT" property="groupId"/>
+        <result column="group_owner_id" jdbcType="BIGINT" property="groupOwnerId"/>
+        <result column="group_name" jdbcType="VARCHAR" property="groupName"/>
+        <result column="group_avatar" jdbcType="VARCHAR" property="groupAvatar"/>
+        <result column="is_private" jdbcType="INTEGER" property="isPrivate"/>
+        <result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
+        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
+        <result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
+        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
+        <result column="data_state" jdbcType="TINYINT" property="dataState"/>
+    </resultMap>
+
+    <insert id="insertList">
+        INSERT INTO serbia_groups
+            (group_id,
+            group_owner_id,
+            group_name,
+            group_avatar,
+            is_private,
+            create_user_id,
+            create_time,
+            update_user_id,
+            update_time,
+            data_state)
+        VALUES
+        <foreach collection="list" index="index" item="item" separator="," >
+            (#{item.groupId},
+            #{item.groupOwnerId},
+            #{item.groupName},
+            #{item.groupAvatar},
+            #{item.isPrivate},
+            #{item.createUserId},
+            #{item.createTime},
+            #{item.updateUserId},
+            #{item.updateTime},
+            #{item.dataState})
+        </foreach>
+    </insert>
+
+    <!-- 如果有逻辑删除字段过滤,请写到这里 -->
+    <sql id="filterRef">
+        <!-- 这里必须加上全包名,否则当filterRef被其他Mapper.xml包含引用的时候,就会调用Mapper.xml中的该SQL片段 -->
+        <include refid="com.serbia.webadmin.app.dao.SerbiaGroupsMapper.inputFilterRef"/>
+        AND serbia_groups.data_state = ${@com.serbia.common.core.constant.GlobalDeletedFlag@NORMAL}
+    </sql>
+
+    <!-- 这里仅包含调用接口输入的主表过滤条件 -->
+    <sql id="inputFilterRef">
+        <if test="serbiaGroupsFilter != null">
+            <if test="serbiaGroupsFilter.groupId != null">
+                AND serbia_groups.group_id = #{serbiaGroupsFilter.groupId}
+            </if>
+            <if test="serbiaGroupsFilter.groupOwnerId != null">
+                AND serbia_groups.group_owner_id = #{serbiaGroupsFilter.groupOwnerId}
+            </if>
+            <if test="serbiaGroupsFilter.groupName != null and serbiaGroupsFilter.groupName != ''">
+                <bind name = "safeSerbiaGroupsGroupName" value = "'%' + serbiaGroupsFilter.groupName + '%'" />
+                AND serbia_groups.group_name LIKE #{safeSerbiaGroupsGroupName}
+            </if>
+            <if test="serbiaGroupsFilter.isPrivate != null">
+                AND serbia_groups.is_private = #{serbiaGroupsFilter.isPrivate}
+            </if>
+            <if test="serbiaGroupsFilter.createUserId != null">
+                AND serbia_groups.create_user_id = #{serbiaGroupsFilter.createUserId}
+            </if>
+            <if test="serbiaGroupsFilter.createTimeStart != null and serbiaGroupsFilter.createTimeStart != ''">
+                AND serbia_groups.create_time &gt;= #{serbiaGroupsFilter.createTimeStart}
+            </if>
+            <if test="serbiaGroupsFilter.createTimeEnd != null and serbiaGroupsFilter.createTimeEnd != ''">
+                AND serbia_groups.create_time &lt;= #{serbiaGroupsFilter.createTimeEnd}
+            </if>
+            <if test="serbiaGroupsFilter.updateTimeStart != null and serbiaGroupsFilter.updateTimeStart != ''">
+                AND serbia_groups.update_time &gt;= #{serbiaGroupsFilter.updateTimeStart}
+            </if>
+            <if test="serbiaGroupsFilter.updateTimeEnd != null and serbiaGroupsFilter.updateTimeEnd != ''">
+                AND serbia_groups.update_time &lt;= #{serbiaGroupsFilter.updateTimeEnd}
+            </if>
+            <if test="serbiaGroupsFilter.searchString != null and serbiaGroupsFilter.searchString != ''">
+                <bind name = "safeSerbiaGroupsSearchString" value = "'%' + serbiaGroupsFilter.searchString + '%'" />
+                AND IFNULL(serbia_groups.group_name,'') LIKE #{safeSerbiaGroupsSearchString}
+            </if>
+        </if>
+    </sql>
+
+    <select id="getSerbiaGroupsList" resultMap="BaseResultMap" parameterType="com.serbia.webadmin.app.model.SerbiaGroups">
+        SELECT * FROM serbia_groups
+        <where>
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+</mapper>

+ 101 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dao/mapper/SerbiaMessagesMapper.xml

@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.app.dao.SerbiaMessagesMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.app.model.SerbiaMessages">
+        <id column="message_id" jdbcType="BIGINT" property="messageId"/>
+        <result column="sender_id" jdbcType="BIGINT" property="senderId"/>
+        <result column="receiver_id" jdbcType="BIGINT" property="receiverId"/>
+        <result column="group_id" jdbcType="BIGINT" property="groupId"/>
+        <result column="messsage_type" jdbcType="INTEGER" property="messsageType"/>
+        <result column="message_content" jdbcType="LONGVARCHAR" property="messageContent"/>
+        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
+        <result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
+        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
+        <result column="data_state" jdbcType="TINYINT" property="dataState"/>
+        <result column="read_status" jdbcType="TINYINT" property="readStatus"/>
+    </resultMap>
+
+    <insert id="insertList">
+        INSERT INTO serbia_messages
+            (message_id,
+            sender_id,
+            receiver_id,
+            group_id,
+            messsage_type,
+            message_content,
+            create_time,
+            update_user_id,
+            update_time,
+            data_state,
+            read_status)
+        VALUES
+        <foreach collection="list" index="index" item="item" separator="," >
+            (#{item.messageId},
+            #{item.senderId},
+            #{item.receiverId},
+            #{item.groupId},
+            #{item.messsageType},
+            #{item.messageContent},
+            #{item.createTime},
+            #{item.updateUserId},
+            #{item.updateTime},
+            #{item.dataState},
+            #{item.readStatus})
+        </foreach>
+    </insert>
+
+    <!-- 如果有逻辑删除字段过滤,请写到这里 -->
+    <sql id="filterRef">
+        <!-- 这里必须加上全包名,否则当filterRef被其他Mapper.xml包含引用的时候,就会调用Mapper.xml中的该SQL片段 -->
+        <include refid="com.serbia.webadmin.app.dao.SerbiaMessagesMapper.inputFilterRef"/>
+        AND serbia_messages.data_state = ${@com.serbia.common.core.constant.GlobalDeletedFlag@NORMAL}
+    </sql>
+
+    <!-- 这里仅包含调用接口输入的主表过滤条件 -->
+    <sql id="inputFilterRef">
+        <if test="serbiaMessagesFilter != null">
+            <if test="serbiaMessagesFilter.messageId != null">
+                AND serbia_messages.message_id = #{serbiaMessagesFilter.messageId}
+            </if>
+            <if test="serbiaMessagesFilter.senderId != null">
+                AND serbia_messages.sender_id = #{serbiaMessagesFilter.senderId}
+            </if>
+            <if test="serbiaMessagesFilter.receiverId != null">
+                AND serbia_messages.receiver_id = #{serbiaMessagesFilter.receiverId}
+            </if>
+            <if test="serbiaMessagesFilter.groupId != null">
+                AND serbia_messages.group_id = #{serbiaMessagesFilter.groupId}
+            </if>
+            <if test="serbiaMessagesFilter.messsageType != null">
+                AND serbia_messages.messsage_type = #{serbiaMessagesFilter.messsageType}
+            </if>
+            <if test="serbiaMessagesFilter.messageContent != null and serbiaMessagesFilter.messageContent != ''">
+                <bind name = "safeSerbiaMessagesMessageContent" value = "'%' + serbiaMessagesFilter.messageContent + '%'" />
+                AND serbia_messages.message_content LIKE #{safeSerbiaMessagesMessageContent}
+            </if>
+            <if test="serbiaMessagesFilter.createTimeStart != null and serbiaMessagesFilter.createTimeStart != ''">
+                AND serbia_messages.create_time &gt;= #{serbiaMessagesFilter.createTimeStart}
+            </if>
+            <if test="serbiaMessagesFilter.createTimeEnd != null and serbiaMessagesFilter.createTimeEnd != ''">
+                AND serbia_messages.create_time &lt;= #{serbiaMessagesFilter.createTimeEnd}
+            </if>
+            <if test="serbiaMessagesFilter.readStatus != null">
+                AND serbia_messages.read_status = #{serbiaMessagesFilter.readStatus}
+            </if>
+            <if test="serbiaMessagesFilter.searchString != null and serbiaMessagesFilter.searchString != ''">
+                <bind name = "safeSerbiaMessagesSearchString" value = "'%' + serbiaMessagesFilter.searchString + '%'" />
+                AND IFNULL(serbia_messages.message_content,'') LIKE #{safeSerbiaMessagesSearchString}
+            </if>
+        </if>
+    </sql>
+
+    <select id="getSerbiaMessagesList" resultMap="BaseResultMap" parameterType="com.serbia.webadmin.app.model.SerbiaMessages">
+        SELECT * FROM serbia_messages
+        <where>
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+</mapper>

+ 143 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dao/mapper/SerbiaUsersMapper.xml

@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.app.dao.SerbiaUsersMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.app.model.SerbiaUsers">
+        <id column="user_id" jdbcType="BIGINT" property="userId"/>
+        <result column="login_name" jdbcType="VARCHAR" property="loginName"/>
+        <result column="password" jdbcType="VARCHAR" property="password"/>
+        <result column="show_name" jdbcType="VARCHAR" property="showName"/>
+        <result column="head_image_url" jdbcType="VARCHAR" property="headImageUrl"/>
+        <result column="user_status" jdbcType="INTEGER" property="userStatus"/>
+        <result column="email" jdbcType="VARCHAR" property="email"/>
+        <result column="mobile" jdbcType="VARCHAR" property="mobile"/>
+        <result column="sex" jdbcType="CHAR" property="sex"/>
+        <result column="birthday" jdbcType="DATE" property="birthday"/>
+        <result column="deleted_flag" jdbcType="INTEGER" property="deletedFlag"/>
+        <result column="country_code" jdbcType="VARCHAR" property="countryCode"/>
+        <result column="address" jdbcType="VARCHAR" property="address"/>
+        <result column="job" jdbcType="VARCHAR" property="job"/>
+        <result column="personal_sign" jdbcType="VARCHAR" property="personalSign"/>
+        <result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
+        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
+        <result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
+        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
+    </resultMap>
+
+    <insert id="insertList">
+        INSERT INTO serbia_users
+            (user_id,
+            login_name,
+            password,
+            show_name,
+            head_image_url,
+            user_status,
+            email,
+            mobile,
+            sex,
+            birthday,
+            deleted_flag,
+            country_code,
+            address,
+            job,
+            personal_sign,
+            create_user_id,
+            create_time,
+            update_user_id,
+            update_time)
+        VALUES
+        <foreach collection="list" index="index" item="item" separator="," >
+            (#{item.userId},
+            #{item.loginName},
+            #{item.password},
+            #{item.showName},
+            #{item.headImageUrl},
+            #{item.userStatus},
+            #{item.email},
+            #{item.mobile},
+            #{item.sex},
+            #{item.birthday},
+            #{item.deletedFlag},
+            #{item.countryCode},
+            #{item.address},
+            #{item.job},
+            #{item.personalSign},
+            #{item.createUserId},
+            #{item.createTime},
+            #{item.updateUserId},
+            #{item.updateTime})
+        </foreach>
+    </insert>
+
+    <!-- 如果有逻辑删除字段过滤,请写到这里 -->
+    <sql id="filterRef">
+        <!-- 这里必须加上全包名,否则当filterRef被其他Mapper.xml包含引用的时候,就会调用Mapper.xml中的该SQL片段 -->
+        <include refid="com.serbia.webadmin.app.dao.SerbiaUsersMapper.inputFilterRef"/>
+        AND serbia_users.deleted_flag = ${@com.serbia.common.core.constant.GlobalDeletedFlag@NORMAL}
+    </sql>
+
+    <!-- 这里仅包含调用接口输入的主表过滤条件 -->
+    <sql id="inputFilterRef">
+        <if test="serbiaUsersFilter != null">
+            <if test="serbiaUsersFilter.userId != null">
+                AND serbia_users.user_id = #{serbiaUsersFilter.userId}
+            </if>
+            <if test="serbiaUsersFilter.loginName != null and serbiaUsersFilter.loginName != ''">
+                <bind name = "safeSerbiaUsersLoginName" value = "'%' + serbiaUsersFilter.loginName + '%'" />
+                AND serbia_users.login_name LIKE #{safeSerbiaUsersLoginName}
+            </if>
+            <if test="serbiaUsersFilter.showName != null and serbiaUsersFilter.showName != ''">
+                <bind name = "safeSerbiaUsersShowName" value = "'%' + serbiaUsersFilter.showName + '%'" />
+                AND serbia_users.show_name LIKE #{safeSerbiaUsersShowName}
+            </if>
+            <if test="serbiaUsersFilter.userStatus != null">
+                AND serbia_users.user_status = #{serbiaUsersFilter.userStatus}
+            </if>
+            <if test="serbiaUsersFilter.email != null and serbiaUsersFilter.email != ''">
+                <bind name = "safeSerbiaUsersEmail" value = "'%' + serbiaUsersFilter.email + '%'" />
+                AND serbia_users.email LIKE #{safeSerbiaUsersEmail}
+            </if>
+            <if test="serbiaUsersFilter.mobile != null and serbiaUsersFilter.mobile != ''">
+                <bind name = "safeSerbiaUsersMobile" value = "'%' + serbiaUsersFilter.mobile + '%'" />
+                AND serbia_users.mobile LIKE #{safeSerbiaUsersMobile}
+            </if>
+            <if test="serbiaUsersFilter.sex != null and serbiaUsersFilter.sex != ''">
+                <bind name = "safeSerbiaUsersSex" value = "'%' + serbiaUsersFilter.sex + '%'" />
+                AND serbia_users.sex LIKE #{safeSerbiaUsersSex}
+            </if>
+            <if test="serbiaUsersFilter.deletedFlag != null">
+                AND serbia_users.deleted_flag = #{serbiaUsersFilter.deletedFlag}
+            </if>
+            <if test="serbiaUsersFilter.countryCode != null and serbiaUsersFilter.countryCode != ''">
+                AND serbia_users.country_code = #{serbiaUsersFilter.countryCode}
+            </if>
+            <if test="serbiaUsersFilter.address != null and serbiaUsersFilter.address != ''">
+                <bind name = "safeSerbiaUsersAddress" value = "'%' + serbiaUsersFilter.address + '%'" />
+                AND serbia_users.address LIKE #{safeSerbiaUsersAddress}
+            </if>
+            <if test="serbiaUsersFilter.job != null and serbiaUsersFilter.job != ''">
+                <bind name = "safeSerbiaUsersJob" value = "'%' + serbiaUsersFilter.job + '%'" />
+                AND serbia_users.job LIKE #{safeSerbiaUsersJob}
+            </if>
+            <if test="serbiaUsersFilter.createTimeStart != null and serbiaUsersFilter.createTimeStart != ''">
+                AND serbia_users.create_time &gt;= #{serbiaUsersFilter.createTimeStart}
+            </if>
+            <if test="serbiaUsersFilter.createTimeEnd != null and serbiaUsersFilter.createTimeEnd != ''">
+                AND serbia_users.create_time &lt;= #{serbiaUsersFilter.createTimeEnd}
+            </if>
+            <if test="serbiaUsersFilter.searchString != null and serbiaUsersFilter.searchString != ''">
+                <bind name = "safeSerbiaUsersSearchString" value = "'%' + serbiaUsersFilter.searchString + '%'" />
+                AND CONCAT(IFNULL(serbia_users.login_name,''), IFNULL(serbia_users.show_name,''), IFNULL(serbia_users.email,''), IFNULL(serbia_users.mobile,''), IFNULL(serbia_users.sex,''), IFNULL(serbia_users.address,''), IFNULL(serbia_users.job,'')) LIKE #{safeSerbiaUsersSearchString}
+            </if>
+        </if>
+    </sql>
+
+    <select id="getSerbiaUsersList" resultMap="BaseResultMap" parameterType="com.serbia.webadmin.app.model.SerbiaUsers">
+        SELECT * FROM serbia_users
+        <where>
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+</mapper>

+ 69 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dto/SerbiaCountryCodeDto.java

@@ -0,0 +1,69 @@
+package com.serbia.webadmin.app.dto;
+
+import com.serbia.common.core.validator.UpdateGroup;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+/**
+ * 国家区域编码管理Dto对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Schema(description = "国家区域编码管理Dto对象")
+@Data
+public class SerbiaCountryCodeDto {
+
+    /**
+     * 主键。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "主键。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,主键不能为空!", groups = {UpdateGroup.class})
+    private Long id;
+
+    /**
+     * 英文名称。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "英文名称。可支持等于操作符的列表数据过滤。")
+    private String englishName;
+
+    /**
+     * 国家/地区。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "国家/地区。可支持等于操作符的列表数据过滤。")
+    private String country;
+
+    /**
+     * 国家代码。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "国家代码。可支持等于操作符的列表数据过滤。")
+    private String countryCode;
+
+    /**
+     * 区号。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "区号。可支持等于操作符的列表数据过滤。")
+    private String areaCode;
+
+    /**
+     * 拼音。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "拼音。可支持等于操作符的列表数据过滤。")
+    private String pinYin;
+
+    /**
+     * 拼音首字母。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "拼音首字母。可支持等于操作符的列表数据过滤。")
+    private String firstCode;
+}

+ 51 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dto/SerbiaGroupMembersDto.java

@@ -0,0 +1,51 @@
+package com.serbia.webadmin.app.dto;
+
+import com.serbia.common.core.validator.UpdateGroup;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+/**
+ * 聊天群组成员管理Dto对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Schema(description = "聊天群组成员管理Dto对象")
+@Data
+public class SerbiaGroupMembersDto {
+
+    /**
+     * 主键id。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "主键id。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,主键id不能为空!", groups = {UpdateGroup.class})
+    private Long id;
+
+    /**
+     * 群聊id。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "群聊id。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,群聊id不能为空!")
+    private Long groupId;
+
+    /**
+     * 用户id。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "用户id。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,用户id不能为空!")
+    private Long userId;
+
+    /**
+     * 成员角色 1群主 2群成员。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "成员角色 1群主 2群成员。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,成员角色 1群主 2群成员不能为空!")
+    private Integer groupRole;
+}

+ 91 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dto/SerbiaGroupsDto.java

@@ -0,0 +1,91 @@
+package com.serbia.webadmin.app.dto;
+
+import com.serbia.common.core.validator.UpdateGroup;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+/**
+ * 聊天群组管理Dto对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Schema(description = "聊天群组管理Dto对象")
+@Data
+public class SerbiaGroupsDto {
+
+    /**
+     * 主键id。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "主键id。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,主键id不能为空!", groups = {UpdateGroup.class})
+    private Long groupId;
+
+    /**
+     * 群主id。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "群主id。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,群主id不能为空!")
+    private Long groupOwnerId;
+
+    /**
+     * 群聊名称。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "群聊名称。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,群聊名称不能为空!")
+    private String groupName;
+
+    /**
+     * 群聊头像。
+     */
+    @Schema(description = "群聊头像。")
+    private String groupAvatar;
+
+    /**
+     * 是否公开 0私密 1公开。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "是否私密 0公开 1私密。可支持等于操作符的列表数据过滤。")
+    private Integer isPrivate;
+
+    /**
+     * createTime 范围过滤起始值(>=)。
+     * NOTE: 可支持范围操作符的列表数据过滤。
+     */
+    @Schema(description = "createTime 范围过滤起始值(>=)。可支持范围操作符的列表数据过滤。")
+    private String createTimeStart;
+
+    /**
+     * createTime 范围过滤结束值(<=)。
+     * NOTE: 可支持范围操作符的列表数据过滤。
+     */
+    @Schema(description = "createTime 范围过滤结束值(<=)。可支持范围操作符的列表数据过滤。")
+    private String createTimeEnd;
+
+    /**
+     * updateTime 范围过滤起始值(>=)。
+     * NOTE: 可支持范围操作符的列表数据过滤。
+     */
+    @Schema(description = "updateTime 范围过滤起始值(>=)。可支持范围操作符的列表数据过滤。")
+    private String updateTimeStart;
+
+    /**
+     * updateTime 范围过滤结束值(<=)。
+     * NOTE: 可支持范围操作符的列表数据过滤。
+     */
+    @Schema(description = "updateTime 范围过滤结束值(<=)。可支持范围操作符的列表数据过滤。")
+    private String updateTimeEnd;
+
+    /**
+     * group_name LIKE搜索字符串。
+     * NOTE: 可支持LIKE操作符的列表数据过滤。
+     */
+    @Schema(description = "LIKE模糊搜索字符串")
+    private String searchString;
+}

+ 95 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dto/SerbiaMessagesDto.java

@@ -0,0 +1,95 @@
+package com.serbia.webadmin.app.dto;
+
+import com.serbia.common.core.validator.UpdateGroup;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+/**
+ * 聊天信息管理Dto对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Schema(description = "聊天信息管理Dto对象")
+@Data
+public class SerbiaMessagesDto {
+
+    /**
+     * 主键id。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "主键id。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,主键id不能为空!", groups = {UpdateGroup.class})
+    private Long messageId;
+
+    /**
+     * 发送人id。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "发送人id。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,发送人id不能为空!")
+    private Long senderId;
+
+    /**
+     * 接收者id。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "接收者id。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,接收者id不能为空!")
+    private Long receiverId;
+
+    /**
+     * 群组id。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "群组id。可支持等于操作符的列表数据过滤。")
+    private Long groupId;
+
+    /**
+     * 消息类型 1文字 2图片 3文件。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "消息类型 1文字 2图片 3文件。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,消息类型 1文字 2图片 3文件不能为空!")
+    private Integer messsageType;
+
+    /**
+     * 消息内容。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "消息内容。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,消息内容不能为空!")
+    private String messageContent;
+
+    /**
+     * 消息已读状态 0未读 1已读。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "消息已读状态 0未读 1已读。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,消息已读状态 0未读 1已读不能为空!", groups = {UpdateGroup.class})
+    private Integer readStatus;
+
+    /**
+     * createTime 范围过滤起始值(>=)。
+     * NOTE: 可支持范围操作符的列表数据过滤。
+     */
+    @Schema(description = "createTime 范围过滤起始值(>=)。可支持范围操作符的列表数据过滤。")
+    private String createTimeStart;
+
+    /**
+     * createTime 范围过滤结束值(<=)。
+     * NOTE: 可支持范围操作符的列表数据过滤。
+     */
+    @Schema(description = "createTime 范围过滤结束值(<=)。可支持范围操作符的列表数据过滤。")
+    private String createTimeEnd;
+
+    /**
+     * message_content LIKE搜索字符串。
+     * NOTE: 可支持LIKE操作符的列表数据过滤。
+     */
+    @Schema(description = "LIKE模糊搜索字符串")
+    private String searchString;
+}

+ 143 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/dto/SerbiaUsersDto.java

@@ -0,0 +1,143 @@
+package com.serbia.webadmin.app.dto;
+
+import com.serbia.common.core.validator.UpdateGroup;
+import com.serbia.common.core.validator.ConstDictRef;
+import com.serbia.webadmin.upms.model.constant.SysUserStatus;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+import java.util.Date;
+
+/**
+ * 塞尔用户管理Dto对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Schema(description = "塞尔用户管理Dto对象")
+@Data
+public class SerbiaUsersDto {
+
+    /**
+     * 主键Id。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "主键Id。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,主键Id不能为空!", groups = {UpdateGroup.class})
+    private Long userId;
+
+    /**
+     * 用户登录名称。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "用户登录名称。可支持等于操作符的列表数据过滤。")
+    private String loginName;
+
+    /**
+     * 密码。
+     */
+    @Schema(description = "密码。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,密码不能为空!")
+    private String password;
+
+    /**
+     * 昵称。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "昵称。可支持等于操作符的列表数据过滤。")
+    private String showName;
+
+    /**
+     * 用户头像的Url。
+     */
+    @Schema(description = "用户头像的Url。")
+    private String headImageUrl;
+
+    /**
+     * 状态(0: 正常 1: 锁定)。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "状态(0: 正常 1: 锁定)。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,状态(0: 正常 1: 锁定)不能为空!")
+    @ConstDictRef(constDictClass = SysUserStatus.class, message = "数据验证失败,状态(0: 正常 1: 锁定)为无效值!")
+    private Integer userStatus;
+
+    /**
+     * 用户邮箱。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "用户邮箱。可支持等于操作符的列表数据过滤。")
+    private String email;
+
+    /**
+     * 用户手机。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "用户手机。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,用户手机不能为空!")
+    private String mobile;
+
+    /**
+     * 性别(1:男 2:女 3:其它)。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "性别(1:男 2:女 3:其它)。可支持等于操作符的列表数据过滤。")
+    private String sex;
+
+    /**
+     * 生日。
+     */
+    @Schema(description = "生日。")
+    private Date birthday;
+
+    /**
+     * 手机号所属国家code。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "手机号所属国家code。可支持等于操作符的列表数据过滤。")
+    private String countryCode;
+
+    /**
+     * 住址。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "住址。可支持等于操作符的列表数据过滤。")
+    private String address;
+
+    /**
+     * 职业。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "职业。可支持等于操作符的列表数据过滤。")
+    private String job;
+
+    /**
+     * 个性签名。
+     */
+    @Schema(description = "个性签名。")
+    private String personalSign;
+
+    /**
+     * createTime 范围过滤起始值(>=)。
+     * NOTE: 可支持范围操作符的列表数据过滤。
+     */
+    @Schema(description = "createTime 范围过滤起始值(>=)。可支持范围操作符的列表数据过滤。")
+    private String createTimeStart;
+
+    /**
+     * createTime 范围过滤结束值(<=)。
+     * NOTE: 可支持范围操作符的列表数据过滤。
+     */
+    @Schema(description = "createTime 范围过滤结束值(<=)。可支持范围操作符的列表数据过滤。")
+    private String createTimeEnd;
+
+    /**
+     * login_name / show_name / email / mobile / sex / address / job LIKE搜索字符串。
+     * NOTE: 可支持LIKE操作符的列表数据过滤。
+     */
+    @Schema(description = "LIKE模糊搜索字符串")
+    private String searchString;
+}

+ 67 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/model/SerbiaCountryCode.java

@@ -0,0 +1,67 @@
+package com.serbia.webadmin.app.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.serbia.common.core.base.model.BaseModel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 国家区域编码管理实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName(value = "serbia_country_code")
+public class SerbiaCountryCode extends BaseModel {
+
+    /**
+     * 主键。
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 英文名称。
+     */
+    @TableField(value = "english_name")
+    private String englishName;
+
+    /**
+     * 国家/地区。
+     */
+    @TableField(value = "country")
+    private String country;
+
+    /**
+     * 国家代码。
+     */
+    @TableField(value = "country_code")
+    private String countryCode;
+
+    /**
+     * 区号。
+     */
+    @TableField(value = "area_code")
+    private String areaCode;
+
+    /**
+     * 拼音。
+     */
+    @TableField(value = "pin_yin")
+    private String pinYin;
+
+    /**
+     * 拼音首字母。
+     */
+    @TableField(value = "first_code")
+    private String firstCode;
+
+    /**
+     * 逻辑删除标记字段(1: 正常 -1: 已删除)。
+     */
+    @TableLogic
+    @TableField(value = "data_state")
+    private Integer dataState;
+}

+ 58 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/model/SerbiaGroupMembers.java

@@ -0,0 +1,58 @@
+package com.serbia.webadmin.app.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.serbia.common.core.annotation.*;
+import com.serbia.common.core.base.model.BaseModel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Map;
+
+/**
+ * 聊天群组成员管理实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName(value = "serbia_group_members")
+public class SerbiaGroupMembers extends BaseModel {
+
+    /**
+     * 主键id。
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 群聊id。
+     */
+    @TableField(value = "group_id")
+    private Long groupId;
+
+    /**
+     * 用户id。
+     */
+    @TableField(value = "user_id")
+    private Long userId;
+
+    /**
+     * 成员角色 1群主 2群成员。
+     */
+    @TableField(value = "group_role")
+    private Integer groupRole;
+
+    /**
+     * 逻辑删除标记字段(1: 正常 -1: 已删除)。
+     */
+    @TableLogic
+    @TableField(value = "data_state")
+    private Integer dataState;
+
+    @RelationGlobalDict(
+            masterIdField = "groupRole",
+            dictCode = "GroupMemberRole")
+    @TableField(exist = false)
+    private Map<String, Object> groupRoleDictMap;
+}

+ 101 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/model/SerbiaGroups.java

@@ -0,0 +1,101 @@
+package com.serbia.webadmin.app.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.serbia.common.core.util.MyCommonUtil;
+import com.serbia.common.core.upload.UploadStoreTypeEnum;
+import com.serbia.common.core.annotation.*;
+import com.serbia.common.core.base.model.BaseModel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Map;
+
+/**
+ * 聊天群组管理实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName(value = "serbia_groups")
+public class SerbiaGroups extends BaseModel {
+
+    /**
+     * 主键id。
+     */
+    @TableId(value = "group_id")
+    private Long groupId;
+
+    /**
+     * 群主id。
+     */
+    @TableField(value = "group_owner_id")
+    private Long groupOwnerId;
+
+    /**
+     * 群聊名称。
+     */
+    @TableField(value = "group_name")
+    private String groupName;
+
+    /**
+     * 群聊头像。
+     */
+    @UploadFlagColumn(storeType = UploadStoreTypeEnum.HUAWEI_OBS_SYSTEM)
+    @TableField(value = "group_avatar")
+    private String groupAvatar;
+
+    /**
+     * 是否公开 0私密 1公开。
+     */
+    @TableField(value = "is_private")
+    private Integer isPrivate;
+
+    /**
+     * 逻辑删除标记字段(1: 正常 -1: 已删除)。
+     */
+    @TableLogic
+    @TableField(value = "data_state")
+    private Integer dataState;
+
+    /**
+     * createTime 范围过滤起始值(>=)。
+     */
+    @TableField(exist = false)
+    private String createTimeStart;
+
+    /**
+     * createTime 范围过滤结束值(<=)。
+     */
+    @TableField(exist = false)
+    private String createTimeEnd;
+
+    /**
+     * updateTime 范围过滤起始值(>=)。
+     */
+    @TableField(exist = false)
+    private String updateTimeStart;
+
+    /**
+     * updateTime 范围过滤结束值(<=)。
+     */
+    @TableField(exist = false)
+    private String updateTimeEnd;
+
+    /**
+     * group_name LIKE搜索字符串。
+     */
+    @TableField(exist = false)
+    private String searchString;
+
+    public void setSearchString(String searchString) {
+        this.searchString = MyCommonUtil.replaceSqlWildcard(searchString);
+    }
+
+    @RelationGlobalDict(
+            masterIdField = "isPrivate",
+            dictCode = "YesOrNo")
+    @TableField(exist = false)
+    private Map<String, Object> isPrivateDictMap;
+}

+ 121 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/model/SerbiaMessages.java

@@ -0,0 +1,121 @@
+package com.serbia.webadmin.app.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.serbia.common.core.util.MyCommonUtil;
+import com.serbia.common.core.annotation.*;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * 聊天信息管理实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Data
+@TableName(value = "serbia_messages")
+public class SerbiaMessages {
+
+    /**
+     * 主键id。
+     */
+    @TableId(value = "message_id")
+    private Long messageId;
+
+    /**
+     * 发送人id。
+     */
+    @TableField(value = "sender_id")
+    private Long senderId;
+
+    /**
+     * 接收者id。
+     */
+    @TableField(value = "receiver_id")
+    private Long receiverId;
+
+    /**
+     * 群组id。
+     */
+    @TableField(value = "group_id")
+    private Long groupId;
+
+    /**
+     * 消息类型 1文字 2图片 3文件。
+     */
+    @TableField(value = "messsage_type")
+    private Integer messsageType;
+
+    /**
+     * 消息内容。
+     */
+    @TableField(value = "message_content")
+    private String messageContent;
+
+    /**
+     * 创建时间。
+     */
+    @TableField(value = "create_time")
+    private Date createTime;
+
+    /**
+     * 更新用户。
+     */
+    @TableField(value = "update_user_id")
+    private Long updateUserId;
+
+    /**
+     * 更新时间。
+     */
+    @TableField(value = "update_time")
+    private Date updateTime;
+
+    /**
+     * 逻辑删除标记字段(1: 正常 -1: 已删除)。
+     */
+    @TableLogic
+    @TableField(value = "data_state")
+    private Integer dataState;
+
+    /**
+     * 消息已读状态 0未读 1已读。
+     */
+    @TableField(value = "read_status")
+    private Integer readStatus;
+
+    /**
+     * createTime 范围过滤起始值(>=)。
+     */
+    @TableField(exist = false)
+    private String createTimeStart;
+
+    /**
+     * createTime 范围过滤结束值(<=)。
+     */
+    @TableField(exist = false)
+    private String createTimeEnd;
+
+    /**
+     * message_content LIKE搜索字符串。
+     */
+    @TableField(exist = false)
+    private String searchString;
+
+    public void setSearchString(String searchString) {
+        this.searchString = MyCommonUtil.replaceSqlWildcard(searchString);
+    }
+
+    @RelationGlobalDict(
+            masterIdField = "messsageType",
+            dictCode = "MessageType")
+    @TableField(exist = false)
+    private Map<String, Object> messsageTypeDictMap;
+
+    @RelationGlobalDict(
+            masterIdField = "readStatus",
+            dictCode = "MessageReadStatus")
+    @TableField(exist = false)
+    private Map<String, Object> readStatusDictMap;
+}

+ 151 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/model/SerbiaUsers.java

@@ -0,0 +1,151 @@
+package com.serbia.webadmin.app.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.serbia.webadmin.upms.model.constant.SysUserStatus;
+import com.serbia.common.core.util.MyCommonUtil;
+import com.serbia.common.core.upload.UploadStoreTypeEnum;
+import com.serbia.common.core.annotation.*;
+import com.serbia.common.core.base.model.BaseModel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * 塞尔用户管理实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName(value = "serbia_users")
+public class SerbiaUsers extends BaseModel {
+
+    /**
+     * 主键Id。
+     */
+    @TableId(value = "user_id")
+    private Long userId;
+
+    /**
+     * 用户登录名称。
+     */
+    @TableField(value = "login_name")
+    private String loginName;
+
+    /**
+     * 密码。
+     */
+    @TableField(value = "password")
+    private String password;
+
+    /**
+     * 昵称。
+     */
+    @TableField(value = "show_name")
+    private String showName;
+
+    /**
+     * 用户头像的Url。
+     */
+    @UploadFlagColumn(storeType = UploadStoreTypeEnum.HUAWEI_OBS_SYSTEM)
+    @TableField(value = "head_image_url")
+    private String headImageUrl;
+
+    /**
+     * 状态(0: 正常 1: 锁定)。
+     */
+    @TableField(value = "user_status")
+    private Integer userStatus;
+
+    /**
+     * 用户邮箱。
+     */
+    @TableField(value = "email")
+    private String email;
+
+    /**
+     * 用户手机。
+     */
+    @TableField(value = "mobile")
+    private String mobile;
+
+    /**
+     * 性别(1:男 2:女 3:其它)。
+     */
+    @TableField(value = "sex")
+    private String sex;
+
+    /**
+     * 生日。
+     */
+    @TableField(value = "birthday")
+    private Date birthday;
+
+    /**
+     * 逻辑删除标记字段(1: 正常 -1: 已删除)。
+     */
+    @TableLogic
+    @TableField(value = "deleted_flag")
+    private Integer deletedFlag;
+
+    /**
+     * 手机号所属国家code。
+     */
+    @TableField(value = "country_code")
+    private String countryCode;
+
+    /**
+     * 住址。
+     */
+    @TableField(value = "address")
+    private String address;
+
+    /**
+     * 职业。
+     */
+    @TableField(value = "job")
+    private String job;
+
+    /**
+     * 个性签名。
+     */
+    @TableField(value = "personal_sign")
+    private String personalSign;
+
+    /**
+     * createTime 范围过滤起始值(>=)。
+     */
+    @TableField(exist = false)
+    private String createTimeStart;
+
+    /**
+     * createTime 范围过滤结束值(<=)。
+     */
+    @TableField(exist = false)
+    private String createTimeEnd;
+
+    /**
+     * login_name / show_name / email / mobile / sex / address / job LIKE搜索字符串。
+     */
+    @TableField(exist = false)
+    private String searchString;
+
+    public void setSearchString(String searchString) {
+        this.searchString = MyCommonUtil.replaceSqlWildcard(searchString);
+    }
+
+    @RelationConstDict(
+            masterIdField = "userStatus",
+            constantDictClass = SysUserStatus.class)
+    @TableField(exist = false)
+    private Map<String, Object> userStatusDictMap;
+
+    @RelationGlobalDict(
+            masterIdField = "sex",
+            dictCode = "UserSex")
+    @TableField(exist = false)
+    private Map<String, Object> sexDictMap;
+}

+ 44 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/model/constant/UserStatus.java

@@ -0,0 +1,44 @@
+package com.serbia.webadmin.app.model.constant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 用户状态常量字典对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-10-30
+ */
+public final class UserStatus {
+
+    /**
+     * 正常状态。
+     */
+    public static final int STATUS_NORMAL = 0;
+    /**
+     * 锁定状态。
+     */
+    public static final int STATUS_LOCKED = 1;
+
+    private static final Map<Object, String> DICT_MAP = new HashMap<>(2);
+    static {
+        DICT_MAP.put(STATUS_NORMAL, "正常状态");
+        DICT_MAP.put(STATUS_LOCKED, "锁定状态");
+    }
+
+    /**
+     * 判断参数是否为当前常量字典的合法值。
+     *
+     * @param value 待验证的参数值。
+     * @return 合法返回true,否则false。
+     */
+    public static boolean isValid(Integer value) {
+        return value != null && DICT_MAP.containsKey(value);
+    }
+
+    /**
+     * 私有构造函数,明确标识该常量类的作用。
+     */
+    private UserStatus() {
+    }
+}

+ 68 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/service/SerbiaCountryCodeService.java

@@ -0,0 +1,68 @@
+package com.serbia.webadmin.app.service;
+
+import com.serbia.webadmin.app.model.*;
+import com.serbia.common.core.base.service.IBaseService;
+
+import java.util.*;
+
+/**
+ * 国家区域编码管理数据操作服务接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SerbiaCountryCodeService extends IBaseService<SerbiaCountryCode, Long> {
+
+    /**
+     * 保存新增对象。
+     *
+     * @param serbiaCountryCode 新增对象。
+     * @return 返回新增对象。
+     */
+    SerbiaCountryCode saveNew(SerbiaCountryCode serbiaCountryCode);
+
+    /**
+     * 利用数据库的insertList语法,批量插入对象列表。
+     *
+     * @param serbiaCountryCodeList 新增对象列表。
+     */
+    void saveNewBatch(List<SerbiaCountryCode> serbiaCountryCodeList);
+
+    /**
+     * 更新数据对象。
+     *
+     * @param serbiaCountryCode         更新的对象。
+     * @param originalSerbiaCountryCode 原有数据对象。
+     * @return 成功返回true,否则false。
+     */
+    boolean update(SerbiaCountryCode serbiaCountryCode, SerbiaCountryCode originalSerbiaCountryCode);
+
+    /**
+     * 删除指定数据。
+     *
+     * @param id 主键Id。
+     * @return 成功返回true,否则false。
+     */
+    boolean remove(Long id);
+
+    /**
+     * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。
+     * 如果需要同时获取关联数据,请移步(getSerbiaCountryCodeListWithRelation)方法。
+     *
+     * @param filter  过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SerbiaCountryCode> getSerbiaCountryCodeList(SerbiaCountryCode filter, String orderBy);
+
+    /**
+     * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。
+     * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。
+     * 如果仅仅需要获取主表数据,请移步(getSerbiaCountryCodeList),以便获取更好的查询性能。
+     *
+     * @param filter 主表过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SerbiaCountryCode> getSerbiaCountryCodeListWithRelation(SerbiaCountryCode filter, String orderBy);
+}

+ 68 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/service/SerbiaGroupMembersService.java

@@ -0,0 +1,68 @@
+package com.serbia.webadmin.app.service;
+
+import com.serbia.webadmin.app.model.*;
+import com.serbia.common.core.base.service.IBaseService;
+
+import java.util.*;
+
+/**
+ * 聊天群组成员管理数据操作服务接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SerbiaGroupMembersService extends IBaseService<SerbiaGroupMembers, Long> {
+
+    /**
+     * 保存新增对象。
+     *
+     * @param serbiaGroupMembers 新增对象。
+     * @return 返回新增对象。
+     */
+    SerbiaGroupMembers saveNew(SerbiaGroupMembers serbiaGroupMembers);
+
+    /**
+     * 利用数据库的insertList语法,批量插入对象列表。
+     *
+     * @param serbiaGroupMembersList 新增对象列表。
+     */
+    void saveNewBatch(List<SerbiaGroupMembers> serbiaGroupMembersList);
+
+    /**
+     * 更新数据对象。
+     *
+     * @param serbiaGroupMembers         更新的对象。
+     * @param originalSerbiaGroupMembers 原有数据对象。
+     * @return 成功返回true,否则false。
+     */
+    boolean update(SerbiaGroupMembers serbiaGroupMembers, SerbiaGroupMembers originalSerbiaGroupMembers);
+
+    /**
+     * 删除指定数据。
+     *
+     * @param id 主键Id。
+     * @return 成功返回true,否则false。
+     */
+    boolean remove(Long id);
+
+    /**
+     * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。
+     * 如果需要同时获取关联数据,请移步(getSerbiaGroupMembersListWithRelation)方法。
+     *
+     * @param filter  过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SerbiaGroupMembers> getSerbiaGroupMembersList(SerbiaGroupMembers filter, String orderBy);
+
+    /**
+     * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。
+     * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。
+     * 如果仅仅需要获取主表数据,请移步(getSerbiaGroupMembersList),以便获取更好的查询性能。
+     *
+     * @param filter 主表过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SerbiaGroupMembers> getSerbiaGroupMembersListWithRelation(SerbiaGroupMembers filter, String orderBy);
+}

+ 68 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/service/SerbiaGroupsService.java

@@ -0,0 +1,68 @@
+package com.serbia.webadmin.app.service;
+
+import com.serbia.webadmin.app.model.*;
+import com.serbia.common.core.base.service.IBaseService;
+
+import java.util.*;
+
+/**
+ * 聊天群组管理数据操作服务接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SerbiaGroupsService extends IBaseService<SerbiaGroups, Long> {
+
+    /**
+     * 保存新增对象。
+     *
+     * @param serbiaGroups 新增对象。
+     * @return 返回新增对象。
+     */
+    SerbiaGroups saveNew(SerbiaGroups serbiaGroups);
+
+    /**
+     * 利用数据库的insertList语法,批量插入对象列表。
+     *
+     * @param serbiaGroupsList 新增对象列表。
+     */
+    void saveNewBatch(List<SerbiaGroups> serbiaGroupsList);
+
+    /**
+     * 更新数据对象。
+     *
+     * @param serbiaGroups         更新的对象。
+     * @param originalSerbiaGroups 原有数据对象。
+     * @return 成功返回true,否则false。
+     */
+    boolean update(SerbiaGroups serbiaGroups, SerbiaGroups originalSerbiaGroups);
+
+    /**
+     * 删除指定数据。
+     *
+     * @param groupId 主键Id。
+     * @return 成功返回true,否则false。
+     */
+    boolean remove(Long groupId);
+
+    /**
+     * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。
+     * 如果需要同时获取关联数据,请移步(getSerbiaGroupsListWithRelation)方法。
+     *
+     * @param filter  过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SerbiaGroups> getSerbiaGroupsList(SerbiaGroups filter, String orderBy);
+
+    /**
+     * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。
+     * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。
+     * 如果仅仅需要获取主表数据,请移步(getSerbiaGroupsList),以便获取更好的查询性能。
+     *
+     * @param filter 主表过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SerbiaGroups> getSerbiaGroupsListWithRelation(SerbiaGroups filter, String orderBy);
+}

+ 68 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/service/SerbiaMessagesService.java

@@ -0,0 +1,68 @@
+package com.serbia.webadmin.app.service;
+
+import com.serbia.webadmin.app.model.*;
+import com.serbia.common.core.base.service.IBaseService;
+
+import java.util.*;
+
+/**
+ * 聊天信息管理数据操作服务接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SerbiaMessagesService extends IBaseService<SerbiaMessages, Long> {
+
+    /**
+     * 保存新增对象。
+     *
+     * @param serbiaMessages 新增对象。
+     * @return 返回新增对象。
+     */
+    SerbiaMessages saveNew(SerbiaMessages serbiaMessages);
+
+    /**
+     * 利用数据库的insertList语法,批量插入对象列表。
+     *
+     * @param serbiaMessagesList 新增对象列表。
+     */
+    void saveNewBatch(List<SerbiaMessages> serbiaMessagesList);
+
+    /**
+     * 更新数据对象。
+     *
+     * @param serbiaMessages         更新的对象。
+     * @param originalSerbiaMessages 原有数据对象。
+     * @return 成功返回true,否则false。
+     */
+    boolean update(SerbiaMessages serbiaMessages, SerbiaMessages originalSerbiaMessages);
+
+    /**
+     * 删除指定数据。
+     *
+     * @param messageId 主键Id。
+     * @return 成功返回true,否则false。
+     */
+    boolean remove(Long messageId);
+
+    /**
+     * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。
+     * 如果需要同时获取关联数据,请移步(getSerbiaMessagesListWithRelation)方法。
+     *
+     * @param filter  过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SerbiaMessages> getSerbiaMessagesList(SerbiaMessages filter, String orderBy);
+
+    /**
+     * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。
+     * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。
+     * 如果仅仅需要获取主表数据,请移步(getSerbiaMessagesList),以便获取更好的查询性能。
+     *
+     * @param filter 主表过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SerbiaMessages> getSerbiaMessagesListWithRelation(SerbiaMessages filter, String orderBy);
+}

+ 78 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/service/SerbiaUsersService.java

@@ -0,0 +1,78 @@
+package com.serbia.webadmin.app.service;
+
+import com.serbia.webadmin.app.model.*;
+import com.serbia.common.core.base.service.IBaseService;
+
+import java.util.*;
+
+/**
+ * 塞尔用户管理数据操作服务接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SerbiaUsersService extends IBaseService<SerbiaUsers, Long> {
+
+    /**
+     * 保存新增对象。
+     *
+     * @param serbiaUsers 新增对象。
+     * @return 返回新增对象。
+     */
+    SerbiaUsers saveNew(SerbiaUsers serbiaUsers);
+
+    /**
+     * 利用数据库的insertList语法,批量插入对象列表。
+     *
+     * @param serbiaUsersList 新增对象列表。
+     */
+    void saveNewBatch(List<SerbiaUsers> serbiaUsersList);
+
+    /**
+     * 更新数据对象。
+     *
+     * @param serbiaUsers         更新的对象。
+     * @param originalSerbiaUsers 原有数据对象。
+     * @return 成功返回true,否则false。
+     */
+    boolean update(SerbiaUsers serbiaUsers, SerbiaUsers originalSerbiaUsers);
+
+    /**
+     * 删除指定数据。
+     *
+     * @param userId 主键Id。
+     * @return 成功返回true,否则false。
+     */
+    boolean remove(Long userId);
+
+    /**
+     * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。
+     * 如果需要同时获取关联数据,请移步(getSerbiaUsersListWithRelation)方法。
+     *
+     * @param filter  过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SerbiaUsers> getSerbiaUsersList(SerbiaUsers filter, String orderBy);
+
+    /**
+     * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。
+     * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。
+     * 如果仅仅需要获取主表数据,请移步(getSerbiaUsersList),以便获取更好的查询性能。
+     *
+     * @param filter 主表过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SerbiaUsers> getSerbiaUsersListWithRelation(SerbiaUsers filter, String orderBy);
+
+
+    /**
+     * 修改用户头像。
+     *
+     * @param userId  用户主键Id。
+     * @param newHeadImage 新的头像信息。
+     * @return 成功返回true,否则false。
+     */
+    boolean changeHeadImage(Long userId, String newHeadImage);
+}

+ 101 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/service/impl/SerbiaCountryCodeServiceImpl.java

@@ -0,0 +1,101 @@
+package com.serbia.webadmin.app.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.serbia.webadmin.app.service.*;
+import com.serbia.webadmin.app.dao.*;
+import com.serbia.webadmin.app.model.*;
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.common.core.constant.GlobalDeletedFlag;
+import com.serbia.common.core.object.MyRelationParam;
+import com.serbia.common.core.base.service.BaseService;
+import com.serbia.common.core.util.MyModelUtil;
+import com.serbia.common.sequence.wrapper.IdGeneratorWrapper;
+import com.github.pagehelper.Page;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+/**
+ * 国家区域编码管理数据操作服务类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Slf4j
+@Service("serbiaCountryCodeService")
+public class SerbiaCountryCodeServiceImpl extends BaseService<SerbiaCountryCode, Long> implements SerbiaCountryCodeService {
+
+    @Autowired
+    private IdGeneratorWrapper idGenerator;
+    @Autowired
+    private SerbiaCountryCodeMapper serbiaCountryCodeMapper;
+
+    /**
+     * 返回当前Service的主表Mapper对象。
+     *
+     * @return 主表Mapper对象。
+     */
+    @Override
+    protected BaseDaoMapper<SerbiaCountryCode> mapper() {
+        return serbiaCountryCodeMapper;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public SerbiaCountryCode saveNew(SerbiaCountryCode serbiaCountryCode) {
+        serbiaCountryCodeMapper.insert(this.buildDefaultValue(serbiaCountryCode));
+        return serbiaCountryCode;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void saveNewBatch(List<SerbiaCountryCode> serbiaCountryCodeList) {
+        if (CollUtil.isNotEmpty(serbiaCountryCodeList)) {
+            serbiaCountryCodeList.forEach(this::buildDefaultValue);
+            serbiaCountryCodeMapper.insertList(serbiaCountryCodeList);
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean update(SerbiaCountryCode serbiaCountryCode, SerbiaCountryCode originalSerbiaCountryCode) {
+        MyModelUtil.fillCommonsForUpdate(serbiaCountryCode, originalSerbiaCountryCode);
+        // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。
+        UpdateWrapper<SerbiaCountryCode> uw = this.createUpdateQueryForNullValue(serbiaCountryCode, serbiaCountryCode.getId());
+        return serbiaCountryCodeMapper.update(serbiaCountryCode, uw) == 1;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean remove(Long id) {
+        return serbiaCountryCodeMapper.deleteById(id) == 1;
+    }
+
+    @Override
+    public List<SerbiaCountryCode> getSerbiaCountryCodeList(SerbiaCountryCode filter, String orderBy) {
+        return serbiaCountryCodeMapper.getSerbiaCountryCodeList(filter, orderBy);
+    }
+
+    @Override
+    public List<SerbiaCountryCode> getSerbiaCountryCodeListWithRelation(SerbiaCountryCode filter, String orderBy) {
+        List<SerbiaCountryCode> resultList = serbiaCountryCodeMapper.getSerbiaCountryCodeList(filter, orderBy);
+        // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。
+        // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。
+        int batchSize = resultList instanceof Page ? 0 : 1000;
+        this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize);
+        return resultList;
+    }
+
+    private SerbiaCountryCode buildDefaultValue(SerbiaCountryCode serbiaCountryCode) {
+        if (serbiaCountryCode.getId() == null) {
+            serbiaCountryCode.setId(idGenerator.nextLongId());
+        }
+        MyModelUtil.fillCommonsForInsert(serbiaCountryCode);
+        serbiaCountryCode.setDataState(GlobalDeletedFlag.NORMAL);
+        return serbiaCountryCode;
+    }
+}

+ 116 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/service/impl/SerbiaGroupMembersServiceImpl.java

@@ -0,0 +1,116 @@
+package com.serbia.webadmin.app.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.serbia.webadmin.app.service.*;
+import com.serbia.webadmin.app.dao.*;
+import com.serbia.webadmin.app.model.*;
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.common.core.constant.GlobalDeletedFlag;
+import com.serbia.common.core.object.MyRelationParam;
+import com.serbia.common.core.object.CallResult;
+import com.serbia.common.core.base.service.BaseService;
+import com.serbia.common.core.util.MyModelUtil;
+import com.serbia.common.sequence.wrapper.IdGeneratorWrapper;
+import com.serbia.common.dict.service.GlobalDictService;
+import com.github.pagehelper.Page;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+/**
+ * 聊天群组成员管理数据操作服务类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Slf4j
+@Service("serbiaGroupMembersService")
+public class SerbiaGroupMembersServiceImpl extends BaseService<SerbiaGroupMembers, Long> implements SerbiaGroupMembersService {
+
+    @Autowired
+    private IdGeneratorWrapper idGenerator;
+    @Autowired
+    private SerbiaGroupMembersMapper serbiaGroupMembersMapper;
+    @Autowired
+    private GlobalDictService globalDictService;
+
+    /**
+     * 返回当前Service的主表Mapper对象。
+     *
+     * @return 主表Mapper对象。
+     */
+    @Override
+    protected BaseDaoMapper<SerbiaGroupMembers> mapper() {
+        return serbiaGroupMembersMapper;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public SerbiaGroupMembers saveNew(SerbiaGroupMembers serbiaGroupMembers) {
+        serbiaGroupMembersMapper.insert(this.buildDefaultValue(serbiaGroupMembers));
+        return serbiaGroupMembers;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void saveNewBatch(List<SerbiaGroupMembers> serbiaGroupMembersList) {
+        if (CollUtil.isNotEmpty(serbiaGroupMembersList)) {
+            serbiaGroupMembersList.forEach(this::buildDefaultValue);
+            serbiaGroupMembersMapper.insertList(serbiaGroupMembersList);
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean update(SerbiaGroupMembers serbiaGroupMembers, SerbiaGroupMembers originalSerbiaGroupMembers) {
+        MyModelUtil.fillCommonsForUpdate(serbiaGroupMembers, originalSerbiaGroupMembers);
+        // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。
+        UpdateWrapper<SerbiaGroupMembers> uw = this.createUpdateQueryForNullValue(serbiaGroupMembers, serbiaGroupMembers.getId());
+        return serbiaGroupMembersMapper.update(serbiaGroupMembers, uw) == 1;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean remove(Long id) {
+        return serbiaGroupMembersMapper.deleteById(id) == 1;
+    }
+
+    @Override
+    public List<SerbiaGroupMembers> getSerbiaGroupMembersList(SerbiaGroupMembers filter, String orderBy) {
+        return serbiaGroupMembersMapper.getSerbiaGroupMembersList(filter, orderBy);
+    }
+
+    @Override
+    public List<SerbiaGroupMembers> getSerbiaGroupMembersListWithRelation(SerbiaGroupMembers filter, String orderBy) {
+        List<SerbiaGroupMembers> resultList = serbiaGroupMembersMapper.getSerbiaGroupMembersList(filter, orderBy);
+        // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。
+        // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。
+        int batchSize = resultList instanceof Page ? 0 : 1000;
+        this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize);
+        return resultList;
+    }
+
+    @Override
+    public CallResult verifyRelatedData(SerbiaGroupMembers serbiaGroupMembers, SerbiaGroupMembers originalSerbiaGroupMembers) {
+        String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!";
+        //这里是基于字典的验证。
+        if (this.needToVerify(serbiaGroupMembers, originalSerbiaGroupMembers, SerbiaGroupMembers::getGroupRole)
+                && !globalDictService.existDictItemFromCache("GroupMemberRole", serbiaGroupMembers.getGroupRole())) {
+            return CallResult.error(String.format(errorMessageFormat, "成员角色 1群主 2群成员"));
+        }
+        return CallResult.ok();
+    }
+
+    private SerbiaGroupMembers buildDefaultValue(SerbiaGroupMembers serbiaGroupMembers) {
+        if (serbiaGroupMembers.getId() == null) {
+            serbiaGroupMembers.setId(idGenerator.nextLongId());
+        }
+        MyModelUtil.fillCommonsForInsert(serbiaGroupMembers);
+        serbiaGroupMembers.setDataState(GlobalDeletedFlag.NORMAL);
+        return serbiaGroupMembers;
+    }
+}

+ 117 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/service/impl/SerbiaGroupsServiceImpl.java

@@ -0,0 +1,117 @@
+package com.serbia.webadmin.app.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.serbia.webadmin.app.service.*;
+import com.serbia.webadmin.app.dao.*;
+import com.serbia.webadmin.app.model.*;
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.common.core.constant.GlobalDeletedFlag;
+import com.serbia.common.core.object.MyRelationParam;
+import com.serbia.common.core.object.CallResult;
+import com.serbia.common.core.base.service.BaseService;
+import com.serbia.common.core.util.MyModelUtil;
+import com.serbia.common.sequence.wrapper.IdGeneratorWrapper;
+import com.serbia.common.dict.service.GlobalDictService;
+import com.github.pagehelper.Page;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+/**
+ * 聊天群组管理数据操作服务类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Slf4j
+@Service("serbiaGroupsService")
+public class SerbiaGroupsServiceImpl extends BaseService<SerbiaGroups, Long> implements SerbiaGroupsService {
+
+    @Autowired
+    private IdGeneratorWrapper idGenerator;
+    @Autowired
+    private SerbiaGroupsMapper serbiaGroupsMapper;
+    @Autowired
+    private GlobalDictService globalDictService;
+
+    /**
+     * 返回当前Service的主表Mapper对象。
+     *
+     * @return 主表Mapper对象。
+     */
+    @Override
+    protected BaseDaoMapper<SerbiaGroups> mapper() {
+        return serbiaGroupsMapper;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public SerbiaGroups saveNew(SerbiaGroups serbiaGroups) {
+        serbiaGroupsMapper.insert(this.buildDefaultValue(serbiaGroups));
+        return serbiaGroups;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void saveNewBatch(List<SerbiaGroups> serbiaGroupsList) {
+        if (CollUtil.isNotEmpty(serbiaGroupsList)) {
+            serbiaGroupsList.forEach(this::buildDefaultValue);
+            serbiaGroupsMapper.insertList(serbiaGroupsList);
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean update(SerbiaGroups serbiaGroups, SerbiaGroups originalSerbiaGroups) {
+        MyModelUtil.fillCommonsForUpdate(serbiaGroups, originalSerbiaGroups);
+        // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。
+        UpdateWrapper<SerbiaGroups> uw = this.createUpdateQueryForNullValue(serbiaGroups, serbiaGroups.getGroupId());
+        return serbiaGroupsMapper.update(serbiaGroups, uw) == 1;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean remove(Long groupId) {
+        return serbiaGroupsMapper.deleteById(groupId) == 1;
+    }
+
+    @Override
+    public List<SerbiaGroups> getSerbiaGroupsList(SerbiaGroups filter, String orderBy) {
+        return serbiaGroupsMapper.getSerbiaGroupsList(filter, orderBy);
+    }
+
+    @Override
+    public List<SerbiaGroups> getSerbiaGroupsListWithRelation(SerbiaGroups filter, String orderBy) {
+        List<SerbiaGroups> resultList = serbiaGroupsMapper.getSerbiaGroupsList(filter, orderBy);
+        // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。
+        // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。
+        int batchSize = resultList instanceof Page ? 0 : 1000;
+        this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize);
+        return resultList;
+    }
+
+    @Override
+    public CallResult verifyRelatedData(SerbiaGroups serbiaGroups, SerbiaGroups originalSerbiaGroups) {
+        String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!";
+        //这里是基于字典的验证。
+        if (this.needToVerify(serbiaGroups, originalSerbiaGroups, SerbiaGroups::getIsPrivate)
+                && !globalDictService.existDictItemFromCache("YesOrNo", serbiaGroups.getIsPrivate())) {
+            return CallResult.error(String.format(errorMessageFormat, "是否公开 0私密 1公开"));
+        }
+        return CallResult.ok();
+    }
+
+    private SerbiaGroups buildDefaultValue(SerbiaGroups serbiaGroups) {
+        if (serbiaGroups.getGroupId() == null) {
+            serbiaGroups.setGroupId(idGenerator.nextLongId());
+        }
+        MyModelUtil.fillCommonsForInsert(serbiaGroups);
+        serbiaGroups.setDataState(GlobalDeletedFlag.NORMAL);
+        MyModelUtil.setDefaultValue(serbiaGroups, "isPrivate", 0);
+        return serbiaGroups;
+    }
+}

+ 122 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/service/impl/SerbiaMessagesServiceImpl.java

@@ -0,0 +1,122 @@
+package com.serbia.webadmin.app.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.serbia.webadmin.app.service.*;
+import com.serbia.webadmin.app.dao.*;
+import com.serbia.webadmin.app.model.*;
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.common.core.constant.GlobalDeletedFlag;
+import com.serbia.common.core.object.TokenData;
+import com.serbia.common.core.object.MyRelationParam;
+import com.serbia.common.core.object.CallResult;
+import com.serbia.common.core.base.service.BaseService;
+import com.serbia.common.sequence.wrapper.IdGeneratorWrapper;
+import com.serbia.common.dict.service.GlobalDictService;
+import com.github.pagehelper.Page;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+/**
+ * 聊天信息管理数据操作服务类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Slf4j
+@Service("serbiaMessagesService")
+public class SerbiaMessagesServiceImpl extends BaseService<SerbiaMessages, Long> implements SerbiaMessagesService {
+
+    @Autowired
+    private IdGeneratorWrapper idGenerator;
+    @Autowired
+    private SerbiaMessagesMapper serbiaMessagesMapper;
+    @Autowired
+    private GlobalDictService globalDictService;
+
+    /**
+     * 返回当前Service的主表Mapper对象。
+     *
+     * @return 主表Mapper对象。
+     */
+    @Override
+    protected BaseDaoMapper<SerbiaMessages> mapper() {
+        return serbiaMessagesMapper;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public SerbiaMessages saveNew(SerbiaMessages serbiaMessages) {
+        serbiaMessagesMapper.insert(this.buildDefaultValue(serbiaMessages));
+        return serbiaMessages;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void saveNewBatch(List<SerbiaMessages> serbiaMessagesList) {
+        if (CollUtil.isNotEmpty(serbiaMessagesList)) {
+            serbiaMessagesList.forEach(this::buildDefaultValue);
+            serbiaMessagesMapper.insertList(serbiaMessagesList);
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean update(SerbiaMessages serbiaMessages, SerbiaMessages originalSerbiaMessages) {
+        serbiaMessages.setUpdateUserId(TokenData.takeFromRequest().getUserId());
+        serbiaMessages.setCreateTime(originalSerbiaMessages.getCreateTime());
+        serbiaMessages.setUpdateTime(new Date());
+        // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。
+        UpdateWrapper<SerbiaMessages> uw = this.createUpdateQueryForNullValue(serbiaMessages, serbiaMessages.getMessageId());
+        return serbiaMessagesMapper.update(serbiaMessages, uw) == 1;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean remove(Long messageId) {
+        return serbiaMessagesMapper.deleteById(messageId) == 1;
+    }
+
+    @Override
+    public List<SerbiaMessages> getSerbiaMessagesList(SerbiaMessages filter, String orderBy) {
+        return serbiaMessagesMapper.getSerbiaMessagesList(filter, orderBy);
+    }
+
+    @Override
+    public List<SerbiaMessages> getSerbiaMessagesListWithRelation(SerbiaMessages filter, String orderBy) {
+        List<SerbiaMessages> resultList = serbiaMessagesMapper.getSerbiaMessagesList(filter, orderBy);
+        // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。
+        // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。
+        int batchSize = resultList instanceof Page ? 0 : 1000;
+        this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize);
+        return resultList;
+    }
+
+    @Override
+    public CallResult verifyRelatedData(SerbiaMessages serbiaMessages, SerbiaMessages originalSerbiaMessages) {
+        String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!";
+        //这里是基于字典的验证。
+        if (this.needToVerify(serbiaMessages, originalSerbiaMessages, SerbiaMessages::getMesssageType)
+                && !globalDictService.existDictItemFromCache("MessageType", serbiaMessages.getMesssageType())) {
+            return CallResult.error(String.format(errorMessageFormat, "消息类型 1文字 2图片 3文件"));
+        }
+        return CallResult.ok();
+    }
+
+    private SerbiaMessages buildDefaultValue(SerbiaMessages serbiaMessages) {
+        if (serbiaMessages.getMessageId() == null) {
+            serbiaMessages.setMessageId(idGenerator.nextLongId());
+        }
+        TokenData tokenData = TokenData.takeFromRequest();
+        serbiaMessages.setUpdateUserId(tokenData.getUserId());
+        Date now = new Date();
+        serbiaMessages.setCreateTime(now);
+        serbiaMessages.setUpdateTime(now);
+        serbiaMessages.setDataState(GlobalDeletedFlag.NORMAL);
+        return serbiaMessages;
+    }
+}

+ 132 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/service/impl/SerbiaUsersServiceImpl.java

@@ -0,0 +1,132 @@
+package com.serbia.webadmin.app.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.serbia.webadmin.app.service.*;
+import com.serbia.webadmin.app.dao.*;
+import com.serbia.webadmin.app.model.*;
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.common.core.constant.GlobalDeletedFlag;
+import com.serbia.common.core.object.MyRelationParam;
+import com.serbia.common.core.object.CallResult;
+import com.serbia.common.core.base.service.BaseService;
+import com.serbia.common.core.util.MyModelUtil;
+import com.serbia.common.sequence.wrapper.IdGeneratorWrapper;
+import com.serbia.common.dict.service.GlobalDictService;
+import com.github.pagehelper.Page;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+/**
+ * 塞尔用户管理数据操作服务类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Slf4j
+@Service("serbiaUsersService")
+public class SerbiaUsersServiceImpl extends BaseService<SerbiaUsers, Long> implements SerbiaUsersService {
+
+    @Autowired
+    private IdGeneratorWrapper idGenerator;
+    @Autowired
+    private SerbiaUsersMapper serbiaUsersMapper;
+    @Autowired
+    private GlobalDictService globalDictService;
+
+    /**
+     * 返回当前Service的主表Mapper对象。
+     *
+     * @return 主表Mapper对象。
+     */
+    @Override
+    protected BaseDaoMapper<SerbiaUsers> mapper() {
+        return serbiaUsersMapper;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public SerbiaUsers saveNew(SerbiaUsers serbiaUsers) {
+        serbiaUsersMapper.insert(this.buildDefaultValue(serbiaUsers));
+        return serbiaUsers;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void saveNewBatch(List<SerbiaUsers> serbiaUsersList) {
+        if (CollUtil.isNotEmpty(serbiaUsersList)) {
+            serbiaUsersList.forEach(this::buildDefaultValue);
+            serbiaUsersMapper.insertList(serbiaUsersList);
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean update(SerbiaUsers serbiaUsers, SerbiaUsers originalSerbiaUsers) {
+        MyModelUtil.fillCommonsForUpdate(serbiaUsers, originalSerbiaUsers);
+        // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。
+        UpdateWrapper<SerbiaUsers> uw = this.createUpdateQueryForNullValue(serbiaUsers, serbiaUsers.getUserId());
+        return serbiaUsersMapper.update(serbiaUsers, uw) == 1;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean remove(Long userId) {
+        return serbiaUsersMapper.deleteById(userId) == 1;
+    }
+
+    @Override
+    public List<SerbiaUsers> getSerbiaUsersList(SerbiaUsers filter, String orderBy) {
+        return serbiaUsersMapper.getSerbiaUsersList(filter, orderBy);
+    }
+
+    @Override
+    public List<SerbiaUsers> getSerbiaUsersListWithRelation(SerbiaUsers filter, String orderBy) {
+        List<SerbiaUsers> resultList = serbiaUsersMapper.getSerbiaUsersList(filter, orderBy);
+        // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。
+        // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。
+        int batchSize = resultList instanceof Page ? 0 : 1000;
+        this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize);
+        return resultList;
+    }
+
+    /**
+     * 修改用户头像。
+     *
+     * @param userId  用户主键Id。
+     * @param newHeadImage 新的头像信息。
+     * @return 成功返回true,否则false。
+     */
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean changeHeadImage(Long userId, String newHeadImage) {
+        SerbiaUsers updatedUser = new SerbiaUsers();
+        updatedUser.setUserId(userId);
+        updatedUser.setHeadImageUrl(newHeadImage);
+        return serbiaUsersMapper.updateById(updatedUser) == 1;
+    }
+
+    @Override
+    public CallResult verifyRelatedData(SerbiaUsers serbiaUsers, SerbiaUsers originalSerbiaUsers) {
+        String errorMessageFormat = "数据验证失败,关联的%s并不存在,请刷新后重试!";
+        //这里是基于字典的验证。
+        if (this.needToVerify(serbiaUsers, originalSerbiaUsers, SerbiaUsers::getSex)
+                && !globalDictService.existDictItemFromCache("UserSex", serbiaUsers.getSex())) {
+            return CallResult.error(String.format(errorMessageFormat, "性别(1:男 2:女 3:其它)"));
+        }
+        return CallResult.ok();
+    }
+
+    private SerbiaUsers buildDefaultValue(SerbiaUsers serbiaUsers) {
+        if (serbiaUsers.getUserId() == null) {
+            serbiaUsers.setUserId(idGenerator.nextLongId());
+        }
+        MyModelUtil.fillCommonsForInsert(serbiaUsers);
+        serbiaUsers.setDeletedFlag(GlobalDeletedFlag.NORMAL);
+        return serbiaUsers;
+    }
+}

+ 250 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/util/FlowIdentityExtHelper.java

@@ -0,0 +1,250 @@
+package com.serbia.webadmin.app.util;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import com.serbia.common.flow.util.BaseFlowIdentityExtHelper;
+import com.serbia.common.flow.util.FlowCustomExtFactory;
+import com.serbia.common.flow.vo.FlowUserInfoVo;
+import com.serbia.webadmin.upms.model.SysDept;
+import com.serbia.webadmin.upms.model.SysUser;
+import com.serbia.webadmin.upms.model.constant.SysUserStatus;
+import com.serbia.webadmin.upms.model.SysDeptPost;
+import com.serbia.webadmin.upms.service.SysDeptService;
+import com.serbia.webadmin.upms.service.SysUserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import jakarta.annotation.PostConstruct;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 为流程提供所需的用户身份相关的等扩展信息的帮助类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Slf4j
+@Component
+public class FlowIdentityExtHelper implements BaseFlowIdentityExtHelper {
+
+    @Autowired
+    private SysDeptService sysDeptService;
+    @Autowired
+    private SysUserService sysUserService;
+    @Autowired
+    private FlowCustomExtFactory flowCustomExtFactory;
+
+    @PostConstruct
+    public void doRegister() {
+        flowCustomExtFactory.registerFlowIdentityExtHelper(this);
+    }
+
+    @Override
+    public Long getLeaderDeptPostId(Long deptId) {
+        List<Long> deptPostIdList = sysDeptService.getLeaderDeptPostIdList(deptId);
+        return CollUtil.isEmpty(deptPostIdList) ? null : deptPostIdList.get(0);
+    }
+
+    @Override
+    public Long getUpLeaderDeptPostId(Long deptId) {
+        List<Long> deptPostIdList = sysDeptService.getUpLeaderDeptPostIdList(deptId);
+        return CollUtil.isEmpty(deptPostIdList) ? null : deptPostIdList.get(0);
+    }
+
+    @Override
+    public Map<String, String> getDeptPostIdMap(Long deptId, Set<String> postIdSet) {
+        Set<Long> postIdSet2 = postIdSet.stream().map(Long::valueOf).collect(Collectors.toSet());
+        List<SysDeptPost> deptPostList = sysDeptService.getSysDeptPostList(deptId, postIdSet2);
+        if (CollUtil.isEmpty(deptPostList)) {
+            return null;
+        }
+        Map<String, String> resultMap = new HashMap<>(deptPostList.size());
+        deptPostList.forEach(sysDeptPost ->
+                resultMap.put(sysDeptPost.getPostId().toString(), sysDeptPost.getDeptPostId().toString()));
+        return resultMap;
+    }
+
+    @Override
+    public Map<String, String> getSiblingDeptPostIdMap(Long deptId, Set<String> postIdSet) {
+        Set<Long> postIdSet2 = postIdSet.stream().map(Long::valueOf).collect(Collectors.toSet());
+        List<SysDeptPost> deptPostList = sysDeptService.getSiblingSysDeptPostList(deptId, postIdSet2);
+        if (CollUtil.isEmpty(deptPostList)) {
+            return null;
+        }
+        Map<String, String> resultMap = new HashMap<>(deptPostList.size());
+        for (SysDeptPost deptPost : deptPostList) {
+            String deptPostId = resultMap.get(deptPost.getPostId().toString());
+            if (deptPostId != null) {
+                deptPostId = deptPostId + "," + deptPost.getDeptPostId();
+            } else {
+                deptPostId = deptPost.getDeptPostId().toString();
+            }
+            resultMap.put(deptPost.getPostId().toString(), deptPostId);
+        }
+        return resultMap;
+    }
+
+    @Override
+    public Map<String, String> getUpDeptPostIdMap(Long deptId, Set<String> postIdSet) {
+        SysDept sysDept = sysDeptService.getById(deptId);
+        if (sysDept == null || sysDept.getParentId() == null) {
+            return null;
+        }
+        return getDeptPostIdMap(sysDept.getParentId(), postIdSet);
+    }
+
+    @Override
+    public Set<String> getUsernameListByRoleIds(Set<String> roleIdSet) {
+        Set<String> usernameSet = new HashSet<>();
+        Set<Long> roleIdSet2 = roleIdSet.stream().map(Long::valueOf).collect(Collectors.toSet());
+        SysUser filter = new SysUser();
+        filter.setUserStatus(SysUserStatus.STATUS_NORMAL);
+        for (Long roleId : roleIdSet2) {
+            List<SysUser> userList = sysUserService.getSysUserListByRoleId(roleId, filter, null);
+            this.extractAndAppendUsernameList(usernameSet, userList);
+        }
+        return usernameSet;
+    }
+
+    @Override
+    public List<FlowUserInfoVo> getUserInfoListByRoleIds(Set<String> roleIdSet) {
+        List<FlowUserInfoVo> resultList = new LinkedList<>();
+        Set<Long> roleIdSet2 = roleIdSet.stream().map(Long::valueOf).collect(Collectors.toSet());
+        SysUser filter = new SysUser();
+        filter.setUserStatus(SysUserStatus.STATUS_NORMAL);
+        for (Long roleId : roleIdSet2) {
+            List<SysUser> userList = sysUserService.getSysUserListByRoleId(roleId, filter, null);
+            if (CollUtil.isNotEmpty(userList)) {
+                resultList.addAll(BeanUtil.copyToList(userList, FlowUserInfoVo.class));
+            }
+        }
+        return resultList;
+    }
+
+    @Override
+    public Set<String> getUsernameListByDeptIds(Set<String> deptIdSet) {
+        Set<String> usernameSet = new HashSet<>();
+        Set<Long> deptIdSet2 = deptIdSet.stream().map(Long::valueOf).collect(Collectors.toSet());
+        for (Long deptId : deptIdSet2) {
+            SysUser filter = new SysUser();
+            filter.setDeptId(deptId);
+            filter.setUserStatus(SysUserStatus.STATUS_NORMAL);
+            List<SysUser> userList = sysUserService.getSysUserList(filter, null);
+            this.extractAndAppendUsernameList(usernameSet, userList);
+        }
+        return usernameSet;
+    }
+
+    @Override
+    public List<FlowUserInfoVo> getUserInfoListByDeptIds(Set<String> deptIdSet) {
+        List<FlowUserInfoVo> resultList = new LinkedList<>();
+        Set<Long> deptIdSet2 = deptIdSet.stream().map(Long::valueOf).collect(Collectors.toSet());
+        for (Long deptId : deptIdSet2) {
+            SysUser filter = new SysUser();
+            filter.setDeptId(deptId);
+            filter.setUserStatus(SysUserStatus.STATUS_NORMAL);
+            List<SysUser> userList = sysUserService.getListByFilter(filter);
+            if (CollUtil.isNotEmpty(userList)) {
+                resultList.addAll(BeanUtil.copyToList(userList, FlowUserInfoVo.class));
+            }
+        }
+        return resultList;
+    }
+
+    @Override
+    public Set<String> getUsernameListByPostIds(Set<String> postIdSet) {
+        Set<String> usernameSet = new HashSet<>();
+        Set<Long> postIdSet2 = postIdSet.stream().map(Long::valueOf).collect(Collectors.toSet());
+        SysUser filter = new SysUser();
+        filter.setUserStatus(SysUserStatus.STATUS_NORMAL);
+        for (Long postId : postIdSet2) {
+            List<SysUser> userList = sysUserService.getSysUserListByPostId(postId, filter, null);
+            this.extractAndAppendUsernameList(usernameSet, userList);
+        }
+        return usernameSet;
+    }
+
+    @Override
+    public List<FlowUserInfoVo> getUserInfoListByPostIds(Set<String> postIdSet) {
+        List<FlowUserInfoVo> resultList = new LinkedList<>();
+        Set<Long> postIdSet2 = postIdSet.stream().map(Long::valueOf).collect(Collectors.toSet());
+        SysUser filter = new SysUser();
+        filter.setUserStatus(SysUserStatus.STATUS_NORMAL);
+        for (Long postId : postIdSet2) {
+            List<SysUser> userList = sysUserService.getSysUserListByPostId(postId, filter, null);
+            if (CollUtil.isNotEmpty(userList)) {
+                resultList.addAll(BeanUtil.copyToList(userList, FlowUserInfoVo.class));
+            }
+        }
+        return resultList;
+    }
+
+    @Override
+    public Set<String> getUsernameListByDeptPostIds(Set<String> deptPostIdSet) {
+        Set<String> usernameSet = new HashSet<>();
+        Set<Long> deptPostIdSet2 = deptPostIdSet.stream().map(Long::valueOf).collect(Collectors.toSet());
+        SysUser filter = new SysUser();
+        filter.setUserStatus(SysUserStatus.STATUS_NORMAL);
+        for (Long deptPostId : deptPostIdSet2) {
+            List<SysUser> userList = sysUserService.getSysUserListByDeptPostId(deptPostId, filter, null);
+            this.extractAndAppendUsernameList(usernameSet, userList);
+        }
+        return usernameSet;
+    }
+
+    @Override
+    public List<FlowUserInfoVo> getUserInfoListByDeptPostIds(Set<String> deptPostIdSet) {
+        List<FlowUserInfoVo> resultList = new LinkedList<>();
+        if (CollUtil.isEmpty(deptPostIdSet)) {
+            return resultList;
+        }
+        Set<Long> deptPostIdSet2 = deptPostIdSet.stream().map(Long::valueOf).collect(Collectors.toSet());
+        SysUser filter = new SysUser();
+        filter.setUserStatus(SysUserStatus.STATUS_NORMAL);
+        for (Long deptPostId : deptPostIdSet2) {
+            List<SysUser> userList = sysUserService.getSysUserListByDeptPostId(deptPostId, filter, null);
+            if (CollUtil.isNotEmpty(userList)) {
+                resultList.addAll(BeanUtil.copyToList(userList, FlowUserInfoVo.class));
+            }
+        }
+        return resultList;
+    }
+
+    @Override
+    public List<FlowUserInfoVo> getUserInfoListByUsernameSet(Set<String> usernameSet) {
+        List<FlowUserInfoVo> resultList = new LinkedList<>();
+        if (CollUtil.isEmpty(usernameSet)) {
+            return resultList;
+        }
+        List<SysUser> userList = sysUserService.getInList("loginName", usernameSet);
+        if (CollUtil.isNotEmpty(userList)) {
+            resultList = BeanUtil.copyToList(userList, FlowUserInfoVo.class);
+        }
+        return resultList;
+    }
+
+    @Override
+    public Boolean supprtDataPerm() {
+        return true;
+    }
+
+    @Override
+    public Map<String, String> mapUserShowNameByLoginName(Set<String> loginNameSet) {
+        if (CollUtil.isEmpty(loginNameSet)) {
+            return new HashMap<>(1);
+        }
+        Map<String, String> resultMap = new HashMap<>(loginNameSet.size());
+        List<SysUser> userList = sysUserService.getInList("loginName", loginNameSet);
+        userList.forEach(user -> resultMap.put(user.getLoginName(), user.getShowName()));
+        return resultMap;
+    }
+
+    private void extractAndAppendUsernameList(Set<String> resultUsernameList, List<SysUser> userList) {
+        List<String> usernameList = userList.stream().map(SysUser::getLoginName).collect(Collectors.toList());
+        if (CollUtil.isNotEmpty(usernameList)) {
+            resultUsernameList.addAll(usernameList);
+        }
+    }
+}

+ 60 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/vo/SerbiaCountryCodeVo.java

@@ -0,0 +1,60 @@
+package com.serbia.webadmin.app.vo;
+
+import com.serbia.common.core.base.vo.BaseVo;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 国家区域编码管理VO视图对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Schema(description = "国家区域编码管理VO视图对象")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SerbiaCountryCodeVo extends BaseVo {
+
+    /**
+     * 主键。
+     */
+    @Schema(description = "主键")
+    private Long id;
+
+    /**
+     * 英文名称。
+     */
+    @Schema(description = "英文名称")
+    private String englishName;
+
+    /**
+     * 国家/地区。
+     */
+    @Schema(description = "国家/地区")
+    private String country;
+
+    /**
+     * 国家代码。
+     */
+    @Schema(description = "国家代码")
+    private String countryCode;
+
+    /**
+     * 区号。
+     */
+    @Schema(description = "区号")
+    private String areaCode;
+
+    /**
+     * 拼音。
+     */
+    @Schema(description = "拼音")
+    private String pinYin;
+
+    /**
+     * 拼音首字母。
+     */
+    @Schema(description = "拼音首字母")
+    private String firstCode;
+}

+ 49 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/vo/SerbiaGroupMembersVo.java

@@ -0,0 +1,49 @@
+package com.serbia.webadmin.app.vo;
+
+import com.serbia.common.core.base.vo.BaseVo;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import java.util.Map;
+
+/**
+ * 聊天群组成员管理VO视图对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Schema(description = "聊天群组成员管理VO视图对象")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SerbiaGroupMembersVo extends BaseVo {
+
+    /**
+     * 主键id。
+     */
+    @Schema(description = "主键id")
+    private Long id;
+
+    /**
+     * 群聊id。
+     */
+    @Schema(description = "群聊id")
+    private Long groupId;
+
+    /**
+     * 用户id。
+     */
+    @Schema(description = "用户id")
+    private Long userId;
+
+    /**
+     * 成员角色 1群主 2群成员。
+     */
+    @Schema(description = "成员角色 1群主 2群成员")
+    private Integer groupRole;
+
+    /**
+     * groupRole 全局字典关联数据。
+     */
+    @Schema(description = "groupRole 全局字典关联数据")
+    private Map<String, Object> groupRoleDictMap;
+}

+ 55 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/vo/SerbiaGroupsVo.java

@@ -0,0 +1,55 @@
+package com.serbia.webadmin.app.vo;
+
+import com.serbia.common.core.base.vo.BaseVo;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import java.util.Map;
+
+/**
+ * 聊天群组管理VO视图对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Schema(description = "聊天群组管理VO视图对象")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SerbiaGroupsVo extends BaseVo {
+
+    /**
+     * 主键id。
+     */
+    @Schema(description = "主键id")
+    private Long groupId;
+
+    /**
+     * 群主id。
+     */
+    @Schema(description = "群主id")
+    private Long groupOwnerId;
+
+    /**
+     * 群聊名称。
+     */
+    @Schema(description = "群聊名称")
+    private String groupName;
+
+    /**
+     * 群聊头像。
+     */
+    @Schema(description = "群聊头像")
+    private String groupAvatar;
+
+    /**
+     * 是否公开 0私密 1公开。
+     */
+    @Schema(description = "是否私密 0公开 1私密")
+    private Integer isPrivate;
+
+    /**
+     * isPrivate 全局字典关联数据。
+     */
+    @Schema(description = "isPrivate 全局字典关联数据")
+    private Map<String, Object> isPrivateDictMap;
+}

+ 90 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/vo/SerbiaMessagesVo.java

@@ -0,0 +1,90 @@
+package com.serbia.webadmin.app.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * 聊天信息管理VO视图对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Schema(description = "聊天信息管理VO视图对象")
+@Data
+public class SerbiaMessagesVo {
+
+    /**
+     * 主键id。
+     */
+    @Schema(description = "主键id")
+    private Long messageId;
+
+    /**
+     * 发送人id。
+     */
+    @Schema(description = "发送人id")
+    private Long senderId;
+
+    /**
+     * 接收者id。
+     */
+    @Schema(description = "接收者id")
+    private Long receiverId;
+
+    /**
+     * 群组id。
+     */
+    @Schema(description = "群组id")
+    private Long groupId;
+
+    /**
+     * 消息类型 1文字 2图片 3文件。
+     */
+    @Schema(description = "消息类型 1文字 2图片 3文件")
+    private Integer messsageType;
+
+    /**
+     * 消息内容。
+     */
+    @Schema(description = "消息内容")
+    private String messageContent;
+
+    /**
+     * 创建时间。
+     */
+    @Schema(description = "创建时间")
+    private Date createTime;
+
+    /**
+     * 更新用户。
+     */
+    @Schema(description = "更新用户")
+    private Long updateUserId;
+
+    /**
+     * 更新时间。
+     */
+    @Schema(description = "更新时间")
+    private Date updateTime;
+
+    /**
+     * 消息已读状态 0未读 1已读。
+     */
+    @Schema(description = "消息已读状态 0未读 1已读")
+    private Integer readStatus;
+
+    /**
+     * messsageType 全局字典关联数据。
+     */
+    @Schema(description = "messsageType 全局字典关联数据")
+    private Map<String, Object> messsageTypeDictMap;
+
+    /**
+     * readStatus 全局字典关联数据。
+     */
+    @Schema(description = "readStatus 全局字典关联数据")
+    private Map<String, Object> readStatusDictMap;
+}

+ 117 - 0
application-webadmin/src/main/java/com/serbia/webadmin/app/vo/SerbiaUsersVo.java

@@ -0,0 +1,117 @@
+package com.serbia.webadmin.app.vo;
+
+import com.serbia.common.core.base.vo.BaseVo;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * 塞尔用户管理VO视图对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Schema(description = "塞尔用户管理VO视图对象")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SerbiaUsersVo extends BaseVo {
+
+    /**
+     * 主键Id。
+     */
+    @Schema(description = "主键Id")
+    private Long userId;
+
+    /**
+     * 用户登录名称。
+     */
+    @Schema(description = "用户登录名称")
+    private String loginName;
+
+    /**
+     * 密码。
+     */
+    @Schema(description = "密码")
+    private String password;
+
+    /**
+     * 昵称。
+     */
+    @Schema(description = "昵称")
+    private String showName;
+
+    /**
+     * 用户头像的Url。
+     */
+    @Schema(description = "用户头像的Url")
+    private String headImageUrl;
+
+    /**
+     * 状态(0: 正常 1: 锁定)。
+     */
+    @Schema(description = "状态(0: 正常 1: 锁定)")
+    private Integer userStatus;
+
+    /**
+     * 用户邮箱。
+     */
+    @Schema(description = "用户邮箱")
+    private String email;
+
+    /**
+     * 用户手机。
+     */
+    @Schema(description = "用户手机")
+    private String mobile;
+
+    /**
+     * 性别(1:男 2:女 3:其它)。
+     */
+    @Schema(description = "性别(1:男 2:女 3:其它)")
+    private String sex;
+
+    /**
+     * 生日。
+     */
+    @Schema(description = "生日")
+    private Date birthday;
+
+    /**
+     * 手机号所属国家code。
+     */
+    @Schema(description = "手机号所属国家code")
+    private String countryCode;
+
+    /**
+     * 住址。
+     */
+    @Schema(description = "住址")
+    private String address;
+
+    /**
+     * 职业。
+     */
+    @Schema(description = "职业")
+    private String job;
+
+    /**
+     * 个性签名。
+     */
+    @Schema(description = "个性签名")
+    private String personalSign;
+
+    /**
+     * userStatus 常量字典关联数据。
+     */
+    @Schema(description = "userStatus 常量字典关联数据")
+    private Map<String, Object> userStatusDictMap;
+
+    /**
+     * sex 全局字典关联数据。
+     */
+    @Schema(description = "sex 全局字典关联数据")
+    private Map<String, Object> sexDictMap;
+}

+ 38 - 0
application-webadmin/src/main/java/com/serbia/webadmin/config/ApplicationConfig.java

@@ -0,0 +1,38 @@
+package com.serbia.webadmin.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 应用程序自定义的程序属性配置文件。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "application")
+public class ApplicationConfig {
+    /**
+     * 用户密码被重置之后的缺省密码
+     */
+    private String defaultUserPassword;
+    /**
+     * 上传文件的基础目录
+     */
+    private String uploadFileBaseDir;
+    /**
+     * 授信ip列表,没有填写表示全部信任。多个ip之间逗号分隔,如: http://10.10.10.1:8080,http://10.10.10.2:8080
+     */
+    private String credentialIpList;
+    /**
+     * Session的用户权限在Redis中的过期时间(秒)。一定要和sa-token.timeout
+     * 缺省值是 one day
+     */
+    private int sessionExpiredSeconds = 86400;
+    /**
+     * 是否排他登录。
+     */
+    private Boolean excludeLogin = false;
+}

+ 49 - 0
application-webadmin/src/main/java/com/serbia/webadmin/config/DataSourceType.java

@@ -0,0 +1,49 @@
+package com.serbia.webadmin.config;
+
+import com.serbia.common.core.constant.ApplicationConstant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 表示数据源类型的常量对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public final class DataSourceType {
+
+    public static final int TEST = 0;
+    /**
+     * 以下所有数据源的类都型是固定值。如果有冲突,请修改上面定义的业务服务的数据源类型值。
+     */
+    public static final int OPERATION_LOG = ApplicationConstant.OPERATION_LOG_DATASOURCE_TYPE;
+    public static final int GLOBAL_DICT = ApplicationConstant.COMMON_GLOBAL_DICT_TYPE;
+    public static final int COMMON_FLOW_AND_ONLINE = ApplicationConstant.COMMON_FLOW_AND_ONLINE_DATASOURCE_TYPE;
+    public static final int COMMON_REPORT = ApplicationConstant.COMMON_REPORT_DATASOURCE_TYPE;
+
+    private static final Map<String, Integer> TYPE_MAP = new HashMap<>(8);
+    static {
+        TYPE_MAP.put("test", TEST);
+        TYPE_MAP.put("operation-log", OPERATION_LOG);
+        TYPE_MAP.put("global-dict", GLOBAL_DICT);
+        TYPE_MAP.put("common-flow-online", COMMON_FLOW_AND_ONLINE);
+        TYPE_MAP.put("common-report", COMMON_REPORT);
+    }
+
+    /**
+     * 根据名称获取字典类型。
+     *
+     * @param name 数据源在配置中的名称。
+     * @return 返回可用于多数据源切换的数据源类型。
+     */
+    public static Integer getDataSourceTypeByName(String name) {
+        return TYPE_MAP.get(name);
+    }
+
+    /**
+     * 私有构造函数,明确标识该常量类的作用。
+     */
+    private DataSourceType() {
+    }
+}

+ 58 - 0
application-webadmin/src/main/java/com/serbia/webadmin/config/FilterConfig.java

@@ -0,0 +1,58 @@
+package com.serbia.webadmin.config;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+import jakarta.servlet.Filter;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 这里主要配置Web的各种过滤器和监听器等Servlet容器组件。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Configuration
+public class FilterConfig {
+
+    /**
+     * 配置Ajax跨域过滤器。
+     */
+    @Bean
+    public CorsFilter corsFilterRegistration(ApplicationConfig applicationConfig) {
+        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
+        CorsConfiguration corsConfiguration = new CorsConfiguration();
+        if (StringUtils.isNotBlank(applicationConfig.getCredentialIpList())) {
+            if ("*".equals(applicationConfig.getCredentialIpList())) {
+                corsConfiguration.addAllowedOriginPattern("*");
+            } else {
+                String[] credentialIpList = StringUtils.split(applicationConfig.getCredentialIpList(), ",");
+                for (String ip : credentialIpList) {
+                    corsConfiguration.addAllowedOrigin(ip);
+                }
+            }
+            corsConfiguration.addAllowedHeader("*");
+            corsConfiguration.addAllowedMethod("*");
+            corsConfiguration.setAllowCredentials(true);
+            configSource.registerCorsConfiguration("/**", corsConfiguration);
+        }
+        return new CorsFilter(configSource);
+    }
+
+    @Bean
+    public FilterRegistrationBean<Filter> characterEncodingFilterRegistration() {
+        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(
+                new org.springframework.web.filter.CharacterEncodingFilter());
+        filterRegistrationBean.addUrlPatterns("/*");
+        filterRegistrationBean.addInitParameter("encoding", StandardCharsets.UTF_8.name());
+        // forceEncoding强制response也被编码,另外即使request中已经设置encoding,forceEncoding也会重新设置
+        filterRegistrationBean.addInitParameter("forceEncoding", "true");
+        filterRegistrationBean.setAsyncSupported(true);
+        return filterRegistrationBean;
+    }
+}

+ 21 - 0
application-webadmin/src/main/java/com/serbia/webadmin/config/InterceptorConfig.java

@@ -0,0 +1,21 @@
+package com.serbia.webadmin.config;
+
+import com.serbia.webadmin.interceptor.AuthenticationInterceptor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * 所有的项目拦截器都在这里集中配置
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Configuration
+public class InterceptorConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/admin/**");
+    }
+}

+ 88 - 0
application-webadmin/src/main/java/com/serbia/webadmin/config/MultiDataSourceConfig.java

@@ -0,0 +1,88 @@
+package com.serbia.webadmin.config;
+
+import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
+import com.serbia.common.core.config.BaseMultiDataSourceConfig;
+import com.serbia.common.core.config.DynamicDataSource;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+import org.mybatis.spring.annotation.MapperScan;
+
+import javax.sql.DataSource;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 多数据源配置对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Configuration
+@EnableTransactionManagement
+@MapperScan(value = {"com.serbia.webadmin.*.dao", "com.serbia.common.*.dao"})
+public class MultiDataSourceConfig extends BaseMultiDataSourceConfig {
+
+    @Bean(initMethod = "init", destroyMethod = "close")
+    @ConfigurationProperties(prefix = "spring.datasource.druid.test")
+    public DataSource testDataSource() {
+        return super.applyCommonProps(DruidDataSourceBuilder.create().build());
+    }
+
+    /**
+     * 默认生成的用于保存操作日志的数据源,可根据需求修改。
+     * 这里我们还是非常推荐给操作日志使用独立的数据源,这样便于今后的数据迁移。
+     */
+    @Bean(initMethod = "init", destroyMethod = "close")
+    @ConfigurationProperties(prefix = "spring.datasource.druid.operation-log")
+    public DataSource operationLogDataSource() {
+        return super.applyCommonProps(DruidDataSourceBuilder.create().build());
+    }
+
+    /**
+     * 默认生成的用于全局编码字典的数据源,可根据需求修改。
+     */
+    @Bean(initMethod = "init", destroyMethod = "close")
+    @ConfigurationProperties(prefix = "spring.datasource.druid.global-dict")
+    public DataSource globalDictDataSource() {
+        return super.applyCommonProps(DruidDataSourceBuilder.create().build());
+    }
+
+    /**
+     * 默认生成的用于在线表单内部表的数据源,可根据需求修改。
+     * 这里我们还是非常推荐使用独立数据源,这样便于今后的服务拆分。
+     */
+    @Bean(initMethod = "init", destroyMethod = "close")
+    @ConfigurationProperties(prefix = "spring.datasource.druid.common-flow-online")
+    public DataSource commonFlowAndOnlineDataSource() {
+        return super.applyCommonProps(DruidDataSourceBuilder.create().build());
+    }
+
+    /**
+     * 默认生成的用于统计打印内部表的数据源,可根据需求修改。
+     * 这里我们还是非常推荐使用独立数据源,这样便于今后的服务拆分。
+     */
+    @Bean(initMethod = "init", destroyMethod = "close")
+    @ConfigurationProperties(prefix = "spring.datasource.druid.common-report")
+    public DataSource commonReportDataSource() {
+        return super.applyCommonProps(DruidDataSourceBuilder.create().build());
+    }
+
+    @Bean
+    @Primary
+    public DynamicDataSource dataSource() {
+        Map<Object, Object> targetDataSources = new HashMap<>(1);
+        targetDataSources.put(DataSourceType.TEST, testDataSource());
+        targetDataSources.put(DataSourceType.OPERATION_LOG, operationLogDataSource());
+        targetDataSources.put(DataSourceType.GLOBAL_DICT, globalDictDataSource());
+        targetDataSources.put(DataSourceType.COMMON_FLOW_AND_ONLINE, commonFlowAndOnlineDataSource());
+        targetDataSources.put(DataSourceType.COMMON_REPORT, commonReportDataSource());
+        // 如果当前工程支持在线表单,这里请务必保证upms数据表所在数据库为缺省数据源。
+        DynamicDataSource dynamicDataSource = new DynamicDataSource();
+        dynamicDataSource.setTargetDataSources(targetDataSources);
+        dynamicDataSource.setDefaultTargetDataSource(testDataSource());
+        return dynamicDataSource;
+    }
+}

+ 66 - 0
application-webadmin/src/main/java/com/serbia/webadmin/config/ThirdPartyAuthConfig.java

@@ -0,0 +1,66 @@
+package com.serbia.webadmin.config;
+
+import cn.hutool.core.collection.CollUtil;
+import lombok.Data;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 第三方应用鉴权配置。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "third-party")
+public class ThirdPartyAuthConfig implements InitializingBean {
+
+    private List<AuthProperties> auth;
+
+    private Map<String, AuthProperties> applicationMap;
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        if (CollUtil.isEmpty(auth)) {
+            applicationMap = new HashMap<>(1);
+        } else {
+            applicationMap = auth.stream().collect(Collectors.toMap(AuthProperties::getAppCode, c -> c));
+        }
+    }
+
+    @Data
+    public static class AuthProperties {
+        /**
+         * 应用Id。
+         */
+        private String appCode;
+        /**
+         * 身份验证相关url的base地址。
+         */
+        private String baseUrl;
+        /**
+         * 是否为橙单框架。
+         */
+        private Boolean orangeFramework = true;
+        /**
+         * token的Http Request Header的key
+         */
+        private String tokenHeaderKey;
+        /**
+         * 数据权限和用户操作权限缓存过期时间,单位秒。
+         */
+        private Integer permExpiredSeconds = 86400;
+        /**
+         * 用户Token缓存过期时间,单位秒。
+         * 如果为0,则每次都要去第三方服务进行验证。
+         */
+        private Integer tokenExpiredSeconds = 0;
+    }
+}

+ 31 - 0
application-webadmin/src/main/java/com/serbia/webadmin/config/WebSocketAutoConfig.java

@@ -0,0 +1,31 @@
+package com.serbia.webadmin.config;
+
+import com.serbia.webadmin.websocket.handler.SingleWebSocketHandler;
+import com.serbia.webadmin.websocket.interceptor.SingleInterceptor;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
+
+@Configuration
+@EnableWebSocket
+public class WebSocketAutoConfig implements WebSocketConfigurer, ApplicationContextAware {
+
+    private static ApplicationContext applicationContext;
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+      if (WebSocketAutoConfig.applicationContext == null){
+        WebSocketAutoConfig.applicationContext = applicationContext;
+      }
+    }
+
+    @Override
+    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
+      registry.addHandler(new SingleWebSocketHandler(applicationContext), "/single")
+              .addInterceptors(new SingleInterceptor())
+              .setAllowedOrigins("*");
+    }
+}

+ 281 - 0
application-webadmin/src/main/java/com/serbia/webadmin/interceptor/AuthenticationInterceptor.java

@@ -0,0 +1,281 @@
+package com.serbia.webadmin.interceptor;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.text.StrFormatter;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.TypeReference;
+import com.serbia.common.core.cache.CacheConfig;
+import com.serbia.common.core.constant.ApplicationConstant;
+import com.serbia.common.core.constant.DataPermRuleType;
+import com.serbia.common.core.constant.ErrorCodeEnum;
+import com.serbia.common.core.exception.MyRuntimeException;
+import com.serbia.common.core.object.ResponseResult;
+import com.serbia.common.core.object.TokenData;
+import com.serbia.common.core.util.ApplicationContextHolder;
+import com.serbia.common.core.util.RedisKeyUtil;
+import com.serbia.common.satoken.util.SaTokenUtil;
+import com.serbia.webadmin.config.ThirdPartyAuthConfig;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RBucket;
+import org.redisson.api.RSet;
+import org.redisson.api.RedissonClient;
+import org.springframework.cache.Cache;
+import org.springframework.cache.CacheManager;
+import org.springframework.util.Assert;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * 登录用户Token验证、生成和权限验证的拦截器。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Slf4j
+public class AuthenticationInterceptor implements HandlerInterceptor {
+
+    private final ThirdPartyAuthConfig thirdPartyAuthConfig =
+            ApplicationContextHolder.getBean("thirdPartyAuthConfig");
+
+    private final RedissonClient redissonClient = ApplicationContextHolder.getBean(RedissonClient.class);
+    private final CacheManager cacheManager = ApplicationContextHolder.getBean("caffeineCacheManager");
+
+    private final SaTokenUtil saTokenUtil =
+            ApplicationContextHolder.getBean("saTokenUtil");
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
+            throws Exception {
+        String appCode = this.getAppCodeFromRequest(request);
+        if (StrUtil.isNotBlank(appCode)) {
+            return this.handleThirdPartyRequest(appCode, request);
+        }
+        ResponseResult<Void> result = saTokenUtil.handleAuthIntercept(request, handler);
+        if (!result.isSuccess()) {
+            ResponseResult.output(result.getHttpStatus(), result);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
+                           ModelAndView modelAndView) throws Exception {
+        // 这里需要空注解,否则sonar会不happy。
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
+            throws Exception {
+        // 这里需要空注解,否则sonar会不happy。
+    }
+
+    private String getTokenFromRequest(HttpServletRequest request, String appCode) {
+        ThirdPartyAuthConfig.AuthProperties prop = thirdPartyAuthConfig.getApplicationMap().get(appCode);
+        String token = request.getHeader(prop.getTokenHeaderKey());
+        if (StrUtil.isBlank(token)) {
+            token = request.getParameter(prop.getTokenHeaderKey());
+        }
+        if (StrUtil.isBlank(token)) {
+            token = request.getHeader(ApplicationConstant.HTTP_HEADER_INTERNAL_TOKEN);
+        }
+        return token;
+    }
+
+    private String getAppCodeFromRequest(HttpServletRequest request) {
+        String appCode = request.getHeader("AppCode");
+        if (StrUtil.isBlank(appCode)) {
+            appCode = request.getParameter("AppCode");
+        }
+        return appCode;
+    }
+
+    private boolean handleThirdPartyRequest(String appCode, HttpServletRequest request) throws IOException {
+        String token = this.getTokenFromRequest(request, appCode);
+        ThirdPartyAuthConfig.AuthProperties authProps = thirdPartyAuthConfig.getApplicationMap().get(appCode);
+        if (authProps == null) {
+            String msg = StrFormatter.format("请求的 appCode[{}] 信息,在当前服务中尚未配置!", appCode);
+            ResponseResult.output(ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, msg));
+            return false;
+        }
+        ResponseResult<TokenData> result = this.getAndCacheThirdPartyTokenData(authProps, token);
+        if (!result.isSuccess()) {
+            ResponseResult.output(result.getHttpStatus(),
+                    ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, result.getErrorMessage()));
+            return false;
+        }
+        TokenData tokenData = result.getData();
+        tokenData.setAppCode(appCode);
+        tokenData.setSessionId(this.prependAppCode(authProps.getAppCode(), tokenData.getSessionId()));
+        TokenData.addToRequest(tokenData);
+        String url = request.getRequestURI();
+        if (Boolean.FALSE.equals(tokenData.getIsAdmin())
+                && !this.hasThirdPartyPermission(authProps, tokenData, url)) {
+            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION));
+            return false;
+        }
+        return true;
+    }
+
+    private ResponseResult<TokenData> getAndCacheThirdPartyTokenData(
+            ThirdPartyAuthConfig.AuthProperties authProps, String token) {
+        if (authProps.getTokenExpiredSeconds() == 0) {
+            return this.getThirdPartyTokenData(authProps, token);
+        }
+        String tokeKey = this.prependAppCode(authProps.getAppCode(), RedisKeyUtil.makeSessionIdKey(token));
+        RBucket<String> sessionData = redissonClient.getBucket(tokeKey);
+        if (sessionData.isExists()) {
+            return ResponseResult.success(JSON.parseObject(sessionData.get(), TokenData.class));
+        }
+        ResponseResult<TokenData> responseResult = this.getThirdPartyTokenData(authProps, token);
+        if (responseResult.isSuccess()) {
+            sessionData.set(JSON.toJSONString(responseResult.getData()), authProps.getTokenExpiredSeconds(), TimeUnit.SECONDS);
+        }
+        return responseResult;
+    }
+
+    private String prependAppCode(String appCode, String key) {
+        return appCode.toUpperCase() + ":" + key;
+    }
+
+    private ResponseResult<TokenData> getThirdPartyTokenData(
+            ThirdPartyAuthConfig.AuthProperties authProps, String token) {
+        try {
+            String resultData = this.invokeThirdPartyUrl(authProps.getBaseUrl() + "/getTokenData", token);
+            return JSON.parseObject(resultData, new TypeReference<ResponseResult<TokenData>>() {});
+        } catch (MyRuntimeException ex) {
+            return ResponseResult.error(ErrorCodeEnum.FAILED_TO_INVOKE_THIRDPARTY_URL, ex.getMessage());
+        }
+    }
+
+    private ResponseResult<ThirdPartyAppPermData> getThirdPartyPermData(
+            ThirdPartyAuthConfig.AuthProperties authProps, String token) {
+        try {
+            String resultData = this.invokeThirdPartyUrl(authProps.getBaseUrl() + "/getPermData", token);
+            return JSON.parseObject(resultData, new TypeReference<ResponseResult<ThirdPartyAppPermData>>() {});
+        } catch (MyRuntimeException ex) {
+            return ResponseResult.error(ErrorCodeEnum.FAILED_TO_INVOKE_THIRDPARTY_URL, ex.getMessage());
+        }
+    }
+
+    private String invokeThirdPartyUrl(String url, String token) {
+        Map<String, String> headerMap = new HashMap<>(1);
+        headerMap.put("Authorization", token);
+        StringBuilder fullUrl = new StringBuilder(128);
+        fullUrl.append(url).append("?token=").append(token);
+        HttpResponse httpResponse = HttpUtil.createGet(fullUrl.toString()).addHeaders(headerMap).execute();
+        if (!httpResponse.isOk()) {
+            String msg = StrFormatter.format(
+                    "Failed to call [{}] with ERROR HTTP Status [{}] and [{}].",
+                    url, httpResponse.getStatus(), httpResponse.body());
+            log.error(msg);
+            throw new MyRuntimeException(msg);
+        }
+        return httpResponse.body();
+    }
+
+    @SuppressWarnings("unchecked")
+    private boolean hasThirdPartyPermission(
+            ThirdPartyAuthConfig.AuthProperties authProps, TokenData tokenData, String url) {
+        // 为了提升效率,先检索Caffeine的一级缓存,如果不存在,再检索Redis的二级缓存,并将结果存入一级缓存。
+        String permKey = RedisKeyUtil.makeSessionPermIdKey(tokenData.getSessionId());
+        Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.USER_PERMISSION_CACHE.name());
+        Assert.notNull(cache, "Cache USER_PERMISSION_CACHE can't be NULL");
+        Cache.ValueWrapper wrapper = cache.get(permKey);
+        if (wrapper != null) {
+            Object cachedData = wrapper.get();
+            if (cachedData != null) {
+                return ((Set<String>) cachedData).contains(url);
+            }
+        }
+        Set<String> localPermSet;
+        RSet<String> permSet = redissonClient.getSet(permKey);
+        if (permSet.isExists()) {
+            localPermSet = permSet.readAll();
+            cache.put(permKey, localPermSet);
+            return localPermSet.contains(url);
+        }
+        ResponseResult<ThirdPartyAppPermData> responseResult = this.getThirdPartyPermData(authProps, tokenData.getToken());
+        this.cacheThirdPartyDataPermData(authProps, tokenData, responseResult.getData().getDataPerms());
+        if (CollUtil.isEmpty(responseResult.getData().urlPerms)) {
+            return false;
+        }
+        permSet.addAll(responseResult.getData().urlPerms);
+        permSet.expire(authProps.getPermExpiredSeconds(), TimeUnit.SECONDS);
+        localPermSet = new HashSet<>(responseResult.getData().urlPerms);
+        cache.put(permKey, localPermSet);
+        return localPermSet.contains(url);
+    }
+
+    private void cacheThirdPartyDataPermData(
+            ThirdPartyAuthConfig.AuthProperties authProps, TokenData tokenData, List<ThirdPartyAppDataPermData> dataPerms) {
+        if (CollUtil.isEmpty(dataPerms)) {
+            return;
+        }
+        Map<Integer, List<ThirdPartyAppDataPermData>> dataPermMap =
+                dataPerms.stream().collect(Collectors.groupingBy(ThirdPartyAppDataPermData::getRuleType));
+        Map<Integer, List<ThirdPartyAppDataPermData>> normalizedDataPermMap = new HashMap<>(dataPermMap.size());
+        for (Map.Entry<Integer, List<ThirdPartyAppDataPermData>> entry : dataPermMap.entrySet()) {
+            List<ThirdPartyAppDataPermData> ruleTypeDataPermDataList;
+            if (entry.getKey().equals(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT)) {
+                ruleTypeDataPermDataList =
+                        normalizedDataPermMap.computeIfAbsent(DataPermRuleType.TYPE_CUSTOM_DEPT_LIST, k -> new LinkedList<>());
+            } else {
+                ruleTypeDataPermDataList =
+                        normalizedDataPermMap.computeIfAbsent(entry.getKey(), k -> new LinkedList<>());
+            }
+            ruleTypeDataPermDataList.addAll(entry.getValue());
+        }
+        Map<Integer, String> resultDataPermMap = new HashMap<>(normalizedDataPermMap.size());
+        for (Map.Entry<Integer, List<ThirdPartyAppDataPermData>> entry : normalizedDataPermMap.entrySet()) {
+            if (entry.getKey().equals(DataPermRuleType.TYPE_CUSTOM_DEPT_LIST)) {
+                String deptIds = entry.getValue().stream()
+                        .map(ThirdPartyAppDataPermData::getDeptIds).collect(Collectors.joining(","));
+                resultDataPermMap.put(entry.getKey(), deptIds);
+            } else {
+                resultDataPermMap.put(entry.getKey(), "null");
+            }
+        }
+        Map<String, Map<Integer, String>> menuDataPermMap = new HashMap<>(1);
+        menuDataPermMap.put(ApplicationConstant.DATA_PERM_ALL_MENU_ID, resultDataPermMap);
+        String dataPermSessionKey = RedisKeyUtil.makeSessionDataPermIdKey(tokenData.getSessionId());
+        RBucket<String> bucket = redissonClient.getBucket(dataPermSessionKey);
+        bucket.set(JSON.toJSONString(menuDataPermMap), authProps.getPermExpiredSeconds(), TimeUnit.SECONDS);
+    }
+
+    @Data
+    public static class ThirdPartyAppPermData {
+        /**
+         * 当前用户会话可访问的url接口地址列表。
+         */
+        private List<String> urlPerms;
+        /**
+         * 当前用户会话的数据权限列表。
+         */
+        private List<ThirdPartyAppDataPermData> dataPerms;
+    }
+
+    @Data
+    public static class ThirdPartyAppDataPermData {
+        /**
+         * 数据权限的规则类型。需要按照橙单的约定返回。具体值可参考DataPermRuleType常量类。
+         */
+        private Integer ruleType;
+        /**
+         * 部门Id集合,多个部门Id之间逗号分隔。
+         * 注意:仅当ruleType为3、4、5时需要包含该字段值。
+         */
+        private String deptIds;
+    }
+}

+ 55 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/bo/SysMenuExtraData.java

@@ -0,0 +1,55 @@
+package com.serbia.webadmin.upms.bo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 菜单扩展数据对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Data
+public class SysMenuExtraData {
+
+    /**
+     * 路由名称。
+     */
+    private String formRouterName;
+
+    /**
+     * 在线表单。
+     */
+    private Long onlineFormId;
+
+    /**
+     * 报表页面。
+     */
+    private Long reportPageId;
+
+    /**
+     * 流程。
+     */
+    private Long onlineFlowEntryId;
+
+    /**
+     * 目标url。
+     */
+    private String targetUrl;
+
+    /**
+     * 绑定类型。
+     */
+    private Integer bindType;
+
+    /**
+     * 前端使用的菜单编码。仅当选择satoken权限框架时使用。
+     */
+    private String menuCode;
+
+    /**
+     * 菜单关联的后台使用的权限字列表。仅当选择satoken权限框架时使用。
+     */
+    private List<String> permCodeList;
+}

+ 66 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/bo/SysMenuPerm.java

@@ -0,0 +1,66 @@
+package com.serbia.webadmin.upms.bo;
+
+import lombok.Data;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 菜单相关的业务对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Data
+public class SysMenuPerm {
+
+    /**
+     * 菜单Id。
+     */
+    private Long menuId;
+
+    /**
+     * 父菜单Id,目录菜单的父菜单为null
+     */
+    private Long parentId;
+
+    /**
+     * 菜单显示名称。
+     */
+    private String menuName;
+
+    /**
+     * 菜单类型 (0: 目录 1: 菜单 2: 按钮 3: UI片段)。
+     */
+    private Integer menuType;
+
+    /**
+     * 在线表单主键Id,仅用于在线表单绑定的菜单。
+     */
+    private Long onlineFormId;
+
+    /**
+     * 在线表单菜单的权限控制类型,具体值可参考SysOnlineMenuPermType常量对象。
+     */
+    private Integer onlineMenuPermType;
+
+    /**
+     * 统计页面主键Id,仅用于统计页面绑定的菜单。
+     */
+    private Long reportPageId;
+
+    /**
+     * 仅用于在线表单的流程Id。
+     */
+    private Long onlineFlowEntryId;
+
+    /**
+     * 关联权限URL集合。
+     */
+    Set<String> permUrlSet = new HashSet<>();
+
+    /**
+     * 关联的某一个url。
+     */
+    String url;
+}

+ 340 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/GlobalDictController.java

@@ -0,0 +1,340 @@
+package com.serbia.webadmin.upms.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.serbia.common.core.constant.ErrorCodeEnum;
+import com.serbia.common.core.object.MyOrderParam;
+import com.serbia.common.core.object.MyPageData;
+import com.serbia.common.core.object.MyPageParam;
+import com.serbia.common.core.object.ResponseResult;
+import com.serbia.common.core.util.MyCommonUtil;
+import com.serbia.common.core.util.MyModelUtil;
+import com.serbia.common.core.util.MyPageUtil;
+import com.serbia.common.core.validator.UpdateGroup;
+import com.serbia.common.dict.dto.GlobalDictDto;
+import com.serbia.common.dict.dto.GlobalDictItemDto;
+import com.serbia.common.dict.model.GlobalDict;
+import com.serbia.common.dict.model.GlobalDictItem;
+import com.serbia.common.dict.service.GlobalDictItemService;
+import com.serbia.common.dict.service.GlobalDictService;
+import com.serbia.common.dict.util.GlobalDictOperationHelper;
+import com.serbia.common.dict.vo.GlobalDictVo;
+import com.serbia.common.log.annotation.OperationLog;
+import com.serbia.common.log.model.constant.SysOperationLogType;
+import com.github.pagehelper.Page;
+import com.github.pagehelper.page.PageMethod;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import jakarta.validation.groups.Default;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 全局通用字典操作接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Tag(name = "全局字典管理接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/upms/globalDict")
+public class GlobalDictController {
+
+    @Autowired
+    private GlobalDictService globalDictService;
+    @Autowired
+    private GlobalDictItemService globalDictItemService;
+    @Autowired
+    private GlobalDictOperationHelper globalDictOperationHelper;
+
+    /**
+     * 新增全局字典接口。
+     *
+     * @param globalDictDto 新增字典对象。
+     * @return 保存后的字典对象。
+     */
+    @ApiOperationSupport(ignoreParameters = {"globalDictDto.dictId"})
+    @SaCheckPermission("globalDict.update")
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/add")
+    public ResponseResult<Long> add(@MyRequestBody GlobalDictDto globalDictDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(globalDictDto);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        // 这里必须手动校验字典编码是否存在,因为我们缺省的实现是逻辑删除,所以字典编码字段没有设置为唯一索引。
+        if (globalDictService.existDictCode(globalDictDto.getDictCode())) {
+            errorMessage = "数据验证失败,字典编码已经存在!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        GlobalDict globalDict = MyModelUtil.copyTo(globalDictDto, GlobalDict.class);
+        globalDictService.saveNew(globalDict);
+        return ResponseResult.success(globalDict.getDictId());
+    }
+
+    /**
+     * 更新全局字典操作。
+     *
+     * @param globalDictDto 更新全局字典对象。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("globalDict.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/update")
+    public ResponseResult<Void> update(@MyRequestBody GlobalDictDto globalDictDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(globalDictDto, Default.class, UpdateGroup.class);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        GlobalDict originalGlobalDict = globalDictService.getById(globalDictDto.getDictId());
+        if (originalGlobalDict == null) {
+            errorMessage = "数据验证失败,当前全局字典并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        GlobalDict globalDict = MyModelUtil.copyTo(globalDictDto, GlobalDict.class);
+        if (ObjectUtil.notEqual(globalDict.getDictCode(), originalGlobalDict.getDictCode())
+                && globalDictService.existDictCode(globalDict.getDictCode())) {
+            errorMessage = "数据验证失败,字典编码已经存在!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        if (!globalDictService.update(globalDict, originalGlobalDict)) {
+            errorMessage = "更新失败,数据不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除指定的全局字典。
+     *
+     * @param dictId 指定全局字典主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("globalDict.update")
+    @OperationLog(type = SysOperationLogType.DELETE)
+    @PostMapping("/delete")
+    public ResponseResult<Void> delete(@MyRequestBody(required = true) Long dictId) {
+        if (!globalDictService.remove(dictId)) {
+            String errorMessage = "数据操作失败,全局字典Id不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 查看全局字典列表。
+     *
+     * @param globalDictDtoFilter 过滤对象。
+     * @param orderParam          排序参数。
+     * @param pageParam           分页参数。
+     * @return 应答结果对象,包含角色列表。
+     */
+    @SaCheckPermission("globalDict.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<GlobalDictVo>> list(
+            @MyRequestBody GlobalDictDto globalDictDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
+        }
+        GlobalDict filter = MyModelUtil.copyTo(globalDictDtoFilter, GlobalDict.class);
+        List<GlobalDict> globalDictList =
+                globalDictService.getGlobalDictList(filter, MyOrderParam.buildOrderBy(orderParam, GlobalDict.class));
+        List<GlobalDictVo> globalDictVoList =
+                MyModelUtil.copyCollectionTo(globalDictList, GlobalDictVo.class);
+        long totalCount = 0L;
+        if (globalDictList instanceof Page) {
+            totalCount = ((Page<GlobalDict>) globalDictList).getTotal();
+        }
+        return ResponseResult.success(MyPageUtil.makeResponseData(globalDictVoList, totalCount));
+    }
+
+    /**
+     * 新增全局字典项目接口。
+     *
+     * @param globalDictItemDto 新增字典项目对象。
+     * @return 保存后的字典对象。
+     */
+    @SaCheckPermission("globalDict.update")
+    @ApiOperationSupport(ignoreParameters = {"globalDictItemDto.id"})
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/addItem")
+    public ResponseResult<Long> addItem(@MyRequestBody GlobalDictItemDto globalDictItemDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(globalDictItemDto);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        if (!globalDictService.existDictCode(globalDictItemDto.getDictCode())) {
+            errorMessage = "数据验证失败,字典编码不存在!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        if (globalDictItemService.existDictCodeAndItemId(
+                globalDictItemDto.getDictCode(), globalDictItemDto.getItemId())) {
+            errorMessage = "数据验证失败,该字典编码的项目Id已存在!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        GlobalDictItem globalDictItem = MyModelUtil.copyTo(globalDictItemDto, GlobalDictItem.class);
+        globalDictItemService.saveNew(globalDictItem);
+        return ResponseResult.success(globalDictItem.getId());
+    }
+
+    /**
+     * 更新全局字典项目。
+     *
+     * @param globalDictItemDto 更新全局字典项目对象。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("globalDict.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/updateItem")
+    public ResponseResult<Void> updateItem(@MyRequestBody GlobalDictItemDto globalDictItemDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(globalDictItemDto, Default.class, UpdateGroup.class);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        GlobalDictItem originalGlobalDictItem = globalDictItemService.getById(globalDictItemDto.getId());
+        if (originalGlobalDictItem == null) {
+            errorMessage = "数据验证失败,当前全局字典项目并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        GlobalDictItem globalDictItem = MyModelUtil.copyTo(globalDictItemDto, GlobalDictItem.class);
+        if (ObjectUtil.notEqual(globalDictItem.getDictCode(), originalGlobalDictItem.getDictCode())) {
+            errorMessage = "数据验证失败,字典项目的字典编码不能修改!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        if (ObjectUtil.notEqual(globalDictItem.getItemId(), originalGlobalDictItem.getItemId())
+                && globalDictItemService.existDictCodeAndItemId(globalDictItem.getDictCode(), globalDictItem.getItemId())) {
+            errorMessage = "数据验证失败,该字典编码已经包含了该项目Id!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        if (!globalDictItemService.update(globalDictItem, originalGlobalDictItem)) {
+            errorMessage = "更新失败,数据不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 更新全局字典项目的状态。
+     *
+     * @param id 更新全局字典项目主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("globalDict.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/updateItemStatus")
+    public ResponseResult<Void> updateItemStatus(
+            @MyRequestBody(required = true) Long id, @MyRequestBody(required = true) Integer status) {
+        String errorMessage;
+        GlobalDictItem dictItem = globalDictItemService.getById(id);
+        if (dictItem == null) {
+            errorMessage = "数据操作失败,全局字典项目Id不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (ObjectUtil.notEqual(dictItem.getStatus(), status)) {
+            globalDictItemService.updateStatus(dictItem, status);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除指定编码的全局字典项目。
+     *
+     * @param id 主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("globalDict.update")
+    @OperationLog(type = SysOperationLogType.DELETE)
+    @PostMapping("/deleteItem")
+    public ResponseResult<Void> deleteItem(@MyRequestBody(required = true) Long id) {
+        String errorMessage;
+        GlobalDictItem dictItem = globalDictItemService.getById(id);
+        if (dictItem == null) {
+            errorMessage = "数据操作失败,全局字典项目Id不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (!globalDictItemService.remove(dictItem)) {
+            errorMessage = "数据操作失败,全局字典项目Id不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 将当前字典表的数据重新加载到缓存中。
+     * 由于缓存的数据更新,在add/update/delete等接口均有同步处理。因此该接口仅当同步过程中出现问题时,
+     * 可手工调用,或者每天晚上定时同步一次。
+     */
+    @SaCheckPermission("globalDict.view")
+    @OperationLog(type = SysOperationLogType.RELOAD_CACHE)
+    @GetMapping("/reloadCachedData")
+    public ResponseResult<Boolean> reloadCachedData(@RequestParam String dictCode) {
+        globalDictService.reloadCachedData(dictCode);
+        return ResponseResult.success(true);
+    }
+
+    /**
+     * 获取指定字典编码的全局字典项目。字典的键值为[itemId, itemName]。
+     * NOTE: 白名单接口。
+     *
+     * @param dictCode   字典编码。
+     * @param itemIdType 字典项目的ItemId值转换到的目标类型。可能值为Integer或Long。
+     * @return 应答结果对象。
+     */
+    @GetMapping("/listDict")
+    public ResponseResult<List<Map<String, Object>>> listDict(
+            @RequestParam String dictCode, @RequestParam(required = false) String itemIdType) {
+        List<GlobalDictItem> resultList =
+                globalDictService.getGlobalDictItemListFromCache(dictCode, null);
+        resultList = resultList.stream()
+                .sorted(Comparator.comparing(GlobalDictItem::getStatus))
+                .sorted(Comparator.comparing(GlobalDictItem::getShowOrder))
+                .collect(Collectors.toList());
+        return ResponseResult.success(globalDictOperationHelper.toDictDataList(resultList, itemIdType));
+    }
+
+    /**
+     * 根据字典Id集合,获取查询后的字典数据。
+     * NOTE: 白名单接口。
+     *
+     * @param dictCode   字典编码。
+     * @param itemIds    字典项目Id集合。
+     * @param itemIdType 字典项目的ItemId值转换到的目标类型。可能值为Integer或Long。
+     * @return 应答结果对象,包含字典形式的数据集合。
+     */
+    @GetMapping("/listDictByIds")
+    public ResponseResult<List<Map<String, Object>>> listDictByIds(
+            @RequestParam String dictCode,
+            @RequestParam List<String> itemIds,
+            @RequestParam(required = false) String itemIdType) {
+        List<GlobalDictItem> resultList =
+                globalDictService.getGlobalDictItemListFromCache(dictCode, new HashSet<>(itemIds));
+        return ResponseResult.success(globalDictOperationHelper.toDictDataList(resultList, itemIdType));
+    }
+
+    /**
+     * 白名单接口,登录用户均可访问。以字典形式返回全部字典数据集合。
+     * fullResultList中的字典列表全部取自于数据库,而cachedResultList全部取自于缓存,前端负责比对。
+     *
+     * @return 应答结果对象,包含字典形式的数据集合。
+     */
+    @GetMapping("/listAll")
+    public ResponseResult<JSONObject> listAll(@RequestParam String dictCode) {
+        List<GlobalDictItem> fullResultList =
+                globalDictItemService.getGlobalDictItemListByDictCode(dictCode);
+        List<GlobalDictItem> cachedList =
+                globalDictService.getGlobalDictItemListFromCache(dictCode, null);
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("fullResultList", globalDictOperationHelper.toDictDataList2(fullResultList));
+        jsonObject.put("cachedResultList", globalDictOperationHelper.toDictDataList2(cachedList));
+        return ResponseResult.success(jsonObject);
+    }
+}

+ 615 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/LoginController.java

@@ -0,0 +1,615 @@
+package com.serbia.webadmin.upms.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.dev33.satoken.session.SaSession;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.BooleanUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.JSONArray;
+import com.github.xiaoymin.knife4j.annotations.ApiSupport;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import com.serbia.webadmin.config.ApplicationConfig;
+import com.serbia.webadmin.upms.bo.SysMenuExtraData;
+import com.serbia.webadmin.upms.service.*;
+import com.serbia.webadmin.upms.model.*;
+import com.serbia.webadmin.upms.model.constant.SysUserStatus;
+import com.serbia.webadmin.upms.model.constant.SysUserType;
+import com.serbia.webadmin.upms.model.constant.SysMenuType;
+import com.serbia.webadmin.upms.model.constant.SysOnlineMenuPermType;
+import com.serbia.common.flow.online.service.FlowOnlineOperationService;
+import com.serbia.common.online.service.OnlineOperationService;
+import com.serbia.common.report.service.ReportOperationService;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.serbia.common.core.annotation.DisableDataFilter;
+import com.serbia.common.core.constant.ErrorCodeEnum;
+import com.serbia.common.core.constant.ApplicationConstant;
+import com.serbia.common.core.object.*;
+import com.serbia.common.core.util.*;
+import com.serbia.common.core.upload.*;
+import com.serbia.common.mobile.model.MobileEntry;
+import com.serbia.common.mobile.object.MobileEntryExtraData;
+import com.serbia.common.mobile.service.MobileEntryService;
+import com.serbia.common.redis.cache.SessionCacheHelper;
+import com.serbia.common.log.annotation.OperationLog;
+import com.serbia.common.log.model.constant.SysOperationLogType;
+import com.serbia.common.satoken.util.SaTokenUtil;
+import org.redisson.api.RSet;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * 登录接口控制器类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@ApiSupport(order = 1)
+@Tag(name = "用户登录接口")
+@DisableDataFilter
+@Slf4j
+@RestController
+@RequestMapping("/admin/upms/login")
+public class LoginController {
+
+    @Autowired
+    private MobileEntryService mobileEntryService;
+    @Autowired
+    private SysUserService sysUserService;
+    @Autowired
+    private SysDeptService sysDeptService;
+    @Autowired
+    private SysMenuService sysMenuService;
+    @Autowired
+    private SysPostService sysPostService;
+    @Autowired
+    private SysRoleService sysRoleService;
+    @Autowired
+    private SysDataPermService sysDataPermService;
+    @Autowired
+    private SysPermWhitelistService sysPermWhitelistService;
+    @Autowired
+    private OnlineOperationService onlineOperationService;
+    @Autowired
+    private FlowOnlineOperationService flowOnlineOperationService;
+    @Autowired
+    private ReportOperationService reportOperationService;
+    @Autowired
+    private ApplicationConfig appConfig;
+    @Autowired
+    private RedissonClient redissonClient;
+    @Autowired
+    private SessionCacheHelper cacheHelper;
+    @Autowired
+    private PasswordEncoder passwordEncoder;
+    @Autowired
+    private UpDownloaderFactory upDownloaderFactory;
+    @Autowired
+    private SaTokenUtil saTokenUtil;
+
+    private static final String IS_ADMIN = "isAdmin";
+    private static final String SHOW_NAME_FIELD = "showName";
+    private static final String SHOW_ORDER_FIELD = "showOrder";
+    private static final String HEAD_IMAGE_URL_FIELD = "headImageUrl";
+
+    /**
+     * 登录接口。
+     *
+     * @param loginName 登录名。
+     * @param password  密码。
+     * @return 应答结果对象,其中包括Token数据,以及菜单列表。
+     */
+    @Parameter(name = "loginName", example = "admin")
+    @Parameter(name = "password", example = "IP3ccke3GhH45iGHB5qP9p7iZw6xUyj28Ju10rnBiPKOI35sc%2BjI7%2FdsjOkHWMfUwGYGfz8ik31HC2Ruk%2Fhkd9f6RPULTHj7VpFdNdde2P9M4mQQnFBAiPM7VT9iW3RyCtPlJexQ3nAiA09OqG%2F0sIf1kcyveSrulxembARDbDo%3D")
+    @SaIgnore
+    @OperationLog(type = SysOperationLogType.LOGIN, saveResponse = false)
+    @PostMapping("/doLogin")
+    public ResponseResult<JSONObject> doLogin(
+            @MyRequestBody String loginName,
+            @MyRequestBody String password) throws UnsupportedEncodingException {
+        if (MyCommonUtil.existBlankArgument(loginName, password)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        ResponseResult<SysUser> verifyResult = this.verifyAndHandleLoginUser(loginName, password);
+        if (!verifyResult.isSuccess()) {
+            return ResponseResult.errorFrom(verifyResult);
+        }
+        JSONObject jsonData = this.buildLoginDataAndLogin(verifyResult.getData());
+        return ResponseResult.success(jsonData);
+    }
+
+    /**
+     * 登录移动端接口。
+     *
+     * @param loginName 登录名。
+     * @param password  密码。
+     * @return 应答结果对象,其中包括Token数据,以及菜单列表。
+     */
+    @Parameter(name = "loginName", example = "admin")
+    @Parameter(name = "password", example = "IP3ccke3GhH45iGHB5qP9p7iZw6xUyj28Ju10rnBiPKOI35sc%2BjI7%2FdsjOkHWMfUwGYGfz8ik31HC2Ruk%2Fhkd9f6RPULTHj7VpFdNdde2P9M4mQQnFBAiPM7VT9iW3RyCtPlJexQ3nAiA09OqG%2F0sIf1kcyveSrulxembARDbDo%3D")
+    @SaIgnore
+    @OperationLog(type = SysOperationLogType.LOGIN_MOBILE, saveResponse = false)
+    @PostMapping("/doMobileLogin")
+    public ResponseResult<JSONObject> doMobileLogin(
+            @MyRequestBody String loginName,
+            @MyRequestBody String password) throws UnsupportedEncodingException {
+        if (MyCommonUtil.existBlankArgument(loginName, password)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        ResponseResult<SysUser> verifyResult = this.verifyAndHandleLoginUser(loginName, password);
+        if (!verifyResult.isSuccess()) {
+            return ResponseResult.errorFrom(verifyResult);
+        }
+        JSONObject jsonData = this.buildMobileLoginDataAndLogin(verifyResult.getData());
+        return ResponseResult.success(jsonData);
+    }
+
+    /**
+     * 登出操作。同时将Session相关的信息从缓存中删除。
+     *
+     * @return 应答结果对象。
+     */
+    @OperationLog(type = SysOperationLogType.LOGOUT)
+    @PostMapping("/doLogout")
+    public ResponseResult<Void> doLogout() {
+        String sessionId = TokenData.takeFromRequest().getSessionId();
+        redissonClient.getBucket(TokenData.takeFromRequest().getMySessionId()).deleteAsync();
+        redissonClient.getBucket(RedisKeyUtil.makeSessionPermCodeKey(sessionId)).deleteAsync();
+        redissonClient.getBucket(RedisKeyUtil.makeSessionPermIdKey(sessionId)).deleteAsync();
+        sysDataPermService.removeDataPermCache(sessionId);
+        cacheHelper.removeAllSessionCache(sessionId);
+        StpUtil.logout();
+        return ResponseResult.success();
+    }
+
+    /**
+     * 在登录之后,通过token再次获取登录信息。
+     * 用于在当前浏览器登录系统后,在新tab页中可以免密登录。
+     *
+     * @return 应答结果对象,其中包括JWT的Token数据,以及菜单列表。
+     */
+    @GetMapping("/getLoginInfo")
+    public ResponseResult<JSONObject> getLoginInfo() {
+        TokenData tokenData = TokenData.takeFromRequest();
+        JSONObject jsonData = new JSONObject();
+        jsonData.put(SHOW_NAME_FIELD, tokenData.getShowName());
+        jsonData.put(IS_ADMIN, tokenData.getIsAdmin());
+        if (StrUtil.isNotBlank(tokenData.getHeadImageUrl())) {
+            jsonData.put(HEAD_IMAGE_URL_FIELD, tokenData.getHeadImageUrl());
+        }
+        Collection<SysMenu> allMenuList;
+        if (BooleanUtil.isTrue(tokenData.getIsAdmin())) {
+            allMenuList = sysMenuService.getAllListByOrder(SHOW_ORDER_FIELD);
+        } else {
+            allMenuList = sysMenuService.getMenuListByRoleIds(tokenData.getRoleIds());
+        }
+        List<String> menuCodeList = new LinkedList<>();
+        OnlinePermData onlinePermData = this.getOnlineMenuPermData(allMenuList);
+        CollUtil.addAll(menuCodeList, onlinePermData.permCodeSet);
+        OnlinePermData onlineFlowPermData = this.getFlowOnlineMenuPermData(allMenuList);
+        CollUtil.addAll(menuCodeList, onlineFlowPermData.permCodeSet);
+        allMenuList.stream().filter(m -> m.getExtraData() != null)
+                .forEach(m -> m.setExtraObject(JSON.parseObject(m.getExtraData(), SysMenuExtraData.class)));
+        this.appendResponseMenuAndPermCodeData(jsonData, allMenuList, menuCodeList);
+        return ResponseResult.success(jsonData);
+    }
+
+    /**
+     * 返回所有可用的权限字列表。
+     * 
+     * @return 整个系统所有可用的权限字列表。
+     */
+    @GetMapping("/getAllPermCodes")
+    public ResponseResult<List<String>> getAllPermCodes() {
+        List<String> permCodes = saTokenUtil.getAllPermCodes();
+        return ResponseResult.success(permCodes);
+    }
+
+    /**
+     * 用户修改自己的密码。
+     *
+     * @param oldPass 原有密码。
+     * @param newPass 新密码。
+     * @return 应答结果对象。
+     */
+    @PostMapping("/changePassword")
+    public ResponseResult<Void> changePassword(
+            @MyRequestBody String oldPass, @MyRequestBody String newPass) throws UnsupportedEncodingException {
+        if (MyCommonUtil.existBlankArgument(newPass, oldPass)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        TokenData tokenData = TokenData.takeFromRequest();
+        SysUser user = sysUserService.getById(tokenData.getUserId());
+        oldPass = URLDecoder.decode(oldPass, StandardCharsets.UTF_8.name());
+        // NOTE: 第一次使用时,请务必阅读ApplicationConstant.PRIVATE_KEY的代码注释。
+        // 执行RsaUtil工具类中的main函数,可以生成新的公钥和私钥。
+        oldPass = RsaUtil.decrypt(oldPass, ApplicationConstant.PRIVATE_KEY);
+        if (user == null || !passwordEncoder.matches(oldPass, user.getPassword())) {
+            return ResponseResult.error(ErrorCodeEnum.INVALID_USERNAME_PASSWORD);
+        }
+        newPass = URLDecoder.decode(newPass, StandardCharsets.UTF_8.name());
+        newPass = RsaUtil.decrypt(newPass, ApplicationConstant.PRIVATE_KEY);
+        if (!sysUserService.changePassword(tokenData.getUserId(), newPass)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+    
+    /**
+     * 上传并修改用户头像。
+     *
+     * @param uploadFile 上传的头像文件。
+     */
+    @PostMapping("/changeHeadImage")
+    public void changeHeadImage(@RequestParam("uploadFile") MultipartFile uploadFile) throws IOException {
+        UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, HEAD_IMAGE_URL_FIELD);
+        BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
+        UploadResponseInfo responseInfo = upDownloader.doUpload(null,
+                appConfig.getUploadFileBaseDir(), SysUser.class.getSimpleName(), HEAD_IMAGE_URL_FIELD, true, uploadFile);
+        if (BooleanUtil.isTrue(responseInfo.getUploadFailed())) {
+            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN,
+                    ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage()));
+            return;
+        }
+        responseInfo.setDownloadUri("/admin/upms/login/downloadHeadImage");
+        String newHeadImage = JSONArray.toJSONString(CollUtil.newArrayList(responseInfo));
+        if (!sysUserService.changeHeadImage(TokenData.takeFromRequest().getUserId(), newHeadImage)) {
+            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN, ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST));
+            return;
+        }
+        ResponseResult.output(ResponseResult.success(responseInfo));
+    }
+
+    /**
+     * 下载用户头像。
+     *
+     * @param filename 文件名。如果没有提供该参数,就从当前记录的指定字段中读取。
+     * @param response Http 应答对象。
+     */
+    @GetMapping("/downloadHeadImage")
+    public void downloadHeadImage(String filename, HttpServletResponse response) {
+        try {
+            UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, HEAD_IMAGE_URL_FIELD);
+            BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
+            upDownloader.doDownload(appConfig.getUploadFileBaseDir(),
+                    SysUser.class.getSimpleName(), HEAD_IMAGE_URL_FIELD, filename, true, response);
+        } catch (Exception e) {
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            log.error(e.getMessage(), e);
+        }
+    }
+
+    private ResponseResult<SysUser> verifyAndHandleLoginUser(
+            String loginName, String password) throws UnsupportedEncodingException {
+        String errorMessage;
+        SysUser user = sysUserService.getSysUserByLoginName(loginName);
+        password = URLDecoder.decode(password, StandardCharsets.UTF_8.name());
+        // NOTE: 第一次使用时,请务必阅读ApplicationConstant.PRIVATE_KEY的代码注释。
+        // 执行RsaUtil工具类中的main函数,可以生成新的公钥和私钥。
+        password = RsaUtil.decrypt(password, ApplicationConstant.PRIVATE_KEY);
+        if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
+            return ResponseResult.error(ErrorCodeEnum.INVALID_USERNAME_PASSWORD);
+        }
+        if (user.getUserStatus() == SysUserStatus.STATUS_LOCKED) {
+            errorMessage = "登录失败,用户账号被锁定!";
+            return ResponseResult.error(ErrorCodeEnum.INVALID_USER_STATUS, errorMessage);
+        }
+        if (BooleanUtil.isTrue(appConfig.getExcludeLogin())) {
+            String deviceType = MyCommonUtil.getDeviceTypeWithString();
+            LoginUserInfo userInfo = BeanUtil.copyProperties(user, LoginUserInfo.class);
+            String loginId = SaTokenUtil.makeLoginId(userInfo);
+            StpUtil.kickout(loginId, deviceType);
+        }
+        return ResponseResult.success(user);
+    }
+
+    private JSONObject buildLoginDataAndLogin(SysUser user) {
+        TokenData tokenData = this.loginAndCreateToken(user);
+        // 这里手动将TokenData存入request,便于OperationLogAspect统一处理操作日志。
+        TokenData.addToRequest(tokenData);
+        JSONObject jsonData = this.createResponseData(user);
+        Collection<SysMenu> allMenuList;
+        boolean isAdmin = user.getUserType() == SysUserType.TYPE_ADMIN;
+        if (isAdmin) {
+            allMenuList = sysMenuService.getAllListByOrder(SHOW_ORDER_FIELD);
+        } else {
+            allMenuList = sysMenuService.getMenuListByRoleIds(tokenData.getRoleIds());
+        }
+        allMenuList.stream().filter(m -> m.getExtraData() != null)
+                .forEach(m -> m.setExtraObject(JSON.parseObject(m.getExtraData(), SysMenuExtraData.class)));
+        Collection<String> permCodeList = new LinkedList<>();
+        allMenuList.stream().filter(m -> m.getExtraObject() != null)
+                .forEach(m -> CollUtil.addAll(permCodeList, m.getExtraObject().getPermCodeList()));
+        Set<String> permSet = new HashSet<>();
+        if (!isAdmin) {
+            // 所有登录用户都有白名单接口的访问权限。
+            CollUtil.addAll(permSet, sysPermWhitelistService.getWhitelistPermList());
+        }
+        List<String> menuCodeList = new LinkedList<>();
+        OnlinePermData onlinePermData = this.getOnlineMenuPermData(allMenuList);
+        CollUtil.addAll(menuCodeList, onlinePermData.permCodeSet);
+        OnlinePermData onlineFlowPermData = this.getFlowOnlineMenuPermData(allMenuList);
+        CollUtil.addAll(menuCodeList, onlineFlowPermData.permCodeSet);
+        if (!isAdmin) {
+            permSet.addAll(onlinePermData.permUrlSet);
+            permSet.addAll(onlineFlowPermData.permUrlSet);
+            Set<String> reportPermSet = this.getReportMenuPermData(allMenuList);
+            permSet.addAll(reportPermSet);
+            String sessionId = tokenData.getSessionId();
+            // 缓存用户的权限资源,这里缓存的是基于URL验证的权限资源,比如在线表单、工作流和数据表中的白名单资源。
+            this.putUserSysPermCache(sessionId, permSet);
+            // 缓存权限字字段,StpInterfaceImpl中会从缓存中读取,并交给satoken进行接口权限的验证。
+            this.putUserSysPermCodeCache(sessionId, permCodeList);
+            sysDataPermService.putDataPermCache(sessionId, user.getUserId(), user.getDeptId());
+        }
+        this.appendResponseMenuAndPermCodeData(jsonData, allMenuList, menuCodeList);
+        return jsonData;
+    }
+
+    private JSONObject buildMobileLoginDataAndLogin(SysUser user) {
+        TokenData tokenData = this.loginAndCreateToken(user);
+        // 这里手动将TokenData存入request,便于OperationLogAspect统一处理操作日志。
+        TokenData.addToRequest(tokenData);
+        JSONObject jsonData = this.createResponseData(user);
+        List<MobileEntry> mobileEntryList;
+        boolean isAdmin = user.getUserType() == SysUserType.TYPE_ADMIN;
+        if (isAdmin) {
+            mobileEntryList = mobileEntryService.getAllListByOrder(SHOW_ORDER_FIELD);
+        } else {
+            mobileEntryList = mobileEntryService.getMobileEntryListByRoleIds(tokenData.getRoleIds());
+        }
+        Collection<String> permCodeList = new LinkedList<>();
+        mobileEntryList.stream().filter(m -> m.getExtraData() != null)
+                .forEach(m -> m.setExtraObject(JSON.parseObject(m.getExtraData(), MobileEntryExtraData.class)));
+        mobileEntryList.stream().filter(m -> m.getExtraObject() != null)
+                .forEach(m -> CollUtil.addAll(permCodeList, m.getExtraObject().getPermCodeList()));
+        jsonData.put("mobileEntryList", mobileEntryList);
+        jsonData.put("permCodeList", permCodeList);
+        Set<String> permSet = new HashSet<>();
+        if (!isAdmin) {
+            CollUtil.addAll(permSet, sysPermWhitelistService.getWhitelistPermList());
+        }
+        if (!isAdmin) {
+            Set<String> onlinePermSet = this.getOnlineMobileEntryPermData(mobileEntryList);
+            permSet.addAll(onlinePermSet);
+            Set<String> onlineFlowPermSet = this.getFlowOnlineMobileEntryPermData(mobileEntryList);
+            permSet.addAll(onlineFlowPermSet);
+            Set<String> reportPermSet = this.getReportMobileEntryPermData(mobileEntryList);
+            permSet.addAll(reportPermSet);
+            String sessionId = tokenData.getSessionId();
+            // 缓存用户的权限资源,这里缓存的是基于URL验证的权限资源,比如在线表单、工作流和数据表中的白名单资源。
+            this.putUserSysPermCache(sessionId, permSet);
+            // 缓存权限字字段,StpInterfaceImpl中会从缓存中读取,并交给satoken进行接口权限的验证。
+            this.putUserSysPermCodeCache(sessionId, permCodeList);
+            sysDataPermService.putDataPermCache(sessionId, user.getUserId(), user.getDeptId());
+        }
+        return jsonData;
+    }
+
+    private TokenData loginAndCreateToken(SysUser user) {
+        String deviceType = MyCommonUtil.getDeviceTypeWithString();
+        LoginUserInfo userInfo = BeanUtil.copyProperties(user, LoginUserInfo.class);
+        String loginId = SaTokenUtil.makeLoginId(userInfo);
+        StpUtil.login(loginId, deviceType);
+        SaSession session = StpUtil.getTokenSession();
+        TokenData tokenData = this.buildTokenData(user, session.getId(), StpUtil.getLoginDevice());
+        String mySessionId = RedisKeyUtil.getSessionIdPrefix(tokenData, user.getLoginName()) + MyCommonUtil.generateUuid();
+        tokenData.setMySessionId(mySessionId);
+        tokenData.setToken(session.getToken());
+        redissonClient.getBucket(mySessionId)
+                .set(JSON.toJSONString(tokenData), appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS);
+        session.set(TokenData.REQUEST_ATTRIBUTE_NAME, tokenData);
+        return tokenData;
+    }
+
+    private JSONObject createResponseData(SysUser user) {
+        JSONObject jsonData = new JSONObject();
+        jsonData.put(TokenData.REQUEST_ATTRIBUTE_NAME, StpUtil.getTokenValue());
+        jsonData.put(SHOW_NAME_FIELD, user.getShowName());
+        jsonData.put(IS_ADMIN, user.getUserType() == SysUserType.TYPE_ADMIN);
+        if (user.getDeptId() != null) {
+            SysDept dept = sysDeptService.getById(user.getDeptId());
+            jsonData.put("deptName", dept.getDeptName());
+        }
+        if (StrUtil.isNotBlank(user.getHeadImageUrl())) {
+            jsonData.put(HEAD_IMAGE_URL_FIELD, user.getHeadImageUrl());
+        }
+        return jsonData;
+    }
+
+    private void appendResponseMenuAndPermCodeData(
+            JSONObject responseData, Collection<SysMenu> allMenuList, Collection<String> menuCodeList) {
+        allMenuList.stream()
+                .filter(m -> m.getExtraObject() != null && StrUtil.isNotBlank(m.getExtraObject().getMenuCode()))
+                .forEach(m -> CollUtil.addAll(menuCodeList, m.getExtraObject().getMenuCode()));
+        List<SysMenu> menuList = allMenuList.stream()
+                .filter(m -> m.getMenuType() <= SysMenuType.TYPE_MENU).collect(Collectors.toList());
+        responseData.put("menuList", menuList);
+        responseData.put("permCodeList", menuCodeList);
+    }
+
+    private TokenData buildTokenData(SysUser user, String sessionId, String deviceType) {
+        TokenData tokenData = new TokenData();
+        tokenData.setSessionId(sessionId);
+        tokenData.setUserId(user.getUserId());
+        tokenData.setDeptId(user.getDeptId());
+        tokenData.setLoginName(user.getLoginName());
+        tokenData.setShowName(user.getShowName());
+        tokenData.setIsAdmin(user.getUserType().equals(SysUserType.TYPE_ADMIN));
+        tokenData.setLoginIp(IpUtil.getRemoteIpAddress(ContextUtil.getHttpRequest()));
+        tokenData.setLoginTime(new Date());
+        tokenData.setDeviceType(deviceType);
+        tokenData.setHeadImageUrl(user.getHeadImageUrl());
+        List<SysUserPost> userPostList = sysPostService.getSysUserPostListByUserId(user.getUserId());
+        if (CollUtil.isNotEmpty(userPostList)) {
+            Set<Long> deptPostIdSet = userPostList.stream().map(SysUserPost::getDeptPostId).collect(Collectors.toSet());
+            tokenData.setDeptPostIds(StrUtil.join(",", deptPostIdSet));
+            Set<Long> postIdSet = userPostList.stream().map(SysUserPost::getPostId).collect(Collectors.toSet());
+            tokenData.setPostIds(StrUtil.join(",", postIdSet));
+        }
+        List<SysUserRole> userRoleList = sysRoleService.getSysUserRoleListByUserId(user.getUserId());
+        if (CollUtil.isNotEmpty(userRoleList)) {
+            Set<Long> userRoleIdSet = userRoleList.stream().map(SysUserRole::getRoleId).collect(Collectors.toSet());
+            tokenData.setRoleIds(StrUtil.join(",", userRoleIdSet));
+        }
+        return tokenData;
+    }
+
+    private void putUserSysPermCache(String sessionId, Collection<String> permUrlSet) {
+        if (CollUtil.isEmpty(permUrlSet)) {
+            return;
+        }
+        String sessionPermKey = RedisKeyUtil.makeSessionPermIdKey(sessionId);
+        RSet<String> redisPermSet = redissonClient.getSet(sessionPermKey);
+        redisPermSet.addAll(permUrlSet);
+        redisPermSet.expire(appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS);
+    }
+
+    private void putUserSysPermCodeCache(String sessionId, Collection<String> permCodeSet) {
+        if (CollUtil.isEmpty(permCodeSet)) {
+            return;
+        }
+        String sessionPermCodeKey = RedisKeyUtil.makeSessionPermCodeKey(sessionId);
+        RSet<String> redisPermSet = redissonClient.getSet(sessionPermCodeKey);
+        redisPermSet.addAll(permCodeSet);
+        redisPermSet.expire(appConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS);
+    }
+
+    private OnlinePermData getOnlineMenuPermData(Collection<SysMenu> allMenuList) {
+        List<SysMenu> onlineMenuList = allMenuList.stream()
+                .filter(m -> m.getOnlineFormId() != null && m.getMenuType().equals(SysMenuType.TYPE_BUTTON))
+                .collect(Collectors.toList());
+        if (CollUtil.isEmpty(onlineMenuList)) {
+            return new OnlinePermData();
+        }
+        Set<Long> formIds = allMenuList.stream()
+                .filter(m -> m.getOnlineFormId() != null
+                        && m.getOnlineFlowEntryId() == null
+                        && m.getMenuType().equals(SysMenuType.TYPE_MENU))
+                .map(SysMenu::getOnlineFormId)
+                .collect(Collectors.toSet());
+        Set<Long> viewFormIds = onlineMenuList.stream()
+                .filter(m -> m.getOnlineMenuPermType() == SysOnlineMenuPermType.TYPE_VIEW)
+                .map(SysMenu::getOnlineFormId)
+                .collect(Collectors.toSet());
+        Set<Long> editFormIds = onlineMenuList.stream()
+                .filter(m -> m.getOnlineMenuPermType() == SysOnlineMenuPermType.TYPE_EDIT)
+                .map(SysMenu::getOnlineFormId)
+                .collect(Collectors.toSet());
+        Map<String, Object> permDataMap =
+                onlineOperationService.calculatePermData(formIds, viewFormIds, editFormIds);
+        OnlinePermData permData = BeanUtil.mapToBean(permDataMap, OnlinePermData.class, false, null);
+        permData.permUrlSet.addAll(permData.onlineWhitelistUrls);
+        return permData;
+    }
+
+    private OnlinePermData getFlowOnlineMenuPermData(Collection<SysMenu> allMenuList) {
+        List<SysMenu> flowOnlineMenuList = allMenuList.stream()
+                .filter(m -> m.getOnlineFlowEntryId() != null).collect(Collectors.toList());
+        Set<Long> entryIds = flowOnlineMenuList.stream()
+                .map(SysMenu::getOnlineFlowEntryId).collect(Collectors.toSet());
+        List<Map<String, Object>> flowPermDataList = flowOnlineOperationService.calculatePermData(entryIds);
+        List<OnlineFlowPermData> flowOnlinePermDataList =
+                MyModelUtil.mapToBeanList(flowPermDataList, OnlineFlowPermData.class);
+        OnlinePermData permData = new OnlinePermData();
+        flowOnlinePermDataList.forEach(perm -> {
+            permData.permCodeSet.addAll(perm.getPermCodeList());
+            permData.permUrlSet.addAll(perm.getPermList());
+        });
+        return permData;
+    }
+
+    private Set<String> getReportMenuPermData(Collection<SysMenu> allMenuList) {
+        Set<String> permSet = new HashSet<>();
+        List<SysMenu> reportMenuList = allMenuList.stream()
+                .filter(m -> m.getReportPageId() != null).collect(Collectors.toList());
+        if (CollUtil.isEmpty(reportMenuList)) {
+            return permSet;
+        }
+        Set<Long> pageIds = reportMenuList.stream().map(SysMenu::getReportPageId).collect(Collectors.toSet());
+        Map<Long, Set<String>> reportPermDataMap = reportOperationService.calculatePermData(pageIds);
+        for (Long pageId : pageIds) {
+            CollUtil.addAll(permSet, reportPermDataMap.get(pageId));
+        }
+        return permSet;
+    }
+
+    private Set<String> getOnlineMobileEntryPermData(Collection<MobileEntry> mobileEntryList) {
+        List<MobileEntry> onlineMobileEntryList = mobileEntryList.stream()
+                .filter(m -> m.getExtraObject() != null
+                        && m.getExtraObject().getOnlineFormId() != null
+                        && m.getExtraObject().getOnlineFlowEntryId() == null)
+                .collect(Collectors.toList());
+        if (CollUtil.isEmpty(onlineMobileEntryList)) {
+            return CollUtil.newHashSet();
+        }
+        Set<Long> onlineFormIds = onlineMobileEntryList.stream()
+                .map(m -> m.getExtraObject().getOnlineFormId()).collect(Collectors.toSet());
+        Map<String, Object> permDataMap =
+                onlineOperationService.calculatePermData(onlineFormIds, onlineFormIds, onlineFormIds);
+        OnlinePermData permData = BeanUtil.mapToBean(permDataMap, OnlinePermData.class, false, null);
+        permData.permUrlSet.addAll(permData.onlineWhitelistUrls);
+        return permData.permUrlSet;
+    }
+
+    private Set<String> getFlowOnlineMobileEntryPermData(Collection<MobileEntry> mobileEntryList) {
+        List<MobileEntry> flowOnlineMobileEntryList = mobileEntryList.stream()
+                .filter(m -> m.getExtraData() != null && m.getExtraObject().getOnlineFlowEntryId() != null)
+                .collect(Collectors.toList());
+        Set<Long> entryIds = flowOnlineMobileEntryList.stream()
+                .map(m -> m.getExtraObject().getOnlineFlowEntryId()).collect(Collectors.toSet());
+        List<Map<String, Object>> flowPermDataList = flowOnlineOperationService.calculatePermData(entryIds);
+        List<OnlineFlowPermData> flowOnlinePermDataList =
+                MyModelUtil.mapToBeanList(flowPermDataList, OnlineFlowPermData.class);
+        Set<String> permSet = new HashSet<>();
+        flowOnlinePermDataList.forEach(perm -> permSet.addAll(perm.getPermList()));
+        return permSet;
+    }
+
+    private Set<String> getReportMobileEntryPermData(Collection<MobileEntry> mobileEntryList) {
+        Set<String> permSet = new HashSet<>();
+        List<MobileEntry> reportMobileEntryList = mobileEntryList.stream()
+                .filter(m -> m.getExtraObject() != null && m.getExtraObject().getReportPageId() != null)
+                .collect(Collectors.toList());
+        if (CollUtil.isEmpty(reportMobileEntryList)) {
+            return permSet;
+        }
+        Set<Long> pageIds = reportMobileEntryList.stream()
+                .map(m -> m.getExtraObject().getReportPageId()).collect(Collectors.toSet());
+        Map<Long, Set<String>> reportPermDataMap = reportOperationService.calculatePermData(pageIds);
+        for (Long pageId : pageIds) {
+            CollUtil.addAll(permSet, reportPermDataMap.get(pageId));
+        }
+        return permSet;
+    }
+
+    static class OnlinePermData {
+        public final Set<String> permCodeSet = new HashSet<>();
+        public final Set<String> permUrlSet = new HashSet<>();
+        public final List<String> onlineWhitelistUrls = new LinkedList<>();
+    }
+
+    @Data
+    static class OnlineFlowPermData {
+        private List<String> permCodeList;
+        private List<String> permList;
+    }
+}

+ 89 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/LoginUserController.java

@@ -0,0 +1,89 @@
+package com.serbia.webadmin.upms.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSON;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.serbia.common.core.object.*;
+import com.serbia.common.core.util.RedisKeyUtil;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RBucket;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.*;
+
+/**
+ * 在线用户控制器对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Tag(name = "在线用户接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/upms/loginUser")
+public class LoginUserController {
+
+    @Autowired
+    private RedissonClient redissonClient;
+
+    /**
+     * 显示在线用户列表。
+     *
+     * @param loginName 登录名过滤。
+     * @param pageParam 分页参数。
+     * @return 登录用户信息列表。
+     */
+    @SaCheckPermission("loginUser.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<LoginUserInfo>> list(
+            @MyRequestBody String loginName, @MyRequestBody MyPageParam pageParam) {
+        int skipCount = (pageParam.getPageNum() - 1) * pageParam.getPageSize();
+        String patternKey;
+        if (StrUtil.isBlank(loginName)) {
+            patternKey = RedisKeyUtil.getSessionIdPrefix() + "*";
+        } else {
+            patternKey = RedisKeyUtil.getSessionIdPrefix(loginName) + "*";
+        }
+        List<LoginUserInfo> loginUserInfoList = new LinkedList<>();
+        Iterable<String> keys = redissonClient.getKeys().getKeysByPattern(patternKey);
+        for (String key : keys) {
+            loginUserInfoList.add(this.buildTokenDataByRedisKey(key));
+        }
+        loginUserInfoList.sort((o1, o2) -> (int) (o2.getLoginTime().getTime() - o1.getLoginTime().getTime()));
+        int toIndex = Math.min(skipCount + pageParam.getPageSize(), loginUserInfoList.size());
+        List<LoginUserInfo> resultList = loginUserInfoList.subList(skipCount, toIndex);
+        return ResponseResult.success(new MyPageData<>(resultList, (long) loginUserInfoList.size()));
+    }
+
+    /**
+     * 强制下线指定登录会话。
+     *
+     * @param sessionId 待强制下线的SessionId。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("loginUser.delete")
+    @PostMapping("/delete")
+    public ResponseResult<Void> delete(@MyRequestBody String sessionId) {
+        RBucket<String> sessionData = redissonClient.getBucket(sessionId);
+        TokenData tokenData = JSON.parseObject(sessionData.get(), TokenData.class);
+        StpUtil.kickoutByTokenValue(tokenData.getToken());
+        sessionData.delete();
+        return ResponseResult.success();
+    }
+
+    private LoginUserInfo buildTokenDataByRedisKey(String key) {
+        RBucket<String> sessionData = redissonClient.getBucket(key);
+        TokenData tokenData = JSON.parseObject(sessionData.get(), TokenData.class);
+        LoginUserInfo userInfo = BeanUtil.copyProperties(tokenData, LoginUserInfo.class);
+        userInfo.setSessionId(tokenData.getMySessionId());
+        return userInfo;
+    }
+}

+ 352 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/SysDataPermController.java

@@ -0,0 +1,352 @@
+package com.serbia.webadmin.upms.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.hutool.core.util.StrUtil;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import com.alibaba.fastjson.TypeReference;
+import com.github.pagehelper.Page;
+import com.github.pagehelper.page.PageMethod;
+import lombok.extern.slf4j.Slf4j;
+import com.serbia.webadmin.upms.dto.SysDataPermDto;
+import com.serbia.webadmin.upms.dto.SysUserDto;
+import com.serbia.webadmin.upms.vo.SysDataPermVo;
+import com.serbia.webadmin.upms.vo.SysUserVo;
+import com.serbia.webadmin.upms.model.SysDataPerm;
+import com.serbia.webadmin.upms.model.SysUser;
+import com.serbia.webadmin.upms.service.SysDataPermService;
+import com.serbia.webadmin.upms.service.SysUserService;
+import com.serbia.common.core.validator.UpdateGroup;
+import com.serbia.common.core.constant.ErrorCodeEnum;
+import com.serbia.common.core.object.*;
+import com.serbia.common.core.util.*;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.serbia.common.log.annotation.OperationLog;
+import com.serbia.common.log.model.constant.SysOperationLogType;
+import org.springdoc.core.annotations.ParameterObject;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import jakarta.validation.groups.Default;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 数据权限接口控制器对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Tag(name = "数据权限管理接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/upms/sysDataPerm")
+public class SysDataPermController {
+
+    @Autowired
+    private SysDataPermService sysDataPermService;
+    @Autowired
+    private SysUserService sysUserService;
+
+    /**
+     * 添加新数据权限操作。
+     *
+     * @param sysDataPermDto    新增对象。
+     * @param deptIdListString  数据权限关联的部门Id列表,多个之间逗号分隔。
+     * @param menuIdListString  数据权限关联的菜单Id列表,多个之间逗号分隔。
+     * @param entryIdListString 数据权限关联的移动端入口Id列表,多个之间逗号分隔。
+     * @return 应答结果对象。包含新增数据权限对象的主键Id。
+     */
+    @ApiOperationSupport(ignoreParameters = {
+            "sysDataPermDto.dataPermId",
+            "sysDataPermDto.createTimeStart",
+            "sysDataPermDto.createTimeEnd",
+            "sysDataPermDto.searchString"})
+    @SaCheckPermission("sysDataPerm.add")
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/add")
+    public ResponseResult<Long> add(
+            @MyRequestBody SysDataPermDto sysDataPermDto,
+            @MyRequestBody String deptIdListString,
+            @MyRequestBody String menuIdListString,
+            @MyRequestBody String entryIdListString) {
+        String errorMessage = MyCommonUtil.getModelValidationError(sysDataPermDto);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SysDataPerm sysDataPerm = MyModelUtil.copyTo(sysDataPermDto, SysDataPerm.class);
+        CallResult result = sysDataPermService.verifyRelatedData(sysDataPerm, deptIdListString, menuIdListString);
+        if (!result.isSuccess()) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage());
+        }
+        Set<Long> menuIdSet = null;
+        if (result.getData() != null) {
+            menuIdSet = result.getData().getObject("menuIdSet", new TypeReference<Set<Long>>(){});
+        }
+        Set<Long> deptIdSet = null;
+        if (result.getData() != null) {
+            deptIdSet = result.getData().getObject("deptIdSet", new TypeReference<Set<Long>>(){});
+        }
+        Set<Long> entryIdSet = null;
+        if (StrUtil.isNotBlank(entryIdListString)) {
+            entryIdSet = StrUtil.split(entryIdListString, ",")
+                    .stream().map(Long::valueOf).collect(Collectors.toSet());
+        }
+        sysDataPermService.saveNew(sysDataPerm, deptIdSet, menuIdSet, entryIdSet);
+        return ResponseResult.success(sysDataPerm.getDataPermId());
+    }
+
+    /**
+     * 更新数据权限操作。
+     *
+     * @param sysDataPermDto    更新的数据权限对象。
+     * @param deptIdListString  数据权限关联的部门Id列表,多个之间逗号分隔。
+     * @param menuIdListString  数据权限关联的菜单Id列表,多个之间逗号分隔。
+     * @param entryIdListString 数据权限关联的移动端入口Id列表,多个之间逗号分隔。
+     * @return 应答结果对象。
+     */
+    @ApiOperationSupport(ignoreParameters = {
+            "sysDataPermDto.createTimeStart",
+            "sysDataPermDto.createTimeEnd",
+            "sysDataPermDto.searchString"})
+    @SaCheckPermission("sysDataPerm.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/update")
+    public ResponseResult<Void> update(
+            @MyRequestBody SysDataPermDto sysDataPermDto,
+            @MyRequestBody String deptIdListString,
+            @MyRequestBody String menuIdListString,
+            @MyRequestBody String entryIdListString) {
+        String errorMessage = MyCommonUtil.getModelValidationError(sysDataPermDto, Default.class, UpdateGroup.class);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SysDataPerm originalSysDataPerm = sysDataPermService.getById(sysDataPermDto.getDataPermId());
+        if (originalSysDataPerm == null) {
+            errorMessage = "数据验证失败,当前数据权限并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        SysDataPerm sysDataPerm = MyModelUtil.copyTo(sysDataPermDto, SysDataPerm.class);
+        CallResult result = sysDataPermService.verifyRelatedData(sysDataPerm, deptIdListString, menuIdListString);
+        if (!result.isSuccess()) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage());
+        }
+        Set<Long> deptIdSet = null;
+        if (result.getData() != null) {
+            deptIdSet = result.getData().getObject("deptIdSet", new TypeReference<Set<Long>>(){});
+        }
+        Set<Long> menuIdSet = null;
+        if (result.getData() != null) {
+            menuIdSet = result.getData().getObject("menuIdSet", new TypeReference<Set<Long>>(){});
+        }
+        Set<Long> entryIdSet = null;
+        if (StrUtil.isNotBlank(entryIdListString)) {
+            entryIdSet = StrUtil.split(entryIdListString, ",")
+                    .stream().map(Long::valueOf).collect(Collectors.toSet());
+        }
+        if (!sysDataPermService.update(sysDataPerm, originalSysDataPerm, deptIdSet, menuIdSet, entryIdSet)) {
+            errorMessage = "更新失败,数据不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除数据权限操作。
+     *
+     * @param dataPermId 待删除数据权限主键Id。
+     * @return 应答数据结果。
+     */
+    @SaCheckPermission("sysDataPerm.delete")
+    @OperationLog(type = SysOperationLogType.DELETE)
+    @PostMapping("/delete")
+    public ResponseResult<Void> delete(@MyRequestBody Long dataPermId) {
+        if (MyCommonUtil.existBlankArgument(dataPermId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        if (!sysDataPermService.remove(dataPermId)) {
+            String errorMessage = "数据操作失败,数据权限不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 查看数据权限列表。
+     *
+     * @param sysDataPermDtoFilter 数据权限查询过滤对象。
+     * @param orderParam           排序参数。
+     * @param pageParam            分页参数。
+     * @return 应答结果对象。包含数据权限列表。
+     */
+    @SaCheckPermission("sysDataPerm.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<SysDataPermVo>> list(
+            @MyRequestBody SysDataPermDto sysDataPermDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
+        }
+        SysDataPerm filter = MyModelUtil.copyTo(sysDataPermDtoFilter, SysDataPerm.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SysDataPerm.class);
+        List<SysDataPerm> dataPermList = sysDataPermService.getSysDataPermListWithRelation(filter, orderBy);
+        List<SysDataPermVo> dataPermVoList = MyModelUtil.copyCollectionTo(dataPermList, SysDataPermVo.class);
+        long totalCount = 0L;
+        if (dataPermList instanceof Page) {
+            totalCount = ((Page<SysDataPerm>) dataPermList).getTotal();
+        }
+        return ResponseResult.success(MyPageUtil.makeResponseData(dataPermVoList, totalCount));
+    }
+
+    /**
+     * 查看单条数据权限详情。
+     *
+     * @param dataPermId 数据权限的主键Id。
+     * @return 应答结果对象,包含数据权限的详情。
+     */
+    @SaCheckPermission("sysDataPerm.view")
+    @GetMapping("/view")
+    public ResponseResult<SysDataPermVo> view(@RequestParam Long dataPermId) {
+        if (MyCommonUtil.existBlankArgument(dataPermId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        SysDataPerm dataPerm = sysDataPermService.getByIdWithRelation(dataPermId, MyRelationParam.full());
+        if (dataPerm == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        SysDataPermVo dataPermVo = MyModelUtil.copyTo(dataPerm, SysDataPermVo.class);
+        return ResponseResult.success(dataPermVo);
+    }
+
+    /**
+     * 拥有指定数据权限的用户列表。
+     *
+     * @param dataPermId       数据权限Id。
+     * @param sysUserDtoFilter 用户过滤对象。
+     * @param orderParam       排序参数。
+     * @param pageParam        分页参数。
+     * @return 应答结果对象,包含用户列表数据。
+     */
+    @SaCheckPermission("sysDataPerm.view")
+    @PostMapping("/listDataPermUser")
+    public ResponseResult<MyPageData<SysUserVo>> listDataPermUser(
+            @MyRequestBody Long dataPermId,
+            @MyRequestBody SysUserDto sysUserDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        ResponseResult<Void> verifyResult = this.doDataPermUserVerify(dataPermId);
+        if (!verifyResult.isSuccess()) {
+            return ResponseResult.errorFrom(verifyResult);
+        }
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
+        }
+        SysUser filter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class);
+        List<SysUser> userList = sysUserService.getSysUserListByDataPermId(dataPermId, filter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(userList, SysUserVo.class));
+    }
+
+    /**
+     * 获取不包含指定数据权限Id的用户列表。
+     * 用户和数据权限是多对多关系,当前接口将返回没有赋值指定DataPermId的用户列表。可用于给数据权限添加新用户。
+     *
+     * @param dataPermId       数据权限主键Id。
+     * @param sysUserDtoFilter 用户数据的过滤对象。
+     * @param orderParam       排序参数。
+     * @param pageParam        分页参数。
+     * @return 应答结果对象,包含用户列表数据。
+     */
+    @SaCheckPermission("sysDataPerm.update")
+    @PostMapping("/listNotInDataPermUser")
+    public ResponseResult<MyPageData<SysUserVo>> listNotInDataPermUser(
+            @MyRequestBody Long dataPermId,
+            @MyRequestBody SysUserDto sysUserDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        ResponseResult<Void> verifyResult = this.doDataPermUserVerify(dataPermId);
+        if (!verifyResult.isSuccess()) {
+            return ResponseResult.errorFrom(verifyResult);
+        }
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
+        }
+        SysUser filter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class);
+        List<SysUser> userList =
+                sysUserService.getNotInSysUserListByDataPermId(dataPermId, filter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(userList, SysUserVo.class));
+    }
+
+    /**
+     * 为指定数据权限添加用户列表。该操作可同时给一批用户赋值数据权限,并在同一事务内完成。
+     *
+     * @param dataPermId       数据权限主键Id。
+     * @param userIdListString 逗号分隔的用户Id列表。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysDataPerm.update")
+    @OperationLog(type = SysOperationLogType.ADD_M2M)
+    @PostMapping("/addDataPermUser")
+    public ResponseResult<Void> addDataPermUser(
+            @MyRequestBody Long dataPermId, @MyRequestBody String userIdListString) {
+        if (MyCommonUtil.existBlankArgument(dataPermId, userIdListString)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        Set<Long> userIdSet =
+                Arrays.stream(userIdListString.split(",")).map(Long::valueOf).collect(Collectors.toSet());
+        if (!sysDataPermService.existId(dataPermId)
+                || !sysUserService.existUniqueKeyList("userId", userIdSet)) {
+            return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID);
+        }
+        sysDataPermService.addDataPermUserList(dataPermId, userIdSet);
+        return ResponseResult.success();
+    }
+
+    /**
+     * 为指定用户移除指定数据权限。
+     *
+     * @param dataPermId 指定数据权限主键Id。
+     * @param userId     指定用户主键Id。
+     * @return 应答数据结果。
+     */
+    @SaCheckPermission("sysDataPerm.update")
+    @OperationLog(type = SysOperationLogType.DELETE_M2M)
+    @PostMapping("/deleteDataPermUser")
+    public ResponseResult<Void> deleteDataPermUser(
+            @MyRequestBody Long dataPermId, @MyRequestBody Long userId) {
+        if (MyCommonUtil.existBlankArgument(dataPermId, userId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        if (!sysDataPermService.removeDataPermUser(dataPermId, userId)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 以字典形式返回全部数据权限管理数据集合。字典的键值为[dataPermId, dataPermName]。
+     * 白名单接口,登录用户均可访问。
+     *
+     * @param filter 过滤对象。
+     * @return 应答结果对象,包含的数据为 List<Map<String, String>>,map中包含两条记录,key的值分别是id和name,value对应具体数据。
+     */
+    @GetMapping("/listDict")
+    public ResponseResult<List<Map<String, Object>>> listDict(@ParameterObject SysDataPermDto filter) {
+        List<SysDataPerm> resultList =
+                sysDataPermService.getListByFilter(MyModelUtil.copyTo(filter, SysDataPerm.class));
+        return ResponseResult.success(
+                MyCommonUtil.toDictDataList(resultList, SysDataPerm::getDataPermId, SysDataPerm::getDataPermName));
+    }
+
+    private ResponseResult<Void> doDataPermUserVerify(Long dataPermId) {
+        if (MyCommonUtil.existBlankArgument(dataPermId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        if (!sysDataPermService.existId(dataPermId)) {
+            return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID);
+        }
+        return ResponseResult.success();
+    }
+}

+ 428 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/SysDeptController.java

@@ -0,0 +1,428 @@
+package com.serbia.webadmin.upms.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.serbia.common.log.annotation.OperationLog;
+import com.serbia.common.log.model.constant.SysOperationLogType;
+import com.github.pagehelper.page.PageMethod;
+import com.serbia.webadmin.upms.vo.*;
+import com.serbia.webadmin.upms.dto.*;
+import com.serbia.webadmin.upms.model.*;
+import com.serbia.webadmin.upms.service.*;
+import com.serbia.common.core.object.*;
+import com.serbia.common.core.util.*;
+import com.serbia.common.core.constant.*;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springdoc.core.annotations.ParameterObject;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 部门管理操作控制器类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Tag(name = "部门管理管理接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/upms/sysDept")
+public class SysDeptController {
+
+    @Autowired
+    private SysPostService sysPostService;
+    @Autowired
+    private SysDeptService sysDeptService;
+
+    /**
+     * 新增部门管理数据。
+     *
+     * @param sysDeptDto 新增对象。
+     * @return 应答结果对象,包含新增对象主键Id。
+     */
+    @ApiOperationSupport(ignoreParameters = {"sysDeptDto.deptId"})
+    @SaCheckPermission("sysDept.add")
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/add")
+    public ResponseResult<Long> add(@MyRequestBody SysDeptDto sysDeptDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(sysDeptDto, false);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SysDept sysDept = MyModelUtil.copyTo(sysDeptDto, SysDept.class);
+        // 验证父Id的数据合法性
+        SysDept parentSysDept = null;
+        if (MyCommonUtil.isNotBlankOrNull(sysDept.getParentId())) {
+            parentSysDept = sysDeptService.getById(sysDept.getParentId());
+            if (parentSysDept == null) {
+                errorMessage = "数据验证失败,关联的父节点并不存在,请刷新后重试!";
+                return ResponseResult.error(ErrorCodeEnum.DATA_PARENT_ID_NOT_EXIST, errorMessage);
+            }
+        }
+        sysDept = sysDeptService.saveNew(sysDept, parentSysDept);
+        return ResponseResult.success(sysDept.getDeptId());
+    }
+
+    /**
+     * 更新部门管理数据。
+     *
+     * @param sysDeptDto 更新对象。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysDept.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/update")
+    public ResponseResult<Void> update(@MyRequestBody SysDeptDto sysDeptDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(sysDeptDto, true);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SysDept sysDept = MyModelUtil.copyTo(sysDeptDto, SysDept.class);
+        SysDept originalSysDept = sysDeptService.getById(sysDept.getDeptId());
+        if (originalSysDept == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        // 验证父Id的数据合法性
+        if (MyCommonUtil.isNotBlankOrNull(sysDept.getParentId())
+                && ObjectUtil.notEqual(sysDept.getParentId(), originalSysDept.getParentId())) {
+            SysDept parentSysDept = sysDeptService.getById(sysDept.getParentId());
+            if (parentSysDept == null) {
+                // NOTE: 修改下面方括号中的话述
+                errorMessage = "数据验证失败,关联的 [父节点] 并不存在,请刷新后重试!";
+                return ResponseResult.error(ErrorCodeEnum.DATA_PARENT_ID_NOT_EXIST, errorMessage);
+            }
+        }
+        if (!sysDeptService.update(sysDept, originalSysDept)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除部门管理数据。
+     *
+     * @param deptId 删除对象主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysDept.delete")
+    @OperationLog(type = SysOperationLogType.DELETE)
+    @PostMapping("/delete")
+    public ResponseResult<Void> delete(@MyRequestBody Long deptId) {
+        if (MyCommonUtil.existBlankArgument(deptId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        return this.doDelete(deptId);
+    }
+
+    /**
+     * 批量删除部门管理数据。
+     *
+     * @param deptIdList 待删除对象的主键Id列表。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysDept.delete")
+    @OperationLog(type = SysOperationLogType.DELETE_BATCH)
+    @PostMapping("/deleteBatch")
+    public ResponseResult<Void> deleteBatch(@MyRequestBody List<Long> deptIdList) {
+        if (MyCommonUtil.existBlankArgument(deptIdList)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        for (Long deptId : deptIdList) {
+            ResponseResult<Void> responseResult = this.doDelete(deptId);
+            if (!responseResult.isSuccess()) {
+                return responseResult;
+            }
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 列出符合过滤条件的部门管理列表。
+     *
+     * @param sysDeptDtoFilter 过滤对象。
+     * @param orderParam 排序参数。
+     * @param pageParam 分页参数。
+     * @return 应答结果对象,包含查询结果集。
+     */
+    @SaCheckPermission("sysDept.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<SysDeptVo>> list(
+            @MyRequestBody SysDeptDto sysDeptDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount());
+        }
+        SysDept sysDeptFilter = MyModelUtil.copyTo(sysDeptDtoFilter, SysDept.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SysDept.class);
+        List<SysDept> sysDeptList = sysDeptService.getSysDeptListWithRelation(sysDeptFilter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(sysDeptList, SysDeptVo.class));
+    }
+
+    /**
+     * 查看指定部门管理对象详情。
+     *
+     * @param deptId 指定对象主键Id。
+     * @return 应答结果对象,包含对象详情。
+     */
+    @SaCheckPermission("sysDept.view")
+    @GetMapping("/view")
+    public ResponseResult<SysDeptVo> view(@RequestParam Long deptId) {
+        SysDept sysDept = sysDeptService.getByIdWithRelation(deptId, MyRelationParam.full());
+        if (sysDept == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        SysDeptVo sysDeptVo = MyModelUtil.copyTo(sysDept, SysDeptVo.class);
+        return ResponseResult.success(sysDeptVo);
+    }
+
+    /**
+     * 列出不与指定部门管理存在多对多关系的 [岗位管理] 列表数据。通常用于查看添加新 [岗位管理] 对象的候选列表。
+     *
+     * @param deptId 主表关联字段。
+     * @param sysPostDtoFilter [岗位管理] 过滤对象。
+     * @param orderParam 排序参数。
+     * @param pageParam 分页参数。
+     * @return 应答结果对象,返回符合条件的数据列表。
+     */
+    @SaCheckPermission("sysDept.update")
+    @PostMapping("/listNotInSysDeptPost")
+    public ResponseResult<MyPageData<SysPostVo>> listNotInSysDeptPost(
+            @MyRequestBody Long deptId,
+            @MyRequestBody SysPostDto sysPostDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (MyCommonUtil.isNotBlankOrNull(deptId) && !sysDeptService.existId(deptId)) {
+            return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID);
+        }
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
+        }
+        SysPost filter = MyModelUtil.copyTo(sysPostDtoFilter, SysPost.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SysPost.class);
+        List<SysPost> sysPostList;
+        if (MyCommonUtil.isNotBlankOrNull(deptId)) {
+            sysPostList = sysPostService.getNotInSysPostListByDeptId(deptId, filter, orderBy);
+        } else {
+            sysPostList = sysPostService.getSysPostList(filter, orderBy);
+            sysPostService.buildRelationForDataList(sysPostList, MyRelationParam.dictOnly());
+        }
+        return ResponseResult.success(MyPageUtil.makeResponseData(sysPostList, SysPostVo.class));
+    }
+
+    /**
+     * 列出与指定部门管理存在多对多关系的 [岗位管理] 列表数据。
+     *
+     * @param deptId 主表关联字段。
+     * @param sysPostDtoFilter [岗位管理] 过滤对象。
+     * @param orderParam 排序参数。
+     * @param pageParam 分页参数。
+     * @return 应答结果对象,返回符合条件的数据列表。
+     */
+    @SaCheckPermission("sysDept.view")
+    @PostMapping("/listSysDeptPost")
+    public ResponseResult<MyPageData<SysPostVo>> listSysDeptPost(
+            @MyRequestBody(required = true) Long deptId,
+            @MyRequestBody SysPostDto sysPostDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (!sysDeptService.existId(deptId)) {
+            return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID);
+        }
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
+        }
+        SysPost filter = MyModelUtil.copyTo(sysPostDtoFilter, SysPost.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SysPost.class);
+        List<SysPost> sysPostList = sysPostService.getSysPostListByDeptId(deptId, filter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(sysPostList, SysPostVo.class));
+    }
+
+    /**
+     * 批量添加部门管理和 [岗位管理] 对象的多对多关联关系数据。
+     *
+     * @param deptId 主表主键Id。
+     * @param sysDeptPostDtoList 关联对象列表。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysDept.update")
+    @PostMapping("/addSysDeptPost")
+    public ResponseResult<Void> addSysDeptPost(
+            @MyRequestBody Long deptId,
+            @MyRequestBody List<SysDeptPostDto> sysDeptPostDtoList) {
+        if (MyCommonUtil.existBlankArgument(deptId, sysDeptPostDtoList)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        String errorMessage = MyCommonUtil.getModelValidationError(sysDeptPostDtoList);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        Set<Long> postIdSet = sysDeptPostDtoList.stream().map(SysDeptPostDto::getPostId).collect(Collectors.toSet());
+        if (!sysDeptService.existId(deptId) || !sysPostService.existUniqueKeyList("postId", postIdSet)) {
+            return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID);
+        }
+        List<SysDeptPost> sysDeptPostList = MyModelUtil.copyCollectionTo(sysDeptPostDtoList, SysDeptPost.class);
+        sysDeptService.addSysDeptPostList(sysDeptPostList, deptId);
+        return ResponseResult.success();
+    }
+
+    /**
+     * 更新指定部门管理和指定 [岗位管理] 的多对多关联数据。
+     *
+     * @param sysDeptPostDto 对多对中间表对象。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysDept.update")
+    @PostMapping("/updateSysDeptPost")
+    public ResponseResult<Void> updateSysDeptPost(@MyRequestBody SysDeptPostDto sysDeptPostDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(sysDeptPostDto);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SysDeptPost sysDeptPost = MyModelUtil.copyTo(sysDeptPostDto, SysDeptPost.class);
+        if (!sysDeptService.updateSysDeptPost(sysDeptPost)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 显示部门管理和指定 [岗位管理] 的多对多关联详情数据。
+     *
+     * @param deptId 主表主键Id。
+     * @param postId 从表主键Id。
+     * @return 应答结果对象,包括中间表详情。
+     */
+    @SaCheckPermission("sysDept.update")
+    @GetMapping("/viewSysDeptPost")
+    public ResponseResult<SysDeptPostVo> viewSysDeptPost(@RequestParam Long deptId, @RequestParam Long postId) {
+        SysDeptPost sysDeptPost = sysDeptService.getSysDeptPost(deptId, postId);
+        if (sysDeptPost == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        SysDeptPostVo sysDeptPostVo = MyModelUtil.copyTo(sysDeptPost, SysDeptPostVo.class);
+        return ResponseResult.success(sysDeptPostVo);
+    }
+
+    /**
+     * 移除指定部门管理和指定 [岗位管理] 的多对多关联关系。
+     *
+     * @param deptId 主表主键Id。
+     * @param postId 从表主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysDept.update")
+    @PostMapping("/deleteSysDeptPost")
+    public ResponseResult<Void> deleteSysDeptPost(@MyRequestBody Long deptId, @MyRequestBody Long postId) {
+        if (MyCommonUtil.existBlankArgument(deptId, postId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        if (!sysDeptService.removeSysDeptPost(deptId, postId)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 获取部门岗位多对多关联数据,及其关联的部门和岗位数据。
+     *
+     * @param deptId 部门Id,如果为空,返回全部数据列表。
+     * @return 部门岗位多对多关联数据,及其关联的部门和岗位数据
+     */
+    @GetMapping("/listSysDeptPostWithRelation")
+    public ResponseResult<List<Map<String, Object>>> listSysDeptPostWithRelation(
+            @RequestParam(required = false) Long deptId) {
+        return ResponseResult.success(sysDeptService.getSysDeptPostListWithRelationByDeptId(deptId));
+    }
+
+    /**
+     * 以字典形式返回全部部门管理数据集合。字典的键值为[deptId, deptName]。
+     * 白名单接口,登录用户均可访问。
+     *
+     * @param filter 过滤对象。
+     * @return 应答结果对象,包含的数据为 List<Map<String, String>>,map中包含两条记录,key的值分别是id和name,value对应具体数据。
+     */
+    @GetMapping("/listDict")
+    public ResponseResult<List<Map<String, Object>>> listDict(@ParameterObject SysDeptDto filter) {
+        List<SysDept> resultList =
+                sysDeptService.getListByFilter(MyModelUtil.copyTo(filter, SysDept.class));
+        return ResponseResult.success(MyCommonUtil.toDictDataList(
+                resultList, SysDept::getDeptId, SysDept::getDeptName, SysDept::getParentId));
+    }
+
+    /**
+     * 根据字典Id集合,获取查询后的字典数据。
+     *
+     * @param dictIds 字典Id集合。
+     * @return 应答结果对象,包含字典形式的数据集合。
+     */
+    @GetMapping("/listDictByIds")
+    public ResponseResult<List<Map<String, Object>>> listDictByIds(@RequestParam List<Long> dictIds) {
+        List<SysDept> resultList = sysDeptService.getInList(new HashSet<>(dictIds));
+        return ResponseResult.success(MyCommonUtil.toDictDataList(
+                resultList, SysDept::getDeptId, SysDept::getDeptName, SysDept::getParentId));
+    }
+
+    /**
+     * 根据父主键Id,以字典的形式返回其下级数据列表。
+     * 白名单接口,登录用户均可访问。
+     *
+     * @param parentId 父主键Id。
+     * @return 按照字典的形式返回下级数据列表。
+     */
+    @GetMapping("/listDictByParentId")
+    public ResponseResult<List<Map<String, Object>>> listDictByParentId(@RequestParam(required = false) Long parentId) {
+        List<SysDept> resultList = sysDeptService.getListByParentId("parentId", parentId);
+        return ResponseResult.success(MyCommonUtil.toDictDataList(
+                resultList, SysDept::getDeptId, SysDept::getDeptName, SysDept::getParentId));
+    }
+    
+    /**
+     * 根据父主键Id列表,获取当前部门Id及其所有下级部门Id列表。
+     * 白名单接口,登录用户均可访问。
+     *
+     * @param parentIds 父主键Id列表,多个Id之间逗号分隔。
+     * @return 获取当前部门Id及其所有下级部门Id列表。
+     */
+    @GetMapping("/listAllChildDeptIdByParentIds")
+    public ResponseResult<List<Long>> listAllChildDeptIdByParentIds(
+            @RequestParam(required = false) String parentIds) {
+        List<Long> parentIdList = StrUtil.split(parentIds, ',')
+                .stream().map(Long::valueOf).collect(Collectors.toList());
+        return ResponseResult.success(sysDeptService.getAllChildDeptIdByParentIds(parentIdList));
+    }
+
+    private ResponseResult<Void> doDelete(Long deptId) {
+        String errorMessage;
+        // 验证关联Id的数据合法性
+        SysDept originalSysDept = sysDeptService.getById(deptId);
+        if (originalSysDept == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (sysDeptService.hasChildren(deptId)) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [对象存在子对象] ,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.HAS_CHILDREN_DATA, errorMessage);
+        }
+        if (sysDeptService.hasChildrenUser(deptId)) {
+            errorMessage = "数据验证失败,请先移除部门用户数据后,再删除当前部门!";
+            return ResponseResult.error(ErrorCodeEnum.HAS_CHILDREN_DATA, errorMessage);
+        }
+        if (!sysDeptService.remove(deptId)) {
+            errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+}

+ 231 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/SysMenuController.java

@@ -0,0 +1,231 @@
+package com.serbia.webadmin.upms.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import com.serbia.webadmin.upms.dto.SysMenuDto;
+import com.serbia.webadmin.upms.vo.SysMenuVo;
+import com.serbia.webadmin.upms.model.SysMenu;
+import com.serbia.webadmin.upms.model.SysDataPerm;
+import com.serbia.webadmin.upms.model.constant.SysMenuType;
+import com.serbia.webadmin.upms.service.SysMenuService;
+import com.serbia.webadmin.upms.service.SysDataPermService;
+import com.serbia.common.core.constant.ErrorCodeEnum;
+import com.serbia.common.core.object.*;
+import com.serbia.common.core.util.*;
+import com.serbia.common.core.validator.UpdateGroup;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.serbia.common.log.annotation.OperationLog;
+import com.serbia.common.log.model.constant.SysOperationLogType;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import jakarta.validation.groups.Default;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 菜单管理接口控制器类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Tag(name = "菜单管理接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/upms/sysMenu")
+public class SysMenuController {
+
+    @Autowired
+    private SysMenuService sysMenuService;
+    @Autowired
+    private SysDataPermService sysDataPermService;
+
+    /**
+     * 添加新菜单操作。
+     *
+     * @param sysMenuDto 新菜单对象。
+     * @return 应答结果对象,包含新增菜单的主键Id。
+     */
+    @ApiOperationSupport(ignoreParameters = {"sysMenuDto.menuId"})
+    @SaCheckPermission("sysMenu.add")
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/add")
+    public ResponseResult<Long> add(@MyRequestBody SysMenuDto sysMenuDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(sysMenuDto);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SysMenu sysMenu = MyModelUtil.copyTo(sysMenuDto, SysMenu.class);
+        if (sysMenu.getParentId() != null) {
+            SysMenu parentSysMenu = sysMenuService.getById(sysMenu.getParentId());
+            if (parentSysMenu == null) {
+                errorMessage = "数据验证失败,关联的父菜单不存在!";
+                return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+            }
+            if (parentSysMenu.getOnlineFormId() != null) {
+                errorMessage = "数据验证失败,不能为动态表单菜单添加子菜单!";
+                return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+            }
+        }
+        CallResult result = sysMenuService.verifyRelatedData(sysMenu, null);
+        if (!result.isSuccess()) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage());
+        }
+        sysMenuService.saveNew(sysMenu);
+        return ResponseResult.success(sysMenu.getMenuId());
+    }
+
+    /**
+     * 更新菜单数据操作。
+     *
+     * @param sysMenuDto 新菜单对象。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysMenu.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/update")
+    public ResponseResult<Void> update(@MyRequestBody SysMenuDto sysMenuDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(sysMenuDto, Default.class, UpdateGroup.class);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SysMenu originalSysMenu = sysMenuService.getById(sysMenuDto.getMenuId());
+        if (originalSysMenu == null) {
+            errorMessage = "数据验证失败,当前菜单并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        SysMenu sysMenu = MyModelUtil.copyTo(sysMenuDto, SysMenu.class);
+        if (ObjectUtil.notEqual(originalSysMenu.getOnlineFormId(), sysMenu.getOnlineFormId())) {
+            if (originalSysMenu.getOnlineFormId() == null) {
+                errorMessage = "数据验证失败,不能为当前菜单添加在线表单Id属性!";
+                return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+            }
+            if (sysMenu.getOnlineFormId() == null) {
+                errorMessage = "数据验证失败,不能去掉当前菜单的在线表单Id属性!";
+                return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+            }
+        }
+        if (originalSysMenu.getOnlineFormId() != null
+                && originalSysMenu.getMenuType().equals(SysMenuType.TYPE_BUTTON)) {
+            errorMessage = "数据验证失败,在线表单的内置菜单不能编辑!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        CallResult result = sysMenuService.verifyRelatedData(sysMenu, originalSysMenu);
+        if (!result.isSuccess()) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage());
+        }
+        if (!sysMenuService.update(sysMenu, originalSysMenu)) {
+            errorMessage = "数据验证失败,当前权限字并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除指定菜单操作。
+     *
+     * @param menuId 指定菜单主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysMenu.delete")
+    @OperationLog(type = SysOperationLogType.DELETE)
+    @PostMapping("/delete")
+    public ResponseResult<Void> delete(@MyRequestBody Long menuId) {
+        if (MyCommonUtil.existBlankArgument(menuId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        String errorMessage;
+        SysMenu menu = sysMenuService.getById(menuId);
+        if (menu == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        if (menu.getOnlineFormId() != null && menu.getMenuType().equals(SysMenuType.TYPE_BUTTON)) {
+            errorMessage = "数据验证失败,在线表单的内置菜单不能删除!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        // 对于在线表单,无需进行子菜单的验证,而是在删除的时候,连同子菜单一起删除。
+        if (menu.getOnlineFormId() == null && sysMenuService.hasChildren(menuId)) {
+            errorMessage = "数据验证失败,当前菜单存在下级菜单!";
+            return ResponseResult.error(ErrorCodeEnum.HAS_CHILDREN_DATA, errorMessage);
+        }
+        List<SysDataPerm> dataPermList = sysDataPermService.getSysDataPermListByMenuId(menuId);
+        if (CollUtil.isNotEmpty(dataPermList)) {
+            SysDataPerm dataPerm = dataPermList.get(0);
+            errorMessage = "数据验证失败,当前菜单正在被数据权限 [" + dataPerm.getDataPermName() + "] 引用,不能直接删除!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        if (!sysMenuService.remove(menu)) {
+            errorMessage = "数据操作失败,菜单不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 获取全部菜单列表。
+     *
+     * @return 应答结果对象,包含全部菜单数据列表。
+     */
+    @SaCheckPermission("sysMenu.view")
+    @PostMapping("/list")
+    public ResponseResult<List<SysMenuVo>> list() {
+        List<SysMenu> resultList = this.getAllMenuListByShowOrder();
+        return ResponseResult.success(MyModelUtil.copyCollectionTo(resultList, SysMenuVo.class));
+    }
+
+    /**
+     * 查看指定菜单数据详情。
+     *
+     * @param menuId 指定菜单主键Id。
+     * @return 应答结果对象,包含菜单详情。
+     */
+    @SaCheckPermission("sysMenu.view")
+    @GetMapping("/view")
+    public ResponseResult<SysMenuVo> view(@RequestParam Long menuId) {
+        if (MyCommonUtil.existBlankArgument(menuId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        SysMenu sysMenu = sysMenuService.getByIdWithRelation(menuId, MyRelationParam.full());
+        if (sysMenu == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        SysMenuVo sysMenuVo = MyModelUtil.copyTo(sysMenu, SysMenuVo.class);
+        return ResponseResult.success(sysMenuVo);
+    }
+
+    /**
+     * 以字典形式返回目录和菜单类型的菜单管理数据集合。字典的键值为[menuId, menuName]。
+     * 白名单接口,登录用户均可访问。
+     *
+     * @return 应答结果对象,包含的数据为 List<Map<String, String>>,map中包含两条记录,key的值分别是id和name,value对应具体数据。
+     */
+    @GetMapping("/listMenuDict")
+    public ResponseResult<List<Map<String, Object>>> listMenuDict() {
+        List<SysMenu> resultList = this.getAllMenuListByShowOrder();
+        resultList = resultList.stream()
+                .filter(m -> m.getMenuType() <= SysMenuType.TYPE_MENU).collect(Collectors.toList());
+        return ResponseResult.success(
+                MyCommonUtil.toDictDataList(resultList, SysMenu::getMenuId, SysMenu::getMenuName, SysMenu::getParentId));
+    }
+
+    /**
+     * 以字典形式返回全部的菜单管理数据集合。字典的键值为[menuId, menuName]。
+     * 白名单接口,登录用户均可访问。
+     *
+     * @return 应答结果对象,包含的数据为 List<Map<String, String>>,map中包含两条记录,key的值分别是id和name,value对应具体数据。
+     */
+    @GetMapping("/listDict")
+    public ResponseResult<List<Map<String, Object>>> listDict() {
+        List<SysMenu> resultList = this.getAllMenuListByShowOrder();
+        return ResponseResult.success(
+                MyCommonUtil.toDictDataList(resultList, SysMenu::getMenuId, SysMenu::getMenuName, SysMenu::getParentId));
+    }    
+
+    private List<SysMenu> getAllMenuListByShowOrder() {
+        return sysMenuService.getAllListByOrder("showOrder");
+    }
+}

+ 63 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/SysOperationLogController.java

@@ -0,0 +1,63 @@
+package com.serbia.webadmin.upms.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.github.pagehelper.Page;
+import com.github.pagehelper.page.PageMethod;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.serbia.common.core.object.*;
+import com.serbia.common.core.util.MyModelUtil;
+import com.serbia.common.core.util.MyPageUtil;
+import com.serbia.common.log.model.SysOperationLog;
+import com.serbia.common.log.service.SysOperationLogService;
+import com.serbia.common.log.dto.SysOperationLogDto;
+import com.serbia.common.log.vo.SysOperationLogVo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 操作日志接口控制器对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Tag(name = "操作日志接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/upms/sysOperationLog")
+public class SysOperationLogController {
+
+    @Autowired
+    private SysOperationLogService operationLogService;
+
+    /**
+     * 数据权限列表。
+     *
+     * @param sysOperationLogDtoFilter 操作日志查询过滤对象。
+     * @param orderParam               排序参数。
+     * @param pageParam                分页参数。
+     * @return 应答结果对象。包含操作日志列表。
+     */
+    @SaCheckPermission("sysOperationLog.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<SysOperationLogVo>> list(
+            @MyRequestBody SysOperationLogDto sysOperationLogDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
+        }
+        SysOperationLog filter = MyModelUtil.copyTo(sysOperationLogDtoFilter, SysOperationLog.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SysOperationLog.class);
+        List<SysOperationLog> operationLogList = operationLogService.getSysOperationLogList(filter, orderBy);
+        List<SysOperationLogVo> operationLogVoList = MyModelUtil.copyCollectionTo(operationLogList, SysOperationLogVo.class);
+        long totalCount = 0L;
+        if (operationLogList instanceof Page) {
+            totalCount = ((Page<SysOperationLog>) operationLogList).getTotal();
+        }
+        return ResponseResult.success(MyPageUtil.makeResponseData(operationLogVoList, totalCount));
+    }
+}

+ 183 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/SysPostController.java

@@ -0,0 +1,183 @@
+package com.serbia.webadmin.upms.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import com.github.pagehelper.page.PageMethod;
+import com.serbia.common.core.object.*;
+import com.serbia.common.core.util.*;
+import com.serbia.common.core.constant.*;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.serbia.common.core.validator.UpdateGroup;
+import com.serbia.webadmin.upms.dto.SysPostDto;
+import com.serbia.webadmin.upms.model.SysPost;
+import com.serbia.webadmin.upms.service.SysPostService;
+import com.serbia.webadmin.upms.vo.SysPostVo;
+import com.serbia.common.log.annotation.OperationLog;
+import com.serbia.common.log.model.constant.SysOperationLogType;
+import lombok.extern.slf4j.Slf4j;
+import org.springdoc.core.annotations.ParameterObject;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+import jakarta.validation.groups.Default;
+
+/**
+ * 岗位管理操作控制器类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Tag(name = "岗位管理操作管理接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/upms/sysPost")
+public class SysPostController {
+
+    @Autowired
+    private SysPostService sysPostService;
+
+    /**
+     * 新增岗位管理数据。
+     *
+     * @param sysPostDto 新增对象。
+     * @return 应答结果对象,包含新增对象主键Id。
+     */
+    @ApiOperationSupport(ignoreParameters = {"sysPostDto.postId"})
+    @SaCheckPermission("sysPost.add")
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/add")
+    public ResponseResult<Long> add(@MyRequestBody SysPostDto sysPostDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(sysPostDto);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SysPost sysPost = MyModelUtil.copyTo(sysPostDto, SysPost.class);
+        sysPost = sysPostService.saveNew(sysPost);
+        return ResponseResult.success(sysPost.getPostId());
+    }
+
+    /**
+     * 更新岗位管理数据。
+     *
+     * @param sysPostDto 更新对象。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysPost.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/update")
+    public ResponseResult<Void> update(@MyRequestBody SysPostDto sysPostDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(sysPostDto, Default.class, UpdateGroup.class);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SysPost sysPost = MyModelUtil.copyTo(sysPostDto, SysPost.class);
+        SysPost originalSysPost = sysPostService.getById(sysPost.getPostId());
+        if (originalSysPost == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (!sysPostService.update(sysPost, originalSysPost)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除岗位管理数据。
+     *
+     * @param postId 删除对象主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysPost.delete")
+    @OperationLog(type = SysOperationLogType.DELETE)
+    @PostMapping("/delete")
+    public ResponseResult<Void> delete(@MyRequestBody Long postId) {
+        String errorMessage;
+        if (MyCommonUtil.existBlankArgument(postId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        // 验证关联Id的数据合法性
+        SysPost originalSysPost = sysPostService.getById(postId);
+        if (originalSysPost == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (!sysPostService.remove(postId)) {
+            errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 列出符合过滤条件的岗位管理列表。
+     *
+     * @param sysPostDtoFilter 过滤对象。
+     * @param orderParam 排序参数。
+     * @param pageParam 分页参数。
+     * @return 应答结果对象,包含查询结果集。
+     */
+    @SaCheckPermission("sysPost.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<SysPostVo>> list(
+            @MyRequestBody SysPostDto sysPostDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
+        }
+        SysPost sysPostFilter = MyModelUtil.copyTo(sysPostDtoFilter, SysPost.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SysPost.class);
+        List<SysPost> sysPostList = sysPostService.getSysPostListWithRelation(sysPostFilter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(sysPostList, SysPostVo.class));
+    }
+
+    /**
+     * 查看指定岗位管理对象详情。
+     *
+     * @param postId 指定对象主键Id。
+     * @return 应答结果对象,包含对象详情。
+     */
+    @SaCheckPermission("sysPost.view")
+    @GetMapping("/view")
+    public ResponseResult<SysPostVo> view(@RequestParam Long postId) {
+        if (MyCommonUtil.existBlankArgument(postId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        SysPost sysPost = sysPostService.getByIdWithRelation(postId, MyRelationParam.full());
+        if (sysPost == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        SysPostVo sysPostVo = MyModelUtil.copyTo(sysPost, SysPostVo.class);
+        return ResponseResult.success(sysPostVo);
+    }
+
+    /**
+     * 以字典形式返回全部岗位管理数据集合。字典的键值为[postId, postName]。
+     * 白名单接口,登录用户均可访问。
+     *
+     * @param filter 过滤对象。
+     * @return 应答结果对象,包含的数据为 List<Map<String, String>>,map中包含两条记录,key的值分别是id和name,value对应具体数据。
+     */
+    @GetMapping("/listDict")
+    public ResponseResult<List<Map<String, Object>>> listDict(@ParameterObject SysPostDto filter) {
+        List<SysPost> resultList = sysPostService.getListByFilter(MyModelUtil.copyTo(filter, SysPost.class));
+        return ResponseResult.success(MyCommonUtil.toDictDataList(resultList, SysPost::getPostId, SysPost::getPostName));
+    }
+
+    /**
+     * 根据字典Id集合,获取查询后的字典数据。
+     *
+     * @param postIds 字典Id集合。
+     * @return 应答结果对象,包含字典形式的数据集合。
+     */
+    @GetMapping("/listDictByIds")
+    public ResponseResult<List<Map<String, Object>>> listDictByIds(@RequestParam List<Long> postIds) {
+        List<SysPost> resultList = sysPostService.getInList(new HashSet<>(postIds));
+        return ResponseResult.success(MyCommonUtil.toDictDataList(resultList, SysPost::getPostId, SysPost::getPostName));
+    }
+}

+ 331 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/SysRoleController.java

@@ -0,0 +1,331 @@
+package com.serbia.webadmin.upms.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import com.alibaba.fastjson.TypeReference;
+import com.github.pagehelper.Page;
+import com.github.pagehelper.page.PageMethod;
+import lombok.extern.slf4j.Slf4j;
+import com.serbia.webadmin.upms.dto.SysRoleDto;
+import com.serbia.webadmin.upms.dto.SysUserDto;
+import com.serbia.webadmin.upms.vo.SysRoleVo;
+import com.serbia.webadmin.upms.vo.SysUserVo;
+import com.serbia.webadmin.upms.model.SysRole;
+import com.serbia.webadmin.upms.model.SysUser;
+import com.serbia.webadmin.upms.model.SysUserRole;
+import com.serbia.webadmin.upms.service.SysRoleService;
+import com.serbia.webadmin.upms.service.SysUserService;
+import com.serbia.common.core.validator.UpdateGroup;
+import com.serbia.common.core.constant.ErrorCodeEnum;
+import com.serbia.common.core.object.*;
+import com.serbia.common.core.util.*;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.serbia.common.log.annotation.OperationLog;
+import com.serbia.common.log.model.constant.SysOperationLogType;
+import org.springdoc.core.annotations.ParameterObject;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import jakarta.validation.groups.Default;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 角色管理接口控制器类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Tag(name = "角色管理接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/upms/sysRole")
+public class SysRoleController {
+
+    @Autowired
+    private SysRoleService sysRoleService;
+    @Autowired
+    private SysUserService sysUserService;
+
+    /**
+     * 新增角色操作。
+     *
+     * @param sysRoleDto       新增角色对象。
+     * @param menuIdListString 与当前角色Id绑定的menuId列表,多个menuId之间逗号分隔。
+     * @return 应答结果对象,包含新增角色的主键Id。
+     */
+    @ApiOperationSupport(ignoreParameters = {"sysRoleDto.roleId", "sysRoleDto.createTimeStart", "sysRoleDto.createTimeEnd"})
+    @SaCheckPermission("sysRole.add")
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/add")
+    public ResponseResult<Long> add(
+            @MyRequestBody SysRoleDto sysRoleDto, @MyRequestBody String menuIdListString) {
+        String errorMessage = MyCommonUtil.getModelValidationError(sysRoleDto);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SysRole sysRole = MyModelUtil.copyTo(sysRoleDto, SysRole.class);
+        CallResult result = sysRoleService.verifyRelatedData(sysRole, null, menuIdListString);
+        if (!result.isSuccess()) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage());
+        }
+        Set<Long> menuIdSet = null;
+        if (result.getData() != null) {
+            menuIdSet = result.getData().getObject("menuIdSet", new TypeReference<Set<Long>>(){});
+        }
+        sysRoleService.saveNew(sysRole, menuIdSet);
+        return ResponseResult.success(sysRole.getRoleId());
+    }
+
+    /**
+     * 更新角色操作。
+     *
+     * @param sysRoleDto       更新角色对象。
+     * @param menuIdListString 与当前角色Id绑定的menuId列表,多个menuId之间逗号分隔。
+     * @return 应答结果对象。
+     */
+    @ApiOperationSupport(ignoreParameters = {"sysRoleDto.createTimeStart", "sysRoleDto.createTimeEnd"})
+    @SaCheckPermission("sysRole.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/update")
+    public ResponseResult<Void> update(
+            @MyRequestBody SysRoleDto sysRoleDto, @MyRequestBody String menuIdListString) {
+        String errorMessage = MyCommonUtil.getModelValidationError(sysRoleDto, Default.class, UpdateGroup.class);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SysRole originalSysRole = sysRoleService.getById(sysRoleDto.getRoleId());
+        if (originalSysRole == null) {
+            errorMessage = "数据验证失败,当前角色并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        SysRole sysRole = MyModelUtil.copyTo(sysRoleDto, SysRole.class);
+        CallResult result = sysRoleService.verifyRelatedData(sysRole, originalSysRole, menuIdListString);
+        if (!result.isSuccess()) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage());
+        }
+        Set<Long> menuIdSet = null;
+        if (result.getData() != null) {
+            menuIdSet = result.getData().getObject("menuIdSet", new TypeReference<Set<Long>>(){});
+        }
+        if (!sysRoleService.update(sysRole, originalSysRole, menuIdSet)) {
+            errorMessage = "更新失败,数据不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除指定角色操作。
+     *
+     * @param roleId 指定角色主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysRole.delete")
+    @OperationLog(type = SysOperationLogType.DELETE)
+    @PostMapping("/delete")
+    public ResponseResult<Void> delete(@MyRequestBody Long roleId) {
+        if (MyCommonUtil.existBlankArgument(roleId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        if (!sysRoleService.remove(roleId)) {
+            String errorMessage = "数据操作失败,角色不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 查看角色列表。
+     *
+     * @param sysRoleDtoFilter 角色过滤对象。
+     * @param orderParam       排序参数。
+     * @param pageParam        分页参数。
+     * @return 应答结果对象,包含角色列表。
+     */
+    @SaCheckPermission("sysRole.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<SysRoleVo>> list(
+            @MyRequestBody SysRoleDto sysRoleDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
+        }
+        SysRole filter = MyModelUtil.copyTo(sysRoleDtoFilter, SysRole.class);
+        List<SysRole> roleList = sysRoleService.getSysRoleList(
+                filter, MyOrderParam.buildOrderBy(orderParam, SysRole.class));
+        List<SysRoleVo> roleVoList = MyModelUtil.copyCollectionTo(roleList, SysRoleVo.class);
+        long totalCount = 0L;
+        if (roleList instanceof Page) {
+            totalCount = ((Page<SysRole>) roleList).getTotal();
+        }
+        return ResponseResult.success(MyPageUtil.makeResponseData(roleVoList, totalCount));
+    }
+
+    /**
+     * 查看角色详情。
+     *
+     * @param roleId 指定角色主键Id。
+     * @return 应答结果对象,包含角色详情对象。
+     */
+    @SaCheckPermission("sysRole.view")
+    @GetMapping("/view")
+    public ResponseResult<SysRoleVo> view(@RequestParam Long roleId) {
+        if (MyCommonUtil.existBlankArgument(roleId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        SysRole sysRole = sysRoleService.getByIdWithRelation(roleId, MyRelationParam.full());
+        if (sysRole == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        SysRoleVo sysRoleVo = MyModelUtil.copyTo(sysRole, SysRoleVo.class);
+        return ResponseResult.success(sysRoleVo);
+    }
+
+    /**
+     * 拥有指定角色的用户列表。
+     *
+     * @param roleId           角色主键Id。
+     * @param sysUserDtoFilter 用户过滤对象。
+     * @param orderParam       排序参数。
+     * @param pageParam        分页参数。
+     * @return 应答结果对象,包含用户列表数据。
+     */
+    @SaCheckPermission("sysRole.view")
+    @PostMapping("/listUserRole")
+    public ResponseResult<MyPageData<SysUserVo>> listUserRole(
+            @MyRequestBody Long roleId,
+            @MyRequestBody SysUserDto sysUserDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        ResponseResult<Void> verifyResult = this.doRoleUserVerify(roleId);
+        if (!verifyResult.isSuccess()) {
+            return ResponseResult.errorFrom(verifyResult);
+        }
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
+        }
+        SysUser filter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class);
+        List<SysUser> userList = sysUserService.getSysUserListByRoleId(roleId, filter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(userList, SysUserVo.class));
+    }
+
+    /**
+     * 获取不包含指定角色Id的用户列表。
+     * 用户和角色是多对多关系,当前接口将返回没有赋值指定RoleId的用户列表。可用于给角色添加新用户。
+     *
+     * @param roleId           角色主键Id。
+     * @param sysUserDtoFilter 用户过滤对象。
+     * @param orderParam       排序参数。
+     * @param pageParam        分页参数。
+     * @return 应答结果对象,包含用户列表数据。
+     */
+    @SaCheckPermission("sysRole.update")
+    @PostMapping("/listNotInUserRole")
+    public ResponseResult<MyPageData<SysUserVo>> listNotInUserRole(
+            @MyRequestBody Long roleId,
+            @MyRequestBody SysUserDto sysUserDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        ResponseResult<Void> verifyResult = this.doRoleUserVerify(roleId);
+        if (!verifyResult.isSuccess()) {
+            return ResponseResult.errorFrom(verifyResult);
+        }
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize());
+        }
+        SysUser filter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class);
+        List<SysUser> userList = sysUserService.getNotInSysUserListByRoleId(roleId, filter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(userList, SysUserVo.class));
+    }
+
+    /**
+     * 为指定角色添加用户列表。该操作可同时给一批用户赋值角色,并在同一事务内完成。
+     *
+     * @param roleId           角色主键Id。
+     * @param userIdListString 逗号分隔的用户Id列表。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysRole.update")
+    @OperationLog(type = SysOperationLogType.ADD_M2M)
+    @PostMapping("/addUserRole")
+    public ResponseResult<Void> addUserRole(@MyRequestBody Long roleId, @MyRequestBody String userIdListString) {
+        if (MyCommonUtil.existBlankArgument(roleId, userIdListString)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        Set<Long> userIdSet = Arrays.stream(
+                userIdListString.split(",")).map(Long::valueOf).collect(Collectors.toSet());
+        if (!sysRoleService.existId(roleId)
+                || !sysUserService.existUniqueKeyList("userId", userIdSet)) {
+            return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID);
+        }
+        List<SysUserRole> userRoleList = new LinkedList<>();
+        for (Long userId : userIdSet) {
+            SysUserRole userRole = new SysUserRole();
+            userRole.setRoleId(roleId);
+            userRole.setUserId(userId);
+            userRoleList.add(userRole);
+        }
+        sysRoleService.addUserRoleList(userRoleList);
+        return ResponseResult.success();
+    }
+
+    /**
+     * 为指定用户移除指定角色。
+     *
+     * @param roleId 指定角色主键Id。
+     * @param userId 指定用户主键Id。
+     * @return 应答数据结果。
+     */
+    @SaCheckPermission("sysRole.update")
+    @OperationLog(type = SysOperationLogType.DELETE_M2M)
+    @PostMapping("/deleteUserRole")
+    public ResponseResult<Void> deleteUserRole(@MyRequestBody Long roleId, @MyRequestBody Long userId) {
+        if (MyCommonUtil.existBlankArgument(roleId, userId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        if (!sysRoleService.removeUserRole(roleId, userId)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 以字典形式返回全部角色管理数据集合。字典的键值为[roleId, roleName]。
+     * 白名单接口,登录用户均可访问。
+     *
+     * @param filter 过滤对象。
+     * @return 应答结果对象,包含的数据为 List<Map<String, String>>,map中包含两条记录,key的值分别是id和name,value对应具体数据。
+     */
+    @GetMapping("/listDict")
+    public ResponseResult<List<Map<String, Object>>> listDict(@ParameterObject SysRoleDto filter) {
+        List<SysRole> resultList = sysRoleService.getListByFilter(MyModelUtil.copyTo(filter, SysRole.class));
+        return ResponseResult.success(MyCommonUtil.toDictDataList(resultList, SysRole::getRoleId, SysRole::getRoleName));
+    }
+
+    /**
+     * 根据字典Id集合,获取查询后的字典数据。
+     *
+     * @param dictIds 字典Id集合。
+     * @return 应答结果对象,包含字典形式的数据集合。
+     */
+    @GetMapping("/listDictByIds")
+    public ResponseResult<List<Map<String, Object>>> listDictByIds(@RequestParam List<Long> dictIds) {
+        List<SysRole> resultList = sysRoleService.getInList(new HashSet<>(dictIds));
+        return ResponseResult.success(MyCommonUtil.toDictDataList(resultList, SysRole::getRoleId, SysRole::getRoleName));
+    }
+
+    private ResponseResult<Void> doRoleUserVerify(Long roleId) {
+        if (MyCommonUtil.existBlankArgument(roleId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        if (!sysRoleService.existId(roleId)) {
+            return ResponseResult.error(ErrorCodeEnum.INVALID_RELATED_RECORD_ID);
+        }
+        return ResponseResult.success();
+    }
+}

+ 378 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/controller/SysUserController.java

@@ -0,0 +1,378 @@
+package com.serbia.webadmin.upms.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.alibaba.fastjson.TypeReference;
+import cn.hutool.core.util.ReflectUtil;
+import com.serbia.common.core.upload.BaseUpDownloader;
+import com.serbia.common.core.upload.UpDownloaderFactory;
+import com.serbia.common.core.upload.UploadResponseInfo;
+import com.serbia.common.core.upload.UploadStoreInfo;
+import com.serbia.common.log.annotation.OperationLog;
+import com.serbia.common.log.model.constant.SysOperationLogType;
+import com.github.pagehelper.page.PageMethod;
+import com.serbia.webadmin.upms.vo.*;
+import com.serbia.webadmin.upms.dto.*;
+import com.serbia.webadmin.upms.model.*;
+import com.serbia.webadmin.upms.service.*;
+import com.serbia.common.core.object.*;
+import com.serbia.common.core.util.*;
+import com.serbia.common.core.constant.*;
+import com.serbia.common.core.annotation.MyRequestBody;
+import com.serbia.common.redis.cache.SessionCacheHelper;
+import com.serbia.webadmin.config.ApplicationConfig;
+import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springdoc.core.annotations.ParameterObject;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * 用户管理操作控制器类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Tag(name = "用户管理管理接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/upms/sysUser")
+public class SysUserController {
+
+    @Autowired
+    private PasswordEncoder passwordEncoder;
+    @Autowired
+    private ApplicationConfig appConfig;
+    @Autowired
+    private SessionCacheHelper cacheHelper;
+    @Autowired
+    private UpDownloaderFactory upDownloaderFactory;
+    @Autowired
+    private SysUserService sysUserService;
+
+    /**
+     * 新增用户操作。
+     *
+     * @param sysUserDto           新增用户对象。
+     * @param deptPostIdListString 逗号分隔的部门岗位Id列表。
+     * @param dataPermIdListString 逗号分隔的数据权限Id列表。
+     * @param roleIdListString     逗号分隔的角色Id列表。
+     * @return 应答结果对象,包含新增用户的主键Id。
+     */
+    @ApiOperationSupport(ignoreParameters = {
+            "sysUserDto.userId",
+            "sysUserDto.createTimeStart",
+            "sysUserDto.createTimeEnd"})
+    @SaCheckPermission("sysUser.add")
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/add")
+    public ResponseResult<Long> add(
+            @MyRequestBody SysUserDto sysUserDto,
+            @MyRequestBody String deptPostIdListString,
+            @MyRequestBody String dataPermIdListString,
+            @MyRequestBody String roleIdListString) {
+        String errorMessage = MyCommonUtil.getModelValidationError(sysUserDto, false);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SysUser sysUser = MyModelUtil.copyTo(sysUserDto, SysUser.class);
+        CallResult result = sysUserService.verifyRelatedData(
+                sysUser, null, roleIdListString, deptPostIdListString, dataPermIdListString);
+        if (!result.isSuccess()) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage());
+        }
+        Set<Long> deptPostIdSet = result.getData().getObject("deptPostIdSet", new TypeReference<Set<Long>>() {});
+        Set<Long> roleIdSet = result.getData().getObject("roleIdSet", new TypeReference<Set<Long>>() {});
+        Set<Long> dataPermIdSet = result.getData().getObject("dataPermIdSet", new TypeReference<Set<Long>>() {});
+        sysUserService.saveNew(sysUser, roleIdSet, deptPostIdSet, dataPermIdSet);
+        return ResponseResult.success(sysUser.getUserId());
+    }
+
+    /**
+     * 更新用户操作。
+     *
+     * @param sysUserDto           更新用户对象。
+     * @param deptPostIdListString 逗号分隔的部门岗位Id列表。
+     * @param dataPermIdListString 逗号分隔的数据权限Id列表。
+     * @param roleIdListString     逗号分隔的角色Id列表。
+     * @return 应答结果对象。
+     */
+    @ApiOperationSupport(ignoreParameters = {
+            "sysUserDto.createTimeStart",
+            "sysUserDto.createTimeEnd"})
+    @SaCheckPermission("sysUser.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/update")
+    public ResponseResult<Void> update(
+            @MyRequestBody SysUserDto sysUserDto,
+            @MyRequestBody String deptPostIdListString,
+            @MyRequestBody String dataPermIdListString,
+            @MyRequestBody String roleIdListString) {
+        String errorMessage = MyCommonUtil.getModelValidationError(sysUserDto, true);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        SysUser originalUser = sysUserService.getById(sysUserDto.getUserId());
+        if (originalUser == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        SysUser sysUser = MyModelUtil.copyTo(sysUserDto, SysUser.class);
+        CallResult result = sysUserService.verifyRelatedData(
+                sysUser, originalUser, roleIdListString, deptPostIdListString, dataPermIdListString);
+        if (!result.isSuccess()) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, result.getErrorMessage());
+        }
+        Set<Long> roleIdSet = result.getData().getObject("roleIdSet", new TypeReference<Set<Long>>() {});
+        Set<Long> deptPostIdSet = result.getData().getObject("deptPostIdSet", new TypeReference<Set<Long>>() {});
+        Set<Long> dataPermIdSet = result.getData().getObject("dataPermIdSet", new TypeReference<Set<Long>>() {});
+        if (!sysUserService.update(sysUser, originalUser, roleIdSet, deptPostIdSet, dataPermIdSet)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 重置密码操作。
+     *
+     * @param userId 指定用户主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysUser.resetPassword")
+    @PostMapping("/resetPassword")
+    public ResponseResult<Void> resetPassword(@MyRequestBody Long userId) {
+        if (MyCommonUtil.existBlankArgument(userId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        if (!sysUserService.changePassword(userId, appConfig.getDefaultUserPassword())) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除用户管理数据。
+     *
+     * @param userId 删除对象主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysUser.delete")
+    @OperationLog(type = SysOperationLogType.DELETE)
+    @PostMapping("/delete")
+    public ResponseResult<Void> delete(@MyRequestBody Long userId) {
+        if (MyCommonUtil.existBlankArgument(userId)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        return this.doDelete(userId);
+    }
+
+    /**
+     * 批量删除用户管理数据。
+     *
+     * @param userIdList 待删除对象的主键Id列表。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("sysUser.delete")
+    @OperationLog(type = SysOperationLogType.DELETE_BATCH)
+    @PostMapping("/deleteBatch")
+    public ResponseResult<Void> deleteBatch(@MyRequestBody List<Long> userIdList) {
+        if (MyCommonUtil.existBlankArgument(userIdList)) {
+            return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST);
+        }
+        for (Long userId : userIdList) {
+            ResponseResult<Void> responseResult = this.doDelete(userId);
+            if (!responseResult.isSuccess()) {
+                return responseResult;
+            }
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 列出符合过滤条件的用户管理列表。
+     *
+     * @param sysUserDtoFilter 过滤对象。
+     * @param orderParam 排序参数。
+     * @param pageParam 分页参数。
+     * @return 应答结果对象,包含查询结果集。
+     */
+    @SaCheckPermission("sysUser.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<SysUserVo>> list(
+            @MyRequestBody SysUserDto sysUserDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount());
+        }
+        SysUser sysUserFilter = MyModelUtil.copyTo(sysUserDtoFilter, SysUser.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, SysUser.class);
+        List<SysUser> sysUserList = sysUserService.getSysUserListWithRelation(sysUserFilter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(sysUserList, SysUserVo.class));
+    }
+
+    /**
+     * 查看指定用户管理对象详情。
+     *
+     * @param userId 指定对象主键Id。
+     * @return 应答结果对象,包含对象详情。
+     */
+    @SaCheckPermission("sysUser.view")
+    @GetMapping("/view")
+    public ResponseResult<SysUserVo> view(@RequestParam Long userId) {
+        // 这里查看用户数据时候,需要把用户多对多关联的角色和数据权限Id一并查出。
+        SysUser sysUser = sysUserService.getByIdWithRelation(userId, MyRelationParam.full());
+        if (sysUser == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        SysUserVo sysUserVo = MyModelUtil.copyTo(sysUser, SysUserVo.class);
+        return ResponseResult.success(sysUserVo);
+    }
+
+    /**
+     * 附件文件下载。
+     * 这里将图片和其他类型的附件文件放到不同的父目录下,主要为了便于今后图片文件的迁移。
+     *
+     * @param userId 附件所在记录的主键Id。
+     * @param fieldName 附件所属的字段名。
+     * @param filename  文件名。如果没有提供该参数,就从当前记录的指定字段中读取。
+     * @param asImage   下载文件是否为图片。
+     * @param response  Http 应答对象。
+     */
+    @SaCheckPermission("sysUser.view")
+    @OperationLog(type = SysOperationLogType.DOWNLOAD, saveResponse = false)
+    @GetMapping("/download")
+    public void download(
+            @RequestParam(required = false) Long userId,
+            @RequestParam String fieldName,
+            @RequestParam String filename,
+            @RequestParam Boolean asImage,
+            HttpServletResponse response) {
+        if (MyCommonUtil.existBlankArgument(fieldName, filename, asImage)) {
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            return;
+        }
+        // 使用try来捕获异常,是为了保证一旦出现异常可以返回500的错误状态,便于调试。
+        // 否则有可能给前端返回的是200的错误码。
+        try {
+            // 如果请求参数中没有包含主键Id,就判断该文件是否为当前session上传的。
+            if (userId == null) {
+                if (!cacheHelper.existSessionUploadFile(filename)) {
+                    ResponseResult.output(HttpServletResponse.SC_FORBIDDEN);
+                    return;
+                }
+            } else {
+                SysUser sysUser = sysUserService.getById(userId);
+                if (sysUser == null) {
+                    ResponseResult.output(HttpServletResponse.SC_NOT_FOUND);
+                    return;
+                }
+                String fieldJsonData = (String) ReflectUtil.getFieldValue(sysUser, fieldName);
+                if (fieldJsonData == null && !cacheHelper.existSessionUploadFile(filename)) {
+                    ResponseResult.output(HttpServletResponse.SC_BAD_REQUEST);
+                    return;
+                }
+                if (!BaseUpDownloader.containFile(fieldJsonData, filename)
+                        && !cacheHelper.existSessionUploadFile(filename)) {
+                    ResponseResult.output(HttpServletResponse.SC_FORBIDDEN);
+                    return;
+                }
+            }
+            UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, fieldName);
+            if (!storeInfo.isSupportUpload()) {
+                ResponseResult.output(HttpServletResponse.SC_NOT_IMPLEMENTED,
+                        ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD));
+                return;
+            }
+            BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
+            upDownloader.doDownload(appConfig.getUploadFileBaseDir(),
+                    SysUser.class.getSimpleName(), fieldName, filename, asImage, response);
+        } catch (Exception e) {
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            log.error(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 文件上传操作。
+     *
+     * @param fieldName  上传文件名。
+     * @param asImage    是否作为图片上传。如果是图片,今后下载的时候无需权限验证。否则就是附件上传,下载时需要权限验证。
+     * @param uploadFile 上传文件对象。
+     */
+    @SaCheckPermission("sysUser.view")
+    @OperationLog(type = SysOperationLogType.UPLOAD, saveResponse = false)
+    @PostMapping("/upload")
+    public void upload(
+            @RequestParam String fieldName,
+            @RequestParam Boolean asImage,
+            @RequestParam("uploadFile") MultipartFile uploadFile) throws IOException {
+        UploadStoreInfo storeInfo = MyModelUtil.getUploadStoreInfo(SysUser.class, fieldName);
+        // 这里就会判断参数中指定的字段,是否支持上传操作。
+        if (!storeInfo.isSupportUpload()) {
+            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN,
+                    ResponseResult.error(ErrorCodeEnum.INVALID_UPLOAD_FIELD));
+            return;
+        }
+        // 根据字段注解中的存储类型,通过工厂方法获取匹配的上传下载实现类,从而解耦。
+        BaseUpDownloader upDownloader = upDownloaderFactory.get(storeInfo.getStoreType());
+        UploadResponseInfo responseInfo = upDownloader.doUpload(null,
+                appConfig.getUploadFileBaseDir(), SysUser.class.getSimpleName(), fieldName, asImage, uploadFile);
+        if (Boolean.TRUE.equals(responseInfo.getUploadFailed())) {
+            ResponseResult.output(HttpServletResponse.SC_FORBIDDEN,
+                    ResponseResult.error(ErrorCodeEnum.UPLOAD_FAILED, responseInfo.getErrorMessage()));
+            return;
+        }
+        cacheHelper.putSessionUploadFile(responseInfo.getFilename());
+        ResponseResult.output(ResponseResult.success(responseInfo));
+    }
+
+    /**
+     * 以字典形式返回全部用户管理数据集合。字典的键值为[userId, showName]。
+     * 白名单接口,登录用户均可访问。
+     *
+     * @param filter 过滤对象。
+     * @return 应答结果对象,包含的数据为 List<Map<String, String>>,map中包含两条记录,key的值分别是id和name,value对应具体数据。
+     */
+    @GetMapping("/listDict")
+    public ResponseResult<List<Map<String, Object>>> listDict(@ParameterObject SysUserDto filter) {
+        List<SysUser> resultList =
+                sysUserService.getListByFilter(MyModelUtil.copyTo(filter, SysUser.class));
+        return ResponseResult.success(
+                MyCommonUtil.toDictDataList(resultList, SysUser::getUserId, SysUser::getShowName));
+    }
+
+    /**
+     * 根据字典Id集合,获取查询后的字典数据。
+     *
+     * @param dictIds 字典Id集合。
+     * @return 应答结果对象,包含字典形式的数据集合。
+     */
+    @GetMapping("/listDictByIds")
+    public ResponseResult<List<Map<String, Object>>> listDictByIds(@RequestParam List<Long> dictIds) {
+        List<SysUser> resultList = sysUserService.getInList(new HashSet<>(dictIds));
+        return ResponseResult.success(
+                MyCommonUtil.toDictDataList(resultList, SysUser::getUserId, SysUser::getShowName));
+    }
+
+    private ResponseResult<Void> doDelete(Long userId) {
+        String errorMessage;
+        // 验证关联Id的数据合法性
+        SysUser originalSysUser = sysUserService.getById(userId);
+        if (originalSysUser == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (!sysUserService.remove(userId)) {
+            errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+}

+ 13 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysDataPermDeptMapper.java

@@ -0,0 +1,13 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysDataPermDept;
+
+/**
+ * 数据权限与部门关系数据访问操作接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysDataPermDeptMapper extends BaseDaoMapper<SysDataPermDept> {
+}

+ 43 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysDataPermMapper.java

@@ -0,0 +1,43 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysDataPerm;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 数据权限数据访问操作接口。
+ * NOTE: 该对象一定不能被 @EnableDataPerm 注解标注,否则会导致无限递归。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysDataPermMapper extends BaseDaoMapper<SysDataPerm> {
+
+    /**
+     * 获取数据权限列表。
+     *
+     * @param sysDataPermFilter 过滤对象。
+     * @param orderBy           排序字符串。
+     * @return 过滤后的数据权限列表。
+     */
+    List<SysDataPerm> getSysDataPermList(
+            @Param("sysDataPermFilter") SysDataPerm sysDataPermFilter, @Param("orderBy") String orderBy);
+
+    /**
+     * 获取指定用户的数据权限列表。
+     *
+     * @param userId 用户Id。
+     * @return 数据权限列表。
+     */
+    List<SysDataPerm> getSysDataPermListByUserId(@Param("userId") Long userId);
+
+    /**
+     * 查询与指定菜单关联的数据权限列表。
+     *
+     * @param menuId 菜单Id。
+     * @return 与菜单Id关联的数据权限列表。
+     */
+    List<SysDataPerm> getSysDataPermListByMenuId(@Param("menuId") Long menuId);
+}

+ 13 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysDataPermMenuMapper.java

@@ -0,0 +1,13 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysDataPermMenu;
+
+/**
+ * 数据权限与菜单关系数据访问操作接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysDataPermMenuMapper extends BaseDaoMapper<SysDataPermMenu> {
+}

+ 13 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysDataPermUserMapper.java

@@ -0,0 +1,13 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysDataPermUser;
+
+/**
+ * 数据权限与用户关系数据访问操作接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysDataPermUserMapper extends BaseDaoMapper<SysDataPermUser> {
+}

+ 33 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysDeptMapper.java

@@ -0,0 +1,33 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysDept;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 部门管理数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysDeptMapper extends BaseDaoMapper<SysDept> {
+
+    /**
+     * 批量插入对象列表。
+     *
+     * @param sysDeptList 新增对象列表。
+     */
+    void insertList(List<SysDept> sysDeptList);
+
+    /**
+     * 获取过滤后的对象列表。
+     *
+     * @param sysDeptFilter 主表过滤对象。
+     * @param orderBy 排序字符串,order by从句的参数。
+     * @return 对象列表。
+     */
+    List<SysDept> getSysDeptList(
+            @Param("sysDeptFilter") SysDept sysDeptFilter, @Param("orderBy") String orderBy);
+}

+ 33 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysDeptPostMapper.java

@@ -0,0 +1,33 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysDeptPost;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 部门岗位数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysDeptPostMapper extends BaseDaoMapper<SysDeptPost> {
+
+    /**
+     * 获取指定部门Id的部门岗位多对多关联数据列表,以及关联的部门和岗位数据。
+     *
+     * @param deptId 部门Id。如果参数为空则返回全部数据。
+     * @return 部门岗位多对多数据列表。
+     */
+    List<Map<String, Object>> getSysDeptPostListWithRelationByDeptId(@Param("deptId") Long deptId);
+
+    /**
+     * 获取指定部门Id的领导部门岗位列表。
+     *
+     * @param deptId 部门Id。
+     * @return 指定部门Id的领导部门岗位列表
+     */
+    List<SysDeptPost> getLeaderDeptPostList(@Param("deptId") Long deptId);
+}

+ 42 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysDeptRelationMapper.java

@@ -0,0 +1,42 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysDeptRelation;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 部门关系树关联关系表访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysDeptRelationMapper extends BaseDaoMapper<SysDeptRelation> {
+
+    /**
+     * 将myDeptId的所有子部门,与其父部门parentDeptId解除关联关系。
+     *
+     * @param parentDeptIds myDeptId的父部门Id列表。
+     * @param myDeptId      当前部门。
+     */
+    void removeBetweenChildrenAndParents(
+            @Param("parentDeptIds") List<Long> parentDeptIds, @Param("myDeptId") Long myDeptId);
+
+    /**
+     * 批量插入部门关联数据。
+     * 由于目前版本(3.4.1)的Mybatis Plus没有提供真正的批量插入,为了保证效率需要自己实现。
+     * 目前我们仅仅给出MySQL和PostgresSQL的insert list实现作为参考,其他数据库需要自行修改。
+     *
+     * @param deptRelationList 部门关联关系数据列表。
+     */
+    void insertList(List<SysDeptRelation> deptRelationList);
+
+    /**
+     * 批量插入当前部门的所有父部门列表,包括自己和自己的关系。
+     *
+     * @param parentDeptId myDeptId的父部门Id。
+     * @param myDeptId     当前部门。
+     */
+    void insertParentList(@Param("parentDeptId") Long parentDeptId, @Param("myDeptId") Long myDeptId);
+}

+ 40 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysMenuMapper.java

@@ -0,0 +1,40 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysMenu;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 菜单数据访问操作接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysMenuMapper extends BaseDaoMapper<SysMenu> {
+
+    /**
+     * 获取登录用户的菜单列表。
+     *
+     * @param userId 登录用户。
+     * @return 菜单列表。
+     */
+    List<SysMenu> getMenuListByUserId(@Param("userId") Long userId);
+
+    /**
+     * 获取指定角色Id集合的菜单列表。
+     *
+     * @param roleIds 角色Id集合。
+     * @return 菜单列表。
+     */
+    List<SysMenu> getMenuListByRoleIds(@Param("roleIds") Set<Long> roleIds);
+
+    /**
+     * 查询包含指定菜单编码的菜单数量,目前仅用于satoken的权限框架。
+     *
+     * @param menuCode 菜单编码。
+     * @return 查询数量
+     */
+    int countMenuCode(@Param("menuCode") String menuCode);
+}

+ 13 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysPermWhitelistMapper.java

@@ -0,0 +1,13 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysPermWhitelist;
+
+/**
+ * 权限资源白名单数据访问操作接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysPermWhitelistMapper extends BaseDaoMapper<SysPermWhitelist> {
+}

+ 52 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysPostMapper.java

@@ -0,0 +1,52 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysPost;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 岗位管理数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysPostMapper extends BaseDaoMapper<SysPost> {
+
+    /**
+     * 获取过滤后的对象列表。
+     *
+     * @param sysPostFilter 主表过滤对象。
+     * @param orderBy       排序字符串,order by从句的参数。
+     * @return 对象列表。
+     */
+    List<SysPost> getSysPostList(
+            @Param("sysPostFilter") SysPost sysPostFilter, @Param("orderBy") String orderBy);
+
+    /**
+     * 获取指定部门的岗位列表。
+     *
+     * @param deptId        部门Id。
+     * @param sysPostFilter 从表过滤对象。
+     * @param orderBy       排序字符串,order by从句的参数。
+     * @return 岗位数据列表。
+     */
+    List<SysPost> getSysPostListByDeptId(
+            @Param("deptId") Long deptId,
+            @Param("sysPostFilter") SysPost sysPostFilter,
+            @Param("orderBy") String orderBy);
+
+    /**
+     * 根据关联主表Id,获取关联从表中没有和主表建立关联关系的数据列表。
+     *
+     * @param deptId        关联主表Id。
+     * @param sysPostFilter 过滤对象。
+     * @param orderBy       排序字符串,order by从句的参数。
+     * @return 与主表没有建立关联的从表数据列表。
+     */
+    List<SysPost> getNotInSysPostListByDeptId(
+            @Param("deptId") Long deptId,
+            @Param("sysPostFilter") SysPost sysPostFilter,
+            @Param("orderBy") String orderBy);
+}

+ 25 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysRoleMapper.java

@@ -0,0 +1,25 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysRole;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 角色数据访问操作接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysRoleMapper extends BaseDaoMapper<SysRole> {
+
+    /**
+     * 获取对象列表,过滤条件中包含like和between条件。
+     *
+     * @param sysRoleFilter 过滤对象。
+     * @param orderBy       排序字符串,order by从句的参数。
+     * @return 对象列表。
+     */
+    List<SysRole> getSysRoleList(@Param("sysRoleFilter") SysRole sysRoleFilter, @Param("orderBy") String orderBy);
+}

+ 13 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysRoleMenuMapper.java

@@ -0,0 +1,13 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysRoleMenu;
+
+/**
+ * 角色与菜单操作关联关系数据访问操作接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysRoleMenuMapper extends BaseDaoMapper<SysRoleMenu> {
+}

+ 188 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysUserMapper.java

@@ -0,0 +1,188 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysUser;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 用户管理数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysUserMapper extends BaseDaoMapper<SysUser> {
+
+    /**
+     * 批量插入对象列表。
+     *
+     * @param sysUserList 新增对象列表。
+     */
+    void insertList(List<SysUser> sysUserList);
+
+    /**
+     * 获取过滤后的对象列表。
+     *
+     * @param sysUserFilter 主表过滤对象。
+     * @param orderBy 排序字符串,order by从句的参数。
+     * @return 对象列表。
+     */
+    List<SysUser> getSysUserList(
+            @Param("sysUserFilter") SysUser sysUserFilter, @Param("orderBy") String orderBy);
+
+    /**
+     * 根据部门Id集合,获取关联的用户列表。
+     *
+     * @param deptIds       关联的部门Id集合。
+     * @param sysUserFilter 用户过滤条件对象。
+     * @param orderBy       order by从句的参数。
+     * @return 和部门Id集合关联的用户列表。
+     */
+    List<SysUser> getSysUserListByDeptIds(
+            @Param("deptIds") Set<Long> deptIds,
+            @Param("sysUserFilter") SysUser sysUserFilter,
+            @Param("orderBy") String orderBy);
+
+    /**
+     * 根据登录名集合,获取关联的用户列表。
+     * @param loginNames    登录名集合。
+     * @param sysUserFilter 用户过滤条件对象。
+     * @param orderBy       order by从句的参数。
+     * @return 和登录名集合关联的用户列表。
+     */
+    List<SysUser> getSysUserListByLoginNames(
+            @Param("loginNames") List<String> loginNames,
+            @Param("sysUserFilter") SysUser sysUserFilter,
+            @Param("orderBy") String orderBy);
+
+    /**
+     * 根据角色Id,获取关联的用户列表。
+     *
+     * @param roleId        关联的角色Id。
+     * @param sysUserFilter 用户过滤条件对象。
+     * @param orderBy       order by从句的参数。
+     * @return 和角色Id关联的用户列表。
+     */
+    List<SysUser> getSysUserListByRoleId(
+            @Param("roleId") Long roleId,
+            @Param("sysUserFilter") SysUser sysUserFilter,
+            @Param("orderBy") String orderBy);
+
+    /**
+     * 根据角色Id集合,获取去重后的用户Id列表。
+     *
+     * @param roleIds       关联的角色Id集合。
+     * @param sysUserFilter 用户过滤条件对象。
+     * @param orderBy       order by从句的参数。
+     * @return 和角色Id集合关联的去重后的用户Id列表。
+     */
+    List<Long> getUserIdListByRoleIds(
+            @Param("roleIds") Set<Long> roleIds,
+            @Param("sysUserFilter") SysUser sysUserFilter,
+            @Param("orderBy") String orderBy);
+
+    /**
+     * 根据角色Id,获取和当前角色Id没有建立多对多关联关系的用户列表。
+     *
+     * @param roleId        关联的角色Id。
+     * @param sysUserFilter 用户过滤条件对象。
+     * @param orderBy order by从句的参数。
+     * @return 和RoleId没有建立关联关系的用户列表。
+     */
+    List<SysUser> getNotInSysUserListByRoleId(
+            @Param("roleId") Long roleId,
+            @Param("sysUserFilter") SysUser sysUserFilter,
+            @Param("orderBy") String orderBy);
+
+    /**
+     * 根据数据权限Id,获取关联的用户列表。
+     *
+     * @param dataPermId    关联的数据权限Id。
+     * @param sysUserFilter 用户过滤条件对象。
+     * @param orderBy order by从句的参数。
+     * @return 和DataPermId关联的用户列表。
+     */
+    List<SysUser> getSysUserListByDataPermId(
+            @Param("dataPermId") Long dataPermId,
+            @Param("sysUserFilter") SysUser sysUserFilter,
+            @Param("orderBy") String orderBy);
+
+    /**
+     * 根据数据权限Id,获取和当前数据权限Id没有建立多对多关联关系的用户列表。
+     *
+     * @param dataPermId    关联的数据权限Id。
+     * @param sysUserFilter 用户过滤条件对象。
+     * @param orderBy order by从句的参数。
+     * @return 和DataPermId没有建立关联关系的用户列表。
+     */
+    List<SysUser> getNotInSysUserListByDataPermId(
+            @Param("dataPermId") Long dataPermId,
+            @Param("sysUserFilter") SysUser sysUserFilter,
+            @Param("orderBy") String orderBy);
+
+    /**
+     * 根据部门岗位Id集合,获取关联的去重后的用户Id列表。
+     *
+     * @param deptPostIds   关联的部门岗位Id集合。
+     * @param sysUserFilter 用户过滤条件对象。
+     * @param orderBy       order by从句的参数。
+     * @return 和部门岗位Id集合关联的去重后的用户Id列表。
+     */
+    List<Long> getUserIdListByDeptPostIds(
+            @Param("deptPostIds") Set<Long> deptPostIds,
+            @Param("sysUserFilter") SysUser sysUserFilter,
+            @Param("orderBy") String orderBy);
+
+    /**
+     * 根据部门岗位Id,获取关联的用户列表。
+     *
+     * @param deptPostId    关联的部门岗位Id。
+     * @param sysUserFilter 用户过滤条件对象。
+     * @param orderBy       order by从句的参数。
+     * @return 和部门岗位Id关联的用户列表。
+     */
+    List<SysUser> getSysUserListByDeptPostId(
+            @Param("deptPostId") Long deptPostId,
+            @Param("sysUserFilter") SysUser sysUserFilter,
+            @Param("orderBy") String orderBy);
+
+    /**
+     * 根据部门岗位Id,获取和当前部门岗位Id没有建立多对多关联关系的用户列表。
+     *
+     * @param deptPostId    关联的部门岗位Id。
+     * @param sysUserFilter 用户过滤条件对象。
+     * @param orderBy       order by从句的参数。
+     * @return 和deptPostId没有建立关联关系的用户列表。
+     */
+    List<SysUser> getNotInSysUserListByDeptPostId(
+            @Param("deptPostId") Long deptPostId,
+            @Param("sysUserFilter") SysUser sysUserFilter,
+            @Param("orderBy") String orderBy);
+
+    /**
+     * 根据岗位Id集合,获取关联的去重后的用户Id列表。
+     *
+     * @param postIds       关联的岗位Id集合。
+     * @param sysUserFilter 用户过滤条件对象。
+     * @param orderBy       order by从句的参数。
+     * @return 和岗位Id集合关联的去重后的用户Id列表。
+     */
+    List<Long> getUserIdListByPostIds(
+            @Param("postIds") Set<Long> postIds,
+            @Param("sysUserFilter") SysUser sysUserFilter,
+            @Param("orderBy") String orderBy);
+
+    /**
+     * 根据岗位Id,获取关联的用户列表。
+     *
+     * @param postId        关联的岗位Id。
+     * @param sysUserFilter 用户过滤条件对象。
+     * @param orderBy       order by从句的参数。
+     * @return 和岗位Id关联的用户列表。
+     */
+    List<SysUser> getSysUserListByPostId(
+            @Param("postId") Long postId,
+            @Param("sysUserFilter") SysUser sysUserFilter,
+            @Param("orderBy") String orderBy);
+}

+ 13 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysUserPostMapper.java

@@ -0,0 +1,13 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysUserPost;
+
+/**
+ * 用户岗位数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysUserPostMapper extends BaseDaoMapper<SysUserPost> {
+}

+ 13 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/SysUserRoleMapper.java

@@ -0,0 +1,13 @@
+package com.serbia.webadmin.upms.dao;
+
+import com.serbia.common.core.base.dao.BaseDaoMapper;
+import com.serbia.webadmin.upms.model.SysUserRole;
+
+/**
+ * 用户与角色关联关系数据访问操作接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+public interface SysUserRoleMapper extends BaseDaoMapper<SysUserRole> {
+}

+ 8 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysDataPermDeptMapper.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysDataPermDeptMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysDataPermDept">
+        <id column="data_perm_id" jdbcType="BIGINT" property="dataPermId"/>
+        <id column="dept_id" jdbcType="BIGINT" property="deptId"/>
+    </resultMap>
+</mapper>

+ 86 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysDataPermMapper.xml

@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysDataPermMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysDataPerm">
+        <id column="data_perm_id" jdbcType="BIGINT" property="dataPermId"/>
+        <result column="data_perm_name" jdbcType="VARCHAR" property="dataPermName"/>
+        <result column="rule_type" jdbcType="INTEGER" property="ruleType"/>
+        <result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
+        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
+        <result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
+        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
+    </resultMap>
+
+    <resultMap id="BaseResultMapEx" type="com.serbia.webadmin.upms.model.SysDataPerm" extends="BaseResultMap">
+        <collection property="dataPermDeptList" column="data_perm_id" javaType="ArrayList"
+                    ofType="com.serbia.webadmin.upms.model.SysDataPermDept" notNullColumn="dept_id"
+                    resultMap="com.serbia.webadmin.upms.dao.SysDataPermDeptMapper.BaseResultMap">
+        </collection>
+        <collection property="dataPermMenuList" column="data_perm_id" javaType="ArrayList"
+                    ofType="com.serbia.webadmin.upms.model.SysDataPermMenu" notNullColumn="menu_id"
+                    resultMap="com.serbia.webadmin.upms.dao.SysDataPermMenuMapper.BaseResultMap">
+        </collection>
+        <collection property="dataPermMobileEntryList" column="data_perm_id" javaType="ArrayList"
+                    ofType="com.serbia.common.mobile.model.MobileEntryDataPerm" notNullColumn="entry_id"
+                    resultMap="com.serbia.common.mobile.dao.MobileEntryDataPermMapper.BaseResultMap">
+        </collection>
+    </resultMap>
+
+    <sql id="filterRef">
+        <if test="sysDataPermFilter != null">
+            <if test="sysDataPermFilter.ruleType != null">
+                AND sys_data_perm.rule_type = #{sysDataPermFilter.ruleType}
+            </if>
+            <if test="sysDataPermFilter.searchString != null and sysDataPermFilter.searchString != ''">
+                <bind name= "safeSearchString" value= "'%' + sysDataPermFilter.searchString + '%'" />
+                AND IFNULL(sys_data_perm.data_perm_name, '') LIKE #{safeSearchString}
+            </if>
+        </if>
+    </sql>
+
+    <select id="getSysDataPermList" resultMap="BaseResultMap" parameterType="com.serbia.webadmin.upms.model.SysDataPerm">
+        SELECT
+            sys_data_perm.*
+        FROM
+            sys_data_perm
+        <where>
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getSysDataPermListByUserId" resultMap="BaseResultMapEx" parameterType="com.serbia.webadmin.upms.model.SysDataPerm">
+        SELECT
+            sys_data_perm.*,
+            sys_data_perm_dept.*,
+            zz_mobile_entry_data_perm.*,
+            sys_data_perm_menu.*
+        FROM
+            sys_data_perm_user
+        INNER JOIN
+            sys_data_perm ON sys_data_perm_user.data_perm_id = sys_data_perm.data_perm_id
+        LEFT JOIN
+            sys_data_perm_dept ON sys_data_perm.data_perm_id = sys_data_perm_dept.data_perm_id
+        LEFT JOIN
+            sys_data_perm_menu ON sys_data_perm.data_perm_id = sys_data_perm_menu.data_perm_id
+        LEFT JOIN
+            zz_mobile_entry_data_perm ON sys_data_perm.data_perm_id = zz_mobile_entry_data_perm.data_perm_id
+        <where>
+            AND sys_data_perm_user.user_id = #{userId}
+        </where>
+    </select>
+
+    <select id="getSysDataPermListByMenuId" resultMap="BaseResultMap" parameterType="com.serbia.webadmin.upms.model.SysDataPerm">
+        SELECT
+            sys_data_perm.*
+        FROM
+            sys_data_perm,
+            sys_data_perm_menu
+        <where>
+            sys_data_perm.data_perm_id = sys_data_perm_menu.data_perm_id
+            AND sys_data_perm_menu.menu_id = #{menuId}
+        </where>
+    </select>
+</mapper>

+ 8 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysDataPermMenuMapper.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysDataPermMenuMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysDataPermMenu">
+        <id column="data_perm_id" jdbcType="BIGINT" property="dataPermId"/>
+        <id column="menu_id" jdbcType="BIGINT" property="menuId"/>
+    </resultMap>
+</mapper>

+ 8 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysDataPermUserMapper.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysDataPermUserMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysDataPermUser">
+        <id column="data_perm_id" jdbcType="BIGINT" property="dataPermId"/>
+        <id column="user_id" jdbcType="BIGINT" property="userId"/>
+    </resultMap>
+</mapper>

+ 70 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysDeptMapper.xml

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysDeptMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysDept">
+        <id column="dept_id" jdbcType="BIGINT" property="deptId"/>
+        <result column="dept_name" jdbcType="VARCHAR" property="deptName"/>
+        <result column="show_order" jdbcType="INTEGER" property="showOrder"/>
+        <result column="parent_id" jdbcType="BIGINT" property="parentId"/>
+        <result column="deleted_flag" jdbcType="INTEGER" property="deletedFlag"/>
+        <result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
+        <result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
+        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
+        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
+    </resultMap>
+
+    <insert id="insertList">
+        INSERT INTO sys_dept
+            (dept_id,
+            dept_name,
+            show_order,
+            parent_id,
+            deleted_flag,
+            create_user_id,
+            update_user_id,
+            create_time,
+            update_time)
+        VALUES
+        <foreach collection="list" index="index" item="item" separator="," >
+            (#{item.deptId},
+            #{item.deptName},
+            #{item.showOrder},
+            #{item.parentId},
+            #{item.deletedFlag},
+            #{item.createUserId},
+            #{item.updateUserId},
+            #{item.createTime},
+            #{item.updateTime})
+        </foreach>
+    </insert>
+
+    <!-- 如果有逻辑删除字段过滤,请写到这里 -->
+    <sql id="filterRef">
+        <!-- 这里必须加上全包名,否则当filterRef被其他Mapper.xml包含引用的时候,就会调用Mapper.xml中的该SQL片段 -->
+        <include refid="com.serbia.webadmin.upms.dao.SysDeptMapper.inputFilterRef"/>
+        AND sys_dept.deleted_flag = ${@com.serbia.common.core.constant.GlobalDeletedFlag@NORMAL}
+    </sql>
+
+    <!-- 这里仅包含调用接口输入的主表过滤条件 -->
+    <sql id="inputFilterRef">
+        <if test="sysDeptFilter != null">
+            <if test="sysDeptFilter.deptName != null and sysDeptFilter.deptName != ''">
+                <bind name = "safeSysDeptDeptName" value = "'%' + sysDeptFilter.deptName + '%'" />
+                AND sys_dept.dept_name LIKE #{safeSysDeptDeptName}
+            </if>
+            <if test="sysDeptFilter.parentId != null">
+                AND sys_dept.parent_id = #{sysDeptFilter.parentId}
+            </if>
+        </if>
+    </sql>
+
+    <select id="getSysDeptList" resultMap="BaseResultMap" parameterType="com.serbia.webadmin.upms.model.SysDept">
+        SELECT * FROM sys_dept
+        <where>
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+</mapper>

+ 46 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysDeptPostMapper.xml

@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysDeptPostMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysDeptPost">
+        <id column="dept_post_id" jdbcType="BIGINT" property="deptPostId"/>
+        <result column="dept_id" jdbcType="BIGINT" property="deptId"/>
+        <result column="post_id" jdbcType="BIGINT" property="postId"/>
+        <result column="post_show_name" jdbcType="VARCHAR" property="postShowName"/>
+    </resultMap>
+
+    <select id="getSysDeptPostListWithRelationByDeptId" resultType="map">
+        SELECT
+            a.dept_post_id deptPostId,
+            a.dept_id deptId,
+            a.post_id postId,
+            a.post_show_name postShowName,
+            b.dept_name deptName,
+            c.post_level postLevel,
+            c.leader_post leaderPost
+        FROM
+            sys_dept_post a,
+            sys_dept b,
+            sys_post c
+        <where>
+            a.dept_id = b.dept_id
+            AND a.post_id = c.post_id
+            <if test="deptId != null">
+                AND a.dept_id = #{deptId}
+            </if>
+        </where>
+    </select>
+
+    <select id="getLeaderDeptPostList" resultMap="BaseResultMap">
+        SELECT
+            a.*
+        FROM
+            sys_dept_post a,
+            sys_post b
+        WHERE
+            a.post_id = b.post_id
+            AND b.leader_post = 1
+            AND a.dept_id = #{deptId}
+        ORDER BY
+            b.post_level
+    </select>
+</mapper>

+ 32 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysDeptRelationMapper.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysDeptRelationMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysDeptRelation">
+        <id column="parent_dept_id" jdbcType="BIGINT" property="parentDeptId"/>
+        <id column="dept_id" jdbcType="BIGINT" property="deptId"/>
+    </resultMap>
+
+    <delete id="removeBetweenChildrenAndParents">
+        DELETE a FROM sys_dept_relation a
+        INNER JOIN sys_dept_relation b ON a.dept_id = b.dept_id
+        WHERE b.parent_dept_id = #{myDeptId} AND a.parent_dept_id IN
+        <foreach collection="parentDeptIds" index="index" item="item" separator="," open="(" close=")">
+            #{item}
+        </foreach>
+    </delete>
+
+    <insert id="insertList">
+        INSERT INTO sys_dept_relation(parent_dept_id, dept_id) VALUES
+        <foreach collection="list" index="index" item="item" separator=",">
+            (#{item.parentDeptId}, #{item.deptId})
+        </foreach>
+    </insert>
+
+    <insert id="insertParentList">
+        INSERT INTO sys_dept_relation(parent_dept_id, dept_id)
+        SELECT t.parent_dept_id, #{myDeptId} FROM sys_dept_relation t
+        WHERE t.dept_id = #{parentDeptId}
+        UNION ALL
+        SELECT #{myDeptId}, #{myDeptId}
+    </insert>
+</mapper>

+ 58 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysMenuMapper.xml

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysMenuMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysMenu">
+        <id column="menu_id" jdbcType="BIGINT" property="menuId"/>
+        <result column="parent_id" jdbcType="BIGINT" property="parentId"/>
+        <result column="menu_name" jdbcType="VARCHAR" property="menuName"/>
+        <result column="menu_type" jdbcType="INTEGER" property="menuType"/>
+        <result column="form_router_name" jdbcType="VARCHAR" property="formRouterName"/>
+        <result column="online_form_id" jdbcType="BIGINT" property="onlineFormId"/>
+        <result column="online_menu_perm_type" jdbcType="INTEGER" property="onlineMenuPermType"/>
+        <result column="report_page_id" jdbcType="BIGINT" property="reportPageId"/>
+        <result column="online_flow_entry_id" jdbcType="BIGINT" property="onlineFlowEntryId"/>
+        <result column="show_order" jdbcType="INTEGER" property="showOrder"/>
+        <result column="icon" jdbcType="VARCHAR" property="icon"/>
+        <result column="extra_data" jdbcType="VARCHAR" property="extraData"/>
+        <result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
+        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
+        <result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
+        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
+    </resultMap>
+
+    <select id="getMenuListByUserId" resultMap="BaseResultMap">
+        SELECT
+            m.*
+        FROM
+            sys_user_role ur,
+            sys_role_menu rm,
+            sys_menu m
+        <where>
+            AND ur.user_id = #{userId}
+            AND ur.role_id = rm.role_id
+            AND rm.menu_id = m.menu_id
+        </where>
+        ORDER BY m.show_order
+    </select>
+
+    <select id="getMenuListByRoleIds" resultMap="BaseResultMap">
+        SELECT
+            m.*
+        FROM
+            sys_role_menu rm,
+            sys_menu m
+        <where>
+            rm.role_id IN
+            <foreach collection="roleIds" item="item" separator="," open="(" close=")">
+                #{item}
+            </foreach>
+            AND rm.menu_id = m.menu_id
+        </where>
+        ORDER BY m.show_order
+    </select>
+
+    <select id="countMenuCode" resultType="java.lang.Integer">
+        <bind name= "safeMenuCode" value= "'%' + menuCode + '%'"/>
+        SELECT COUNT(*) FROM sys_menu WHERE extra_data LIKE #{safeMenuCode}
+    </select>
+</mapper>

+ 9 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysPermWhitelistMapper.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysPermWhitelistMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysPermWhitelist">
+        <id column="perm_url" jdbcType="VARCHAR" property="permUrl"/>
+        <result column="module_name" jdbcType="VARCHAR" property="moduleName"/>
+        <result column="perm_name" jdbcType="VARCHAR" property="permName"/>
+    </resultMap>
+</mapper>

+ 80 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysPostMapper.xml

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysPostMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysPost">
+        <id column="post_id" jdbcType="BIGINT" property="postId"/>
+        <result column="post_name" jdbcType="VARCHAR" property="postName"/>
+        <result column="post_level" jdbcType="INTEGER" property="postLevel"/>
+        <result column="leader_post" jdbcType="BOOLEAN" property="leaderPost"/>
+        <result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
+        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
+        <result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
+        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
+    </resultMap>
+
+    <resultMap id="BaseResultMapWithSysDeptPost" type="com.serbia.webadmin.upms.model.SysPost" extends="BaseResultMap">
+        <association property="sysDeptPost" column="post_id" foreignColumn="post_id"
+                     notNullColumn="post_id" resultMap="com.serbia.webadmin.upms.dao.SysDeptPostMapper.BaseResultMap" />
+    </resultMap>
+
+    <!-- 如果有逻辑删除字段过滤,请写到这里 -->
+    <sql id="filterRef">
+        <!-- 这里必须加上全包名,否则当filterRef被其他Mapper.xml包含引用的时候,就会调用Mapper.xml中的该SQL片段 -->
+        <include refid="com.serbia.webadmin.upms.dao.SysPostMapper.inputFilterRef"/>
+    </sql>
+
+    <!-- 这里仅包含调用接口输入的主表过滤条件 -->
+    <sql id="inputFilterRef">
+        <if test="sysPostFilter != null">
+            <if test="sysPostFilter.postName != null and sysPostFilter.postName != ''">
+                <bind name = "safeSysPostPostName" value = "'%' + sysPostFilter.postName + '%'" />
+                AND sys_post.post_name LIKE #{safeSysPostPostName}
+            </if>
+            <if test="sysPostFilter.leaderPost != null">
+                AND sys_post.leader_post = #{sysPostFilter.leaderPost}
+            </if>
+        </if>
+    </sql>
+
+    <select id="getSysPostList" resultMap="BaseResultMap" parameterType="com.serbia.webadmin.upms.model.SysPost">
+        SELECT * FROM sys_post
+        <where>
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getSysPostListByDeptId" resultMap="BaseResultMapWithSysDeptPost">
+        SELECT
+            sys_post.*,
+            sys_dept_post.*
+        FROM
+            sys_post,
+            sys_dept_post
+        <where>
+            AND sys_dept_post.dept_id = #{deptId}
+            AND sys_dept_post.post_id = sys_post.post_id
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getNotInSysPostListByDeptId" resultMap="BaseResultMap">
+        SELECT
+            sys_post.*
+        FROM
+            sys_post
+        <where>
+            AND NOT EXISTS (SELECT * FROM sys_dept_post
+                WHERE sys_dept_post.dept_id = #{deptId} AND sys_dept_post.post_id = sys_post.post_id)
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+</mapper>

+ 31 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysRoleMapper.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysRoleMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysRole">
+        <id column="role_id" jdbcType="BIGINT" property="roleId"/>
+        <result column="role_name" jdbcType="VARCHAR" property="roleName"/>
+        <result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
+        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
+        <result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
+        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
+    </resultMap>
+
+    <sql id="filterRef">
+        <if test="sysRoleFilter != null">
+            <if test="sysRoleFilter.roleName != null and sysRoleFilter.roleName != ''">
+                <bind name= "safeRoleName" value= "'%' + sysRoleFilter.roleName + '%'"/>
+                AND role_name LIKE #{safeRoleName}
+            </if>
+        </if>
+    </sql>
+
+    <select id="getSysRoleList" resultMap="BaseResultMap" parameterType="com.serbia.webadmin.upms.model.SysRole">
+        SELECT * FROM sys_role
+        <where>
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+</mapper>

+ 8 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysRoleMenuMapper.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysRoleMenuMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysRoleMenu">
+        <id column="role_id" jdbcType="BIGINT" property="roleId"/>
+        <id column="menu_id" jdbcType="BIGINT" property="menuId"/>
+    </resultMap>
+</mapper>

+ 294 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysUserMapper.xml

@@ -0,0 +1,294 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysUserMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysUser">
+        <id column="user_id" jdbcType="BIGINT" property="userId"/>
+        <result column="login_name" jdbcType="VARCHAR" property="loginName"/>
+        <result column="password" jdbcType="VARCHAR" property="password"/>
+        <result column="dept_id" jdbcType="BIGINT" property="deptId"/>
+        <result column="show_name" jdbcType="VARCHAR" property="showName"/>
+        <result column="user_type" jdbcType="INTEGER" property="userType"/>
+        <result column="head_image_url" jdbcType="VARCHAR" property="headImageUrl"/>
+        <result column="user_status" jdbcType="INTEGER" property="userStatus"/>
+        <result column="email" jdbcType="VARCHAR" property="email"/>
+        <result column="mobile" jdbcType="VARCHAR" property="mobile"/>
+        <result column="create_user_id" jdbcType="BIGINT" property="createUserId"/>
+        <result column="update_user_id" jdbcType="BIGINT" property="updateUserId"/>
+        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
+        <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
+        <result column="deleted_flag" jdbcType="INTEGER" property="deletedFlag"/>
+    </resultMap>
+
+    <insert id="insertList">
+        INSERT INTO sys_user
+            (user_id,
+            login_name,
+            password,
+            dept_id,
+            show_name,
+            user_type,
+            head_image_url,
+            user_status,
+            email,
+            mobile,
+            create_user_id,
+            update_user_id,
+            create_time,
+            update_time,
+            deleted_flag)
+        VALUES
+        <foreach collection="list" index="index" item="item" separator="," >
+            (#{item.userId},
+            #{item.loginName},
+            #{item.password},
+            #{item.deptId},
+            #{item.showName},
+            #{item.userType},
+            #{item.headImageUrl},
+            #{item.userStatus},
+            #{item.email},
+            #{item.mobile},
+            #{item.createUserId},
+            #{item.updateUserId},
+            #{item.createTime},
+            #{item.updateTime},
+            #{item.deletedFlag})
+        </foreach>
+    </insert>
+
+    <!-- 如果有逻辑删除字段过滤,请写到这里 -->
+    <sql id="filterRef">
+        <!-- 这里必须加上全包名,否则当filterRef被其他Mapper.xml包含引用的时候,就会调用Mapper.xml中的该SQL片段 -->
+        <include refid="com.serbia.webadmin.upms.dao.SysUserMapper.inputFilterRef"/>
+        AND sys_user.deleted_flag = ${@com.serbia.common.core.constant.GlobalDeletedFlag@NORMAL}
+    </sql>
+
+    <!-- 这里仅包含调用接口输入的主表过滤条件 -->
+    <sql id="inputFilterRef">
+        <if test="sysUserFilter != null">
+            <if test="sysUserFilter.loginName != null and sysUserFilter.loginName != ''">
+                <bind name = "safeSysUserLoginName" value = "'%' + sysUserFilter.loginName + '%'" />
+                AND sys_user.login_name LIKE #{safeSysUserLoginName}
+            </if>
+            <if test="sysUserFilter.deptId != null">
+                AND (EXISTS (SELECT 1 FROM sys_dept_relation WHERE
+                        sys_dept_relation.parent_dept_id = #{sysUserFilter.deptId}
+                        AND sys_user.dept_id = sys_dept_relation.dept_id))
+            </if>
+            <if test="sysUserFilter.showName != null and sysUserFilter.showName != ''">
+                <bind name = "safeSysUserShowName" value = "'%' + sysUserFilter.showName + '%'" />
+                AND sys_user.show_name LIKE #{safeSysUserShowName}
+            </if>
+            <if test="sysUserFilter.userStatus != null">
+                AND sys_user.user_status = #{sysUserFilter.userStatus}
+            </if>
+            <if test="sysUserFilter.createTimeStart != null and sysUserFilter.createTimeStart != ''">
+                AND sys_user.create_time &gt;= #{sysUserFilter.createTimeStart}
+            </if>
+            <if test="sysUserFilter.createTimeEnd != null and sysUserFilter.createTimeEnd != ''">
+                AND sys_user.create_time &lt;= #{sysUserFilter.createTimeEnd}
+            </if>
+        </if>
+    </sql>
+
+    <select id="getSysUserList" resultMap="BaseResultMap" parameterType="com.serbia.webadmin.upms.model.SysUser">
+        SELECT * FROM sys_user
+        <where>
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getSysUserListByDeptIds" resultMap="BaseResultMap">
+        SELECT * FROM sys_user
+        <where>
+            <if test="deptIds != null">
+                AND (EXISTS (SELECT 1 FROM sys_dept_relation WHERE
+                        sys_dept_relation.parent_dept_id IN
+                        <foreach collection="deptIds" item="item" separator="," open="(" close=")">
+                            #{item}
+                        </foreach>
+                        AND sys_user.dept_id = sys_dept_relation.dept_id))
+            </if>
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getSysUserListByLoginNames" resultMap="BaseResultMap">
+        SELECT * FROM sys_user
+        <where>
+            <if test="loginNames != null">
+                AND sys_user.login_name IN
+                <foreach collection="loginNames" item="item" separator="," open="(" close=")">
+                    #{item}
+                </foreach>
+            </if>
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getUserIdListByRoleIds" resultType="java.lang.Long">
+        SELECT
+            DISTINCT sys_user.user_id
+        FROM
+            sys_user_role,
+            sys_user
+        <where>
+            AND sys_user_role.role_id IN
+            <foreach collection="roleIds" item="item" separator="," open="(" close=")">
+                #{item}
+            </foreach>
+            AND sys_user_role.user_id = sys_user.user_id
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getSysUserListByRoleId" resultMap="BaseResultMap">
+        SELECT
+            sys_user.*
+        FROM
+            sys_user_role,
+            sys_user
+        <where>
+            AND sys_user_role.role_id = #{roleId}
+            AND sys_user_role.user_id = sys_user.user_id
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getNotInSysUserListByRoleId" resultMap="BaseResultMap">
+        SELECT * FROM sys_user
+        <where>
+            NOT EXISTS (SELECT * FROM sys_user_role
+                WHERE sys_user_role.role_id = #{roleId} AND sys_user_role.user_id = sys_user.user_id)
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getSysUserListByDataPermId" resultMap="BaseResultMap">
+        SELECT
+            sys_user.*
+        FROM
+            sys_data_perm_user,
+            sys_user
+        <where>
+            AND sys_data_perm_user.data_perm_id = #{dataPermId}
+            AND sys_data_perm_user.user_id = sys_user.user_id
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getNotInSysUserListByDataPermId" resultMap="BaseResultMap">
+        SELECT * FROM sys_user
+        <where>
+            NOT EXISTS (SELECT * FROM sys_data_perm_user
+                WHERE sys_data_perm_user.data_perm_id = #{dataPermId} AND sys_data_perm_user.user_id = sys_user.user_id)
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getUserIdListByDeptPostIds" resultType="java.lang.Long">
+        SELECT
+            DISTINCT sys_user.user_id
+        FROM
+            sys_user_post,
+            sys_user
+        <where>
+            AND sys_user_post.dept_post_id IN
+            <foreach collection="deptPostIds" item="item" separator="," open="(" close=")">
+                #{item}
+            </foreach>
+            AND sys_user_post.user_id = sys_user.user_id
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getSysUserListByDeptPostId" resultMap="BaseResultMap">
+        SELECT
+            sys_user.*
+        FROM
+            sys_user_post,
+            sys_user
+        <where>
+            AND sys_user_post.dept_post_id = #{deptPostId}
+            AND sys_user_post.user_id = sys_user.user_id
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getNotInSysUserListByDeptPostId" resultMap="BaseResultMap">
+        SELECT * FROM sys_user
+        <where>
+            NOT EXISTS (SELECT * FROM sys_user_post
+                WHERE sys_user_post.dept_post_id = #{deptPostId} AND sys_user_post.user_id = sys_user.user_id)
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getUserIdListByPostIds" resultType="java.lang.Long">
+        SELECT
+            DISTINCT sys_user.user_id
+        FROM
+            sys_user_post,
+            sys_user
+        <where>
+            AND sys_user_post.post_id IN
+            <foreach collection="postIds" item="item" separator="," open="(" close=")">
+                #{item}
+            </foreach>
+            AND sys_user_post.user_id = sys_user.user_id
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+
+    <select id="getSysUserListByPostId" resultMap="BaseResultMap">
+        SELECT
+            sys_user.*
+        FROM
+            sys_user_post,
+            sys_user
+        <where>
+            AND sys_user_post.post_id = #{postId}
+            AND sys_user_post.user_id = sys_user.user_id
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+</mapper>

+ 9 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysUserPostMapper.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysUserPostMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysUserPost">
+        <id column="user_id" jdbcType="BIGINT" property="userId"/>
+        <id column="dept_post_id" jdbcType="BIGINT" property="deptPostId"/>
+        <id column="post_id" jdbcType="BIGINT" property="postId"/>
+    </resultMap>
+</mapper>

+ 8 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dao/mapper/SysUserRoleMapper.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.serbia.webadmin.upms.dao.SysUserRoleMapper">
+    <resultMap id="BaseResultMap" type="com.serbia.webadmin.upms.model.SysUserRole">
+        <id column="user_id" jdbcType="BIGINT" property="userId"/>
+        <id column="role_id" jdbcType="BIGINT" property="roleId"/>
+    </resultMap>
+</mapper>

+ 27 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dto/SysDataPermDeptDto.java

@@ -0,0 +1,27 @@
+package com.serbia.webadmin.upms.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 数据权限与部门关联Dto。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Schema(description = "数据权限与部门关联Dto")
+@Data
+public class SysDataPermDeptDto {
+
+    /**
+     * 数据权限Id。
+     */
+    @Schema(description = "数据权限Id", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Long dataPermId;
+
+    /**
+     * 关联部门Id。
+     */
+    @Schema(description = "关联部门Id", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Long deptId;
+}

+ 55 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dto/SysDataPermDto.java

@@ -0,0 +1,55 @@
+package com.serbia.webadmin.upms.dto;
+
+import com.serbia.common.core.validator.UpdateGroup;
+import com.serbia.common.core.validator.ConstDictRef;
+import com.serbia.common.core.constant.DataPermRuleType;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+/**
+ * 数据权限Dto。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Schema(description = "数据权限Dto")
+@Data
+public class SysDataPermDto {
+
+    /**
+     * 数据权限Id。
+     */
+    @Schema(description = "数据权限Id", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据权限Id不能为空!", groups = {UpdateGroup.class})
+    private Long dataPermId;
+
+    /**
+     * 显示名称。
+     */
+    @Schema(description = "显示名称", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据权限名称不能为空!")
+    private String dataPermName;
+
+    /**
+     * 数据权限规则类型(0: 全部可见 1: 只看自己 2: 只看本部门 3: 本部门及子部门 4: 多部门及子部门 5: 自定义部门列表)。
+     */
+    @Schema(description = "数据权限规则类型", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据权限规则类型不能为空!")
+    @ConstDictRef(constDictClass = DataPermRuleType.class)
+    private Integer ruleType;
+
+    /**
+     * 部门Id列表(逗号分隔)。
+     */
+    @Schema(hidden = true)
+    private String deptIdListString;
+
+    /**
+     * 搜索字符串。
+     */
+    @Schema(description = "LIKE 模糊搜索字符串")
+    private String searchString;
+}

+ 27 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dto/SysDataPermMenuDto.java

@@ -0,0 +1,27 @@
+package com.serbia.webadmin.upms.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 数据权限与菜单关联Dto。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Schema(description = "数据权限与菜单关联Dto")
+@Data
+public class SysDataPermMenuDto {
+
+    /**
+     * 数据权限Id。
+     */
+    @Schema(description = "数据权限Id", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Long dataPermId;
+
+    /**
+     * 关联菜单Id。
+     */
+    @Schema(description = "关联菜单Id", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Long menuId;
+}

+ 48 - 0
application-webadmin/src/main/java/com/serbia/webadmin/upms/dto/SysDeptDto.java

@@ -0,0 +1,48 @@
+package com.serbia.webadmin.upms.dto;
+
+import com.serbia.common.core.validator.UpdateGroup;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+/**
+ * 部门管理Dto对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-12-10
+ */
+@Schema(description = "部门管理Dto对象")
+@Data
+public class SysDeptDto {
+
+    /**
+     * 部门Id。
+     */
+    @Schema(description = "部门Id。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,部门Id不能为空!", groups = {UpdateGroup.class})
+    private Long deptId;
+
+    /**
+     * 部门名称。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "部门名称。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,部门名称不能为空!")
+    private String deptName;
+
+    /**
+     * 显示顺序。
+     */
+    @Schema(description = "显示顺序。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,显示顺序不能为空!")
+    private Integer showOrder;
+
+    /**
+     * 父部门Id。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "父部门Id。可支持等于操作符的列表数据过滤。")
+    private Long parentId;
+}

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff