Browse Source

Merge branch 'master' of gitee.com:zhijiantianya/ruoyi-vue-pro into feature/ut-job

neilz 4 years ago
parent
commit
08bc389ba5
46 changed files with 1791 additions and 948 deletions
  1. 4 0
      lombok.config
  2. 16 0
      ruoyi-ui/src/api/infra/dbDoc.js
  3. 7 1
      ruoyi-ui/src/main.js
  4. 15 0
      ruoyi-ui/src/utils/ruoyi.js
  5. 751 715
      ruoyi-ui/src/views/system/user/index.vue
  6. 37 5
      ruoyi-ui/src/views/tool/dbDoc/index.vue
  7. 3 1
      src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java
  8. 3 1
      src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java
  9. 63 19
      src/main/java/cn/iocoder/dashboard/modules/infra/controller/doc/InfDbDocController.java
  10. 1 1
      src/main/java/cn/iocoder/dashboard/modules/infra/controller/logger/vo/apierrorlog/InfApiErrorLogExportReqVO.java
  11. 1 1
      src/main/java/cn/iocoder/dashboard/modules/infra/controller/logger/vo/apierrorlog/InfApiErrorLogPageReqVO.java
  12. 1 1
      src/main/java/cn/iocoder/dashboard/modules/infra/dal/dataobject/logger/InfApiAccessLogDO.java
  13. 2 2
      src/main/java/cn/iocoder/dashboard/modules/infra/dal/dataobject/logger/InfApiErrorLogDO.java
  14. 4 4
      src/main/java/cn/iocoder/dashboard/modules/infra/service/job/impl/InfJobServiceImpl.java
  15. 5 3
      src/main/java/cn/iocoder/dashboard/modules/infra/service/logger/impl/InfApiAccessLogServiceImpl.java
  16. 6 3
      src/main/java/cn/iocoder/dashboard/modules/infra/service/logger/impl/InfApiErrorLogServiceImpl.java
  17. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/controller/common/SysCaptchaController.java
  18. 2 2
      src/main/java/cn/iocoder/dashboard/modules/system/controller/user/SysUserController.java
  19. 74 86
      src/main/java/cn/iocoder/dashboard/modules/system/controller/user/SysUserProfileController.java
  20. 37 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/user/vo/user/SysUserProfileRespVO.java
  21. 44 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/user/vo/user/SysUserProfileUpdateReqVO.java
  22. 16 8
      src/main/java/cn/iocoder/dashboard/modules/system/convert/auth/SysAuthConvert.java
  23. 16 1
      src/main/java/cn/iocoder/dashboard/modules/system/convert/user/SysUserConvert.java
  24. 3 0
      src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java
  25. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java
  26. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java
  27. 1 1
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java
  28. 27 4
      src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserService.java
  29. 81 21
      src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserServiceImpl.java
  30. 5 5
      src/main/java/cn/iocoder/dashboard/modules/tool/service/codegen/impl/ToolCodegenServiceImpl.java
  31. 2 2
      src/main/resources/application-dev.yaml
  32. 2 2
      src/main/resources/application-local.yaml
  33. 2 2
      src/test/java/cn/iocoder/dashboard/BaseDbAndRedisUnitTest.java
  34. 32 0
      src/test/java/cn/iocoder/dashboard/BaseRedisUnitTest.java
  35. 0 32
      src/test/java/cn/iocoder/dashboard/BaseSpringBootUnitTest.java
  36. 2 4
      src/test/java/cn/iocoder/dashboard/config/RedisTestConfiguration.java
  37. 2 2
      src/test/java/cn/iocoder/dashboard/framework/quartz/core/scheduler/SchedulerManagerTest.java
  38. 177 0
      src/test/java/cn/iocoder/dashboard/modules/infra/service/logger/InfApiAccessLogServiceImplTest.java
  39. 207 0
      src/test/java/cn/iocoder/dashboard/modules/infra/service/logger/InfApiErrorLogServiceImplTest.java
  40. 66 0
      src/test/java/cn/iocoder/dashboard/modules/system/service/common/SysCaptchaServiceTest.java
  41. 2 2
      src/test/java/cn/iocoder/dashboard/modules/tool/dal/mysql/codegen/ToolInformationSchemaColumnMapperTest.java
  42. 2 2
      src/test/java/cn/iocoder/dashboard/modules/tool/dal/mysql/codegen/ToolInformationSchemaTableMapperTest.java
  43. 2 3
      src/test/java/cn/iocoder/dashboard/modules/tool/service/codegen/impl/ToolCodegenEngineTest.java
  44. 2 2
      src/test/java/cn/iocoder/dashboard/modules/tool/service/codegen/impl/ToolCodegenSQLParserTest.java
  45. 2 3
      src/test/java/cn/iocoder/dashboard/modules/tool/service/codegen/impl/ToolCodegenServiceImplTest.java
  46. 61 4
      src/test/resources/sql/create_tables.sql

+ 4 - 0
lombok.config

@@ -0,0 +1,4 @@
+config.stopBubbling = true
+lombok.tostring.callsuper=true
+lombok.equalsandhashcode.callsuper=true
+lombok.accessors.chain=true

+ 16 - 0
ruoyi-ui/src/api/infra/dbDoc.js

@@ -8,3 +8,19 @@ export function exportHtml() {
     responseType: 'blob'
     responseType: 'blob'
   })
   })
 }
 }
+
+export function exportWord() {
+  return request({
+    url: '/infra/db-doc/export-word',
+    method: 'get',
+    responseType: 'blob'
+  })
+}
+
+export function exportMarkdown() {
+  return request({
+    url: '/infra/db-doc/export-markdown',
+    method: 'get',
+    responseType: 'blob'
+  })
+}

+ 7 - 1
ruoyi-ui/src/main.js

@@ -25,7 +25,10 @@ import {
   download,
   download,
   handleTree,
   handleTree,
   downloadExcel,
   downloadExcel,
-  downloadZip
+  downloadWord,
+  downloadZip,
+  downloadHtml,
+  downloadMarkdown,
 } from "@/utils/ruoyi";
 } from "@/utils/ruoyi";
 import Pagination from "@/components/Pagination";
 import Pagination from "@/components/Pagination";
 // 自定义表格工具扩展
 // 自定义表格工具扩展
@@ -48,6 +51,9 @@ Vue.prototype.getDictDataLabel = getDictDataLabel
 Vue.prototype.DICT_TYPE = DICT_TYPE
 Vue.prototype.DICT_TYPE = DICT_TYPE
 Vue.prototype.download = download
 Vue.prototype.download = download
 Vue.prototype.downloadExcel = downloadExcel
 Vue.prototype.downloadExcel = downloadExcel
+Vue.prototype.downloadWord = downloadWord
+Vue.prototype.downloadHtml = downloadHtml
+Vue.prototype.downloadMarkdown = downloadMarkdown
 Vue.prototype.downloadZip = downloadZip
 Vue.prototype.downloadZip = downloadZip
 Vue.prototype.handleTree = handleTree
 Vue.prototype.handleTree = handleTree
 
 

+ 15 - 0
ruoyi-ui/src/utils/ruoyi.js

@@ -120,11 +120,26 @@ export function downloadExcel(data, fileName) {
   download0(data, fileName, 'application/vnd.ms-excel');
   download0(data, fileName, 'application/vnd.ms-excel');
 }
 }
 
 
+// 下载 Word 方法
+export function downloadWord(data, fileName) {
+  download0(data, fileName, 'application/msword');
+}
+
 // 下载 Zip 方法
 // 下载 Zip 方法
 export function downloadZip(data, fileName) {
 export function downloadZip(data, fileName) {
   download0(data, fileName, 'application/zip');
   download0(data, fileName, 'application/zip');
 }
 }
 
 
+// 下载 Html 方法
+export function downloadHtml(data, fileName) {
+  download0(data, fileName, 'text/html');
+}
+
+// 下载 Markdown 方法
+export function downloadMarkdown(data, fileName) {
+  download0(data, fileName, 'text/markdown');
+}
+
 function download0(data, fileName, mineType) {
 function download0(data, fileName, mineType) {
   // 创建 blob
   // 创建 blob
   let blob = new Blob([data], {type: mineType});
   let blob = new Blob([data], {type: mineType});

+ 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>

+ 37 - 5
ruoyi-ui/src/views/tool/dbDoc/index.vue

@@ -1,10 +1,21 @@
 <template>
 <template>
-  <div v-loading="loading" :style="'height:'+ height">
-    <iframe :src="src" frameborder="no" style="width: 100%;height: 100%" scrolling="auto" />
+  <div class="app-container">
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleExportHtml">导出 HTML</el-button>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleExportWord">导出 Word</el-button>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleExportMarkdown">导出 Markdown</el-button>
+      </el-col>
+    </el-row>
+
+    <!-- 展示文档 -->
+    <div v-loading="loading" :style="'height:'+ height">
+      <iframe :src="src" frameborder="no" style="width: 100%;height: 100%" scrolling="auto" />
+    </div>
   </div>
   </div>
 </template>
 </template>
 <script>
 <script>
-import {exportHtml} from "@/api/infra/dbDoc";
+import { exportHtml, exportWord, exportMarkdown} from "@/api/infra/dbDoc";
 
 
 export default {
 export default {
   name: "DBDoc",
   name: "DBDoc",
@@ -25,10 +36,31 @@ export default {
     };
     };
   },
   },
   created() {
   created() {
+    // 加载 Html,进行预览
     exportHtml().then(response => {
     exportHtml().then(response => {
-      // var blob = new Blob(['<a id="a"><b id="b">hey!</b></a>'], {type : 'text/html'});
-      this.src = window.URL.createObjectURL(response);
+      let blob = new Blob([response], {type : 'text/html'});
+      this.src = window.URL.createObjectURL(blob);
     })
     })
   },
   },
+  methods: {
+    /** 处理导出 HTML */
+    handleExportHtml() {
+      exportHtml().then(response => {
+        this.downloadHtml(response, '数据库文档.html');
+      })
+    },
+    /** 处理导出 Word */
+    handleExportWord() {
+      exportWord().then(response => {
+        this.downloadWord(response, '数据库文档.doc');
+      })
+    },
+    /** 处理导出 Markdown */
+    handleExportMarkdown() {
+      exportMarkdown().then(response => {
+        this.downloadMarkdown(response, '数据库文档.md');
+      })
+    }
+  }
 };
 };
 </script>
 </script>

+ 3 - 1
src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiAccessLogFrameworkService.java

@@ -3,6 +3,7 @@ package cn.iocoder.dashboard.framework.logger.apilog.core.service;
 import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO;
 import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO;
 
 
 import javax.validation.Valid;
 import javax.validation.Valid;
+import java.util.concurrent.Future;
 
 
 /**
 /**
  * API 访问日志 Framework Service 接口
  * API 访问日志 Framework Service 接口
@@ -15,7 +16,8 @@ public interface ApiAccessLogFrameworkService {
      * 创建 API 访问日志
      * 创建 API 访问日志
      *
      *
      * @param createDTO 创建信息
      * @param createDTO 创建信息
+     * @return 是否创建成功
      */
      */
-    void createApiAccessLogAsync(@Valid ApiAccessLogCreateDTO createDTO);
+    Future<Boolean> createApiAccessLogAsync(@Valid ApiAccessLogCreateDTO createDTO);
 
 
 }
 }

+ 3 - 1
src/main/java/cn/iocoder/dashboard/framework/logger/apilog/core/service/ApiErrorLogFrameworkService.java

@@ -3,6 +3,7 @@ package cn.iocoder.dashboard.framework.logger.apilog.core.service;
 import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiErrorLogCreateDTO;
 import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiErrorLogCreateDTO;
 
 
 import javax.validation.Valid;
 import javax.validation.Valid;
