DevOps平台 — 第六篇:成员管理与工作台

成员管理不是一个独立的用户体系,而是站在用户管理和企业管理的肩膀上,解决「谁属于当前企业、在当前企业里做什么」的问题。工作台则是平台的入口面板------让用户一登录就能看到「我的项目、我的待办、我的预警」,而不是面对一个空白的首页。本篇记录成员管理的设计思路(如何复用底座而非重复造轮子)和工作台的分层架构(当前全部为 Mock 占位数据、未来如何逐步替换)。

一、成员管理:基于底座的「加法」而非「重建」

1.1 为什么成员管理不等于用户管理

平台的用户管理设计为全局视角------管理员能看到所有用户,不区分企业。但多企业隔离要求企业 A 的管理员不应该看到企业 B 的成员,也不应该把企业 B 的人拉进自己的项目。

如果直接在用户管理上加企业过滤,会导致全局管理员和企业管理员看到的数据范围混淆。因此,平台选择了一条更清晰的路:保留全局用户管理,新增一套企业维度的成员管理接口,复用同一张用户表,但在查询时强制按企业隔离过滤。

markdown 复制代码
成员管理 与 用户管理 的关系

  全局用户管理(用户列表查询)
    ├─ 视角:平台管理员
    ├─ 数据范围:全部用户
    └─ 不受企业隔离约束

  企业成员管理(当前企业成员查询)
    ├─ 视角:企业管理员
    ├─ 数据范围:仅当前企业的成员
    ├─ 复用同一张 sys_user 表
    └─ 强制 @CompanyIsolated 隔离

  底座共享
    ├─ sys_user              用户主表(账号、密码、状态)
    ├─ sys_user_company      用户-企业关联(多对多)
    ├─ sys_user_dept         用户-部门关联
    ├─ sys_user_team         用户-团队关联
    ├─ sys_user_position     用户-职位关联
    └─ sys_user_role         用户-角色关联

功能截图:

1.2 企业隔离的实现机制

成员管理的所有接口都标注了 @CompanyIsolated 注解。这个注解的作用是:拦截器检查请求头中的 X-Company-Code 与 JWT 中存储的企业编码是否一致,一致则通过 CompanyScope.enable() 开启当前请求的企业隔离。

开启后,SysUserServiceImpl.selectPage() 在执行查询时会做以下处理:

scss 复制代码
企业隔离查询流程

  1. 检查 CompanyScope.isEnabled()
       ├─ 否 → 不按企业过滤(全局用户管理走这条路)
       └─ 是 → 读取 LoginHelper.getCompanyId()
              ├─ 为 null → 返回空列表(未选择企业)
              └─ 有值 → 查询 sys_user_company 获取该企业关联的 userIds
                        └─ wrapper.in(SysUser::getId, compUserIds)

这意味着成员管理的列表查询本质上是一个「带关联表过滤的分页查询」------先查 sys_user_company 拿到当前企业的用户 ID 集合,再用这个集合作为 IN 条件查 sys_user 主表。

1.3 批量填充关联信息

成员列表不只展示用户名,还需要展示该用户的企业/部门、团队、职位、角色、锁定状态等信息。如果逐条查询会产生严重的 N+1 问题,因此采用批量填充策略:

scss 复制代码
批量填充流程(单次分页查询后)

  查询 sys_user 分页结果(10 条)
    │
    ├─ fillRoles()       批量查 sys_user_role → sys_role,填充角色列表
    ├─ fillPositions()   批量查 sys_user_position → sys_position,填充职位列表
    ├─ fillCompanies()   批量查 sys_user_company → sys_company + sys_user_dept → sys_dept
    │                    填充企业-部门关联(按企业分组)
    ├─ fillTeams()       批量查 sys_user_team → sys_team,填充团队列表
    └─ fillLocks()       批量查 sys_user_lock,填充锁定状态

  总 SQL 次数:约 8-10 次(固定),与分页大小无关

