SaaS系统用户权限设计2

本文涉及的产品
RDS MySQL DuckDB 分析主实例,基础系列 4核8GB
RDS Agent(兼容OpenClaw),2核4GB
RDS DuckDB + QuickBI 企业套餐,8核32GB + QuickBI 专业版
简介: SaaS系统用户权限设计2

<el-dropdown class="item">
<span class="el-dropdown-link">
  操作<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
    <el-dropdown-item>
      <el-button type="text" @click="handlAdd(data.id)">添加子部门</el-button>
    </el-dropdown-item>
    <el-dropdown-item>
      <el-button type="text" @click="handUpdate(data.id)">查看部门</el-button>
    </el-dropdown-item>
    <el-dropdown-item>
      <el-button type="text" @click="handleList()">查看待分配员工</el-button>
    </el-dropdown-item>
    <el-dropdown-item>
    <el-button type="text" @click="handleDelete(data.id)">删除部门</el-button>
  </el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>


通过以上代码实现以下效果

(3) 构造数据

<!-- 引入组件 -->
<script>
//引入api
import {list,saveOrupdate,find,deleteById} from "@/api/base/dept"
import commonApi from '@/utils/common'
import deptAdd from './../components/add'
export default {
  components:{deptAdd},
  data() {
    return {
      deptAdd:'deptAdd',
      activeName: 'first', 
      departData:{},
      depts:[]
    }
  },
  methods: {
      //添加部门
    handlAdd(parentId) {
      //父页面调用子组件中的内容
      this.$refs.addDept.parentId = parentId;
      this.$refs.addDept.dialogFormVisible = true
    },
    //查看部门
    handUpdate(id) {
      //根据id查询部门
      find({id:id}).then(res => {
         //数据绑定到dept对象中
         this.$refs.addDept.dept = res.data.data;
         this.$refs.addDept.dialogFormVisible = true
      })
    },
    handleDelete(id) {
       this.$confirm('是否删除此条记录?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
               deleteById({id:id}).then(res=> {
                this.$message({
                  message: res.data.message,
                  type: res.data.success?'success':'error'
                });
                if(res.data.success) {
                  location.reload();
                }
              })
        })
    },
    //构造查询方法
    getList() {
      list().then(res => {
       this.departData = res.data.data
       //将普通的数据转化为父子接口
       this.depts = commonApi.transformTozTreeFormat(res.data.data.depts);
       console.log(this.depts)
      })
    }
  },
  created: function() {
    this.getList();
  },
}
</script>

(4)树形组件

export default {
  timestampToTime: (timestamp) => {
    let date = new Date(timestamp * 1000)
    let Y = date.getFullYear() + '-'
    let M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'
    let D = date.getDate() + ' '
    let h = date.getHours() + ':'
    let m = date.getMinutes() + ':'
    let s = date.getSeconds()
    return Y + M + D + h + m + s
  },
  transformTozTreeFormat: function (sNodes) {
    var i, l;
    var r = [];
    var tmpMap = {};
    for (i = 0, l = sNodes.length; i < l; i++) {
      tmpMap[sNodes[i].id] = sNodes[i];
    }
    for (i = 0, l = sNodes.length; i < l; i++) {
      var p = tmpMap[sNodes[i].parentId];
      if (p && sNodes[i].id != sNodes[i].parentId) {
        var children = this.nodeChildren(p);
        if (!children) {
          children = this.nodeChildren(p, []);
        }
        children.push(sNodes[i]);
      } else {
        r.push(sNodes[i]);
      }
    }
    return r;
  },
  nodeChildren: function (node, newChildren) {
    if (typeof newChildren !== 'undefined') {
      node.children = newChildren;
    }
    return node.children;
  }
}

骚戴理解:这是个好东西,可以把后端传过来的List集合解析成树状的数据结构

组织机构的增删改查

新增部门

使用element-ui提供的dialog的弹出层构造弹出添加页面

<el-dialog title="编辑部门" :visible.sync="dialogFormVisible">
    <el-form ref="dataForm" :model="formData" label-width="120px">
        <el-form-item label="部门名称">
       <el-input v-model="formData.name" placeholder='请输入部门名称'></el-input>
        </el-form-item>
        <el-form-item label="部门编码">
       <el-input v-model="formData.code" placeholder='请输入部门编码'></el-input>
        </el-form-item>
        <el-form-item label="部门负责人">
       <el-input v-model="formData.manager" placeholder='请输入负责人'></el-input>
        </el-form-item>
        <el-form-item label="部门介绍">
       <el-input v-model="formData.introduce" placeholder='请输入介绍'></el-input>
        </el-form-item>
    </el-form>
    <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="createData">确定</el-button>
        <el-button @click="dialogFormVisible=false">取消</el-button>
    </div>
