limeng 6 maanden geleden
commit
2db5904941
100 gewijzigde bestanden met toevoegingen van 9184 en 0 verwijderingen
  1. 26 0
      .gitignore
  2. 17 0
      README.md
  3. 101 0
      application-webadmin/pom.xml
  4. 23 0
      application-webadmin/src/main/java/com/tourism/webadmin/WebAdminApplication.java
  5. 255 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/controller/CompanyInfoController.java
  6. 33 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/dao/CompanyInfoMapper.java
  7. 83 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/dao/mapper/CompanyInfoMapper.xml
  8. 89 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/dto/CompanyInfoDto.java
  9. 107 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/model/CompanyInfo.java
  10. 44 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/model/constant/CompanyType.java
  11. 76 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/service/CompanyInfoService.java
  12. 132 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/service/impl/CompanyInfoServiceImpl.java
  13. 244 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/util/FlowIdentityExtHelper.java
  14. 89 0
      application-webadmin/src/main/java/com/tourism/webadmin/app/vo/CompanyInfoVo.java
  15. 38 0
      application-webadmin/src/main/java/com/tourism/webadmin/config/ApplicationConfig.java
  16. 49 0
      application-webadmin/src/main/java/com/tourism/webadmin/config/DataSourceType.java
  17. 58 0
      application-webadmin/src/main/java/com/tourism/webadmin/config/FilterConfig.java
  18. 21 0
      application-webadmin/src/main/java/com/tourism/webadmin/config/InterceptorConfig.java
  19. 88 0
      application-webadmin/src/main/java/com/tourism/webadmin/config/MultiDataSourceConfig.java
  20. 66 0
      application-webadmin/src/main/java/com/tourism/webadmin/config/ThirdPartyAuthConfig.java
  21. 281 0
      application-webadmin/src/main/java/com/tourism/webadmin/interceptor/AuthenticationInterceptor.java
  22. 55 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/bo/SysMenuExtraData.java
  23. 66 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/bo/SysMenuPerm.java
  24. 340 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/controller/GlobalDictController.java
  25. 615 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/controller/LoginController.java
  26. 89 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/controller/LoginUserController.java
  27. 352 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/controller/SysDataPermController.java
  28. 428 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/controller/SysDeptController.java
  29. 231 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/controller/SysMenuController.java
  30. 63 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/controller/SysOperationLogController.java
  31. 183 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/controller/SysPostController.java
  32. 331 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/controller/SysRoleController.java
  33. 378 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/controller/SysUserController.java
  34. 13 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysDataPermDeptMapper.java
  35. 43 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysDataPermMapper.java
  36. 13 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysDataPermMenuMapper.java
  37. 13 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysDataPermUserMapper.java
  38. 33 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysDeptMapper.java
  39. 33 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysDeptPostMapper.java
  40. 42 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysDeptRelationMapper.java
  41. 40 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysMenuMapper.java
  42. 13 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysPermWhitelistMapper.java
  43. 52 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysPostMapper.java
  44. 25 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysRoleMapper.java
  45. 13 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysRoleMenuMapper.java
  46. 188 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysUserMapper.java
  47. 13 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysUserPostMapper.java
  48. 13 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/SysUserRoleMapper.java
  49. 8 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysDataPermDeptMapper.xml
  50. 86 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysDataPermMapper.xml
  51. 8 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysDataPermMenuMapper.xml
  52. 8 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysDataPermUserMapper.xml
  53. 70 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysDeptMapper.xml
  54. 46 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysDeptPostMapper.xml
  55. 32 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysDeptRelationMapper.xml
  56. 58 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysMenuMapper.xml
  57. 9 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysPermWhitelistMapper.xml
  58. 80 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysPostMapper.xml
  59. 31 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysRoleMapper.xml
  60. 8 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysRoleMenuMapper.xml
  61. 294 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysUserMapper.xml
  62. 9 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysUserPostMapper.xml
  63. 8 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dao/mapper/SysUserRoleMapper.xml
  64. 27 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dto/SysDataPermDeptDto.java
  65. 55 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dto/SysDataPermDto.java
  66. 27 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dto/SysDataPermMenuDto.java
  67. 48 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dto/SysDeptDto.java
  68. 47 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dto/SysDeptPostDto.java
  69. 92 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dto/SysMenuDto.java
  70. 47 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dto/SysPostDto.java
  71. 32 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dto/SysRoleDto.java
  72. 110 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/dto/SysUserDto.java
  73. 69 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysDataPerm.java
  74. 29 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysDataPermDept.java
  75. 29 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysDataPermMenu.java
  76. 27 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysDataPermUser.java
  77. 49 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysDept.java
  78. 39 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysDeptPost.java
  79. 31 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysDeptRelation.java
  80. 96 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysMenu.java
  81. 33 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysPermWhitelist.java
  82. 48 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysPost.java
  83. 39 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysRole.java
  84. 27 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysRoleMenu.java
  85. 152 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysUser.java
  86. 33 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysUserPost.java
  87. 27 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysUserRole.java
  88. 54 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/constant/SysMenuType.java
  89. 44 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/constant/SysOnlineMenuPermType.java
  90. 44 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/constant/SysUserStatus.java
  91. 49 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/model/constant/SysUserType.java
  92. 116 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/service/SysDataPermService.java
  93. 170 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/service/SysDeptService.java
  94. 72 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/service/SysMenuService.java
  95. 23 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/service/SysPermWhitelistService.java
  96. 99 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/service/SysPostService.java
  97. 87 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/service/SysRoleService.java
  98. 176 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/service/SysUserService.java
  99. 368 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/service/impl/SysDataPermServiceImpl.java
  100. 316 0
      application-webadmin/src/main/java/com/tourism/webadmin/upms/service/impl/SysDeptServiceImpl.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/

+ 17 - 0
README.md

@@ -0,0 +1,17 @@
+### 服务接口文档
+---
+- Knife4j
+  - 服务启动后,Knife4j的文档入口地址 [http://localhost:8082/doc.html#/plus](http://localhost:8082/doc.html#/plus)
+- Postman
+  - 无需启动服务,即可将当前工程的接口导出成Postman格式。在工程的common/common-tools/模块下,找到ExportApiApp文件,并执行main函数。
+
+### 服务启动环境依赖
+---
+
+执行docker-compose up -d 命令启动下面依赖的服务。
+执行docker-compose down 命令停止下面服务。
+
+- Redis
+  - 版本:4
+  - 端口: 6379
+  - 推荐客户端工具 [AnotherRedisDesktopManager](https://github.com/qishibo/AnotherRedisDesktopManager)

+ 101 - 0
application-webadmin/pom.xml

@@ -0,0 +1,101 @@
+<?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.tourism</groupId>
+        <artifactId>tourism_platform</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.tourism</groupId>
+            <artifactId>common-satoken</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tourism</groupId>
+            <artifactId>common-ext</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tourism</groupId>
+            <artifactId>common-redis</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tourism</groupId>
+            <artifactId>common-mobile</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tourism</groupId>
+            <artifactId>common-report</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tourism</groupId>
+            <artifactId>common-online</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tourism</groupId>
+            <artifactId>common-flow-online</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tourism</groupId>
+            <artifactId>common-log</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tourism</groupId>
+            <artifactId>common-qcloud-cos</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tourism</groupId>
+            <artifactId>common-sequence</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tourism</groupId>
+            <artifactId>common-datafilter</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tourism</groupId>
+            <artifactId>common-swagger</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.tourism</groupId>
+            <artifactId>common-dict</artifactId>
+            <version>1.0.0</version>
+        </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/tourism/webadmin/WebAdminApplication.java

@@ -0,0 +1,23 @@
+package com.tourism.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-09-06
+ */
+@EnableAsync
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
+@ComponentScan("com.tourism")
+public class WebAdminApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(WebAdminApplication.class, args);
+	}
+}

+ 255 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/controller/CompanyInfoController.java

@@ -0,0 +1,255 @@
+package com.tourism.webadmin.app.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.tourism.common.log.annotation.OperationLog;
+import com.tourism.common.log.model.constant.SysOperationLogType;
+import com.github.pagehelper.page.PageMethod;
+import com.tourism.webadmin.app.vo.*;
+import com.tourism.webadmin.app.dto.*;
+import com.tourism.webadmin.app.model.*;
+import com.tourism.webadmin.app.service.*;
+import com.tourism.common.core.object.*;
+import com.tourism.common.core.util.*;
+import com.tourism.common.core.constant.*;
+import com.tourism.common.core.annotation.MyRequestBody;
+import com.tourism.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 java.io.IOException;
+import java.util.*;
+
+/**
+ * 公司信息管理操作控制器类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Tag(name = "公司信息管理管理接口")
+@Slf4j
+@RestController
+@RequestMapping("/admin/app/companyInfo")
+public class CompanyInfoController {
+
+    @Autowired
+    private ApplicationConfig appConfig;
+    @Autowired
+    private CompanyInfoService companyInfoService;
+
+    /**
+     * 新增公司信息管理数据。
+     *
+     * @param companyInfoDto 新增对象。
+     * @return 应答结果对象,包含新增对象主键Id。
+     */
+    @ApiOperationSupport(ignoreParameters = {"companyInfoDto.id", "companyInfoDto.searchString"})
+    @SaCheckPermission("companyInfo.add")
+    @OperationLog(type = SysOperationLogType.ADD)
+    @PostMapping("/add")
+    public ResponseResult<Long> add(@MyRequestBody CompanyInfoDto companyInfoDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(companyInfoDto, false);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        CompanyInfo companyInfo = MyModelUtil.copyTo(companyInfoDto, CompanyInfo.class);
+        companyInfo = companyInfoService.saveNew(companyInfo);
+        return ResponseResult.success(companyInfo.getId());
+    }
+
+    /**
+     * 更新公司信息管理数据。
+     *
+     * @param companyInfoDto 更新对象。
+     * @return 应答结果对象。
+     */
+    @ApiOperationSupport(ignoreParameters = {"companyInfoDto.searchString"})
+    @SaCheckPermission("companyInfo.update")
+    @OperationLog(type = SysOperationLogType.UPDATE)
+    @PostMapping("/update")
+    public ResponseResult<Void> update(@MyRequestBody CompanyInfoDto companyInfoDto) {
+        String errorMessage = MyCommonUtil.getModelValidationError(companyInfoDto, true);
+        if (errorMessage != null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage);
+        }
+        CompanyInfo companyInfo = MyModelUtil.copyTo(companyInfoDto, CompanyInfo.class);
+        CompanyInfo originalCompanyInfo = companyInfoService.getById(companyInfo.getId());
+        if (originalCompanyInfo == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (!companyInfoService.update(companyInfo, originalCompanyInfo)) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        return ResponseResult.success();
+    }
+
+    /**
+     * 删除公司信息管理数据。
+     *
+     * @param id 删除对象主键Id。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("companyInfo.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("companyInfo.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 companyInfoDtoFilter 过滤对象。
+     * @param orderParam 排序参数。
+     * @param pageParam 分页参数。
+     * @return 应答结果对象,包含查询结果集。
+     */
+    @SaCheckPermission("companyInfo.view")
+    @PostMapping("/list")
+    public ResponseResult<MyPageData<CompanyInfoVo>> list(
+            @MyRequestBody CompanyInfoDto companyInfoDtoFilter,
+            @MyRequestBody MyOrderParam orderParam,
+            @MyRequestBody MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount());
+        }
+        CompanyInfo companyInfoFilter = MyModelUtil.copyTo(companyInfoDtoFilter, CompanyInfo.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, CompanyInfo.class);
+        List<CompanyInfo> companyInfoList =
+                companyInfoService.getCompanyInfoListWithRelation(companyInfoFilter, orderBy);
+        return ResponseResult.success(MyPageUtil.makeResponseData(companyInfoList, CompanyInfoVo.class));
+    }
+
+    /**
+     * 导入主表数据列表。
+     *
+     * @param importFile 上传的文件,目前仅仅支持xlsx和xls两种格式。
+     * @return 应答结果对象。
+     */
+    @SaCheckPermission("companyInfo.import")
+    @OperationLog(type = SysOperationLogType.IMPORT)
+    @PostMapping("/import")
+    public ResponseResult<Void> importBatch(
+            @RequestParam Boolean skipHeader,
+            @RequestParam("importFile") MultipartFile importFile) throws IOException {
+        String filename = ImportUtil.saveImportFile(appConfig.getUploadFileBaseDir(), null, importFile);
+        // 这里可以指定需要忽略导入的字段集合。如创建时间、创建人、更新时间、更新人、主键Id和逻辑删除,
+        // 以及一些存在缺省值且无需导入的字段。其中主键字段和逻辑删除字段不需要在这里设置,批量插入逻辑会自动处理的。
+        Set<String> ignoreFieldSet = new HashSet<>();
+        ignoreFieldSet.add("createUserId");
+        ignoreFieldSet.add("createTime");
+        ignoreFieldSet.add("updateTime");
+        List<ImportUtil.ImportHeaderInfo> headerInfoList = ImportUtil.makeHeaderInfoList(CompanyInfo.class, ignoreFieldSet);
+        // 下面是导入时需要注意的地方,如果我们缺省生成的代码,与实际情况存在差异,请手动修改。
+        // 1. 头信息数据字段,我们只是根据当前的主表实体对象生成了缺省数组,开发者可根据实际情况,对headerInfoList进行修改。
+        ImportUtil.ImportHeaderInfo[] headerInfos = headerInfoList.toArray(new ImportUtil.ImportHeaderInfo[]{});
+        // 2. 这里需要根据实际情况决定,导入文件中第一行是否为中文头信息,如果是可以跳过。这里我们默认为true。
+        // 这里根据自己的实际需求,为doImport的最后一个参数,传递需要进行字典转换的字段集合。
+        // 注意,集合中包含需要翻译的Java字段名,如: gradeId。
+        Set<String> translatedDictFieldSet = new HashSet<>();
+        List<CompanyInfo> dataList =
+                ImportUtil.doImport(headerInfos, skipHeader, filename, CompanyInfo.class, translatedDictFieldSet);
+        companyInfoService.saveNewBatch(dataList, -1);
+        return ResponseResult.success();
+    }
+
+    /**
+     * 导出符合过滤条件的公司信息管理列表。
+     *
+     * @param companyInfoDtoFilter 过滤对象。
+     * @param orderParam 排序参数。
+     * @throws IOException 文件读写失败。
+     */
+    @SaCheckPermission("companyInfo.export")
+    @OperationLog(type = SysOperationLogType.EXPORT, saveResponse = false)
+    @PostMapping("/export")
+    public void export(
+            @MyRequestBody CompanyInfoDto companyInfoDtoFilter,
+            @MyRequestBody MyOrderParam orderParam) throws IOException {
+        CompanyInfo companyInfoFilter = MyModelUtil.copyTo(companyInfoDtoFilter, CompanyInfo.class);
+        String orderBy = MyOrderParam.buildOrderBy(orderParam, CompanyInfo.class);
+        List<CompanyInfo> resultList =
+                companyInfoService.getCompanyInfoListWithRelation(companyInfoFilter, orderBy);
+        // 导出文件的标题数组
+        // NOTE: 下面的代码中仅仅导出了主表数据,主表聚合计算数据和主表关联字典的数据。
+        // 一对一从表数据的导出,可根据需要自行添加。如:headerMap.put("slaveFieldName.xxxField", "标题名称")
+        Map<String, String> headerMap = new LinkedHashMap<>(13);
+        headerMap.put("id", "主键id");
+        headerMap.put("companyName", "公司名称");
+        headerMap.put("address", "公司地址");
+        headerMap.put("establishDate", "成立日期");
+        headerMap.put("telephone", "联系电话");
+        headerMap.put("website", "公司网站链接");
+        headerMap.put("companyType", "公司类型");
+        headerMap.put("email", "公司邮箱");
+        headerMap.put("createUserId", "创建用户");
+        headerMap.put("createTime", "创建时间");
+        headerMap.put("updateUserId", "更新用户");
+        headerMap.put("updateTime", "更新时间");
+        headerMap.put("dataState", "删除标记(1: 正常 -1: 已删除)");
+        ExportUtil.doExport(resultList, headerMap, "companyInfo.xlsx");
+    }
+
+    /**
+     * 查看指定公司信息管理对象详情。
+     *
+     * @param id 指定对象主键Id。
+     * @return 应答结果对象,包含对象详情。
+     */
+    @SaCheckPermission("companyInfo.view")
+    @GetMapping("/view")
+    public ResponseResult<CompanyInfoVo> view(@RequestParam Long id) {
+        CompanyInfo companyInfo = companyInfoService.getByIdWithRelation(id, MyRelationParam.full());
+        if (companyInfo == null) {
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST);
+        }
+        CompanyInfoVo companyInfoVo = MyModelUtil.copyTo(companyInfo, CompanyInfoVo.class);
+        return ResponseResult.success(companyInfoVo);
+    }
+
+    private ResponseResult<Void> doDelete(Long id) {
+        String errorMessage;
+        // 验证关联Id的数据合法性
+        CompanyInfo originalCompanyInfo = companyInfoService.getById(id);
+        if (originalCompanyInfo == null) {
+            // NOTE: 修改下面方括号中的话述
+            errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        if (!companyInfoService.remove(id)) {
+            errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!";
+            return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage);
+        }
+        return ResponseResult.success();
+    }
+}