功能截图:

1.4 添加成员:从已有账号「拉人」而非「注册」

成员管理的添加逻辑不是「新建用户」,而是「从平台已有账号中拉人加入当前企业」。这个设计基于一个前提:平台是内部系统,用户通常已经在其他企业注册过账号了。

scss 复制代码
添加成员流程

  前端打开「添加成员」抽屉
    │
    ├─ 调用「可挑选成员」接口
    │    └─ selectPickablePage():不按企业过滤,但标记 inCurrentCompany 字段
    │       └─ 已在当前企业的成员:勾选框禁用且默认选中
    │       └─ 不在当前企业的成员:可勾选
    │
    ├─ 用户勾选要添加的成员(已在企业的不可取消)
    │
    └─ 提交 → 「加入当前企业」接口
         └─ addUsersToCurrentCompany()
              ├─ 校验每个 userId 是否存在
              └─ ensureCurrentCompanyRelation()
                   ├─ 检查 sys_user_company 是否已存在关联
                   ├─ 已存在 → 跳过
                   └─ 不存在 → 写入关联记录(isDefault = false)

关键设计点: selectPickablePage 故意不按当前企业过滤------因为它需要展示全部用户,让企业管理员看到「谁还没加入」。但通过 fillInCurrentCompany() 方法为每个用户标记 inCurrentCompany 字段,前端据此禁用已加入成员的勾选框。

功能截图:

1.5 移出成员:解关联不删账号

移出成员的核心原则是只解除关联,不删除账号。用户可能属于多个企业,从企业 A 移出不应影响其在企业 B 的数据。

scss 复制代码
移出成员流程

  removeUsersFromCurrentCompany(userIds)
    │
    └─ 遍历 userIds
         └─ removeCompanyRelation(userId, companyId)
              ├─ 1. 删除 sys_user_company 关联记录
              ├─ 2. 查询该企业下的所有部门 IDs
              │    └─ 删除 sys_user_dept 中该用户 + 这些部门的关联
              ├─ 3. 查询这些部门下的所有团队 IDs
              │    └─ 删除 sys_user_team 中该用户 + 这些团队的关联
              └─ 全程事务保护 @Transactional

级联清理的必要性: 用户从企业移出后,其在该企业下的部门和团队关联就失去了意义。如果不清理,会导致「用户不属于企业但仍属于该企业下的团队」这种数据不一致。

1.6 团队管理:成员管理的子模块

团队(SysTeam)挂在部门下,一个部门可以有多个团队。团队管理是成员管理的侧栏功能,支持:

scss 复制代码
团队管理功能树

  团队 CRUD
    ├─ 新增团队(指定所属部门)
    ├─ 修改团队信息
    ├─ 删除团队
    └─ 查询团队列表(按当前企业)

  团队成员管理
    ├─ 查询团队成员(企业隔离 + 团队过滤)
    ├─ 批量添加团队成员
    │    └─ 校验:用户必须属于当前企业才能加入团队
    ├─ 批量移出团队成员
    └─ 同步团队成员(增量模式)
         ├─ 对比勾选集合 vs 已有集合
         ├─ 新增勾选 → addTeamMembers()
         └─ 取消勾选 → removeTeamMembers()

团队管理在添加成员时也有一个校验:assertUsersInCurrentCompany() 确保被添加的用户已经属于当前企业。这是防止「把不属于企业的人拉进企业下的团队」的安全兜底。

功能截图:

1.7 成员管理的操作能力

