Browse Source

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

neilz 4 năm trước cách đây
mục cha
commit
08bc389ba5
46 tập tin đã thay đổi với 1791 bổ sung948 xóa
  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'
   })
 }
+
+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,
   handleTree,
   downloadExcel,
-  downloadZip
+  downloadWord,
+  downloadZip,
+  downloadHtml,
+  downloadMarkdown,
 } from "@/utils/ruoyi";
 import Pagination from "@/components/Pagination";
 // 自定义表格工具扩展
@@ -48,6 +51,9 @@ Vue.prototype.getDictDataLabel = getDictDataLabel
 Vue.prototype.DICT_TYPE = DICT_TYPE
 Vue.prototype.download = download
 Vue.prototype.downloadExcel = downloadExcel
+Vue.prototype.downloadWord = downloadWord
+Vue.prototype.downloadHtml = downloadHtml
+Vue.prototype.downloadMarkdown = downloadMarkdown
 Vue.prototype.downloadZip = downloadZip
 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');
 }
 
+// 下载 Word 方法
+export function downloadWord(data, fileName) {
+  download0(data, fileName, 'application/msword');
+}
+
 // 下载 Zip 方法
 export function downloadZip(data, fileName) {
   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) {
   // 创建 blob
   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>
-  <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>
 </template>
 <script>
-import {exportHtml} from "@/api/infra/dbDoc";
+import { exportHtml, exportWord, exportMarkdown} from "@/api/infra/dbDoc";
 
 export default {
   name: "DBDoc",
@@ -25,10 +36,31 @@ export default {
     };
   },
   created() {
+    // 加载 Html,进行预览
     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>

+ 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 javax.validation.Valid;
+import java.util.concurrent.Future;
 
 /**
  * API 访问日志 Framework Service 接口
@@ -15,7 +16,8 @@ public interface ApiAccessLogFrameworkService {
      * 创建 API 访问日志
      *
      * @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 javax.validation.Valid;
+import java.util.concurrent.Future;
 
 /**
  * API 错误日志 Framework Service 接口
@@ -15,7 +16,8 @@ public interface ApiErrorLogFrameworkService {
      * 创建 API 错误日志
      *
      * @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;
 
-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.engine.EngineConfig;
 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.HikariDataSource;
 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.http.MediaType;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletResponse;
-import javax.sql.DataSource;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.Collections;
 
 @Api(tags = "数据库文档")
@@ -34,34 +36,77 @@ public class InfDbDocController {
 
     private static final String FILE_OUTPUT_DIR = System.getProperty("java.io.tmpdir") + File.separator
             + "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_VERSION = "1.0.0";
     private static final String DOC_DESCRIPTION = "文档描述";
 
-    @Resource
-    private DataSource dataSource;
-
     @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()) {
             // 创建 screw 的配置
             Configuration config = Configuration.builder()
                     .version(DOC_VERSION)  // 版本
                     .description(DOC_DESCRIPTION) // 描述
                     .dataSource(dataSource) // 数据源
-                    .engineConfig(buildEngineConfig()) // 引擎配置
+                    .engineConfig(buildEngineConfig(fileOutputType, fileName)) // 引擎配置
                     .produceConfig(buildProcessConfig()) // 处理配置
                     .build();
 
             // 执行 screw,生成数据库文档
             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() {
         // 创建 HikariConfig 配置类
         HikariConfig hikariConfig = new HikariConfig();
-//        hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
         hikariConfig.setJdbcUrl(dataSourceProperties.getUrl());
         hikariConfig.setUsername(dataSourceProperties.getUsername());
         hikariConfig.setPassword(dataSourceProperties.getPassword());
@@ -83,13 +127,13 @@ public class InfDbDocController {
     /**
      * 创建 screw 的引擎配置
      */
-    private static EngineConfig buildEngineConfig() {
+    private static EngineConfig buildEngineConfig(EngineFileType fileOutputType, String docFileName) {
         return EngineConfig.builder()
                 .fileOutputDir(FILE_OUTPUT_DIR) // 生成文件路径
                 .openOutputDir(false) // 打开目录
-                .fileType(FILE_OUTPUT_TYPE) // 文件类型
+                .fileType(fileOutputType) // 文件类型
                 .produceType(EngineTemplateType.freemarker) // 文件类型
-                .fileName(DOC_FILE_NAME) // 自定义文件名称
+                .fileName(docFileName) // 自定义文件名称
                 .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 {
 
     @ApiModelProperty(value = "用户编号", example = "666")
-    private Integer userId;
+    private Long userId;
 
     @ApiModelProperty(value = "用户类型", example = "1")
     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 {
 
     @ApiModelProperty(value = "用户编号", example = "666")
-    private Integer userId;
+    private Long userId;
 
     @ApiModelProperty(value = "用户类型", example = "1")
     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()}
      */
-    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;
 
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public Long createJob(InfJobCreateReqVO createReqVO) throws SchedulerException {
         validateCronExpression(createReqVO.getCronExpression());
         // 校验唯一性
@@ -68,7 +68,7 @@ public class InfJobServiceImpl implements InfJobService {
     }
 
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void updateJob(InfJobUpdateReqVO updateReqVO) throws SchedulerException {
         validateCronExpression(updateReqVO.getCronExpression());
         // 校验存在
@@ -88,7 +88,7 @@ public class InfJobServiceImpl implements InfJobService {
     }
 
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void updateJobStatus(Long id, Integer status) throws SchedulerException {
         // 校验 status
         if (!containsAny(status, InfJobStatusEnum.NORMAL.getStatus(), InfJobStatusEnum.STOP.getStatus())) {
@@ -122,7 +122,7 @@ public class InfJobServiceImpl implements InfJobService {
     }
 
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void deleteJob(Long id) throws SchedulerException {
         // 校验存在
         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.service.logger.InfApiAccessLogService;
 import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.AsyncResult;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
-import javax.validation.Valid;
 import java.util.List;
+import java.util.concurrent.Future;
 
 /**
  * API 访问日志 Service 实现类
@@ -30,10 +31,11 @@ public class InfApiAccessLogServiceImpl implements InfApiAccessLogService {
 
     @Override
     @Async
-    public void createApiAccessLogAsync(ApiAccessLogCreateDTO createDTO) {
+    public Future<Boolean> createApiAccessLogAsync(ApiAccessLogCreateDTO createDTO) {
         // 插入
         InfApiAccessLogDO apiAccessLog = InfApiAccessLogConvert.INSTANCE.convert(createDTO);
-        apiAccessLogMapper.insert(apiAccessLog);
+        int insert = apiAccessLogMapper.insert(apiAccessLog);
+        return new AsyncResult<>(insert == 1);
     }
 
     @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.service.logger.InfApiErrorLogService;
 import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.AsyncResult;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
+import java.util.concurrent.Future;
 
 import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.API_ERROR_LOG_NOT_FOUND;
@@ -35,10 +37,11 @@ public class InfApiErrorLogServiceImpl implements InfApiErrorLogService {
 
     @Override
     @Async
-    public void createApiErrorLogAsync(ApiErrorLogCreateDTO createDTO) {
+    public Future<Boolean> createApiErrorLogAsync(ApiErrorLogCreateDTO createDTO) {
         InfApiErrorLogDO apiErrorLog = InfApiErrorLogConvert.INSTANCE.convert(createDTO);
         apiErrorLog.setProcessStatus(InfApiErrorLogProcessStatusEnum.INIT.getStatus());
-        apiErrorLogMapper.insert(apiErrorLog);
+        int insert = apiErrorLogMapper.insert(apiErrorLog);
+        return new AsyncResult<>(insert == 1);
     }
 
     @Override
@@ -62,7 +65,7 @@ public class InfApiErrorLogServiceImpl implements InfApiErrorLogService {
         }
         // 标记处理
         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
     private SysCaptchaService captchaService;
 
-    @ApiOperation("生成图片验证码")
     @GetMapping("/get-image")
+    @ApiOperation("生成图片验证码")
     public CommonResult<SysCaptchaImageRespVO> 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
     private SysDeptService deptService;
 
-    @ApiOperation("获得用户分页列表")
     @GetMapping("/page")
+    @ApiOperation("获得用户分页列表")
     @PreAuthorize("@ss.hasPermission('system:user:list')")
     public CommonResult<PageResult<SysUserPageItemRespVO>> pageUsers(@Validated SysUserPageReqVO reqVO) {
         // 获得用户分页列表
@@ -66,9 +66,9 @@ public class SysUserController {
     /**
      * 根据用户编号获取详细信息
      */
+    @GetMapping("/get")
     @ApiOperation("获得用户详情")
     @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
-    @GetMapping("/get")
 //    @PreAuthorize("@ss.hasPermi('system:user:query')")
     public CommonResult<SysUserRespVO> getInfo(@RequestParam("id") Long 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;
 
+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 {
 
-//    /**
-//     * 个人信息
-//     */
-//    @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.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.user.vo.user.SysUserProfileUpdateReqVO;
 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.user.SysUserDO;
@@ -13,26 +14,33 @@ import org.mapstruct.Mapping;
 import org.mapstruct.factory.Mappers;
 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
 public interface SysAuthConvert {
 
     SysAuthConvert INSTANCE = Mappers.getMapper(SysAuthConvert.class);
 
-    @Mapping(source = "updateTime", target = "updateTime", ignore = true) // 字段相同,但是含义不同,忽略
+    @Mapping(source = "updateTime", target = "updateTime", ignore = true)
+        // 字段相同,但是含义不同,忽略
     LoginUser convert(SysUserDO bean);
 
     default SysAuthPermissionInfoRespVO convert(SysUserDO user, List<SysRoleDO> roleList, List<SysMenuDO> menuList) {
         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);
 
+    LoginUser convert(SysUserProfileUpdateReqVO reqVO);
+
     /**
      * 将菜单列表,构建成菜单树
      *
@@ -47,12 +55,12 @@ public interface SysAuthConvert {
         Map<Long, SysAuthMenuRespVO> treeNodeMap = new LinkedHashMap<>();
         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());
             if (parentNode == null) {
                 LoggerFactory.getLogger(getClass()).error("[buildRouterTree][resource({}) 找不到父资源({})]",
-                        childNode.getId(), childNode.getParentId());
+                    childNode.getId(), childNode.getParentId());
                 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;
 
-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.permission.SysRoleDO;
 import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
@@ -23,4 +30,12 @@ public interface SysUserConvert {
 
     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_NOT_EXISTS = new ErrorCode(1002004003, "用户不存在");
     ErrorCode USER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1002004004, "导入用户数据不能为空!");
+    ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1002004005, "用户密码校验失败");
 
     // ========== 部门模块 1002005000 ==========
     ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1002004001, "已经存在该名字的部门");
@@ -74,5 +75,7 @@ public interface SysErrorCodeConstants {
 
     // ========== 文件 1002009000 ==========
     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 菜单编号
      */
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void deleteMenu(Long menuId) {
         // 校验更新的菜单是否存在
         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
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void assignRoleMenu(Long roleId, Set<Long> menuIds) {
         // 获得角色拥有菜单编号
         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
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void deleteRole(Long 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.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.util.collection.CollectionUtils;
 
+import java.io.InputStream;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -102,6 +109,14 @@ public interface SysUserService {
      */
     void updateUser(SysUserUpdateReqVO reqVO);
 
+    /**
+     * 修改用户个人信息
+     *
+     * @param reqVO 用户个人信息
+     * @return 修改结果
+     */
+    void updateUserProfile(SysUserProfileUpdateReqVO reqVO);
+
     /**
      * 删除用户
      *
@@ -112,7 +127,7 @@ public interface SysUserService {
     /**
      * 修改密码
      *
-     * @param id 用户编号
+     * @param id       用户编号
      * @param password 密码
      */
     void updateUserPassword(Long id, String password);
@@ -120,7 +135,7 @@ public interface SysUserService {
     /**
      * 修改密码
      *
-     * @param id 用户编号
+     * @param id     用户编号
      * @param status 状态
      */
     void updateUserStatus(Long id, Integer status);
@@ -128,12 +143,20 @@ public interface SysUserService {
     /**
      * 批量导入用户
      *
-     * @param importUsers 导入用户列表
+     * @param importUsers     导入用户列表
      * @param isUpdateSupport 是否支持更新
      * @return 导入结果
      */
     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;
 
 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.iocoder.dashboard.common.enums.CommonStatusEnum;
 import cn.iocoder.dashboard.common.exception.ServiceException;
 import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
 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.dal.mysql.user.SysUserMapper;
 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.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.SysPostService;
 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 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.*;
 
@@ -49,18 +65,8 @@ public class SysUserServiceImpl implements SysUserService {
     @Resource
     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
     public SysUserDO getUserByUserName(String username) {
@@ -108,7 +114,7 @@ public class SysUserServiceImpl implements SysUserService {
             return Collections.emptySet();
         }
         Set<Long> deptIds = CollectionUtils.convertSet(deptService.listDeptsByParentIdFromCache(
-                deptId, true), SysDeptDO::getId);
+            deptId, true), SysDeptDO::getId);
         deptIds.add(deptId); // 包括自身
         return deptIds;
     }
@@ -117,7 +123,7 @@ public class SysUserServiceImpl implements SysUserService {
     public Long createUser(SysUserCreateReqVO reqVO) {
         // 校验正确性
         this.checkCreateOrUpdate(null, reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(),
-                reqVO.getDeptId(), reqVO.getPostIds());
+            reqVO.getDeptId(), reqVO.getPostIds());
         // 插入用户
         SysUserDO user = SysUserConvert.INSTANCE.convert(reqVO);
         user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启
@@ -130,12 +136,31 @@ public class SysUserServiceImpl implements SysUserService {
     public void updateUser(SysUserUpdateReqVO reqVO) {
         // 校验正确性
         this.checkCreateOrUpdate(reqVO.getId(), reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(),
-                reqVO.getDeptId(), reqVO.getPostIds());
+            reqVO.getDeptId(), reqVO.getPostIds());
         // 更新用户
         SysUserDO updateObj = SysUserConvert.INSTANCE.convert(reqVO);
         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
     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
-    @Transactional // 添加事务,异常则回滚所有导入
+    @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
     public SysUserImportRespVO importUsers(List<SysUserImportExcelVO> importUsers, boolean isUpdateSupport) {
         if (CollUtil.isEmpty(importUsers)) {
             throw ServiceExceptionUtil.exception(USER_IMPORT_LIST_IS_EMPTY);
         }
         SysUserImportRespVO respVO = SysUserImportRespVO.builder().createUsernames(new ArrayList<>())
-                .updateUsernames(new ArrayList<>()).failureUsernames(new LinkedHashMap<>()).build();
+            .updateUsernames(new ArrayList<>()).failureUsernames(new LinkedHashMap<>()).build();
         importUsers.forEach(importUser -> {
             // 校验,判断是否有不符合的原因
             try {
                 checkCreateOrUpdate(null, null, importUser.getMobile(), importUser.getEmail(),
-                        importUser.getDeptId(), null);
+                    importUser.getDeptId(), null);
             } catch (ServiceException ex) {
                 respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage());
                 return;
@@ -316,4 +364,16 @@ public class SysUserServiceImpl implements SysUserService {
         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
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public List<Long> createCodegenListFromDB(List<String> tableNames) {
         List<Long> ids = new ArrayList<>(tableNames.size());
         // 遍历添加。虽然效率会低一点,但是没必要做成完全批量,因为不会这么大量
@@ -118,7 +118,7 @@ public class ToolCodegenServiceImpl implements ToolCodegenService {
     }
 
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void updateCodegen(ToolCodegenUpdateReqVO updateReqVO) {
         // 校验是否已经存在
         if (codegenTableMapper.selectById(updateReqVO.getTable().getId()) == null) {
@@ -134,7 +134,7 @@ public class ToolCodegenServiceImpl implements ToolCodegenService {
     }
 
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void syncCodegenFromDB(Long tableId) {
         // 校验是否已经存在
         ToolCodegenTableDO table = codegenTableMapper.selectById(tableId);
@@ -149,7 +149,7 @@ public class ToolCodegenServiceImpl implements ToolCodegenService {
     }
 
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void syncCodegenFromSQL(Long tableId, String sql) {
         // 校验是否已经存在
         ToolCodegenTableDO table = codegenTableMapper.selectById(tableId);
@@ -201,7 +201,7 @@ public class ToolCodegenServiceImpl implements ToolCodegenService {
     }
 
     @Override
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     public void deleteCodegen(Long tableId) {
         // 校验是否已经存在
         if (codegenTableMapper.selectById(tableId) == null) {

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

@@ -145,14 +145,14 @@ yudao:
   swagger:
     title: 管理后台
     description: 提供管理员管理的所有功能
-    version: ${yudao.info.base-package}
+    version: ${yudao.info.version}
     base-package: ${yudao.info.base-package}.modules
   captcha:
     timeout: 5m
     width: 160
     height: 60
   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:
     base-package: ${yudao.info.base-package}
     db-schemas: ${spring.datasource.name}

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

@@ -145,14 +145,14 @@ yudao:
   swagger:
     title: 管理后台
     description: 提供管理员管理的所有功能
-    version: ${yudao.info.base-package}
+    version: ${yudao.info.version}
     base-package: ${yudao.info.base-package}.modules
   captcha:
     timeout: 5m
     width: 160
     height: 60
   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:
     base-package: ${yudao.info.base-package}
     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;
 
 /**
- * 依赖内存 DB 的单元测试
+ * 依赖内存 DB + Redis 的单元测试
  *
- * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法
+ * 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis
  *
  * @author 芋道源码
  */

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -131,7 +131,7 @@ CREATE TABLE IF NOT EXISTS "sys_menu" (
     PRIMARY KEY ("id")
 ) COMMENT '菜单权限表';
 
-CREATE TABLE "sys_dict_type" (
+CREATE TABLE IF NOT EXISTS "sys_dict_type" (
     "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     "name" varchar(100) NOT NULL DEFAULT '',
     "type" varchar(100) NOT NULL DEFAULT '',
@@ -145,7 +145,7 @@ CREATE TABLE "sys_dict_type" (
     PRIMARY KEY ("id")
 ) COMMENT '字典类型表';
 
-CREATE TABLE `sys_user_session` (
+CREATE TABLE IF NOT EXISTS `sys_user_session` (
     `id` varchar(32) NOT NULL,
     `user_id` bigint DEFAULT NULL,
     `username` varchar(50) NOT NULL DEFAULT '',
@@ -209,7 +209,7 @@ CREATE TABLE IF NOT EXISTS `sys_login_log` (
 ) COMMENT ='系统访问记录';
 
 
-CREATE TABLE `sys_operate_log` (
+CREATE TABLE IF NOT EXISTS `sys_operate_log` (
     `id`               bigint(20)    NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     `trace_id`         varchar(64)   NOT NULL DEFAULT '',
     `user_id`          bigint(20)    NOT NULL,
@@ -237,7 +237,7 @@ CREATE TABLE `sys_operate_log` (
     PRIMARY KEY (`id`)
 ) COMMENT ='操作日志记录';
 
-create table "sys_user" (
+create table IF NOT EXISTS "sys_user" (
     "id" bigint not null GENERATED BY DEFAULT AS IDENTITY,
     "username" varchar(30) not null,
     "password" varchar(100) not null default '',
@@ -259,3 +259,60 @@ create table "sys_user" (
     "deleted" bit not null default false,
     primary key ("id")
 ) 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 '系统异常日志';