尚品甄選電商SpringBoot-Web開發2用戶權限管理

目標

  • 用戶管理需求和準備
  • 用戶管理api
    • 添加
    • 修改
    • 刪除
  • 用戶管理前端
  • 用戶頭像
    • minIO伺服器
    • 上傳檔案
    • 上傳檔案前端
  • 為用戶分配角色
    • 需求
    • API
    • 整合前端

用戶條件分頁查詢API

  • 一樣是透過controller -> service -> mapper拿到資料list後再回傳給前端
// 用戶條件分頁查詢介面
@GetMapping(value = "/findByPage/{pageNum}/{pageSize}")
public Result findByPage(@PathVariable("pageNum") Integer pageNum,
                            @PathVariable("pageSize") Integer pageSize,
                            SysUserDto sysUserDto) {
    PageInfo<SysUser> pageInfo = sysUserService.findByPage(pageNum, pageSize, sysUserDto);
    return Result.build(pageInfo, ResultCodeEnum.SUCCESS);
}

新增用戶

  • 也是同樣的切割code
  • 用戶名不能重複
  • 只列出未被刪除(is_deleted=0)的數據

Controller

// 用戶新增
@PostMapping("/saveSysUser")
public Result saveSysUser(@RequestBody SysUser sysUser) {
    sysUserService.saveSysUser(sysUser);
    return Result.build(null, ResultCodeEnum.SUCCESS);
}

Service

@Override
public void saveSysUser(SysUser sysUser) {
    // 判斷用戶名不能重複
    String userName = sysUser.getUserName();
    SysUser dbSysUser = sysUserMapper.selectUserInfoByUserName(userName);
    if (dbSysUser != null) {
        throw new SimonException(ResultCodeEnum.USER_NAME_IS_EXISTS);
    }

    // 加密密碼
    String md5_password = DigestUtils.md5DigestAsHex(sysUser.getPassword().getBytes());
    sysUser.setPassword(md5_password);

    // 新增用戶
    sysUserMapper.save(sysUser);
}

剩餘的用戶修改以及刪除功能也都大同小異

用戶管理-前端

前面已經寫好後端了,接下來前端一步一步完成,以下就直接放完成的code了,因為大同小異,調用方法、接收數據、傳送數據、顯示數據…

  • 新增用戶
  • 修改用戶
  • 刪除用戶
  • 搜尋用戶

實作新增用戶的時候後端出現錯誤,通過在globalException printStackTrace找到原因,提示status不能為null,於是在service設定一下sysUser.setStatus(1);順利解決

修改用戶時如果沒有修改用戶名,原本的後端邏輯會檢測到數據庫有重名,造成操作失敗,改為以下程式碼修復成功

// 用戶修改
@Override
public void updateSysUser(SysUser sysUser) {
    // 用戶名不能重複
    String userName = sysUser.getUserName();
    SysUser dbSysUser = sysUserMapper.selectUserInfoByUserName(userName);
    // 當數據庫存在該名稱,且他們id相異時拋出
    if (dbSysUser != null && !sysUser.getId().equals(dbSysUser.getId())) {
        throw new SimonException(ResultCodeEnum.USER_NAME_IS_EXISTS);
    }

    // 修改
    sysUserMapper.update(sysUser);
}
// 全域異常處理
@ExceptionHandler(Exception.class)
@ResponseBody // 回傳JSON
public Result error(Exception e) {
    // 除錯用
    e.printStackTrace();
    return Result.build(null, ResultCodeEnum.SYSTEM_ERROR);
}
import request from '@/utils/request'
import SysUser from '@/views/system/sysUser.vue'

const base_api = '/admin/system/sysUser'

// 用戶列表
export const GetSysUserListByPage = (current, limit, queryDto) => {
  return request({
    // ``模板字串
    url: `${base_api}/findByPage/${current}/${limit}`, // 路徑
    method: 'get', // 提交方式
    // 接口@RequestBody 前端 data:名稱,以JSON格式傳遞
    // 接口沒有註解,前端 params:名稱,以URL格式傳遞
    params: queryDto, // 其他參數
  })
}

// 用戶新增
export const SaveSysUser = sysUser => {
  return request({
    // ``模板字串
    url: `${base_api}/saveSysUser`, // 路徑
    method: 'post', // 提交方式
    data: sysUser, // 其他參數
  })
}

// 用戶修改
export const UpdateSysUser = sysUser => {
  return request({
    // ``模板字串
    url: `${base_api}/updateSysUser`, // 路徑
    method: 'put', // 提交方式
    data: sysUser, // 其他參數
  })
}

// 用戶刪除
export const DeleteSysUser = userId => {
  return request({
    // ``模板字串
    url: `${base_api}/deleteById/${userId}`, // 路徑
    method: 'delete', // 提交方式
  })
}
<script setup>
import { ref, onMounted } from 'vue'
import {
  GetSysUserListByPage,
  SaveSysUser,
  UpdateSysUser,
  DeleteSysUser,
} from '@/api/sysUser'
import { ElMessage, ElMessageBox } from 'element-plus'