</el-dialog>


配置保存方法

<script>
import {list,saveOrupdate,find,deleteById} from "@/api/base/dept"
export default {
 data () {
     return {
      //添加部门的模型
      parentId:'',
      dialogFormVisible:false,
      dept:{}
     }
 },
  methods: {
    saveDept() {
      this.dept.parentId = this.parentId
      saveOrupdate(this.dept).then(res => {
        this.$message({
          message: res.data.message,
          type: res.data.success?'success':'error'
        });
        //保存成功
        if(res.data.success) {
          //如果成功
          location.reload();
        }
      })
    }
  }
}
</script>

骚戴理解:新增有两个步骤,弹出新增框和点击保存。这里有两种类型的新增,分别是有父节点的新增和没有父节点的新增,只需要把父节点的id传过来即可, saveOrupdate(this.dept)这是其中的一个API,如果传过去的this.dept有id,那就是修改操作,没有id就是新增操作。这里可以发现this.dept模型是没有赋值id的,所以是新增操作,这样搞应该是为了让新增和修改都使用同一个组件,也就是都用这个对话框,res.data.success?'success':'error'的意思的res.data.success返回的是false就是error类型,如果返回的是true,那就是success类型。


location.reload();刷新页面有很明显的迟钝,所以我还是用的getList方法 来获取新的数据,但是在组件中怎么引用父类Vue的方法呢? this.$parent.getList();这样写就可以调父类的方法了


修改部门


根据id查询部门

    //修改部门
    handUpdate(id) {
      //根据id查询部门
      find({id:id}).then(res => {
         //数据绑定到dept对象中
         this.$refs.addDept.dept = res.data.data;
         this.$refs.addDept.dialogFormVisible = true
      })
    }

骚戴理解:新增和修改用的都是同一个弹窗,后面也抽离成为一个add.vue页面,新增和修改都是调用的saveOrupdate这个API,这个API可以根据是否有id值才判断调用save或update这两个API。


在前端{id:id}就表示一个对象,只要是{}这样的格式就是对象,所以下面的update接口用的是data对象,而不是id!

export const saveOrupdate = data => {return data.id?update(data):save(data)}
export const update = data => createAPI(`/company/department/${data.id}`, 'put', data)

这里我一开始搞不懂为什么前端只传了一个id,后端却可以接收到Department对象?其实是我搞混淆了,上面API的data其实对应的是后端的Department对象,然后后端的id是从url路径中获取的,前端传过去的id是从data中获取的

删除部门

   handleDelete(id) {
       this.$confirm('是否删除此条记录?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
               deleteById({id:id}).then(res=> {
                this.$message({
                  message: res.data.message,
                  type: res.data.success?'success':'error'
                });
                if(res.data.success) {
                  location.reload();
                }
              })
        })
    },


抽取组件

组件(Component)是Vue.js 最强大的功能。可以通过将不同的业务拆分为不同的组件进行开发,让代码更加优雅提供可读性。当然页可以封装可重用的代码,通过传入对象的不同,实现组件的复用。


(1)抽取新增/修改页面到 /module-departments/components/add.vue 中

<template>
    <el-dialog title="编辑部门" :visible.sync="dialogFormVisible">
      <!-- model : 数据模型 -->
      <el-form :model="dept" label-width="120px">
        <el-form-item label="部门名称">
          <el-input v-model="dept.name" autocomplete="off"></el-input>
        </el-form-item>
                <el-form-item label="部门编码">
          <el-input v-model="dept.code" autocomplete="off"></el-input>
        </el-form-item>
                <el-form-item label="部门负责人">
          <el-input v-model="dept.manager" autocomplete="off"></el-input>
        </el-form-item>
                <el-form-item label="部门介绍">
          <el-input v-model="dept.introduce" autocomplete="off"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="saveDept">确 定</el-button>
      </div>
    </el-dialog>