+ 33 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/dao/CompanyInfoMapper.java

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

+ 83 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/dao/mapper/CompanyInfoMapper.xml

@@ -0,0 +1,83 @@
+<?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.tourism.webadmin.app.dao.CompanyInfoMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.webadmin.app.model.CompanyInfo">
+        <id column="id" jdbcType="BIGINT" property="id"/>
+        <result column="company_name" jdbcType="VARCHAR" property="companyName"/>
+        <result column="address" jdbcType="VARCHAR" property="address"/>
+        <result column="establish_date" jdbcType="DATE" property="establishDate"/>
+        <result column="telephone" jdbcType="VARCHAR" property="telephone"/>
+        <result column="website" jdbcType="VARCHAR" property="website"/>
+        <result column="company_type" jdbcType="TINYINT" property="companyType"/>
+        <result column="email" jdbcType="VARCHAR" property="email"/>
+        <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 tour_company_info
+            (id,
+            company_name,
+            address,
+            establish_date,
+            telephone,
+            website,
+            company_type,
+            email,
+            create_user_id,
+            create_time,
+            update_user_Id,
+            update_time,
+            data_state)
+        VALUES
+        <foreach collection="list" index="index" item="item" separator="," >
+            (#{item.id},
+            #{item.companyName},
+            #{item.address},
+            #{item.establishDate},
+            #{item.telephone},
+            #{item.website},
+            #{item.companyType},
+            #{item.email},
+            #{item.createUserId},
+            #{item.createTime},
+            #{item.updateUserId},
+            #{item.updateTime},
+            #{item.dataState})
+        </foreach>
+    </insert>
+
+    <!-- 如果有逻辑删除字段过滤,请写到这里 -->
+    <sql id="filterRef">
+        <!-- 这里必须加上全包名,否则当filterRef被其他Mapper.xml包含引用的时候,就会调用Mapper.xml中的该SQL片段 -->
+        <include refid="com.tourism.webadmin.app.dao.CompanyInfoMapper.inputFilterRef"/>
+        AND tour_company_info.data_state = ${@com.tourism.common.core.constant.GlobalDeletedFlag@NORMAL}
+    </sql>
+
+    <!-- 这里仅包含调用接口输入的主表过滤条件 -->
+    <sql id="inputFilterRef">
+        <if test="companyInfoFilter != null">
+            <if test="companyInfoFilter.companyName != null and companyInfoFilter.companyName != ''">
+                <bind name = "safeCompanyInfoCompanyName" value = "'%' + companyInfoFilter.companyName + '%'" />
+                AND tour_company_info.company_name LIKE #{safeCompanyInfoCompanyName}
+            </if>
+            <if test="companyInfoFilter.searchString != null and companyInfoFilter.searchString != ''">
+                <bind name = "safeCompanyInfoSearchString" value = "'%' + companyInfoFilter.searchString + '%'" />
+                AND IFNULL(tour_company_info.company_name,'') LIKE #{safeCompanyInfoSearchString}
+            </if>
+        </if>
+    </sql>
+
+    <select id="getCompanyInfoList" resultMap="BaseResultMap" parameterType="com.tourism.webadmin.app.model.CompanyInfo">
+        SELECT * FROM tour_company_info
+        <where>
+            <include refid="filterRef"/>
+        </where>
+        <if test="orderBy != null and orderBy != ''">
+            ORDER BY ${orderBy}
+        </if>
+    </select>
+</mapper>

+ 89 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/dto/CompanyInfoDto.java

@@ -0,0 +1,89 @@
+package com.tourism.webadmin.app.dto;
+
+import com.tourism.common.core.validator.UpdateGroup;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+import java.util.Date;
+
+/**
+ * 公司信息管理Dto对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Schema(description = "CompanyInfoDto对象")
+@Data
+public class CompanyInfoDto {
+
+    /**
+     * 主键id。
+     */
+    @Schema(description = "主键id。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,主键id不能为空!", groups = {UpdateGroup.class})
+    private Long id;
+
+    /**
+     * 公司名称。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "公司名称。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,公司名称不能为空!")
+    private String companyName;
+
+    /**
+     * 公司地址。
+     */
+    @Schema(description = "公司地址。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,公司地址不能为空!")
+    private String address;
+
+    /**
+     * 成立日期。
+     */
+    @Schema(description = "成立日期。")
+    private Date establishDate;
+
+    /**
+     * 联系电话。
+     */
+    @Schema(description = "联系电话。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,联系电话不能为空!")
+    private String telephone;
+
+    /**
+     * 公司网站链接。
+     */
+    @Schema(description = "公司网站链接。")
+    private String website;
+
+    /**
+     * 公司类型。
+     */
+    @Schema(description = "公司类型。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,公司类型不能为空!")
+    private Integer companyType;
+
+    /**
+     * 公司邮箱。
+     */
+    @Schema(description = "公司邮箱。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,公司邮箱不能为空!")
+    private String email;
+
+    /**
+     * 更新用户。
+     */
+    @Schema(description = "更新用户。")
+    private Long updateUserId;
+
+    /**
+     * company_name LIKE搜索字符串。
+     * NOTE: 可支持LIKE操作符的列表数据过滤。
+     */
+    @Schema(description = "LIKE模糊搜索字符串")
+    private String searchString;
+}

+ 107 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/model/CompanyInfo.java

@@ -0,0 +1,107 @@
+package com.tourism.webadmin.app.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.tourism.common.core.util.MyCommonUtil;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 公司信息管理实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@TableName(value = "tour_company_info")
+public class CompanyInfo {
+
+    /**
+     * 主键id。
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 公司名称。
+     */
+    @TableField(value = "company_name")
+    private String companyName;
+
+    /**
+     * 公司地址。
+     */
+    @TableField(value = "address")
+    private String address;
+
+    /**
+     * 成立日期。
+     */
+    @TableField(value = "establish_date")
+    private Date establishDate;
+
+    /**
+     * 联系电话。
+     */
+    @TableField(value = "telephone")
+    private String telephone;
+
+    /**
+     * 公司网站链接。
+     */
+    @TableField(value = "website")
+    private String website;
+
+    /**
+     * 公司类型。
+     */
+    @TableField(value = "company_type")
+    private Integer companyType;
+
+    /**
+     * 公司邮箱。
+     */
+    @TableField(value = "email")
+    private String email;
+
+    /**
+     * 创建用户。
+     */
+    @TableField(value = "create_user_id")
+    private Long createUserId;
+
+    /**
+     * 创建时间。
+     */
+    @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;
+
+    /**
+     * company_name LIKE搜索字符串。
+     */
+    @TableField(exist = false)
+    private String searchString;
+
+    public void setSearchString(String searchString) {
+        this.searchString = MyCommonUtil.replaceSqlWildcard(searchString);
+    }
+}

+ 44 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/model/constant/CompanyType.java

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

+ 76 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/service/CompanyInfoService.java

@@ -0,0 +1,76 @@
+package com.tourism.webadmin.app.service;
+
+import com.tourism.webadmin.app.model.*;
+import com.tourism.common.core.base.service.IBaseService;
+
+import java.util.*;
+
+/**
+ * 公司信息管理数据操作服务接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public interface CompanyInfoService extends IBaseService<CompanyInfo, Long> {
+
+    /**
+     * 保存新增对象。
+     *
+     * @param companyInfo 新增对象。
+     * @return 返回新增对象。
+     */
+    CompanyInfo saveNew(CompanyInfo companyInfo);
+
+    /**
+     * 利用数据库的insertList语法,批量插入对象列表。
+     *
+     * @param companyInfoList 新增对象列表。
+     */
+    void saveNewBatch(List<CompanyInfo> companyInfoList);
+
+    /**
+     * 利用数据库的insertList语法,批量插入对象列表。通常适用于更大的插入数据量,如批量导入。
+     *
+     * @param companyInfoList 新增对象列表。
+     * @param batchSize  每批插入的数量。如果该值小于等于0,则使用缺省值10000。
+     */
+    void saveNewBatch(List<CompanyInfo> companyInfoList, int batchSize);
+
+    /**
+     * 更新数据对象。
+     *
+     * @param companyInfo         更新的对象。
+     * @param originalCompanyInfo 原有数据对象。
+     * @return 成功返回true,否则false。
+     */
+    boolean update(CompanyInfo companyInfo, CompanyInfo originalCompanyInfo);
+
+    /**
+     * 删除指定数据。
+     *
+     * @param id 主键Id。
+     * @return 成功返回true,否则false。
+     */
+    boolean remove(Long id);
+
+    /**
+     * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。
+     * 如果需要同时获取关联数据,请移步(getCompanyInfoListWithRelation)方法。
+     *
+     * @param filter  过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<CompanyInfo> getCompanyInfoList(CompanyInfo filter, String orderBy);
+
+    /**
+     * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。
+     * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。
+     * 如果仅仅需要获取主表数据,请移步(getCompanyInfoList),以便获取更好的查询性能。
+     *
+     * @param filter 主表过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<CompanyInfo> getCompanyInfoListWithRelation(CompanyInfo filter, String orderBy);
+}

+ 132 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/service/impl/CompanyInfoServiceImpl.java

@@ -0,0 +1,132 @@
+package com.tourism.webadmin.app.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.tourism.webadmin.app.service.*;
+import com.tourism.webadmin.app.dao.*;
+import com.tourism.webadmin.app.model.*;
+import com.tourism.common.core.base.dao.BaseDaoMapper;
+import com.tourism.common.core.constant.GlobalDeletedFlag;
+import com.tourism.common.core.object.TokenData;
+import com.tourism.common.core.object.MyRelationParam;
+import com.tourism.common.core.base.service.BaseService;
+import com.tourism.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-09-06
+ */
+@Slf4j
+@Service("companyInfoService")
+public class CompanyInfoServiceImpl extends BaseService<CompanyInfo, Long> implements CompanyInfoService {
+
+    @Autowired
+    private IdGeneratorWrapper idGenerator;
+    @Autowired
+    private CompanyInfoMapper companyInfoMapper;
+
+    /**
+     * 返回当前Service的主表Mapper对象。
+     *
+     * @return 主表Mapper对象。
+     */
+    @Override
+    protected BaseDaoMapper<CompanyInfo> mapper() {
+        return companyInfoMapper;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public CompanyInfo saveNew(CompanyInfo companyInfo) {
+        companyInfoMapper.insert(this.buildDefaultValue(companyInfo));
+        return companyInfo;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void saveNewBatch(List<CompanyInfo> companyInfoList) {
+        if (CollUtil.isNotEmpty(companyInfoList)) {
+            companyInfoList.forEach(this::buildDefaultValue);
+            companyInfoMapper.insertList(companyInfoList);
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void saveNewBatch(List<CompanyInfo> companyInfoList, int batchSize) {
+        if (CollUtil.isEmpty(companyInfoList)) {
+            return;
+        }
+        if (batchSize <= 0) {
+            batchSize = 10000;
+        }
+        int start = 0;
+        do {
+            int end = Math.min(companyInfoList.size(), start + batchSize);
+            List<CompanyInfo> subList = companyInfoList.subList(start, end);
+            // 如果数据量过大,同时当前表中存在createTime或updateTime等字段,可以考虑在外部创建一次 new Date(),
+            // 然后传入buildDefaultValue,这样可以减少对象的创建次数,降低GC,提升效率。橙单之所以没有这样生成,是因为
+            // 有些业务场景下需要按照这两个日期字段排序,因此我们只是在这里给出优化建议。
+            subList.forEach(this::buildDefaultValue);
+            companyInfoMapper.insertList(subList);
+            if (end == companyInfoList.size()) {
+                break;
+            }
+            start += batchSize;
+        } while (true);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean update(CompanyInfo companyInfo, CompanyInfo originalCompanyInfo) {
+        companyInfo.setCreateUserId(originalCompanyInfo.getCreateUserId());
+        companyInfo.setCreateTime(originalCompanyInfo.getCreateTime());
+        companyInfo.setUpdateTime(new Date());
+        // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。
+        UpdateWrapper<CompanyInfo> uw = this.createUpdateQueryForNullValue(companyInfo, companyInfo.getId());
+        return companyInfoMapper.update(companyInfo, uw) == 1;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean remove(Long id) {
+        return companyInfoMapper.deleteById(id) == 1;
+    }
+
+    @Override
+    public List<CompanyInfo> getCompanyInfoList(CompanyInfo filter, String orderBy) {
+        return companyInfoMapper.getCompanyInfoList(filter, orderBy);
+    }
+
+    @Override
+    public List<CompanyInfo> getCompanyInfoListWithRelation(CompanyInfo filter, String orderBy) {
+        List<CompanyInfo> resultList = companyInfoMapper.getCompanyInfoList(filter, orderBy);
+        // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。
+        // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。
+        int batchSize = resultList instanceof Page ? 0 : 1000;
+        this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize);
+        return resultList;
+    }
+
+    private CompanyInfo buildDefaultValue(CompanyInfo companyInfo) {
+        if (companyInfo.getId() == null) {
+            companyInfo.setId(idGenerator.nextLongId());
+        }
+        TokenData tokenData = TokenData.takeFromRequest();
+        companyInfo.setCreateUserId(tokenData.getUserId());
+        Date now = new Date();
+        companyInfo.setCreateTime(now);
+        companyInfo.setUpdateTime(now);
+        companyInfo.setDataState(GlobalDeletedFlag.NORMAL);
+        return companyInfo;
+    }
+}

+ 244 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/util/FlowIdentityExtHelper.java

@@ -0,0 +1,244 @@
+package com.tourism.webadmin.app.util;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import com.tourism.common.flow.util.BaseFlowIdentityExtHelper;
+import com.tourism.common.flow.util.FlowCustomExtFactory;
+import com.tourism.common.flow.vo.FlowUserInfoVo;
+import com.tourism.webadmin.upms.model.SysDept;
+import com.tourism.webadmin.upms.model.SysUser;
+import com.tourism.webadmin.upms.model.constant.SysUserStatus;
+import com.tourism.webadmin.upms.model.SysDeptPost;
+import com.tourism.webadmin.upms.service.SysDeptService;
+import com.tourism.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-09-06
+ */
+@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.getSysUserList(filter, null);
+            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<>();
+        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 = null;
+        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);
+        }
+    }
+}

+ 89 - 0
application-webadmin/src/main/java/com/tourism/webadmin/app/vo/CompanyInfoVo.java

@@ -0,0 +1,89 @@
+package com.tourism.webadmin.app.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 公司信息管理VO视图对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Schema(description = "CompanyInfoVO视图对象")
+@Data
+public class CompanyInfoVo {
+
+    /**
+     * 主键id。
+     */
+    @Schema(description = "主键id")
+    private Long id;
+
+    /**
+     * 公司名称。
+     */
+    @Schema(description = "公司名称")
+    private String companyName;
+
+    /**
+     * 公司地址。
+     */
+    @Schema(description = "公司地址")
+    private String address;
+
+    /**
+     * 成立日期。
+     */
+    @Schema(description = "成立日期")
+    private Date establishDate;
+
+    /**
+     * 联系电话。
+     */
+    @Schema(description = "联系电话")
+    private String telephone;
+
+    /**
+     * 公司网站链接。
+     */
+    @Schema(description = "公司网站链接")
+    private String website;
+
+    /**
+     * 公司类型。
+     */
+    @Schema(description = "公司类型")
+    private Integer companyType;
+
+    /**
+     * 公司邮箱。
+     */
+    @Schema(description = "公司邮箱")
+    private String email;
+
+    /**
+     * 创建用户。
+     */
+    @Schema(description = "创建用户")
+    private Long createUserId;
+
+    /**
+     * 创建时间。
+     */
+    @Schema(description = "创建时间")
+    private Date createTime;
+
+    /**
+     * 更新用户。
+     */
+    @Schema(description = "更新用户")
+    private Long updateUserId;
+
+    /**
+     * 更新时间。
+     */
+    @Schema(description = "更新时间")
+    private Date updateTime;
+}

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

@@ -0,0 +1,38 @@
+package com.tourism.webadmin.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 应用程序自定义的程序属性配置文件。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@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/tourism/webadmin/config/DataSourceType.java

@@ -0,0 +1,49 @@
+package com.tourism.webadmin.config;
+
+import com.tourism.common.core.constant.ApplicationConstant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 表示数据源类型的常量对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public final class DataSourceType {
+
+    public static final int MAIN = 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("main", MAIN);
+        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/tourism/webadmin/config/FilterConfig.java

@@ -0,0 +1,58 @@
+package com.tourism.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-09-06
+ */
+@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/tourism/webadmin/config/InterceptorConfig.java

@@ -0,0 +1,21 @@
+package com.tourism.webadmin.config;
+
+import com.tourism.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-09-06
+ */
+@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/tourism/webadmin/config/MultiDataSourceConfig.java

@@ -0,0 +1,88 @@
+package com.tourism.webadmin.config;
+
+import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
+import com.tourism.common.core.config.BaseMultiDataSourceConfig;
+import com.tourism.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-09-06
+ */
+@Configuration
+@EnableTransactionManagement
+@MapperScan(value = {"com.tourism.webadmin.*.dao", "com.tourism.common.*.dao"})
+public class MultiDataSourceConfig extends BaseMultiDataSourceConfig {
+
+    @Bean(initMethod = "init", destroyMethod = "close")
+    @ConfigurationProperties(prefix = "spring.datasource.druid.main")
+    public DataSource mainDataSource() {
+        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.MAIN, mainDataSource());
+        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(mainDataSource());
+        return dynamicDataSource;
+    }
+}

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

@@ -0,0 +1,66 @@
+package com.tourism.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-09-06
+ */
+@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;
+    }
+}

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

