Files
cn-rdms-web/关心人功能-API接口文档.html

1145 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>关心人功能 — API 接口文档</title>
<style>
:root {
--fg: #1f2328;
--fg-muted: #57606a;
--bg: #ffffff;
--bg-soft: #f6f8fa;
--border: #d0d7de;
--border-soft: #e7ecf2;
--accent: #0969da;
--accent-soft: #ddf4ff;
--warn: #9a6700;
--warn-soft: #fff8c5;
--danger: #cf222e;
--danger-soft: #ffebe9;
--ok: #1a7f37;
--ok-soft: #dafbe1;
--code-bg: #f6f8fa;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--fg);
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Hiragino Sans GB',
'Source Han Sans CN', 'Noto Sans CJK SC', Helvetica, Arial, sans-serif;
font-size: 15px;
line-height: 1.75;
}
.wrap {
max-width: 1000px;
margin: 0 auto;
padding: 48px 32px 96px;
}
header.doc-header {
border-bottom: 1px solid var(--border);
padding-bottom: 16px;
margin-bottom: 24px;
}
header.doc-header h1 {
margin: 0 0 8px;
font-size: 28px;
letter-spacing: 0.5px;
}
header.doc-header .meta {
color: var(--fg-muted);
font-size: 13px;
margin: 0;
}
h2 {
margin: 40px 0 12px;
padding-bottom: 6px;
border-bottom: 1px solid var(--border-soft);
font-size: 22px;
}
h3 {
margin: 32px 0 8px;
font-size: 18px;
}
h4 {
margin: 20px 0 6px;
font-size: 15px;
color: var(--fg);
}
p {
margin: 8px 0;
}
ul,
ol {
margin: 8px 0;
padding-left: 24px;
}
ul li,
ol li {
margin: 3px 0;
}
code,
pre {
font-family: 'JetBrains Mono', Menlo, Consolas, 'Courier New', monospace;
font-size: 13px;
}
code {
background: var(--code-bg);
padding: 1px 6px;
border-radius: 4px;
border: 1px solid var(--border-soft);
}
pre {
background: var(--code-bg);
border: 1px solid var(--border-soft);
border-radius: 6px;
padding: 14px 16px;
overflow-x: auto;
line-height: 1.55;
}
pre code {
background: transparent;
border: 0;
padding: 0;
}
table {
border-collapse: collapse;
width: 100%;
margin: 12px 0;
font-size: 13px;
}
th,
td {
border: 1px solid var(--border);
padding: 7px 10px;
text-align: left;
vertical-align: top;
}
th {
background: var(--bg-soft);
font-weight: 600;
}
tr:nth-child(2n) td {
background: #fafbfc;
}
.callout {
border-left: 4px solid var(--accent);
background: var(--accent-soft);
padding: 10px 14px;
border-radius: 4px;
margin: 12px 0;
}
.callout.warn {
border-left-color: var(--warn);
background: var(--warn-soft);
}
.callout.danger {
border-left-color: var(--danger);
background: var(--danger-soft);
}
.callout.ok {
border-left-color: var(--ok);
background: var(--ok-soft);
}
.callout .title {
font-weight: 600;
margin-bottom: 4px;
}
.endpoint {
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px 20px;
margin: 18px 0;
background: var(--bg);
}
.endpoint-head {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 8px;
}
.method {
display: inline-block;
padding: 3px 12px;
border-radius: 4px;
font-family: 'JetBrains Mono', Menlo, Consolas, monospace;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.5px;
}
.method.post {
background: #1a7f37;
color: white;
}
.method.get {
background: #0969da;
color: white;
}
.path {
font-family: 'JetBrains Mono', Menlo, Consolas, monospace;
font-size: 14px;
font-weight: 600;
}
.endpoint-desc {
color: var(--fg-muted);
font-size: 13px;
margin: 0 0 12px;
}
.badge {
display: inline-block;
padding: 1px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 500;
border: 1px solid var(--border);
background: var(--bg-soft);
color: var(--fg-muted);
}
.badge.required {
background: var(--danger-soft);
color: var(--danger);
border-color: #f0a1a8;
}
.badge.optional {
background: var(--bg-soft);
color: var(--fg-muted);
}
hr {
border: 0;
border-top: 1px solid var(--border-soft);
margin: 32px 0;
}
.toc {
background: var(--bg-soft);
border: 1px solid var(--border-soft);
border-radius: 6px;
padding: 14px 20px;
margin: 16px 0 24px;
}
.toc ol {
margin: 4px 0;
}
.toc a {
color: var(--accent);
text-decoration: none;
}
.toc a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="wrap">
<header class="doc-header">
<h1>关心人功能 — API 接口文档</h1>
<p class="meta">面向前端开发 · 后端已就绪 · 编写日期 2026-05-14</p>
</header>
<div class="callout">
<div class="title">一句话总结</div>
在产品 / 项目创建向导第 2 步增加"关心人"多选输入框,前端把选中的
<code>userId</code>
列表通过
<code>watcherUserIds</code>
字段一并提交到现有的
<code>create-with-team</code>
接口即可。后端自动给这些用户授予
<strong>产品/项目关心人</strong>
角色,让他们后续能在列表 / 详情看到这个对象(菜单按 watcher 角色权限渲染)。
</div>
<div class="toc">
<strong>目录</strong>
<ol>
<li><a href="#sec-1">设计意图</a></li>
<li><a href="#sec-2">用户旅程 / 前端交互</a></li>
<li><a href="#sec-3">接口列表速览</a></li>
<li><a href="#sec-4">主接口 — 创建产品(含团队 + 关心人)</a></li>
<li><a href="#sec-5">主接口 — 创建项目(含团队 + 关心人)</a></li>
<li><a href="#sec-6">辅助接口 — 用户精简列表(用于选关心人)</a></li>
<li><a href="#sec-7">业务规则 / 边界 / FAQ</a></li>
</ol>
</div>
<!-- =================== 1. 设计意图 =================== -->
<h2 id="sec-1">1. 设计意图</h2>
<h3>1.1 关心人是什么</h3>
<p>
"关心人"watcher
<strong>对产品 / 项目本身长期感兴趣,但不参与交付</strong>
的人。典型场景:
</p>
<ul>
<li>外部顾问需要长期看某产品的进度 / 需求</li>
<li>跨部门评审人(如营销、财务)需要看某项目的概览</li>
<li>领导层非直接负责,但要持续关注某产品</li>
</ul>
<h3>1.2 为什么不直接加成员?</h3>
<table>
<tr>
<th>身份</th>
<th>角色 code</th>
<th>能看什么</th>
<th>能做什么</th>
</tr>
<tr>
<td>产品经理 / 项目负责人</td>
<td>
<code>product_manager</code>
/
<code>project_manager</code>
</td>
<td>对象内全部 tab</td>
<td>所有操作(分配任务、改预算、删对象等)</td>
</tr>
<tr>
<td>团队成员(开发 / 测试 / 协办等)</td>
<td>各业务角色</td>
<td>对象内执行细节(任务、代码、文档等)</td>
<td>跟自己角色 / 任务挂钩的写操作</td>
</tr>
<tr>
<td><strong>关心人 watcher</strong></td>
<td>
<strong>
<code>product_watcher</code>
/
<code>project_watcher</code>
</strong>
</td>
<td><strong>对象主要 tab概览 / 进度 / 需求)</strong></td>
<td><strong>只读 — 不参与交付</strong></td>
</tr>
</table>
<div class="callout">
<div class="title">为什么单独做 watcher不复用现有 member</div>
数据权限设计里有
<strong>三条互不重叠的边界</strong>
<ul style="margin: 6px 0">
<li>
<strong>个别长期看几个对象</strong>
→ 用 watcher粒度=单个对象)
</li>
<li>
<strong>跨方向 / 跨部门看一片对象</strong>
→ 用用户可见性配置
<code>visibility_config</code>
(粒度=方向 or 全局,由系统管理员配)
</li>
<li>
<strong>实际交付参与</strong>
→ 用 member绑业务角色
</li>
</ul>
少量人盯几个对象就是 watcher 的位置 —— 比逐条加成员省事,比配 visibility_config 颗粒细。
</div>
<h3>1.3 关心人的数据权限路径</h3>
<p>
关心人记录最终落到
<code>rdms_user_object_role</code>
表(与 member 同表,只是
<code>role_id</code>
指向 watcher 角色)。系统按这个角色让用户进入"数据权限通道 1自己参与",跟普通成员路径一致 —— 区别只在
<strong>菜单 / 按钮粒度</strong>
由 watcher 角色绑定的权限决定。
</p>
<!-- =================== 2. 用户旅程 =================== -->
<h2 id="sec-2">2. 用户旅程 / 前端交互</h2>
<h3>2.1 当前两步向导(产品 / 项目都一样)</h3>
<ol>
<li>
<strong>第 1 步</strong>
:填基础资料 —— 名称 / 编码 / 方向 / 负责人等
</li>
<li>
<strong>第 2 步</strong>
:维护初始团队 —— 必须含负责人本人 + 其他成员(每人一个业务角色)
</li>
</ol>
<h3>2.2 本次新增 — 第 2 步追加"关心人"区块</h3>
<p>
建议 UI第 2 步页面里在"团队成员"列表下方加一个独立区块"
<strong>关心人(选填)</strong>
",含一个用户多选控件。
</p>
<pre><code>┌─────────────────────────────────────────────────────────┐
│ 第 2 步:维护初始团队 │
├─────────────────────────────────────────────────────────┤
│ │
│ 团队成员 * │
│ ┌────────────────────────────────────────────────────┐ │
│ │ [+] 添加成员 │ │
│ │ · 张三 产品经理 操作 ▾ │ │
│ │ · 李四 开发 操作 ▾ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 关心人(选填) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ [多选用户] 王五 赵六 │ │
│ └────────────────────────────────────────────────────┘ │
│ 提示:关心人将获得"产品/项目关心人"角色,能在列表和概览看到此对象, │
│ 但不开放代码 / 任务 / 文档等执行细节。可与团队成员重叠。 │
│ │
│ [取消] [上一步] [完成] │
└─────────────────────────────────────────────────────────┘</code></pre>
<h3>2.3 提交时数据组装</h3>
<p>
点击"完成"时,按
<a href="#sec-4">§4</a>
/
<a href="#sec-5">§5</a>
的 ReqVO 结构组装:
</p>
<ul>
<li>
<code>product</code>
/
<code>project</code>
块 = 第 1 步表单
</li>
<li>
<code>members</code>
块 = 第 2 步团队成员列表
</li>
<li>
<strong>
<code>watcherUserIds</code>
= 第 2 步关心人多选控件的 userId 数组
</strong>
(可空 / 可省略字段)
</li>
</ul>
<!-- =================== 3. 接口速览 =================== -->
<h2 id="sec-3">3. 接口列表速览</h2>
<table>
<tr>
<th>方法</th>
<th>路径</th>
<th>用途</th>
<th>本次是否新接口</th>
</tr>
<tr>
<td><span class="method post">POST</span></td>
<td><code>/project/product/create-with-team</code></td>
<td>创建产品(含初始团队 + 关心人)</td>
<td>
已有接口;本次只是 ReqVO 多了
<code>watcherUserIds</code>
字段
</td>
</tr>
<tr>
<td><span class="method post">POST</span></td>
<td><code>/project/project/create-with-team</code></td>
<td>创建项目(含初始团队 + 关心人)</td>
<td>
已有接口;本次只是 ReqVO 多了
<code>watcherUserIds</code>
字段
</td>
</tr>
<tr>
<td><span class="method get">GET</span></td>
<td>
<code>/system/user/list-all-simple</code>
<br />
(别名
<code>/system/user/simple-list</code>
</td>
<td>获取启用用户精简列表(前端选关心人下拉用)</td>
<td>已有接口,无需任何改动</td>
</tr>
</table>
<!-- =================== 4. 产品接口 =================== -->
<h2 id="sec-4">4. 主接口 — 创建产品(含团队 + 关心人)</h2>
<div class="endpoint">
<div class="endpoint-head">
<span class="method post">POST</span>
<span class="path">/project/product/create-with-team</span>
</div>
<p class="endpoint-desc">
事务性原子接口:产品基础资料 + 初始团队 + 关心人在同一事务内完成,任一步失败整体回滚。
</p>
<h4>权限</h4>
<p>
<code>@PreAuthorize("@ss.hasPermission('project:product:create')")</code>
— 创建产品权限码
</p>
<h4>
请求体(
<code>ProductCreateWithTeamReqVO</code>
</h4>
<pre><code>{
"product": {
"code": "CNPD2026001",
"directionCode": "system",
"name": "RDMS 产品平台",
"managerUserId": 1024,
"description": "面向研发管理的一体化产品"
},
"members": [
{ "userId": 1024, "roleId": 3100000002001, "remark": "产品经理本人" },
{ "userId": 1025, "roleId": 3100000002002, "remark": "开发" }
],
"watcherUserIds": [2001, 2002]
}</code></pre>
<h4>字段说明</h4>
<table>
<tr>
<th>路径</th>
<th>类型</th>
<th>必填</th>
<th>说明</th>
</tr>
<tr>
<td><code>product</code></td>
<td>object</td>
<td><span class="badge required">必填</span></td>
<td>产品基础资料,结构见下</td>
</tr>
<tr>
<td><code>product.code</code></td>
<td>string ≤ 64</td>
<td><span class="badge optional">选填</span></td>
<td>产品编码;为空时由后端按规则生成</td>
</tr>
<tr>
<td><code>product.directionCode</code></td>
<td>string ≤ 32</td>
<td><span class="badge required">必填</span></td>
<td>
产品方向字典值(字典类型
<code>object_direction</code>
</td>
</tr>
<tr>
<td><code>product.name</code></td>
<td>string ≤ 128</td>
<td><span class="badge required">必填</span></td>
<td>产品名称</td>
</tr>
<tr>
<td><code>product.managerUserId</code></td>
<td>Long</td>
<td><span class="badge required">必填</span></td>
<td>
产品经理 user_id必须在
<code>members</code>
列表里有对应的产品经理角色记录
</td>
</tr>
<tr>
<td><code>product.description</code></td>
<td>string</td>
<td><span class="badge optional">选填</span></td>
<td>产品描述</td>
</tr>
<tr>
<td><code>members</code></td>
<td>array&lt;object&gt;</td>
<td><span class="badge required">必填</span></td>
<td>
初始团队,
<strong>
必须包含 userId == product.managerUserId 且 roleId 是
<code>product_manager</code>
的记录
</strong>
;后端不会自动追加经理
</td>
</tr>
<tr>
<td><code>members[].userId</code></td>
<td>Long</td>
<td><span class="badge required">必填</span></td>
<td>成员 user_id</td>
</tr>
<tr>
<td><code>members[].roleId</code></td>
<td>Long</td>
<td><span class="badge required">必填</span></td>
<td>
对象域角色 ID
<code>scope_type='object'</code>
,
<code>object_type='product'</code>
);前端用 role code 反查得到(如
<code>product_manager</code>
</td>
</tr>
<tr>
<td><code>members[].remark</code></td>
<td>string ≤ 500</td>
<td><span class="badge optional">选填</span></td>
<td>成员备注</td>
</tr>
<tr>
<td>
<strong><code>watcherUserIds</code></strong>
</td>
<td>array&lt;Long&gt;</td>
<td><span class="badge optional">选填</span></td>
<td>
<strong>关心人 user_id 数组</strong>
。允许为空 / 省略字段 / 与 members 重叠;后端按 (user, object, role) 三元组写入,重复跳过 / INACTIVE
复活;后端会自动去 null 去重
</td>
</tr>
</table>
<h4>响应</h4>
<pre><code>{
"code": 0,
"data": 30001, // 新创建的 productId
"msg": ""
}</code></pre>
<h4>典型错误</h4>
<table>
<tr>
<th>错误码 / 信息</th>
<th>触发条件</th>
</tr>
<tr>
<td><code>产品基础资料不能为空</code></td>
<td>
<code>product</code>
字段缺失
</td>
</tr>
<tr>
<td><code>初始团队成员不能为空</code></td>
<td>
<code>members</code>
空数组或缺失
</td>
</tr>
<tr>
<td><code>产品经理不能为空</code></td>
<td>
<code>product.managerUserId</code>
缺失
</td>
</tr>
<tr>
<td><code>产品方向不能为空</code></td>
<td>
<code>product.directionCode</code>
缺失
</td>
</tr>
<tr>
<td><code>PRODUCT_MEMBER_ALREADY_EXISTS</code></td>
<td>
<code>members</code>
里同 user 同 role 重复multi-role 唯一索引拦截)
</td>
</tr>
<tr>
<td><code>PRODUCT_INTERNAL_ROLE_NOT_CONFIGURED</code></td>
<td>
后端启动校验通过但运行时找不到
<code>product_watcher</code>
角色(理论上不会发生 — 启动校验会拦)
</td>
</tr>
</table>
</div>
<!-- =================== 5. 项目接口 =================== -->
<h2 id="sec-5">5. 主接口 — 创建项目(含团队 + 关心人)</h2>
<div class="endpoint">
<div class="endpoint-head">
<span class="method post">POST</span>
<span class="path">/project/project/create-with-team</span>
</div>
<p class="endpoint-desc">
事务性原子接口:项目基础资料 + 初始团队 + 关心人在同一事务内完成,任一步失败整体回滚。
</p>
<h4>权限</h4>
<p>
<code>@PreAuthorize("@ss.hasPermission('project:project:create')")</code>
— 创建项目权限码
</p>
<h4>
请求体(
<code>ProjectCreateWithTeamReqVO</code>
</h4>
<pre><code>{
"project": {
"projectCode": "CNPJ2026001",
"projectName": "客户交付项目 A",
"projectType": "delivery",
"directionCode": "system",
"productId": 30001,
"managerUserId": 1024,
"plannedStartDate": "2026-06-01",
"plannedEndDate": "2026-09-30",
"projectDesc": "为客户 XX 定制交付"
},
"members": [
{ "userId": 1024, "roleId": 3100000003001, "remark": "项目负责人本人" },
{ "userId": 1030, "roleId": 3100000003002, "remark": "前端开发" }
],
"watcherUserIds": [2001, 2002]
}</code></pre>
<h4>字段说明</h4>
<table>
<tr>
<th>路径</th>
<th>类型</th>
<th>必填</th>
<th>说明</th>
</tr>
<tr>
<td><code>project</code></td>
<td>object</td>
<td><span class="badge required">必填</span></td>
<td>
项目基础资料;新建场景不传
<code>actualStartDate</code>
/
<code>actualEndDate</code>
(实际日期由执行阶段才有值)
</td>
</tr>
<tr>
<td><code>project.projectCode</code></td>
<td>string ≤ 64</td>
<td><span class="badge optional">选填</span></td>
<td>项目编码;为空时由后端按规则生成</td>
</tr>
<tr>
<td><code>project.projectName</code></td>
<td>string ≤ 200</td>
<td><span class="badge required">必填</span></td>
<td>项目名称</td>
</tr>
<tr>
<td><code>project.projectType</code></td>
<td>string ≤ 32</td>
<td><span class="badge required">必填</span></td>
<td>项目类型字典值</td>
</tr>
<tr>
<td><code>project.directionCode</code></td>
<td>string</td>
<td>条件必填</td>
<td>
项目方向;
<strong>未选产品时必填;选了产品则后端按产品方向覆盖</strong>
,前端可不传
</td>
</tr>
<tr>
<td><code>project.productId</code></td>
<td>Long</td>
<td><span class="badge optional">选填</span></td>
<td>所属产品 ID不选表示无产品归属</td>
</tr>
<tr>
<td><code>project.managerUserId</code></td>
<td>Long</td>
<td><span class="badge required">必填</span></td>
<td>
项目负责人 user_id同产品规则必须在
<code>members</code>
里出现且对应负责人角色
</td>
</tr>
<tr>
<td>
<code>project.plannedStartDate</code>
/
<code>plannedEndDate</code>
</td>
<td>
date
<code>YYYY-MM-DD</code>
</td>
<td><span class="badge optional">选填</span></td>
<td>计划日期</td>
</tr>
<tr>
<td><code>project.projectDesc</code></td>
<td>string ≤ 4000</td>
<td><span class="badge optional">选填</span></td>
<td>项目说明</td>
</tr>
<tr>
<td><code>members</code></td>
<td>array&lt;object&gt;</td>
<td><span class="badge required">必填</span></td>
<td>
结构同
<code>ProjectMemberSaveReqVO</code>
,与产品对称。必须包含 userId == project.managerUserId 且 roleId 为
<code>project_manager</code>
的记录
</td>
</tr>
<tr>
<td><code>members[].userId</code></td>
<td>Long</td>
<td><span class="badge required">必填</span></td>
<td>成员 user_id</td>
</tr>
<tr>
<td><code>members[].roleId</code></td>
<td>Long</td>
<td><span class="badge required">必填</span></td>
<td>
对象域角色 ID
<code>scope_type='object'</code>
,
<code>object_type='project'</code>
</td>
</tr>
<tr>
<td><code>members[].remark</code></td>
<td>string ≤ 500</td>
<td><span class="badge optional">选填</span></td>
<td>成员备注</td>
</tr>
<tr>
<td>
<strong><code>watcherUserIds</code></strong>
</td>
<td>array&lt;Long&gt;</td>
<td><span class="badge optional">选填</span></td>
<td>
<strong>关心人 user_id 数组</strong>
。规则与产品端一致:允许空 / 省略 / 与 members 重叠;后端去 null 去重,按三元组幂等写入
</td>
</tr>
</table>
<h4>响应</h4>
<pre><code>{
"code": 0,
"data": 40001, // 新创建的 projectId
"msg": ""
}</code></pre>
<h4>典型错误</h4>
<table>
<tr>
<th>错误码 / 信息</th>
<th>触发条件</th>
</tr>
<tr>
<td><code>项目基础资料不能为空</code></td>
<td>
<code>project</code>
字段缺失
</td>
</tr>
<tr>
<td><code>项目名称不能为空</code></td>
<td>
<code>project.projectName</code>
缺失
</td>
</tr>
<tr>
<td><code>项目负责人不能为空</code></td>
<td>
<code>project.managerUserId</code>
缺失
</td>
</tr>
<tr>
<td><code>初始团队成员不能为空</code></td>
<td>
<code>members</code>
</td>
</tr>
<tr>
<td><code>PROJECT_MEMBER_ALREADY_EXISTS</code></td>
<td>
<code>members</code>
里同 user 同 role 重复
</td>
</tr>
<tr>
<td><code>PROJECT_INTERNAL_ROLE_NOT_CONFIGURED</code></td>
<td>
后端运行时找不到
<code>project_watcher</code>
角色(理论上不会发生)
</td>
</tr>
</table>
</div>
<!-- =================== 6. 用户列表 =================== -->
<h2 id="sec-6">6. 辅助接口 — 用户精简列表</h2>
<div class="endpoint">
<div class="endpoint-head">
<span class="method get">GET</span>
<span class="path">/system/user/list-all-simple</span>
</div>
<p class="endpoint-desc">
获取所有
<strong>已启用</strong>
用户的精简信息,用于前端下拉 / 多选控件。也可访问别名
<code>/system/user/simple-list</code>
</p>
<h4>权限</h4>
<p>登录态即可(无显式权限码限制)。</p>
<h4>请求</h4>
<p>无参数。</p>
<h4>
响应(
<code>List&lt;UserSimpleRespVO&gt;</code>
</h4>
<pre><code>{
"code": 0,
"data": [
{ "id": 1024, "nickname": "张三" },
{ "id": 1025, "nickname": "李四" },
{ "id": 2001, "nickname": "王五" }
],
"msg": ""
}</code></pre>
<div class="callout">
<div class="title">前端使用建议</div>
这个接口返回的字段比较精简(一般是 id + 昵称)。如果需要按部门 / 工号 / 状态筛选,可以叠加用
<code>GET /system/user/page</code>
走分页查询;或者拉一次全量后在前端做 search filter。
</div>
</div>
<!-- =================== 7. FAQ =================== -->
<h2 id="sec-7">7. 业务规则 / 边界 / FAQ</h2>
<h3>7.1 watcher 与 member 可不可以是同一人?</h3>
<p>
可以。多角色合法,会落两条
<code>rdms_user_object_role</code>
记录(一条 member 角色 + 一条 watcher 角色),数据权限
<strong>取并集</strong>
。这种情况通常不会出现 —— 已经在团队里的人不需要再设为 watcher —— 但允许重叠不报错,前端不需要做"过滤已在
members 里的 user"的校验,让后端兜底。
</p>
<h3>7.2 创建之后能不能再加 / 删关心人?</h3>
<p>
当前
<strong>create-with-team 是唯一的批量入口</strong>
。创建后维护 watcher 的接口暂未单独提供 —— 如果业务后续需要"在产品 /
项目编辑页面再加关心人",需要再拉一条接口(沿用现有的
<code>add member</code>
/
<code>remove member</code>
模式即可roleId 传 watcher 角色 ID
</p>
<h3>7.3 watcher 进入对象后能看到什么、能做什么?</h3>
<p>
取决于业务侧给
<code>product_watcher</code>
/
<code>project_watcher</code>
角色在权限管理界面绑了哪些菜单(对象域菜单
<code>system_menu.scope_type='object'</code>
)。建议粒度:
</p>
<ul>
<li>
<strong>能看</strong>
:概览 / 进度 / 需求(即对象主线 tab
</li>
<li>
<strong>不开放</strong>
:代码 / 任务详情编辑 / 文档 / 工时 / 成员管理等执行细节
</li>
<li>
<strong>按钮粒度</strong>
:后端不扩字段,按"对象域内按钮可见性由前端控制"原则,前端按当前用户在该对象内的 role code 自行判断。watcher
几乎所有写按钮都不显示。
</li>
</ul>
<div class="callout warn">
<div class="title">前端可能用到的角色 code 常量</div>
<ul style="margin: 4px 0">
<li>
<code>product_manager</code>
/
<code>project_manager</code>
— 负责人
</li>
<li>
<code>product_creator</code>
/
<code>project_creator</code>
— 创建者(非负责人)
</li>
<li>
<strong>
<code>product_watcher</code>
/
<code>project_watcher</code>
</strong>
<strong>关心人</strong>
</li>
<li>
<code>implicit_observer_product</code>
/
<code>implicit_observer_project</code>
— 隐式只读 observer不会出现在 members 列表里,后端运行时兜底返回)
</li>
</ul>
</div>
<h3>7.4 watcher 字段不传会怎样?</h3>
<p>
完全等价于传空数组。
<code>watcherUserIds</code>
是可选字段 —— 不写 / 写 null / 写
<code>[]</code>
都不会落任何 watcher 记录,接口正常完成。
</p>
<h3>7.5 watcher 列表能放多少人?</h3>
<p>
当前没有上限校验。从产品语义看,关心人本来就是"少量长期看的人",前端可以加个软性提示(如超过 20
人时提示考虑用方向级 visibility_config 配置),但不阻塞提交。
</p>
<h3>7.6 关心人能看到对象里的"工时"吗?</h3>
<p>
看不到。工时是敏感数据,按对象内身份控制:协办人只看自己填报的工时;任务/项目负责人可看团队成员工时。
<strong>watcher 既不是协办人也不是负责人,工时列表对其不可见</strong>
</p>
<h3>7.7 watcher 和"用户可见性配置visibility_config"什么区别?</h3>
<table>
<tr>
<th>维度</th>
<th>关心人 watcher</th>
<th>用户可见性配置 visibility_config</th>
</tr>
<tr>
<td>粒度</td>
<td>单个产品 / 项目</td>
<td>整个方向 or 全局all / directions</td>
</tr>
<tr>
<td>谁来加</td>
<td>产品 / 项目创建人在新建表单里加</td>
<td>系统管理员在用户管理 / 可见性配置页加</td>
</tr>
<tr>
<td>典型场景</td>
<td>外部顾问盯几个产品 / 长期评审人</td>
<td>跨方向工程师看一片方向的所有产品 / 技术支持组协调全部</td>
</tr>
<tr>
<td>底层存储</td>
<td>
<code>rdms_user_object_role</code>
(一条对象成员记录)
</td>
<td>
<code>system_user_visibility_config</code>
(一条用户级配置)
</td>
</tr>
</table>
<div class="callout ok">
<div class="title">前端落地 checklist</div>
<ul style="margin: 6px 0">
<li>✅ 产品 / 项目新建向导第 2 步,加"关心人(选填)"区块(多选用户控件)</li>
<li>
✅ 多选控件数据源:
<code>GET /system/user/list-all-simple</code>
</li>
<li>
✅ 提交时按 ReqVO 结构发
<code>watcherUserIds: number[]</code>
,可空 / 可省略
</li>
<li>✅ 不需要在前端做"watcher 与 member 是否重叠"的校验 —— 后端去重</li>
<li>✅ UI 文案提示:关心人能在列表 / 概览看到对象,但不开放执行细节;可与团队成员重叠</li>
<li>⏸ 后续如需"创建后再加 / 删关心人",需要后端补单独接口(沿用 add/remove member 模式)</li>
</ul>
</div>
</div>
</body>
</html>