</template>
<script>
import {list,saveOrupdate,find,deleteById} from "@/api/base/dept"
export default {
 data () {
     return {
      //添加部门的模型
      parentId:'',
      dialogFormVisible:false,
      dept:{}
     }
 },
  methods: {
    saveDept() {
      this.dept.parentId = this.parentId
      saveOrupdate(this.dept).then(res => {
        this.$message({
          message: res.data.message,
          type: res.data.success?'success':'error'
        });
        //保存成功
        if(res.data.success) {
          //如果成功
          location.reload();
        }
      })
    }
  }
}
</script>
<style>
</style>

(2) 在 /module-departments/page/index.vue 中引用组件

  • 导入组件
import deptAdd from './../components/add'  //导入组件
export default { 
  components: { deptAdd }, //声明组件
  data() {
    return {
      deptAdd: 'deptAdd', //配置组件别名
      activeName: 'first', 
      departData:{},
   }
 },
  ....
}
  • 使用组件
//v-bind:is (绑定的组件名称)
//ref : 引用子组件中内容的别名
<component v-bind:is="deptAdd" ref="deptAdd"></component>
  • 改造新增修改方法
    handlAdd(parentId) {
      //对子组件中的属性复制
      this.$refs.deptAdd.formData = {};
      this.$refs.deptAdd.parentId = parentId
      this.$refs.deptAdd.dialogFormVisible = true;
   },
    handleEdit(id) {
      detail({id}).then( res=> {
        this.$refs.deptAdd.formData = res.data.data
        this.$refs.deptAdd.dialogFormVisible = true
        this.$refs.deptAdd.parentId = res.data.data.parentId
     })
   },

骚戴理解:this.$refs.deptAdd.formData这样写可以在父Vue中调用组件的属性formData


RBAC模型

什么是RBAC

RBAC(全称:Role-Based Access Control)基于角色的权限访问控制,作为传统访问控制(自主访问,强制访问)的有前景的代替受到广泛的关注。在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些 角色的权限。这就极大地简化了权限的管理。在一个组织中,角色是为了完成各种工作而创造,用户则依据它的责 任和资格来被指派相应的角色,用户可以很容易地从一个角色被指派到另一个角色。角色可依新的需求和系统的合 并而赋予新的权限,而权限也可根据需要而从某角色中回收。角色与角色的关系可以建立起来以囊括更广泛的客观情况。


访问控制是针对越权使用资源的防御措施,目的是为了限制访问主体(如用户等) 对访问客体(如数据库资源等) 的访问权限。企业环境中的访问控制策略大部分都采用基于角色的访问控制(RBAC)模型,是目前公认的解决大型企业的统一资源访问控制的有效方法


基于RBAC的设计思路

基于角色的访问控制基本原理是在用户和访问权限之间加入角色这一层,实现用户和权限的分离,用户只有通过激 活角色才能获得访问权限。通过角色对权限分组,大大简化了用户权限分配表,间接地实现了对用户的分组,提高 了权限的分配效率。且加入角色层后,访问控制机制更接近真实世界中的职业分配,便于权限管理。



在RBAC模型中,角色是系统根据管理中相对稳定的职权和责任来划分,每种角色可以完成一定的职能。用户通过 饰演不同的角色获得角色所拥有的权限,一旦某个用户成为某角色的成员,则此用户可以完成该角色所具有的职能。通过将权限指定给角色而不是用户,在权限分派上提供了极大的灵活性和极细的权限指定粒度。


骚戴理解:RBAC模型其实就是给角色分配固定的权限,然后给用户去分配角色,这样用户就具有了这个角色有的所有权限,从而达到权限控制的目的,如果不这样的话,那就需要手动的为用户添加一个个的权限来实现权限管理


表结构分析

一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。

SAAS-HRM中的权限设计

需求分析

SAAS平台的基本元素

SAAS平台管理员:负责平台的日常维护和管理,包括用户日志的管理、租户账号审核、租户状态管理、租户费用 的管理,要注意的是平台管理员不能对租户的具体业务进行管理


企业租户:指访问SaaS平台的用户企业,在SaaS平台中各租户之间信息是独立的。


租户管理员:为租户角色分配权限和相关系统管理、维护。


租户角色:根据业务功能租户管理员进行角色划分,划分好角色后,租户管理员可以对相应的角色进行权限分配


租户用户:需对租户用户进行角色分配,租户用户只能访问授权的模块信息。


需求分析