成员管理复用了用户管理的全部操作接口,但通过 /current-company/* 路径提供企业隔离版本:

  • 启用/禁用:禁用时需填写原因,支持操作日志 Diff
  • 锁定/解锁:手动锁定,与密码策略的自动锁定互不冲突
  • 重置密码:由企业管理员代为重置,触发强制改密
  • 编辑成员:修改职位、团队等企业维度信息
  • 移出企业:解除关联,不删除账号

所有操作均通过 @LogRecord 注解记录操作日志,通过 @CompanyIsolated 确保企业隔离。

二、工作台:平台入口的数据编排

2.1 工作台的定位

工作台是用户登录后看到的第一屏。它的设计目标不是「功能大全」,而是让用户 3 秒内知道「我现在要做什么」。因此,工作台的内容按优先级从上到下排列:

scss 复制代码
工作台布局(自上而下)

  ┌─────────────────────────────────────────┐
  │  问候区                   │
  │  「上午好,张明 👋  当前租户:XX企业」      │
  ├──────────────────┬──────────────────────┤
  │  个人数据概览     │  企业数据概览          │
  │  (今日/本周/本月)  │  (今日/本周/本月)      │
  ├──────────────────┴──────────────────────┤
  │  我的项目(卡片网格)                     │
  │  ┌────┐ ┌────┐ ┌────┐                   │
  │  │项目A│ │项目B│ │项目C│                   │
  │  └────┘ └────┘ └────┘                   │
  ├─────────────────────────────────────────┤
  │  我的工作项(选项卡表格)                  │
  │  全部 | 我负责的 | 我协作的 | 我创建的      │
  ├──────────────────┬──────────────────────┤
  │  我的待办        │  预警中心              │
  │  (审批/任务)      │  (项目延期/缺陷超时)    │
  └──────────────────┴──────────────────────┘

2.2 数据来源分层

工作台当前的数据全部为 Mock 占位数据,按区域分为以下几类:

区域 数据来源 说明
问候区 Mock 从前端 Store 读取用户信息和企业信息
个人数据概览 Mock 工作项统计待工作项模块开发后替换
企业数据概览 Mock 同上
我的项目 Mock 项目数据待项目模块完善后替换
项目卡片统计 Mock 成员数、进行中/已完成/逾期数前端推算
我的工作项 Mock 前端硬编码数据,待工作项模块开发后替换
我的待办 Mock 前端硬编码数据,待流程中心开发后替换
预警中心 Mock 前端硬编码数据,待预警引擎开发后替换

注意: 工作台当前所有数据均为 Mock 占位,包括项目卡片中的项目信息、统计数字(成员数、进行中工作项数等)全部是前端推算或硬编码的 Mock 值。后续各业务模块开发完成后逐步替换为真实数据。

2.3 我的项目:Mock 数据链路

「我的项目」区域当前同样使用 Mock 数据,其数据链路如下:

scss 复制代码
我的项目 Mock 数据链路

  前端 workbench/index.vue
    │
    ├─ onMounted() → loadProjects()
    │    └─ getMyProjects() → 工作台「我的项目」接口
    │         └─ 当前返回 Mock 项目数据
    │
  后端工作台接口
    │
    ├─ @CompanyIsolated(企业隔离)
    ├─ 获取当前用户 ID
    └─ 项目服务查询用户参与的项目
         │
         └─ 当前阶段:返回 Mock 项目卡片数据
              包含:项目名称、状态、进度、我在项目中的身份、
                   部门名称、项目经理姓名、创建时间
              → 后续项目模块完善后替换为真实查询

核心设计: 工作台「我的项目」接口已按未来真实查询的契约设计------通过项目成员关联表,以当前用户 ID 为条件过滤,只返回该用户参与的项目,同时联查部门表和项目经理信息。当前阶段返回 Mock 数据,后续项目模块完善后直接替换为真实查询即可。

2.4 Mock 数据的分层策略

工作台的 Mock 数据不是随意堆砌的,而是按未来业务模块的边界分层放置:

markdown 复制代码
Mock 数据分层与未来替换计划

  第一层:前端 Mock(硬编码在组件中)
    ├─ WorkbenchStats.vue     个人/企业数据概览
    │                         → 待工作项统计接口替换
    ├─ MyWorkItems.vue        我的工作项列表
    │                         → 待工作项列表接口替换
    ├─ WorkbenchTodo.vue      我的待办
    │                         → 待流程中心待办接口替换
    └─ WorkbenchAlert.vue     预警中心
                              → 待预警引擎接口替换

  第二层:后端 Mock(Controller 返回硬编码数据)
    ├─ 待办聚合接口             待办聚合
    ├─ 预警聚合接口             预警聚合
    ├─ 全局数据接口             全局数据速览
    ├─ 待办列表接口             待办列表
    └─ 预警列表接口             预警列表
         → 后续各业务模块开发完成后,替换为真实查询

  第三层:前端推算(基于 Mock 项目数据派生)
    └─ WorkbenchProjects.vue  项目卡片统计
         ├─ 成员数:随机推算
         ├─ 进行中:基于进度推算
         ├─ 已完成:基于进度推算
         └─ 逾期数:基于状态推算
              → 待项目模块完善后,改为项目统计接口返回

为什么后端也要放 Mock? 因为前端的 Mock 数据无法被多个客户端共享(比如移动端也需要工作台)。后端 Mock 数据统一了接口契约,前端只需要调用接口,未来替换时只需改后端实现,前端无需调整。

2.5 后端 Mock 的接口设计

后端的 Mock 接口并非随意返回数据,而是按照未来接口的契约设计 VO 结构:

复制代码
待办聚合 VO(TodoSummaryVO)
  ├─ pendingFirstReview    待初审数
  ├─ pendingReview         待评审数
  ├─ pendingApproval       待审批数
  ├─ pendingTask           待处理任务数
  └─ pendingRegression     待回归数

预警聚合 VO(AlertSummaryVO)
  ├─ projectDelay          项目延期数
  ├─ defectTimeout         缺陷超时数
  └─ highRisk              高风险事项数

全局数据 VO(DashboardStatVO)
  ├─ requirementThroughput  本月需求吞吐量
  ├─ throughputTrend        吞吐量环比
  ├─ deliveryRate           项目交付率
  ├─ deliveryTrend          交付率环比
  ├─ defectRate             缺陷率
  ├─ defectTrend            缺陷率环比
  ├─ delayRate              延期率
  └─ delayTrend             延期率环比

这些 VO 的字段设计已经对齐了未来业务模块的数据模型。当需求管理、缺陷管理、项目迭代管理等模块开发完成后,只需将 Mock 返回替换为真实查询,前端无需任何改动。

功能截图:

2.6 企业隔离在工作台的体现

DashboardController 标注了类级别的 @CompanyIsolated,这意味着工作台的所有接口都要求用户已选择企业。如果用户未选择企业就访问工作台:

scss 复制代码
企业隔离校验流程

  请求到达 DashboardController
    │
    ├─ 拦截器检查 X-Company-Code 请求头
    │    ├─ 与 JWT 中的 companyCode 不一致 → 403
    │    └─ 一致 → CompanyScope.enable()
    │
    ├─ myProjects() 方法执行
    │    └─ LoginHelper.getUserId() 获取用户 ID
    │         └─ 如果未登录 → 返回空列表
    │
    └─ 项目查询通过 biz_project_member 关联
         └─ 项目本身也有企业隔离(创建时绑定了企业)

2.7 问候区:用户感知的第一触点

问候区虽然简单,但它是用户感知最直接的区域。展示逻辑如下:

  • 问候语:根据当前时间动态生成(上午好/下午好/晚上好)
  • 用户名 :从 Pinia Store 读取 userInfo.nickname,优先昵称
  • 当前租户 :从 Store 读取 currentCompany.companyName
  • 日期:实时计算的中文格式日期

这部分数据全部来自前端 Store,不产生额外网络请求,确保首屏加载速度。

三、成员管理与工作台的协作关系

3.1 成员是项目参与的前提

工作台「我的项目」展示的是当前用户参与的项目。而用户能参与项目的前提是------他必须先被添加为当前企业的成员。这形成了一条数据链路:

markdown 复制代码
数据依赖链路

  企业管理员添加成员
    └─ sys_user_company 写入关联
         └─ 用户成为当前企业成员
              └─ 项目管理员添加项目成员
                   └─ biz_project_member 写入关联
                        └─ 用户出现在该项目中
                             └─ 工作台「我的项目」展示该项目

如果用户没有被添加为企业成员,即使他在平台有账号,也不会出现在项目成员的候选名单中(项目成员管理也有企业隔离校验)。

3.2 核心实现伪代码 --- 成员列表查询

scss 复制代码
// 成员管理列表查询(企业隔离)
function selectPage(条件, 分页参数) {
    wrapper = 构建查询条件

    // 关键词搜索(用户名 OR 姓名)
    applyKeyword(wrapper, 条件)

    // 企业隔离过滤
    if (企业隔离已开启) {
        companyId = 获取当前企业ID()
        if (companyId 为空) 返回空列表

        // 先查关联表获取该企业的用户 ID 集合
        userIds = 查询用户企业关联表(企业ID).map(取userId)
        if (userIds 为空) 返回空列表
        wrapper.in(用户ID, userIds)
    }

    // 分页查询主表
    result = 分页查询(用户表, wrapper, 分页参数)

    // 批量填充关联信息(避免 N+1)
    ids = result.记录列表.map(取ID)
    批量填充角色(记录列表, ids)
    批量填充职位(记录列表, ids)
    批量填充企业部门(记录列表, ids)
    批量填充团队(记录列表, ids)
    批量填充锁定状态(记录列表, ids)

    返回 分页结果(记录列表, 总数)
}

3.3 核心实现伪代码 --- 添加已有账号到当前企业

scss 复制代码
// 从已有账号添加成员到当前企业
@Transactional
function addUsersToCurrentCompany(用户ID列表) {
    // 前置校验:企业隔离必须开启
    requireCompanyScope()
    if (列表为空) 抛出异常("请选择要添加的成员")

    for (userId in 用户ID列表) {
        if (userId 为空) continue

        // 校验用户存在
        if (根据ID查用户(userId) 为空) 抛出异常("用户不存在")

        // 写入关联(已存在则跳过)
        ensureCurrentCompanyRelation(userId)
    }
}

// 确保用户与当前企业的关联记录存在
function ensureCurrentCompanyRelation(userId) {
    if (企业隔离未开启) return

    companyId = 获取当前企业ID()
    if (companyId 为空) 抛出异常("未选择企业")

    // 查询是否已存在关联
    exists = 查询关联表数量(userId, companyId) > 0

    if (!exists) {
        rel = 新建关联记录()
        rel.userId = userId
        rel.companyId = companyId
        rel.isDefault = false
        rel.createTime = 当前时间()
        插入关联表(rel)
    }
}

3.4 核心实现伪代码 --- 移出企业(级联清理)

scss 复制代码
// 移出企业:解除关联 + 级联清理部门/团队关系
@Transactional
function removeCompanyRelation(userId, companyId) {
    // 校验用户和企业存在
    if (根据ID查用户(userId) 为空) 抛出异常("用户不存在")
    if (根据ID查企业(companyId) 为空) 抛出异常("企业不存在")

    // 校验关联存在
    if (查询关联表数量(userId, companyId) == 0)
        抛出异常("该用户不属于该企业")

    // 1. 删除用户-企业关联
    删除关联表(userId, companyId)

    // 2. 查询该企业下的所有部门 IDs
    deptIds = 查询部门表(企业ID).map(取ID)

    if (deptIds 非空) {
        // 删除用户在这些部门下的关联
        删除用户部门关联(userId, deptIds)

        // 3. 查询这些部门下的所有团队 IDs
        teamIds = 查询团队表(deptIds).map(取ID)

        if (teamIds 非空) {
            // 删除用户在这些团队下的关联
            删除用户团队关联(userId, teamIds)
        }
    }
}

3.5 核心实现伪代码 --- 我的项目查询

vbnet 复制代码
// 工作台:查询当前用户参与的项目卡片
function selectMyProjects(userId) {
    return 项目Mapper.联查用户参与的项目卡片(userId)
}

// 联查逻辑(伪 SQL 描述)
/*
SELECT
    项目ID, 项目名称, 项目模式, 状态, 进度,
    我在项目中的身份,
    项目描述, 部门名称, 缺陷策略,
    项目经理姓名, 创建时间
FROM 项目表
INNER JOIN 项目成员表 ON 项目ID匹配 AND 用户ID = 当前用户 AND 未删除
LEFT JOIN 部门表 ON 部门ID匹配 AND 未删除
LEFT JOIN 项目成员表(项目经理) ON 项目ID匹配 AND 身份 = PM AND 未删除
LEFT JOIN 用户表(项目经理) ON 用户ID匹配 AND 未删除
WHERE 项目未删除
ORDER BY 更新时间倒序
*/