// 用戶刪除
const deleteById = row => {
  ElMessageBox.confirm('此操作将永久删除该记录, 是否继续?', 'Warning', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
  }).then(async () => {
    const { code } = await DeleteSysUser(row.id)
    if (code == 200) {
      ElMessage.success('操作成功')
      fetchData()
    } else {
      ElMessage.error('操作失败')
    }
  })
}

// 用戶新增與修改
const dialogVisible = ref(false)

// 定义提交表单数据模型
const form = {
  userName: '',
  name: '',
  phone: '',
  password: '',
  description: '',
  avatar: '',
}
const sysUser = ref(form)

// 點修改,彈框
const editSysUser = row => {
  sysUser.value = { ...row }
  dialogVisible.value = true
}

// 點添加,彈框
const addShow = () => {
  sysUser.value = {}
  dialogVisible.value = true
}

// 提交方法
const submit = async () => {
  // 無id,新增
  if (!sysUser.value.id) {
    const { code } = await SaveSysUser(sysUser.value)
    if (code == 200) {
      dialogVisible.value = false
      ElMessage.success('操作成功')
      fetchData()
    } else {
      ElMessage.error('操作失败')
    }
  } else {
    const { code } = await UpdateSysUser(sysUser.value)
    if (code == 200) {
      dialogVisible.value = false
      ElMessage.success('操作成功')
      fetchData()
    } else {
      ElMessage.error('操作失败')
    }
  }
}

// 表格数据模型
const list = ref([])

// 分页条数据模型
const total = ref(0)

// 定义搜索表单数据模型
const queryDto = ref({
  keyword: '',
  createTimeBegin: '',
  createTimeEnd: '',
})
const createTimes = ref([])

//分页数据
const pageParamsForm = {
  page: 1, // 页码
  limit: 3, // 每页记录数
}
const pageParams = ref(pageParamsForm)

// onMounted钩子函数
onMounted(() => {
  fetchData()
})

// 搜尋按钮点击事件处理函数
const searchSysUser = () => {
  fetchData()
}

// 重置按钮点击事件处理函数
const resetData = () => {
  queryDto.value = {}
  createTimes.value = []
}

// 定义分页查询方法
const fetchData = async () => {
  if (createTimes.value.length == 2) {
    queryDto.value.createTimeBegin = createTimes.value[0]
    queryDto.value.createTimeEnd = createTimes.value[1]
  }
  // 请求后端接口进行分页查询
  const { data } = await GetSysUserListByPage(
    pageParams.value.page,
    pageParams.value.limit,
    queryDto.value
  )
  list.value = data.list
  total.value = data.total
}

用戶頭像

使用minio

一個使用Golang開發的雲端儲存的開源專案,專注於儲存大量的非結構化的數據,如圖片、影片、文字等任何非結構化的數據,類似於AWS S3的開源版本,可以在影像辨識、NLP模型的訓練及重新部署等情境發揮作用

安裝後新增bucket並且設定Access Policypublic

上傳頭像

  • 使用新的controllerservice專門處理檔案上傳
  • 直接使用原始檔案名稱會有重複檔名出現的可能性
@PostMapping("/fileUpload")
public Result fileUpload(MultipartFile file) {
    // 取得上傳的檔案
    // 呼叫service的方法上傳,回傳miniox路徑
    String url = fileUploadService.upload(file);
    return Result.build(url, ResultCodeEnum.SUCCESS);
}
@Service
public class FileUploadServiceImpl implements FileUploadService {
    @Override
    public String upload(MultipartFile file) {
        final String BUCKET_NAME = "spzx-bucket";

        try {
            // 建立Minio物件,使用帳號密碼
            MinioClient minioClient =
                    MinioClient.builder()
                            .endpoint("http://localhost:9000")
                            .credentials("minioadmin", "minioadmin")
                            .build();

            // 建立spzx-bucket如果不存在的話
            boolean found =
                    minioClient.bucketExists(BucketExistsArgs.builder().bucket(BUCKET_NAME).build());
            if (!found) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(BUCKET_NAME).build());
            } else {
                System.out.println("Bucket '" + BUCKET_NAME + "' already exists.");
            }

            final String FILENAME = file.getOriginalFilename();

            // 上傳
            minioClient.putObject(
                    PutObjectArgs.builder().bucket(BUCKET_NAME)
                            .object(FILENAME)
                            .stream(file.getInputStream(), file.getSize(), -1)
                            .build());

            // 取得上傳檔案在minio的路徑
            // http://127.0.0.1:9000/spzx-bucket/csmap.jpg
            return "http://127.0.0.1:9000/spzx-bucket/" + FILENAME;
        } catch (Exception e) {
            e.printStackTrace();
            throw new SimonException(ResultCodeEnum.SYSTEM_ERROR);
        }
    }
}

基礎的做完之後,接下來優化程式碼

使用設定檔案去除寫死的string

設定檔案

