Vue3+SpringBoot】构建企业日报管理saas系统
目录
- 可行性研究与计划
- 需求分析
- 软件设计
- 前端架构设计
- 数据库设计
- 后端架构设计
- 编程开发
- 功能
- 登录注册
- 前端
- 后端
- 选择机构
- 前端
- 后端
- 首页
- 前端
- 后端
- 个人信息页
- 前端
- 后端
- 角色管理
- 前端
- 后端
- 用户管理
- 前端
- 后端
- 机构管理
- 前端
- 后端
- 组织架构
- 前端
- 后端
- 项目模块管理
- 前端
- 后端
- 日报模块
- 消息模块
- 前端
- 后端
- 预览
- 源码
- 感谢
可行性研究与计划
作为企业的员工,相信很多小伙伴们每天上班的第一件事情,就是需要写工作日报,来向上级汇报今天的工作计划、上个工作日的工作完成情况以及汇报工作中遇到的项目延误等问题。工作日报不仅是对工作的目标有一个更清晰的规划也是上级了解工作情况的重要的信息来源。
那么,仅仅通过简单的聊天工具发送日报,既不能直观方便地对每个人的每天工作日报进行一个归类和管理,也不方便上级查看历史的工作情况,从而对整个团队的工作不好进行一个整体的分析。
综合以上几点,那么一个企业可以管理所有员工的工作情况,进行方便分类管理和查阅是有必要的。
需求分析
首先系统面对的是企业所有的员工。以企业为独立单位,每个企业有对应的部门组织架构,每个组织架构中包含管理者。每个组织中的管理者可以收集和查阅对应组织以及他的下级发送的日报。譬如:
一个企业中包含技术部组织,下面有前端组、后端组、产品组、测试组,前端组包含一个组织管理者,他可以查看其组织的其他成员所发送的日报,但不能查看其他组织的成员发送的日报。而技术部组织的管理者则可以查看其下所有组织成员的发送的日报。并且要做到及时通知,否则将不能保证日报的发送及时性。
需要实现的功能:企业机构管理,用户管理,角色管理,权限管理,项目模块管理,组织管理,日报管理,导出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
推荐阅读
-
openEuler郑州用户组成立!openEuler与hyperfusion携手共建河南地区用户生态 - 开幕致辞 超融合操作系统业务总经理、openEuler委员会成员蒋振华先生为本次活动致辞。 在本次活动的致辞中,他提到,作为openEuler社区早期的成员,超融合见证了openEuler从成立到在各行业商业落地,再到跨越生态拐点的过程,感谢openEuler提供了一个全产业链共同创新的平台,共同推动创新技术的商业落地。 同时,本次活动得到了郑州市郑东新区大数据管理局、郑州中原科技城投资服务局的大力支持。 郑东新区大数据管理局曹光远 在活动致辞中表示,openEuler的应用和*应用设施的深度优化,为郑东新区数字化转型提供了安全、可靠、高性能的技术基础;郑州中原科技城招商服务局王林表示,郑东新区欢迎所有openEuler生态相关企业扎根当地,围绕openEuler社区共同发展,形成合力。 openEuler社区及运维功能介绍 openEuler技术委员会委员胡峰 openEuler技术委员会委员胡峰先生在本次活动中介绍了openEuler社区目前发展的整体情况,并重点从技术层面介绍了openEuler的运维功能。 openEuler 晚会 胡峰先生介绍智能运维工具 A-Ops 和 openEuler gala、 阿波罗 Apollo、智能漏洞管理解决方案等新功能,以及涵盖各种运维场景的精品运维组件。在*交流环节,许多用户就目前使用的 openEuler 在*交流环节,许多用户就自己在使用openEuler过程中遇到的一些问题与胡峰先生进行了进一步的交流。 软硬结合,构建多样化算力操作系统 Hyperfusion 基于 openEuler 的基础上,结合自身软硬件技术积累,推出了富讯服务器操作系统 FusionOS FusionOS. FusionOS 首席架构师张海亮 分享了 FusionOS FusionOS首席架构师张海亮分享了FusionOS的软硬件协同优势、卓越的性能和可靠性,以及FusionOS在金融、运营商、*、互联网等行业的实践案例,引起了众多用户的兴趣,分享结束后,不少参会者就FusionOS的特点向讲师提问并进行了交流。
-
Vue3+SpringBoot】构建企业日报管理saas系统
-
用Vue3.3、Vite和Element-Plus构建企业级后端管理系统的全栈教程(源代码公开)
-
SaaS新十年:餐饮数字化转型的三大趋势- 一体化系统 2012年,亚马逊公司前首席科学家安德里亚斯·维真德表示,数据是新石油,但石油需要加以提炼后才能使用,从事数据处理的公司就是炼油厂。 如今,数据是一种资源已经获得广泛共识,这场竞争的核心就在于数据的占有和应用能力,占有的数据越多、运用数据能力越强的公司就越有价值。 对于to B型企业,只做单点业务很难产生差异化优势,因为数据只有流动起来才有价值,就算CRM功能再好,不能跟收银之类的体系打通也没有意义。 在企业管理层面,由于过往理念、*、各业务的机制不统一,过程标准的规范缺失,导致各系统之间兼容性和集成性难以提高。比如餐饮商家在POS数据、会员管理、供应链管理等不同环节都要面对众多系统供应商,多系统难以融合,由此导致的数据割裂问题日益凸显。 即便前期已实现不同服务商系统间的一体化打通与数据规范统一,但随着餐饮企业发展,不断产生新的功能需求,一家服务商出现软件升级,意味着其他服务商也必须做出对策,这时如果其中任何一家出现应对能力不足或者倒闭情况发生,都会影响餐饮企业的进步发展。 因此,餐饮数字化服务具有天然的all in one属性,没有商家愿意收银用一家的系统,供应链用另一家,资金归集再用另外一家,商户对效率的追求天然决定其必然会选择一家功能最全的系统。 沉淀的数据只是资源,只有用起来,数据的价值才能释放。