@@ -0,0 +1,281 @@
+package com.tourism.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.tourism.common.core.cache.CacheConfig;
+import com.tourism.common.core.constant.ApplicationConstant;
+import com.tourism.common.core.constant.DataPermRuleType;
+import com.tourism.common.core.constant.ErrorCodeEnum;
+import com.tourism.common.core.exception.MyRuntimeException;
+import com.tourism.common.core.object.ResponseResult;
+import com.tourism.common.core.object.TokenData;
+import com.tourism.common.core.util.ApplicationContextHolder;
+import com.tourism.common.core.util.RedisKeyUtil;
+import com.tourism.common.satoken.util.SaTokenUtil;
+import com.tourism.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-09-06
+ */
+@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/tourism/webadmin/upms/bo/SysMenuExtraData.java

@@ -0,0 +1,55 @@
+package com.tourism.webadmin.upms.bo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 菜单扩展数据对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@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/tourism/webadmin/upms/bo/SysMenuPerm.java

@@ -0,0 +1,66 @@
+package com.tourism.webadmin.upms.bo;
+
+import lombok.Data;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 菜单相关的业务对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@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/tourism/webadmin/upms/controller/GlobalDictController.java

@@ -0,0 +1,340 @@
+package com.tourism.webadmin.upms.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.tourism.common.core.annotation.MyRequestBody;
+import com.tourism.common.core.constant.ErrorCodeEnum;
+import com.tourism.common.core.object.MyOrderParam;
+import com.tourism.common.core.object.MyPageData;
+import com.tourism.common.core.object.MyPageParam;
+import com.tourism.common.core.object.ResponseResult;
+import com.tourism.common.core.util.MyCommonUtil;
+import com.tourism.common.core.util.MyModelUtil;
+import com.tourism.common.core.util.MyPageUtil;
+import com.tourism.common.core.validator.UpdateGroup;
+import com.tourism.common.dict.dto.GlobalDictDto;
+import com.tourism.common.dict.dto.GlobalDictItemDto;
+import com.tourism.common.dict.model.GlobalDict;
+import com.tourism.common.dict.model.GlobalDictItem;
+import com.tourism.common.dict.service.GlobalDictItemService;
+import com.tourism.common.dict.service.GlobalDictService;
+import com.tourism.common.dict.util.GlobalDictOperationHelper;
+import com.tourism.common.dict.vo.GlobalDictVo;
+import com.tourism.common.log.annotation.OperationLog;
+import com.tourism.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-09-06
+ */
+@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/tourism/webadmin/upms/controller/LoginController.java

@@ -0,0 +1,615 @@
+package com.tourism.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.tourism.webadmin.config.ApplicationConfig;
+import com.tourism.webadmin.upms.bo.SysMenuExtraData;
+import com.tourism.webadmin.upms.service.*;
+import com.tourism.webadmin.upms.model.*;
+import com.tourism.webadmin.upms.model.constant.SysUserStatus;
+import com.tourism.webadmin.upms.model.constant.SysUserType;
+import com.tourism.webadmin.upms.model.constant.SysMenuType;
+import com.tourism.webadmin.upms.model.constant.SysOnlineMenuPermType;
+import com.tourism.common.flow.online.service.FlowOnlineOperationService;
+import com.tourism.common.online.service.OnlineOperationService;
+import com.tourism.common.report.service.ReportOperationService;
+import com.tourism.common.core.annotation.MyRequestBody;
+import com.tourism.common.core.annotation.DisableDataFilter;
+import com.tourism.common.core.constant.ErrorCodeEnum;
+import com.tourism.common.core.constant.ApplicationConstant;
+import com.tourism.common.core.object.*;
+import com.tourism.common.core.util.*;
+import com.tourism.common.core.upload.*;
+import com.tourism.common.mobile.model.MobileEntry;
+import com.tourism.common.mobile.object.MobileEntryExtraData;
+import com.tourism.common.mobile.service.MobileEntryService;
+import com.tourism.common.redis.cache.SessionCacheHelper;
+import com.tourism.common.log.annotation.OperationLog;
+import com.tourism.common.log.model.constant.SysOperationLogType;
+import com.tourism.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-09-06
+ */
+@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/tourism/webadmin/upms/controller/LoginUserController.java

@@ -0,0 +1,89 @@
+package com.tourism.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.tourism.common.core.annotation.MyRequestBody;
+import com.tourism.common.core.object.*;
+import com.tourism.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-09-06
+ */
+@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/tourism/webadmin/upms/controller/SysDataPermController.java

@@ -0,0 +1,352 @@
+package com.tourism.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.tourism.webadmin.upms.dto.SysDataPermDto;
+import com.tourism.webadmin.upms.dto.SysUserDto;
+import com.tourism.webadmin.upms.vo.SysDataPermVo;
+import com.tourism.webadmin.upms.vo.SysUserVo;
+import com.tourism.webadmin.upms.model.SysDataPerm;
+import com.tourism.webadmin.upms.model.SysUser;
+import com.tourism.webadmin.upms.service.SysDataPermService;
+import com.tourism.webadmin.upms.service.SysUserService;
+import com.tourism.common.core.validator.UpdateGroup;
+import com.tourism.common.core.constant.ErrorCodeEnum;
+import com.tourism.common.core.object.*;
+import com.tourism.common.core.util.*;
+import com.tourism.common.core.annotation.MyRequestBody;
+import com.tourism.common.log.annotation.OperationLog;
+import com.tourism.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-09-06
+ */
+@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/tourism/webadmin/upms/controller/SysDeptController.java

@@ -0,0 +1,428 @@
+package com.tourism.webadmin.upms.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.tourism.common.log.annotation.OperationLog;
+import com.tourism.common.log.model.constant.SysOperationLogType;
+import com.github.pagehelper.page.PageMethod;
+import com.tourism.webadmin.upms.vo.*;
+import com.tourism.webadmin.upms.dto.*;
+import com.tourism.webadmin.upms.model.*;
+import com.tourism.webadmin.upms.service.*;
+import com.tourism.common.core.object.*;
+import com.tourism.common.core.util.*;
+import com.tourism.common.core.constant.*;
+import com.tourism.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-09-06
+ */
+@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/tourism/webadmin/upms/controller/SysMenuController.java

@@ -0,0 +1,231 @@
+package com.tourism.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.tourism.webadmin.upms.dto.SysMenuDto;
+import com.tourism.webadmin.upms.vo.SysMenuVo;
+import com.tourism.webadmin.upms.model.SysMenu;
+import com.tourism.webadmin.upms.model.SysDataPerm;
+import com.tourism.webadmin.upms.model.constant.SysMenuType;
+import com.tourism.webadmin.upms.service.SysMenuService;
+import com.tourism.webadmin.upms.service.SysDataPermService;
+import com.tourism.common.core.constant.ErrorCodeEnum;
+import com.tourism.common.core.object.*;
+import com.tourism.common.core.util.*;
+import com.tourism.common.core.validator.UpdateGroup;
+import com.tourism.common.core.annotation.MyRequestBody;
+import com.tourism.common.log.annotation.OperationLog;
+import com.tourism.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-09-06
+ */
+@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/tourism/webadmin/upms/controller/SysOperationLogController.java

@@ -0,0 +1,63 @@
+package com.tourism.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.tourism.common.core.annotation.MyRequestBody;
+import com.tourism.common.core.object.*;
+import com.tourism.common.core.util.MyModelUtil;
+import com.tourism.common.core.util.MyPageUtil;
+import com.tourism.common.log.model.SysOperationLog;
+import com.tourism.common.log.service.SysOperationLogService;
+import com.tourism.common.log.dto.SysOperationLogDto;
+import com.tourism.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-09-06
+ */
+@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/tourism/webadmin/upms/controller/SysPostController.java

@@ -0,0 +1,183 @@
+package com.tourism.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.tourism.common.core.object.*;
+import com.tourism.common.core.util.*;
+import com.tourism.common.core.constant.*;
+import com.tourism.common.core.annotation.MyRequestBody;
+import com.tourism.common.core.validator.UpdateGroup;
+import com.tourism.webadmin.upms.dto.SysPostDto;
+import com.tourism.webadmin.upms.model.SysPost;
+import com.tourism.webadmin.upms.service.SysPostService;
+import com.tourism.webadmin.upms.vo.SysPostVo;
+import com.tourism.common.log.annotation.OperationLog;
+import com.tourism.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-09-06
+ */
+@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/tourism/webadmin/upms/controller/SysRoleController.java

@@ -0,0 +1,331 @@
+package com.tourism.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.tourism.webadmin.upms.dto.SysRoleDto;
+import com.tourism.webadmin.upms.dto.SysUserDto;
+import com.tourism.webadmin.upms.vo.SysRoleVo;
+import com.tourism.webadmin.upms.vo.SysUserVo;
+import com.tourism.webadmin.upms.model.SysRole;
+import com.tourism.webadmin.upms.model.SysUser;
+import com.tourism.webadmin.upms.model.SysUserRole;
+import com.tourism.webadmin.upms.service.SysRoleService;
+import com.tourism.webadmin.upms.service.SysUserService;
+import com.tourism.common.core.validator.UpdateGroup;
+import com.tourism.common.core.constant.ErrorCodeEnum;
+import com.tourism.common.core.object.*;
+import com.tourism.common.core.util.*;
+import com.tourism.common.core.annotation.MyRequestBody;
+import com.tourism.common.log.annotation.OperationLog;
+import com.tourism.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-09-06
+ */
+@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/tourism/webadmin/upms/controller/SysUserController.java

@@ -0,0 +1,378 @@
+package com.tourism.webadmin.upms.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.alibaba.fastjson.TypeReference;
+import cn.hutool.core.util.ReflectUtil;
+import com.tourism.common.core.upload.BaseUpDownloader;
+import com.tourism.common.core.upload.UpDownloaderFactory;
+import com.tourism.common.core.upload.UploadResponseInfo;
+import com.tourism.common.core.upload.UploadStoreInfo;
+import com.tourism.common.log.annotation.OperationLog;
+import com.tourism.common.log.model.constant.SysOperationLogType;
+import com.github.pagehelper.page.PageMethod;
+import com.tourism.webadmin.upms.vo.*;
+import com.tourism.webadmin.upms.dto.*;
+import com.tourism.webadmin.upms.model.*;
+import com.tourism.webadmin.upms.service.*;
+import com.tourism.common.core.object.*;
+import com.tourism.common.core.util.*;
+import com.tourism.common.core.constant.*;
+import com.tourism.common.core.annotation.MyRequestBody;
+import com.tourism.common.redis.cache.SessionCacheHelper;
+import com.tourism.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-09-06
+ */
+@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/tourism/webadmin/upms/dao/SysDataPermDeptMapper.java

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

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

@@ -0,0 +1,43 @@
+package com.tourism.webadmin.upms.dao;
+
+import com.tourism.common.core.base.dao.BaseDaoMapper;
+import com.tourism.webadmin.upms.model.SysDataPerm;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 数据权限数据访问操作接口。
+ * NOTE: 该对象一定不能被 @EnableDataPerm 注解标注,否则会导致无限递归。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+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/tourism/webadmin/upms/dao/SysDataPermMenuMapper.java

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

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

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

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

@@ -0,0 +1,33 @@
+package com.tourism.webadmin.upms.dao;
+
+import com.tourism.common.core.base.dao.BaseDaoMapper;
+import com.tourism.webadmin.upms.model.SysDept;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 部门管理数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+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/tourism/webadmin/upms/dao/SysDeptPostMapper.java

@@ -0,0 +1,33 @@
+package com.tourism.webadmin.upms.dao;
+
+import com.tourism.common.core.base.dao.BaseDaoMapper;
+import com.tourism.webadmin.upms.model.SysDeptPost;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 部门岗位数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+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/tourism/webadmin/upms/dao/SysDeptRelationMapper.java

@@ -0,0 +1,42 @@
+package com.tourism.webadmin.upms.dao;
+
+import com.tourism.common.core.base.dao.BaseDaoMapper;
+import com.tourism.webadmin.upms.model.SysDeptRelation;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 部门关系树关联关系表访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+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/tourism/webadmin/upms/dao/SysMenuMapper.java

@@ -0,0 +1,40 @@
+package com.tourism.webadmin.upms.dao;
+
+import com.tourism.common.core.base.dao.BaseDaoMapper;
+import com.tourism.webadmin.upms.model.SysMenu;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 菜单数据访问操作接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+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/tourism/webadmin/upms/dao/SysPermWhitelistMapper.java

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

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

@@ -0,0 +1,52 @@
+package com.tourism.webadmin.upms.dao;
+
+import com.tourism.common.core.base.dao.BaseDaoMapper;
+import com.tourism.webadmin.upms.model.SysPost;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 岗位管理数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+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/tourism/webadmin/upms/dao/SysRoleMapper.java

@@ -0,0 +1,25 @@
+package com.tourism.webadmin.upms.dao;
+
+import com.tourism.common.core.base.dao.BaseDaoMapper;
+import com.tourism.webadmin.upms.model.SysRole;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 角色数据访问操作接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+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/tourism/webadmin/upms/dao/SysRoleMenuMapper.java

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

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

