瀏覽代碼

Merge branch 'master' into feat_userprofile

# Conflicts:
#	src/main/resources/application-dev.yaml
#	src/main/resources/application-local.yaml
niudehua 4 年之前
父節點
當前提交
e4bda22b73
共有 46 個文件被更改,包括 1605 次插入1022 次删除
  1. 1 1
      README.md
  2. 19 9
      pom.xml
  3. 18 0
      ruoyi-ui/src/api/infra/file.js
  4. 202 0
      ruoyi-ui/src/views/infra/file/index.vue
  5. 751 715
      ruoyi-ui/src/views/system/user/index.vue
  6. 23 78
      sql/ruoyi-vue-pro.sql
  7. 2 2
      src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java
  8. 1 1
      src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java
  9. 36 11
      src/main/java/cn/iocoder/dashboard/modules/infra/controller/file/InfFileController.java
  10. 35 0
      src/main/java/cn/iocoder/dashboard/modules/infra/controller/file/vo/InfFilePageReqVO.java
  11. 22 0
      src/main/java/cn/iocoder/dashboard/modules/infra/controller/file/vo/InfFileRespVO.java
  12. 18 0
      src/main/java/cn/iocoder/dashboard/modules/infra/convert/file/InfFileConvert.java
  13. 43 0
      src/main/java/cn/iocoder/dashboard/modules/infra/dal/dataobject/file/InfFileDO.java
  14. 25 0
      src/main/java/cn/iocoder/dashboard/modules/infra/dal/mysql/file/InfFileMapper.java
  15. 3 0
      src/main/java/cn/iocoder/dashboard/modules/infra/enums/InfErrorCodeConstants.java
  16. 46 0
      src/main/java/cn/iocoder/dashboard/modules/infra/service/file/InfFileService.java
  17. 72 0
      src/main/java/cn/iocoder/dashboard/modules/infra/service/file/impl/InfFileServiceImpl.java
  18. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/controller/common/SysCaptchaController.java
  19. 0 30
      src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/common/SysFileDO.java
  20. 0 15
      src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/common/SysFileMapper.java
  21. 0 29
      src/main/java/cn/iocoder/dashboard/modules/system/service/common/SysFileService.java
  22. 0 47
      src/main/java/cn/iocoder/dashboard/modules/system/service/common/impl/SysFileServiceImpl.java
  23. 2 1
      src/main/java/cn/iocoder/dashboard/modules/tool/dal/dataobject/codegen/ToolCodegenColumnDO.java
  24. 2 1
      src/main/java/cn/iocoder/dashboard/modules/tool/dal/dataobject/codegen/ToolCodegenTableDO.java
  25. 1 0
      src/main/java/cn/iocoder/dashboard/modules/tool/service/codegen/impl/ToolCodegenBuilder.java
  26. 1 1
      src/main/resources/codegen/java/controller/controller.vm
  27. 2 1
      src/main/resources/codegen/java/service/serviceImpl.vm
  28. 7 7
      src/main/resources/codegen/java/test/serviceTest.vm
  29. 4 4
      src/main/resources/codegen/sql/sql.vm
  30. 1 1
      src/main/resources/codegen/vue/views/index.vue.vm
  31. 2 2
      src/test/java/cn/iocoder/dashboard/BaseDbAndRedisUnitTest.java
  32. 32 0
      src/test/java/cn/iocoder/dashboard/BaseRedisUnitTest.java
  33. 0 32
      src/test/java/cn/iocoder/dashboard/BaseSpringBootUnitTest.java
  34. 2 4
      src/test/java/cn/iocoder/dashboard/config/RedisTestConfiguration.java
  35. 2 2
      src/test/java/cn/iocoder/dashboard/framework/quartz/core/scheduler/SchedulerManagerTest.java
  36. 126 0
      src/test/java/cn/iocoder/dashboard/modules/infra/service/file/InfFileServiceTest.java
  37. 10 11
      src/test/java/cn/iocoder/dashboard/modules/system/service/auth/SysUserSessionServiceImplTest.java
  38. 66 0
      src/test/java/cn/iocoder/dashboard/modules/system/service/common/SysCaptchaServiceTest.java
  39. 2 2
      src/test/java/cn/iocoder/dashboard/modules/tool/dal/mysql/codegen/ToolInformationSchemaColumnMapperTest.java
  40. 2 2
      src/test/java/cn/iocoder/dashboard/modules/tool/dal/mysql/codegen/ToolInformationSchemaTableMapperTest.java
  41. 2 3
      src/test/java/cn/iocoder/dashboard/modules/tool/service/codegen/impl/ToolCodegenEngineTest.java
  42. 2 2
      src/test/java/cn/iocoder/dashboard/modules/tool/service/codegen/impl/ToolCodegenSQLParserTest.java
  43. 2 3
      src/test/java/cn/iocoder/dashboard/modules/tool/service/codegen/impl/ToolCodegenServiceImplTest.java
  44. 二進制
      src/test/resources/file/erweima.jpg
  45. 1 0
      src/test/resources/sql/clean.sql
  46. 16 4
      src/test/resources/sql/create_tables.sql

+ 1 - 1
README.md

@@ -50,6 +50,7 @@
 | --- | --- | --- |
 | 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 |
 | | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 |
+| 🚀 | 文件服务 | 支持本地文件存储,同时支持兼容 Amazon S3 协议的云服务、开源组件 | 
 | 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 |
 |  | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈 |
 | | Redis 监控 |监控 Redis 数据库的使用情况,使用的 Redis Key 管理 |
@@ -64,7 +65,6 @@
 计划新增:
 * 工作流
 * 错误码
-* 文件服务
 
 ### 研发工具
 

+ 19 - 9
pom.xml

@@ -40,8 +40,8 @@
         <skywalking.version>8.3.0</skywalking.version>
         <spring-boot-admin.version>2.3.1</spring-boot-admin.version>
         <!-- 工具类相关 -->
-        <mapstruct.version>1.4.2.Final</mapstruct.version>
-        <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
+        <lombok.version>1.16.14</lombok.version>
+        <mapstruct.version>1.4.1.Final</mapstruct.version>
         <hutool.version>5.5.6</hutool.version>
         <easyexcel.verion>2.2.7</easyexcel.verion>
         <velocity.version>2.2</velocity.version>
@@ -227,26 +227,24 @@
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
-            <optional>true</optional>
+            <version>${lombok.version}</version>
         </dependency>
 
         <dependency>
             <groupId>org.mapstruct</groupId>
             <artifactId>mapstruct</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher -->
             <version>${mapstruct.version}</version>
-            <optional>true</optional>
         </dependency>
         <dependency>
             <groupId>org.mapstruct</groupId>
-            <artifactId>mapstruct-jdk8</artifactId>
+            <artifactId>mapstruct-processor</artifactId>
             <version>${mapstruct.version}</version>
             <optional>true</optional>
         </dependency>
         <dependency>
-            <groupId>org.projectlombok</groupId>
-            <artifactId>lombok-mapstruct-binding</artifactId>
-            <version>${lombok-mapstruct-binding.version}</version>
-            <optional>true</optional>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-jdk8</artifactId>
+            <version>${mapstruct.version}</version>
         </dependency>
 
         <dependency>
@@ -325,6 +323,18 @@
                 <configuration>
                     <source>${java.version}</source> <!-- or higher, depending on your project -->
                     <target>${java.version}</target> <!-- or higher, depending on your project -->
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>org.mapstruct</groupId>
+                            <artifactId>mapstruct-processor</artifactId>
+                            <version>${mapstruct.version}</version>
+                        </path>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                            <version>${lombok.version}</version>
+                        </path>
+                    </annotationProcessorPaths>
                 </configuration>
             </plugin>
         </plugins>

+ 18 - 0
ruoyi-ui/src/api/infra/file.js