四、总结与展望

4.1 当前已实现

  • 成员管理:完整的列表查询(企业隔离)、添加成员(从已有账号拉入)、移出成员(级联清理)、启用/禁用/锁定/解锁/重置密码、编辑成员信息
  • 团队管理:完整的 CRUD + 团队成员管理(增量同步模式)
  • 工作台:问候区、数据概览、我的项目(Mock)、我的工作项(Mock)、待办(Mock)、预警中心(Mock)
  • 企业隔离:@CompanyIsolated + CompanyScope + LoginHelper 三件套贯穿全部接口

4.2 后续替换计划

markdown 复制代码
Mock 替换路线图

  Phase 1:工作项模块
    ├─ 替换 MyWorkItems.vue → 真实工作项列表接口
    ├─ 替换 WorkbenchStats.vue → 真实工作项统计接口
    └─ 替换项目卡片统计 → 项目维度工作项聚合

  Phase 2:流程中心
    ├─ 替换 WorkbenchTodo.vue → 真实待办列表接口
    └─ 替换待办相关后端 Mock

  Phase 3:预警引擎
    ├─ 替换 WorkbenchAlert.vue → 真实预警列表接口
    └─ 替换预警相关后端 Mock

  Phase 4:全局统计
    └─ 替换全局数据接口 → 真实数据聚合接口