@@ -0,0 +1,188 @@
+package com.tourism.webadmin.upms.dao;
+
+import com.tourism.common.core.base.dao.BaseDaoMapper;
+import com.tourism.webadmin.upms.model.SysUser;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.*;
+
+/**
+ * 用户管理数据操作访问接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+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/tourism/webadmin/upms/dao/SysUserPostMapper.java

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

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

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

+ 8 - 0
application-webadmin/src/main/java/com/tourism/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.tourism.webadmin.upms.dao.SysDataPermDeptMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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/tourism/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.tourism.webadmin.upms.dao.SysDataPermMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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.tourism.webadmin.upms.model.SysDataPerm" extends="BaseResultMap">
+        <collection property="dataPermDeptList" column="data_perm_id" javaType="ArrayList"
+                    ofType="com.tourism.webadmin.upms.model.SysDataPermDept" notNullColumn="dept_id"
+                    resultMap="com.tourism.webadmin.upms.dao.SysDataPermDeptMapper.BaseResultMap">
+        </collection>
+        <collection property="dataPermMenuList" column="data_perm_id" javaType="ArrayList"
+                    ofType="com.tourism.webadmin.upms.model.SysDataPermMenu" notNullColumn="menu_id"
+                    resultMap="com.tourism.webadmin.upms.dao.SysDataPermMenuMapper.BaseResultMap">
+        </collection>
+        <collection property="dataPermMobileEntryList" column="data_perm_id" javaType="ArrayList"
+                    ofType="com.tourism.common.mobile.model.MobileEntryDataPerm" notNullColumn="entry_id"
+                    resultMap="com.tourism.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.tourism.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.tourism.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.tourism.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/tourism/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.tourism.webadmin.upms.dao.SysDataPermMenuMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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/tourism/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.tourism.webadmin.upms.dao.SysDataPermUserMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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/tourism/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.tourism.webadmin.upms.dao.SysDeptMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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.tourism.webadmin.upms.dao.SysDeptMapper.inputFilterRef"/>
+        AND sys_dept.deleted_flag = ${@com.tourism.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.tourism.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/tourism/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.tourism.webadmin.upms.dao.SysDeptPostMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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/tourism/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.tourism.webadmin.upms.dao.SysDeptRelationMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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/tourism/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.tourism.webadmin.upms.dao.SysMenuMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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/tourism/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.tourism.webadmin.upms.dao.SysPermWhitelistMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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/tourism/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.tourism.webadmin.upms.dao.SysPostMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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.tourism.webadmin.upms.model.SysPost" extends="BaseResultMap">
+        <association property="sysDeptPost" column="post_id" foreignColumn="post_id"
+                     notNullColumn="post_id" resultMap="com.tourism.webadmin.upms.dao.SysDeptPostMapper.BaseResultMap" />
+    </resultMap>
+
+    <!-- 如果有逻辑删除字段过滤,请写到这里 -->
+    <sql id="filterRef">
+        <!-- 这里必须加上全包名,否则当filterRef被其他Mapper.xml包含引用的时候,就会调用Mapper.xml中的该SQL片段 -->
+        <include refid="com.tourism.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.tourism.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/tourism/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.tourism.webadmin.upms.dao.SysRoleMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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.tourism.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/tourism/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.tourism.webadmin.upms.dao.SysRoleMenuMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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/tourism/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.tourism.webadmin.upms.dao.SysUserMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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.tourism.webadmin.upms.dao.SysUserMapper.inputFilterRef"/>
+        AND sys_user.deleted_flag = ${@com.tourism.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.tourism.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/tourism/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.tourism.webadmin.upms.dao.SysUserPostMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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/tourism/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.tourism.webadmin.upms.dao.SysUserRoleMapper">
+    <resultMap id="BaseResultMap" type="com.tourism.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/tourism/webadmin/upms/dto/SysDataPermDeptDto.java

@@ -0,0 +1,27 @@
+package com.tourism.webadmin.upms.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 数据权限与部门关联Dto。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@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/tourism/webadmin/upms/dto/SysDataPermDto.java

@@ -0,0 +1,55 @@
+package com.tourism.webadmin.upms.dto;
+
+import com.tourism.common.core.validator.UpdateGroup;
+import com.tourism.common.core.validator.ConstDictRef;
+import com.tourism.common.core.constant.DataPermRuleType;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+/**
+ * 数据权限Dto。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@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/tourism/webadmin/upms/dto/SysDataPermMenuDto.java

@@ -0,0 +1,27 @@
+package com.tourism.webadmin.upms.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 数据权限与菜单关联Dto。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@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/tourism/webadmin/upms/dto/SysDeptDto.java

@@ -0,0 +1,48 @@
+package com.tourism.webadmin.upms.dto;
+
+import com.tourism.common.core.validator.UpdateGroup;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+/**
+ * 部门管理Dto对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Schema(description = "SysDeptDto对象")
+@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;
+}

+ 47 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/dto/SysDeptPostDto.java

@@ -0,0 +1,47 @@
+package com.tourism.webadmin.upms.dto;
+
+import com.tourism.common.core.validator.UpdateGroup;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+/**
+ * 部门岗位Dto对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Schema(description = "部门岗位Dto")
+@Data
+public class SysDeptPostDto {
+
+    /**
+     * 部门岗位Id。
+     */
+    @Schema(description = "部门岗位Id", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,部门岗位Id不能为空!", groups = {UpdateGroup.class})
+    private Long deptPostId;
+
+    /**
+     * 部门Id。
+     */
+    @Schema(description = "部门Id", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,部门Id不能为空!", groups = {UpdateGroup.class})
+    private Long deptId;
+
+    /**
+     * 岗位Id。
+     */
+    @Schema(description = "岗位Id", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,岗位Id不能为空!", groups = {UpdateGroup.class})
+    private Long postId;
+
+    /**
+     * 部门岗位显示名称。
+     */
+    @Schema(description = "部门岗位显示名称", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,部门岗位显示名称不能为空!")
+    private String postShowName;
+}

+ 92 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/dto/SysMenuDto.java

@@ -0,0 +1,92 @@
+package com.tourism.webadmin.upms.dto;
+
+import com.tourism.common.core.validator.ConstDictRef;
+import com.tourism.common.core.validator.UpdateGroup;
+import com.tourism.webadmin.upms.model.constant.SysMenuType;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+/**
+ * 菜单Dto。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Schema(description = "菜单Dto")
+@Data
+public class SysMenuDto {
+
+    /**
+     * 菜单Id。
+     */
+    @Schema(description = "菜单Id", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "菜单Id不能为空!", groups = {UpdateGroup.class})
+    private Long menuId;
+
+    /**
+     * 父菜单Id,目录菜单的父菜单为null
+     */
+    @Schema(description = "父菜单Id")
+    private Long parentId;
+
+    /**
+     * 菜单显示名称。
+     */
+    @Schema(description = "菜单显示名称", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "菜单显示名称不能为空!")
+    private String menuName;
+
+    /**
+     * 菜单类型 (0: 目录 1: 菜单 2: 按钮 3: UI片段)。
+     */
+    @Schema(description = "菜单类型", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "菜单类型不能为空!")
+    @ConstDictRef(constDictClass = SysMenuType.class, message = "数据验证失败,菜单类型为无效值!")
+    private Integer menuType;
+
+    /**
+     * 前端表单路由名称,仅用于menu_type为1的菜单类型。
+     */
+    @Schema(description = "前端表单路由名称")
+    private String formRouterName;
+
+    /**
+     * 在线表单主键Id,仅用于在线表单绑定的菜单。
+     */
+    @Schema(description = "在线表单主键Id")
+    private Long onlineFormId;
+
+    /**
+     * 统计页面主键Id,仅用于统计页面绑定的菜单。
+     */
+    @Schema(description = "统计页面主键Id")
+    private Long reportPageId;
+
+    /**
+     * 仅用于在线表单的流程Id。
+     */
+    @Schema(description = "仅用于在线表单的流程Id")
+    private Long onlineFlowEntryId;
+
+    /**
+     * 菜单显示顺序 (值越小,排序越靠前)。
+     */
+    @Schema(description = "菜单显示顺序", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "菜单显示顺序不能为空!")
+    private Integer showOrder;
+
+    /**
+     * 菜单图标。
+     */
+    @Schema(description = "菜单显示图标")
+    private String icon;
+
+    /**
+     * 附加信息。
+     */
+    @Schema(description = "附加信息")
+    private String extraData;
+}

+ 47 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/dto/SysPostDto.java

@@ -0,0 +1,47 @@
+package com.tourism.webadmin.upms.dto;
+
+import com.tourism.common.core.validator.UpdateGroup;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+/**
+ * 岗位Dto对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Schema(description = "岗位Dto")
+@Data
+public class SysPostDto {
+
+    /**
+     * 岗位Id。
+     */
+    @Schema(description = "岗位Id", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,岗位Id不能为空!", groups = {UpdateGroup.class})
+    private Long postId;
+
+    /**
+     * 岗位名称。
+     */
+    @Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,岗位名称不能为空!")
+    private String postName;
+
+    /**
+     * 岗位层级,数值越小级别越高。
+     */
+    @Schema(description = "岗位层级", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,岗位层级不能为空!")
+    private Integer postLevel;
+
+    /**
+     * 是否领导岗位。
+     */
+    @Schema(description = "是否领导岗位", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,领导岗位不能为空!", groups = {UpdateGroup.class})
+    private Boolean leaderPost;
+}

+ 32 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/dto/SysRoleDto.java

@@ -0,0 +1,32 @@
+package com.tourism.webadmin.upms.dto;
+
+import com.tourism.common.core.validator.UpdateGroup;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+/**
+ * 角色Dto。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Schema(description = "角色Dto")
+@Data
+public class SysRoleDto {
+
+    /**
+     * 角色Id。
+     */
+    @Schema(description = "角色Id", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "角色Id不能为空!", groups = {UpdateGroup.class})
+    private Long roleId;
+
+    /**
+     * 角色名称。
+     */
+    @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "角色名称不能为空!")
+    private String roleName;
+}

+ 110 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/dto/SysUserDto.java

@@ -0,0 +1,110 @@
+package com.tourism.webadmin.upms.dto;
+
+import com.tourism.common.core.validator.AddGroup;
+import com.tourism.common.core.validator.UpdateGroup;
+import com.tourism.common.core.validator.ConstDictRef;
+import com.tourism.webadmin.upms.model.constant.SysUserType;
+import com.tourism.webadmin.upms.model.constant.SysUserStatus;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import jakarta.validation.constraints.*;
+
+/**
+ * 用户管理Dto对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Schema(description = "SysUserDto对象")
+@Data
+public class SysUserDto {
+
+    /**
+     * 用户Id。
+     */
+    @Schema(description = "用户Id。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,用户Id不能为空!", groups = {UpdateGroup.class})
+    private Long userId;
+
+    /**
+     * 登录用户名。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "登录用户名。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,登录用户名不能为空!")
+    private String loginName;
+
+    /**
+     * 用户密码。
+     */
+    @Schema(description = "用户密码。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,用户密码不能为空!", groups = {AddGroup.class})
+    private String password;
+
+    /**
+     * 用户部门Id。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "用户部门Id。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,用户部门Id不能为空!")
+    private Long deptId;
+
+    /**
+     * 用户显示名称。
+     * NOTE: 可支持等于操作符的列表数据过滤。
+     */
+    @Schema(description = "用户显示名称。可支持等于操作符的列表数据过滤。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotBlank(message = "数据验证失败,用户显示名称不能为空!")
+    private String showName;
+
+    /**
+     * 用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)。
+     */
+    @Schema(description = "用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)。", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "数据验证失败,用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)不能为空!")
+    @ConstDictRef(constDictClass = SysUserType.class, message = "数据验证失败,用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)为无效值!")
+    private Integer userType;
+
+    /**
+     * 用户头像的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;
+
+    /**
+     * 用户邮箱。
+     */
+    @Schema(description = "用户邮箱。")
+    private String email;
+
+    /**
+     * 用户手机。
+     */
+    @Schema(description = "用户手机。")
+    private String mobile;
+
+    /**
+     * createTime 范围过滤起始值(>=)。
+     * NOTE: 可支持范围操作符的列表数据过滤。
+     */
+    @Schema(description = "createTime 范围过滤起始值(>=)。可支持范围操作符的列表数据过滤。")
+    private String createTimeStart;
+
+    /**
+     * createTime 范围过滤结束值(<=)。
+     * NOTE: 可支持范围操作符的列表数据过滤。
+     */
+    @Schema(description = "createTime 范围过滤结束值(<=)。可支持范围操作符的列表数据过滤。")
+    private String createTimeEnd;
+}

+ 69 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysDataPerm.java

@@ -0,0 +1,69 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.tourism.common.core.util.MyCommonUtil;
+import com.tourism.common.core.annotation.RelationManyToMany;
+import com.tourism.common.core.base.model.BaseModel;
+import com.tourism.common.mobile.model.MobileEntryDataPerm;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.*;
+
+/**
+ * 数据权限实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName(value = "sys_data_perm")
+public class SysDataPerm extends BaseModel {
+
+    /**
+     * 主键Id。
+     */
+    @TableId(value = "data_perm_id")
+    private Long dataPermId;
+
+    /**
+     * 显示名称。
+     */
+    @TableField(value = "data_perm_name")
+    private String dataPermName;
+
+    /**
+     * 数据权限规则类型(0: 全部可见 1: 只看自己 2: 只看本部门 3: 本部门及子部门 4: 多部门及子部门 5: 自定义部门列表)。
+     */
+    @TableField(value = "rule_type")
+    private Integer ruleType;
+
+    @TableField(exist = false)
+    private String deptIdListString;
+
+    @RelationManyToMany(
+            relationMasterIdField = "dataPermId",
+            relationModelClass = SysDataPermDept.class)
+    @TableField(exist = false)
+    private List<SysDataPermDept> dataPermDeptList;
+
+    @RelationManyToMany(
+            relationMasterIdField = "dataPermId",
+            relationModelClass = SysDataPermMenu.class)
+    @TableField(exist = false)
+    private List<SysDataPermMenu> dataPermMenuList;
+
+    @RelationManyToMany(
+            relationMasterIdField = "dataPermId",
+            relationModelClass = MobileEntryDataPerm.class)
+    @TableField(exist = false)
+    private List<MobileEntryDataPerm> dataPermMobileEntryList;
+
+    @TableField(exist = false)
+    private String searchString;
+
+    public void setSearchString(String searchString) {
+        this.searchString = MyCommonUtil.replaceSqlWildcard(searchString);
+    }
+}

+ 29 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysDataPermDept.java

@@ -0,0 +1,29 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.ToString;
+
+/**
+ * 数据权限与部门关联实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@ToString(of = {"deptId"})
+@TableName(value = "sys_data_perm_dept")
+public class SysDataPermDept {
+
+    /**
+     * 数据权限Id。
+     */
+    @TableField(value = "data_perm_id")
+    private Long dataPermId;
+
+    /**
+     * 关联部门Id。
+     */
+    @TableField(value = "dept_id")
+    private Long deptId;
+}

+ 29 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysDataPermMenu.java

@@ -0,0 +1,29 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.ToString;
+
+/**
+ * 数据权限与菜单关联实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@ToString(of = {"menuId"})
+@TableName(value = "sys_data_perm_menu")
+public class SysDataPermMenu {
+
+    /**
+     * 数据权限Id。
+     */
+    @TableField(value = "data_perm_id")
+    private Long dataPermId;
+
+    /**
+     * 关联菜单Id。
+     */
+    @TableField(value = "menu_id")
+    private Long menuId;
+}

+ 27 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysDataPermUser.java

@@ -0,0 +1,27 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+/**
+ * 数据权限与用户关联实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@TableName(value = "sys_data_perm_user")
+public class SysDataPermUser {
+
+    /**
+     * 数据权限Id。
+     */
+    @TableField(value = "data_perm_id")
+    private Long dataPermId;
+
+    /**
+     * 用户Id。
+     */
+    @TableField(value = "user_id")
+    private Long userId;
+}

+ 49 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysDept.java

@@ -0,0 +1,49 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.tourism.common.core.base.model.BaseModel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 部门管理实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName(value = "sys_dept")
+public class SysDept extends BaseModel {
+
+    /**
+     * 部门Id。
+     */
+    @TableId(value = "dept_id")
+    private Long deptId;
+
+    /**
+     * 部门名称。
+     */
+    @TableField(value = "dept_name")
+    private String deptName;
+
+    /**
+     * 显示顺序。
+     */
+    @TableField(value = "show_order")
+    private Integer showOrder;
+
+    /**
+     * 父部门Id。
+     */
+    @TableField(value = "parent_id")
+    private Long parentId;
+
+    /**
+     * 逻辑删除标记字段(1: 正常 -1: 已删除)。
+     */
+    @TableLogic
+    @TableField(value = "deleted_flag")
+    private Integer deletedFlag;
+}

+ 39 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysDeptPost.java

@@ -0,0 +1,39 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+/**
+ * 部门岗位多对多关联实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@TableName(value = "sys_dept_post")
+public class SysDeptPost {
+
+    /**
+     * 部门岗位Id。
+     */
+    @TableId(value = "dept_post_id")
+    private Long deptPostId;
+
+    /**
+     * 部门Id。
+     */
+    @TableField(value = "dept_id")
+    private Long deptId;
+
+    /**
+     * 岗位Id。
+     */
+    @TableField(value = "post_id")
+    private Long postId;
+
+    /**
+     * 部门岗位显示名称。
+     */
+    @TableField(value = "post_show_name")
+    private String postShowName;
+}

+ 31 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysDeptRelation.java

@@ -0,0 +1,31 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 部门关联实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@TableName(value = "sys_dept_relation")
+public class SysDeptRelation {
+
+    /**
+     * 上级部门Id。
+     */
+    @TableField(value = "parent_dept_id")
+    private Long parentDeptId;
+
+    /**
+     * 部门Id。
+     */
+    @TableField(value = "dept_id")
+    private Long deptId;
+}

+ 96 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysMenu.java

@@ -0,0 +1,96 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.tourism.common.core.base.model.BaseModel;
+import com.tourism.webadmin.upms.bo.SysMenuExtraData;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 菜单实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName(value = "sys_menu")
+public class SysMenu extends BaseModel {
+
+    /**
+     * 菜单Id。
+     */
+    @TableId(value = "menu_id")
+    private Long menuId;
+
+    /**
+     * 父菜单Id,目录菜单的父菜单为null。
+     */
+    @TableField(value = "parent_id")
+    private Long parentId;
+
+    /**
+     * 菜单显示名称。
+     */
+    @TableField(value = "menu_name")
+    private String menuName;
+
+    /**
+     * 菜单类型(0: 目录 1: 菜单 2: 按钮 3: UI片段)。
+     */
+    @TableField(value = "menu_type")
+    private Integer menuType;
+
+    /**
+     * 前端表单路由名称,仅用于menu_type为1的菜单类型。
+     */
+    @TableField(value = "form_router_name")
+    private String formRouterName;
+
+    /**
+     * 在线表单主键Id,仅用于在线表单绑定的菜单。
+     */
+    @TableField(value = "online_form_id")
+    private Long onlineFormId;
+
+    /**
+     * 在线表单菜单的权限控制类型,具体值可参考SysOnlineMenuPermType常量对象。
+     */
+    @TableField(value = "online_menu_perm_type")
+    private Integer onlineMenuPermType;
+
+    /**
+     * 统计页面主键Id,仅用于统计页面绑定的菜单。
+     */
+    @TableField(value = "report_page_id")
+    private Long reportPageId;
+
+    /**
+     * 仅用于在线表单的流程Id。
+     */
+    @TableField(value = "online_flow_entry_id")
+    private Long onlineFlowEntryId;
+
+    /**
+     * 菜单显示顺序 (值越小,排序越靠前)。
+     */
+    @TableField(value = "show_order")
+    private Integer showOrder;
+
+    /**
+     * 菜单图标。
+     */
+    private String icon;
+
+    /**
+     * 附加信息。
+     */
+    @TableField(value = "extra_data")
+    private String extraData;
+
+    /**
+     * extraData字段解析后的对象数据。
+     */
+    @TableField(exist = false)
+    private SysMenuExtraData extraObject;
+}

