Browse Source

Merge branch 'master' into feat_userprofile

# Conflicts:
#	src/main/resources/application-dev.yaml
#	src/main/resources/application-local.yaml
niudehua 4 years ago
parent
commit
e4bda22b73
46 changed files with 1605 additions and 1022 deletions
  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. BIN
      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>

File diff suppressed because it is too large
+ 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;

BIN
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 '',

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