1145 lines
36 KiB
HTML
1145 lines
36 KiB
HTML
<!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<object></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<Long></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<object></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<Long></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<UserSimpleRespVO></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>
|