数据库最终版本
sys_role

sys_team

sys_user_team

sys_invitation

建表
-- 用户-团队关联表(用户与团队的多对多关系,存储用户在团队中的角色)
DROP TABLE IF EXISTS `sys_user_team`;
CREATE TABLE `sys_user_team` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` INT NOT NULL COMMENT '用户ID(关联user表id)',
`team_id` INT NOT NULL COMMENT '团队ID(关联sys_team表team_id)',
`role_id` TINYINT NOT NULL COMMENT '角色ID(关联sys_role表role_id)',
`join_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '加入团队时间',
`user_name` VARCHAR(50) DEFAULT '' COMMENT '关联用户姓名(冗余字段,优化查询)',
`team_name` VARCHAR(100) DEFAULT '' COMMENT '关联团队名称(冗余字段,优化查询)',
`role_name` VARCHAR(50) DEFAULT '' COMMENT '关联角色名称(冗余字段,优化查询)',
PRIMARY KEY (`id`),
-- 联合唯一索引:避免用户在同一团队重复关联
UNIQUE KEY `uk_SYS_user_team` (`user_id`, `team_id`),
-- 外键约束(关联用户表)
CONSTRAINT `fk_SYS_user_team_user` FOREIGN KEY (`user_id`)
REFERENCES `user` (`id`)
ON DELETE CASCADE -- 当用户被删除时,自动删除该用户的所有团队关联记录
ON UPDATE CASCADE, -- 当用户ID更新时,自动同步更新
-- 外键约束(关联团队表)
CONSTRAINT `fk_SYS_user_team_team` FOREIGN KEY (`team_id`)
REFERENCES `sys_team` (`team_id`)
ON DELETE CASCADE -- 当团队被删除时,自动删除该团队的所有用户关联记录
ON UPDATE CASCADE,
-- 外键约束(关联角色表)
CONSTRAINT `fk_SYS_user_team_role` FOREIGN KEY (`role_id`)
REFERENCES `sys_role` (`role_id`)
ON DELETE RESTRICT -- 禁止删除已被用户-团队关联使用的角色
ON UPDATE CASCADE,
-- 普通索引:优化按团队、角色查询的场景
KEY `idx_team_id` (`team_id`),
KEY `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户-团队关联表';
修改user表新增email字段,用于邀请成员逻辑

前端
1. api配置
javascript
// api/team.js
import request from '@/utils/request'
// 获取团队成员分页列表
export function getTeamMembers(params) {
return request({
url: '/team/members',
method: 'get',
params: {
page: params.page,
size: params.size
}
})
}
// 邀请成员
export function inviteMember(data) {
return request({
url: '/team/invite',
method: 'post',
data
})
}
// 修改成员角色
export function updateMemberRole(data) {
return request({
url: '/team/member/role',
method: 'put',
data
})
}
// 移除成员
export function removeMember(userTeamId) {
return request({
url: `/team/member/${userTeamId}`,
method: 'delete'
})
}
2. 前端逻辑
javascript
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
UserFilled, Plus, Edit, Delete, Message, Link
} from '@element-plus/icons-vue'
import {
getTeamMembers, inviteMember, updateMemberRole, removeMember
} from '@/api/team.js'
const route = useRoute()
// ========== 页面状态 ==========
const loading = ref(false)
const inviteDialogVisible = ref(false)
const inviteMethod = ref('email')
const selectedRole = ref('member')
const emailAddress = ref('')
const inviteLink = ref('')
// 删除成员的对话框状态
const removeDialogVisible = ref(false)
const removeTarget = ref(null)
// 打开删除对话框
const handleRemoveDialog = (member) => {
removeTarget.value = member
removeDialogVisible.value = true
}
//执行删除
const confirmRemoveMember = async () => {
try {
const response = await removeMember(removeTarget.value.id)
if (response.code === 200) {
ElMessage.success('成员移除成功')
removeDialogVisible.value = false
loadTeamMembers()
} else {
ElMessage.error(response.msg || '移除失败')
}
} catch (error) {
ElMessage.error('移除失败,请稍后重试')
}
}
// 分页
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 角色选项
const roleOptions = [
{ label: '管理员', value: 'admin' },
{ label: '只读角色', value: 'readonly' },
{ label: '普通成员', value: 'member' }
]
// 成员列表
const teamMembers = ref([])
const roleDialogVisible = ref(false)
const roleForm = reactive({
userId: null,
role: ''
})
// 打开编辑角色对话框
const openRoleDialog = (member) => {
roleForm.userId = member.id
roleForm.role = member.role
roleDialogVisible.value = true
}
// 提交修改
const submitRoleUpdate = async () => {
const roleMap = {
admin: 3,
member: 2,
readonly: 1
}
const response = await updateMemberRole({
userId: roleForm.userId,
roleId: roleMap[roleForm.role]
})
if (response.code === 1) {
ElMessage.success('角色修改成功')
roleDialogVisible.value = false
loadTeamMembers()
} else {
ElMessage.error(response.msg || '修改失败')
}
}
// ========== 加载团队成员 ==========
const loadTeamMembers = async () => {
try {
loading.value = true
const params = {
page: pagination.currentPage,
size: pagination.pageSize
}
const response = await getTeamMembers(params)
if (response.code === 1) {
teamMembers.value = response.data.records || []
pagination.total = response.data.total || 0
} else {
ElMessage.error(response.msg || '获取团队成员失败')
}
} catch (error) {
ElMessage.error('网络错误,请稍后重试')
console.error('加载团队成员失败:', error)
} finally {
loading.value = false
}
}
// 页面初始化加载
onMounted(() => loadTeamMembers())
// 路由变化自动刷新
watch(() => route.fullPath, () => loadTeamMembers())
// ========== 邀请成员 ==========
const handleInvite = async () => {
try {
if (inviteMethod.value === 'email' && !emailAddress.value) {
ElMessage.warning('请输入邮箱地址')
return
}
const inviteData = {
inviteMethod: inviteMethod.value,
email: emailAddress.value,
role: selectedRole.value
}
const response = await inviteMember(inviteData)
if (response.code === 1) {
ElMessage.success(inviteMethod.value === 'email' ? '邀请发送成功' : '邀请链接生成成功')
if (inviteMethod.value === 'link') {
inviteLink.value = response.data
// 链接邀请:不关闭窗口,仅生成链接(用户手动点击取消关闭)
} else {
// 邮件邀请:成功后关闭窗口,清空邮箱
inviteDialogVisible.value = false
emailAddress.value = ''
loadTeamMembers() // 邮件邀请成功后刷新成员列表(链接邀请无需刷新)
}
} else {
ElMessage.error(response.msg || '邀请失败')
}
} catch (error) {
ElMessage.error('邀请失败,请检查网络连接')
console.error('邀请成员失败:', error)
}
}
// ========== 修改权限(最终生效版,解决弹窗无响应) ==========
const handleEditRole = async (member) => {
try {
// 1. 确认修改弹窗(保持不变)
await ElMessageBox.confirm(
`确定要修改 ${member.username} 的角色吗?`,
'修改角色',
{
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消',
// 确保按钮可点击(避免样式阻塞)
customClass: 'no-block-btn'
}
)
// 2. 创建响应式的角色值
const selectedRole = ref(member.role)
// 3. 使用 h() 函数创建包含下拉框的 MessageBox
const { value: newRole } = await ElMessageBox({
title: '选择新角色',
message: h('div', [
h('div', { style: 'margin-bottom: 15px; color: #606266;' }, '请选择新角色:'),
h('el-select', {
modelValue: selectedRole.value,
'onUpdate:modelValue': (val) => { selectedRole.value = val },
style: 'width: 100%',
placeholder: '请选择角色'
}, [
h('el-option', { label: '管理员', value: 'admin' }),
h('el-option', { label: '只读角色', value: 'readonly' }),
h('el-option', { label: '普通成员', value: 'member' })
])
]),
beforeClose: (action, instance, done) => {
if (action === 'confirm' && !selectedRole.value) {
ElMessage.warning('请选择角色')
return false
}
done()
}
})
// 3. 角色字符串 → 后端数字ID(不变)
const roleMap = {
admin: 3,
member: 2,
readonly: 1
};
const roleId = roleMap[newRole];
// 4. 调用后端接口(不变)
const response = await updateMemberRole({
userId: member.id,
roleId: roleId
});
// 5. 结果处理(不变)
if (response.code === 1) {
ElMessage.success('角色修改成功');
loadTeamMembers();
} else {
ElMessage.error(response.msg || '修改失败');
}
} catch (error) {
// 捕获取消操作(兼容 Element Plus 不同版本的错误格式)
if (error === 'cancel' || error?.name === 'CanceledError' || error?.code === 'CANCEL') {
return;
}
ElMessage.error('修改角色失败');
console.error('修改角色异常:', error);
}
}
// ========== 移除成员 ==========
const handleRemoveMember = async (member) => {
try {
await ElMessageBox.confirm(
`确定要移除成员 ${member.username} 吗?`,
'确认操作',
{ type: 'warning' }
)
const response = await removeMember(member.id)
if (response.code === 1) {
ElMessage.success('成员移除成功')
loadTeamMembers()
} else {
ElMessage.error(response.msg || '移除失败')
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('移除成员失败')
}
}
}
// ========== 工具方法 ==========
const getRoleTagType = (role) => ({
admin: 'danger',
readonly: 'warning',
member: 'success'
}[role] || 'info')
const getRoleText = (role) => ({
admin: '管理员',
readonly: '只读角色',
member: '普通成员'
}[role] || role)
const handlePageChange = () => loadTeamMembers()
// ========== 复制邀请链接(最终修复版) ==========
const copyInviteLink = () => {
// 1. 先验证链接是否存在
if (!inviteLink.value) {
ElMessage.warning('邀请链接尚未生成,请先点击"确认"生成链接')
return
}
try {
// 方案1:优先使用剪贴板 API(现代浏览器 HTTPS 环境)
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(inviteLink.value).then(() => {
ElMessage.success('链接已复制到剪贴板')
}).catch(() => {
// API 失败,自动降级到方案2
fallbackCopy(inviteLink.value)
})
} else {
// 非 HTTPS/本地环境,直接用方案2
fallbackCopy(inviteLink.value)
}
} catch (error) {
// 极端情况,直接提示手动复制
ElMessage.error('复制失败,请手动选中链接复制')
console.error('复制失败:', error)
}
}
// 降级方案:创建临时输入框复制(兼容 HTTP/本地开发/旧浏览器)
const fallbackCopy = (text) => {
// 创建临时 input 框(确保能被选中)
const tempInput = document.createElement('input')
// 关键:设置 input 为可见(部分浏览器对隐藏元素的 select 有限制)
tempInput.style.position = 'absolute'
tempInput.style.left = '-9999px'
tempInput.style.top = '0'
// 绑定值(直接用传入的 text,避免 v-model 绑定延迟)
tempInput.value = text
// 必须添加到 DOM 中才能选中
document.body.appendChild(tempInput)
// 选中所有文本(focus + select 双重保障)
tempInput.focus()
tempInput.select()
// 执行复制命令(兼容性覆盖所有浏览器)
const success = document.execCommand('copy')
// 复制后立即移除临时元素
document.body.removeChild(tempInput)
// 提示结果
if (success) {
ElMessage.success('链接已复制到剪贴板')
} else {
ElMessage.error('复制失败,请手动选中链接复制')
}
}
3. 前端页面
html
<template>
<div class="team-page">
<!-- 页面标题区域 -->
<div class="page-header">
<div class="header-content">
<div class="title-wrapper">
<h1>团队管理</h1>
</div>
<p class="subtitle">管理团队成员和权限分配</p>
</div>
</div>
<!-- 内容区域 -->
<div class="content-wrapper">
<!-- 操作栏 -->
<div class="operate-bar">
<el-button type="primary" @click="inviteDialogVisible = true">
<el-icon><Plus /></el-icon>邀请成员
</el-button>
</div>
<!-- 成员列表 -->
<el-card class="member-list" shadow="never">
<template #header>
<div class="card-header">
<div class="header-left">
<el-icon :size="20"><UserFilled /></el-icon>
<span class="header-title">成员列表</span>
</div>
</div>
</template>
<el-table
:data="teamMembers"
style="width: 100%"
:header-cell-style="{
backgroundColor: '#f5f7fa',
fontSize: '15px',
fontWeight: 600
}"
>
<el-table-column
prop="username"
label="用户名"
min-width="200"
/>
<el-table-column
prop="role"
label="角色"
width="500"
>
<template #default="{ row }">
<el-tag
:type="getRoleTagType(row.role)"
effect="light"
>
{{ getRoleText(row.role) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="joinTime"
label="加入时间"
width="500"
/>
<el-table-column
label="操作"
width="200"
fixed="right"
>
<template #default="{ row }">
<div class="operation-buttons">
<el-tooltip content="修改权限" placement="top">
<el-button type="primary" @click="openRoleDialog(row)" link>
<el-icon :size="20"><Edit /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="移除成员" placement="top">
<el-button
type="danger"
@click="handleRemoveDialog(row)"
link
>
<el-icon :size="20"><Delete /></el-icon>
</el-button>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handlePageChange"
@current-change="handlePageChange"
background
/>
</div>
</el-card>
</div>
<!-- 邀请对话框 -->
<el-dialog
v-model="inviteDialogVisible"
title="邀请成员"
width="500px"
align-center
class="export-dialog"
>
<div class="invite-form">
<div class="section-title">选择邀请方式:</div>
<el-radio-group v-model="inviteMethod" class="invite-method">
<el-radio label="email" border class="method-radio">
<div class="method-content">
<el-icon><Message /></el-icon>
<span>邮件邀请</span>
</div>
</el-radio>
<el-radio label="link" border class="method-radio">
<div class="method-content">
<el-icon><Link /></el-icon>
<span>链接邀请</span>
</div>
</el-radio>
</el-radio-group>
<el-divider content-position="center">角色设置</el-divider>
<div class="role-select">
<div class="section-title">选择角色权限:</div>
<el-select v-model="selectedRole" placeholder="请选择角色" class="role-selector">
<el-option
v-for="option in roleOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
<template v-if="inviteMethod === 'email'">
<div class="email-input">
<div class="section-title">邮箱地址:</div>
<el-input
v-model="emailAddress"
placeholder="请输入邮箱地址"
size="large"
/>
</div>
</template>
<template v-else>
<div class="link-display">
<div class="section-title">邀请链接:</div>
<el-alert
title="请妥善保管邀请链接,链接有效期为24小时"
type="info"
:closable="false"
show-icon
class="link-alert"
/>
<div class="link-box">
<el-input
v-model:value="inviteLink"
readonly
size="large"
:value="inviteLink"
>
<template #append>
<el-button type="button" @click="copyInviteLink">复制</el-button>
</template>
</el-input>
</div>
</div>
</template>
</div>
<template #footer>
<span class="dialog-footer">
<el-button plain @click="inviteDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleInvite">
确认
</el-button>
</span>
</template>
</el-dialog>
<el-dialog
v-model="roleDialogVisible"
title="修改角色"
width="400px" >
<el-form label-width="100px">
<el-form-item label="角色">
<el-select v-model="roleForm.role" placeholder="请选择角色">
<el-option label="管理员" value="admin" />
<el-option label="只读角色" value="readonly" />
<el-option label="普通成员" value="member" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="roleDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitRoleUpdate">
确认
</el-button>
</template>
</el-dialog>
<el-dialog v-model="removeDialogVisible" title="确认移除" width="400px">
<span>确定要移除成员 {{ removeTarget?.username }} 吗?此操作不可恢复。</span>
<template #footer>
<el-button @click="removeDialogVisible = false">取消</el-button>
<el-button type="danger" @click="confirmRemoveMember">移除</el-button>
</template>
</el-dialog>
</div>
</template>
后端
邮箱邀请时先校验用户是否存在,存在则直接加入团队,不存在则返回错误
- 邮箱邀请逻辑分支 :单独处理
email邀请方式,先查询user表是否存在该邮箱; - 用户存在校验 :存在则直接创建
sys_user_team关联记录(加入团队),不存在则返回code=0错误; - 保留原有邀请记录:即使直接加入团队,仍创建邀请记录(状态设为 "已确认"),便于追溯;
- 新增 Mapper 方法:查询邮箱对应的用户 ID、校验用户是否已在团队。
0. 实体类、DTO、VO
SysInvitation
java
package com.sky.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SysInvitation implements Serializable {
private static final long serialVersionUID = 1L;
// 邀请唯一标识
private Integer inviteId;
// 邀请者ID(必须是团队管理员)
private Integer createUser;
// 被邀请人邮箱(用户可能未注册,所以不用user_id)
private String inviteEmail;
// 邀请加入的团队ID
private Integer teamId;
// 角色(1-只读,2-普通,3-管理员)
private Integer roleId;
// 邀请状态:0-待确认,1-已接受,2-已拒绝
private Integer status;
// 邀请链接(用于未注册用户点击注册并加入)
private String inviteLink;
// 邀请发起时间
private LocalDateTime createTime;
// 被邀请人确认时间(接受/拒绝时更新)
private LocalDateTime updateTime;
// 更新操作人(可选)
private Integer updateUser;
}
SysUserTeam
java
package com.sky.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SysUserTeam implements Serializable {
private static final long serialVersionUID = 1L;
// 自增主键
private Integer id;
// 用户id
private Integer userId;
// 团队id
private Integer teamId;
// 角色id
private Integer roleId;
// 用户加入团队时间
private LocalDateTime joinTime;
private String userName;
private String teamName;
private String roleName;
// 状态(0-不可用,1-可用)
private Integer status;
private LocalDateTime updateTime;
private Integer updateUser;
}
Team
java
package com.sky.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Team implements Serializable {
private static final long serialVersionUID = 1L;
private Integer teamId;
private String teamName;
private String teamDesc;
private Integer createUser;
private Integer updateUser;
private LocalDateTime createTime;
private LocalDateTime updateTime;
// 团队状态:0-无效状态,1-有效状态
private Integer status;
}
InviteRequestDTO
java
package com.sky.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InviteRequestDTO implements Serializable {
private static final long serialVersionUID = 1L;
@NotBlank(message = "邀请方式不能为空")
private String inviteMethod; // email/link
@Email(message = "邮箱格式不正确")
private String email;
@NotNull(message = "角色不能为空")
// 后端传过来的数据:admin、readonly、member
private String role;
private Integer teamId; // 从当前用户信息获取
}
UpdateRoleRequestDTO
java
package com.sky.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UpdateRoleRequestDTO implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull(message = "用户ID不能为空")
private Integer userId; // 前端传:成员ID
@NotNull(message = "角色不能为空")
private Integer roleId;
}
TeamMemberVO
java
package com.sky.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TeamMemberVO implements Serializable {
private static final long serialVersionUID = 1L;
@JsonProperty("id")
private Integer userId; // 映射前端的id字段
private String username;
@JsonProperty("role")
private String role; // 角色代码:admin/readonly/member
private String joinTime;
// 获取角色显示名称
public String getRoleName() {
switch (role) {
case "admin": return "管理员";
case "readonly": return "只读用户";
case "member": return "普通成员";
default: return "未知角色";
}
}
}
1. Controller
SysTeamController
java
package com.sky.controller;
import com.sky.dto.InviteRequestDTO;
import com.sky.entity.SysInvitation;
import com.sky.dto.UpdateRoleRequestDTO;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.SysTeamService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* 团队管理
*/
@RestController
@RequestMapping("/team")
@Slf4j
@Api(tags = "团队相关接口")
public class SysTeamController {
@Autowired
private SysTeamService sysTeamService;
/**
* 获取当前用户所在团队成员列表
* @param page
* @param size
* @return
*/
@GetMapping("/members")
@ApiOperation("获取当前用户所在团队成员列表")
public Result<PageResult> getPageTeamMembers(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
log.info("正在分页查询获取当前用户所在的团队成员列表...");
PageResult pageResult = sysTeamService.getPageTeamMembers(page, size);
return Result.success(pageResult);
}
/**
* 邀请成员
* @param request
* @return
*/
@PostMapping("/invite")
@ApiOperation("邀请成员")
public Result<String> inviteMember(@RequestBody @Valid InviteRequestDTO request) {
log.info("用户正在邀请成员,参数为:{}" , request);
return sysTeamService.inviteMember(request);// 直接返回result
}
/**
* 修改成员角色
* @param request
* @return
*/
@PutMapping("/member/role")
@ApiOperation("修改成员角色")
public Result<String> updateMemberRole(@RequestBody @Valid UpdateRoleRequestDTO request) {
log.info("用户修改成员角色,参数为:{}" , request);
sysTeamService.updateMemberRole(request);
return Result.success("角色修改成功");
}
/**
* 移除成员
* @param userTeamId
* @return
*/
@DeleteMapping("/member/{userTeamId}")
@ApiOperation("移除成员")
public Result<String> removeMember(@PathVariable Integer userTeamId) {
log.info("用户移除成员,userTeamId:{}" , userTeamId);
sysTeamService.removeMember(userTeamId);
return Result.success("成员移除成功");
}
/**
* 获取邀请信息
* @param inviteId
* @return
*/
@GetMapping("/invitation/{inviteId}")
@ApiOperation("获取邀请信息")
public Result<SysInvitation> getInvitation(@PathVariable Integer inviteId) {
log.info("正在获取邀请信息,inviteId:{}" , inviteId);
sysTeamService.getInvitationById(inviteId);
return Result.success();
}
/**
* 接受邀请
* @param inviteId
* @return
*/
@PostMapping("/invitation/{inviteId}/accept")
@ApiOperation("接受邀请")
public Result<String> acceptInvitation(@PathVariable Integer inviteId) {
log.info("接受邀请信息,inviteId:{}" , inviteId);
sysTeamService.acceptInvitation(inviteId);
return Result.success("加入团队成功");
}
}
2. Service
SysTeamService
java
package com.sky.service;
import com.sky.dto.InviteRequestDTO;
import com.sky.dto.UpdateRoleRequestDTO;
import com.sky.result.PageResult;
import com.sky.result.Result;
public interface SysTeamService {
/**
* 分页查询当前团队成员
* @param page
* @param size
* @return
*/
PageResult getPageTeamMembers(Integer page, Integer size);
/**
* 邀请成员
* @param request
* @return
*/
Result inviteMember(InviteRequestDTO request);
/**
* 修改成员角色
* @param request
* @return
*/
Result updateMemberRole(UpdateRoleRequestDTO request);
/**
* 移除成员
* @param userTeamId
* @return
*/
Result removeMember(Integer userTeamId);
/**
* 获取邀请信息
* @param inviteId
* @return
*/
void getInvitationById(Integer inviteId);
/**
* 接受邀请
* @param inviteId
* @return
*/
void acceptInvitation(Integer inviteId);
}
SysTeamServiceImpl
java
package com.sky.service.impl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.context.BaseContext;
import com.sky.dto.InviteRequestDTO;
import com.sky.dto.UpdateRoleRequestDTO;
import com.sky.entity.*;
import com.sky.exception.TeamException;
import com.sky.mapper.SysTeamMapper;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.SysTeamService;
import com.sky.vo.TeamMemberVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
@Service
@Slf4j
public class SysTeamServiceImpl implements SysTeamService {
@Autowired
private SysTeamMapper sysTeamMapper;
/**
* 分页查询当期团队成员信息
* @param page
* @param size
* @return
*/
public PageResult getPageTeamMembers(Integer page, Integer size) {
// 获取当前用户ID和团队ID
Integer currentUserId = BaseContext.getCurrentId();
// 修改或创建团队ID
Integer currentTeamId = getOrCreateUserTeam(currentUserId);
// 设置分页参数
PageHelper.startPage(page, size);
// 查询团队成员列表
Page<TeamMemberVO> memberPage = sysTeamMapper.selectTeamMembersByTeamId(currentTeamId);
return new PageResult(memberPage.getTotal(), memberPage.getResult());
}
/**
* 邀请新成员--邮件邀请、链接邀请
* @param request
* @return
*/
@Transactional
public Result inviteMember(InviteRequestDTO request) {
Integer currentUserId = BaseContext.getCurrentId();
Integer currentTeamId = getOrCreateUserTeam(currentUserId);
// 验证当前用户是否有邀请权限
if (!hasInvitePermission(currentUserId, currentTeamId)) {
throw new TeamException("无权限邀请成员");
}
// 2. 新增:角色字符串 → 数字ID 映射转换(核心修复)
Integer roleId = mapRoleToId(request.getRole());
if (roleId == null) {
return Result.error("无效的角色类型");
}
// 2. 根据邀请方式处理
if ("email".equals(request.getInviteMethod())) {
// 2.1 获取前端传入的邮箱
String inviteEmail = request.getEmail();
if (inviteEmail == null || inviteEmail.trim().isEmpty()) {
return Result.error("邮箱不能为空");
}
// 2.2 查找该邮箱对应的用户ID
Integer invitedUserId = sysTeamMapper.getUserIdByEmail(inviteEmail);
if (invitedUserId == null) {
// 邮箱不存在,返回 code=0,前端提示"该用户不存在"
return Result.error("该用户不存在");
}
// 2.3 校验该用户是否已在团队中
Integer isInTeam = sysTeamMapper.countUserInTeam(invitedUserId, currentTeamId);
if (isInTeam != null && isInTeam > 0) {
return Result.error("该用户已在团队中");
}
// 2.4 构建邀请记录(状态设为"已确认",因为直接加入)
SysInvitation invitation = buildInvitation(request, currentUserId, currentTeamId);
invitation.setStatus(1); // 0=待确认,1=已确认(直接加入)
sysTeamMapper.insertInvitation(invitation);
// 2.5 直接将用户加入团队(创建 sys_user_team 关联记录)
SysUserTeam userTeam = SysUserTeam.builder()
.userId(invitedUserId)
.teamId(currentTeamId)
.roleId(roleId) // 前端选择的角色ID
.joinTime(LocalDateTime.now())
.status(1) // 正常状态
.build();
sysTeamMapper.insertUserTeam(userTeam);
// 2.6 可选:发送"已加入团队"通知邮件(而非邀请链接)
//sendJoinTeamNoticeEmail(inviteEmail, currentTeamId);
log.info("通过邮箱邀请用户成功:invitedUserId={}, teamId={}, roleId={}",
invitedUserId, currentTeamId, roleId);
return Result.success("邀请成功,用户已加入团队");
} else if ("link".equals(request.getInviteMethod())) {
// 链接邀请:保留原有逻辑(生成邀请链接,创建待确认记录)
SysInvitation invitation = buildInvitation(request, currentUserId, currentTeamId);
sysTeamMapper.insertInvitation(invitation);
log.info("生成的邀请链接: {}", invitation.getInviteLink());
return Result.success(invitation.getInviteLink());
}
return Result.error("无效的邀请方式");
}
/**
* 更新成员角色
* @param request
* @return
*/
@Transactional
public Result updateMemberRole(UpdateRoleRequestDTO request) {
// 1. 获取当前登录用户的团队ID(当前管理员所在团队)
Integer currentUserId = BaseContext.getCurrentId();
Integer currentTeamId = sysTeamMapper.getTeamIdByUserId(currentUserId);
if (currentTeamId == null) {
return Result.error("当前用户无所属团队");
}
// 2. 自动查询 userTeamId(核心:通过 团队ID + 目标用户ID 定位关联记录)
Integer userTeamId = sysTeamMapper.getUserTeamIdByTeamIdAndUserId(currentTeamId, request.getUserId());
if (userTeamId == null) {
return Result.error("该用户不在当前团队中");
}
// 3. 校验当前用户是否是团队管理员(只有管理员能修改角色)
Integer currentUserRole = sysTeamMapper.selectUserRoleInTeam(currentUserId, currentTeamId);
if (currentUserRole == null || currentUserRole != 3) { // 3=管理员ID
return Result.error("无权限修改角色");
}
// 4. 校验角色ID合法性
List<Integer> validRoleIds = Arrays.asList(1, 2, 3); // 允许的角色ID
if (!validRoleIds.contains(request.getRoleId())) {
return Result.error("无效的角色ID");
}
// 5. 构造 SysUserTeam 实体类(关键修改)
SysUserTeam sysUserTeam = new SysUserTeam();
sysUserTeam.setId(userTeamId); // 设置用户-团队关系ID(主键)
sysUserTeam.setRoleId(request.getRoleId()); // 设置新角色ID
//sysUserTeam.setUserId(BaseContext.getCurrentId());
// 5. 执行修改(更新 sys_user_team 表的 role_id)
int rows = sysTeamMapper.updateUserTeamRole(sysUserTeam);
return rows > 0 ? Result.success("角色修改成功") : Result.error("修改失败");
}
/**
* 从团队中移除成员
* @param userId
* @return
*/
@Transactional
public Result removeMember(Integer userId) {
// 注意:前端传的是 userId,后端自动查 userTeamId
// 1. 获取当前登录用户(管理员)信息
Integer currentUserId = BaseContext.getCurrentId();
Integer currentTeamId = sysTeamMapper.getTeamIdByUserId(currentUserId);
if (currentTeamId == null) {
return Result.error("当前用户无所属团队");
}
// 2. 校验当前用户是否是团队管理员(只有管理员能移除成员)
Integer currentUserRole = sysTeamMapper.selectUserRoleInTeam(currentUserId, currentTeamId);
if (currentUserRole == null || currentUserRole != 3) { // 3=管理员ID
return Result.error("无权限移除成员");
}
// 3. 自动查询 userTeamId(通过 团队ID + 目标用户ID)
Integer userTeamId = sysTeamMapper.getUserTeamIdByTeamIdAndUserId(currentTeamId, userId);
if (userTeamId == null) {
return Result.error("该用户不在当前团队中");
}
// 4. 校验:不能移除自己(管理员不能删除自己)
if (currentUserId.equals(userId)) {
return Result.error("不能移除自己");
}
// 5. 构造 SysUserTeam 实体类(关键修改)
SysUserTeam sysUserTeam = new SysUserTeam();
sysUserTeam.setId(userTeamId);
// 5. 执行删除(移除用户-团队关联记录)
int rows = sysTeamMapper.removeMember(sysUserTeam);
return rows > 0 ? Result.success("成员移除成功") : Result.error("移除失败");
}
@Override
public void getInvitationById(Integer inviteId) {
}
@Override
public void acceptInvitation(Integer inviteId) {
}
// ============ 私有方法 ============
/**
* 验证是否有邀请权限
*/
private boolean hasInvitePermission(Integer userId, Integer teamId) {
// 查询用户在团队中的角色
Integer roleId = sysTeamMapper.selectUserRoleInTeam(userId, teamId);
// 管理员有邀请权限(role_id = 3)
return roleId != null && roleId == 3;
}
/**
* 前端角色字符串 → 后端数字ID 映射(关键方法)
*/
private Integer mapRoleToId(String role) {
if (role == null) {
return 2; // 默认普通成员
}
switch (role.toLowerCase()) {
case "admin":
return 3; // 管理员
case "readonly":
return 1; // 只读角色(根据后端实际ID调整)
case "member":
return 2; // 普通成员(根据后端实际ID调整)
default:
return null; // 无效角色
}
}
/**
* 构建邀请记录
*/
private SysInvitation buildInvitation(InviteRequestDTO request, Integer createUser, Integer teamId) {
return SysInvitation.builder()
.inviteEmail(request.getEmail())
.teamId(teamId)
.roleId(mapRoleToId(request.getRole())) // 使用role_id替代isInviteAdmin
.status(0) // 待确认
.inviteLink(generateInviteLink())
.build();
}
/**
* 生成邀请链接
*/
private String generateInviteLink() {
String token = UUID.randomUUID().toString();
return "https://shield-chain.com/invite/" + token;
}
/**
* 发送邀请邮件
*/
private void sendInviteEmail(String email, String inviteLink) {
// 实际项目中应集成邮件服务
log.info("发送邀请邮件到: {},链接: {}", email, inviteLink);
// 这里可以调用邮件服务发送实际邮件
}
/**
* 验证是否有管理权限
*/
private boolean hasManagePermission(Integer userId) {
// 简化实现,实际应根据具体权限判断
Integer roleId = sysTeamMapper.selectUserRoleByUserId(userId);
return roleId != null && roleId == 3; // 管理员权限
}
/**
* 获取或创建当前用户的团队ID
*/
private Integer getOrCreateUserTeam(Integer userId) {
// 首先尝试获取用户现有的团队ID
Integer teamId = sysTeamMapper.getTeamIdByUserId(userId);
if (teamId == null) {
// 用户没有团队,自动创建新团队
teamId = createNewTeamForUser(userId);
log.info("为用户 {} 自动创建新团队 {}", userId, teamId);
}
return teamId;
}
/**
* 为用户创建新团队
*/
@Transactional
public Integer createNewTeamForUser(Integer userId) {
// 1. 创建团队基本信息
Team newTeam = Team.builder()
.teamName(generateDefaultTeamName(userId))
.createUser(userId)
.createTime(LocalDateTime.now())
.status(1) // 激活状态
.build();
sysTeamMapper.insertTeam(newTeam);
Integer newTeamId = newTeam.getTeamId();
// 调试日志:打印回写后的 teamId(修复后应显示具体数值,如 1、2)
System.out.println("创建团队后回写的 teamId:" + newTeam.getTeamId());
// 强制校验:若仍为 null,直接抛异常,避免后续错误
if (newTeam.getTeamId() == null) {
throw new RuntimeException("团队创建失败!自增 teamId 未回写,请检查 XML 配置和表结构");
}
// 2. 将用户设置为团队管理员
SysUserTeam userTeam = SysUserTeam.builder()
.userId(userId)
.teamId(newTeamId)
.roleId(3) // 管理员角色
.joinTime(LocalDateTime.now())
.status(1)
.build();
sysTeamMapper.insertUserTeam(userTeam);
System.out.println("用户-团队关联插入成功:userId=" + userId + ", teamId=" + newTeam.getTeamId());
return newTeam.getTeamId();
}
/**
* 生成默认团队名称
*/
private String generateDefaultTeamName(Integer userId) {
return "团队_" + userId + "_" + System.currentTimeMillis();
}
}
3. Mapper
SysTeamMapper
java
package com.sky.mapper;
import com.github.pagehelper.Page;
import com.sky.annotation.AutoFill;
import com.sky.entity.SysInvitation;
import com.sky.entity.SysUserTeam;
import com.sky.entity.Team;
import com.sky.enumeration.OperationType;
import com.sky.vo.TeamMemberVO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface SysTeamMapper {
/**
* 根据用户ID查询团队ID
* @param currentUserId
* @return
*/
@Select("select team_id from sys_user_team where user_id = #{currentUserId} ")
Integer getTeamIdByUserId(Integer currentUserId);
/**
* 团队成员分页查询
* @param currentTeamId
* @return
*/
Page<TeamMemberVO> selectTeamMembersByTeamId(Integer currentTeamId);
/**
* 查询用户在团队中的角色
* @param userId
* @param teamId
* @return
*/
@Select("SELECT role_id FROM sys_user_team WHERE user_id = #{userId} AND team_id = #{teamId} AND status = 1")
Integer selectUserRoleInTeam(Integer userId, Integer teamId);
/**
* 插入邀请记录
* @param invitation
*/
@AutoFill(OperationType.INSERT)
void insertInvitation(SysInvitation invitation);
/**
* 根据用户ID查询角色
* @param userId
* @return
*/
@Select("SELECT role_id FROM sys_user_team WHERE user_id = #{userId} AND status = 1")
Integer selectUserRoleByUserId(Integer userId);
//Integer countRoleById(Integer roleId);
/**
* 插入新团队
* @param newTeam
*/
@AutoFill(OperationType.INSERT)
void insertTeam(Team newTeam);
/**
* 插入用户-团队对应关系
* @param userTeam
*/
void insertUserTeam(SysUserTeam userTeam);
/**
* 根据邮箱查找用户id
* @param inviteEmail
* @return
*/
@Select("select id from user where email = #{inviteEmail}")
Integer getUserIdByEmail(String inviteEmail);
/**
* 校验用户是否已经在团队中
* @param invitedUserId
* @param currentTeamId
* @return
*/
@Select("select count(1) from sys_user_team where user_id = #{invitedUserId} and team_id = #{currentTeamId} ")
Integer countUserInTeam(Integer invitedUserId, Integer currentTeamId);
/**
* 通过 团队ID + 用户ID 查询 userTeamId(自动关联,不用前端传)
* @param currentTeamId
* @param userId
* @return
*/
@Select("select id from sys_user_team where team_id = #{currentTeamId} and user_id = #{userId}")
Integer getUserTeamIdByTeamIdAndUserId(Integer currentTeamId, Integer userId);
/**
* 修改成员角色
* @param sysUserTeam
* @return
*/
@AutoFill(OperationType.UPDATE)
@Update("update sys_user_team set role_id = #{roleId}, update_time = #{updateTime}, update_user = #{updateUser} " +
"where id = #{id}")
int updateUserTeamRole(SysUserTeam sysUserTeam);
/**
* 从当前团队移除用户(status = 0)
* @param sysUserTeam
* @return
*/
@AutoFill(OperationType.UPDATE)
@Update("update sys_user_team set status = 0, update_time = #{updateTime}, update_user = #{updateUser} where id = #{id}")
int removeMember(SysUserTeam sysUserTeam);
}
java
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SysTeamMapper">
<select id="selectTeamMembersByTeamId" resultType="com.sky.vo.TeamMemberVO">
SELECT
u.id as userId,
u.username as username,
CASE
WHEN ut.role_id = 1 THEN 'readonly'
WHEN ut.role_id = 2 THEN 'member'
WHEN ut.role_id = 3 THEN 'admin'
ELSE 'unknown'
END as role,
DATE_FORMAT(ut.join_time, '%Y-%m-%d %H:%i:%s') as joinTime
FROM sys_user_team ut
INNER JOIN user u ON ut.user_id = u.id
WHERE ut.team_id = #{currentTeamId}
AND ut.status = 1
ORDER BY ut.join_time DESC
</select>
<!-- 动态插入邀请记录:只插入非空字段 -->
<insert id="insertInvitation" parameterType="com.sky.entity.SysInvitation"
useGeneratedKeys="true" keyProperty="inviteId" keyColumn="invite_id">
INSERT INTO sys_invitation
<trim prefix="(" suffix=")" suffixOverrides=",">
<!-- 主键自增,无需手动插入(useGeneratedKeys 自动返回主键) -->
<if test="createUser != null">create_user,</if>
<if test="inviteEmail != null and inviteEmail != ''">invite_email,</if>
<if test="teamId != null">team_id,</if>
<if test="roleId != null">role_id,</if>
<if test="status != null">status,</if>
<if test="inviteLink != null and inviteLink != ''">invite_link,</if>
<if test="createTime != null">create_time,</if>
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<if test="createUser != null">#{createUser},</if>
<if test="inviteEmail != null and inviteEmail != ''">#{inviteEmail},</if>
<if test="teamId != null">#{teamId},</if>
<if test="roleId != null">#{roleId},</if>
<if test="status != null">#{status},</if>
<if test="inviteLink != null and inviteLink != ''">#{inviteLink},</if>
<if test="createTime != null">#{createTime},</if>
</trim>
</insert>
<insert id="insertUserTeam" parameterType="com.sky.entity.SysUserTeam"
useGeneratedKeys="true" keyProperty="id" keyColumn="id">
INSERT INTO sys_user_team
(
user_id,
team_id,
role_id,
join_time,
status,
user_name,
team_name,
role_name
)
VALUES (
#{userId},
#{teamId},
#{roleId},
#{joinTime},
#{status},
-- 1. 子查询获取用户名(给 user 表加反引号,避免关键字冲突)
(SELECT COALESCE(username, '') FROM `user` WHERE id = #{userId}),
-- 2. 子查询获取团队名(处理 teamId 为 null 的情况,返回空字符串)
(SELECT COALESCE(team_name, '') FROM sys_team WHERE team_id = #{teamId} LIMIT 1),
-- 3. 子查询获取角色名
(SELECT COALESCE(role_name, '') FROM sys_role WHERE role_id = #{roleId} LIMIT 1)
)
</insert>
<!-- 修复 insertTeam:显式指定字段,确保自增 teamId 回写 -->
<insert id="insertTeam" parameterType="com.sky.entity.Team"
useGeneratedKeys="true" keyProperty="teamId" keyColumn="team_id">
INSERT INTO sys_team (team_name, create_user, create_time, status)
VALUES (#{teamName}, #{createUser}, #{createTime}, #{status})
</insert>
</mapper>
测试效果



邀请成员:
第一次邀请时自动为该用户创建一个团队
邮箱邀请-- 只要输入正确的邮箱就可以直接将该成员加入团队
链接邀请:点击确定,后端返回一个测试邀请链接


实际权限控制
团队功能的权限控制核心是 「基于角色的访问控制(RBAC)」 ------ 已经在功能中设计了 admin(管理员)、member(普通成员)、readonly(只读成员)三种角色,现在需要通过「后端接口校验」+「前端 UI 适配」实现全链路权限控制,确保不同角色只能操作对应权限的功能。
权限控制包括资源权限和数据权限
资源权限:一般用RBAC

第一步:修改后端核心代码(UserServiceImpl + 新增依赖组件)
- 新建实体sysrole
java
package com.sky.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SysRole implements Serializable {
private static final long serialVersionUID = 1L;
private Integer roleId;
private String roleName;
private String roleDesc;
private String permCodes; // 数据库中存储的JSON字符串,如["SOFTWARE_VIEW",...]
}
- 修改UserLoginVO
java
// com.sky.vo.UserLoginVO.java
import lombok.Data;
import java.util.List;
@Data
public class UserLoginVO {
private Integer id; // 用户ID
private String username; // 用户名
private String did; // 用户DID
private Integer status; // 账号状态
private List<String> permCodes; // 权限码集合(核心:前端用于控制UI)
}
- 修改UserService
java
// com.sky.service.UserService.java
import com.sky.dto.UserLoginDTO;
import com.sky.dto.UserRegisterDTO;
import com.sky.vo.UserLoginVO;
public interface UserService {
// 登录方法返回类型改为 UserLoginVO
UserLoginVO login(UserLoginDTO userLoginDTO);
boolean register(UserRegisterDTO userRegisterDTO);
}
- serviceImpl
java
import com.alibaba.fastjson.JSON;
import com.sky.entity.SysRole;
import com.sky.mapper.SysRoleMapper;
import com.sky.mapper.SysTeamMapper;
import com.sky.vo.UserLoginVO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@Slf4j
public class UserServiceImpl implements UserService {
// ... 原有常量和注入保持不变 ...
// 新增注入:团队Mapper和角色Mapper
@Autowired
private SysTeamMapper sysTeamMapper;
@Autowired
private SysRoleMapper sysRoleMapper;
/**
* 用户登录(核心修改:返回UserLoginVO + 权限码)
*/
public UserLoginVO login(UserLoginDTO userLoginDTO) {
String username = userLoginDTO.getUsername();
String password = userLoginDTO.getPassword();
// 1. 根据用户名查询数据库(原有逻辑不变)
User user = userMapper.getByUsername(username);
if (user == null) {
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
if (!password.equals(user.getPassword())) { // 后期需MD5加密
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}
if (user.getStatus() == StatusConstant.DISABLE) {
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}
// 2. 查询用户的权限码集合(新增核心逻辑)
List<String> permCodes = getPermCodesByUserId(user.getId());
// 3. 封装UserLoginVO(屏蔽敏感字段,只返回前端需要的信息)
UserLoginVO loginVO = new UserLoginVO();
BeanUtils.copyProperties(user, loginVO); // 拷贝id、username、did、status等字段
loginVO.setPermCodes(permCodes); // 设置权限码集合
// 4. 返回VO对象
return loginVO;
}
/**
* 新增:根据用户ID查询权限码集合(用户→团队→角色→权限)
*/
private List<String> getPermCodesByUserId(Integer userId) {
try {
// 步骤1:查询用户所属团队ID(无团队则返回空权限)
Integer teamId = sysTeamMapper.getTeamIdByUserId(userId);
if (teamId == null) {
log.warn("用户{}暂无所属团队,仅返回基础查看权限", userId);
return List.of("SOFTWARE_VIEW", "DID_VIEW", "SBOM_VIEW", "VULN_VIEW"); // 默认只读权限
}
// 步骤2:查询用户在该团队的角色ID
Integer roleId = sysTeamMapper.getRoleIdByUserIdAndTeamId(userId, teamId);
if (roleId == null) {
log.warn("用户{}在团队{}无角色,仅返回基础查看权限", userId, teamId);
return List.of("SOFTWARE_VIEW", "DID_VIEW", "SBOM_VIEW", "VULN_VIEW");
}
// 步骤3:查询角色对应的perm_codes(JSON字符串转List)
SysRole role = sysRoleMapper.selectById(roleId);
if (role == null || role.getPermCodes() == null) {
log.warn("角色{}未配置权限码", roleId);
return new ArrayList<>();
}
// 步骤4:JSON字符串解析为List<String>(匹配数据库存储格式)
return JSON.parseArray(role.getPermCodes(), String.class);
} catch (Exception e) {
log.error("查询用户{}权限码失败", userId, e);
return new ArrayList<>(); // 异常时返回空权限,避免登录失败
}
}
// ... 注册方法(register)保持不变 ...
}
第二步:后端权限拦截(接口级校验)
通过「自定义注解 + AOP」拦截接口,确保无权限用户无法调用敏感接口(如邀请成员、上传软件)。
- 自定义新注解
java
// com.sky.annotation.RequiresPerm.java
import java.lang.annotation.*;
@Target({ElementType.METHOD}) // 仅用于接口方法
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequiresPerm {
String value(); // 传入需要的权限码(如"TEAM_INVITE")
}
- 新增权限拦截切面
java
// com.sky.aspect.PermAspect.java
import com.sky.exception.NoPermissionException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Aspect
@Component
@Order(1) // 确保权限校验优先执行
public class PermAspect {
// 切入点:拦截所有带@RequiresPerm注解的方法
@Pointcut("@annotation(com.sky.annotation.RequiresPerm)")
public void permPointcut() {}
@Around("permPointcut() && @annotation(requiresPerm)")
public Object checkPerm(ProceedingJoinPoint joinPoint, com.sky.annotation.RequiresPerm requiresPerm) throws Throwable {
// 1. 获取当前登录用户的权限码(从SecurityContext,需配合Spring Security)
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new NoPermissionException("用户未登录");
}
// 2. 从用户信息中获取权限码集合(这里假设登录时已将permCodes存入Authentication)
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
List<String> userPerms = Arrays.asList(userDetails.getAuthorities().stream()
.map(grantedAuthority -> grantedAuthority.getAuthority())
.toArray(String[]::new));
// 3. 校验是否拥有注解指定的权限码
String requiredPerm = requiresPerm.value();
if (!userPerms.contains(requiredPerm)) {
throw new NoPermissionException("无权限执行该操作");
}
// 4. 有权限,执行原接口方法
return joinPoint.proceed();
}
}
- 接口添加权限注释
给需要权限控制的接口添加 @RequiresPerm,绑定对应的权限码:
java
// 团队管理接口(示例)
@RestController
@RequestMapping("/team")
public class TeamController {
// 邀请成员:仅管理员有TEAM_INVITE权限
@PostMapping("/invite")
@RequiresPerm("TEAM_INVITE")
public Result inviteMember(@RequestBody InviteDTO inviteDTO) {
return teamService.inviteMember(inviteDTO);
}
// 移除成员:仅管理员有TEAM_REMOVE权限
@DeleteMapping("/member/{userId}")
@RequiresPerm("TEAM_REMOVE")
public Result removeMember(@PathVariable Integer userId) {
return teamService.removeMember(userId);
}
// 上传软件包:普通用户/管理员有SOFTWARE_UPLOAD权限
@PostMapping("/software/upload")
@RequiresPerm("SOFTWARE_UPLOAD")
public Result uploadSoftware(@RequestParam MultipartFile file) {
return softwareService.upload(file);
}
// 查看软件列表:所有角色有SOFTWARE_VIEW权限
@GetMapping("/software/list")
@RequiresPerm("SOFTWARE_VIEW")
public Result<PageResult> listSoftware() {
return softwareService.getPageList();
}
}
- 新增无权限异常类(NoPermissionException)
java
// com.sky.exception.NoPermissionException.java
import com.sky.constant.MessageConstant;
public class NoPermissionException extends RuntimeException {
public NoPermissionException(String message) {
super(message);
}
}
- 全局异常处理
java
// com.sky.exception.GlobalExceptionHandler.java
import com.sky.result.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
// 无权限异常处理
@ExceptionHandler(NoPermissionException.class)
public Result handleNoPermission(NoPermissionException e) {
return Result.error(e.getMessage()); // 返回code=0 + 错误信息
}
// ... 其他异常处理(如账号不存在、密码错误)保持不变 ...
}
第三步:前端权限控制(UI 适配)
前端核心是「登录时存储权限码 + 根据权限码显示 / 隐藏功能按钮 + 路由拦截」。
1.登录时存储权限码
javascript
// src/api/user.js(登录接口)
export const login = (userLoginDTO) => {
return request({
url: '/user/login',
method: 'POST',
data: userLoginDTO
});
};
// 登录页面逻辑(src/views/login/Login.vue)
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { login } from '@/api/user';
import { ElMessage } from 'element-plus';
const router = useRouter();
const form = ref({ username: '', password: '' });
const handleLogin = async () => {
try {
const res = await login(form.value);
if (res.code === 1) {
const { id, username, permCodes } = res.data;
// 存储用户信息和权限码到本地存储(或Pinia)
localStorage.setItem('userId', id);
localStorage.setItem('username', username);
localStorage.setItem('permCodes', JSON.stringify(permCodes)); // 关键:存储权限码
ElMessage.success('登录成功');
router.push('/home');
}
} catch (err) {
ElMessage.error(err.message || '登录失败');
}
};
- 封装权限工具函数
java
// src/utils/permission.js
/**
* 判断用户是否拥有指定权限码
* @param {string} perm - 权限码(如"TEAM_INVITE")
* @returns {boolean}
*/
export const hasPerm = (perm) => {
const permCodes = JSON.parse(localStorage.getItem('permCodes') || '[]');
return permCodes.includes(perm);
};
/**
* 判断用户是否拥有任意一个权限码(适用于多权限兼容场景)
* @param {string[]} perms - 权限码数组(如["SOFTWARE_VIEW", "SOFTWARE_FULL_VIEW"])
* @returns {boolean}
*/
export const hasAnyPerm = (perms) => {
const permCodes = JSON.parse(localStorage.getItem('permCodes') || '[]');
return perms.some(perm => permCodes.includes(perm));
};
- 组件中控制 UI 显示 / 隐藏
根据权限码显示按钮,完全匹配你的角色权限设计:
java
<!-- 团队管理页面示例 -->
<template>
<!-- 邀请成员按钮:仅管理员有TEAM_INVITE权限 -->
<el-button
type="primary"
@click="showInviteDialog = true"
v-if="hasPerm('TEAM_INVITE')"
style="margin-bottom: 20px;"
>
邀请成员
</el-button>
<!-- 软件上传按钮:普通用户/管理员有SOFTWARE_UPLOAD权限 -->
<el-button
type="success"
@click="handleUpload"
v-if="hasPerm('SOFTWARE_UPLOAD')"
>
上传软件包
</el-button>
<!-- 成员操作列:修改权限/移除成员(仅管理员可见) -->
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button
type="text"
@click="handleEditRole(row)"
v-if="hasPerm('TEAM_REMOVE')"
>
修改权限
</el-button>
<el-button
type="text"
color="red"
@click="handleRemoveMember(row)"
v-if="hasPerm('TEAM_REMOVE')"
>
移除成员
</el-button>
</template>
</el-table-column>
</template>
<script setup>
import { hasPerm } from '@/utils/permission';
// ... 其他逻辑(如showInviteDialog、handleUpload等)
</script>
- 路由权限拦截
避免用户直接通过 URL 访问无权限页面:
java
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import { hasPerm } from '@/utils/permission';
import { ElMessage } from 'element-plus';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/team/invite',
component: () => import('@/views/team/InviteMember.vue'),
meta: { requiredPerm: 'TEAM_INVITE' } // 需要的权限码
},
{
path: '/software/upload',
component: () => import('@/views/software/Upload.vue'),
meta: { requiredPerm: 'SOFTWARE_UPLOAD' }
}
]
});
// 路由守卫:进入页面前置校验
router.beforeEach((to, from, next) => {
const requiredPerm = to.meta.requiredPerm;
if (requiredPerm) { // 该页面需要权限
if (hasPerm(requiredPerm)) {
next(); // 有权限,放行
} else {
ElMessage.warning('无权限访问该页面');
next(from.path); // 无权限,跳回原页面
}
} else {
next(); // 无需权限,放行
}
});
export default router;