每一层 Mock 的替换都只需修改对应组件的数据源,不影响工作台的整体布局和交互逻辑。这是分层架构的核心价值------接口契约先行,实现可插拔

相关推荐
Lyy4 小时前
DevOps平台 — 第五篇:操作日志与调用链追踪
devops
Lyy4 小时前
DevOps平台 — 第四篇:密码策略与安全防护
devops
Lyy7 小时前
DevOps平台 — 第二篇:登录认证与用户角色管理
devops
终末圆8 小时前
GitHub Actions 实现 CI/CD
运维·ci/cd·github·运维开发·devops
运维开发那些事3 天前
GitOps最佳实践 (gitlab ci + ArgoCD)
ci/cd·devops
可乐ea3 天前
Anthropic 的 CI/CD 值班智能体:Claude Tag 当一线响应者的架构拆解与踩坑复盘
ci/cd·架构·claude·devops·ai智能体·mcp
Zadig4 天前
Zadig 全面支持 CRD,至此所有 K8s 资源类型均可一键发布!
后端·devops
NineData9 天前
DTCC 2026 预告|NineData CEO& 创始人叶正盛:面向 AI Agent 的数据库 DevOps 与数据复制实践
数据库·人工智能·数据库开发·devops·ninedata·数据库技术·dtcc
7177779 天前
如何搭建信创研发交付体系?Gitee DevOps 国产化适配、安全与流水线能力解析
安全·gitee·devops