@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// 删除文件
+export function deleteFile(id) {
+  return request({
+    url: '/infra/file/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得文件分页
+export function getFilePage(query) {
+  return request({
+    url: '/infra/file/page',
+    method: 'get',
+    params: query
+  })
+}

+ 202 - 0
ruoyi-ui/src/views/infra/file/index.vue

@@ -0,0 +1,202 @@
+<template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="文件路径" prop="id">
+        <el-input v-model="queryParams.id" placeholder="请输入文件路径" clearable size="small" @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="文件类型" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择文件类型" clearable size="small">
+          <el-option label="请选择字典生成" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间">
+        <el-date-picker v-model="dateRangeCreateTime" size="small" style="width: 240px" value-format="yyyy-MM-dd"
+                        type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">上传文件</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="文件路径" align="center" prop="id" width="300" />
+      <el-table-column label="文件类型" align="center" prop="type" width="80" />
+      <el-table-column label="文件内容" align="center" prop="content">
+        <template slot-scope="scope">
+          <img v-if="scope.row.type === 'jpg' || scope.row.type === 'png' || scope.row.type === 'gif'"
+               width="200px" :src="getFileUrl + scope.row.id">
+          <i v-else>非图片,无法预览</i>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['infra:file:delete']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
+      <el-upload ref="upload" :limit="1" accept=".jpg, .png, .gif" :auto-upload="false" drag
+                 :headers="upload.headers" :action="upload.url" :data="upload.data" :disabled="upload.isUploading"
+                 :on-change="handleFileChange"
+                 :on-progress="handleFileUploadProgress"
+                 :on-success="handleFileSuccess">
+        <i class="el-icon-upload"></i>
+        <div class="el-upload__text">
+          将文件拖到此处,或 <em>点击上传</em>
+        </div>
+        <div class="el-upload__tip" style="color:red" slot="tip">提示:仅允许导入 jpg、png、gif 格式文件!</div>
+      </el-upload>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitFileForm">确 定</el-button>
+        <el-button @click="upload.open = false">取 消</el-button>
+      </div>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import { deleteFile, getFilePage } from "@/api/infra/file";
+import {getToken} from "@/utils/auth";
+
+export default {
+  name: "File",
+  data() {
+    return {
+      getFileUrl: process.env.VUE_APP_BASE_API + '/api/infra/file/get/',
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 文件列表
+      list: [],
+      // 弹出层标题
+      title: "",
+      dateRangeCreateTime: [],
+      // 查询参数
+      queryParams: {
+        pageNo: 1,
+        pageSize: 10,
+        id: null,
+        type: null,
+      },
+      // 用户导入参数
+      upload: {
+        open: false, // 是否显示弹出层
+        title: "", // 弹出层标题
+        isUploading: false, // 是否禁用上传
+        url: process.env.VUE_APP_BASE_API + '/api/' + "/infra/file/upload", // 请求地址
+        headers: { Authorization: "Bearer " + getToken() }, // 设置上传的请求头部
+        data: {} // 上传的额外数据,用于文件名
+      },
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      // 处理查询参数
+      let params = {...this.queryParams};
+      this.addBeginAndEndTime(params, this.dateRangeCreateTime, 'createTime');
+      // 执行查询
+      getFilePage(params).then(response => {
+        this.list = response.data.list;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    /** 取消按钮 */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        content: undefined,
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRangeCreateTime = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.upload.open = true;
+      this.upload.title = "上传文件";
+    },
+    /** 处理上传的文件发生变化 */
+    handleFileChange(file, fileList) {
+      this.upload.data.path = file.name;
+    },
+    /** 处理文件上传中 */
+    handleFileUploadProgress(event, file, fileList) {
+      this.upload.isUploading = true; // 禁止修改
+    },
+    /** 发起文件上窜 */
+    submitFileForm() {
+      this.$refs.upload.submit();
+    },
+    /** 文件上传成功处理 */
+    handleFileSuccess(response, file, fileList) {
+      // 清理
+      this.upload.open = false;
+      this.upload.isUploading = false;
+      this.$refs.upload.clearFiles();
+      // 提示成功,并刷新
+      this.msgSuccess("上传成功");
+      this.getList();
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const id = row.id;
+      this.$confirm('是否确认删除文件编号为"' + id + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return deleteFile(id);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      })
+    },
+  }
+};
+</script>

+ 751 - 715
ruoyi-ui/src/views/system/user/index.vue

@@ -1,715 +1,751 @@
-<template>
-  <div class="app-container">
-    <el-row :gutter="20">
-      <!--部门数据-->
-      <el-col :span="4" :xs="24">
-        <div class="head-container">
-          <el-input
-            v-model="deptName"
-            placeholder="请输入部门名称"
-            clearable
-            size="small"
-            prefix-icon="el-icon-search"
-            style="margin-bottom: 20px"
-          />
-        </div>
-        <div class="head-container">
-          <el-tree
-            :data="deptOptions"
-            :props="defaultProps"
-            :expand-on-click-node="false"
-            :filter-node-method="filterNode"
-            ref="tree"
-            default-expand-all
-            @node-click="handleNodeClick"
-          />
-        </div>
-      </el-col>
-      <!--用户数据-->
-      <el-col :span="20" :xs="24">
-        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
-          <el-form-item label="用户名称" prop="username">
-            <el-input
-              v-model="queryParams.username"
-              placeholder="请输入用户名称"
-              clearable
-              size="small"
-              style="width: 240px"
-              @keyup.enter.native="handleQuery"
-            />
-          </el-form-item>
-          <el-form-item label="手机号码" prop="mobile">
-            <el-input
-              v-model="queryParams.mobile"
-              placeholder="请输入手机号码"
-              clearable
-              size="small"
-              style="width: 240px"
-              @keyup.enter.native="handleQuery"
-            />
-          </el-form-item>
-          <el-form-item label="状态" prop="status">
-            <el-select
-              v-model="queryParams.status"
-              placeholder="用户状态"
-              clearable
-              size="small"
-              style="width: 240px"
-            >
-              <el-option
-                  v-for="dict in statusDictDatas"
-                  :key="parseInt(dict.value)"
-                  :label="dict.label"
-                  :value="parseInt(dict.value)"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item label="创建时间">
-            <el-date-picker
-              v-model="dateRange"
-              size="small"
-              style="width: 240px"
-              value-format="yyyy-MM-dd"
-              type="daterange"
-              range-separator="-"
-              start-placeholder="开始日期"
-              end-placeholder="结束日期"
-            ></el-date-picker>
-          </el-form-item>
-          <el-form-item>
-            <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
-            <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
-          </el-form-item>
-        </el-form>
-
-        <el-row :gutter="10" class="mb8">
-          <el-col :span="1.5">
-            <el-button
-              type="primary"
-              icon="el-icon-plus"
-              size="mini"
-              @click="handleAdd"
-              v-hasPermi="['system:user:add']"
-            >新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button
-              type="info"
-              icon="el-icon-upload2"
-              size="mini"
-              @click="handleImport"
-              v-hasPermi="['system:user:import']"
-            >导入</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button
-              type="warning"
-              icon="el-icon-download"
-              size="mini"
-              @click="handleExport"
-              v-hasPermi="['system:user:export']"
-            >导出</el-button>
-          </el-col>
-          <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
-        </el-row>
-
-        <el-table v-loading="loading" :data="userList">
-          <el-table-column label="用户编号" align="center" prop="id" />
-          <el-table-column label="用户名称" align="center" prop="username" :show-overflow-tooltip="true" />
-          <el-table-column label="用户昵称" align="center" prop="nickname" :show-overflow-tooltip="true" />
-          <el-table-column label="部门" align="center" prop="dept.name" :show-overflow-tooltip="true" />
-          <el-table-column label="手机号码" align="center" prop="mobile" width="120" />
-          <el-table-column label="状态" align="center">
-            <template slot-scope="scope">
-              <el-switch
-                  v-model="scope.row.status"
-                  :active-value="0"
-                  :inactive-value="1"
-                  @change="handleStatusChange(scope.row)"
-              ></el-switch>
-            </template>
-          </el-table-column>
-          <el-table-column label="创建时间" align="center" prop="createTime" width="160">
-            <template slot-scope="scope">
-              <span>{{ parseTime(scope.row.createTime) }}</span>
-            </template>
-          </el-table-column>
-          <el-table-column
-            label="操作"
-            align="center"
-            width="160"
-            class-name="small-padding fixed-width"
-          >
-            <template slot-scope="scope">
-              <el-button
-                size="mini"
-                type="text"
-                icon="el-icon-edit"
-                @click="handleUpdate(scope.row)"
-                v-hasPermi="['system:user:edit']"
-              >修改</el-button>
-              <el-button
-                v-if="scope.row.id !== 1"
-                size="mini"
-                type="text"
-                icon="el-icon-delete"
-                @click="handleDelete(scope.row)"
-                v-hasPermi="['system:user:remove']"
-              >删除</el-button>
-              <el-button
-                size="mini"
-                type="text"
-                icon="el-icon-key"
-                @click="handleResetPwd(scope.row)"
-                v-hasPermi="['system:user:resetPwd']"
-              >重置</el-button>
-              <el-button
-                  size="mini"
-                  type="text"
-                  icon="el-icon-circle-check"
-                  @click="handleRole(scope.row)"
-                  v-hasPermi="['system:permission:assign-user-role']"
-              >分配角色</el-button>
-            </template>
-          </el-table-column>
-        </el-table>
-
-        <pagination
-          v-show="total>0"
-          :total="total"
-          :page.sync="queryParams.pageNo"
-          :limit.sync="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </el-col>
-    </el-row>
-
-    <!-- 添加或修改参数配置对话框 -->
-    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
-        <el-row>
-          <el-col :span="12">
-            <el-form-item label="用户昵称" prop="nickname">
-              <el-input v-model="form.nickname" placeholder="请输入用户昵称" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="归属部门" prop="deptId">
-              <treeselect v-model="form.deptId" :options="deptOptions" :show-count="true"
-                          placeholder="请选择归属部门" :normalizer="normalizer"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row>
-          <el-col :span="12">
-            <el-form-item label="手机号码" prop="mobile">
-              <el-input v-model="form.mobile" placeholder="请输入手机号码" maxlength="11" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="邮箱" prop="email">
-              <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row>
-          <el-col :span="12">
-            <el-form-item v-if="form.id === undefined" label="用户名称" prop="username">
-              <el-input v-model="form.username" placeholder="请输入用户名称" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item v-if="form.id === undefined" label="用户密码" prop="password">
-              <el-input v-model="form.password" placeholder="请输入用户密码" type="password" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row>
-          <el-col :span="12">
-            <el-form-item label="用户性别">
-              <el-select v-model="form.sex" placeholder="请选择">
-                <el-option
-                  v-for="dict in sexDictDatas"
-                  :key="parseInt(dict.value)"
-                  :label="dict.label"
-                  :value="parseInt(dict.value)"
-                ></el-option>
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="岗位">
-              <el-select v-model="form.postIds" multiple placeholder="请选择">
-                <el-option
-                    v-for="item in postOptions"
-                    :key="item.id"
-                    :label="item.name"
-                    :value="item.id"
-                ></el-option>
-              </el-select>
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row>
-          <el-col :span="24">
-            <el-form-item label="备注">
-              <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
-            </el-form-item>
-          </el-col>
-        </el-row>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </div>
-    </el-dialog>
-
-    <!-- 用户导入对话框 -->
-    <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
-      <el-upload
-        ref="upload"
-        :limit="1"
-        accept=".xlsx, .xls"
-        :headers="upload.headers"
-        :action="upload.url + '?updateSupport=' + upload.updateSupport"
-        :disabled="upload.isUploading"
-        :on-progress="handleFileUploadProgress"
-        :on-success="handleFileSuccess"
-        :auto-upload="false"
-        drag
-      >
-        <i class="el-icon-upload"></i>
-        <div class="el-upload__text">
-          将文件拖到此处,或
-          <em>点击上传</em>
-        </div>
-        <div class="el-upload__tip" slot="tip">
-          <el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据
-          <el-link type="info" style="font-size:12px" @click="importTemplate">下载模板</el-link>
-        </div>
-        <div class="el-upload__tip" style="color:red" slot="tip">提示:仅允许导入“xls”或“xlsx”格式文件!</div>
-      </el-upload>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitFileForm">确 定</el-button>
-        <el-button @click="upload.open = false">取 消</el-button>
-      </div>
-    </el-dialog>
-
-    <!-- 分配角色 -->
-    <el-dialog title="分配角色" :visible.sync="openRole" width="500px" append-to-body>
-      <el-form :model="form" label-width="80px">
-        <el-form-item label="用户名称">
-          <el-input v-model="form.username" :disabled="true" />
-        </el-form-item>
-        <el-form-item label="用户昵称">
-          <el-input v-model="form.nickname" :disabled="true" />
-        </el-form-item>
-        <el-form-item label="角色">
-          <el-select v-model="form.roleIds" multiple placeholder="请选择">
-            <el-option
-                v-for="item in roleOptions"
-                :key="parseInt(item.id)"
-                :label="item.name"
-                :value="parseInt(item.id)"
-            ></el-option>
-          </el-select>
-        </el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitRole">确 定</el-button>
-        <el-button @click="cancelRole">取 消</el-button>
-      </div>
-    </el-dialog>
-
-  </div>
-</template>
-
-<script>
-import { listUser, getUser, delUser, addUser, updateUser, exportUser, resetUserPwd, changeUserStatus, importTemplate } from "@/api/system/user";
-import { getToken } from "@/utils/auth";
-import Treeselect from "@riophae/vue-treeselect";
-import "@riophae/vue-treeselect/dist/vue-treeselect.css";
-
-import {listSimpleDepts} from "@/api/system/dept";
-import {listSimplePosts} from "@/api/system/post";
-
-import {SysCommonStatusEnum} from "@/utils/constants";
-import {DICT_TYPE, getDictDatas} from "@/utils/dict";
-import {assignUserRole, listUserRoles} from "@/api/system/permission";
-import {listSimpleRoles} from "@/api/system/role";
-
-export default {
-  name: "User",
-  components: { Treeselect },
-  data() {
-    return {
-      // 遮罩层
-      loading: true,
-      // 显示搜索条件
-      showSearch: true,
-      // 总条数
-      total: 0,
-      // 用户表格数据
-      userList: null,
-      // 弹出层标题
-      title: "",
-      // 部门树选项
-      deptOptions: undefined,
-      // 是否显示弹出层
-      open: false,
-      // 部门名称
-      deptName: undefined,
-      // 默认密码
-      initPassword: undefined,
-      // 日期范围
-      dateRange: [],
-      // 状态数据字典
-      statusOptions: [],
-      // 性别状态字典
-      sexOptions: [],
-      // 岗位选项
-      postOptions: [],
-      // 角色选项
-      roleOptions: [],
-      // 表单参数
-      form: {},
-      defaultProps: {
-        children: "children",
-        label: "name"
-      },
-      // 用户导入参数
-      upload: {
-        // 是否显示弹出层(用户导入)
-        open: false,
-        // 弹出层标题(用户导入)
-        title: "",
-        // 是否禁用上传
-        isUploading: false,
-        // 是否更新已经存在的用户数据
-        updateSupport: 0,
-        // 设置上传的请求头部
-        headers: { Authorization: "Bearer " + getToken() },
-        // 上传的地址
-        url: process.env.VUE_APP_BASE_API + '/api/' + "/system/user/import"
-      },
-      // 查询参数
-      queryParams: {
-        pageNo: 1,
-        pageSize: 10,
-        username: undefined,
-        mobile: undefined,
-        status: undefined,
-        deptId: undefined
-      },
-      // 表单校验
-      rules: {
-        username: [
-          { required: true, message: "用户名称不能为空", trigger: "blur" }
-        ],
-        nickname: [
-          { required: true, message: "用户昵称不能为空", trigger: "blur" }
-        ],
-        password: [
-          { required: true, message: "用户密码不能为空", trigger: "blur" }
-        ],
-        email: [
-          {
-            type: "email",
-            message: "'请输入正确的邮箱地址",
-            trigger: ["blur", "change"]
-          }
-        ],
-        mobile: [
-          {
-            pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
-            message: "请输入正确的手机号码",
-            trigger: "blur"
-          }
-        ]
-      },
-      // 是否显示弹出层(角色权限)
-      openRole: false,
-
-      // 枚举
-      SysCommonStatusEnum: SysCommonStatusEnum,
-      // 数据字典
-      statusDictDatas: getDictDatas(DICT_TYPE.SYS_COMMON_STATUS),
-      sexDictDatas: getDictDatas(DICT_TYPE.SYS_USER_SEX),
-    };
-  },
-  watch: {
-    // 根据名称筛选部门树
-    deptName(val) {
-      this.$refs.tree.filter(val);
-    }
-  },
-  created() {
-    this.getList();
-    this.getTreeselect();
-    this.getConfigKey("sys.user.initPassword").then(response => {
-      this.initPassword = response.msg;
-    });
-  },
-  methods: {
-    /** 查询用户列表 */
-    getList() {
-      this.loading = true;
-      listUser(this.addDateRange(this.queryParams, [
-        this.dateRange[0] ? this.dateRange[0] + ' 00:00:00' : undefined,
-        this.dateRange[1] ? this.dateRange[1] + ' 23:59:59' : undefined,
-      ])).then(response => {
-          this.userList = response.data.list;
-          this.total = response.data.total;
-          this.loading = false;
-        }
-      );
-    },
-    /** 查询部门下拉树结构 + 岗位下拉 */
-    getTreeselect() {
-      listSimpleDepts().then(response => {
-        // 处理 deptOptions 参数
-        this.deptOptions = [];
-        this.deptOptions.push(...this.handleTree(response.data, "id"));
-      });
-      listSimplePosts().then(response => {
-        // 处理 postOptions 参数
-        this.postOptions = [];
-        this.postOptions.push(...response.data);
-      });
-    },
-    // 筛选节点
-    filterNode(value, data) {
-      if (!value) return true;
-      return data.name.indexOf(value) !== -1;
-    },
-    // 节点单击事件
-    handleNodeClick(data) {
-      this.queryParams.deptId = data.id;
-      this.getList();
-    },
-    // 用户状态修改
-    handleStatusChange(row) {
-      let text = row.status === SysCommonStatusEnum.ENABLE ? "启用" : "停用";
-      this.$confirm('确认要"' + text + '""' + row.username + '"用户吗?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return changeUserStatus(row.id, row.status);
-        }).then(() => {
-          this.msgSuccess(text + "成功");
-        }).catch(function() {
-          row.status = row.status === SysCommonStatusEnum.ENABLE ? SysCommonStatusEnum.DISABLE
-              : SysCommonStatusEnum.ENABLE;
-        });
-    },
-    // 取消按钮
-    cancel() {
-      this.open = false;
-      this.reset();
-    },
-    // 取消按钮(角色权限)
-    cancelRole() {
-      this.openRole = false;
-      this.reset();
-    },
-    // 表单重置
-    reset() {
-      this.form = {
-        id: undefined,
-        deptId: undefined,
-        username: undefined,
-        nickname: undefined,
-        password: undefined,
-        mobile: undefined,
-        email: undefined,
-        sex: undefined,
-        status: "0",
-        remark: undefined,
-        postIds: [],
-        roleIds: []
-      };
-      this.resetForm("form");
-    },
-    /** 搜索按钮操作 */
-    handleQuery() {
-      this.queryParams.pageNo = 1;
-      this.getList();
-    },
-    /** 重置按钮操作 */
-    resetQuery() {
-      this.dateRange = [];
-      this.resetForm("queryForm");
-      this.handleQuery();
-    },
-    /** 新增按钮操作 */
-    handleAdd() {
-      this.reset();
-      // 获得下拉数据
-      this.getTreeselect();
-      // 打开表单,并设置初始化
-      this.open = true;
-      this.title = "添加用户";
-      this.form.password = this.initPassword;
-    },
-    /** 修改按钮操作 */
-    handleUpdate(row) {
-      this.reset();
-      this.getTreeselect();
-      const id = row.id;
-      getUser(id).then(response => {
-        this.form = response.data;
-        this.open = true;
-        this.title = "修改用户";
-        this.form.password = "";
-      });
-    },
-    /** 重置密码按钮操作 */
-    handleResetPwd(row) {
-      this.$prompt('请输入"' + row.username + '"的新密码', "提示", {
-        confirmButtonText: "确定",
-        cancelButtonText: "取消"
-      }).then(({ value }) => {
-          resetUserPwd(row.id, value).then(response => {
-            this.msgSuccess("修改成功,新密码是:" + value);
-          });
-        }).catch(() => {});
-    },
-    /** 分配用户角色操作 */
-    handleRole(row) {
-      this.reset();
-      const id = row.id
-      // 处理了 form 的用户 username 和 nickname 的展示
-      this.form.id = id;
-      this.form.username = row.username;
-      this.form.nickname = row.nickname;
-      // 打开弹窗
-      this.openRole = true;
-      // 获得角色列表
-      listSimpleRoles().then(response => {
-        // 处理 roleOptions 参数
-        this.roleOptions = [];
-        this.roleOptions.push(...response.data);
-      });
-      // 获得角色拥有的菜单集合
-      listUserRoles(id).then(response => {
-        // 设置选中
-        this.form.roleIds = response.data;
-      })
-    },
-    /** 提交按钮 */
-    submitForm: function() {
-      this.$refs["form"].validate(valid => {
-        if (valid) {
-          if (this.form.id !== undefined) {
-            updateUser(this.form).then(response => {
-              this.msgSuccess("修改成功");
-              this.open = false;
-              this.getList();
-            });
-          } else {
-            addUser(this.form).then(response => {
-              this.msgSuccess("新增成功");
-              this.open = false;
-              this.getList();
-            });
-          }
-        }
-      });
-    },
-    /** 提交按钮(角色权限) */
-    submitRole: function() {
-      if (this.form.id !== undefined) {
-        assignUserRole({
-          userId: this.form.id,
-          roleIds: this.form.roleIds,
-        }).then(response => {
-          this.msgSuccess("分配角色成功");
-          this.openRole = false;
-          this.getList();
-        });
-      }
-    },
-    /** 删除按钮操作 */
-    handleDelete(row) {
-      const ids = row.id || this.ids;
-      this.$confirm('是否确认删除用户编号为"' + ids + '"的数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return delUser(ids);
-        }).then(() => {
-          this.getList();
-          this.msgSuccess("删除成功");
-        })
-    },
-    /** 导出按钮操作 */
-    handleExport() {
-      const queryParams = this.addDateRange(this.queryParams, [
-        this.dateRange[0] ? this.dateRange[0] + ' 00:00:00' : undefined,
-        this.dateRange[1] ? this.dateRange[1] + ' 23:59:59' : undefined,
-      ]);
-      this.$confirm('是否确认导出所有用户数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return exportUser(queryParams);
-        }).then(response => {
-          this.downloadExcel(response, '用户数据.xls');
-        })
-    },
-    /** 导入按钮操作 */
-    handleImport() {
-      this.upload.title = "用户导入";
-      this.upload.open = true;
-    },
-    /** 下载模板操作 */
-    importTemplate() {
-      importTemplate().then(response => {
-        this.downloadExcel(response, '用户导入模板.xls');
-      });
-    },
-    // 文件上传中处理
-    handleFileUploadProgress(event, file, fileList) {
-      this.upload.isUploading = true;
-    },
-    // 文件上传成功处理
-    handleFileSuccess(response, file, fileList) {
-      this.upload.open = false;
-      this.upload.isUploading = false;
-      this.$refs.upload.clearFiles();
-      // 拼接提示语
-      let data = response.data;
-      let text = '创建成功数量:' + data.createUsernames.length;
-      for (const username of data.createUsernames) {
-        text += '<br />&nbsp;&nbsp;&nbsp;&nbsp;' + username;
-      }
-      text += '<br />更新成功数量:' + data.updateUsernames.length;
-      for (const username of data.updateUsernames) {
-        text += '<br />&nbsp;&nbsp;&nbsp;&nbsp;' + username;
-      }
-      text += '<br />更新失败数量:' + Object.keys(data.failureUsernames).length;
-      for (const username in data.failureUsernames) {
-        text += '<br />&nbsp;&nbsp;&nbsp;&nbsp;' + username + ':' + data.failureUsernames[username];
-      }
-      this.$alert(text, "导入结果", { dangerouslyUseHTMLString: true });
-      this.getList();
-    },
-    // 提交上传文件
-    submitFileForm() {
-      this.$refs.upload.submit();
-    },
-    // 格式化部门的下拉框
-    normalizer(node) {
-      return {
-        id: node.id,
-        label: node.name,
-        children: node.children
-      }
-    }
-  }
-};
-</script>
+<template>
+  <div class="app-container">
+    <el-row :gutter="20">
+      <!--部门数据-->
+      <el-col :span="4" :xs="24">
+        <div class="head-container">
+          <el-input
+            v-model="deptName"
+            placeholder="请输入部门名称"
+            clearable
+            size="small"
+            prefix-icon="el-icon-search"
+            style="margin-bottom: 20px"
+          />
+        </div>
+        <div class="head-container">
+          <el-tree
+            :data="deptOptions"
+            :props="defaultProps"
+            :expand-on-click-node="false"
+            :filter-node-method="filterNode"
+            ref="tree"
+            default-expand-all
+            @node-click="handleNodeClick"
+          />
+        </div>
+      </el-col>
+      <!--用户数据-->
+      <el-col :span="20" :xs="24">
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+          <el-form-item label="用户名称" prop="username">
+            <el-input
+              v-model="queryParams.username"
+              placeholder="请输入用户名称"
+              clearable
+              size="small"
+              style="width: 240px"
+              @keyup.enter.native="handleQuery"
+            />
+          </el-form-item>
+          <el-form-item label="手机号码" prop="mobile">
+            <el-input
+              v-model="queryParams.mobile"
+              placeholder="请输入手机号码"
+              clearable
+              size="small"
+              style="width: 240px"
+              @keyup.enter.native="handleQuery"
+            />
+          </el-form-item>
+          <el-form-item label="状态" prop="status">
+            <el-select
+              v-model="queryParams.status"
+              placeholder="用户状态"
+              clearable
+              size="small"
+              style="width: 240px"
+            >
+              <el-option
+                  v-for="dict in statusDictDatas"
+                  :key="parseInt(dict.value)"
+                  :label="dict.label"
+                  :value="parseInt(dict.value)"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="创建时间">
+            <el-date-picker
+              v-model="dateRange"
+              size="small"
+              style="width: 240px"
+              value-format="yyyy-MM-dd"
+              type="daterange"
+              range-separator="-"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+            ></el-date-picker>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+            <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+          </el-form-item>
+        </el-form>
+
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button
+              type="primary"
+              icon="el-icon-plus"
+              size="mini"
+              @click="handleAdd"
+              v-hasPermi="['system:user:add']"
+            >新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button
+              type="info"
+              icon="el-icon-upload2"
+              size="mini"
+              @click="handleImport"
+              v-hasPermi="['system:user:import']"
+            >导入</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button
+              type="warning"
+              icon="el-icon-download"
+              size="mini"
+              @click="handleExport"
+              v-hasPermi="['system:user:export']"
+            >导出</el-button>
+          </el-col>
+          <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+
+        <el-table v-loading="loading" :data="userList">
+          <el-table-column label="用户编号" align="center" prop="id" />
+          <el-table-column label="用户名称" align="center" prop="username" :show-overflow-tooltip="true" />
+          <el-table-column label="用户昵称" align="center" prop="nickname" :show-overflow-tooltip="true" />
+          <el-table-column label="部门" align="center" prop="dept.name" :show-overflow-tooltip="true" />
+          <el-table-column label="手机号码" align="center" prop="mobile" width="120" />
+          <el-table-column label="状态" align="center">
+            <template slot-scope="scope">
+              <el-switch
+                v-model="scope.row.status"
+                :active-value="0"
+                :inactive-value="1"
+                @change="handleStatusChange(scope.row)"
+              ></el-switch>
+            </template>
+          </el-table-column>
+          <el-table-column label="创建时间" align="center" prop="createTime" width="160">
+            <template slot-scope="scope">
+              <span>{{ parseTime(scope.row.createTime) }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="操作"
+            align="center"
+            width="160"
+            class-name="small-padding fixed-width"
+          >
+            <template slot-scope="scope">
+              <el-button
+                size="large"
+                type="text"
+                icon="el-icon-edit"
+                @click="handleUpdate(scope.row)"
+                v-hasPermi="['system:role:edit']"
+              >修改</el-button>
+              <el-dropdown  @command="(command) => handleCommand(command, scope.$index, scope.row)">
+                    <span class="el-dropdown-link">
+                      更多操作<i class="el-icon-arrow-down el-icon--right"></i>
+                    </span>
+                <el-dropdown-menu slot="dropdown">
+                  <el-dropdown-item
+                    command="handleDelete"
+                    v-if="scope.row.id !== 1"
+                    size="mini"
+                    type="text"
+                    icon="el-icon-delete"
+                    v-hasPermi="['system:user:remove']"
+                  >删除</el-dropdown-item>
+                  <el-dropdown-item
+                    command="handleResetPwd"
+                    size="mini"
+                    type="text"
+                    icon="el-icon-key"
+                    v-hasPermi="['system:user:resetPwd']"
+                  >重置</el-dropdown-item>
+                  <el-dropdown-item
+                    command="handleRole"
+                    size="mini"
+                    type="text"
+                    icon="el-icon-circle-check"
+                    v-hasPermi="['system:permission:assign-user-role']"
+                  >分配角色</el-dropdown-item>
+                </el-dropdown-menu>
+              </el-dropdown>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <pagination
+          v-show="total>0"
+          :total="total"
+          :page.sync="queryParams.pageNo"
+          :limit.sync="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </el-col>
+    </el-row>
+
+    <!-- 添加或修改参数配置对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="用户昵称" prop="nickname">
+              <el-input v-model="form.nickname" placeholder="请输入用户昵称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="归属部门" prop="deptId">
+              <treeselect v-model="form.deptId" :options="deptOptions" :show-count="true"
+                          placeholder="请选择归属部门" :normalizer="normalizer"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="手机号码" prop="mobile">
+              <el-input v-model="form.mobile" placeholder="请输入手机号码" maxlength="11" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="邮箱" prop="email">
+              <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item v-if="form.id === undefined" label="用户名称" prop="username">
+              <el-input v-model="form.username" placeholder="请输入用户名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item v-if="form.id === undefined" label="用户密码" prop="password">
+              <el-input v-model="form.password" placeholder="请输入用户密码" type="password" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="用户性别">
+              <el-select v-model="form.sex" placeholder="请选择">
+                <el-option
+                  v-for="dict in sexDictDatas"
+                  :key="parseInt(dict.value)"
+                  :label="dict.label"
+                  :value="parseInt(dict.value)"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="岗位">
+              <el-select v-model="form.postIds" multiple placeholder="请选择">
+                <el-option
+                    v-for="item in postOptions"
+                    :key="item.id"
+                    :label="item.name"
+                    :value="item.id"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="备注">
+              <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 用户导入对话框 -->
+    <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
+      <el-upload
+        ref="upload"
+        :limit="1"
+        accept=".xlsx, .xls"
+        :headers="upload.headers"
+        :action="upload.url + '?updateSupport=' + upload.updateSupport"
+        :disabled="upload.isUploading"
+        :on-progress="handleFileUploadProgress"
+        :on-success="handleFileSuccess"
+        :auto-upload="false"
+        drag
+      >
+        <i class="el-icon-upload"></i>
+        <div class="el-upload__text">
+          将文件拖到此处,或
+          <em>点击上传</em>
+        </div>
+        <div class="el-upload__tip" slot="tip">
+          <el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据
+          <el-link type="info" style="font-size:12px" @click="importTemplate">下载模板</el-link>
+        </div>
+        <div class="el-upload__tip" style="color:red" slot="tip">提示:仅允许导入“xls”或“xlsx”格式文件!</div>
+      </el-upload>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitFileForm">确 定</el-button>
+        <el-button @click="upload.open = false">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 分配角色 -->
+    <el-dialog title="分配角色" :visible.sync="openRole" width="500px" append-to-body>
+      <el-form :model="form" label-width="80px">
+        <el-form-item label="用户名称">
+          <el-input v-model="form.username" :disabled="true" />
+        </el-form-item>
+        <el-form-item label="用户昵称">
+          <el-input v-model="form.nickname" :disabled="true" />
+        </el-form-item>
+        <el-form-item label="角色">
+          <el-select v-model="form.roleIds" multiple placeholder="请选择">
+            <el-option
+                v-for="item in roleOptions"
+                :key="parseInt(item.id)"
+                :label="item.name"
+                :value="parseInt(item.id)"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitRole">确 定</el-button>
+        <el-button @click="cancelRole">取 消</el-button>
+      </div>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import { listUser, getUser, delUser, addUser, updateUser, exportUser, resetUserPwd, changeUserStatus, importTemplate } from "@/api/system/user";
+import { getToken } from "@/utils/auth";
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+
+import {listSimpleDepts} from "@/api/system/dept";
+import {listSimplePosts} from "@/api/system/post";
+
+import {SysCommonStatusEnum} from "@/utils/constants";
+import {DICT_TYPE, getDictDatas} from "@/utils/dict";
+import {assignUserRole, listUserRoles} from "@/api/system/permission";
+import {listSimpleRoles} from "@/api/system/role";
+
+export default {
+  name: "User",
+  components: { Treeselect },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 用户表格数据
+      userList: null,
+      // 弹出层标题
+      title: "",
+      // 部门树选项
+      deptOptions: undefined,
+      // 是否显示弹出层
+      open: false,
+      // 部门名称
+      deptName: undefined,
+      // 默认密码
+      initPassword: undefined,
+      // 日期范围
+      dateRange: [],
+      // 状态数据字典
+      statusOptions: [],
+      // 性别状态字典
+      sexOptions: [],
+      // 岗位选项
+      postOptions: [],
+      // 角色选项
+      roleOptions: [],
+      // 表单参数
+      form: {},
+      defaultProps: {
+        children: "children",
+        label: "name"
+      },
+      // 用户导入参数
+      upload: {
+        // 是否显示弹出层(用户导入)
+        open: false,
+        // 弹出层标题(用户导入)
+        title: "",
+        // 是否禁用上传
+        isUploading: false,
+        // 是否更新已经存在的用户数据
+        updateSupport: 0,
+        // 设置上传的请求头部
+        headers: { Authorization: "Bearer " + getToken() },
+        // 上传的地址
+        url: process.env.VUE_APP_BASE_API + '/api/' + "/system/user/import"
+      },
+      // 查询参数
+      queryParams: {
+        pageNo: 1,
+        pageSize: 10,
+        username: undefined,
+        mobile: undefined,
+        status: undefined,
+        deptId: undefined
+      },
+      // 表单校验
+      rules: {
+        username: [
+          { required: true, message: "用户名称不能为空", trigger: "blur" }
+        ],
+        nickname: [
+          { required: true, message: "用户昵称不能为空", trigger: "blur" }
+        ],
+        password: [
+          { required: true, message: "用户密码不能为空", trigger: "blur" }
+        ],
+        email: [
+          {
+            type: "email",
+            message: "'请输入正确的邮箱地址",
+            trigger: ["blur", "change"]
+          }
+        ],
+        mobile: [
+          {
+            pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
+            message: "请输入正确的手机号码",
+            trigger: "blur"
+          }
+        ]
+      },
+      // 是否显示弹出层(角色权限)
+      openRole: false,
+
+      // 枚举
+      SysCommonStatusEnum: SysCommonStatusEnum,
+      // 数据字典
+      statusDictDatas: getDictDatas(DICT_TYPE.SYS_COMMON_STATUS),
+      sexDictDatas: getDictDatas(DICT_TYPE.SYS_USER_SEX),
+    };
+  },
+  watch: {
+    // 根据名称筛选部门树
+    deptName(val) {
+      this.$refs.tree.filter(val);
+    }
+  },
+  created() {
+    this.getList();
+    this.getTreeselect();
+    this.getConfigKey("sys.user.initPassword").then(response => {
+      this.initPassword = response.msg;
+    });
+  },
+  methods: {
+    // 更多操作
+    handleCommand(command, index, row) {
+      switch (command) {
+        case 'handleUpdate':
+          this.handleUpdate(row);//修改客户信息
+          break;
+        case 'handleDelete':
+          this.handleDelete(row);//红号变更
+          break;
+        case 'handleResetPwd':
+          this.handleResetPwd(row);
+          break;
+        case 'handleRole':
+          this.handleRole(row);
+          break;
+        default:
+          break;
+      }
+    },
+    /** 查询用户列表 */
+    getList() {
+      this.loading = true;
+      listUser(this.addDateRange(this.queryParams, [
+        this.dateRange[0] ? this.dateRange[0] + ' 00:00:00' : undefined,
+        this.dateRange[1] ? this.dateRange[1] + ' 23:59:59' : undefined,
+      ])).then(response => {
+          this.userList = response.data.list;
+          this.total = response.data.total;
+          this.loading = false;
+        }
+      );
+    },
+    /** 查询部门下拉树结构 + 岗位下拉 */
+    getTreeselect() {
+      listSimpleDepts().then(response => {
+        // 处理 deptOptions 参数
+        this.deptOptions = [];
+        this.deptOptions.push(...this.handleTree(response.data, "id"));
+      });
+      listSimplePosts().then(response => {
+        // 处理 postOptions 参数
+        this.postOptions = [];
+        this.postOptions.push(...response.data);
+      });
+    },
+    // 筛选节点
+    filterNode(value, data) {
+      if (!value) return true;
+      return data.name.indexOf(value) !== -1;
+    },
+    // 节点单击事件
+    handleNodeClick(data) {
+      this.queryParams.deptId = data.id;
+      this.getList();
+    },
+    // 用户状态修改
+    handleStatusChange(row) {
+      let text = row.status === SysCommonStatusEnum.ENABLE ? "启用" : "停用";
+      this.$confirm('确认要"' + text + '""' + row.username + '"用户吗?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return changeUserStatus(row.id, row.status);
+        }).then(() => {
+          this.msgSuccess(text + "成功");
+        }).catch(function() {
+          row.status = row.status === SysCommonStatusEnum.ENABLE ? SysCommonStatusEnum.DISABLE
+              : SysCommonStatusEnum.ENABLE;
+        });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 取消按钮(角色权限)
+    cancelRole() {
+      this.openRole = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: undefined,
+        deptId: undefined,
+        username: undefined,
+        nickname: undefined,
+        password: undefined,
+        mobile: undefined,
+        email: undefined,
+        sex: undefined,
+        status: "0",
+        remark: undefined,
+        postIds: [],
+        roleIds: []
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRange = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      // 获得下拉数据
+      this.getTreeselect();
+      // 打开表单,并设置初始化
+      this.open = true;
+      this.title = "添加用户";
+      this.form.password = this.initPassword;
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      this.getTreeselect();
+      const id = row.id;
+      getUser(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改用户";
+        this.form.password = "";
+      });
+    },
+    /** 重置密码按钮操作 */
+    handleResetPwd(row) {
+      this.$prompt('请输入"' + row.username + '"的新密码', "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+      }).then(({ value }) => {
+          resetUserPwd(row.id, value).then(response => {
+            this.msgSuccess("修改成功,新密码是:" + value);
+          });
+        }).catch(() => {});
+    },
+    /** 分配用户角色操作 */
+    handleRole(row) {
+      this.reset();
+      const id = row.id
+      // 处理了 form 的用户 username 和 nickname 的展示
+      this.form.id = id;
+      this.form.username = row.username;
+      this.form.nickname = row.nickname;
+      // 打开弹窗
+      this.openRole = true;
+      // 获得角色列表
+      listSimpleRoles().then(response => {
+        // 处理 roleOptions 参数
+        this.roleOptions = [];
+        this.roleOptions.push(...response.data);
+      });
+      // 获得角色拥有的菜单集合
+      listUserRoles(id).then(response => {
+        // 设置选中
+        this.form.roleIds = response.data;
+      })
+    },
+    /** 提交按钮 */
+    submitForm: function() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id !== undefined) {
+            updateUser(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addUser(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 提交按钮(角色权限) */
+    submitRole: function() {
+      if (this.form.id !== undefined) {
+        assignUserRole({
+          userId: this.form.id,
+          roleIds: this.form.roleIds,
+        }).then(response => {
+          this.msgSuccess("分配角色成功");
+          this.openRole = false;
+          this.getList();
+        });
+      }
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除用户编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delUser(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        })
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.addDateRange(this.queryParams, [
+        this.dateRange[0] ? this.dateRange[0] + ' 00:00:00' : undefined,
+        this.dateRange[1] ? this.dateRange[1] + ' 23:59:59' : undefined,
+      ]);
+      this.$confirm('是否确认导出所有用户数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return exportUser(queryParams);
+        }).then(response => {
+          this.downloadExcel(response, '用户数据.xls');
+        })
+    },
+    /** 导入按钮操作 */
+    handleImport() {
+      this.upload.title = "用户导入";
+      this.upload.open = true;
+    },
+    /** 下载模板操作 */
+    importTemplate() {
+      importTemplate().then(response => {
+        this.downloadExcel(response, '用户导入模板.xls');
+      });
+    },
+    // 文件上传中处理
+    handleFileUploadProgress(event, file, fileList) {
+      this.upload.isUploading = true;
+    },
+    // 文件上传成功处理
+    handleFileSuccess(response, file, fileList) {
+      this.upload.open = false;
+      this.upload.isUploading = false;
+      this.$refs.upload.clearFiles();
+      // 拼接提示语
+      let data = response.data;
+      let text = '创建成功数量:' + data.createUsernames.length;
+      for (const username of data.createUsernames) {
+        text += '<br />&nbsp;&nbsp;&nbsp;&nbsp;' + username;
+      }
+      text += '<br />更新成功数量:' + data.updateUsernames.length;
+      for (const username of data.updateUsernames) {
+        text += '<br />&nbsp;&nbsp;&nbsp;&nbsp;' + username;
+      }
+      text += '<br />更新失败数量:' + Object.keys(data.failureUsernames).length;
+      for (const username in data.failureUsernames) {
+        text += '<br />&nbsp;&nbsp;&nbsp;&nbsp;' + username + ':' + data.failureUsernames[username];
+      }
+      this.$alert(text, "导入结果", { dangerouslyUseHTMLString: true });
+      this.getList();
+    },
+    // 提交上传文件
+    submitFileForm() {
+      this.$refs.upload.submit();
+    },
+    // 格式化部门的下拉框
+    normalizer(node) {
+      return {
+        id: node.id,
+        label: node.name,
+        children: node.children
+      }
+    }
+  }
+};
+</script>
+<style>
+  .el-dropdown-link {
+    cursor: pointer;
+    color: #1890ff;
+    margin-left: 5px;
+  }
+  .el-icon-arrow-down {
+    font-size: 14px;
+  }
+</style>