+ 33 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysPermWhitelist.java

@@ -0,0 +1,33 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+/**
+ * 白名单实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@TableName(value = "sys_perm_whitelist")
+public class SysPermWhitelist {
+
+    /**
+     * 权限资源的URL。
+     */
+    @TableId(value = "perm_url")
+    private String permUrl;
+
+    /**
+     * 权限资源所属模块名字(通常是Controller的名字)。
+     */
+    @TableField(value = "module_name")
+    private String moduleName;
+
+    /**
+     * 权限的名称。
+     */
+    @TableField(value = "perm_name")
+    private String permName;
+}

+ 48 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysPost.java

@@ -0,0 +1,48 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.tourism.common.core.base.model.BaseModel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 岗位实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName(value = "sys_post")
+public class SysPost extends BaseModel {
+
+    /**
+     * 岗位Id。
+     */
+    @TableId(value = "post_id")
+    private Long postId;
+
+    /**
+     * 岗位名称。
+     */
+    @TableField(value = "post_name")
+    private String postName;
+
+    /**
+     * 岗位层级,数值越小级别越高。
+     */
+    @TableField(value = "post_level")
+    private Integer postLevel;
+
+    /**
+     * 是否领导岗位。
+     */
+    @TableField(value = "leader_post")
+    private Boolean leaderPost;
+
+    /**
+     * postId 的多对多关联表数据对象。
+     */
+    @TableField(exist = false)
+    private SysDeptPost sysDeptPost;
+}

+ 39 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysRole.java

@@ -0,0 +1,39 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.tourism.common.core.annotation.RelationManyToMany;
+import com.tourism.common.core.base.model.BaseModel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.*;
+
+/**
+ * 角色实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName(value = "sys_role")
+public class SysRole extends BaseModel {
+
+    /**
+     * 角色Id。
+     */
+    @TableId(value = "role_id")
+    private Long roleId;
+
+    /**
+     * 角色名称。
+     */
+    @TableField(value = "role_name")
+    private String roleName;
+
+    @RelationManyToMany(
+            relationMasterIdField = "roleId",
+            relationModelClass = SysRoleMenu.class)
+    @TableField(exist = false)
+    private List<SysRoleMenu> sysRoleMenuList;
+}

+ 27 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysRoleMenu.java

@@ -0,0 +1,27 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+/**
+ * 角色菜单实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@TableName(value = "sys_role_menu")
+public class SysRoleMenu {
+
+    /**
+     * 角色Id。
+     */
+    @TableField(value = "role_id")
+    private Long roleId;
+
+    /**
+     * 菜单Id。
+     */
+    @TableField(value = "menu_id")
+    private Long menuId;
+}

+ 152 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysUser.java

@@ -0,0 +1,152 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.tourism.webadmin.upms.model.constant.SysUserType;
+import com.tourism.webadmin.upms.model.constant.SysUserStatus;
+import com.tourism.common.core.upload.UploadStoreTypeEnum;
+import com.tourism.common.core.annotation.*;
+import com.tourism.common.core.base.model.BaseModel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Map;
+import java.util.List;
+
+/**
+ * 用户管理实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName(value = "sys_user")
+public class SysUser extends BaseModel {
+
+    /**
+     * 用户Id。
+     */
+    @TableId(value = "user_id")
+    private Long userId;
+
+    /**
+     * 登录用户名。
+     */
+    @TableField(value = "login_name")
+    private String loginName;
+
+    /**
+     * 用户密码。
+     */
+    @TableField(value = "password")
+    private String password;
+
+    /**
+     * 用户部门Id。
+     */
+    @TableField(value = "dept_id")
+    private Long deptId;
+
+    /**
+     * 用户显示名称。
+     */
+    @TableField(value = "show_name")
+    private String showName;
+
+    /**
+     * 用户类型(0: 管理员 1: 系统管理用户 2: 系统业务用户)。
+     */
+    @TableField(value = "user_type")
+    private Integer userType;
+
+    /**
+     * 用户头像的Url。
+     */
+    @UploadFlagColumn(storeType = UploadStoreTypeEnum.LOCAL_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: 正常 -1: 已删除)。
+     */
+    @TableLogic
+    @TableField(value = "deleted_flag")
+    private Integer deletedFlag;
+
+    /**
+     * createTime 范围过滤起始值(>=)。
+     */
+    @TableField(exist = false)
+    private String createTimeStart;
+
+    /**
+     * createTime 范围过滤结束值(<=)。
+     */
+    @TableField(exist = false)
+    private String createTimeEnd;
+
+    /**
+     * 多对多用户部门岗位数据集合。
+     */
+    @RelationManyToMany(
+            relationMasterIdField = "userId",
+            relationModelClass = SysUserPost.class)
+    @TableField(exist = false)
+    private List<SysUserPost> sysUserPostList;
+
+    /**
+     * 多对多用户角色数据集合。
+     */
+    @RelationManyToMany(
+            relationMasterIdField = "userId",
+            relationModelClass = SysUserRole.class)
+    @TableField(exist = false)
+    private List<SysUserRole> sysUserRoleList;
+
+    /**
+     * 多对多用户数据权限数据集合。
+     */
+    @RelationManyToMany(
+            relationMasterIdField = "userId",
+            relationModelClass = SysDataPermUser.class)
+    @TableField(exist = false)
+    private List<SysDataPermUser> sysDataPermUserList;
+
+    @RelationDict(
+            masterIdField = "deptId",
+            slaveModelClass = SysDept.class,
+            slaveIdField = "deptId",
+            slaveNameField = "deptName")
+    @TableField(exist = false)
+    private Map<String, Object> deptIdDictMap;
+
+    @RelationConstDict(
+            masterIdField = "userType",
+            constantDictClass = SysUserType.class)
+    @TableField(exist = false)
+    private Map<String, Object> userTypeDictMap;
+
+    @RelationConstDict(
+            masterIdField = "userStatus",
+            constantDictClass = SysUserStatus.class)
+    @TableField(exist = false)
+    private Map<String, Object> userStatusDictMap;
+}

+ 33 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysUserPost.java

@@ -0,0 +1,33 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+/**
+ * 用户岗位多对多关系实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@TableName(value = "sys_user_post")
+public class SysUserPost {
+
+    /**
+     * 用户Id。
+     */
+    @TableField(value = "user_id")
+    private Long userId;
+
+    /**
+     * 部门岗位Id。
+     */
+    @TableField(value = "dept_post_id")
+    private Long deptPostId;
+
+    /**
+     * 岗位Id。
+     */
+    @TableField(value = "post_id")
+    private Long postId;
+}

+ 27 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/SysUserRole.java

@@ -0,0 +1,27 @@
+package com.tourism.webadmin.upms.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+/**
+ * 用户角色实体对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Data
+@TableName(value = "sys_user_role")
+public class SysUserRole {
+
+    /**
+     * 用户Id。
+     */
+    @TableField(value = "user_id")
+    private Long userId;
+
+    /**
+     * 角色Id。
+     */
+    @TableField(value = "role_id")
+    private Long roleId;
+}

+ 54 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/constant/SysMenuType.java

@@ -0,0 +1,54 @@
+package com.tourism.webadmin.upms.model.constant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 菜单类型常量对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public final class SysMenuType {
+
+    /**
+     * 目录菜单。
+     */
+    public static final int TYPE_DIRECTORY = 0;
+    /**
+     * 普通菜单。
+     */
+    public static final int TYPE_MENU = 1;
+    /**
+     * 表单片段类型。
+     */
+    public static final int TYPE_UI_FRAGMENT = 2;
+    /**
+     * 按钮类型。
+     */
+    public static final int TYPE_BUTTON = 3;
+
+    private static final Map<Object, String> DICT_MAP = new HashMap<>(4);
+    static {
+        DICT_MAP.put(TYPE_DIRECTORY, "目录菜单");
+        DICT_MAP.put(TYPE_MENU, "普通菜单");
+        DICT_MAP.put(TYPE_UI_FRAGMENT, "表单片段类型");
+        DICT_MAP.put(TYPE_BUTTON, "按钮类型");
+    }
+
+    /**
+     * 判断参数是否为当前常量字典的合法值。
+     *
+     * @param value 待验证的参数值。
+     * @return 合法返回true,否则false。
+     */
+    public static boolean isValid(Integer value) {
+        return value != null && DICT_MAP.containsKey(value);
+    }
+
+    /**
+     * 私有构造函数,明确标识该常量类的作用。
+     */
+    private SysMenuType() {
+    }
+}

+ 44 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/constant/SysOnlineMenuPermType.java

@@ -0,0 +1,44 @@
+package com.tourism.webadmin.upms.model.constant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 菜单关联在线表单的控制权限类型。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public final class SysOnlineMenuPermType {
+
+    /**
+     * 查看。
+     */
+    public static final int TYPE_VIEW = 0;
+    /**
+     * 编辑。
+     */
+    public static final int TYPE_EDIT = 1;
+
+    private static final Map<Object, String> DICT_MAP = new HashMap<>(4);
+    static {
+        DICT_MAP.put(TYPE_VIEW, "查看");
+        DICT_MAP.put(TYPE_EDIT, "编辑");
+    }
+
+    /**
+     * 判断参数是否为当前常量字典的合法值。
+     *
+     * @param value 待验证的参数值。
+     * @return 合法返回true,否则false。
+     */
+    public static boolean isValid(Integer value) {
+        return value != null && DICT_MAP.containsKey(value);
+    }
+
+    /**
+     * 私有构造函数,明确标识该常量类的作用。
+     */
+    private SysOnlineMenuPermType() {
+    }
+}

+ 44 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/constant/SysUserStatus.java

@@ -0,0 +1,44 @@
+package com.tourism.webadmin.upms.model.constant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 用户状态常量字典对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public final class SysUserStatus {
+
+    /**
+     * 正常状态。
+     */
+    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 SysUserStatus() {
+    }
+}

+ 49 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/model/constant/SysUserType.java

@@ -0,0 +1,49 @@
+package com.tourism.webadmin.upms.model.constant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 用户类型常量字典对象。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public final class SysUserType {
+
+    /**
+     * 管理员。
+     */
+    public static final int TYPE_ADMIN = 0;
+    /**
+     * 系统操作员。
+     */
+    public static final int TYPE_SYSTEM = 1;
+    /**
+     * 普通操作员。
+     */
+    public static final int TYPE_OPERATOR = 2;
+
+    private static final Map<Object, String> DICT_MAP = new HashMap<>(3);
+    static {
+        DICT_MAP.put(TYPE_ADMIN, "管理员");
+        DICT_MAP.put(TYPE_SYSTEM, "系统操作员");
+        DICT_MAP.put(TYPE_OPERATOR, "普通操作员");
+    }
+
+    /**
+     * 判断参数是否为当前常量字典的合法值。
+     *
+     * @param value 待验证的参数值。
+     * @return 合法返回true,否则false。
+     */
+    public static boolean isValid(Integer value) {
+        return value != null && DICT_MAP.containsKey(value);
+    }
+
+    /**
+     * 私有构造函数,明确标识该常量类的作用。
+     */
+    private SysUserType() {
+    }
+}

+ 116 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/service/SysDataPermService.java

@@ -0,0 +1,116 @@
+package com.tourism.webadmin.upms.service;
+
+import com.tourism.common.core.base.service.IBaseService;
+import com.tourism.common.core.object.CallResult;
+import com.tourism.webadmin.upms.model.*;
+
+import java.util.*;
+
+/**
+ * 数据权限数据服务接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public interface SysDataPermService extends IBaseService<SysDataPerm, Long> {
+
+    /**
+     * 保存新增的数据权限对象。
+     *
+     * @param dataPerm   新增的数据权限对象。
+     * @param deptIdSet  关联的部门Id列表。
+     * @param menuIdSet  关联的菜单Id列表。
+     * @param entryIdSet 关联的移动端入口Id列表。
+     * @return 新增后的数据权限对象。
+     */
+    SysDataPerm saveNew(SysDataPerm dataPerm, Set<Long> deptIdSet, Set<Long> menuIdSet, Set<Long> entryIdSet);
+
+    /**
+     * 更新数据权限对象。
+     *
+     * @param dataPerm         更新的数据权限对象。
+     * @param originalDataPerm 原有的数据权限对象。
+     * @param deptIdSet        关联的部门Id列表。
+     * @param menuIdSet        关联的菜单Id列表。
+     * @param entryIdSet       关联的移动端入口Id列表。
+     * @return 更新成功返回true,否则false。
+     */
+    boolean update(SysDataPerm dataPerm, SysDataPerm originalDataPerm, Set<Long> deptIdSet, Set<Long> menuIdSet, Set<Long> entryIdSet);
+
+    /**
+     * 删除指定数据权限。
+     *
+     * @param dataPermId 数据权限主键Id。
+     * @return 删除成功返回true,否则false。
+     */
+    boolean remove(Long dataPermId);
+
+    /**
+     * 获取数据权限列表及其关联数据。
+     *
+     * @param filter  数据权限过滤对象。
+     * @param orderBy 排序参数。
+     * @return 数据权限查询列表。
+     */
+    List<SysDataPerm> getSysDataPermListWithRelation(SysDataPerm filter, String orderBy);
+
+    /**
+     * 将指定用户的指定会话的数据权限集合存入缓存。
+     *
+     * @param sessionId 会话Id。
+     * @param userId    用户主键Id。
+     * @param deptId    用户所属部门主键Id。
+     */
+    void putDataPermCache(String sessionId, Long userId, Long deptId);
+
+    /**
+     * 将指定会话的数据权限集合从缓存中移除。
+     *
+     * @param sessionId 会话Id。
+     */
+    void removeDataPermCache(String sessionId);
+
+    /**
+     * 获取指定用户Id的数据权限列表。并基于menuId和权限规则类型进行了一级分组。
+     *
+     * @param userId 指定的用户Id。
+     * @param deptId 用户所属部门主键Id。
+     * @return 合并优化后的数据权限列表。返回格式为,Map<MenuId, Map<RuleType, DeptIdString>>。
+     */
+    Map<String, Map<Integer, String>> getSysDataPermListByUserId(Long userId, Long deptId);
+
+    /**
+     * 查询与指定菜单关联的数据权限列表。
+     *
+     * @param menuId 菜单Id。
+     * @return 与菜单Id关联的数据权限列表。
+     */
+    List<SysDataPerm> getSysDataPermListByMenuId(Long menuId);
+
+    /**
+     * 添加用户和数据权限之间的多对多关联关系。
+     *
+     * @param dataPermId 数据权限Id。
+     * @param userIdSet  关联的用户Id列表。
+     */
+    void addDataPermUserList(Long dataPermId, Set<Long> userIdSet);
+
+    /**
+     * 移除用户和数据权限之间的多对多关联关系。
+     *
+     * @param dataPermId 数据权限主键Id。
+     * @param userId     用户主键Id。
+     * @return true移除成功,否则false。
+     */
+    boolean removeDataPermUser(Long dataPermId, Long userId);
+
+    /**
+     * 验证数据权限对象关联菜单数据是否都合法。
+     *
+     * @param dataPerm         数据权限关对象。
+     * @param deptIdListString 与数据权限关联的部门Id列表。
+     * @param menuIdListString 与数据权限关联的菜单Id列表。
+     * @return 验证结果。
+     */
+    CallResult verifyRelatedData(SysDataPerm dataPerm, String deptIdListString, String menuIdListString);
+}

+ 170 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/service/SysDeptService.java