spzx:
  auth:
    noAuthUrls:
      - /admin/system/index/login
      - /admin/system/index/generateValidateCode

  minio:
    endpointUrl: http://127.0.0.1:9000
    accessKey: minioadmin
    secretKey: minioadmin
    bucketName: spzx-bucket

定義設定檔案class

@Data
@ConfigurationProperties(prefix = "spzx.minio")
public class MinioProperties {

    private String endpointUrl;
    private String accessKey;
    private String secretKey;
    private String bucketName;
}

加入設定檔案至主程式

@EnableConfigurationProperties(value = {UserProperties.class, MinioProperties.class})
public class ManagerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ManagerApplication.class, args);
    }
}

重複檔案名稱問題解決

直接加上uuid隨機化名子以及加上日期分組

// 讓上傳的檔案名稱是唯一的,使用uuid
// 根據上傳日期對檔案進行分類,20240418

// e.g. 20240418/tu29iueo0-2csmap.jpg
String dateDir = DateUtil.format(new Date(), "yyyyMMdd");

final String uuid = UUID.randomUUID().toString().replaceAll("-", "");
final String FILENAME = dateDir + "/" + uuid + file.getOriginalFilename();

修改用戶頭像時有bug,需要在mapper寫入資料庫

<if test="avatar != null and avatar != ''">
    avatar = #{avatar},
</if>

角色分配

為了讓每個用戶有自己的角色(多個角色),需要更新用戶與角色之間的關係表,也就是做用戶與角色表之間的關聯

為用戶分配角色

查詢所有角色

SysRoleController.java

使用Map的目的是我們最終要兩種資料

  • 全部角色的list
  • 每個用戶被分配的角色list
// 查詢所有角色
@GetMapping("/findAllRoles")
public Result findAllRoles() {
    // 資料名稱->資料內容
    Map<String, Object> map = sysRoleService.findAll();
    return Result.build(map, ResultCodeEnum.SUCCESS);
}

SysRoleServiceImpl.java

// 查詢所有角色
@Override
public Map<String, Object> findAll() {
    // 查詢所有角色
    List<SysRole> roleList = sysRoleMapper.findAll();

    // 分配的角色列表
    // TODO

    Map<String, Object> map = new HashMap<>();
    map.put("allRolesList", roleList);

    return null;
}

SysRoleMapper.xml

<!-- 查詢所有角色 -->
<select id="findAll" resultMap="sysRoleMap">
    select <include refid="columns"/>
    from sys_role
    where is_deleted=0
</select>

前端一樣調用後端API然後顯示數據,vue內要記得引入js檔案

儲存用戶分配的角色

controller從前端拿到請求後,呼叫service執行兩個動作

  1. 根據userId刪除該用戶之前已經分配的所有角色
  2. 重新插入用戶新分配之角色

省去查詢與比對的麻煩

SysUserServiceImpl.java

// 用戶分配角色
@Transactional
@Override
public void doAssign(AssignRoleDto assignRoleDto) {
    // 根據userId刪除之前分配的角色資料
    sysRoleUserMapper.deleteByUserId(assignRoleDto.getUserId());

    // 取得角色ID list
    List<Long> roleIdList = assignRoleDto.getRoleIdList();
    // 疊代取得所有角色ID
    // 插入資料庫,關聯 userId <-> roleId
    for (Long roleId :
            roleIdList) {
        sysRoleUserMapper.doAssign(assignRoleDto.getUserId(), roleId);
    }
}

顯示用戶分配過的角色

沿用前面搜尋所有角色的api,只需要多傳入userId

// 查詢所有角色,以及每個用戶被分配的角色
@GetMapping("/findAllRoles/{userId}")
public Result findAllRoles(@PathVariable("userId") Long userId) {
    // 資料名稱->資料內容
    Map<String, Object> map = sysRoleService.findAll(userId);
    return Result.build(map, ResultCodeEnum.SUCCESS);
}

然後再多寫一個方法利用userId查詢該id的所有roleId

@Override
public Map<String, Object> findAll(Long userId) {
    // 查詢所有角色
    List<SysRole> roleList = sysRoleMapper.findAll();

    // 分配的角色列表
    // 根據userId搜尋用戶分配過的角色ids
    List<Long> roleIds = sysRoleUserMapper.selectRoleIdsByUserId(userId);

    Map<String, Object> map = new HashMap<>();
    map.put("allRolesList", roleList);
    map.put("sysUserRoles", roleIds);

    return map;
}

前端的部分也是改成呼叫時傳userId給後端

export const GetAllRoleList = (userId) => {
  return request({
    url: `/admin/system/sysRole/findAllRoles/${userId}`,
    method: 'get',
  })
}

結果拿到後element-plus就會依照roleId把每個用戶自己的role打勾,完成顯示


尚品甄選電商SpringBoot-Web開發2用戶權限管理
https://f88083.github.io/2024/04/22/尚品甄選電商SpringBoot-Web開發2用戶權限管理/
作者
Simon Lai
發布於
2024年4月22日
許可協議