Files
cn-rdms-web/成员列表接口变更-前端对接说明.html

755 lines
22 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>成员列表接口变更 — 前端对接说明</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;
}
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: 28px 0 8px;
font-size: 17px;
}
h4 {
margin: 20px 0 6px;
font-size: 15px;
}
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: 14px 0;
background: var(--bg);
}
.method {
display: inline-block;
padding: 3px 12px;
border-radius: 4px;
font-family: 'JetBrains Mono', Menlo, Consolas, monospace;
font-size: 12px;
font-weight: 700;
margin-right: 10px;
}
.method.get {
background: #0969da;
color: white;
}
.badge {
display: inline-block;
padding: 1px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 500;
border: 1px solid var(--border);
}
.badge.new {
background: var(--ok-soft);
color: var(--ok);
border-color: #aceeb6;
}
.badge.keep {
background: var(--bg-soft);
color: var(--fg-muted);
}
.compare {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin: 12px 0;
}
.compare > div {
border: 1px solid var(--border);
border-radius: 6px;
padding: 12px 14px;
}
.compare .before {
border-top: 3px solid var(--warn);
}
.compare .after {
border-top: 3px solid var(--ok);
}
.compare h4 {
margin-top: 0;
}
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>成员列表接口变更 — 前端对接说明</h1>
<p class="meta">变更日期 2026-05-14 · 后端已就绪 · 前端需要配合调整渲染逻辑</p>
</header>
<div class="callout">
<div class="title">一句话总结</div>
<strong>产品 / 项目成员列表接口</strong>
现在按"
<strong>一人一行</strong>
"返回,同一用户的多个角色合并到主角色行,其他角色名通过新增的
<code>additionalRoleNames: string[]</code>
字段返回。前端只需要把这个数组拼到角色名旁边显示即可。
</div>
<div class="toc">
<strong>目录</strong>
<ol>
<li><a href="#sec-1">背景:为什么改</a></li>
<li><a href="#sec-2">受影响的接口(仅 2 个)</a></li>
<li><a href="#sec-3">改动前后对比</a></li>
<li><a href="#sec-4">新增字段 additionalRoleNames</a></li>
<li><a href="#sec-5">前端渲染建议</a></li>
<li><a href="#sec-6">边界场景</a></li>
<li><a href="#sec-7">不受影响的接口</a></li>
<li><a href="#sec-8">前端落地 checklist</a></li>
</ol>
</div>
<!-- ====== 1 ====== -->
<h2 id="sec-1">1. 背景:为什么改</h2>
<p>
多角色改造后,
<strong>
产品/项目的"创建者"角色会自动落一条
<code>rdms_user_object_role</code>
</strong>
。当
<strong>创建者本人就是负责人</strong>
时,同一用户在同一对象内会出现两条 ACTIVE 记录:
</p>
<ul>
<li>
一条
<code>product_manager</code>
/
<code>project_manager</code>
</li>
<li>
一条
<code>product_creator</code>
/
<code>project_creator</code>
</li>
</ul>
<p>
如果后端原样返回,前端列表会出现"同一个人重复两行"。讨论后约定:后端在
<strong>列表接口</strong>
层做合并展示,
<strong>不影响</strong>
底层数据(每个角色行仍独立存在,便于后续可能的"按角色单独操作")。
</p>
<div class="callout warn">
<div class="title">业务边界(重要)</div>
当前业务上
<strong>
只有
<code>creator + manager</code>
这一种组合
</strong>
会让同人多角色出现。其他角色(产品专员、开发等)仍是一人一角色。所以前端可以放心按"主角色 + 附加角色名"
的方式渲染,附加列表通常是 0 或 1 个元素。
</div>
<!-- ====== 2 ====== -->
<h2 id="sec-2">2. 受影响的接口(仅 2 个)</h2>
<div class="endpoint">
<span class="method get">GET</span>
<code>/admin-api/project/product/{productId}/members</code>
<p style="color: var(--fg-muted); font-size: 13px; margin: 6px 0 0">产品团队成员列表</p>
</div>
<div class="endpoint">
<span class="method get">GET</span>
<code>/admin-api/project/project/{projectId}/members</code>
<p style="color: var(--fg-muted); font-size: 13px; margin: 6px 0 0">项目团队成员列表</p>
</div>
<p>
响应结构两个接口对称,本文档下文统一用
<code>RespVO</code>
表示,字段路径一致。
</p>
<!-- ====== 3 ====== -->
<h2 id="sec-3">3. 改动前后对比</h2>
<p>
假设:产品 P 里
<code>用户A</code>
是产品经理(同时创建了该产品,所以也有
<code>product_creator</code>
角色),
<code>用户B</code>
是产品专员,
<code>用户C</code>
是已退场的历史成员。
</p>
<div class="compare">
<div class="before">
<h4>改之前数据原样返回4 行)</h4>
<pre><code>[
{ id: "100", userId: "A", roleCode: "product_manager",
roleName: "产品经理", status: 0, ... },
{ id: "101", userId: "A", roleCode: "product_creator",
roleName: "产品创建者", status: 0, ... },
{ id: "102", userId: "B", roleCode: "product_specialist",
roleName: "产品专员", status: 0, ... },
{ id: "103", userId: "C", roleCode: "product_specialist",
roleName: "产品专员", status: 1, leftTime: "..." }
]</code></pre>
<p style="color: var(--fg-muted); font-size: 12px">用户 A 出现 2 次 — 前端要么重复显示,要么自己去重。</p>
</div>
<div class="after">
<h4>改之后合并后3 行)</h4>
<pre><code>[
{ id: "100", userId: "A", roleCode: "product_manager",
roleName: "产品经理",
additionalRoleNames: ["产品创建者"], // ← 新增字段
status: 0, ... },
{ id: "102", userId: "B", roleCode: "product_specialist",
roleName: "产品专员",
additionalRoleNames: [],
status: 0, ... },
{ id: "103", userId: "C", roleCode: "product_specialist",
roleName: "产品专员",
additionalRoleNames: [],
status: 1, leftTime: "..." }
]</code></pre>
<p style="color: var(--fg-muted); font-size: 12px">
用户 A 1 行 — 主行 managercreator 名字进
<code>additionalRoleNames</code>
</p>
</div>
</div>
<h3>合并规则</h3>
<table>
<tr>
<th>规则</th>
<th>说明</th>
</tr>
<tr>
<td>仅合并 ACTIVE 行</td>
<td>
<code>status = 0</code>
的多角色行才聚合;
<code>status = 1</code>
的历史 INACTIVE 行
<strong>保持每条独立成行</strong>
(历史角色留痕,便于审计)
</td>
</tr>
<tr>
<td>主角色选择</td>
<td>
同人多角色时,
<code>product_manager</code>
/
<code>project_manager</code>
角色行优先做主;不在则按
<code>roleId</code>
升序选第一条(理论不该走到这条兜底)
</td>
</tr>
<tr>
<td>主行字段</td>
<td>
<code>id</code>
/
<code>roleId</code>
/
<code>roleCode</code>
/
<code>roleName</code>
/
<code>joinedTime</code>
/
<code>leftTime</code>
/
<code>remark</code>
等都按主角色行的值返回
</td>
</tr>
<tr>
<td>非主角色名顺序</td>
<td>
<code>additionalRoleNames</code>
按角色中文名字典序升序,前端可以直接顺序渲染
</td>
</tr>
</table>
<!-- ====== 4 ====== -->
<h2 id="sec-4">
4. 新增字段
<code>additionalRoleNames</code>
</h2>
<table>
<tr>
<th>字段名</th>
<th>类型</th>
<th>状态</th>
<th>说明</th>
</tr>
<tr>
<td><code>additionalRoleNames</code></td>
<td><code>string[]</code></td>
<td><span class="badge new">本次新增</span></td>
<td>
非主角色的中文名列表,多角色场景使用;
<strong>
单角色时为空数组
<code>[]</code>
</strong>
,前端可以放心
<code>length</code>
判空
</td>
</tr>
</table>
<p>
所有
<strong>原有字段保持不变</strong>
<code>id</code>
<code>userId</code>
<code>userNickname</code>
<code>roleId</code>
<code>roleName</code>
<code>roleCode</code>
<code>managerFlag</code>
<code>status</code>
<code>joinedTime</code>
<code>leftTime</code>
<code>remark</code>
),前端原有代码不会因为字段消失而报错。
</p>
<!-- ====== 5 ====== -->
<h2 id="sec-5">5. 前端渲染建议</h2>
<h3>5.1 最简单的展示方式 — 拼成一个字符串</h3>
<pre><code>const displayRoleName = additionalRoleNames?.length
? `${roleName} + ${additionalRoleNames.join(', ')}`
: roleName;
//
// 例roleName="产品经理", additionalRoleNames=["产品创建者"]
// → "产品经理 + 产品创建者"
//
// 例roleName="产品专员", additionalRoleNames=[]
// → "产品专员"</code></pre>
<h3>5.2 更友好的展示 — 主角色 + 浅色 chip 标签</h3>
<pre><code>&lt;span class="role-main"&gt;{{ roleName }}&lt;/span&gt;
&lt;span v-for="extra in additionalRoleNames" :key="extra" class="role-tag"&gt;
{{ extra }}
&lt;/span&gt;
//
// CSSrole-tag 设计成浅色 background + 小圆角,跟主角色拉开视觉层级</code></pre>
<h3>5.3 表格单元格示意</h3>
<pre><code>| 用户 | 角色 | 状态 |
|-----------|---------------------------|------|
| 灿能管理 | 产品经理 [产品创建者] | 有效 |
| 洪圣文 | 产品专员 | 有效 |
| 李凡 | 游客 | 历史 |</code></pre>
<!-- ====== 6 ====== -->
<h2 id="sec-6">6. 边界场景</h2>
<table>
<tr>
<th>场景</th>
<th>预期返回</th>
<th>前端展示</th>
</tr>
<tr>
<td>用户 A 仅是产品经理(不是创建者)</td>
<td>
1 行,
<code>additionalRoleNames=[]</code>
</td>
<td>"产品经理"</td>
</tr>
<tr>
<td>用户 A 同时是产品经理 + 创建者</td>
<td>
1 行(主行 manager
<code>additionalRoleNames=["产品创建者"]</code>
</td>
<td>"产品经理 + 产品创建者"</td>
</tr>
<tr>
<td>用户 A 仅是创建者(罕见,理论上创建后立即把 manager 转给别人才会出现)</td>
<td>
1 行,主行就是 creator
<code>additionalRoleNames=[]</code>
</td>
<td>"产品创建者"</td>
</tr>
<tr>
<td>用户 A 是产品专员(单角色非 manager</td>
<td>
1 行,
<code>additionalRoleNames=[]</code>
</td>
<td>"产品专员"</td>
</tr>
<tr>
<td>用户 A 退场status=1 INACTIVE 历史行)</td>
<td>
每条 INACTIVE 行独立 1 行,
<code>additionalRoleNames=[]</code>
</td>
<td>"产品专员(已退场)"等灰显</td>
</tr>
<tr>
<td>
用户 A 同时有
<strong>历史失效</strong>
的角色行 +
<strong>当前生效</strong>
的角色行
</td>
<td>ACTIVE 行合并 1 行 + 每条 INACTIVE 行各占 1 行;同用户在列表里会出现多次(不同 status</td>
<td>分别用"有效 / 历史"区分;不要再二次合并</td>
</tr>
</table>
<!-- ====== 7 ====== -->
<h2 id="sec-7">7. 不受影响的接口</h2>
<p>
以下接口
<strong>不变</strong>
,前端原有调用方式保持:
</p>
<table>
<tr>
<th>接口</th>
<th>说明</th>
</tr>
<tr>
<td>
<code>POST /admin-api/project/product/{productId}/members</code>
<br />
<code>POST /admin-api/project/project/{projectId}/members</code>
</td>
<td>
新增成员 — 仍按"一个角色一条记录"操作,传
<code>userId + roleId</code>
</td>
</tr>
<tr>
<td><code>PUT /admin-api/project/.../members/{memberId}</code></td>
<td>
更新成员 — 按
<code>memberId</code>
(即
<code>rdms_user_object_role.id</code>
)定位具体角色行操作
</td>
</tr>
<tr>
<td><code>PUT /admin-api/project/.../members/{memberId}/inactive</code></td>
<td>
失效成员 — 同上,按
<code>memberId</code>
操作
</td>
</tr>
<tr>
<td>
<code>GET /admin-api/project/product/{productId}/context</code>
<br />
<code>GET /admin-api/project/project/{projectId}/context</code>
</td>
<td>
对象上下文 —
<code>currentRole.additionalRoleNames</code>
字段早已存在,
<strong>不在本次变更范围</strong>
</td>
</tr>
</table>
<div class="callout warn">
<div class="title">特别提醒 — 编辑 / 失效操作</div>
当用户 A 同时是 manager + creator合并展示成 1 行)时,前端如果允许"编辑这一行的角色"或"踢出",传的
<code>memberId</code>
<strong>
主角色行的
<code>id</code>
</strong>
(也就是 manager 那条)。
<strong>不会影响</strong>
同人的 creator 角色行 — creator 角色仍保留。
<br />
<br />
这是符合设计的行为creator 是"留痕"角色,原则上不应该被踢出。如果业务上要踢出整个人,需要前端先调一次 list
接口拿到该用户的所有 active 角色行(注意 — 合并后的 list 里看不到 creator 那条的
<code>id</code>
),后续
<strong>是否要单独提供"列出某 user 所有角色行"接口</strong>
取决于业务实际诉求,目前没有这个接口。
</div>
<!-- ====== 8 ====== -->
<h2 id="sec-8">8. 前端落地 checklist</h2>
<div class="callout ok">
<ul style="margin: 6px 0">
<li>
✅ 把
<code>additionalRoleNames</code>
数组加到 TypeScript 类型定义ProductMemberRespVO / ProjectMemberRespVO 两处)
</li>
<li>
✅ 列表渲染逻辑:把
<code>additionalRoleNames</code>
拼到
<code>roleName</code>
旁边显示(字符串或 chip 标签均可)
</li>
<li>
✅ 验证:找一个"创建者 = 经理"的产品/项目,确认列表里这个人只出现
<strong>1 行</strong>
,且能看到"产品经理 + 产品创建者"字样
</li>
<li>
✅ 验证:找一个普通成员(产品专员等),
<code>additionalRoleNames</code>
应该是
<code>[]</code>
,不影响展示
</li>
<li>
✅ 验证:历史退场成员(
<code>status=1</code>
),仍按原方式各占一行
</li>
<li>
⏸ 编辑/失效操作 — 当前不动,仍按
<code>memberId</code>
操作主角色行;若业务需要"踢人整体",后端再补接口
</li>
</ul>
</div>
<hr />
<h3>变更影响最小范围说明</h3>
<p style="color: var(--fg-muted); font-size: 13px">
本次后端改动仅在
<code>ProductMemberServiceImpl.getProductMemberList</code>
<code>ProjectMemberServiceImpl.getProjectMemberList</code>
两个方法中实现,
<strong>不修改底层数据</strong>
<code>rdms_user_object_role</code>
仍按一行一角色存储)。即使前端暂时不读
<code>additionalRoleNames</code>
字段,也只是看不到"+ 产品创建者"字样,不会出现数据错误或重复行问题。
</p>
</div>
</body>
</html>