+import java.util.concurrent.Future;
 
 
 /**
 /**
  * API 错误日志 Framework Service 接口
  * API 错误日志 Framework Service 接口
@@ -15,7 +16,8 @@ public interface ApiErrorLogFrameworkService {
      * 创建 API 错误日志
      * 创建 API 错误日志
      *
      *
      * @param createDTO 创建信息
      * @param createDTO 创建信息
+     * @return 是否创建成功
      */
      */
-    void createApiErrorLogAsync(@Valid ApiErrorLogCreateDTO createDTO);
+    Future<Boolean> createApiErrorLogAsync(@Valid ApiErrorLogCreateDTO createDTO);
 
 
 }
 }

+ 63 - 19
src/main/java/cn/iocoder/dashboard/modules/infra/controller/doc/InfDbDocController.java

@@ -1,6 +1,8 @@
 package cn.iocoder.dashboard.modules.infra.controller.doc;
 package cn.iocoder.dashboard.modules.infra.controller.doc;
 
 
-import cn.hutool.extra.servlet.ServletUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.iocoder.dashboard.util.servlet.ServletUtils;
 import cn.smallbun.screw.core.Configuration;
 import cn.smallbun.screw.core.Configuration;
 import cn.smallbun.screw.core.engine.EngineConfig;
 import cn.smallbun.screw.core.engine.EngineConfig;
 import cn.smallbun.screw.core.engine.EngineFileType;
 import cn.smallbun.screw.core.engine.EngineFileType;
@@ -10,18 +12,18 @@ import cn.smallbun.screw.core.process.ProcessConfig;
 import com.zaxxer.hikari.HikariConfig;
 import com.zaxxer.hikari.HikariConfig;
 import com.zaxxer.hikari.HikariDataSource;
 import com.zaxxer.hikari.HikariDataSource;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiOperation;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
-import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.bind.annotation.RestController;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpServletResponse;
-import javax.sql.DataSource;
 import java.io.File;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.Collections;
 import java.util.Collections;
 
 
 @Api(tags = "数据库文档")
 @Api(tags = "数据库文档")