在应用系统中,权限是以什么样的形式展现出来的?对菜单的访问,页面上按钮的可见性,后端接口的控制,都要进行充分考虑


前端


前端菜单:根据是否有请求菜单权限进行动态加载按钮:根据是否具有此权限点进行显示/隐藏的控制


后端


前端发送请求到后端接口,有必要对接口的访问进行权限的验证

权限设计

针对这样的需求,在有些设计中可以将菜单,按钮,后端API请求等作为资源,这样就构成了基于RBAC的另一种授权模型(用户-角色-权限-资源)。在SAAS-HRM系统的权限设计中我们就是才用了此方案


针对此种权限模型,其中权限究竟是属于菜单,按钮,还是API的权限呢?那就需要在设计数据库权限表的时候添加类型加以区分(如权限类型 1为菜单 2为功能 3为API)。


表结构分析


这里要注意的是,权限表与权限菜单表、页面元素表与API接口表都是一对一的关系,与传统的RBAC模型对比不难发现此种设计的好处:


不需要区分哪些是操作,哪些是资源

方便扩展,当系统要对新的东西进行权限控制时,我只需要建立一个新的资源表,并确定这类权限的权限类 型标识即可。

骚戴理解:这里的权限可能是菜单,页面元素,API接口,具体这个权限是什么由权限表里的权限字段来控制,权限字段如果是1,那就是菜单,如果是2,那就是页面元素,如果是3,那就是API接口,然后再去对应的表中去查询这个权限有哪些菜单,有哪些页面元素,有哪些API接口,这里我一开始理解的是一对多得到关系,而它这里是其实是指的权限表和一套菜单,一套页面元素,一套API,所以是一对一关系


用户管理

需求分析

用户其实就是saas企业访问的员工,对企业员工完成基本的CRUD操作表结构如下:

CREATE TABLE `bs_user` (
`id` varchar(40) NOT NULL COMMENT 'ID',
`mobile` varchar(40) NOT NULL COMMENT '手机号码',
`username` varchar(255) NOT NULL COMMENT '用户名称',
`password` varchar(255) DEFAULT NULL COMMENT '密码',
`enable_state` int(2) DEFAULT '1' COMMENT '启用状态 0是禁用,1是启用',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`department_id` varchar(40) DEFAULT NULL COMMENT '部门ID',
`time_of_entry` datetime DEFAULT NULL COMMENT '入职时间',
`form_of_employment` int(1) DEFAULT NULL COMMENT '聘用形式',
`work_number` varchar(20) DEFAULT NULL COMMENT '工号',
`form_of_management` varchar(8) DEFAULT NULL COMMENT '管理形式',
`working_city` varchar(16) DEFAULT NULL COMMENT '工作城市',
`correction_time` datetime DEFAULT NULL COMMENT '转正时间',
`in_service_status` int(1) DEFAULT NULL COMMENT '在职状态 1.在职  2.离职',
`company_id` varchar(40) DEFAULT NULL COMMENT '企业ID',
`company_name` varchar(40) DEFAULT NULL,
`department_name` varchar(40) DEFAULT NULL, PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_phone` (`mobile`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

配置系统微服务

  • 搭建系统微服务模块(ihrm_system),pom引入依赖
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.ihrm</groupId>
            <artifactId>ihrm_common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>


  • 配置application.yml
server:
 port: 9002
spring:
 application:
   name: ihrm-system #指定服务名
 datasource:
   driver-class-name: com.mysql.jdbc.Driver
   url: jdbc:mysql://localhost:3306/ihrm?useUnicode=true&characterEncoding=utf8
   username: root
   password: 111111
 jpa:
   database: MySQL
   show-sql: true
   open-in-view: true
  • 配置启动类
package com.ihrm.system;
import com.ihrm.common.utils.IdWorker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
@SpringBootApplication(scanBasePackages = "com.ihrm")
@EntityScan("com.ihrm.domain.system")
public class SystemApplication {
    public static void main(String[] args) {
        SpringApplication.run(SystemApplication.class, args);
   }
    @Bean
    public IdWorker idWorkker() {
        return new IdWorker(1, 1);
   }
}


后端用户基本操作

实体类

package com.ihrm.domain.system;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
/**
 * 用户实体类
 */
@Entity
@Table(name = "bs_user")
@Getter
@Setter
public class User implements Serializable {
    private static final long serialVersionUID = 4297464181093070302L;
    /**
     * ID
     */
    @Id
    private String id;
    /**
     * 手机号码
     */
    private String mobile;
    /**
     * 用户名称
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 启用状态 0为禁用 1为启用
     */
    private Integer enableState;
    /**
     * 创建时间
     */
    private Date createTime;
    private String companyId;
    private String companyName;
    /**
     * 部门ID
     */
    private String departmentId;
    /**
     * 入职时间
     */
    private Date timeOfEntry;
    /**
     * 聘用形式
     */
    private Integer formOfEmployment;
    /**
     * 工号
     */
    private String workNumber;
    /**
     * 管理形式
     */
    private String formOfManagement;
    /**
     * 工作城市
     */
    private String workingCity;
    /**
     * 转正时间
     */
    private Date correctionTime;
    /**
     * 在职状态 1.在职  2.离职
     */
    private Integer inServiceStatus;
    private String departmentName;
    /**
     *  JsonIgnore
     *     : 忽略json转化
     */
    @JsonIgnore
    @ManyToMany
    @JoinTable(name="pe_user_role",
            joinColumns={@JoinColumn(name="user_id",referencedColumnName="id")},
            inverseJoinColumns={@JoinColumn(name="role_id",referencedColumnName="id")}
    )
    private Set<Role> roles = new HashSet<Role>();//用户与角色   多对多
}

骚戴理解:


@jsonignore注解用于忽略指定的属性(字段),即在序列化/反序列化过程中不将该字段作为json数据的一部分。该注解通常用于保护敏感信息或避免无限循环引用等问题。这里是为了避免循环引用的情况,因为在User里private Set<Role> roles = new HashSet<Role>();而在Role里private Set<User> users = new HashSet<User>(0);所以循环依赖了,加这个注解就没事了

@manytomany注解表示多对多关系,即一个实体类(entity)实例可以关联到多个其他实体类例,同时一个实体类实例也可以被多个其他实体类实例所关联。在jpa中,多对多关系需要通过中间表来实现。

@jointable注解则用于定义多对多关系中的中间表的结构细节和关联方式。其中包括中间表的名称、关联字段、外键等信息。这些信息可以通过joincolumns和inversejoincolumns子注解进行详细配置。

name: 指定关联表(中间表)的名称,这里为pe_user_role。

joinColumns={@JoinColumn(name="user_id",referencedColumnName="id")}里的joinColumns表示关联当前类和中间表和外部表的字段的关系,name表示当前类所对应表的主键在关联表(中间表)中的字段名,referencedColumnName表示外部表的字段,也就是角色表中的id

inverseJoinColumns={@JoinColumn(name="role_id",referencedColumnName="id")}里的inverseJoinColumns表示关联外部表和当前类所对应的表和中间表的字段的关系,name表示外部表主键在关联表(中间表)中的字段名,referencedColumnName表示当前类所对应的表的字段,也就是用户表中的id


持久化层

package com.ihrm.system.dao;
import com.ihrm.system.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
/**
  * 企业数据访问接口
  */
public interface UserDao extends JpaRepository<User, String>, 
JpaSpecificationExecutor<User> {
}


业务逻辑层

package com.ihrm.system.service;
import com.ihrm.common.utils.IdWorker;
import com.ihrm.domain.system.Role;
import com.ihrm.domain.system.User;
import com.ihrm.system.dao.RoleDao;
import com.ihrm.system.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.*;
/**
* 部门操作业务逻辑层
*/
@Service
public class UserService {
    @Autowired
    private IdWorker idWorker;
    @Autowired
    private UserDao userDao;
    @Autowired
    private RoleDao roleDao;
    public User findByMobileAndPassword(String mobile, String password) {
        User user = userDao.findByMobile(mobile);
        if (user != null && password.equals(user.getPassword())) {
            return user;
       } else {
            return null;
       }
   }
    /**
     * 添加用户
     */
    public void save(User user) {
        //填充其他参数
        user.setId(idWorker.nextId() + "");
        user.setCreateTime(new Date()); //创建时间
        user.setPassword("123456");//设置默认登录密码
        user.setEnableState(1);//状态
        userDao.save(user);
   }
    /**
     * 更新用户
     */
    public void update(User user) {
        User targer = userDao.getOne(user.getId());
        targer.setPassword(user.getPassword());
        targer.setUsername(user.getUsername());
        targer.setMobile(user.getMobile());
        targer.setDepartmentId(user.getDepartmentId());
        targer.setDepartmentName(user.getDepartmentName());
        userDao.save(targer);
   }
    /**
     * 根据ID查询用户
     */
    public User findById(String id) {
        return userDao.findById(id).get();
   }
    /**
     * 删除部门
     *
     * @param id 部门ID
     */
    public void delete(String id) {
        userDao.deleteById(id);
   }
    public Page<User> findSearch(Map<String,Object> map, int page, int size) {
        return userDao.findAll(createSpecification(map), PageRequest.of(page-1, size));
   }
    /**
     * 调整部门
     */
    public void changeDept(String deptId,String deptName,List<String> ids) {
        for (String id : ids) {
            User user = userDao.findById(id).get();
            user.setDepartmentName(deptName);
            user.setDepartmentId(deptId);
            userDao.save(user);
       }
   }
    /**
     * 分配角色
     */
    public void assignRoles(String userId,List<String> roleIds) {
        User user = userDao.findById(userId).get();
        Set<Role> roles = new HashSet<>();
        for (String id : roleIds) {
            Role role = roleDao.findById(id).get();
            roles.add(role);
       }
        //设置用户和角色之间的关系
        user.setRoles(roles);
        userDao.save(user);
   }
    /**
     * 动态条件构建
     * @param searchMap
     * @return
     */
    private Specification<User> createSpecification(Map searchMap) {
        return new Specification<User>() {
            @Override
            public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, 
CriteriaBuilder cb) {
                List<Predicate> predicateList = new ArrayList<Predicate>();
                // ID
                if (searchMap.get("id") !=null && !"".equals(searchMap.get("id"))) {
                    predicateList.add(cb.equal(root.get("id").as(String.class), 
(String)searchMap.get("id")));
               }
                // 手机号码
                if (searchMap.get("mobile")!=null &&
!"".equals(searchMap.get("mobile"))) {
                    predicateList.add(cb.equal(root.get("mobile").as(String.class), 
(String)searchMap.get("mobile")));
               }
                // 用户ID
                if (searchMap.get("departmentId")!=null &&
!"".equals(searchMap.get("departmentId"))) {
predicateList.add(cb.like(root.get("departmentId").as(String.class), 
(String)searchMap.get("departmentId")));
               }
                // 标题
                if (searchMap.get("formOfEmployment")!=null &&
!"".equals(searchMap.get("formOfEmployment"))) {
predicateList.add(cb.like(root.get("formOfEmployment").as(String.class), 
(String)searchMap.get("formOfEmployment")));
               }
                if (searchMap.get("companyId")!=null &&
!"".equals(searchMap.get("companyId"))) {
                    predicateList.add(cb.like(root.get("companyId").as(String.class), 
(String)searchMap.get("companyId")));
               }
                if (searchMap.get("hasDept")!=null &&
!"".equals(searchMap.get("hasDept"))) {
                    if("0".equals((String)searchMap.get("hasDept"))) {
                        predicateList.add(cb.isNull(root.get("departmentId")));
                   }else{
                        predicateList.add(cb.isNotNull(root.get("departmentId")));
                   }
               }
                return cb.and( predicateList.toArray(new
Predicate[predicateList.size()]));
           }
       };
   }
}

骚戴理解:在SpringData的JPA中save方法是保持数据,当实体中包含主键时,JPA的save方法会进行更新操作。这个系统的所有新增都是给了id的,所以新增其实都是修改操作。


new PageRequest(page-1, size)中page-1是因为下标是从0开始的


criteriabuilder.and(list.toarray(new predicate[list.size()])); 是一个java语言的代码片段,用于创建一个and条件的jpa查询。


该代码的解释如下:


criteriabuilder代表一个criteriabuilder实例,它是jpa criteria api中的一个辅助类。criteriabuilder可以用于创建一些查询相关的对象,比如predicate(查询条件)。

list是一个list<predicate>类型的集合,用于存放对某个字段的多个查询条件(例如:相等,大于,小于等),多个条件之间的关系是“且”的关系。

toarray()方法将predicate集合转换成数组类型。

new predicate[list.size()] 创建了一个长度为list.size()的predicate数组。

list.toarray(new predicate[list.size()])简单理解就是把list转化成predicate数组

criteriabuilder.and(...)表示将predicate数组中的所有条件使用“且”的关系连接在一起。

因此,该代码片段的作用是在jpa查询中添加多个and条件


控制器层

package com.ihrm.system.controller;
import com.ihrm.common.controller.BaseController;
import com.ihrm.common.entity.PageResult;
import com.ihrm.common.entity.Result;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.domain.system.User;
import com.ihrm.system.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
import javax.websocket.server.PathParam;
import java.util.List;
import java.util.Map;
//1.解决跨域
@CrossOrigin
//2.声明restContoller
@RestController
//3.设置父路径
@RequestMapping(value="/sys")
public class UserController extends BaseController {
    @Autowired
    private UserService userService;
    /**
     * 保存
     */
    @RequestMapping(value = "/user", method = RequestMethod.POST)
    public Result save(@RequestBody User user) {
        //1.设置保存的企业id
        user.setCompanyId(parseCompanyId());
        user.setCompanyName(parseCompanyName());
        //2.调用service完成保存企业
        userService.save(user);
        //3.构造返回结果
        return new Result(ResultCode.SUCCESS);
    }
    /**
     * 查询企业的用户列表
     * 指定企业id
     */
    @RequestMapping(value = "/user", method = RequestMethod.GET)
    public Result findAll(int page, int size, @RequestParam Map map) {
        //1.获取当前的企业id
        map.put("companyId",parseCompanyId());
        //2.完成查询
        Page<User> pageUser = userService.findAll(map,page,size);
        //3.构造返回结果
        PageResult pageResult = new PageResult(pageUser.getTotalElements(),pageUser.getContent());
        return new Result(ResultCode.SUCCESS, pageResult);
    }
    /**
     * 根据ID查询user
     */
    @RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
    public Result findById(@PathVariable(value = "id") String id) {
        User user = userService.findById(id);
        return new Result(ResultCode.SUCCESS, user);
    }
    /**
     * 修改User
     */
    @RequestMapping(value = "/user/{id}", method = RequestMethod.PUT)
    public Result update(@PathVariable(value = "id") String id, @RequestBody User user) {
        //1.设置修改的部门id
        user.setId(id);
        //2.调用service更新
        userService.update(user);
        return new Result(ResultCode.SUCCESS);
    }
    /**
     * 根据id删除
     */
    @RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)
    public Result delete(@PathVariable(value = "id") String id) {
        userService.deleteById(id);
        return new Result(ResultCode.SUCCESS);
    }
}


前端用户基本操作

由于时间有限,本着不浪费时间的原则,页面部分的基本功能都是大致相似的。使用提供的基本模块代码构建模块信息。


配置接口请求路径

由于后端是微服务,每个工程的端口号都不一样,所以这里就需要用到vue提供的代理,修改config\dev.env.js

'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
  NODE_ENV: '"development"',
  BASE_API: '"api"'
})

在config/index.js中通过proxyTable配置代理转发的请求后端地址

   proxyTable: {
      //企业信息请求的远程服务
      '/api/company': {
          target: 'http://localhost:9001/company/',
          changeOrigin: true,
          pathRewrite: {
            '^/api/company': ''
          }
        },
        //api/sys/     user
       '/api/sys': {
          target: 'http://localhost:9002/sys/',
          changeOrigin: true,
          pathRewrite: {
            '^/api/sys': ''
          }
        }
    },

骚戴理解:上面这段代码是一个vue.js项目中的配置文件,用于将本地请求代理到对应的远程服务上,以解决跨域问题。其中,proxytable是一个对象,它包含了需要代理的不同路径及其对应的远程服务。


例如,当有请求发起至'/api/company'时,在本地服务器上不能直接访问远程服务


'http://localhost:9001/company/',因此我们在这里通过配置proxytable将其代理到对应的远程服务上。changeorigin为true表示是否改变源,pathrewrite是一个映射规则,将请求的路径中的


'/api/company'部分替换为'',即去除了/api/company的前缀。同样的方式适用于/api/sys的请求。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
8月前
|
供应链 JavaScript 数据挖掘
一套SaaS ERP管理系统源码,生产管理系统源代码
小微企业SaaS ERP系统,基于SpringBoot+Vue+UniAPP开发,集成进销存、采购销售、MRP生产、财务、CRM、OA等全流程管理功能,支持自定义表单与工作流,助力企业数字化转型。
487 1
|
8月前
|
消息中间件 运维 监控
SaaS云医院HIS系统源码,运行稳定的区域HIS系统
一套SaaS架构的Java版云HIS系统源码,支持电子病历四级应用。采用前后端分离技术,前端基于Angular,后端使用SpringBoot+MyBatisPlus,结合Redis、RabbitMQ、XXL-JOB等主流组件。
695 2
SaaS云医院HIS系统源码,运行稳定的区域HIS系统
|
存储 缓存 监控
怎么更好地设计一个优秀的SaaS系统
设计一个优秀的SaaS系统,需要从架构、性能、安全性、租户隔离、扩展性等多方面进行深思熟虑。根据业务需求选择合适的多租户架构,保证数据隔离的同时提高系统性能。
1558 1
|
供应链 JavaScript 前端开发
Java基于SaaS模式多租户ERP系统源码
ERP,全称 Enterprise Resource Planning 即企业资源计划。是一种集成化的管理软件系统,它通过信息技术手段,将企业的各个业务流程和资源管理进行整合,以提高企业的运营效率和管理水平,它是一种先进的企业管理理念和信息化管理系统。 适用于小微企业的 SaaS模式多租户ERP管理系统, 采用最新的技术栈开发, 让企业简单上云。专注于小微企业的应用需求,如企业基本的进销存、询价,报价, 采购、销售、MRP生产制造、品质管理、仓库库存管理、财务应收付款, OA办公单据、CRM等。
903 23
|
传感器 小程序 搜索推荐
(源码)java开发的一套(智慧校园系统源码、电子班牌、原生小程序开发)多端展示:web端、saas端、家长端、教师端
通过电子班牌设备和智慧校园数据平台的统一管理,在电子班牌上,班牌展示、学生上课刷卡考勤、考勤状况汇总展示,课表展示,考场管理,请假管理,成绩查询,考试优秀标兵展示、校园通知展示,班级文化各片展示等多种化展示。
310 0
(源码)java开发的一套(智慧校园系统源码、电子班牌、原生小程序开发)多端展示:web端、saas端、家长端、教师端
|
人工智能 BI API
Dify-Plus:企业级AI管理核弹!开源方案吊打SaaS,额度+密钥+鉴权系统全面集成
Dify-Plus 是基于 Dify 二次开发的企业级增强版项目,新增用户额度、密钥管理、Web 登录鉴权等功能,优化权限管理,适合企业场景使用。
2090 3
Dify-Plus:企业级AI管理核弹!开源方案吊打SaaS,额度+密钥+鉴权系统全面集成
|
运维 供应链 前端开发
中小医院云HIS系统源码,系统融合HIS与EMR功能,采用B/S架构与SaaS模式,快速交付并简化运维
这是一套专为中小医院和乡镇卫生院设计的云HIS系统源码,基于云端部署,采用B/S架构与SaaS模式,快速交付并简化运维。系统融合HIS与EMR功能,涵盖门诊挂号、预约管理、一体化电子病历、医生护士工作站、收费财务、药品进销存及统计分析等模块。技术栈包括前端Angular+Nginx,后端Java+Spring系列框架,数据库使用MySQL+MyCat。该系统实现患者管理、医嘱处理、费用结算、药品管控等核心业务全流程数字化,助力医疗机构提升效率和服务质量。
808 4
|
安全 API 定位技术
房产SaaS系统如何利用HTTP代理IP
在信息化时代,网络成为生活的重要部分,HTTP代理IP的应用日益广泛。房产SaaS系统使用HTTP代理IP,可提高数据抓取效率、增强市场竞争力、优化用户体验,并确保系统安全稳定,是不可或缺的工具。主要应用于数据抓取、市场分析、策略调整、用户行为分析、多地区房源展示、提高访问速度和API请求管理等方面。
229 0
|
Oracle 安全 关系型数据库
ERP系统的云计算与SaaS模式:实现高效灵活的企业管理
【7月更文挑战第29天】 ERP系统的云计算与SaaS模式:实现高效灵活的企业管理
834 4
|
数据挖掘 BI API
简单了解CRM与SaaS系统
本文介绍了CRM(客户关系管理系统)和SaaS(软件即服务)的概念、应用场景、两者之间的关系以及CRM接口的作用和设置流程,强调了SaaS模式为CRM系统提供了灵活、便捷、经济高效的使用方式,以及CRM接口在数据集成、自动化流程、功能扩展和数据分析方面的重要性。
650 0