欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Vue3+SpringBoot】构建企业日报管理saas系统

最编程 2024-03-26 18:48:29
...

目录

      • 可行性研究与计划
      • 需求分析
      • 软件设计
        • 前端架构设计
        • 数据库设计
        • 后端架构设计
      • 编程开发
      • 功能
        • 登录注册
          • 前端
          • 后端
        • 选择机构
          • 前端
          • 后端
        • 首页
          • 前端
          • 后端
        • 个人信息页
          • 前端
          • 后端
        • 角色管理
          • 前端
          • 后端
        • 用户管理
          • 前端
          • 后端
        • 机构管理
          • 前端
          • 后端
        • 组织架构
          • 前端
          • 后端
        • 项目模块管理
          • 前端
          • 后端
        • 日报模块
        • 消息模块
          • 前端
          • 后端
      • 预览
      • 源码
      • 感谢

可行性研究与计划

作为企业的员工,相信很多小伙伴们每天上班的第一件事情,就是需要写工作日报,来向上级汇报今天的工作计划、上个工作日的工作完成情况以及汇报工作中遇到的项目延误等问题。工作日报不仅是对工作的目标有一个更清晰的规划也是上级了解工作情况的重要的信息来源。

那么,仅仅通过简单的聊天工具发送日报,既不能直观方便地对每个人的每天工作日报进行一个归类和管理,也不方便上级查看历史的工作情况,从而对整个团队的工作不好进行一个整体的分析。

综合以上几点,那么一个企业可以管理所有员工的工作情况,进行方便分类管理和查阅是有必要的。

需求分析

首先系统面对的是企业所有的员工。以企业为独立单位,每个企业有对应的部门组织架构,每个组织架构中包含管理者。每个组织中的管理者可以收集和查阅对应组织以及他的下级发送的日报。譬如:

一个企业中包含技术部组织,下面有前端组、后端组、产品组、测试组,前端组包含一个组织管理者,他可以查看其组织的其他成员所发送的日报,但不能查看其他组织的成员发送的日报。而技术部组织的管理者则可以查看其下所有组织成员的发送的日报。并且要做到及时通知,否则将不能保证日报的发送及时性。

需要实现的功能:企业机构管理,用户管理,角色管理,权限管理,项目模块管理,组织管理,日报管理,导出Excel,消息模块等功能。

软件设计

前端架构设计

技术栈:Vue3 + vuex + vue-router + less + element-plus + axios + echats + mitt + websocket + webworker + canvas
使用了 vite 作为开发和打包工具

  • — src:
  • |— api:存放请求的接口;方便统一管理接口。
  • |— assets:存放静态文件;图片、字体图标等。
  • |— components:存放公共组件;方便组件复用。
  • |— config:存放配置文件;请求域名、文件访问路径、websocket请求路径等。
  • |— router:存放页面路由、路由拦截。
  • |— store:存放公共状态管理。
  • |— utils:存放通用的js文件,工具函数等。
  • |— views:存放页面

在main.js 中,通过使用 vite 工具的 import.meta.globEager 自动导入api下的模块,挂载到app.config.globalProperties下,方便全局使用:
api/index.js:

const modulesFiles = import.meta.globEager("./*/*api.js");

for (const modulePath in modulesFiles) {
  const path = modulePath.replace(/^\.\/(.*)\.api\.\w+$/, "$1");
  const pathArr = path.split("/");
  const moduleName = pathArr.length ? pathArr[pathArr.length - 1] : pathArr;
  apiObj[moduleName] = modulesFiles[modulePath];
}

export default apiObj;

main.js:

// 引入api模块
import api from "./api/index.js";

app.config.globalProperties.$api = api;

引入自定义指令:

import directives from "./utils/directives";

Object.keys(directives).forEach((key) => {
  app.directive(key, directives[key]);
});

引入权限编码:

import permission from "./utils/permission";

app.config.globalProperties.$permission = permission;

权限判断:

/**
 * 是否是超级管理员
 *
 * @returns Boolean
 */
app.config.globalProperties.$isSupperAdmin = function () {
  if (store.state.user.userInfo.isSupperAdmin) {
    return true;
  }
  return false;
};

/**
 * 是否有某个权限
 *
 * @param {*} permissionCode 权限编码
 * @returns Boolean
 */
app.config.globalProperties.$hasPermission = function (permissionCode) {
  if (store.state.user.userInfo.isSupperAdmin) {
    return true;
  }
  let has = false;
  const userPermission = store.state.user.userInfo.permissions;
  if (userPermission && userPermission.includes(permissionCode)) {
    has = true;
  }
  return has;
};

/**
 * 是否有多个权限中至少一个
 *
 * @param {*} permissionCodeList 权限编码数组
 * @returns Boolean
 */