@@ -0,0 +1,170 @@
+package com.tourism.webadmin.upms.service;
+
+import com.tourism.webadmin.upms.model.*;
+import com.tourism.common.core.base.service.IBaseService;
+
+import java.util.*;
+
+/**
+ * 部门管理数据操作服务接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public interface SysDeptService extends IBaseService<SysDept, Long> {
+
+    /**
+     * 保存新增的部门对象。
+     *
+     * @param sysDept       新增的部门对象。
+     * @param parentSysDept 上级部门对象。
+     * @return 新增后的部门对象。
+     */
+    SysDept saveNew(SysDept sysDept, SysDept parentSysDept);
+
+    /**
+     * 更新部门对象。
+     *
+     * @param sysDept         更新的部门对象。
+     * @param originalSysDept 原有的部门对象。
+     * @return 更新成功返回true,否则false。
+     */
+    boolean update(SysDept sysDept, SysDept originalSysDept);
+
+    /**
+     * 删除指定数据。
+     *
+     * @param deptId 主键Id。
+     * @return 成功返回true,否则false。
+     */
+    boolean remove(Long deptId);
+
+    /**
+     * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。
+     * 如果需要同时获取关联数据,请移步(getSysDeptListWithRelation)方法。
+     *
+     * @param filter  过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SysDept> getSysDeptList(SysDept filter, String orderBy);
+
+    /**
+     * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。
+     * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。
+     * 如果仅仅需要获取主表数据,请移步(getSysDeptList),以便获取更好的查询性能。
+     *
+     * @param filter 主表过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SysDept> getSysDeptListWithRelation(SysDept filter, String orderBy);
+
+    /**
+     * 判断指定对象是否包含下级对象。
+     *
+     * @param deptId 主键Id。
+     * @return 存在返回true,否则false。
+     */
+    boolean hasChildren(Long deptId);
+
+    /**
+     * 判断指定部门Id是否包含用户对象。
+     *
+     * @param deptId 部门主键Id。
+     * @return 存在返回true,否则false。
+     */
+    boolean hasChildrenUser(Long deptId);
+
+    /**
+     * 批量添加多对多关联关系。
+     *
+     * @param sysDeptPostList 多对多关联表对象集合。
+     * @param deptId 主表Id。
+     */
+    void addSysDeptPostList(List<SysDeptPost> sysDeptPostList, Long deptId);
+
+    /**
+     * 更新中间表数据。
+     *
+     * @param sysDeptPost 中间表对象。
+     * @return 更新成功与否。
+     */
+    boolean updateSysDeptPost(SysDeptPost sysDeptPost);
+
+    /**
+     * 移除单条多对多关系。
+     *
+     * @param deptId 主表Id。
+     * @param postId 从表Id。
+     * @return 成功返回true,否则false。
+     */
+    boolean removeSysDeptPost(Long deptId, Long postId);
+
+    /**
+     * 获取中间表数据。
+     *
+     * @param deptId 主表Id。
+     * @param postId 从表Id。
+     * @return 中间表对象。
+     */
+    SysDeptPost getSysDeptPost(Long deptId, Long postId);
+
+    /**
+     * 根据部门岗位Id获取部门岗位关联对象。
+     *
+     * @param deptPostId 部门岗位Id。
+     * @return 部门岗位对象。
+     */
+    SysDeptPost getSysDeptPost(Long deptPostId);
+
+    /**
+     * 获取指定部门Id的部门岗位多对多关联数据列表,以及关联的部门和岗位数据。
+     *
+     * @param deptId 部门Id。如果参数为空则返回全部数据。
+     * @return 部门岗位多对多数据列表。
+     */
+    List<Map<String, Object>> getSysDeptPostListWithRelationByDeptId(Long deptId);
+
+    /**
+     * 获取指定部门Id和岗位Id集合的部门岗位多对多关联数据列表。
+     *
+     * @param deptId    部门Id。
+     * @param postIdSet 指定的岗位Id集合。
+     * @return 部门岗位多对多数据列表。
+     */
+    List<SysDeptPost> getSysDeptPostList(Long deptId, Set<Long> postIdSet);
+
+    /**
+     * 获取与指定部门Id同级部门和岗位Id集合的部门岗位多对多关联数据列表。
+     *
+     * @param deptId    部门Id。
+     * @param postIdSet 指定的岗位Id集合。
+     * @return 部门岗位多对多数据列表。
+     */
+    List<SysDeptPost> getSiblingSysDeptPostList(Long deptId, Set<Long> postIdSet);
+
+    /**
+     * 根据部门Id获取该部门领导岗位的部门岗位Id集合。
+     *
+     * @param deptId 部门Id。
+     * @return 部门领导岗位的部门岗位Id集合。
+     */
+    List<Long> getLeaderDeptPostIdList(Long deptId);
+
+    /**
+     * 根据部门Id获取上级部门领导岗位的部门岗位Id集合。
+     *
+     * @param deptId 部门Id。
+     * @return 上级部门领导岗位的部门岗位Id集合。
+     */
+    List<Long> getUpLeaderDeptPostIdList(Long deptId);
+
+    /**
+     * 根据父主键Id列表,获取当前部门Id及其所有下级部门Id列表。
+     *
+     * @param parentIds 父主键Id列表。
+     * @return 获取当前部门Id及其所有下级部门Id列表。
+     */
+    List<Long> getAllChildDeptIdByParentIds(List<Long> parentIds);
+}

+ 72 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/service/SysMenuService.java

@@ -0,0 +1,72 @@
+package com.tourism.webadmin.upms.service;
+
+import com.tourism.common.core.base.service.IBaseService;
+import com.tourism.webadmin.upms.model.SysMenu;
+
+import java.util.*;
+
+/**
+ * 菜单数据服务接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public interface SysMenuService extends IBaseService<SysMenu, Long> {
+
+    /**
+     * 保存新增的菜单对象。
+     *
+     * @param sysMenu       新增的菜单对象。
+     * @return 新增后的菜单对象。
+     */
+    SysMenu saveNew(SysMenu sysMenu);
+
+    /**
+     * 更新菜单对象。
+     *
+     * @param sysMenu         更新的菜单对象。
+     * @param originalSysMenu 原有的菜单对象。
+     * @return 更新成功返回true,否则false。
+     */
+    boolean update(SysMenu sysMenu, SysMenu originalSysMenu);
+
+    /**
+     * 删除指定的菜单。
+     *
+     * @param menu 菜单对象。
+     * @return 删除成功返回true,否则false。
+     */
+    boolean remove(SysMenu menu);
+
+    /**
+     * 获取指定用户Id的菜单列表,已去重。
+     *
+     * @param userId 用户主键Id。
+     * @return 用户关联的菜单列表。
+     */
+    Collection<SysMenu> getMenuListByUserId(Long userId);
+    
+    /**
+     * 根据角色Id集合获取菜单对象列表。
+     *
+     * @param roleIds 逗号分隔的角色Id集合。
+     * @return 菜单对象列表。
+     */
+    Collection<SysMenu> getMenuListByRoleIds(String roleIds);
+
+    /**
+     * 判断当前菜单是否存在子菜单。
+     *
+     * @param menuId 菜单主键Id。
+     * @return 存在返回true,否则false。
+     */
+    boolean hasChildren(Long menuId);
+
+    /**
+     * 获取指定类型的所有在线表单的菜单。
+     *
+     * @param menuType 菜单类型,NULL则返回全部类型。
+     * @return 在线表单关联的菜单列表。
+     */
+    List<SysMenu> getAllOnlineMenuList(Integer menuType);
+}

+ 23 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/service/SysPermWhitelistService.java

@@ -0,0 +1,23 @@
+package com.tourism.webadmin.upms.service;
+
+import com.tourism.common.core.base.service.IBaseService;
+import com.tourism.webadmin.upms.model.SysPermWhitelist;
+
+import java.util.List;
+
+/**
+ * 权限资源白名单数据服务接口。
+ * 白名单中的权限资源,可以不受权限控制,任何用户皆可访问,一般用于常用的字典数据列表接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public interface SysPermWhitelistService extends IBaseService<SysPermWhitelist, String> {
+
+    /**
+     * 获取白名单权限资源的列表。
+     *
+     * @return 白名单权限资源地址列表。
+     */
+    List<String> getWhitelistPermList();
+}

+ 99 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/service/SysPostService.java

@@ -0,0 +1,99 @@
+package com.tourism.webadmin.upms.service;
+
+import com.tourism.common.core.base.service.IBaseService;
+import com.tourism.webadmin.upms.model.SysPost;
+import com.tourism.webadmin.upms.model.SysUserPost;
+
+import java.util.*;
+
+/**
+ * 岗位管理数据操作服务接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public interface SysPostService extends IBaseService<SysPost, Long> {
+
+    /**
+     * 保存新增对象。
+     *
+     * @param sysPost 新增对象。
+     * @return 返回新增对象。
+     */
+    SysPost saveNew(SysPost sysPost);
+
+    /**
+     * 更新数据对象。
+     *
+     * @param sysPost         更新的对象。
+     * @param originalSysPost 原有数据对象。
+     * @return 成功返回true,否则false。
+     */
+    boolean update(SysPost sysPost, SysPost originalSysPost);
+
+    /**
+     * 删除指定数据。
+     *
+     * @param postId 主键Id。
+     * @return 成功返回true,否则false。
+     */
+    boolean remove(Long postId);
+
+    /**
+     * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。
+     * 如果需要同时获取关联数据,请移步(getSysPostListWithRelation)方法。
+     *
+     * @param filter  过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SysPost> getSysPostList(SysPost filter, String orderBy);
+
+    /**
+     * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。
+     * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。
+     * 如果仅仅需要获取主表数据,请移步(getSysPostList),以便获取更好的查询性能。
+     *
+     * @param filter  主表过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SysPost> getSysPostListWithRelation(SysPost filter, String orderBy);
+
+    /**
+     * 在多对多关系中,当前Service的数据表为从表,返回不与指定主表主键Id存在对多对关系的列表。
+     *
+     * @param deptId  主表主键Id。
+     * @param filter  从表的过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SysPost> getNotInSysPostListByDeptId(Long deptId, SysPost filter, String orderBy);
+
+    /**
+     * 获取指定部门的岗位列表。
+     *
+     * @param deptId  部门Id。
+     * @param filter  从表的过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SysPost> getSysPostListByDeptId(Long deptId, SysPost filter, String orderBy);
+
+    /**
+     * 获取指定用户的用户岗位多对多关联数据列表。
+     *
+     * @param userId 用户Id。
+     * @return 用户岗位多对多关联数据列表。
+     */
+    List<SysUserPost> getSysUserPostListByUserId(Long userId);
+
+    /**
+     * 判断指定的部门岗位Id集合是否都属于指定的部门Id。
+     *
+     * @param deptPostIdSet 部门岗位Id集合。
+     * @param deptId        部门Id。
+     * @return 全部是返回true,否则false。
+     */
+    boolean existAllPrimaryKeys(Set<Long> deptPostIdSet, Long deptId);
+}

+ 87 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/service/SysRoleService.java

@@ -0,0 +1,87 @@
+package com.tourism.webadmin.upms.service;
+
+import com.tourism.common.core.base.service.IBaseService;
+import com.tourism.common.core.object.CallResult;
+import com.tourism.webadmin.upms.model.SysRole;
+import com.tourism.webadmin.upms.model.SysUserRole;
+
+import java.util.*;
+
+/**
+ * 角色数据服务接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public interface SysRoleService extends IBaseService<SysRole, Long> {
+
+    /**
+     * 保存新增的角色对象。
+     *
+     * @param role      新增的角色对象。
+     * @param menuIdSet 菜单Id列表。
+     * @return 新增后的角色对象。
+     */
+    SysRole saveNew(SysRole role, Set<Long> menuIdSet);
+
+    /**
+     * 更新角色对象。
+     *
+     * @param role         更新的角色对象。
+     * @param originalRole 原有的角色对象。
+     * @param menuIdSet    菜单Id列表。
+     * @return 更新成功返回true,否则false。
+     */
+    boolean update(SysRole role, SysRole originalRole, Set<Long> menuIdSet);
+
+    /**
+     * 删除指定角色。
+     *
+     * @param roleId 角色主键Id。
+     * @return 删除成功返回true,否则false。
+     */
+    boolean remove(Long roleId);
+
+    /**
+     * 获取角色列表。
+     *
+     * @param filter  角色过滤对象。
+     * @param orderBy 排序参数。
+     * @return 角色列表。
+     */
+    List<SysRole> getSysRoleList(SysRole filter, String orderBy);
+
+    /**
+     * 获取用户的用户角色对象列表。
+     *
+     * @param userId 用户Id。
+     * @return 用户角色对象列表。
+     */
+    List<SysUserRole> getSysUserRoleListByUserId(Long userId);
+
+    /**
+     * 批量新增用户角色关联。
+     *
+     * @param userRoleList 用户角色关系数据列表。
+     */
+    void addUserRoleList(List<SysUserRole> userRoleList);
+
+    /**
+     * 移除指定用户和指定角色的关联关系。
+     *
+     * @param roleId 角色主键Id。
+     * @param userId 用户主键Id。
+     * @return 移除成功返回true,否则false。
+     */
+    boolean removeUserRole(Long roleId, Long userId);
+
+    /**
+     * 验证角色对象关联的数据是否都合法。
+     *
+     * @param sysRole          当前操作的对象。
+     * @param originalSysRole  原有对象。
+     * @param menuIdListString 逗号分隔的menuId列表。
+     * @return 验证结果。
+     */
+    CallResult verifyRelatedData(SysRole sysRole, SysRole originalSysRole, String menuIdListString);
+}

+ 176 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/service/SysUserService.java

@@ -0,0 +1,176 @@
+package com.tourism.webadmin.upms.service;
+
+import com.tourism.webadmin.upms.model.*;
+import com.tourism.common.core.object.CallResult;
+import com.tourism.common.core.base.service.IBaseService;
+
+import java.util.*;
+
+/**
+ * 用户管理数据操作服务接口。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+public interface SysUserService extends IBaseService<SysUser, Long> {
+
+    /**
+     * 获取指定登录名的用户对象。
+     *
+     * @param loginName 指定登录用户名。
+     * @return 用户对象。
+     */
+    SysUser getSysUserByLoginName(String loginName);
+
+    /**
+     * 保存新增的用户对象。
+     *
+     * @param user          新增的用户对象。
+     * @param roleIdSet     用户角色Id集合。
+     * @param deptPostIdSet 部门岗位Id集合。
+     * @param dataPermIdSet 数据权限Id集合。
+     * @return 新增后的用户对象。
+     */
+    SysUser saveNew(SysUser user, Set<Long> roleIdSet, Set<Long> deptPostIdSet, Set<Long> dataPermIdSet);
+
+    /**
+     * 更新用户对象。
+     *
+     * @param user          更新的用户对象。
+     * @param originalUser  原有的用户对象。
+     * @param roleIdSet     用户角色Id列表。
+     * @param deptPostIdSet 部门岗位Id集合。
+     * @param dataPermIdSet 数据权限Id集合。
+     * @return 更新成功返回true,否则false。
+     */
+    boolean update(SysUser user, SysUser originalUser, Set<Long> roleIdSet, Set<Long> deptPostIdSet, Set<Long> dataPermIdSet);
+
+    /**
+     * 修改用户密码。
+     * @param userId  用户主键Id。
+     * @param newPass 新密码。
+     * @return 成功返回true,否则false。
+     */
+    boolean changePassword(Long userId, String newPass);
+
+    /**
+     * 修改用户头像。
+     *
+     * @param userId  用户主键Id。
+     * @param newHeadImage 新的头像信息。
+     * @return 成功返回true,否则false。
+     */
+    boolean changeHeadImage(Long userId, String newHeadImage);
+
+    /**
+     * 删除指定数据。
+     *
+     * @param userId 主键Id。
+     * @return 成功返回true,否则false。
+     */
+    boolean remove(Long userId);
+
+    /**
+     * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。
+     * 如果需要同时获取关联数据,请移步(getSysUserListWithRelation)方法。
+     *
+     * @param filter  过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SysUser> getSysUserList(SysUser filter, String orderBy);
+
+    /**
+     * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。
+     * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。
+     * 如果仅仅需要获取主表数据,请移步(getSysUserList),以便获取更好的查询性能。
+     *
+     * @param filter 主表过滤对象。
+     * @param orderBy 排序参数。
+     * @return 查询结果集。
+     */
+    List<SysUser> getSysUserListWithRelation(SysUser filter, String orderBy);
+
+    /**
+     * 获取指定角色的用户列表。
+     *
+     * @param roleId  角色主键Id。
+     * @param filter  用户过滤对象。
+     * @param orderBy 排序参数。
+     * @return 用户列表。
+     */
+    List<SysUser> getSysUserListByRoleId(Long roleId, SysUser filter, String orderBy);
+
+    /**
+     * 获取不属于指定角色的用户列表。
+     *
+     * @param roleId  角色主键Id。
+     * @param filter  用户过滤对象。
+     * @param orderBy 排序参数。
+     * @return 用户列表。
+     */
+    List<SysUser> getNotInSysUserListByRoleId(Long roleId, SysUser filter, String orderBy);
+
+    /**
+     * 获取指定数据权限的用户列表。
+     *
+     * @param dataPermId 数据权限主键Id。
+     * @param filter     用户过滤对象。
+     * @param orderBy    排序参数。
+     * @return 用户列表。
+     */
+    List<SysUser> getSysUserListByDataPermId(Long dataPermId, SysUser filter, String orderBy);
+
+    /**
+     * 获取不属于指定数据权限的用户列表。
+     *
+     * @param dataPermId 数据权限主键Id。
+     * @param filter     用户过滤对象。
+     * @param orderBy    排序参数。
+     * @return 用户列表。
+     */
+    List<SysUser> getNotInSysUserListByDataPermId(Long dataPermId, SysUser filter, String orderBy);
+
+    /**
+     * 获取指定部门岗位的用户列表。
+     *
+     * @param deptPostId 部门岗位主键Id。
+     * @param filter     用户过滤对象。
+     * @param orderBy    排序参数。
+     * @return 用户列表。
+     */
+    List<SysUser> getSysUserListByDeptPostId(Long deptPostId, SysUser filter, String orderBy);
+
+    /**
+     * 获取不属于指定部门岗位的用户列表。
+     *
+     * @param deptPostId 部门岗位主键Id。
+     * @param filter     用户过滤对象。
+     * @param orderBy    排序参数。
+     * @return 用户列表。
+     */
+    List<SysUser> getNotInSysUserListByDeptPostId(Long deptPostId, SysUser filter, String orderBy);
+
+    /**
+     * 获取指定岗位的用户列表。
+     *
+     * @param postId  岗位主键Id。
+     * @param filter  用户过滤对象。
+     * @param orderBy 排序参数。
+     * @return 用户列表。
+     */
+    List<SysUser> getSysUserListByPostId(Long postId, SysUser filter, String orderBy);
+
+    /**
+     * 验证用户对象关联的数据是否都合法。
+     *
+     * @param sysUser         当前操作的对象。
+     * @param originalSysUser 原有对象。
+     * @param roleIds         逗号分隔的角色Id列表字符串。
+     * @param deptPostIds     逗号分隔的部门岗位Id列表字符串。
+     * @param dataPermIds     逗号分隔的数据权限Id列表字符串。
+     * @return 验证结果。
+     */
+    CallResult verifyRelatedData(
+            SysUser sysUser, SysUser originalSysUser, String roleIds, String deptPostIds, String dataPermIds);
+}