@@ -34,34 +36,77 @@ public class InfDbDocController {
 
 
     private static final String FILE_OUTPUT_DIR = System.getProperty("java.io.tmpdir") + File.separator
     private static final String FILE_OUTPUT_DIR = System.getProperty("java.io.tmpdir") + File.separator
             + "db-doc";
             + "db-doc";
-    private static final EngineFileType FILE_OUTPUT_TYPE = EngineFileType.HTML; // 可以设置 Word 或者 Markdown 格式
     private static final String DOC_FILE_NAME = "数据库文档";
     private static final String DOC_FILE_NAME = "数据库文档";
     private static final String DOC_VERSION = "1.0.0";
     private static final String DOC_VERSION = "1.0.0";
     private static final String DOC_DESCRIPTION = "文档描述";
     private static final String DOC_DESCRIPTION = "文档描述";
 
 
-    @Resource
-    private DataSource dataSource;
-
     @GetMapping("/export-html")
     @GetMapping("/export-html")
-    public synchronized void exportHtml(HttpServletResponse response) throws FileNotFoundException {
+    @ApiOperation("导出 html 格式的数据文档")
+    @ApiImplicitParam(name = "deleteFile", value = "是否删除在服务器本地生成的数据库文档", example = "true", dataTypeClass = Boolean.class)
+    public void exportHtml(@RequestParam(defaultValue = "true") Boolean deleteFile,
+                           HttpServletResponse response) throws IOException {
+        doExportFile(EngineFileType.HTML, deleteFile, response);
+    }
+
+    @GetMapping("/export-word")
+    @ApiOperation("导出 word 格式的数据文档")
+    @ApiImplicitParam(name = "deleteFile", value = "是否删除在服务器本地生成的数据库文档", example = "true", dataTypeClass = Boolean.class)
+    public void exportWord(@RequestParam(defaultValue = "true") Boolean deleteFile,
+                           HttpServletResponse response) throws IOException {
+        doExportFile(EngineFileType.WORD, deleteFile, response);
+    }
+
+    @GetMapping("/export-markdown")
+    @ApiOperation("导出 markdown 格式的数据文档")
+    @ApiImplicitParam(name = "deleteFile", value = "是否删除在服务器本地生成的数据库文档", example = "true", dataTypeClass = Boolean.class)
+    public void exportMarkdown(@RequestParam(defaultValue = "true") Boolean deleteFile,
+                               HttpServletResponse response) throws IOException {
+        doExportFile(EngineFileType.MD, deleteFile, response);
+    }
+
+    private void doExportFile(EngineFileType fileOutputType, Boolean deleteFile,
+                              HttpServletResponse response) throws IOException {
+        String docFileName = DOC_FILE_NAME + "_" + IdUtil.fastSimpleUUID();
+        String filePath = doExportFile(fileOutputType, docFileName);
+        String downloadFileName = DOC_FILE_NAME + fileOutputType.getFileSuffix(); //下载后的文件名
+        try {
+            // 读取,返回
+            ServletUtils.writeAttachment(response, downloadFileName, FileUtil.readBytes(filePath));
+        } finally {
+            handleDeleteFile(deleteFile, filePath);
+        }
+    }
+
+    /**
+     * 输出文件,返回文件路径
+     *
+     * @param fileOutputType 文件类型
+     * @param fileName       文件名, 无需 ".docx" 等文件后缀
+     * @return 生成的文件所在路径
+     */
+    private String doExportFile(EngineFileType fileOutputType, String fileName) {
         try (HikariDataSource dataSource = buildDataSource()) {
         try (HikariDataSource dataSource = buildDataSource()) {
             // 创建 screw 的配置
             // 创建 screw 的配置
             Configuration config = Configuration.builder()
             Configuration config = Configuration.builder()
                     .version(DOC_VERSION)  // 版本
                     .version(DOC_VERSION)  // 版本
                     .description(DOC_DESCRIPTION) // 描述
                     .description(DOC_DESCRIPTION) // 描述
                     .dataSource(dataSource) // 数据源
                     .dataSource(dataSource) // 数据源
-                    .engineConfig(buildEngineConfig()) // 引擎配置
+                    .engineConfig(buildEngineConfig(fileOutputType, fileName)) // 引擎配置
                     .produceConfig(buildProcessConfig()) // 处理配置
                     .produceConfig(buildProcessConfig()) // 处理配置
                     .build();
                     .build();
 
 
             // 执行 screw,生成数据库文档
             // 执行 screw,生成数据库文档
             new DocumentationExecute(config).execute();
             new DocumentationExecute(config).execute();
 
 
-            // 读取,返回
-            ServletUtil.write(response,
-                    new FileInputStream(FILE_OUTPUT_DIR + File.separator + DOC_FILE_NAME + FILE_OUTPUT_TYPE.getFileSuffix()),
-                    MediaType.TEXT_HTML_VALUE);
+            return FILE_OUTPUT_DIR + File.separator + fileName + fileOutputType.getFileSuffix();
+        }
+    }
+
+    private void handleDeleteFile(Boolean deleteFile, String filePath) {
+        if (!deleteFile) {
+            return;
         }
         }
+        FileUtil.del(filePath);
     }
     }
 
 
     /**
     /**
@@ -71,7 +116,6 @@ public class InfDbDocController {
     private HikariDataSource buildDataSource() {
     private HikariDataSource buildDataSource() {
         // 创建 HikariConfig 配置类
         // 创建 HikariConfig 配置类
         HikariConfig hikariConfig = new HikariConfig();
         HikariConfig hikariConfig = new HikariConfig();
-//        hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
         hikariConfig.setJdbcUrl(dataSourceProperties.getUrl());
         hikariConfig.setJdbcUrl(dataSourceProperties.getUrl());
         hikariConfig.setUsername(dataSourceProperties.getUsername());
         hikariConfig.setUsername(dataSourceProperties.getUsername());
         hikariConfig.setPassword(dataSourceProperties.getPassword());
         hikariConfig.setPassword(dataSourceProperties.getPassword());
@@ -83,13 +127,13 @@ public class InfDbDocController {
     /**
     /**
      * 创建 screw 的引擎配置
      * 创建 screw 的引擎配置
      */
      */
-    private static EngineConfig buildEngineConfig() {
+    private static EngineConfig buildEngineConfig(EngineFileType fileOutputType, String docFileName) {
         return EngineConfig.builder()
         return EngineConfig.builder()
                 .fileOutputDir(FILE_OUTPUT_DIR) // 生成文件路径
                 .fileOutputDir(FILE_OUTPUT_DIR) // 生成文件路径
                 .openOutputDir(false) // 打开目录
                 .openOutputDir(false) // 打开目录
-                .fileType(FILE_OUTPUT_TYPE) // 文件类型
+                .fileType(fileOutputType) // 文件类型
                 .produceType(EngineTemplateType.freemarker) // 文件类型
                 .produceType(EngineTemplateType.freemarker) // 文件类型
-                .fileName(DOC_FILE_NAME) // 自定义文件名称
+                .fileName(docFileName) // 自定义文件名称
                 .build();
                 .build();
     }
     }
 
 

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/infra/controller/logger/vo/apierrorlog/InfApiErrorLogExportReqVO.java

@@ -14,7 +14,7 @@ import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOU
 public class InfApiErrorLogExportReqVO {
 public class InfApiErrorLogExportReqVO {
 
 
     @ApiModelProperty(value = "用户编号", example = "666")
     @ApiModelProperty(value = "用户编号", example = "666")
-    private Integer userId;
+    private Long userId;
 
 
     @ApiModelProperty(value = "用户类型", example = "1")
     @ApiModelProperty(value = "用户类型", example = "1")
     private Integer userType;
     private Integer userType;

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/infra/controller/logger/vo/apierrorlog/InfApiErrorLogPageReqVO.java

@@ -19,7 +19,7 @@ import static cn.iocoder.dashboard.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOU
 public class InfApiErrorLogPageReqVO extends PageParam {
 public class InfApiErrorLogPageReqVO extends PageParam {
 
 
     @ApiModelProperty(value = "用户编号", example = "666")
     @ApiModelProperty(value = "用户编号", example = "666")
-    private Integer userId;
+    private Long userId;
 
 
     @ApiModelProperty(value = "用户类型", example = "1")
     @ApiModelProperty(value = "用户类型", example = "1")
     private Integer userType;
     private Integer userType;

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/infra/dal/dataobject/logger/InfApiAccessLogDO.java

@@ -37,7 +37,7 @@ public class InfApiAccessLogDO extends BaseDO {
     /**
     /**
      * 用户编号
      * 用户编号
      */
      */
-    private Integer userId;
+    private Long userId;
     /**
     /**
      * 用户类型
      * 用户类型
      *
      *

+ 2 - 2
src/main/java/cn/iocoder/dashboard/modules/infra/dal/dataobject/logger/InfApiErrorLogDO.java

@@ -30,7 +30,7 @@ public class InfApiErrorLogDO extends BaseDO {
     /**
     /**
      * 用户编号
      * 用户编号
      */
      */
-    private Integer userId;
+    private Long userId;
     /**
     /**
      * 链路追踪编号
      * 链路追踪编号
      *
      *
@@ -148,6 +148,6 @@ public class InfApiErrorLogDO extends BaseDO {
      *
      *
      * 关联 {@link SysUserDO#getId()}
      * 关联 {@link SysUserDO#getId()}
      */
      */
-    private Integer processUserId;
+    private Long processUserId;
 
 
 }
 }

+ 4 - 4
src/main/java/cn/iocoder/dashboard/modules/infra/service/job/impl/InfJobServiceImpl.java

@@ -43,7 +43,7 @@ public class InfJobServiceImpl implements InfJobService {
     private SchedulerManager schedulerManager;
     private SchedulerManager schedulerManager;
 
 
     @Override
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public Long createJob(InfJobCreateReqVO createReqVO) throws SchedulerException {
     public Long createJob(InfJobCreateReqVO createReqVO) throws SchedulerException {
         validateCronExpression(createReqVO.getCronExpression());
         validateCronExpression(createReqVO.getCronExpression());
         // 校验唯一性
         // 校验唯一性
@@ -68,7 +68,7 @@ public class InfJobServiceImpl implements InfJobService {
     }
     }
 
 
     @Override
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void updateJob(InfJobUpdateReqVO updateReqVO) throws SchedulerException {
     public void updateJob(InfJobUpdateReqVO updateReqVO) throws SchedulerException {
         validateCronExpression(updateReqVO.getCronExpression());
         validateCronExpression(updateReqVO.getCronExpression());
         // 校验存在
         // 校验存在
@@ -88,7 +88,7 @@ public class InfJobServiceImpl implements InfJobService {
     }
     }
 
 
     @Override
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void updateJobStatus(Long id, Integer status) throws SchedulerException {
     public void updateJobStatus(Long id, Integer status) throws SchedulerException {
         // 校验 status
         // 校验 status
         if (!containsAny(status, InfJobStatusEnum.NORMAL.getStatus(), InfJobStatusEnum.STOP.getStatus())) {
         if (!containsAny(status, InfJobStatusEnum.NORMAL.getStatus(), InfJobStatusEnum.STOP.getStatus())) {
@@ -122,7 +122,7 @@ public class InfJobServiceImpl implements InfJobService {
     }
     }
 
 
     @Override
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void deleteJob(Long id) throws SchedulerException {
     public void deleteJob(Long id) throws SchedulerException {
         // 校验存在
         // 校验存在
         InfJobDO job = this.validateJobExists(id);
         InfJobDO job = this.validateJobExists(id);

+ 5 - 3
src/main/java/cn/iocoder/dashboard/modules/infra/service/logger/impl/InfApiAccessLogServiceImpl.java

@@ -9,12 +9,13 @@ import cn.iocoder.dashboard.modules.infra.dal.dataobject.logger.InfApiAccessLogD
 import cn.iocoder.dashboard.modules.infra.dal.mysql.logger.InfApiAccessLogMapper;
 import cn.iocoder.dashboard.modules.infra.dal.mysql.logger.InfApiAccessLogMapper;
 import cn.iocoder.dashboard.modules.infra.service.logger.InfApiAccessLogService;
 import cn.iocoder.dashboard.modules.infra.service.logger.InfApiAccessLogService;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.AsyncResult;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.validation.annotation.Validated;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
-import javax.validation.Valid;
 import java.util.List;
 import java.util.List;
+import java.util.concurrent.Future;
 
 
 /**
 /**
  * API 访问日志 Service 实现类
  * API 访问日志 Service 实现类
@@ -30,10 +31,11 @@ public class InfApiAccessLogServiceImpl implements InfApiAccessLogService {
 
 
     @Override
     @Override
     @Async
     @Async
-    public void createApiAccessLogAsync(ApiAccessLogCreateDTO createDTO) {
+    public Future<Boolean> createApiAccessLogAsync(ApiAccessLogCreateDTO createDTO) {
         // 插入
         // 插入
         InfApiAccessLogDO apiAccessLog = InfApiAccessLogConvert.INSTANCE.convert(createDTO);
         InfApiAccessLogDO apiAccessLog = InfApiAccessLogConvert.INSTANCE.convert(createDTO);
-        apiAccessLogMapper.insert(apiAccessLog);
+        int insert = apiAccessLogMapper.insert(apiAccessLog);
+        return new AsyncResult<>(insert == 1);
     }
     }
 
 
     @Override
     @Override

+ 6 - 3
src/main/java/cn/iocoder/dashboard/modules/infra/service/logger/impl/InfApiErrorLogServiceImpl.java

@@ -10,12 +10,14 @@ import cn.iocoder.dashboard.modules.infra.dal.mysql.logger.InfApiErrorLogMapper;
 import cn.iocoder.dashboard.modules.infra.enums.logger.InfApiErrorLogProcessStatusEnum;
 import cn.iocoder.dashboard.modules.infra.enums.logger.InfApiErrorLogProcessStatusEnum;
 import cn.iocoder.dashboard.modules.infra.service.logger.InfApiErrorLogService;
 import cn.iocoder.dashboard.modules.infra.service.logger.InfApiErrorLogService;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.AsyncResult;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.validation.annotation.Validated;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.Date;
 import java.util.List;
 import java.util.List;
+import java.util.concurrent.Future;
 
 
 import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.API_ERROR_LOG_NOT_FOUND;
 import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.API_ERROR_LOG_NOT_FOUND;
@@ -35,10 +37,11 @@ public class InfApiErrorLogServiceImpl implements InfApiErrorLogService {
 
 
     @Override
     @Override
     @Async
     @Async
-    public void createApiErrorLogAsync(ApiErrorLogCreateDTO createDTO) {
+    public Future<Boolean> createApiErrorLogAsync(ApiErrorLogCreateDTO createDTO) {
         InfApiErrorLogDO apiErrorLog = InfApiErrorLogConvert.INSTANCE.convert(createDTO);
         InfApiErrorLogDO apiErrorLog = InfApiErrorLogConvert.INSTANCE.convert(createDTO);
         apiErrorLog.setProcessStatus(InfApiErrorLogProcessStatusEnum.INIT.getStatus());
         apiErrorLog.setProcessStatus(InfApiErrorLogProcessStatusEnum.INIT.getStatus());
-        apiErrorLogMapper.insert(apiErrorLog);
+        int insert = apiErrorLogMapper.insert(apiErrorLog);
+        return new AsyncResult<>(insert == 1);
     }
     }
 
 
     @Override
     @Override
@@ -62,7 +65,7 @@ public class InfApiErrorLogServiceImpl implements InfApiErrorLogService {
         }
         }
         // 标记处理
         // 标记处理
         apiErrorLogMapper.updateById(InfApiErrorLogDO.builder().id(id).processStatus(processStatus)
         apiErrorLogMapper.updateById(InfApiErrorLogDO.builder().id(id).processStatus(processStatus)
-                .processUserId(processStatus).processTime(new Date()).build());
+                .processUserId(processUserId).processTime(new Date()).build());
     }
     }
 
 
 }
 }

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

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

+ 2 - 2
src/main/java/cn/iocoder/dashboard/modules/system/controller/user/SysUserController.java

@@ -40,8 +40,8 @@ public class SysUserController {
     @Resource
     @Resource
     private SysDeptService deptService;
     private SysDeptService deptService;
 
 
-    @ApiOperation("获得用户分页列表")
     @GetMapping("/page")
     @GetMapping("/page")
+    @ApiOperation("获得用户分页列表")
     @PreAuthorize("@ss.hasPermission('system:user:list')")
     @PreAuthorize("@ss.hasPermission('system:user:list')")
     public CommonResult<PageResult<SysUserPageItemRespVO>> pageUsers(@Validated SysUserPageReqVO reqVO) {
     public CommonResult<PageResult<SysUserPageItemRespVO>> pageUsers(@Validated SysUserPageReqVO reqVO) {
         // 获得用户分页列表
         // 获得用户分页列表
@@ -66,9 +66,9 @@ public class SysUserController {
     /**
     /**
      * 根据用户编号获取详细信息
      * 根据用户编号获取详细信息
      */
      */
+    @GetMapping("/get")
     @ApiOperation("获得用户详情")
     @ApiOperation("获得用户详情")
     @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
     @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
-    @GetMapping("/get")
 //    @PreAuthorize("@ss.hasPermi('system:user:query')")
 //    @PreAuthorize("@ss.hasPermi('system:user:query')")
     public CommonResult<SysUserRespVO> getInfo(@RequestParam("id") Long id) {
     public CommonResult<SysUserRespVO> getInfo(@RequestParam("id") Long id) {
         return success(SysUserConvert.INSTANCE.convert(userService.getUser(id)));
         return success(SysUserConvert.INSTANCE.convert(userService.getUser(id)));

+ 74 - 86
src/main/java/cn/iocoder/dashboard/modules/system/controller/user/SysUserProfileController.java

@@ -1,92 +1,80 @@
 package cn.iocoder.dashboard.modules.system.controller.user;
 package cn.iocoder.dashboard.modules.system.controller.user;
 
 
+import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
+import cn.iocoder.dashboard.common.pojo.CommonResult;
+import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserProfileRespVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserProfileUpdateReqVO;
+import cn.iocoder.dashboard.modules.system.convert.auth.SysAuthConvert;
+import cn.iocoder.dashboard.modules.system.convert.user.SysUserConvert;
+import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysRoleDO;
+import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
+import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
+import cn.iocoder.dashboard.modules.system.service.permission.SysRoleService;
+import cn.iocoder.dashboard.modules.system.service.user.SysUserService;
+import cn.iocoder.dashboard.util.collection.CollectionUtils;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
+import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.FILE_IS_EMPTY;
+
+/**
+ * @author niudehua
+ */
+@RestController
+@RequestMapping("/system/user/profile")
+@Api(tags = "用户个人中心")
+@Slf4j
 public class SysUserProfileController {
 public class SysUserProfileController {
 
 
-//    /**
-//     * 个人信息
-//     */
-//    @GetMapping
-//    public AjaxResult profile()
-//    {
-//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-//        SysUser user = loginUser.getUser();
-//        AjaxResult ajax = AjaxResult.success(user);
-//        ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername()));
-//        ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername()));
-//        return ajax;
-//    }
-//
-//    /**
-//     * 修改用户
-//     */
-//    @Log(title = "个人信息", businessType = BusinessType.UPDATE)
-//    @PutMapping
-//    public AjaxResult updateProfile(@RequestBody SysUser user)
-//    {
-//        if (userService.updateUserProfile(user) > 0)
-//        {
-//            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-//            // 更新缓存用户信息
-//            loginUser.getUser().setNickName(user.getNickName());
-//            loginUser.getUser().setPhonenumber(user.getPhonenumber());
-//            loginUser.getUser().setEmail(user.getEmail());
-//            loginUser.getUser().setSex(user.getSex());
-//            tokenService.setLoginUser(loginUser);
-//            return AjaxResult.success();
-//        }
-//        return AjaxResult.error("修改个人信息异常,请联系管理员");
-//    }
-//
-//    /**
-//     * 重置密码
-//     */
-//    @Log(title = "个人信息", businessType = BusinessType.UPDATE)
-//    @PutMapping("/updatePwd")
-//    public AjaxResult updatePwd(String oldPassword, String newPassword)
-//    {
-//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-//        String userName = loginUser.getUsername();
-//        String password = loginUser.getPassword();
-//        if (!SecurityUtils.matchesPassword(oldPassword, password))
-//        {
-//            return AjaxResult.error("修改密码失败,旧密码错误");
-//        }
-//        if (SecurityUtils.matchesPassword(newPassword, password))
-//        {
-//            return AjaxResult.error("新密码不能与旧密码相同");
-//        }
-//        if (userService.resetUserPwd(userName, SecurityUtils.encryptPassword(newPassword)) > 0)
-//        {
-//            // 更新缓存用户密码
-//            loginUser.getUser().setPassword(SecurityUtils.encryptPassword(newPassword));
-//            tokenService.setLoginUser(loginUser);
-//            return AjaxResult.success();
-//        }
-//        return AjaxResult.error("修改密码异常,请联系管理员");
-//    }
-//
-//    /**
-//     * 头像上传
-//     */
-//    @Log(title = "用户头像", businessType = BusinessType.UPDATE)
-//    @PostMapping("/avatar")
-//    public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws IOException
-//    {
-//        if (!file.isEmpty())
-//        {
-//            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-//            String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file);
-//            if (userService.updateUserAvatar(loginUser.getUsername(), avatar))
-//            {
-//                AjaxResult ajax = AjaxResult.success();
-//                ajax.put("imgUrl", avatar);
-//                // 更新缓存用户头像
-//                loginUser.getUser().setAvatar(avatar);
-//                tokenService.setLoginUser(loginUser);
-//                return ajax;
-//            }
-//        }
-//        return AjaxResult.error("上传图片异常,请联系管理员");
-//    }
+    @Resource
+    private SysUserService userService;
+    @Resource
+    private SysPermissionService permissionService;
+    @Resource
+    private SysRoleService roleService;
+
+    @GetMapping("/get")
+    @ApiOperation("获得登录用户信息")
+    public CommonResult<SysUserProfileRespVO> profile() {
+        // 获取用户信息
+        Long userId = SecurityFrameworkUtils.getLoginUserId();
+        SysUserDO user = userService.getUser(userId);
+        SysUserProfileRespVO userProfileRespVO = SysUserConvert.INSTANCE.convert03(user);
+        List<SysRoleDO> userRoles = roleService.listRolesFromCache(permissionService.listUserRoleIs(userId));
+        userProfileRespVO.setRoles(CollectionUtils.convertSet(userRoles, SysUserConvert.INSTANCE::convert));
+        return success(userProfileRespVO);
+    }
+
+    @PostMapping("/update")
+    @ApiOperation("修改用户个人信息")
+    public CommonResult<Boolean> updateProfile(@RequestBody SysUserProfileUpdateReqVO reqVO, HttpServletRequest request) {
+        userService.updateUserProfile(reqVO);
+        SecurityFrameworkUtils.setLoginUser(SysAuthConvert.INSTANCE.convert(reqVO), request);
+        return success(true);
+    }
 
 
+    @PostMapping("/upload-avatar")
+    @ApiOperation("上传用户个人头像")
+    public CommonResult<Boolean> uploadAvatar(@RequestParam("avatarFile") MultipartFile file) throws IOException {
+        if (file.isEmpty()) {
+            throw ServiceExceptionUtil.exception(FILE_IS_EMPTY);
+        }
+        userService.updateAvatar(SecurityFrameworkUtils.getLoginUserId(), file.getInputStream());
+        return success(true);
+    }
 }
 }

+ 37 - 0
src/main/java/cn/iocoder/dashboard/modules/system/controller/user/vo/user/SysUserProfileRespVO.java

@@ -0,0 +1,37 @@
+package cn.iocoder.dashboard.modules.system.controller.user.vo.user;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.util.Set;
+
+
+@ApiModel("用户个人中心信息 Response VO")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class SysUserProfileRespVO extends SysUserRespVO {
+
+    /**
+     * 所属角色
+     */
+    @ApiModelProperty(value = "所属角色", required = true, example = "123456")
+    private Set<Role> roles;
+
+    @ApiModel("角色")
+    @Data
+    public static class Role {
+
+        @ApiModelProperty(value = "角色编号", required = true, example = "1")
+        private Long id;
+
+        @ApiModelProperty(value = "角色名称", required = true, example = "普通角色")
+        private String name;
+
+    }
+}

+ 44 - 0
src/main/java/cn/iocoder/dashboard/modules/system/controller/user/vo/user/SysUserProfileUpdateReqVO.java

@@ -0,0 +1,44 @@
+package cn.iocoder.dashboard.modules.system.controller.user.vo.user;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+@ApiModel("用户个人信息更新 Request VO")
+@Data
+public class SysUserProfileUpdateReqVO {
+
+    @ApiModelProperty(value = "用户编号", required = true, example = "1024")
+    @NotNull(message = "用户编号不能为空")
+    private Long id;
+
+    @ApiModelProperty(value = "用户昵称", required = true, example = "芋艿")
+    @Size(max = 30, message = "用户昵称长度不能超过30个字符")
+    private String nickname;
+
+    @ApiModelProperty(value = "用户邮箱", example = "yudao@iocoder.cn")
+    @Email(message = "邮箱格式不正确")
+    @Size(max = 50, message = "邮箱长度不能超过50个字符")
+    private String email;
+
+    @ApiModelProperty(value = "手机号码", example = "15601691300")
+    @Size(max = 11, message = "手机号码长度不能超过11个字符")
+    private String mobile;
+
+    @ApiModelProperty(value = "用户性别", example = "1", notes = "参见 SysSexEnum 枚举类")
+    private Integer sex;
+
+    @ApiModelProperty(value = "用户头像", example = "http://www.iocoder.cn/xxx.png")
+    private String avatar;
+
+    @ApiModelProperty(value = "旧密码", required = true, example = "123456")
+    private String oldPassword;
+
+    @ApiModelProperty(value = "新密码", required = true, example = "654321")
+    private String newPassword;
+
+}

+ 16 - 8
src/main/java/cn/iocoder/dashboard/modules/system/convert/auth/SysAuthConvert.java

@@ -3,6 +3,7 @@ package cn.iocoder.dashboard.modules.system.convert.auth;
 import cn.iocoder.dashboard.framework.security.core.LoginUser;
 import cn.iocoder.dashboard.framework.security.core.LoginUser;
 import cn.iocoder.dashboard.modules.system.controller.auth.vo.auth.SysAuthMenuRespVO;
 import cn.iocoder.dashboard.modules.system.controller.auth.vo.auth.SysAuthMenuRespVO;
 import cn.iocoder.dashboard.modules.system.controller.auth.vo.auth.SysAuthPermissionInfoRespVO;
 import cn.iocoder.dashboard.modules.system.controller.auth.vo.auth.SysAuthPermissionInfoRespVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserProfileUpdateReqVO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysMenuDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysMenuDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysRoleDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysRoleDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
@@ -13,26 +14,33 @@ import org.mapstruct.Mapping;
 import org.mapstruct.factory.Mappers;
 import org.mapstruct.factory.Mappers;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
 
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 
 
 @Mapper
 @Mapper
 public interface SysAuthConvert {
 public interface SysAuthConvert {
 
 
     SysAuthConvert INSTANCE = Mappers.getMapper(SysAuthConvert.class);
     SysAuthConvert INSTANCE = Mappers.getMapper(SysAuthConvert.class);
 
 
-    @Mapping(source = "updateTime", target = "updateTime", ignore = true) // 字段相同,但是含义不同,忽略
+    @Mapping(source = "updateTime", target = "updateTime", ignore = true)
+        // 字段相同,但是含义不同,忽略
     LoginUser convert(SysUserDO bean);
     LoginUser convert(SysUserDO bean);
 
 
     default SysAuthPermissionInfoRespVO convert(SysUserDO user, List<SysRoleDO> roleList, List<SysMenuDO> menuList) {
     default SysAuthPermissionInfoRespVO convert(SysUserDO user, List<SysRoleDO> roleList, List<SysMenuDO> menuList) {
         return SysAuthPermissionInfoRespVO.builder()
         return SysAuthPermissionInfoRespVO.builder()
-                .user(SysAuthPermissionInfoRespVO.UserVO.builder().nickname(user.getNickname()).avatar(user.getAvatar()).build())
-                .roles(CollectionUtils.convertSet(roleList, SysRoleDO::getCode))
-                .permissions(CollectionUtils.convertSet(menuList, SysMenuDO::getPermission))
-                .build();
+            .user(SysAuthPermissionInfoRespVO.UserVO.builder().nickname(user.getNickname()).avatar(user.getAvatar()).build())
+            .roles(CollectionUtils.convertSet(roleList, SysRoleDO::getCode))
+            .permissions(CollectionUtils.convertSet(menuList, SysMenuDO::getPermission))
+            .build();
     }
     }
 
 
     SysAuthMenuRespVO convertTreeNode(SysMenuDO menu);
     SysAuthMenuRespVO convertTreeNode(SysMenuDO menu);
 
 
+    LoginUser convert(SysUserProfileUpdateReqVO reqVO);
+
     /**
     /**
      * 将菜单列表,构建成菜单树
      * 将菜单列表,构建成菜单树
      *
      *
@@ -47,12 +55,12 @@ public interface SysAuthConvert {
         Map<Long, SysAuthMenuRespVO> treeNodeMap = new LinkedHashMap<>();
         Map<Long, SysAuthMenuRespVO> treeNodeMap = new LinkedHashMap<>();
         menuList.forEach(menu -> treeNodeMap.put(menu.getId(), SysAuthConvert.INSTANCE.convertTreeNode(menu)));
         menuList.forEach(menu -> treeNodeMap.put(menu.getId(), SysAuthConvert.INSTANCE.convertTreeNode(menu)));
         // 处理父子关系
         // 处理父子关系
-        treeNodeMap.values().stream().filter(node -> !node.getParentId().equals(MenuIdEnum.ROOT.getId())).forEach((childNode) -> {
+        treeNodeMap.values().stream().filter(node -> !node.getParentId().equals(MenuIdEnum.ROOT.getId())).forEach(childNode -> {
             // 获得父节点
             // 获得父节点
             SysAuthMenuRespVO parentNode = treeNodeMap.get(childNode.getParentId());
             SysAuthMenuRespVO parentNode = treeNodeMap.get(childNode.getParentId());
             if (parentNode == null) {
             if (parentNode == null) {
                 LoggerFactory.getLogger(getClass()).error("[buildRouterTree][resource({}) 找不到父资源({})]",
                 LoggerFactory.getLogger(getClass()).error("[buildRouterTree][resource({}) 找不到父资源({})]",
-                        childNode.getId(), childNode.getParentId());
+                    childNode.getId(), childNode.getParentId());
                 return;
                 return;
             }
             }
             // 将自己添加到父节点中
             // 将自己添加到父节点中

+ 16 - 1
src/main/java/cn/iocoder/dashboard/modules/system/convert/user/SysUserConvert.java

@@ -1,7 +1,14 @@
 package cn.iocoder.dashboard.modules.system.convert.user;
 package cn.iocoder.dashboard.modules.system.convert.user;
 
 
-import cn.iocoder.dashboard.modules.system.controller.user.vo.user.*;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserCreateReqVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserExcelVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserImportExcelVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserPageItemRespVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserProfileRespVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserProfileUpdateReqVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserUpdateReqVO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.dept.SysDeptDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.dept.SysDeptDO;
+import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysRoleDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
 import org.mapstruct.factory.Mappers;
@@ -23,4 +30,12 @@ public interface SysUserConvert {
 
 
     SysUserDO convert(SysUserImportExcelVO bean);
     SysUserDO convert(SysUserImportExcelVO bean);
 
 
+    SysUserProfileRespVO convert03(SysUserDO bean);
+
+    SysUserProfileRespVO.Role convert(SysRoleDO bean);
+
+    SysUserDO convert(SysUserProfileUpdateReqVO bean);
+
+
+
 }
 }

+ 3 - 0
src/main/java/cn/iocoder/dashboard/modules/system/enums/SysErrorCodeConstants.java

@@ -40,6 +40,7 @@ public interface SysErrorCodeConstants {
     ErrorCode USER_EMAIL_EXISTS = new ErrorCode(1002004002, "邮箱已经存在");
     ErrorCode USER_EMAIL_EXISTS = new ErrorCode(1002004002, "邮箱已经存在");
     ErrorCode USER_NOT_EXISTS = new ErrorCode(1002004003, "用户不存在");
     ErrorCode USER_NOT_EXISTS = new ErrorCode(1002004003, "用户不存在");
     ErrorCode USER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1002004004, "导入用户数据不能为空!");
     ErrorCode USER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1002004004, "导入用户数据不能为空!");
+    ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1002004005, "用户密码校验失败");
 
 
     // ========== 部门模块 1002005000 ==========
     // ========== 部门模块 1002005000 ==========
     ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1002004001, "已经存在该名字的部门");
     ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1002004001, "已经存在该名字的部门");
@@ -74,5 +75,7 @@ public interface SysErrorCodeConstants {
 
 
     // ========== 文件 1002009000 ==========
     // ========== 文件 1002009000 ==========
     ErrorCode FILE_PATH_EXISTS = new ErrorCode(1002009001, "文件路径已经存在");
     ErrorCode FILE_PATH_EXISTS = new ErrorCode(1002009001, "文件路径已经存在");
+    ErrorCode FILE_UPLOAD_FAILED = new ErrorCode(1002009002, "文件上传失败");
+    ErrorCode FILE_IS_EMPTY= new ErrorCode(1002009003, "文件为空");
 
 
 }
 }

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java

@@ -206,7 +206,7 @@ public class SysMenuServiceImpl implements SysMenuService {
      *
      *
      * @param menuId 菜单编号
      * @param menuId 菜单编号
      */
      */
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void deleteMenu(Long menuId) {
     public void deleteMenu(Long menuId) {
         // 校验更新的菜单是否存在
         // 校验更新的菜单是否存在
         if (menuMapper.selectById(menuId) == null) {
         if (menuMapper.selectById(menuId) == null) {

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java

@@ -176,7 +176,7 @@ public class SysPermissionServiceImpl implements SysPermissionService {
     }
     }
 
 
     @Override
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void assignRoleMenu(Long roleId, Set<Long> menuIds) {
     public void assignRoleMenu(Long roleId, Set<Long> menuIds) {
         // 获得角色拥有菜单编号
         // 获得角色拥有菜单编号
         Set<Long> dbMenuIds = CollectionUtils.convertSet(roleMenuMapper.selectListByRoleId(roleId),
         Set<Long> dbMenuIds = CollectionUtils.convertSet(roleMenuMapper.selectListByRoleId(roleId),

+ 1 - 1
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java

@@ -174,7 +174,7 @@ public class SysRoleServiceImpl implements SysRoleService {
     }
     }
 
 
     @Override
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void deleteRole(Long id) {
     public void deleteRole(Long id) {
         // 校验是否可以更新
         // 校验是否可以更新
         this.checkUpdateRole(id);
         this.checkUpdateRole(id);

+ 27 - 4
src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserService.java

@@ -2,10 +2,17 @@ package cn.iocoder.dashboard.modules.system.service.user;
 
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.dashboard.common.pojo.PageResult;
 import cn.iocoder.dashboard.common.pojo.PageResult;
-import cn.iocoder.dashboard.modules.system.controller.user.vo.user.*;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserCreateReqVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserExportReqVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserImportExcelVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserImportRespVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserPageReqVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserProfileUpdateReqVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserUpdateReqVO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
 import cn.iocoder.dashboard.util.collection.CollectionUtils;
 import cn.iocoder.dashboard.util.collection.CollectionUtils;
 
 
+import java.io.InputStream;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.List;
 import java.util.List;
@@ -102,6 +109,14 @@ public interface SysUserService {
      */
      */
     void updateUser(SysUserUpdateReqVO reqVO);
     void updateUser(SysUserUpdateReqVO reqVO);
 
 
+    /**
+     * 修改用户个人信息
+     *
+     * @param reqVO 用户个人信息
+     * @return 修改结果
+     */
+    void updateUserProfile(SysUserProfileUpdateReqVO reqVO);
+
     /**
     /**
      * 删除用户
      * 删除用户
      *
      *
@@ -112,7 +127,7 @@ public interface SysUserService {
     /**
     /**
      * 修改密码
      * 修改密码
      *
      *
-     * @param id 用户编号
+     * @param id       用户编号
      * @param password 密码
      * @param password 密码
      */
      */
     void updateUserPassword(Long id, String password);
     void updateUserPassword(Long id, String password);
@@ -120,7 +135,7 @@ public interface SysUserService {
     /**
     /**
      * 修改密码
      * 修改密码
      *
      *
-     * @param id 用户编号
+     * @param id     用户编号
      * @param status 状态
      * @param status 状态
      */
      */
     void updateUserStatus(Long id, Integer status);
     void updateUserStatus(Long id, Integer status);
@@ -128,12 +143,20 @@ public interface SysUserService {
     /**
     /**
      * 批量导入用户
      * 批量导入用户
      *
      *
-     * @param importUsers 导入用户列表
+     * @param importUsers     导入用户列表
      * @param isUpdateSupport 是否支持更新
      * @param isUpdateSupport 是否支持更新
      * @return 导入结果
      * @return 导入结果
      */
      */
     SysUserImportRespVO importUsers(List<SysUserImportExcelVO> importUsers, boolean isUpdateSupport);
     SysUserImportRespVO importUsers(List<SysUserImportExcelVO> importUsers, boolean isUpdateSupport);
 
 
+    /**
+     * 更新用户头像
+     *
+     * @param id         用户 id
+     * @param avatarFile 头像文件
+     */
+    void updateAvatar(Long id, InputStream avatarFile);
+
 //
 //
 //    /**
 //    /**
 //     * 修改用户基本信息
 //     * 修改用户基本信息

+ 81 - 21
src/main/java/cn/iocoder/dashboard/modules/system/service/user/SysUserServiceImpl.java

@@ -1,17 +1,26 @@
 package cn.iocoder.dashboard.modules.system.service.user;
 package cn.iocoder.dashboard.modules.system.service.user;
 
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
 import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
 import cn.iocoder.dashboard.common.exception.ServiceException;
 import cn.iocoder.dashboard.common.exception.ServiceException;
 import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
 import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
 import cn.iocoder.dashboard.common.pojo.PageResult;
 import cn.iocoder.dashboard.common.pojo.PageResult;
-import cn.iocoder.dashboard.modules.system.controller.user.vo.user.*;
+import cn.iocoder.dashboard.modules.infra.service.file.InfFileService;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserCreateReqVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserExportReqVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserImportExcelVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserImportRespVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserPageReqVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserProfileUpdateReqVO;
+import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserUpdateReqVO;
 import cn.iocoder.dashboard.modules.system.convert.user.SysUserConvert;
 import cn.iocoder.dashboard.modules.system.convert.user.SysUserConvert;
-import cn.iocoder.dashboard.modules.system.dal.mysql.user.SysUserMapper;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.dept.SysDeptDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.dept.SysDeptDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.dept.SysPostDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.dept.SysPostDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
+import cn.iocoder.dashboard.modules.system.dal.mysql.user.SysUserMapper;
 import cn.iocoder.dashboard.modules.system.service.dept.SysDeptService;
 import cn.iocoder.dashboard.modules.system.service.dept.SysDeptService;
 import cn.iocoder.dashboard.modules.system.service.dept.SysPostService;
 import cn.iocoder.dashboard.modules.system.service.dept.SysPostService;
 import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
 import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
@@ -22,7 +31,14 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
-import java.util.*;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 
 import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
 import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
 
 
@@ -49,18 +65,8 @@ public class SysUserServiceImpl implements SysUserService {
     @Resource
     @Resource
     private PasswordEncoder passwordEncoder;
     private PasswordEncoder passwordEncoder;
 
 
-//    /**
-//     * 根据条件分页查询用户列表
-//     *
-//     * @param user 用户信息
-//     * @return 用户信息集合信息
-//     */
-//    @Override
-//    @DataScope(deptAlias = "d", userAlias = "u")
-//    public List<SysUser> selectUserList(SysUser user)
-//    {
-//        return userMapper.selectUserList(user);
-//    }
+    @Resource
+    private InfFileService fileService;
 
 
     @Override
     @Override
     public SysUserDO getUserByUserName(String username) {
     public SysUserDO getUserByUserName(String username) {
@@ -108,7 +114,7 @@ public class SysUserServiceImpl implements SysUserService {
             return Collections.emptySet();
             return Collections.emptySet();
         }
         }
         Set<Long> deptIds = CollectionUtils.convertSet(deptService.listDeptsByParentIdFromCache(
         Set<Long> deptIds = CollectionUtils.convertSet(deptService.listDeptsByParentIdFromCache(
-                deptId, true), SysDeptDO::getId);
+            deptId, true), SysDeptDO::getId);
         deptIds.add(deptId); // 包括自身
         deptIds.add(deptId); // 包括自身
         return deptIds;
         return deptIds;
     }
     }
@@ -117,7 +123,7 @@ public class SysUserServiceImpl implements SysUserService {
     public Long createUser(SysUserCreateReqVO reqVO) {
     public Long createUser(SysUserCreateReqVO reqVO) {
         // 校验正确性
         // 校验正确性
         this.checkCreateOrUpdate(null, reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(),
         this.checkCreateOrUpdate(null, reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(),
-                reqVO.getDeptId(), reqVO.getPostIds());
+            reqVO.getDeptId(), reqVO.getPostIds());
         // 插入用户
         // 插入用户
         SysUserDO user = SysUserConvert.INSTANCE.convert(reqVO);
         SysUserDO user = SysUserConvert.INSTANCE.convert(reqVO);
         user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启
         user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启
@@ -130,12 +136,31 @@ public class SysUserServiceImpl implements SysUserService {
     public void updateUser(SysUserUpdateReqVO reqVO) {
     public void updateUser(SysUserUpdateReqVO reqVO) {
         // 校验正确性
         // 校验正确性
         this.checkCreateOrUpdate(reqVO.getId(), reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(),
         this.checkCreateOrUpdate(reqVO.getId(), reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(),
-                reqVO.getDeptId(), reqVO.getPostIds());
+            reqVO.getDeptId(), reqVO.getPostIds());
         // 更新用户
         // 更新用户
         SysUserDO updateObj = SysUserConvert.INSTANCE.convert(reqVO);
         SysUserDO updateObj = SysUserConvert.INSTANCE.convert(reqVO);
         userMapper.updateById(updateObj);
         userMapper.updateById(updateObj);
     }
     }
 
 
+    @Override
+    public void updateUserProfile(SysUserProfileUpdateReqVO reqVO) {
+        // 校验正确性
+        this.checkUserExists(reqVO.getId());
+        this.checkEmailUnique(reqVO.getId(), reqVO.getEmail());
+        this.checkMobileUnique(reqVO.getId(), reqVO.getMobile());
+        // 校验填写密码
+        String encode = null;
+        if (this.checkOldPassword(reqVO.getId(), reqVO.getOldPassword(), reqVO.getNewPassword())) {
+            // 更新密码
+            encode = passwordEncoder.encode(reqVO.getNewPassword());
+        }
+        SysUserDO updateObj = SysUserConvert.INSTANCE.convert(reqVO);
+        if (StrUtil.isNotBlank(encode)) {
+            updateObj.setPassword(encode);
+        }
+        userMapper.updateById(updateObj);
+    }
+
     @Override
     @Override
     public void deleteUser(Long id) {
     public void deleteUser(Long id) {
         // 校验用户存在
         // 校验用户存在
@@ -278,19 +303,42 @@ public class SysUserServiceImpl implements SysUserService {
         });
         });
     }
     }
 
 
+    /**
+     * 校验旧密码、新密码
+     *
+     * @param id          用户 id
+     * @param oldPassword 旧密码
+     * @param newPassword 新密码
+     * @return 校验结果
+     */
+    private boolean checkOldPassword(Long id, String oldPassword, String newPassword) {
+        if (id == null || StrUtil.isBlank(oldPassword) || StrUtil.isBlank(newPassword)) {
+            return false;
+        }
+        SysUserDO user = userMapper.selectById(id);
+        if (user == null) {
+            throw ServiceExceptionUtil.exception(USER_NOT_EXISTS);
+        }
+
+        if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
+            throw ServiceExceptionUtil.exception(USER_PASSWORD_FAILED);
+        }
+        return true;
+    }
+
     @Override
     @Override
-    @Transactional // 添加事务,异常则回滚所有导入
+    @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
     public SysUserImportRespVO importUsers(List<SysUserImportExcelVO> importUsers, boolean isUpdateSupport) {
     public SysUserImportRespVO importUsers(List<SysUserImportExcelVO> importUsers, boolean isUpdateSupport) {
         if (CollUtil.isEmpty(importUsers)) {
         if (CollUtil.isEmpty(importUsers)) {
             throw ServiceExceptionUtil.exception(USER_IMPORT_LIST_IS_EMPTY);
             throw ServiceExceptionUtil.exception(USER_IMPORT_LIST_IS_EMPTY);
         }
         }
         SysUserImportRespVO respVO = SysUserImportRespVO.builder().createUsernames(new ArrayList<>())
         SysUserImportRespVO respVO = SysUserImportRespVO.builder().createUsernames(new ArrayList<>())
-                .updateUsernames(new ArrayList<>()).failureUsernames(new LinkedHashMap<>()).build();
+            .updateUsernames(new ArrayList<>()).failureUsernames(new LinkedHashMap<>()).build();
         importUsers.forEach(importUser -> {
         importUsers.forEach(importUser -> {
             // 校验,判断是否有不符合的原因
             // 校验,判断是否有不符合的原因
             try {
             try {
                 checkCreateOrUpdate(null, null, importUser.getMobile(), importUser.getEmail(),
                 checkCreateOrUpdate(null, null, importUser.getMobile(), importUser.getEmail(),
-                        importUser.getDeptId(), null);
+                    importUser.getDeptId(), null);
             } catch (ServiceException ex) {
             } catch (ServiceException ex) {
                 respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage());
                 respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage());
                 return;
                 return;
@@ -316,4 +364,16 @@ public class SysUserServiceImpl implements SysUserService {
         return respVO;
         return respVO;
     }
     }
 
 
+    @Override
+    public void updateAvatar(Long id, InputStream avatarFile) {
+        this.checkUserExists(id);
+        // 存储文件
+        String avatar = fileService.createFile(IdUtil.fastUUID(), IoUtil.readBytes(avatarFile));
+        // 更新路径
+        SysUserDO sysUserDO = new SysUserDO();
+        sysUserDO.setId(id);
+        sysUserDO.setAvatar(avatar);
+        userMapper.updateById(sysUserDO);
+    }
+
 }
 }

+ 5 - 5
src/main/java/cn/iocoder/dashboard/modules/tool/service/codegen/impl/ToolCodegenServiceImpl.java

@@ -109,7 +109,7 @@ public class ToolCodegenServiceImpl implements ToolCodegenService {
     }
     }
 
 
     @Override
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public List<Long> createCodegenListFromDB(List<String> tableNames) {
     public List<Long> createCodegenListFromDB(List<String> tableNames) {
         List<Long> ids = new ArrayList<>(tableNames.size());
         List<Long> ids = new ArrayList<>(tableNames.size());
         // 遍历添加。虽然效率会低一点,但是没必要做成完全批量,因为不会这么大量
         // 遍历添加。虽然效率会低一点,但是没必要做成完全批量,因为不会这么大量
@@ -118,7 +118,7 @@ public class ToolCodegenServiceImpl implements ToolCodegenService {
     }
     }
 
 
     @Override
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void updateCodegen(ToolCodegenUpdateReqVO updateReqVO) {
     public void updateCodegen(ToolCodegenUpdateReqVO updateReqVO) {
         // 校验是否已经存在
         // 校验是否已经存在
         if (codegenTableMapper.selectById(updateReqVO.getTable().getId()) == null) {
         if (codegenTableMapper.selectById(updateReqVO.getTable().getId()) == null) {
@@ -134,7 +134,7 @@ public class ToolCodegenServiceImpl implements ToolCodegenService {
     }
     }
 
 
     @Override
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void syncCodegenFromDB(Long tableId) {
     public void syncCodegenFromDB(Long tableId) {
         // 校验是否已经存在
         // 校验是否已经存在
         ToolCodegenTableDO table = codegenTableMapper.selectById(tableId);
         ToolCodegenTableDO table = codegenTableMapper.selectById(tableId);
@@ -149,7 +149,7 @@ public class ToolCodegenServiceImpl implements ToolCodegenService {
     }
     }
 
 
     @Override
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void syncCodegenFromSQL(Long tableId, String sql) {
     public void syncCodegenFromSQL(Long tableId, String sql) {
         // 校验是否已经存在
         // 校验是否已经存在
         ToolCodegenTableDO table = codegenTableMapper.selectById(tableId);
         ToolCodegenTableDO table = codegenTableMapper.selectById(tableId);
@@ -201,7 +201,7 @@ public class ToolCodegenServiceImpl implements ToolCodegenService {
     }
     }
 
 
     @Override
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void deleteCodegen(Long tableId) {
     public void deleteCodegen(Long tableId) {
         // 校验是否已经存在
         // 校验是否已经存在
         if (codegenTableMapper.selectById(tableId) == null) {
         if (codegenTableMapper.selectById(tableId) == null) {

+ 2 - 2
src/main/resources/application-dev.yaml

@@ -145,14 +145,14 @@ yudao:
   swagger:
   swagger:
     title: 管理后台
     title: 管理后台
     description: 提供管理员管理的所有功能
     description: 提供管理员管理的所有功能
-    version: ${yudao.info.base-package}
+    version: ${yudao.info.version}
     base-package: ${yudao.info.base-package}.modules
     base-package: ${yudao.info.base-package}.modules
   captcha:
   captcha:
     timeout: 5m
     timeout: 5m
     width: 160
     width: 160
     height: 60
     height: 60
   file:
   file:
-    base-path: http://127.0.0.1:${server.port}/${yudao.web.api-prefix}/infra/file/get/
+    base-path: http://127.0.0.1:${server.port}${yudao.web.api-prefix}/system/file/get/
   codegen:
   codegen:
     base-package: ${yudao.info.base-package}
     base-package: ${yudao.info.base-package}
     db-schemas: ${spring.datasource.name}
     db-schemas: ${spring.datasource.name}

+ 2 - 2
src/main/resources/application-local.yaml

@@ -145,14 +145,14 @@ yudao:
   swagger:
   swagger:
     title: 管理后台
     title: 管理后台
     description: 提供管理员管理的所有功能
     description: 提供管理员管理的所有功能
-    version: ${yudao.info.base-package}
+    version: ${yudao.info.version}
     base-package: ${yudao.info.base-package}.modules
     base-package: ${yudao.info.base-package}.modules
   captcha:
   captcha:
     timeout: 5m
     timeout: 5m
     width: 160
     width: 160
     height: 60
     height: 60
   file:
   file:
-    base-path: http://127.0.0.1:${server.port}/${yudao.web.api-prefix}/infra/file/get/
+    base-path: http://127.0.0.1:${server.port}${yudao.web.api-prefix}/system/file/get/
   codegen:
   codegen:
     base-package: ${yudao.info.base-package}
     base-package: ${yudao.info.base-package}
     db-schemas: ${spring.datasource.name}
     db-schemas: ${spring.datasource.name}

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

@@ -16,9 +16,9 @@ import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.jdbc.Sql;
 import org.springframework.test.context.jdbc.Sql;
 
 
 /**
 /**
- * 依赖内存 DB 的单元测试
+ * 依赖内存 DB + Redis 的单元测试
  *
  *
- * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法
+ * 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis
  *
  *
  * @author 芋道源码
  * @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;
 package cn.iocoder.dashboard.config;
 
 
 import com.github.fppt.jedismock.RedisServer;
 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.autoconfigure.data.redis.RedisProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
 
 
 import java.io.IOException;
 import java.io.IOException;
 
 
 @Configuration(proxyBeanMethods = false)
 @Configuration(proxyBeanMethods = false)
+@Lazy(false) // 禁止延迟加载
 @EnableConfigurationProperties(RedisProperties.class)
 @EnableConfigurationProperties(RedisProperties.class)
-@AutoConfigureBefore({RedisAutoConfiguration.class, RedissonAutoConfiguration.class}) // 在 Redis 自动配置前,进行初始化
 public class RedisTestConfiguration {
 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;
 package cn.iocoder.dashboard.framework.quartz.core.scheduler;
 
 
 import cn.hutool.core.util.StrUtil;
 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 cn.iocoder.dashboard.modules.system.job.auth.SysUserSessionTimeoutJob;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 import org.quartz.SchedulerException;
 import org.quartz.SchedulerException;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 
 
-class SchedulerManagerTest extends BaseSpringBootUnitTest {
+class SchedulerManagerTest extends BaseDbUnitTest {
 
 
     @Resource
     @Resource
     private SchedulerManager schedulerManager;
     private SchedulerManager schedulerManager;

+ 177 - 0
src/test/java/cn/iocoder/dashboard/modules/infra/service/logger/InfApiAccessLogServiceImplTest.java

@@ -0,0 +1,177 @@
+package cn.iocoder.dashboard.modules.infra.service.logger;
+
+import cn.hutool.core.util.RandomUtil;
+import cn.iocoder.dashboard.BaseDbUnitTest;
+import cn.iocoder.dashboard.common.enums.UserTypeEnum;
+import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
+import cn.iocoder.dashboard.common.pojo.PageResult;
+import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO;
+import cn.iocoder.dashboard.modules.infra.controller.logger.vo.apiaccesslog.InfApiAccessLogExportReqVO;
+import cn.iocoder.dashboard.modules.infra.controller.logger.vo.apiaccesslog.InfApiAccessLogPageReqVO;
+import cn.iocoder.dashboard.modules.infra.dal.dataobject.logger.InfApiAccessLogDO;
+import cn.iocoder.dashboard.modules.infra.dal.mysql.logger.InfApiAccessLogMapper;
+import cn.iocoder.dashboard.modules.infra.service.logger.impl.InfApiAccessLogServiceImpl;
+import cn.iocoder.dashboard.util.RandomUtils;
+import cn.iocoder.dashboard.util.object.ObjectUtils;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Future;
+
+import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals;
+import static cn.iocoder.dashboard.util.date.DateUtils.buildTime;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * {@link InfApiAccessLogServiceImpl} 单元测试
+ */
+@Import(InfApiAccessLogServiceImpl.class)
+public class InfApiAccessLogServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private InfApiAccessLogService infApiAccessLogServiceImpl;
+
+    @Resource
+    private InfApiAccessLogMapper infApiAccessLogMapper;
+
+
+    @Test
+    public void testCreateApiAccessLogAsync() throws Exception {
+        ApiAccessLogCreateDTO createDTO = RandomUtils.randomPojo(
+                ApiAccessLogCreateDTO.class,
+                dto -> dto.setUserType(RandomUtil.randomEle(UserTypeEnum.values()).getValue())
+        );
+
+        // 执行service方法
+        Future<Boolean> future = infApiAccessLogServiceImpl.createApiAccessLogAsync(createDTO);
+
+        // 等异步执行完
+        future.get();
+
+        InfApiAccessLogDO infApiAccessLogDO = infApiAccessLogMapper.selectOne(null);
+        // 断言
+        assertNotNull(infApiAccessLogDO);
+        // 断言,忽略基本字段
+        assertPojoEquals(createDTO, infApiAccessLogDO);
+    }
+
+
+    @Test
+    public void testGetApiAccessLogPage() {
+        // 构造测试数据
+        long userId = 2233L;
+        int userType = UserTypeEnum.ADMIN.getValue();
+        String applicationName = "ruoyi-test";
+        String requestUrl = "foo";
+        Date beginTime = buildTime(2021, 3, 13);
+        int duration = 1000;
+        int resultCode = GlobalErrorCodeConstants.SUCCESS.getCode();
+
+        InfApiAccessLogDO infApiAccessLogDO = RandomUtils.randomPojo(InfApiAccessLogDO.class, dto -> {
+            dto.setUserId(userId);
+            dto.setUserType(userType);
+            dto.setApplicationName(applicationName);
+            dto.setRequestUrl(requestUrl);
+            dto.setBeginTime(beginTime);
+            dto.setDuration(duration);
+            dto.setResultCode(resultCode);
+        });
+        infApiAccessLogMapper.insert(infApiAccessLogDO);
+
+        // 下面几个都是不匹配的数据
+        // userId 不同的
+        infApiAccessLogMapper.insert(ObjectUtils.clone(infApiAccessLogDO, logDO -> logDO.setUserId(3344L)));
+        // userType
+        infApiAccessLogMapper.insert(ObjectUtils.clone(infApiAccessLogDO, logDO -> logDO.setUserType(UserTypeEnum.MEMBER.getValue())));
+        // applicationName 不同的
+        infApiAccessLogMapper.insert(ObjectUtils.clone(infApiAccessLogDO, logDO -> logDO.setApplicationName("test")));
+        // requestUrl 不同的
+        infApiAccessLogMapper.insert(ObjectUtils.clone(infApiAccessLogDO, logDO -> logDO.setRequestUrl("bar")));
+        // 构造一个早期时间 2021-02-06 00:00:00
+        infApiAccessLogMapper.insert(ObjectUtils.clone(infApiAccessLogDO, logDO -> logDO.setBeginTime(buildTime(2021, 2, 6))));
+        // duration 不同的
+        infApiAccessLogMapper.insert(ObjectUtils.clone(infApiAccessLogDO, logDO -> logDO.setDuration(100)));
+        // resultCode 不同的
+        infApiAccessLogMapper.insert(ObjectUtils.clone(infApiAccessLogDO, logDO -> logDO.setResultCode(2)));
+
+        // 构造调用参数
+        InfApiAccessLogPageReqVO reqVO = new InfApiAccessLogPageReqVO();
+        reqVO.setUserId(userId);
+        reqVO.setUserType(userType);
+        reqVO.setApplicationName(applicationName);
+        reqVO.setRequestUrl(requestUrl);
+        reqVO.setBeginBeginTime(buildTime(2021, 3, 12));
+        reqVO.setEndBeginTime(buildTime(2021, 3, 14));
+        reqVO.setDuration(duration);
+        reqVO.setResultCode(resultCode);
+
+        // 调用service方法
+        PageResult<InfApiAccessLogDO> pageResult = infApiAccessLogServiceImpl.getApiAccessLogPage(reqVO);
+
+        // 断言,只查到了一条符合条件的
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(infApiAccessLogDO, pageResult.getList().get(0));
+    }
+
+    @Test
+    public void testGetApiAccessLogList() {
+        // 构造测试数据
+        long userId = 2233L;
+        int userType = UserTypeEnum.ADMIN.getValue();
+        String applicationName = "ruoyi-test";
+        String requestUrl = "foo";
+        Date beginTime = buildTime(2021, 3, 13);
+        int duration = 1000;
+        int resultCode = GlobalErrorCodeConstants.SUCCESS.getCode();
+
+        InfApiAccessLogDO infApiAccessLogDO = RandomUtils.randomPojo(InfApiAccessLogDO.class, dto -> {
+            dto.setUserId(userId);
+            dto.setUserType(userType);
+            dto.setApplicationName(applicationName);
+            dto.setRequestUrl(requestUrl);
+            dto.setBeginTime(beginTime);
+            dto.setDuration(duration);
+            dto.setResultCode(resultCode);
+        });
+        infApiAccessLogMapper.insert(infApiAccessLogDO);
+
+        // 下面几个都是不匹配的数据
+        // userId 不同的
+        infApiAccessLogMapper.insert(ObjectUtils.clone(infApiAccessLogDO, logDO -> logDO.setUserId(3344L)));
+        // userType
+        infApiAccessLogMapper.insert(ObjectUtils.clone(infApiAccessLogDO, logDO -> logDO.setUserType(UserTypeEnum.MEMBER.getValue())));
+        // applicationName 不同的
+        infApiAccessLogMapper.insert(ObjectUtils.clone(infApiAccessLogDO, logDO -> logDO.setApplicationName("test")));
+        // requestUrl 不同的
+        infApiAccessLogMapper.insert(ObjectUtils.clone(infApiAccessLogDO, logDO -> logDO.setRequestUrl("bar")));
+        // 构造一个早期时间 2021-02-06 00:00:00
+        infApiAccessLogMapper.insert(ObjectUtils.clone(infApiAccessLogDO, logDO -> logDO.setBeginTime(buildTime(2021, 2, 6))));
+        // duration 不同的
+        infApiAccessLogMapper.insert(ObjectUtils.clone(infApiAccessLogDO, logDO -> logDO.setDuration(100)));
+        // resultCode 不同的
+        infApiAccessLogMapper.insert(ObjectUtils.clone(infApiAccessLogDO, logDO -> logDO.setResultCode(2)));
+
+        // 构造调用参数
+        InfApiAccessLogExportReqVO reqVO = new InfApiAccessLogExportReqVO();
+        reqVO.setUserId(userId);
+        reqVO.setUserType(userType);
+        reqVO.setApplicationName(applicationName);
+        reqVO.setRequestUrl(requestUrl);
+        reqVO.setBeginBeginTime(buildTime(2021, 3, 12));
+        reqVO.setEndBeginTime(buildTime(2021, 3, 14));
+        reqVO.setDuration(duration);
+        reqVO.setResultCode(resultCode);
+
+        // 调用service方法
+        List<InfApiAccessLogDO> list = infApiAccessLogServiceImpl.getApiAccessLogList(reqVO);
+
+        // 断言,只查到了一条符合条件的
+        assertEquals(1, list.size());
+        assertPojoEquals(infApiAccessLogDO, list.get(0));
+    }
+}

+ 207 - 0
src/test/java/cn/iocoder/dashboard/modules/infra/service/logger/InfApiErrorLogServiceImplTest.java

@@ -0,0 +1,207 @@
+package cn.iocoder.dashboard.modules.infra.service.logger;
+
+import cn.hutool.core.util.RandomUtil;
+import cn.iocoder.dashboard.BaseDbUnitTest;
+import cn.iocoder.dashboard.common.enums.UserTypeEnum;
+import cn.iocoder.dashboard.common.pojo.PageResult;
+import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiErrorLogCreateDTO;
+import cn.iocoder.dashboard.modules.infra.controller.logger.vo.apierrorlog.InfApiErrorLogExportReqVO;
+import cn.iocoder.dashboard.modules.infra.controller.logger.vo.apierrorlog.InfApiErrorLogPageReqVO;
+import cn.iocoder.dashboard.modules.infra.dal.dataobject.logger.InfApiErrorLogDO;
+import cn.iocoder.dashboard.modules.infra.dal.mysql.logger.InfApiErrorLogMapper;
+import cn.iocoder.dashboard.modules.infra.enums.logger.InfApiErrorLogProcessStatusEnum;
+import cn.iocoder.dashboard.modules.infra.service.logger.impl.InfApiErrorLogServiceImpl;
+import cn.iocoder.dashboard.util.RandomUtils;
+import cn.iocoder.dashboard.util.object.ObjectUtils;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Future;
+
+import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.API_ERROR_LOG_NOT_FOUND;
+import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.API_ERROR_LOG_PROCESSED;
+import static cn.iocoder.dashboard.util.AssertUtils.assertPojoEquals;
+import static cn.iocoder.dashboard.util.AssertUtils.assertServiceException;
+import static cn.iocoder.dashboard.util.date.DateUtils.buildTime;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * {@link InfApiErrorLogServiceImpl} 单元测试
+ */
+@Import(InfApiErrorLogServiceImpl.class)
+public class InfApiErrorLogServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private InfApiErrorLogService infApiErrorLogServiceImpl;
+
+    @Resource
+    private InfApiErrorLogMapper infApiErrorLogMapper;
+
+
+    @Test
+    public void testCreateApiErrorLogAsync() throws Exception {
+        ApiErrorLogCreateDTO createDTO = RandomUtils.randomPojo(
+                ApiErrorLogCreateDTO.class,
+                dto -> dto.setUserType(RandomUtil.randomEle(UserTypeEnum.values()).getValue())
+        );
+
+        // 执行service方法
+        Future<Boolean> future = infApiErrorLogServiceImpl.createApiErrorLogAsync(createDTO);
+
+        // 等异步执行完
+        future.get();
+
+        InfApiErrorLogDO infApiErrorLogDO = infApiErrorLogMapper.selectOne(null);
+        // 断言
+        assertNotNull(infApiErrorLogDO);
+        // 断言,忽略基本字段
+        assertPojoEquals(createDTO, infApiErrorLogDO);
+    }
+
+
+    @Test
+    public void testGetApiErrorLogPage() {
+        // 构造测试数据
+        long userId = 2233L;
+        int userType = UserTypeEnum.ADMIN.getValue();
+        String applicationName = "ruoyi-test";
+        String requestUrl = "foo";
+        Date beginTime = buildTime(2021, 3, 13);
+        int progressStatus = InfApiErrorLogProcessStatusEnum.INIT.getStatus();
+
+        InfApiErrorLogDO infApiErrorLogDO = RandomUtils.randomPojo(InfApiErrorLogDO.class, logDO -> {
+            logDO.setUserId(userId);
+            logDO.setUserType(userType);
+            logDO.setApplicationName(applicationName);
+            logDO.setRequestUrl(requestUrl);
+            logDO.setExceptionTime(beginTime);
+            logDO.setProcessStatus(progressStatus);
+        });
+        infApiErrorLogMapper.insert(infApiErrorLogDO);
+
+        // 下面几个都是不匹配的数据
+        // userId 不同的
+        infApiErrorLogMapper.insert(ObjectUtils.clone(infApiErrorLogDO, logDO -> logDO.setUserId(3344L)));
+        // userType
+        infApiErrorLogMapper.insert(ObjectUtils.clone(infApiErrorLogDO, logDO -> logDO.setUserType(UserTypeEnum.MEMBER.getValue())));
+        // applicationName 不同的
+        infApiErrorLogMapper.insert(ObjectUtils.clone(infApiErrorLogDO, logDO -> logDO.setApplicationName("test")));
+        // requestUrl 不同的
+        infApiErrorLogMapper.insert(ObjectUtils.clone(infApiErrorLogDO, logDO -> logDO.setRequestUrl("bar")));
+        // 构造一个早期时间 2021-02-06 00:00:00
+        infApiErrorLogMapper.insert(ObjectUtils.clone(infApiErrorLogDO, logDO -> logDO.setExceptionTime(buildTime(2021, 2, 6))));
+        // progressStatus 不同的
+        infApiErrorLogMapper.insert(ObjectUtils.clone(infApiErrorLogDO, logDO -> logDO.setProcessStatus(InfApiErrorLogProcessStatusEnum.DONE.getStatus())));
+
+        // 构造调用参数
+        InfApiErrorLogPageReqVO reqVO = new InfApiErrorLogPageReqVO();
+        reqVO.setUserId(userId);
+        reqVO.setUserType(userType);
+        reqVO.setApplicationName(applicationName);
+        reqVO.setRequestUrl(requestUrl);
+        reqVO.setBeginExceptionTime(buildTime(2021, 3, 12));
+        reqVO.setEndExceptionTime(buildTime(2021, 3, 14));
+        reqVO.setProcessStatus(progressStatus);
+
+        // 调用service方法
+        PageResult<InfApiErrorLogDO> pageResult = infApiErrorLogServiceImpl.getApiErrorLogPage(reqVO);
+
+        // 断言,只查到了一条符合条件的
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(infApiErrorLogDO, pageResult.getList().get(0));
+    }
+
+    @Test
+    public void testGetApiErrorLogList() {
+        // 构造测试数据
+        long userId = 2233L;
+        int userType = UserTypeEnum.ADMIN.getValue();
+        String applicationName = "ruoyi-test";
+        String requestUrl = "foo";
+        Date beginTime = buildTime(2021, 3, 13);
+        int progressStatus = InfApiErrorLogProcessStatusEnum.INIT.getStatus();
+
+        InfApiErrorLogDO infApiErrorLogDO = RandomUtils.randomPojo(InfApiErrorLogDO.class, logDO -> {
+            logDO.setUserId(userId);
+            logDO.setUserType(userType);
+            logDO.setApplicationName(applicationName);
+            logDO.setRequestUrl(requestUrl);
+            logDO.setExceptionTime(beginTime);
+            logDO.setProcessStatus(progressStatus);
+        });
+        infApiErrorLogMapper.insert(infApiErrorLogDO);
+
+        // 下面几个都是不匹配的数据
+        // userId 不同的
+        infApiErrorLogMapper.insert(ObjectUtils.clone(infApiErrorLogDO, logDO -> logDO.setUserId(3344L)));
+        // userType
+        infApiErrorLogMapper.insert(ObjectUtils.clone(infApiErrorLogDO, logDO -> logDO.setUserType(UserTypeEnum.MEMBER.getValue())));
+        // applicationName 不同的
+        infApiErrorLogMapper.insert(ObjectUtils.clone(infApiErrorLogDO, logDO -> logDO.setApplicationName("test")));
+        // requestUrl 不同的
+        infApiErrorLogMapper.insert(ObjectUtils.clone(infApiErrorLogDO, logDO -> logDO.setRequestUrl("bar")));
+        // 构造一个早期时间 2021-02-06 00:00:00
+        infApiErrorLogMapper.insert(ObjectUtils.clone(infApiErrorLogDO, logDO -> logDO.setExceptionTime(buildTime(2021, 2, 6))));
+        // progressStatus 不同的
+        infApiErrorLogMapper.insert(ObjectUtils.clone(infApiErrorLogDO, logDO -> logDO.setProcessStatus(InfApiErrorLogProcessStatusEnum.DONE.getStatus())));
+
+        // 构造调用参数
+        InfApiErrorLogExportReqVO reqVO = new InfApiErrorLogExportReqVO();
+        reqVO.setUserId(userId);
+        reqVO.setUserType(userType);
+        reqVO.setApplicationName(applicationName);
+        reqVO.setRequestUrl(requestUrl);
+        reqVO.setBeginExceptionTime(buildTime(2021, 3, 12));
+        reqVO.setEndExceptionTime(buildTime(2021, 3, 14));
+        reqVO.setProcessStatus(progressStatus);
+
+        // 调用service方法
+        List<InfApiErrorLogDO> list = infApiErrorLogServiceImpl.getApiErrorLogList(reqVO);
+
+        // 断言,只查到了一条符合条件的
+        assertEquals(1, list.size());
+        assertPojoEquals(infApiErrorLogDO, list.get(0));
+    }
+
+
+    @Test
+    public void testUpdateApiErrorLogProcess() {
+        // 先构造两条数据,第一条用于抛出异常,第二条用于正常的执行update操作
+        Long processUserId = 2233L;
+
+        InfApiErrorLogDO first = RandomUtils.randomPojo(InfApiErrorLogDO.class, logDO -> {
+            logDO.setProcessUserId(processUserId);
+            logDO.setUserType(UserTypeEnum.ADMIN.getValue());
+            logDO.setProcessStatus(InfApiErrorLogProcessStatusEnum.DONE.getStatus());
+        });
+        infApiErrorLogMapper.insert(first);
+
+        InfApiErrorLogDO second = RandomUtils.randomPojo(InfApiErrorLogDO.class, logDO -> {
+            logDO.setProcessUserId(1122L);
+            logDO.setUserType(UserTypeEnum.ADMIN.getValue());
+            logDO.setProcessStatus(InfApiErrorLogProcessStatusEnum.INIT.getStatus());
+        });
+        infApiErrorLogMapper.insert(second);
+
+        Long firstId = first.getId();
+        Long secondId = second.getId();
+
+        // 执行正常的 update 操作
+        infApiErrorLogServiceImpl.updateApiErrorLogProcess(secondId, InfApiErrorLogProcessStatusEnum.DONE.getStatus(), processUserId);
+        InfApiErrorLogDO secondSelect = infApiErrorLogMapper.selectOne("id", secondId);
+
+        // id 为 0 查询不到,应该抛出异常 API_ERROR_LOG_NOT_FOUND
+        assertServiceException(() -> infApiErrorLogServiceImpl.updateApiErrorLogProcess(0L, InfApiErrorLogProcessStatusEnum.DONE.getStatus(), processUserId), API_ERROR_LOG_NOT_FOUND);
+        // id 为 first 的 progressStatus 为 DONE ,应该抛出 API_ERROR_LOG_PROCESSED
+        assertServiceException(() -> infApiErrorLogServiceImpl.updateApiErrorLogProcess(firstId, InfApiErrorLogProcessStatusEnum.DONE.getStatus(), processUserId), API_ERROR_LOG_PROCESSED);
+        // 验证 progressStatus 是否修改成功
+        assertEquals(InfApiErrorLogProcessStatusEnum.DONE.getStatus(), secondSelect.getProcessStatus());
+        // 验证 progressUserId 是否修改成功
+        assertEquals(processUserId, secondSelect.getProcessUserId());
+    }
+}

+ 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;
 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 cn.iocoder.dashboard.modules.tool.dal.dataobject.codegen.ToolSchemaColumnDO;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 
 
@@ -9,7 +9,7 @@ import java.util.List;
 
 
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 
-public class ToolInformationSchemaColumnMapperTest extends BaseSpringBootUnitTest {
+public class ToolInformationSchemaColumnMapperTest extends BaseDbUnitTest {
 
 
     @Resource
     @Resource
     private ToolSchemaColumnMapper toolInformationSchemaColumnMapper;
     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;
 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 cn.iocoder.dashboard.modules.tool.dal.dataobject.codegen.ToolSchemaTableDO;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 
 
@@ -9,7 +9,7 @@ import java.util.List;
 
 
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 
-class ToolInformationSchemaTableMapperTest extends BaseSpringBootUnitTest {
+class ToolInformationSchemaTableMapperTest extends BaseDbUnitTest {
 
 
     @Resource
     @Resource
     private ToolSchemaTableMapper toolInformationSchemaTableMapper;
     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;
 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.ToolCodegenColumnDO;
 import cn.iocoder.dashboard.modules.tool.dal.dataobject.codegen.ToolCodegenTableDO;
 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.ToolCodegenColumnMapper;
 import cn.iocoder.dashboard.modules.tool.dal.mysql.codegen.ToolCodegenTableMapper;
 import cn.iocoder.dashboard.modules.tool.dal.mysql.codegen.ToolCodegenTableMapper;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 
 
-public class ToolCodegenEngineTest extends BaseSpringBootUnitTest {
+public class ToolCodegenEngineTest extends BaseDbUnitTest {
 
 
     @Resource
     @Resource
     private ToolCodegenTableMapper codegenTableMapper;
     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;
 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.junit.jupiter.api.Test;
 
 
-public class ToolCodegenSQLParserTest extends BaseSpringBootUnitTest {
+public class ToolCodegenSQLParserTest extends BaseDbUnitTest {
 
 
     @Test
     @Test
     public void testParse() {
     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;
 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.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 
 
-class ToolCodegenServiceImplTest extends BaseSpringBootUnitTest {
+class ToolCodegenServiceImplTest extends BaseDbUnitTest {
 
 
     @Resource
     @Resource
     private ToolCodegenServiceImpl toolCodegenService;
     private ToolCodegenServiceImpl toolCodegenService;

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

@@ -131,7 +131,7 @@ CREATE TABLE IF NOT EXISTS "sys_menu" (
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '菜单权限表';
 ) COMMENT '菜单权限表';
 
 
-CREATE TABLE "sys_dict_type" (
+CREATE TABLE IF NOT EXISTS "sys_dict_type" (
     "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     "name" varchar(100) NOT NULL DEFAULT '',
     "name" varchar(100) NOT NULL DEFAULT '',
     "type" varchar(100) NOT NULL DEFAULT '',
     "type" varchar(100) NOT NULL DEFAULT '',
@@ -145,7 +145,7 @@ CREATE TABLE "sys_dict_type" (
     PRIMARY KEY ("id")
     PRIMARY KEY ("id")
 ) COMMENT '字典类型表';
 ) COMMENT '字典类型表';
 
 
-CREATE TABLE `sys_user_session` (
+CREATE TABLE IF NOT EXISTS `sys_user_session` (
     `id` varchar(32) NOT NULL,
     `id` varchar(32) NOT NULL,
     `user_id` bigint DEFAULT NULL,
     `user_id` bigint DEFAULT NULL,
     `username` varchar(50) NOT NULL DEFAULT '',
     `username` varchar(50) NOT NULL DEFAULT '',
@@ -209,7 +209,7 @@ CREATE TABLE IF NOT EXISTS `sys_login_log` (
 ) COMMENT ='系统访问记录';
 ) COMMENT ='系统访问记录';
 
 
 
 
-CREATE TABLE `sys_operate_log` (
+CREATE TABLE IF NOT EXISTS `sys_operate_log` (
     `id`               bigint(20)    NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     `id`               bigint(20)    NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     `trace_id`         varchar(64)   NOT NULL DEFAULT '',
     `trace_id`         varchar(64)   NOT NULL DEFAULT '',
     `user_id`          bigint(20)    NOT NULL,
     `user_id`          bigint(20)    NOT NULL,
@@ -237,7 +237,7 @@ CREATE TABLE `sys_operate_log` (
     PRIMARY KEY (`id`)
     PRIMARY KEY (`id`)
 ) COMMENT ='操作日志记录';
 ) COMMENT ='操作日志记录';
 
 
-create table "sys_user" (
+create table IF NOT EXISTS "sys_user" (
     "id" bigint not null GENERATED BY DEFAULT AS IDENTITY,
     "id" bigint not null GENERATED BY DEFAULT AS IDENTITY,
     "username" varchar(30) not null,
     "username" varchar(30) not null,
     "password" varchar(100) not null default '',
     "password" varchar(100) not null default '',
@@ -259,3 +259,60 @@ create table "sys_user" (
     "deleted" bit not null default false,
     "deleted" bit not null default false,
     primary key ("id")
     primary key ("id")
 ) comment '用户信息表';
 ) comment '用户信息表';
+
+
+create table "inf_api_access_log" (
+  "id" bigint not null GENERATED BY DEFAULT AS IDENTITY,
+  "trace_id" varchar(64) not null default '',
+  "user_id" bigint not null default '0',
+  "user_type" tinyint not null default '0',
+  "application_name" varchar(50) not null,
+  "request_method" varchar(16) not null default '',
+  "request_url" varchar(255) not null default '',
+  "request_params" varchar(8000) not null default '',
+  "user_ip" varchar(50) not null,
+  "user_agent" varchar(512) not null,
+  "begin_time" timestamp not null,
+  "end_time" timestamp not null,
+  "duration" integer not null,
+  "result_code" integer not null default '0',
+  "result_msg" varchar(512) default '',
+  "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 'API 访问日志表';
+
+
+create table "inf_api_error_log" (
+ "id" integer not null GENERATED BY DEFAULT AS IDENTITY,
+ "trace_id" varchar(64) not null,
+ "user_id" bigint not null default '0',
+ "user_type" tinyint not null default '0',
+ "application_name" varchar(50) not null,
+ "request_method" varchar(16) not null,
+ "request_url" varchar(255) not null,
+ "request_params" varchar(8000) not null,
+ "user_ip" varchar(50) not null,
+ "user_agent" varchar(512) not null,
+ "exception_time" timestamp not null,
+ "exception_name" varchar(128) not null default '',
+ "exception_message" clob not null,
+ "exception_root_cause_message" clob not null,
+ "exception_stack_trace" clob not null,
+ "exception_class_name" varchar(512) not null,
+ "exception_file_name" varchar(512) not null,
+ "exception_method_name" varchar(512) not null,
+ "exception_line_number" integer not null,
+ "process_status" tinyint not null,
+ "process_time" timestamp default null,
+ "process_user_id" bigint default '0',
+ "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 '系统异常日志';