app.config.globalProperties.$hasOneOfPermissions = function (
  permissionCodeList
) {
  if (store.state.user.userInfo.isSupperAdmin) {
    return true;
  }
  let has = false;
  const userPermission = store.state.user.userInfo.permissions;
  if (
    userPermission &&
    permissionCodeList &&
    Array.isArray(permissionCodeList)
  ) {
    has = permissionCodeList.some((item) => userPermission.includes(item));
  }
  return has;
};

/**
 * 是否有全部权限
 *
 * @param {*} permissionCodeList 权限编码数组
 * @returns Boolean
 */
app.config.globalProperties.$havePermissions = function (permissionCodeList) {
  if (store.state.user.userInfo.isSupperAdmin) {
    return true;
  }
  let has = false;
  const userPermission = store.state.user.userInfo.permissions;
  if (
    userPermission &&
    permissionCodeList &&
    Array.isArray(permissionCodeList)
  ) {
    has = permissionCodeList.every((item) => userPermission.includes(item));
  }
  return has;
};

因为Vue3已经不再支持过滤器,这里写个全局方法代替过滤器的功能:

/**
 * 全局过滤器
 */
app.config.globalProperties.$filters = {
  // 性别过滤
  sexFilter(val) {
    let name = "";
    switch (val) {
      case "1":
        name = "男";
        break;
      case "0":
        name = "女";
        break;
      default:
        name = "未填写";
    }
    return name;
  },
};
数据库设计

使用MySQL作为数据库
在这里插入图片描述

后端架构设计

技术栈:springBoot + myBatisPlus + MySQL + easyexcel + websocket + mybatis-plus-generator
使用 swagger 作为接口文档工具

  • — cofig :存放配置文件;接口登录拦截配置、跨域配置、mybatisPlus的自动填充配置等。
  • — interceptor :存放拦截器类;对需要进行登录验证或者权限验证的接口,进行拦截并验证,通过才给放行。
  • — modules :存放业务模块;对不同的业务模块进行分开管理,方便多人进行开发。
  • |— controller :存放对应模块的前端控制器。
  • |— dto : 存放对应模块的数据交换类。
  • |— entity:存放对应模块的数据库表对应的实体类。
  • |— enums:存放对应模块使用到的枚举类。
  • |— exception:存放对应模块使用到的异常类。
  • |— vo:存放返回给前端的数据类。
  • |— service:存放对应模块的业务接口。
  • |— | — impl:存放对应业务接口实现类。
  • — util :存放工具类。
  • — ws: 存放 wobsocket 模块相关。

通过实现 WebMvcConfigurer 接口,覆写 addInterceptors 和 addCorsMappings 配置请求拦截和跨域:

@Override
public void addInterceptors(InterceptorRegistry registry) {
     registry.addInterceptor(authHandlerInterceptor)
             // 拦截所有请求,通过判断是否有 @UserLoginToken 注解 决定是否需要登录
             .addPathPatterns("/**");
//                .excludePathPatterns("/user/**");
 }

@Override
public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
            .allowedHeaders("Content-Type", "X-Requested-With", "accept,Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", "login-token")
            .allowedMethods("*")
            .allowedOrigins("*")
            .allowCredentials(true);
}

使用拦截器通过自定义 @UserLoginToken 注解来判断是否需要进行登录和权限验证:

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {

    /**
     * 是否开启token验证 (默认开启)
     * @return
     */
    boolean required() default true;

    /**
     * 用户角色权限 (默认普通用户)
     * @return
     */
     PermissionEnum[] permission() default {};
}
@Log4j2
@Component
public class AuthHandlerInterceptor implements HandlerInterceptor {
    @Autowired
    IUserService userService;

    @Autowired
    IRoleService roleService;

    @Autowired
    TokenUtil tokenUtil;

    @Autowired
    TokenConfiguration tokenConfiguration;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("login-token");

        // 如果不是映射到方法直接通过
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        Class clazz = handlerMethod.getBeanType();