+ 368 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/service/impl/SysDataPermServiceImpl.java

@@ -0,0 +1,368 @@
+package com.tourism.webadmin.upms.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.tourism.common.mobile.dao.MobileEntryDataPermMapper;
+import com.tourism.common.mobile.model.MobileEntryDataPerm;
+import com.tourism.common.sequence.wrapper.IdGeneratorWrapper;
+import com.tourism.common.core.constant.DataPermRuleType;
+import com.tourism.common.core.base.service.BaseService;
+import com.tourism.common.core.base.dao.BaseDaoMapper;
+import com.tourism.common.core.object.MyRelationParam;
+import com.tourism.common.core.object.CallResult;
+import com.tourism.common.core.util.MyModelUtil;
+import com.tourism.common.core.util.RedisKeyUtil;
+import com.tourism.common.core.constant.ApplicationConstant;
+import com.tourism.webadmin.config.ApplicationConfig;
+import com.tourism.webadmin.upms.dao.SysDataPermDeptMapper;
+import com.tourism.webadmin.upms.dao.SysDataPermMapper;
+import com.tourism.webadmin.upms.dao.SysDataPermUserMapper;
+import com.tourism.webadmin.upms.dao.SysDataPermMenuMapper;
+import com.tourism.webadmin.upms.model.*;
+import com.tourism.webadmin.upms.service.SysDataPermService;
+import com.tourism.webadmin.upms.service.SysDeptService;
+import com.tourism.webadmin.upms.service.SysMenuService;
+import com.tourism.webadmin.upms.service.SysUserService;
+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.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * 数据权限数据服务类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Slf4j
+@Service("sysDataPermService")
+public class SysDataPermServiceImpl extends BaseService<SysDataPerm, Long> implements SysDataPermService {
+
+    @Autowired
+    private SysDataPermMapper sysDataPermMapper;
+    @Autowired
+    private SysDataPermDeptMapper sysDataPermDeptMapper;
+    @Autowired
+    private SysDataPermUserMapper sysDataPermUserMapper;
+    @Autowired
+    private SysDataPermMenuMapper sysDataPermMenuMapper;
+    @Autowired
+    private SysUserService sysUserService;
+    @Autowired
+    private SysDeptService sysDeptService;
+    @Autowired
+    private SysMenuService sysMenuService;
+    @Autowired
+    private RedissonClient redissonClient;
+    @Autowired
+    private ApplicationConfig applicationConfig;
+    @Autowired
+    private IdGeneratorWrapper idGenerator;
+    @Autowired
+    private MobileEntryDataPermMapper mobileEntryDataPermMapper;
+
+    /**
+     * 返回主对象的Mapper对象。
+     *
+     * @return 主对象的Mapper对象。
+     */
+    @Override
+    protected BaseDaoMapper<SysDataPerm> mapper() {
+        return sysDataPermMapper;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public SysDataPerm saveNew(SysDataPerm dataPerm, Set<Long> deptIdSet, Set<Long> menuIdSet, Set<Long> entryIdSet) {
+        dataPerm.setDataPermId(idGenerator.nextLongId());
+        MyModelUtil.fillCommonsForInsert(dataPerm);
+        sysDataPermMapper.insert(dataPerm);
+        this.insertRelationData(dataPerm, deptIdSet, menuIdSet, entryIdSet);
+        return dataPerm;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean update(
+            SysDataPerm dataPerm, SysDataPerm originalDataPerm, Set<Long> deptIdSet, Set<Long> menuIdSet, Set<Long> entryIdSet) {
+        MyModelUtil.fillCommonsForUpdate(dataPerm, originalDataPerm);
+        UpdateWrapper<SysDataPerm> uw = this.createUpdateQueryForNullValue(dataPerm, dataPerm.getDataPermId());
+        if (sysDataPermMapper.update(dataPerm, uw) != 1) {
+            return false;
+        }
+        SysDataPermDept dataPermDept = new SysDataPermDept();
+        dataPermDept.setDataPermId(dataPerm.getDataPermId());
+        sysDataPermDeptMapper.delete(new QueryWrapper<>(dataPermDept));
+        SysDataPermMenu dataPermMenu = new SysDataPermMenu();
+        dataPermMenu.setDataPermId(dataPerm.getDataPermId());
+        sysDataPermMenuMapper.delete(new QueryWrapper<>(dataPermMenu));
+        MobileEntryDataPerm mobileEntryDataPerm = new MobileEntryDataPerm();
+        mobileEntryDataPerm.setDataPermId(dataPerm.getDataPermId());
+        mobileEntryDataPermMapper.delete(new QueryWrapper<>(mobileEntryDataPerm));
+        this.insertRelationData(dataPerm, deptIdSet, menuIdSet, entryIdSet);
+        return true;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean remove(Long dataPermId) {
+        if (sysDataPermMapper.deleteById(dataPermId) != 1) {
+            return false;
+        }
+        SysDataPermDept dataPermDept = new SysDataPermDept();
+        dataPermDept.setDataPermId(dataPermId);
+        sysDataPermDeptMapper.delete(new QueryWrapper<>(dataPermDept));
+        SysDataPermUser dataPermUser = new SysDataPermUser();
+        dataPermUser.setDataPermId(dataPermId);
+        sysDataPermUserMapper.delete(new QueryWrapper<>(dataPermUser));
+        SysDataPermMenu dataPermMenu = new SysDataPermMenu();
+        dataPermMenu.setDataPermId(dataPermId);
+        sysDataPermMenuMapper.delete(new QueryWrapper<>(dataPermMenu));
+        MobileEntryDataPerm mobileEntryDataPerm = new MobileEntryDataPerm();
+        mobileEntryDataPerm.setDataPermId(dataPermId);
+        mobileEntryDataPermMapper.delete(new QueryWrapper<>(mobileEntryDataPerm));
+        return true;
+    }
+
+    @Override
+    public List<SysDataPerm> getSysDataPermListWithRelation(SysDataPerm filter, String orderBy) {
+        List<SysDataPerm> resultList = sysDataPermMapper.getSysDataPermList(filter, orderBy);
+        buildRelationForDataList(resultList, MyRelationParam.full(), CollUtil.newHashSet("dataPermDeptList"));
+        return resultList;
+    }
+
+    @Override
+    public void putDataPermCache(String sessionId, Long userId, Long deptId) {
+        Map<String, Map<Integer, String>> menuDataPermMap = getSysDataPermListByUserId(userId, deptId);
+        if (menuDataPermMap.size() > 0) {
+            String dataPermSessionKey = RedisKeyUtil.makeSessionDataPermIdKey(sessionId);
+            RBucket<String> bucket = redissonClient.getBucket(dataPermSessionKey);
+            bucket.set(JSON.toJSONString(menuDataPermMap),
+                    applicationConfig.getSessionExpiredSeconds(), TimeUnit.SECONDS);
+        }
+    }
+
+    @Override
+    public void removeDataPermCache(String sessionId) {
+        String sessionPermKey = RedisKeyUtil.makeSessionDataPermIdKey(sessionId);
+        redissonClient.getBucket(sessionPermKey).deleteAsync();
+    }
+
+    @Override
+    public Map<String, Map<Integer, String>> getSysDataPermListByUserId(Long userId, Long deptId) {
+        List<SysDataPerm> dataPermList = sysDataPermMapper.getSysDataPermListByUserId(userId);
+        dataPermList.forEach(dataPerm -> {
+            if (CollUtil.isNotEmpty(dataPerm.getDataPermDeptList())) {
+                Set<Long> deptIdSet = dataPerm.getDataPermDeptList().stream()
+                        .map(SysDataPermDept::getDeptId).collect(Collectors.toSet());
+                dataPerm.setDeptIdListString(StrUtil.join(",", deptIdSet));
+            }
+        });
+        Map<String, List<SysDataPerm>> menuIdMap = new HashMap<>(4);
+        for (SysDataPerm dataPerm : dataPermList) {
+            if (CollUtil.isNotEmpty(dataPerm.getDataPermMenuList())
+                    || CollUtil.isNotEmpty(dataPerm.getDataPermMobileEntryList())) {
+                for (SysDataPermMenu dataPermMenu : dataPerm.getDataPermMenuList()) {
+                    menuIdMap.computeIfAbsent(
+                            dataPermMenu.getMenuId().toString(), k -> new LinkedList<>()).add(dataPerm);
+                }
+                for (MobileEntryDataPerm dataPermMobileEntry : dataPerm.getDataPermMobileEntryList()) {
+                    menuIdMap.computeIfAbsent(
+                            dataPermMobileEntry.getEntryId().toString(), k -> new LinkedList<>()).add(dataPerm);
+                }
+            } else {
+                menuIdMap.computeIfAbsent(
+                        ApplicationConstant.DATA_PERM_ALL_MENU_ID, k -> new LinkedList<>()).add(dataPerm);
+            }
+        }
+        Map<String, Map<Integer, String>> menuResultMap = new HashMap<>(menuIdMap.size());
+        for (Map.Entry<String, List<SysDataPerm>> entry : menuIdMap.entrySet()) {
+            Map<Integer, String> resultMap = this.mergeAndOptimizeDataPermRule(entry.getValue(), deptId);
+            menuResultMap.put(entry.getKey(), resultMap);
+        }
+        return menuResultMap;
+    }
+
+    @Override
+    public List<SysDataPerm> getSysDataPermListByMenuId(Long menuId) {
+        return sysDataPermMapper.getSysDataPermListByMenuId(menuId);
+    }
+
+    private Map<Integer, String> mergeAndOptimizeDataPermRule(List<SysDataPerm> dataPermList, Long deptId) {
+        // 为了更方便进行后续的合并优化处理,这里再基于菜单Id和规则类型进行分组。ruleMap的key是规则类型。
+        Map<Integer, List<SysDataPerm>> ruleMap =
+                dataPermList.stream().collect(Collectors.groupingBy(SysDataPerm::getRuleType));
+        Map<Integer, String> resultMap = new HashMap<>(ruleMap.size());
+        // 如有有ALL存在,就可以直接退出了,没有必要在处理后续的规则了。
+        if (ruleMap.containsKey(DataPermRuleType.TYPE_ALL)) {
+            resultMap.put(DataPermRuleType.TYPE_ALL, "null");
+            return resultMap;
+        }
+        // 这里优先合并最复杂的多部门及子部门场景。
+        String deptIds = processMultiDeptAndChildren(ruleMap, deptId);
+        if (deptIds != null) {
+            resultMap.put(DataPermRuleType.TYPE_MULTI_DEPT_AND_CHILD_DEPT, deptIds);
+        }
+        // 合并当前部门及子部门的优化
+        if (ruleMap.get(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT) != null) {
+            // 需要与仅仅当前部门规则进行合并。
+            ruleMap.remove(DataPermRuleType.TYPE_DEPT_ONLY);
+            resultMap.put(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT, "null");
+        }
+        // 合并自定义部门了。
+        deptIds = processMultiDept(ruleMap, deptId);
+        if (deptIds != null) {
+            resultMap.put(DataPermRuleType.TYPE_CUSTOM_DEPT_LIST, deptIds);
+        }
+        // 最后处理当前部门和当前用户。
+        if (ruleMap.get(DataPermRuleType.TYPE_DEPT_ONLY) != null) {
+            resultMap.put(DataPermRuleType.TYPE_DEPT_ONLY, "null");
+        }
+        if (ruleMap.get(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT_USERS) != null) {
+            // 合并当前部门用户和当前用户
+            ruleMap.remove(DataPermRuleType.TYPE_USER_ONLY);
+            ruleMap.remove(DataPermRuleType.TYPE_DEPT_USERS);
+            SysUser filter = new SysUser();
+            filter.setDeptId(deptId);
+            List<SysUser> userList = sysUserService.getSysUserList(filter, null);
+            Set<Long> userIdSet = userList.stream().map(SysUser::getUserId).collect(Collectors.toSet());
+            resultMap.put(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT_USERS, CollUtil.join(userIdSet, ","));
+        }
+        if (ruleMap.get(DataPermRuleType.TYPE_DEPT_USERS) != null) {
+            SysUser filter = new SysUser();
+            filter.setDeptId(deptId);
+            List<SysUser> userList = sysUserService.getListByFilter(filter);
+            Set<Long> userIdSet = userList.stream().map(SysUser::getUserId).collect(Collectors.toSet());
+            // 合并仅当前用户
+            ruleMap.remove(DataPermRuleType.TYPE_USER_ONLY);
+            resultMap.put(DataPermRuleType.TYPE_DEPT_USERS, CollUtil.join(userIdSet, ","));
+        }
+        if (ruleMap.get(DataPermRuleType.TYPE_USER_ONLY) != null) {
+            resultMap.put(DataPermRuleType.TYPE_USER_ONLY, "null");
+        }
+        return resultMap;
+    }
+
+    private String processMultiDeptAndChildren(Map<Integer, List<SysDataPerm>> ruleMap, Long deptId) {
+        List<SysDataPerm> parentDeptList = ruleMap.get(DataPermRuleType.TYPE_MULTI_DEPT_AND_CHILD_DEPT);
+        if (parentDeptList == null) {
+            return null;
+        }
+        Set<Long> deptIdSet = new HashSet<>();
+        for (SysDataPerm parentDept : parentDeptList) {
+            deptIdSet.addAll(StrUtil.split(parentDept.getDeptIdListString(), ',')
+                    .stream().map(Long::valueOf).collect(Collectors.toSet()));
+        }
+        // 在合并所有的多父部门Id之后,需要判断是否有本部门及子部门的规则。如果有,就继续合并。
+        if (ruleMap.containsKey(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT)) {
+            // 如果多父部门列表中包含当前部门,那么可以直接删除该规则了,如果没包含,就加入到多部门的DEPT_ID的IN LIST中。
+            deptIdSet.add(deptId);
+            ruleMap.remove(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT);
+        }
+        // 需要与仅仅当前部门规则进行合并。
+        if (ruleMap.containsKey(DataPermRuleType.TYPE_DEPT_ONLY) && deptIdSet.contains(deptId)) {
+            ruleMap.remove(DataPermRuleType.TYPE_DEPT_ONLY);
+        }
+        return StrUtil.join(",", deptIdSet);
+    }
+
+    private String processMultiDept(Map<Integer, List<SysDataPerm>> ruleMap, Long deptId) {
+        List<SysDataPerm> customDeptList = ruleMap.get(DataPermRuleType.TYPE_CUSTOM_DEPT_LIST);
+        if (customDeptList == null) {
+            return null;
+        }
+        Set<Long> deptIdSet = new HashSet<>();
+        for (SysDataPerm customDept : customDeptList) {
+            deptIdSet.addAll(StrUtil.split(customDept.getDeptIdListString(), ',')
+                    .stream().map(Long::valueOf).collect(Collectors.toSet()));
+        }
+        if (ruleMap.containsKey(DataPermRuleType.TYPE_DEPT_ONLY)) {
+            deptIdSet.add(deptId);
+            ruleMap.remove(DataPermRuleType.TYPE_DEPT_ONLY);
+        }
+        return StrUtil.join(",", deptIdSet);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void addDataPermUserList(Long dataPermId, Set<Long> userIdSet) {
+        for (Long userId : userIdSet) {
+            SysDataPermUser dataPermUser = new SysDataPermUser();
+            dataPermUser.setDataPermId(dataPermId);
+            dataPermUser.setUserId(userId);
+            sysDataPermUserMapper.insert(dataPermUser);
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean removeDataPermUser(Long dataPermId, Long userId) {
+        SysDataPermUser dataPermUser = new SysDataPermUser();
+        dataPermUser.setDataPermId(dataPermId);
+        dataPermUser.setUserId(userId);
+        return sysDataPermUserMapper.delete(new QueryWrapper<>(dataPermUser)) == 1;
+    }
+
+    @Override
+    public CallResult verifyRelatedData(SysDataPerm dataPerm, String deptIdListString, String menuIdListString) {
+        JSONObject jsonObject = new JSONObject();
+        if (dataPerm.getRuleType() == DataPermRuleType.TYPE_MULTI_DEPT_AND_CHILD_DEPT
+                || dataPerm.getRuleType() == DataPermRuleType.TYPE_CUSTOM_DEPT_LIST) {
+            if (StrUtil.isBlank(deptIdListString)) {
+                return CallResult.error("数据验证失败,部门列表不能为空!");
+            }
+            Set<Long> deptIdSet = StrUtil.split(
+                    deptIdListString, ",").stream().map(Long::valueOf).collect(Collectors.toSet());
+            if (!sysDeptService.existAllPrimaryKeys(deptIdSet)) {
+                return CallResult.error("数据验证失败,存在不合法的部门数据,请刷新后重试!");
+            }
+            jsonObject.put("deptIdSet", deptIdSet);
+        }
+        if (StrUtil.isNotBlank(menuIdListString)) {
+            Set<Long> menuIdSet = StrUtil.split(
+                    menuIdListString, ",").stream().map(Long::valueOf).collect(Collectors.toSet());
+            if (!sysMenuService.existAllPrimaryKeys(menuIdSet)) {
+                return CallResult.error("数据验证失败,存在不合法的菜单数据,请刷新后重试!");
+            }
+            jsonObject.put("menuIdSet", menuIdSet);
+        }
+        return CallResult.ok(jsonObject);
+    }
+
+    private void insertRelationData(SysDataPerm dataPerm, Set<Long> deptIdSet, Set<Long> menuIdSet, Set<Long> entryIdSet) {
+        if (CollUtil.isNotEmpty(deptIdSet)) {
+            for (Long deptId : deptIdSet) {
+                SysDataPermDept dataPermDept = new SysDataPermDept();
+                dataPermDept.setDataPermId(dataPerm.getDataPermId());
+                dataPermDept.setDeptId(deptId);
+                sysDataPermDeptMapper.insert(dataPermDept);
+            }
+        }
+        if (CollUtil.isNotEmpty(menuIdSet)) {
+            for (Long menuId : menuIdSet) {
+                SysDataPermMenu dataPermMenu = new SysDataPermMenu();
+                dataPermMenu.setDataPermId(dataPerm.getDataPermId());
+                dataPermMenu.setMenuId(menuId);
+                sysDataPermMenuMapper.insert(dataPermMenu);
+            }
+        }
+        if (CollUtil.isNotEmpty(entryIdSet)) {
+            for (Long entryId : entryIdSet) {
+                MobileEntryDataPerm mobileEntryDataPerm = new MobileEntryDataPerm();
+                mobileEntryDataPerm.setDataPermId(dataPerm.getDataPermId());
+                mobileEntryDataPerm.setEntryId(entryId);
+                mobileEntryDataPermMapper.insert(mobileEntryDataPerm);
+            }
+        }
+    }
+}

+ 316 - 0
application-webadmin/src/main/java/com/tourism/webadmin/upms/service/impl/SysDeptServiceImpl.java

@@ -0,0 +1,316 @@
+package com.tourism.webadmin.upms.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.*;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.github.pagehelper.page.PageMethod;
+import com.tourism.webadmin.upms.service.*;
+import com.tourism.webadmin.upms.dao.*;
+import com.tourism.webadmin.upms.model.*;
+import com.tourism.common.ext.util.BizWidgetDatasourceExtHelper;
+import com.tourism.common.ext.base.BizWidgetDatasource;
+import com.tourism.common.ext.constant.BizWidgetDatasourceType;
+import com.tourism.common.core.base.dao.BaseDaoMapper;
+import com.tourism.common.core.constant.GlobalDeletedFlag;
+import com.tourism.common.core.object.*;
+import com.tourism.common.core.base.service.BaseService;
+import com.tourism.common.core.util.MyModelUtil;
+import com.tourism.common.core.util.MyPageUtil;
+import com.tourism.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 jakarta.annotation.PostConstruct;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 部门管理数据操作服务类。
+ *
+ * @author 吃饭睡觉
+ * @date 2024-09-06
+ */
+@Slf4j
+@Service("sysDeptService")
+public class SysDeptServiceImpl extends BaseService<SysDept, Long> implements SysDeptService, BizWidgetDatasource {
+
+    @Autowired
+    private IdGeneratorWrapper idGenerator;
+    @Autowired
+    private SysDeptMapper sysDeptMapper;
+    @Autowired
+    private SysDeptRelationMapper sysDeptRelationMapper;
+    @Autowired
+    private SysUserService sysUserService;
+    @Autowired
+    private SysDeptPostMapper sysDeptPostMapper;
+    @Autowired
+    private SysDataPermDeptMapper sysDataPermDeptMapper;
+    @Autowired
+    private BizWidgetDatasourceExtHelper bizWidgetDatasourceExtHelper;
+
+    /**
+     * 返回当前Service的主表Mapper对象。
+     *
+     * @return 主表Mapper对象。
+     */
+    @Override
+    protected BaseDaoMapper<SysDept> mapper() {
+        return sysDeptMapper;
+    }
+
+    @PostConstruct
+    private void registerBizWidgetDatasource() {
+        bizWidgetDatasourceExtHelper.registerDatasource(BizWidgetDatasourceType.UPMS_DEPT_TYPE, this);
+    }
+
+    @Override
+    public MyPageData<Map<String, Object>> getDataList(
+            String type, Map<String, Object> filter, MyOrderParam orderParam, MyPageParam pageParam) {
+        if (pageParam != null) {
+            PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize(), pageParam.getCount());
+        }
+        String orderBy = orderParam == null ? null : MyOrderParam.buildOrderBy(orderParam, SysDept.class);
+        SysDept deptFilter = filter == null ? null : BeanUtil.toBean(filter, SysDept.class);
+        List<SysDept> deptList = this.getSysDeptList(deptFilter, orderBy);
+        this.buildRelationForDataList(deptList, MyRelationParam.dictOnly());
+        return MyPageUtil.makeResponseData(deptList, BeanUtil::beanToMap);
+    }
+
+    @Override
+    public List<Map<String, Object>> getDataListWithInList(String type, String fieldName, List<String> fieldValues) {
+        List<SysDept> deptList;
+        if (StrUtil.isBlank(fieldName)) {
+            deptList = this.getInList(fieldValues.stream().map(Long::valueOf).collect(Collectors.toSet()));
+        } else {
+            deptList = this.getInList(fieldName, MyModelUtil.convertToTypeValues(SysDept.class, fieldName, fieldValues));
+        }
+        this.buildRelationForDataList(deptList, MyRelationParam.dictOnly());
+        return MyModelUtil.beanToMapList(deptList);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public SysDept saveNew(SysDept sysDept, SysDept parentSysDept) {
+        sysDept.setDeptId(idGenerator.nextLongId());
+        sysDept.setDeletedFlag(GlobalDeletedFlag.NORMAL);
+        MyModelUtil.fillCommonsForInsert(sysDept);
+        sysDeptMapper.insert(sysDept);
+        // 同步插入部门关联关系数据
+        if (parentSysDept == null) {
+            sysDeptRelationMapper.insert(new SysDeptRelation(sysDept.getDeptId(), sysDept.getDeptId()));
+        } else {
+            sysDeptRelationMapper.insertParentList(parentSysDept.getDeptId(), sysDept.getDeptId());
+        }
+        return sysDept;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean update(SysDept sysDept, SysDept originalSysDept) {
+        MyModelUtil.fillCommonsForUpdate(sysDept, originalSysDept);
+        UpdateWrapper<SysDept> uw = this.createUpdateQueryForNullValue(sysDept, sysDept.getDeptId());
+        if (sysDeptMapper.update(sysDept, uw) == 0) {
+            return false;
+        }
+        if (ObjectUtil.notEqual(sysDept.getParentId(), originalSysDept.getParentId())) {
+            this.updateParentRelation(sysDept, originalSysDept);
+        }
+        return true;
+    }
+
+    private void updateParentRelation(SysDept sysDept, SysDept originalSysDept) {
+        List<Long> originalParentIdList = null;
+        // 1. 因为层级关系变化了,所以要先遍历出,当前部门的原有父部门Id列表。
+        if (originalSysDept.getParentId() != null) {
+            LambdaQueryWrapper<SysDeptRelation> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(SysDeptRelation::getDeptId, sysDept.getDeptId());
+            List<SysDeptRelation> relationList = sysDeptRelationMapper.selectList(queryWrapper);
+            originalParentIdList = relationList.stream()
+                    .filter(c -> !c.getParentDeptId().equals(sysDept.getDeptId()))
+                    .map(SysDeptRelation::getParentDeptId).collect(Collectors.toList());
+        }
+        // 2. 毕竟当前部门的上级部门变化了,所以当前部门和他的所有子部门,与当前部门的原有所有上级部门
+        // 之间的关联关系就要被移除。
+        // 这里先移除当前部门的所有子部门,与当前部门的所有原有上级部门之间的关联关系。
+        if (CollUtil.isNotEmpty(originalParentIdList)) {
+            sysDeptRelationMapper.removeBetweenChildrenAndParents(originalParentIdList, sysDept.getDeptId());
+        }
+        // 这里更进一步,将当前部门Id与其原有所有上级部门Id之间的关联关系删除。
+        SysDeptRelation filter = new SysDeptRelation();
+        filter.setDeptId(sysDept.getDeptId());
+        sysDeptRelationMapper.delete(new QueryWrapper<>(filter));
+        // 3. 重新计算当前部门的新上级部门列表。
+        List<Long> newParentIdList = new LinkedList<>();
+        // 这里要重新计算出当前部门所有新的上级部门Id列表。
+        if (sysDept.getParentId() != null) {
+            LambdaQueryWrapper<SysDeptRelation> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(SysDeptRelation::getDeptId, sysDept.getParentId());
+            List<SysDeptRelation> relationList = sysDeptRelationMapper.selectList(queryWrapper);
+            newParentIdList = relationList.stream()
+                    .map(SysDeptRelation::getParentDeptId).collect(Collectors.toList());
+        }
+        // 4. 先查询出当前部门的所有下级子部门Id列表。
+        LambdaQueryWrapper<SysDeptRelation> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(SysDeptRelation::getParentDeptId, sysDept.getDeptId());
+        List<SysDeptRelation> childRelationList = sysDeptRelationMapper.selectList(queryWrapper);
+        // 5. 将当前部门及其所有子部门Id与其新的所有上级部门Id之间,建立关联关系。
+        List<SysDeptRelation> deptRelationList = new LinkedList<>();
+        deptRelationList.add(new SysDeptRelation(sysDept.getDeptId(), sysDept.getDeptId()));
+        for (Long newParentId : newParentIdList) {
+            deptRelationList.add(new SysDeptRelation(newParentId, sysDept.getDeptId()));
+            for (SysDeptRelation childDeptRelation : childRelationList) {
+                deptRelationList.add(new SysDeptRelation(newParentId, childDeptRelation.getDeptId()));
+            }
+        }
+        // 6. 执行批量插入SQL语句,插入当前部门Id及其所有下级子部门Id,与所有新上级部门Id之间的关联关系。
+        sysDeptRelationMapper.insertList(deptRelationList);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean remove(Long deptId) {
+        if (sysDeptMapper.deleteById(deptId) == 0) {
+            return false;
+        }
+        // 这里删除当前部门及其父部门的关联关系。
+        // 当前部门和子部门的关系无需在这里删除,因为包含子部门时不能删除父部门。
+        SysDeptRelation deptRelation = new SysDeptRelation();
+        deptRelation.setDeptId(deptId);
+        sysDeptRelationMapper.delete(new QueryWrapper<>(deptRelation));
+        SysDataPermDept dataPermDept = new SysDataPermDept();
+        dataPermDept.setDeptId(deptId);
+        sysDataPermDeptMapper.delete(new QueryWrapper<>(dataPermDept));
+        return true;
+    }
+
+    @Override
+    public List<SysDept> getSysDeptList(SysDept filter, String orderBy) {
+        return sysDeptMapper.getSysDeptList(filter, orderBy);
+    }
+
+    @Override
+    public List<SysDept> getSysDeptListWithRelation(SysDept filter, String orderBy) {
+        List<SysDept> resultList = sysDeptMapper.getSysDeptList(filter, orderBy);
+        // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。
+        // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。
+        int batchSize = resultList instanceof Page ? 0 : 1000;
+        this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize);
+        return resultList;
+    }
+
+    @Override
+    public boolean hasChildren(Long deptId) {
+        SysDept filter = new SysDept();
+        filter.setParentId(deptId);
+        return getCountByFilter(filter) > 0;
+    }
+
+    @Override
+    public boolean hasChildrenUser(Long deptId) {
+        SysUser sysUser = new SysUser();
+        sysUser.setDeptId(deptId);
+        return sysUserService.getCountByFilter(sysUser) > 0;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void addSysDeptPostList(List<SysDeptPost> sysDeptPostList, Long deptId) {
+        for (SysDeptPost sysDeptPost : sysDeptPostList) {
+            sysDeptPost.setDeptPostId(idGenerator.nextLongId());
+            sysDeptPost.setDeptId(deptId);
+            sysDeptPostMapper.insert(sysDeptPost);
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean updateSysDeptPost(SysDeptPost sysDeptPost) {
+        SysDeptPost filter = new SysDeptPost();
+        filter.setDeptPostId(sysDeptPost.getDeptPostId());
+        filter.setDeptId(sysDeptPost.getDeptId());
+        filter.setPostId(sysDeptPost.getPostId());
+        UpdateWrapper<SysDeptPost> uw =
+                BaseService.createUpdateQueryForNullValue(sysDeptPost, SysDeptPost.class);
+        uw.setEntity(filter);
+        return sysDeptPostMapper.update(sysDeptPost, uw) > 0;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean removeSysDeptPost(Long deptId, Long postId) {
+        SysDeptPost filter = new SysDeptPost();
+        filter.setDeptId(deptId);
+        filter.setPostId(postId);
+        return sysDeptPostMapper.delete(new QueryWrapper<>(filter)) > 0;
+    }
+
+    @Override
+    public SysDeptPost getSysDeptPost(Long deptId, Long postId) {
+        SysDeptPost filter = new SysDeptPost();
+        filter.setDeptId(deptId);
+        filter.setPostId(postId);
+        return sysDeptPostMapper.selectOne(new QueryWrapper<>(filter));
+    }
+
+    @Override
+    public SysDeptPost getSysDeptPost(Long deptPostId) {
+        return sysDeptPostMapper.selectById(deptPostId);
+    }
+
+    @Override
+    public List<Map<String, Object>> getSysDeptPostListWithRelationByDeptId(Long deptId) {
+        return sysDeptPostMapper.getSysDeptPostListWithRelationByDeptId(deptId);
+    }
+
+    @Override
+    public List<SysDeptPost> getSysDeptPostList(Long deptId, Set<Long> postIdSet) {
+        LambdaQueryWrapper<SysDeptPost> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(SysDeptPost::getDeptId, deptId);
+        queryWrapper.in(SysDeptPost::getPostId, postIdSet);
+        return sysDeptPostMapper.selectList(queryWrapper);
+    }
+
+    @Override
+    public List<SysDeptPost> getSiblingSysDeptPostList(Long deptId, Set<Long> postIdSet) {
+        SysDept sysDept = this.getById(deptId);
+        if (sysDept == null) {
+            return new LinkedList<>();
+        }
+        List<SysDept> deptList = this.getListByParentId("parentId", sysDept.getParentId());
+        Set<Long> deptIdSet = deptList.stream().map(SysDept::getDeptId).collect(Collectors.toSet());
+        LambdaQueryWrapper<SysDeptPost> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(SysDeptPost::getDeptId, deptIdSet);
+        queryWrapper.in(SysDeptPost::getPostId, postIdSet);
+        return sysDeptPostMapper.selectList(queryWrapper);
+    }
+
+    @Override
+    public List<Long> getLeaderDeptPostIdList(Long deptId) {
+        List<SysDeptPost> resultList = sysDeptPostMapper.getLeaderDeptPostList(deptId);
+        return resultList.stream().map(SysDeptPost::getDeptPostId).collect(Collectors.toList());
+    }
+
+    @Override
+    public List<Long> getUpLeaderDeptPostIdList(Long deptId) {
+        SysDept sysDept = this.getById(deptId);
+        if (sysDept.getParentId() == null) {
+            return new LinkedList<>();
+        }
+        return this.getLeaderDeptPostIdList(sysDept.getParentId());
+    }
+
+    @Override
+    public List<Long> getAllChildDeptIdByParentIds(List<Long> parentIds) {
+        LambdaQueryWrapper<SysDeptRelation> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(SysDeptRelation::getParentDeptId, parentIds);
+        return sysDeptRelationMapper.selectList(queryWrapper)
+                .stream().map(SysDeptRelation::getDeptId).collect(Collectors.toList());
+    }
+}

Some files were not shown because too many files changed in this diff