文件差異過大導致無法顯示
+ 23 - 78
sql/ruoyi-vue-pro.sql


+ 2 - 2
src/main/java/cn/iocoder/dashboard/framework/file/config/FileProperties.java

@@ -1,6 +1,6 @@
 package cn.iocoder.dashboard.framework.file.config;
 
-import cn.iocoder.dashboard.modules.system.controller.common.SysFileController;
+import cn.iocoder.dashboard.modules.infra.controller.file.InfFileController;
 import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.validation.annotation.Validated;
@@ -13,7 +13,7 @@ import javax.validation.constraints.NotNull;
 public class FileProperties {
 
     /**
-     * 对应 {@link SysFileController#}
+     * 对应 {@link InfFileController#}
      */
     @NotNull(message = "基础文件路径不能为空")
     private String basePath;

+ 1 - 1
src/main/java/cn/iocoder/dashboard/framework/security/config/SecurityConfiguration.java

@@ -134,7 +134,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
                     // 静态资源,可匿名访问
                     .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
                     // 文件的获取接口,可匿名访问
-                    .antMatchers(webProperties.getApiPrefix() + "/system/file/get/**").anonymous()
+                    .antMatchers(webProperties.getApiPrefix() + "/infra/file/get/**").anonymous()
                     // Swagger 接口文档
                     .antMatchers("/swagger-ui.html").anonymous()
                     .antMatchers("/swagger-resources/**").anonymous()

+ 36 - 11
src/main/java/cn/iocoder/dashboard/modules/system/controller/common/SysFileController.java → src/main/java/cn/iocoder/dashboard/modules/infra/controller/file/InfFileController.java

@@ -1,9 +1,13 @@
-package cn.iocoder.dashboard.modules.system.controller.common;
+package cn.iocoder.dashboard.modules.infra.controller.file;
 
 import cn.hutool.core.io.IoUtil;
 import cn.iocoder.dashboard.common.pojo.CommonResult;
-import cn.iocoder.dashboard.modules.system.dal.dataobject.common.SysFileDO;
-import cn.iocoder.dashboard.modules.system.service.common.SysFileService;
+import cn.iocoder.dashboard.common.pojo.PageResult;
+import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFilePageReqVO;
+import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFileRespVO;
+import cn.iocoder.dashboard.modules.infra.convert.file.InfFileConvert;
+import cn.iocoder.dashboard.modules.infra.dal.dataobject.file.InfFileDO;
+import cn.iocoder.dashboard.modules.infra.service.file.InfFileService;
 import cn.iocoder.dashboard.util.servlet.ServletUtils;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiImplicitParam;
@@ -11,40 +15,53 @@ import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
 import java.io.IOException;
 
 import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
 
 @Api(tags = "文件存储")
 @RestController
-@RequestMapping("/system/file")
+@RequestMapping("/infra/file")
+@Validated
 @Slf4j
-public class SysFileController {
+public class InfFileController {
 
     @Resource
-    private SysFileService fileService;
+    private InfFileService fileService;
 
+    @PostMapping("/upload")
     @ApiOperation("上传文件")
     @ApiImplicitParams({
-            @ApiImplicitParam(name = "path", value = "文件附件", required = true, dataTypeClass = MultipartFile.class),
-            @ApiImplicitParam(name = "path", value = "文件路径", required = true, example = "yudaoyuanma.png", dataTypeClass = Long.class)
+            @ApiImplicitParam(name = "file", value = "文件附件", required = true, dataTypeClass = MultipartFile.class),
+            @ApiImplicitParam(name = "path", value = "文件路径", required = false, example = "yudaoyuanma.png", dataTypeClass = String.class)
     })
-    @PostMapping("/upload")
     public CommonResult<String> uploadFile(@RequestParam("file") MultipartFile file,
                                            @RequestParam("path") String path) throws IOException {
         return success(fileService.createFile(path, IoUtil.readBytes(file.getInputStream())));
     }
 
+    @DeleteMapping("/delete")
+    @ApiOperation("删除文件")
+    @ApiImplicitParam(name = "id", value = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:file:delete')")
+    public CommonResult<Boolean> deleteFile(@RequestParam("id") String id) {
+        fileService.deleteFile(id);
+        return success(true);
+    }
+
+    @GetMapping("/get/{path}")
     @ApiOperation("下载文件")
     @ApiImplicitParam(name = "path", value = "文件附件", required = true, dataTypeClass = MultipartFile.class)
-    @GetMapping("/get/{path}")
     public void getFile(HttpServletResponse response, @PathVariable("path") String path) throws IOException {
-        SysFileDO file = fileService.getFile(path);
+        InfFileDO file = fileService.getFile(path);
         if (file == null) {
             log.warn("[getFile][path({}) 文件不存在]", path);
             response.setStatus(HttpStatus.NOT_FOUND.value());
@@ -53,4 +70,12 @@ public class SysFileController {
         ServletUtils.writeAttachment(response, path, file.getContent());
     }
 
+    @GetMapping("/page")
+    @ApiOperation("获得文件分页")
+    @PreAuthorize("@ss.hasPermission('infra:file:query')")
+    public CommonResult<PageResult<InfFileRespVO>> getFilePage(@Valid InfFilePageReqVO pageVO) {
+        PageResult<InfFileDO> pageResult = fileService.getFilePage(pageVO);
+        return success(InfFileConvert.INSTANCE.convertPage(pageResult));
+    }
+
 }

+ 35 - 0
src/main/java/cn/iocoder/dashboard/modules/infra/controller/file/vo/InfFilePageReqVO.java

@@ -0,0 +1,35 @@
+package cn.iocoder.dashboard.modules.infra.controller.file.vo;
+
+import cn.iocoder.dashboard.common.pojo.PageParam;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.util.Date;
+
+import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@ApiModel("文件分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class InfFilePageReqVO extends PageParam {
+
+    @ApiModelProperty(value = "文件路径", example = "yudao", notes = "模糊匹配")
+    private String id;
+
+    @ApiModelProperty(value = "文件类型", example = "jpg", notes = "模糊匹配")
+    private String type;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "开始创建时间")
+    private Date beginCreateTime;
+
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @ApiModelProperty(value = "结束创建时间")
+    private Date endCreateTime;
+
+}

+ 22 - 0
src/main/java/cn/iocoder/dashboard/modules/infra/controller/file/vo/InfFileRespVO.java

@@ -0,0 +1,22 @@
+package cn.iocoder.dashboard.modules.infra.controller.file.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@ApiModel(value = "文件 Response VO", description = "不返回 content 字段,太大")
+@Data
+public class InfFileRespVO {
+
+    @ApiModelProperty(value = "文件路径", required = true, example = "yudao.jpg")
+    private String id;
+
+    @ApiModelProperty(value = "文件类型", required = true, example = "jpg")
+    private String type;
+
+    @ApiModelProperty(value = "创建时间", required = true)
+    private Date createTime;
+
+}

+ 18 - 0
src/main/java/cn/iocoder/dashboard/modules/infra/convert/file/InfFileConvert.java

@@ -0,0 +1,18 @@
+package cn.iocoder.dashboard.modules.infra.convert.file;
+
+import cn.iocoder.dashboard.common.pojo.PageResult;
+import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFileRespVO;
+import cn.iocoder.dashboard.modules.infra.dal.dataobject.file.InfFileDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface InfFileConvert {
+
+    InfFileConvert INSTANCE = Mappers.getMapper(InfFileConvert.class);
+
+    InfFileRespVO convert(InfFileDO bean);
+
+    PageResult<InfFileRespVO> convertPage(PageResult<InfFileDO> page);
+
+}

+ 43 - 0
src/main/java/cn/iocoder/dashboard/modules/infra/dal/dataobject/file/InfFileDO.java

@@ -0,0 +1,43 @@
+package cn.iocoder.dashboard.modules.infra.dal.dataobject.file;
+
+import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.io.InputStream;
+
+/**
+ * 文件表
+ *
+ * @author 芋道源码
+ */
+@Data
+@TableName("inf_file")
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfFileDO extends BaseDO {
+
+    /**
+     * 文件路径
+     */
+    @TableId(type = IdType.INPUT)
+    private String id;
+    /**
+     * 文件类型
+     *
+     * 通过 {@link cn.hutool.core.io.FileTypeUtil#getType(InputStream)} 获取
+     */
+    @TableField(value = "`type`")
+    private String type;
+    /**
+     * 文件内容
+     */
+    private byte[] content;
+
+}

+ 25 - 0
src/main/java/cn/iocoder/dashboard/modules/infra/dal/mysql/file/InfFileMapper.java

@@ -0,0 +1,25 @@
+package cn.iocoder.dashboard.modules.infra.dal.mysql.file;
+
+import cn.iocoder.dashboard.common.pojo.PageResult;
+import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX;
+import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFilePageReqVO;
+import cn.iocoder.dashboard.modules.infra.dal.dataobject.file.InfFileDO;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface InfFileMapper extends BaseMapperX<InfFileDO> {
+
+    default Integer selectCountById(String id) {
+        return selectCount("id", id);
+    }
+
+    default PageResult<InfFileDO> selectPage(InfFilePageReqVO reqVO) {
+        return selectPage(reqVO, new QueryWrapperX<InfFileDO>()
+                .likeIfPresent("id", reqVO.getId())
+                .likeIfPresent("type", reqVO.getType())
+                .betweenIfPresent("create_time", reqVO.getBeginCreateTime(), reqVO.getEndCreateTime())
+                .orderByDesc("create_time"));
+    }
+
+}

+ 3 - 0
src/main/java/cn/iocoder/dashboard/modules/infra/enums/InfErrorCodeConstants.java

@@ -27,4 +27,7 @@ public interface InfErrorCodeConstants {
     ErrorCode API_ERROR_LOG_NOT_FOUND = new ErrorCode(1001002000, "API 错误日志不存在");
     ErrorCode API_ERROR_LOG_PROCESSED = new ErrorCode(1001002001, "API 错误日志已处理");
 
+    // ========== 文件 1001003000 ==========
+    ErrorCode FILE_NOT_EXISTS = new ErrorCode(1001003000, "文件不存在");
+
 }

+ 46 - 0
src/main/java/cn/iocoder/dashboard/modules/infra/service/file/InfFileService.java

@@ -0,0 +1,46 @@
+package cn.iocoder.dashboard.modules.infra.service.file;
+
+import cn.iocoder.dashboard.common.pojo.PageResult;
+import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFilePageReqVO;
+import cn.iocoder.dashboard.modules.infra.dal.dataobject.file.InfFileDO;
+
+/**
+ * 文件 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface InfFileService {
+
+    /**
+     * 保存文件,并返回文件的访问路径
+     *
+     * @param path 文件路径
+     * @param content 文件内容
+     * @return 文件路径
+     */
+    String createFile(String path, byte[] content);
+
+    /**
+     * 删除文件
+     *
+     * @param id 编号
+     */
+    void deleteFile(String id);
+
+    /**
+     * 获得文件
+     *
+     * @param path 文件路径
+     * @return 文件
+     */
+    InfFileDO getFile(String path);
+
+    /**
+     * 获得文件分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 文件分页
+     */
+    PageResult<InfFileDO> getFilePage(InfFilePageReqVO pageReqVO);
+
+}

+ 72 - 0
src/main/java/cn/iocoder/dashboard/modules/infra/service/file/impl/InfFileServiceImpl.java

@@ -0,0 +1,72 @@
+package cn.iocoder.dashboard.modules.infra.service.file.impl;
+
+import cn.hutool.core.io.FileTypeUtil;
+import cn.iocoder.dashboard.common.pojo.PageResult;
+import cn.iocoder.dashboard.framework.file.config.FileProperties;
+import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFilePageReqVO;
+import cn.iocoder.dashboard.modules.infra.dal.dataobject.file.InfFileDO;
+import cn.iocoder.dashboard.modules.infra.dal.mysql.file.InfFileMapper;
+import cn.iocoder.dashboard.modules.infra.service.file.InfFileService;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.io.ByteArrayInputStream;
+
+import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.FILE_NOT_EXISTS;
+import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.FILE_PATH_EXISTS;
+
+/**
+ * 文件 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+public class InfFileServiceImpl implements InfFileService {
+
+    @Resource
+    private InfFileMapper fileMapper;
+
+    @Resource
+    private FileProperties fileProperties;
+
+    @Override
+    public String createFile(String path, byte[] content) {
+        if (fileMapper.selectCountById(path) > 0) {
+            throw exception(FILE_PATH_EXISTS);
+        }
+        // 保存到数据库
+        InfFileDO file = new InfFileDO();
+        file.setId(path);
+        file.setType(FileTypeUtil.getType(new ByteArrayInputStream(content)));
+        file.setContent(content);
+        fileMapper.insert(file);
+        // 拼接路径返回
+        return fileProperties.getBasePath() + path;
+    }
+
+    @Override
+    public void deleteFile(String id) {
+        // 校验存在
+        this.validateFileExists(id);
+        // 更新
+        fileMapper.deleteById(id);
+    }
+
+    private void validateFileExists(String id) {
+        if (fileMapper.selectById(id) == null) {
+            throw exception(FILE_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public InfFileDO getFile(String path) {
+        return fileMapper.selectById(path);
+    }
+
+    @Override
+    public PageResult<InfFileDO> getFilePage(InfFilePageReqVO pageReqVO) {
+        return fileMapper.selectPage(pageReqVO);
+    }
+
+}

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/controller/common/SysCaptchaController.java

@@ -21,8 +21,8 @@ public class SysCaptchaController {
     @Resource
     private SysCaptchaService captchaService;
 
-    @ApiOperation("生成图片验证码")
     @GetMapping("/get-image")
+    @ApiOperation("生成图片验证码")
     public CommonResult<SysCaptchaImageRespVO> getCaptchaImage() {
         return success(captchaService.getCaptchaImage());
     }

+ 0 - 30
src/main/java/cn/iocoder/dashboard/modules/system/dal/dataobject/common/SysFileDO.java

@@ -1,30 +0,0 @@
-package cn.iocoder.dashboard.modules.system.dal.dataobject.common;
-
-import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-
-/**
- * 文件表
- *
- * @author 芋道源码
- */
-@Data
-@TableName("sys_file")
-@EqualsAndHashCode(callSuper = true)
-public class SysFileDO extends BaseDO {
-
-    /**
-     * 文件路径
-     */
-    @TableId(type = IdType.INPUT)
-    private String id;
-    /**
-     * 文件内容
-     */
-    private byte[] content;
-
-}

+ 0 - 15
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/common/SysFileMapper.java

@@ -1,15 +0,0 @@
-package cn.iocoder.dashboard.modules.system.dal.mysql.common;
-
-import cn.iocoder.dashboard.modules.system.dal.dataobject.common.SysFileDO;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
-
-@Mapper
-public interface SysFileMapper extends BaseMapper<SysFileDO> {
-
-    default Integer selectCountById(String id) {
-        return selectCount(new QueryWrapper<SysFileDO>().eq("id", id));
-    }
-
-}

+ 0 - 29
src/main/java/cn/iocoder/dashboard/modules/system/service/common/SysFileService.java

@@ -1,29 +0,0 @@
-package cn.iocoder.dashboard.modules.system.service.common;
-
-import cn.iocoder.dashboard.modules.system.dal.dataobject.common.SysFileDO;
-
-/**
- * 文件 Service 接口
- *
- * @author 芋道源码
- */
-public interface SysFileService {
-
-    /**
-     * 保存文件,并返回文件的访问路径
-     *
-     * @param path 文件路径
-     * @param content 文件内容
-     * @return 文件路径
-     */
-    String createFile(String path, byte[] content);
-
-    /**
-     * 获得文件
-     *
-     * @param path 文件路径
-     * @return 文件
-     */
-    SysFileDO getFile(String path);
-
-}

+ 0 - 47
src/main/java/cn/iocoder/dashboard/modules/system/service/common/impl/SysFileServiceImpl.java

@@ -1,47 +0,0 @@
-package cn.iocoder.dashboard.modules.system.service.common.impl;
-
-import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
-import cn.iocoder.dashboard.framework.file.config.FileProperties;
-import cn.iocoder.dashboard.modules.system.dal.dataobject.common.SysFileDO;
-import cn.iocoder.dashboard.modules.system.dal.mysql.common.SysFileMapper;
-import cn.iocoder.dashboard.modules.system.service.common.SysFileService;
-import org.springframework.stereotype.Service;
-
-import javax.annotation.Resource;
-
-import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.FILE_PATH_EXISTS;
-
-/**
- * 文件 Service 实现类
- *
- * @author 芋道源码
- */
-@Service
-public class SysFileServiceImpl implements SysFileService {
-
-    @Resource
-    private SysFileMapper fileMapper;
-
-    @Resource
-    private FileProperties fileProperties;
-
-    @Override
-    public String createFile(String path, byte[] content) {
-        if (fileMapper.selectCountById(path) > 0) {
-            throw ServiceExceptionUtil.exception(FILE_PATH_EXISTS);
-        }
-        // 保存到数据库
-        SysFileDO file = new SysFileDO();
-        file.setId(path);
-        file.setContent(content);
-        fileMapper.insert(file);
-        // 拼接路径返回
-        return fileProperties.getBasePath() + path;
-    }
-
-    @Override
-    public SysFileDO getFile(String path) {
-        return fileMapper.selectById(path);
-    }
-
-}

+ 2 - 1
src/main/java/cn/iocoder/dashboard/modules/tool/dal/dataobject/codegen/ToolCodegenColumnDO.java

@@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Builder;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
 
 /**
  * 代码生成 column 字段定义
@@ -17,7 +18,7 @@ import lombok.EqualsAndHashCode;
  */
 @TableName(value = "tool_codegen_column", autoResultMap = true)
 @Data
-@Builder
+@Accessors(chain = true)
 @EqualsAndHashCode(callSuper = true)
 public class ToolCodegenColumnDO extends BaseDO {
 

+ 2 - 1
src/main/java/cn/iocoder/dashboard/modules/tool/dal/dataobject/codegen/ToolCodegenTableDO.java

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Builder;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
 
 /**
  * 代码生成 table 表定义
@@ -15,7 +16,7 @@ import lombok.EqualsAndHashCode;
  */
 @TableName(value = "tool_codegen_table", autoResultMap = true)
 @Data
-@Builder
+@Accessors(chain = true)
 @EqualsAndHashCode(callSuper = true)
 public class ToolCodegenTableDO extends BaseDO {
 

+ 1 - 0
src/main/java/cn/iocoder/dashboard/modules/tool/service/codegen/impl/ToolCodegenBuilder.java

@@ -99,6 +99,7 @@ public class ToolCodegenBuilder {
             .put(String.class.getSimpleName(), Sets.newHashSet("tinytext", "text", "mediumtext", "longtext", // 长文本
                     "char", "varchar", "nvarchar", "varchar2")) // 短文本
             .put(Date.class.getSimpleName(), Sets.newHashSet("datetime", "time", "date", "timestamp"))
+            .put("byte[]", Sets.newHashSet("blob"))
             .build();
 
     static {

+ 1 - 1
src/main/resources/codegen/java/controller/controller.vm

@@ -55,7 +55,7 @@ public class ${table.className}Controller {
     @DeleteMapping("/delete")
     @ApiOperation("删除${table.classComment}")
     @ApiImplicitParam(name = "id", value = "编号", required = true)
-	@PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')")
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')")
     public CommonResult<Boolean> delete${simpleClassName}(@RequestParam("id") ${primaryColumn.javaType} id) {
         ${classNameVar}Service.delete${simpleClassName}(id);
         return success(true);

+ 2 - 1
src/main/resources/codegen/java/service/serviceImpl.vm

@@ -16,6 +16,7 @@ import ${basePackage}.modules.${table.moduleName}.service.${table.businessName}.
 
 import ${ServiceExceptionUtilClassName};
 
+import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
 import static ${basePackage}.modules.${table.moduleName}.enums.${simpleModuleName_upperFirst}ErrorCodeConstants.*;
 
 /**
@@ -58,7 +59,7 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
 
     private void validate${simpleClassName}Exists(${primaryColumn.javaType} id) {
         if (${classNameVar}Mapper.selectById(id) == null) {
-            throw ServiceExceptionUtil.exception(${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS);
+            throw exception(${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS);
         }
     }
 

+ 7 - 7
src/main/resources/codegen/java/test/serviceTest.vm

@@ -1,13 +1,11 @@
 package ${basePackage}.modules.${table.moduleName}.service.${table.businessName};
 
-import ${basePackage}.BaseSpringBootUnitTest;
-
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
 
 import javax.annotation.Resource;
 
-import cn.iocoder.dashboard.BaseSpringBootUnitTest;
+import ${basePackage}.BaseDbUnitTest;
 import ${basePackage}.modules.${table.moduleName}.service.${table.businessName}.impl.${table.className}ServiceImpl;
 import ${basePackage}.modules.${table.moduleName}.controller.${table.businessName}.vo.*;
 import ${basePackage}.modules.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO;
@@ -16,6 +14,7 @@ import ${basePackage}.util.object.ObjectUtils;
 import ${PageResultClassName};
 
 import javax.annotation.Resource;
+import org.springframework.context.annotation.Import;
 import java.util.*;
 
 import static cn.hutool.core.util.RandomUtil.*;
@@ -64,7 +63,8 @@ import static org.mockito.Mockito.*;
 *
 * @author ${table.author}
 */
-public class ${table.className}ServiceTest extends BaseSpringBootUnitTest {
+@Import(${table.className}ServiceImpl.class)
+public class ${table.className}ServiceTest extends BaseDbUnitTest {
 
     @Resource
     private ${table.className}ServiceImpl ${classNameVar}Service;
@@ -78,7 +78,7 @@ public class ${table.className}ServiceTest extends BaseSpringBootUnitTest {
         ${table.className}CreateReqVO reqVO = randomPojo(${table.className}CreateReqVO.class);
 
         // 调用
-        Long ${classNameVar}Id = ${classNameVar}Service.create${simpleClassName}(reqVO);
+        ${primaryColumn.javaType} ${classNameVar}Id = ${classNameVar}Service.create${simpleClassName}(reqVO);
         // 断言
         assertNotNull(${classNameVar}Id);
         // 校验记录的属性是否正确
@@ -118,7 +118,7 @@ public class ${table.className}ServiceTest extends BaseSpringBootUnitTest {
         ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class);
         ${classNameVar}Mapper.insert(db${simpleClassName});// @Sql: 先插入出一条存在的数据
         // 准备参数
-        Long id = db${simpleClassName}.getId();
+        ${primaryColumn.javaType} id = db${simpleClassName}.getId();
 
         // 调用
         ${classNameVar}Service.delete${simpleClassName}(id);
@@ -129,7 +129,7 @@ public class ${table.className}ServiceTest extends BaseSpringBootUnitTest {
     @Test
     public void testDelete${simpleClassName}_notExists() {
         // 准备参数
-        Long id = randomLongId();
+        ${primaryColumn.javaType} id = random${primaryColumn.javaType}Id();
 
         // 调用, 并断言异常
         assertServiceException(() -> ${classNameVar}Service.delete${simpleClassName}(id), ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS);

+ 4 - 4
src/main/resources/codegen/sql/sql.vm

@@ -4,7 +4,7 @@ INSERT INTO `sys_menu`(
     `path`, `icon`, `component`, `status`
 )
 VALUES (
-    '${table.classComment}管理', '${permissionPrefix}:query', 2, 0, ${table.parentMenuId},
+    '${table.classComment}管理', '', 2, 0, ${table.parentMenuId},
     '${simpleClassName_strikeCase}', '', '${table.moduleName}/${classNameVar}/index', 0
 );
 
@@ -12,8 +12,8 @@ VALUES (
 SELECT @parentId := LAST_INSERT_ID();
 
 -- 按钮 SQL
-#set ($functionNames = ['创建', '更新', '删除', '导出'])
-#set ($functionOps = ['create', 'update', 'delete', 'export'])
+#set ($functionNames = ['查询', '创建', '更新', '删除', '导出'])
+#set ($functionOps = ['query', 'create', 'update', 'delete', 'export'])
 #foreach ($functionName in $functionNames)
 #set ($index = $foreach.count - 1)
 INSERT INTO `sys_menu`(
@@ -21,7 +21,7 @@ INSERT INTO `sys_menu`(
     `path`, `icon`, `component`, `status`
 )
 VALUES (
-    '${table.tableComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId,
+    '${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId,
     '', '', '', 0
 );
 #end

+ 1 - 1
src/main/resources/codegen/vue/views/index.vue.vm

@@ -68,7 +68,7 @@
 #if ($column.javaType == "Date")## 时间类型
       <el-table-column label="${comment}" align="center" prop="${javaField}" width="180">
         <template slot-scope="scope">
-          <span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span>
+          <span>{{ parseTime(scope.row.${javaField}) }}</span>
         </template>
       </el-table-column>
 #elseif("" != $column.dictType)## 数据字典

+ 2 - 2
src/test/java/cn/iocoder/dashboard/BaseDbAndRedisUnitTest.java

@@ -15,9 +15,9 @@ import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.jdbc.Sql;
 
 /**
- * 依赖内存 DB 的单元测试
+ * 依赖内存 DB + Redis 的单元测试
  *
- * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法
+ * 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis
  *
  * @author 芋道源码
  */

+ 32 - 0
src/test/java/cn/iocoder/dashboard/BaseRedisUnitTest.java

@@ -0,0 +1,32 @@
+package cn.iocoder.dashboard;
+
+import cn.iocoder.dashboard.config.RedisTestConfiguration;
+import cn.iocoder.dashboard.framework.redis.config.RedisConfig;
+import org.redisson.spring.starter.RedissonAutoConfiguration;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+
+/**
+ * 依赖内存 Redis 的单元测试
+ *
+ * 相比 {@link BaseDbUnitTest} 来说,从内存 DB 改成了内存 Redis
+ *
+ * @author 芋道源码
+ */
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisUnitTest.Application.class)
+@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
+public class BaseRedisUnitTest {
+
+    @Import({
+            // Redis 配置类
+            RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer
+            RedisAutoConfiguration.class, // Spring Redis 自动配置类
+            RedisConfig.class, // 自己的 Redis 配置类
+            RedissonAutoConfiguration.class, // Redisson 自动高配置类
+    })
+    public static class Application {
+    }
+
+}

+ 0 - 32
src/test/java/cn/iocoder/dashboard/BaseSpringBootUnitTest.java

@@ -1,32 +0,0 @@
-package cn.iocoder.dashboard;
-
-import org.junit.jupiter.api.AfterEach;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.data.redis.core.RedisCallback;
-import org.springframework.data.redis.core.StringRedisTemplate;
-import org.springframework.test.context.ActiveProfiles;
-import org.springframework.test.context.jdbc.Sql;
-
-import javax.annotation.Resource;
-
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
-@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
-@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
-@Deprecated
-public class BaseSpringBootUnitTest {
-
-    @Resource
-    private StringRedisTemplate stringRedisTemplate;
-
-    /**
-     * 每个单元测试结束后,清理 Redis
-     */
-    @AfterEach
-    public void cleanRedis() {
-        stringRedisTemplate.execute((RedisCallback<Object>) connection -> {
-            connection.flushDb();
-            return null;
-        });
-    }
-
-}

+ 2 - 4
src/test/java/cn/iocoder/dashboard/config/RedisTestConfiguration.java

@@ -1,19 +1,17 @@
 package cn.iocoder.dashboard.config;
 
 import com.github.fppt.jedismock.RedisServer;
-import org.redisson.spring.starter.RedissonAutoConfiguration;
-import org.springframework.boot.autoconfigure.AutoConfigureBefore;
-import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
 import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
 
 import java.io.IOException;
 
 @Configuration(proxyBeanMethods = false)
+@Lazy(false) // 禁止延迟加载
 @EnableConfigurationProperties(RedisProperties.class)
-@AutoConfigureBefore({RedisAutoConfiguration.class, RedissonAutoConfiguration.class}) // 在 Redis 自动配置前,进行初始化
 public class RedisTestConfiguration {
 
     /**

+ 2 - 2
src/test/java/cn/iocoder/dashboard/framework/quartz/core/scheduler/SchedulerManagerTest.java

@@ -1,14 +1,14 @@
 package cn.iocoder.dashboard.framework.quartz.core.scheduler;
 
 import cn.hutool.core.util.StrUtil;
-import cn.iocoder.dashboard.BaseSpringBootUnitTest;
+import cn.iocoder.dashboard.BaseDbUnitTest;
 import cn.iocoder.dashboard.modules.system.job.auth.SysUserSessionTimeoutJob;
 import org.junit.jupiter.api.Test;
 import org.quartz.SchedulerException;
 
 import javax.annotation.Resource;
 
-class SchedulerManagerTest extends BaseSpringBootUnitTest {
+class SchedulerManagerTest extends BaseDbUnitTest {
 
     @Resource
     private SchedulerManager schedulerManager;

+ 126 - 0
src/test/java/cn/iocoder/dashboard/modules/infra/service/file/InfFileServiceTest.java

@@ -0,0 +1,126 @@
+package cn.iocoder.dashboard.modules.infra.service.file;
+
+import cn.hutool.core.io.resource.ResourceUtil;
+import cn.iocoder.dashboard.BaseDbUnitTest;
+import cn.iocoder.dashboard.common.pojo.PageResult;
+import cn.iocoder.dashboard.framework.file.config.FileProperties;
+import cn.iocoder.dashboard.modules.infra.controller.file.vo.InfFilePageReqVO;
+import cn.iocoder.dashboard.modules.infra.dal.dataobject.file.InfFileDO;
+import cn.iocoder.dashboard.modules.infra.dal.mysql.file.InfFileMapper;
+import cn.iocoder.dashboard.modules.infra.service.file.impl.InfFileServiceImpl;
+import cn.iocoder.dashboard.util.object.ObjectUtils;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+
+import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.FILE_NOT_EXISTS;
+import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.FILE_PATH_EXISTS;
+import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals;
+import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException;
+import static cn.iocoder.dashboard.util.RandomUtils.randomPojo;
+import static cn.iocoder.dashboard.util.RandomUtils.randomString;
+import static cn.iocoder.dashboard.util.date.DateUtils.buildTime;
+import static org.junit.jupiter.api.Assertions.*;
+
+@Import({InfFileServiceImpl.class, FileProperties.class})
+public class InfFileServiceTest extends BaseDbUnitTest {
+
+    @Resource
+    private InfFileServiceImpl fileService;
+
+    @Resource
+    private FileProperties fileProperties;
+    @Resource
+    private InfFileMapper fileMapper;
+
+    @Test
+    public void testCreateFile_success() {
+        // 准备参数
+        String path = randomString();
+        byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
+
+        // 调用
+        String url = fileService.createFile(path, content);
+        // 断言
+        assertEquals(fileProperties.getBasePath() + path, url);
+        // 校验数据
+        InfFileDO file = fileMapper.selectById(path);
+        assertEquals(path, file.getId());
+        assertEquals("jpg", file.getType());
+        assertArrayEquals(content, file.getContent());
+    }
+
+    @Test
+    public void testCreateFile_exists() {
+        // mock 数据
+        InfFileDO dbFile = randomPojo(InfFileDO.class);
+        fileMapper.insert(dbFile);
+        // 准备参数
+        String path = dbFile.getId(); // 模拟已存在
+        byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
+
+        // 调用,并断言异常
+        assertServiceException(() -> fileService.createFile(path, content), FILE_PATH_EXISTS);
+    }
+
+    @Test
+    public void testDeleteFile_success() {
+        // mock 数据
+        InfFileDO dbFile = randomPojo(InfFileDO.class);
+        fileMapper.insert(dbFile);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        String id = dbFile.getId();
+
+        // 调用
+        fileService.deleteFile(id);
+        // 校验数据不存在了
+        assertNull(fileMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteFile_notExists() {
+        // 准备参数
+        String id = randomString();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> fileService.deleteFile(id), FILE_NOT_EXISTS);
+    }
+
+    @Test
+    public void testGetFilePage() {
+        // mock 数据
+        InfFileDO dbFile = randomPojo(InfFileDO.class, o -> { // 等会查询到
+            o.setId("yudao");
+            o.setType("jpg");
+            o.setCreateTime(buildTime(2021, 1, 15));
+        });
+        fileMapper.insert(dbFile);
+        // 测试 id 不匹配
+        fileMapper.insert(ObjectUtils.clone(dbFile, o -> o.setId("tudou")));
+        // 测试 type 不匹配
+        fileMapper.insert(ObjectUtils.clone(dbFile, o -> {
+            o.setId("yudao02");
+            o.setType("png");
+        }));
+        // 测试 createTime 不匹配
+        fileMapper.insert(ObjectUtils.clone(dbFile, o -> {
+            o.setId("yudao03");
+            o.setCreateTime(buildTime(2020, 1, 15));
+        }));
+        // 准备参数
+        InfFilePageReqVO reqVO = new InfFilePageReqVO();
+        reqVO.setId("yudao");
+        reqVO.setType("jp");
+        reqVO.setBeginCreateTime(buildTime(2021, 1, 10));
+        reqVO.setEndCreateTime(buildTime(2021, 1, 20));
+
+        // 调用
+        PageResult<InfFileDO> pageResult = fileService.getFilePage(reqVO);
+        // 断言
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(dbFile, pageResult.getList().get(0), "content");
+    }
+
+}

+ 10 - 11
src/test/java/cn/iocoder/dashboard/modules/system/service/auth/SysUserSessionServiceImplTest.java

@@ -2,7 +2,6 @@ package cn.iocoder.dashboard.modules.system.service.auth;
 
 import cn.hutool.core.date.DateUtil;
 import cn.iocoder.dashboard.BaseDbAndRedisUnitTest;
-import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX;
 import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.auth.SysUserSessionDO;
 import cn.iocoder.dashboard.modules.system.dal.mysql.auth.SysUserSessionMapper;
@@ -32,24 +31,24 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
  * @version 1.0
  * @since <pre>3月 8, 2021</pre>
  */
-@Import(
-        SysUserSessionServiceImpl.class)
+@Import(SysUserSessionServiceImpl.class)
 public class SysUserSessionServiceImplTest extends BaseDbAndRedisUnitTest {
 
     @Resource
-    SysUserSessionServiceImpl sysUserSessionService;
+    private SysUserSessionServiceImpl sysUserSessionService;
     @Resource
-    SysUserSessionMapper sysUserSessionMapper;
+    private SysUserSessionMapper sysUserSessionMapper;
+
     @MockBean
-    SecurityProperties securityProperties;
+    private SecurityProperties securityProperties;
     @MockBean
-    SysDeptServiceImpl sysDeptService;
+    private SysDeptServiceImpl sysDeptService;
     @MockBean
-    SysUserServiceImpl sysUserService;
+    private SysUserServiceImpl sysUserService;
     @MockBean
-    SysLoginLogServiceImpl sysLoginLogService;
+    private SysLoginLogServiceImpl sysLoginLogService;
     @MockBean
-    SysLoginUserRedisDAO sysLoginUserRedisDAO;
+    private SysLoginUserRedisDAO sysLoginUserRedisDAO;
 
     @Test
     public void testClearSessionTimeout_success() throws Exception {
@@ -75,4 +74,4 @@ public class SysUserSessionServiceImplTest extends BaseDbAndRedisUnitTest {
         AssertUtils.assertPojoEquals(sessionDO, userSessionDOS.get(0), "updateTime");
     }
 
-} 
+}

+ 66 - 0
src/test/java/cn/iocoder/dashboard/modules/system/service/common/SysCaptchaServiceTest.java

@@ -0,0 +1,66 @@
+package cn.iocoder.dashboard.modules.system.service.common;
+
+import cn.iocoder.dashboard.BaseRedisUnitTest;
+import cn.iocoder.dashboard.framework.captcha.config.CaptchaProperties;
+import cn.iocoder.dashboard.modules.system.controller.common.vo.SysCaptchaImageRespVO;
+import cn.iocoder.dashboard.modules.system.dal.redis.common.SysCaptchaRedisDAO;
+import cn.iocoder.dashboard.modules.system.service.common.impl.SysCaptchaServiceImpl;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+
+import static cn.iocoder.dashboard.util.RandomUtils.randomString;
+import static org.junit.jupiter.api.Assertions.*;
+
+@Import({SysCaptchaServiceImpl.class, CaptchaProperties.class, SysCaptchaRedisDAO.class})
+public class SysCaptchaServiceTest extends BaseRedisUnitTest {
+
+    @Resource
+    private SysCaptchaServiceImpl captchaService;
+
+    @Resource
+    private SysCaptchaRedisDAO captchaRedisDAO;
+    @Resource
+    private CaptchaProperties captchaProperties;
+
+    @Test
+    public void testGetCaptchaImage() {
+        // 调用
+        SysCaptchaImageRespVO respVO = captchaService.getCaptchaImage();
+        // 断言
+        assertNotNull(respVO.getUuid());
+        assertNotNull(respVO.getImg());
+        String captchaCode = captchaRedisDAO.get(respVO.getUuid());
+        assertNotNull(captchaCode);
+    }
+
+    @Test
+    public void testGetCaptchaCode() {
+        // 准备参数
+        String uuid = randomString();
+        String code = randomString();
+        // mock 数据
+        captchaRedisDAO.set(uuid, code, captchaProperties.getTimeout());
+
+        // 调用
+        String resultCode = captchaService.getCaptchaCode(uuid);
+        // 断言
+        assertEquals(code, resultCode);
+    }
+
+    @Test
+    public void testDeleteCaptchaCode() {
+        // 准备参数
+        String uuid = randomString();
+        String code = randomString();
+        // mock 数据
+        captchaRedisDAO.set(uuid, code, captchaProperties.getTimeout());
+
+        // 调用
+        captchaService.deleteCaptchaCode(uuid);
+        // 断言
+        assertNull(captchaRedisDAO.get(uuid));
+    }
+
+}

+ 2 - 2
src/test/java/cn/iocoder/dashboard/modules/tool/dal/mysql/codegen/ToolInformationSchemaColumnMapperTest.java

@@ -1,6 +1,6 @@
 package cn.iocoder.dashboard.modules.tool.dal.mysql.codegen;
 
-import cn.iocoder.dashboard.BaseSpringBootUnitTest;
+import cn.iocoder.dashboard.BaseDbUnitTest;
 import cn.iocoder.dashboard.modules.tool.dal.dataobject.codegen.ToolSchemaColumnDO;
 import org.junit.jupiter.api.Test;
 
@@ -9,7 +9,7 @@ import java.util.List;
 
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-public class ToolInformationSchemaColumnMapperTest extends BaseSpringBootUnitTest {
+public class ToolInformationSchemaColumnMapperTest extends BaseDbUnitTest {
 
     @Resource
     private ToolSchemaColumnMapper toolInformationSchemaColumnMapper;

+ 2 - 2
src/test/java/cn/iocoder/dashboard/modules/tool/dal/mysql/codegen/ToolInformationSchemaTableMapperTest.java

@@ -1,6 +1,6 @@
 package cn.iocoder.dashboard.modules.tool.dal.mysql.codegen;
 
-import cn.iocoder.dashboard.BaseSpringBootUnitTest;
+import cn.iocoder.dashboard.BaseDbUnitTest;
 import cn.iocoder.dashboard.modules.tool.dal.dataobject.codegen.ToolSchemaTableDO;
 import org.junit.jupiter.api.Test;
 
@@ -9,7 +9,7 @@ import java.util.List;
 
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-class ToolInformationSchemaTableMapperTest extends BaseSpringBootUnitTest {
+class ToolInformationSchemaTableMapperTest extends BaseDbUnitTest {
 
     @Resource
     private ToolSchemaTableMapper toolInformationSchemaTableMapper;

+ 2 - 3
src/test/java/cn/iocoder/dashboard/modules/tool/service/codegen/impl/ToolCodegenEngineTest.java

@@ -1,18 +1,17 @@
 package cn.iocoder.dashboard.modules.tool.service.codegen.impl;
 
-import cn.iocoder.dashboard.BaseSpringBootUnitTest;
+import cn.iocoder.dashboard.BaseDbUnitTest;
 import cn.iocoder.dashboard.modules.tool.dal.dataobject.codegen.ToolCodegenColumnDO;
 import cn.iocoder.dashboard.modules.tool.dal.dataobject.codegen.ToolCodegenTableDO;
 import cn.iocoder.dashboard.modules.tool.dal.mysql.codegen.ToolCodegenColumnMapper;
 import cn.iocoder.dashboard.modules.tool.dal.mysql.codegen.ToolCodegenTableMapper;
 import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
 
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.Map;
 
-public class ToolCodegenEngineTest extends BaseSpringBootUnitTest {
+public class ToolCodegenEngineTest extends BaseDbUnitTest {
 
     @Resource
     private ToolCodegenTableMapper codegenTableMapper;

+ 2 - 2
src/test/java/cn/iocoder/dashboard/modules/tool/service/codegen/impl/ToolCodegenSQLParserTest.java

@@ -1,9 +1,9 @@
 package cn.iocoder.dashboard.modules.tool.service.codegen.impl;
 
-import cn.iocoder.dashboard.BaseSpringBootUnitTest;
+import cn.iocoder.dashboard.BaseDbUnitTest;
 import org.junit.jupiter.api.Test;
 
-public class ToolCodegenSQLParserTest extends BaseSpringBootUnitTest {
+public class ToolCodegenSQLParserTest extends BaseDbUnitTest {
 
     @Test
     public void testParse() {

+ 2 - 3
src/test/java/cn/iocoder/dashboard/modules/tool/service/codegen/impl/ToolCodegenServiceImplTest.java

@@ -1,12 +1,11 @@
 package cn.iocoder.dashboard.modules.tool.service.codegen.impl;
 
-import cn.iocoder.dashboard.BaseSpringBootUnitTest;
+import cn.iocoder.dashboard.BaseDbUnitTest;
 import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
 
 import javax.annotation.Resource;
 
-class ToolCodegenServiceImplTest extends BaseSpringBootUnitTest {
+class ToolCodegenServiceImplTest extends BaseDbUnitTest {
 
     @Resource
     private ToolCodegenServiceImpl toolCodegenService;

二進制
src/test/resources/file/erweima.jpg


+ 1 - 0
src/test/resources/sql/clean.sql

@@ -1,5 +1,6 @@
 -- inf 开头的 DB
 DELETE FROM "inf_config";
+DELETE FROM "inf_file";
 
 -- sys 开头的 DB
 DELETE FROM "sys_dept";

+ 16 - 4
src/test/resources/sql/create_tables.sql

@@ -17,6 +17,18 @@ CREATE TABLE IF NOT EXISTS "inf_config" (
     PRIMARY KEY ("id")
 ) COMMENT '参数配置表';
 
+CREATE TABLE IF NOT EXISTS "inf_file" (
+    "id" varchar(188) NOT NULL,
+    "type" varchar(63) DEFAULT NULL,
+    "content" blob NOT NULL,
+    "creator" varchar(64) DEFAULT '',
+    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater" varchar(64) DEFAULT '',
+    "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted" bit NOT NULL DEFAULT FALSE,
+    PRIMARY KEY ("id")
+) COMMENT '文件表';
+
 -- sys 开头的 DB
 
 CREATE TABLE IF NOT EXISTS "sys_dept" (
@@ -101,7 +113,7 @@ CREATE TABLE IF NOT EXISTS "sys_menu" (
     PRIMARY KEY ("id")
 ) COMMENT '菜单权限表';
 
-CREATE TABLE "sys_dict_type" (
+CREATE TABLE IF NOT EXISTS "sys_dict_type" (
     "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     "name" varchar(100) NOT NULL DEFAULT '',
     "type" varchar(100) NOT NULL DEFAULT '',
@@ -115,7 +127,7 @@ CREATE TABLE "sys_dict_type" (
     PRIMARY KEY ("id")
 ) COMMENT '字典类型表';
 
-CREATE TABLE `sys_user_session` (
+CREATE TABLE IF NOT EXISTS `sys_user_session` (
     `id` varchar(32) NOT NULL,
     `user_id` bigint DEFAULT NULL,
     `username` varchar(50) NOT NULL DEFAULT '',
@@ -179,7 +191,7 @@ CREATE TABLE IF NOT EXISTS `sys_login_log` (
 ) COMMENT ='系统访问记录';
 
 
-CREATE TABLE `sys_operate_log` (
+CREATE TABLE IF NOT EXISTS `sys_operate_log` (
     `id`               bigint(20)    NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     `trace_id`         varchar(64)   NOT NULL DEFAULT '',
     `user_id`          bigint(20)    NOT NULL,
@@ -207,7 +219,7 @@ CREATE TABLE `sys_operate_log` (
     PRIMARY KEY (`id`)
 ) COMMENT ='操作日志记录';
 
-create table "sys_user" (
+create table IF NOT EXISTS "sys_user" (
     "id" bigint not null GENERATED BY DEFAULT AS IDENTITY,
     "username" varchar(30) not null,
     "password" varchar(100) not null default '',

部分文件因文件數量過多而無法顯示