        // 1. 检查请求的【方法】中是否有 passtoken 注解,有则直接跳过认证
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }

        // 2. 检查请求的【方法】或者【类】中有没有需要用户权限 UserLoginToken 的注解
        if (method.isAnnotationPresent(UserLoginToken.class) || clazz.isAnnotationPresent(UserLoginToken.class)) {
            UserLoginToken userLoginToken;
            if (method.isAnnotationPresent(UserLoginToken.class)) {
                userLoginToken = method.getAnnotation(UserLoginToken.class);
            } else {
                userLoginToken = (UserLoginToken) clazz.getAnnotation(UserLoginToken.class);
            }
            if (userLoginToken.required()) {

                // 执行 token 认证
                if (null == token || "".equals(token.trim())) {
                    throw new TokenAuthExpiredException("需要登录才能访问,请登录!");
                }

                Map<String, Long> tokenDataMap = tokenUtil.parseToken(token);
                Integer userId = Math.toIntExact(tokenDataMap.get("userId"));

                long timeOfToken = System.currentTimeMillis() - tokenDataMap.get("timeStamp");
                // 1.判断 token 是否过期
                // 年轻 token
                if (timeOfToken < tokenConfiguration.getYangToken()) {
//                   log.info("token 未过期且不需要刷新");
                    System.out.println("\t年轻 token 不需要刷新");
                }
                // 老年 token 就刷新 token
                else if (timeOfToken >= tokenConfiguration.getYangToken() && timeOfToken <= tokenConfiguration.getOldToken()) {
                    System.out.println("\t老年 token 需要刷新");
                    response.addHeader("login-token", tokenUtil.getToken(userId));
                }
                // 过期 token 就返回 token 无效
                else {
                    throw new TokenAuthExpiredException("token 已过期,请重新登录!");
                }

                // 根据 token 中的 userId 获取用户信息
                UserEntity user = userService.getById(userId);

                // 拦截不存在或已被停用的用户
                if (ObjectUtil.isEmpty(user) || IsEnum.YES.equals(user.getDeleted())) {
                    throw new TokenAuthExpiredException("用户不存在,请重新登录");
                }

                // 把 用户信息 存在当前线程的缓存中
                UserChacheFromToken.setUser(user);

                // 超级管理员跳过权限验证
                if (!ObjectUtil.isEmpty(user.getIsSupperAdmin()) && user.getIsSupperAdmin()) {
                    log.info("超级管理员跳过权限验证");
                    return true;
                }

                // 2.角色匹配
                PermissionEnum[] needPermissionList = userLoginToken.permission();
                System.err.println("\t当前接口需要的权限 ====>" + Arrays.toString(needPermissionList));

                // 接口需要权限
                if (needPermissionList.length > 0) {
                    // 因为角色权限是跟机构绑定,如果没有绑定机构,则优先提示机构未绑定
                    if (ObjectUtil.isEmpty(user.getOrgId())) {
                        throw new HasNoPermissionException("当前用户未关联机构,请先关联");
                    }
                    if (ObjectUtil.isEmpty(user.getRoleId())) {
                        throw new HasNoPermissionException("当前用户未关联角色,请联系管理员");
                    }
                    Role userRole = roleService.getById(user.getRoleId());
                    if (ObjectUtil.isEmpty(userRole)) {
                        throw new HasNoPermissionException("当前用户关联角色不存在,请联系管理员");
                    }
                    List<String> userPermissionList = userRole.getPermissions();
                    log.info("当前用户权限列表 ===>" + userPermissionList);
                    for (PermissionEnum needPermission: needPermissionList) {
                        for (String userPermission: userPermissionList) {
                            if (needPermission.getValue().equals(userPermission)) {
                                return true;
                            }
                        }
                    }
                    throw new HasNoPermissionException("抱歉,当前用户没有权限,请联系管理员");
                }

                return true;
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
       // 执行结束后释放 ThreadLocal 资源防止oom(资源溢出)
        UserChacheFromToken.removeUser();
    }
}

编程开发

功能

登录注册

在这里插入图片描述
在这里插入图片描述

前端
  • 做了记住密码的功能,加密处理之后把密码存入 localstorage中,取出时再进行解密。
    登录成功之后存入
const loginMethod = () => {
  $api.users
     .login(formData)
     .then((res) => {
       ElMessage.success("登录成功");
       window.localStorage.setItem(
         REMEMBER_PASSWORD,
         JSON.stringify({
           username: formData.username || "",
           password: encryptData(formData.password),
         })
       );
       // 登录之后把用户信息存入 store 中
       store.commit("user/login", res.data);
       router.replace("/");
     })
     .catch((err) => {
       console.error("login error: ", err);
     })
     .finally(() => {
       submitLoading.value = false;
     });
 };

打开页面时取出:

// 取出
let rememberPassword = window.localStorage.getItem(REMEMBER_PASSWORD);
if (rememberPassword) {
  rememberPassword = JSON.parse(rememberPassword);
  formData.username = rememberPassword.username || "";
  formData.password = decryptData(rememberPassword.password);
}
  • 使用了动态背景图片,每次打开或者刷新会从图库里随机抽取一张图作为背景。
// 随机背景图
const pageBgIndex = getRandom(0, imgArr.length - 1, true);
const pageBgUrl = ref(imgArr[pageB					

推荐阅读