尚品甄選電商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 Policy
為public
上傳頭像
- 使用新的
controller
和service
專門處理檔案上傳 - 直接使用原始檔案名稱會有重複檔名出現的可能性
@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
執行兩個動作
- 根據
userId
刪除該用戶之前已經分配的所有角色 - 重新插入用戶新分配之角色
省去查詢與比對的麻煩
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
打勾,完成顯示