refactor(projects): 优化产品项目新增逻辑
This commit is contained in:
@@ -33,6 +33,8 @@ interface ProductMemberResponse {
|
||||
roleId: string | number;
|
||||
roleName: string;
|
||||
roleCode: string;
|
||||
/** 多角色合并展示的非主角色名列表 */
|
||||
additionalRoleNames?: string[] | null;
|
||||
managerFlag: boolean;
|
||||
status: 0 | 1;
|
||||
joinedTime: string;
|
||||
@@ -74,6 +76,7 @@ export function normalizeProductMember(response: ProductMemberResponse): Api.Pro
|
||||
roleId: normalizeStringId(response.roleId),
|
||||
roleName: response.roleName || '',
|
||||
roleCode: response.roleCode || '',
|
||||
additionalRoleNames: response.additionalRoleNames ?? [],
|
||||
managerFlag: Boolean(response.managerFlag),
|
||||
status: response.status,
|
||||
joinedTime: response.joinedTime,
|
||||
|
||||
@@ -147,6 +147,8 @@ export interface ProjectMemberResponse {
|
||||
roleId: string | number;
|
||||
roleName: string;
|
||||
roleCode: string;
|
||||
/** 多角色合并展示的非主角色名列表 */
|
||||
additionalRoleNames?: string[] | null;
|
||||
managerFlag: boolean;
|
||||
status: 0 | 1;
|
||||
joinedTime: string;
|
||||
@@ -225,6 +227,7 @@ export function normalizeProjectMember(response: ProjectMemberResponse): Api.Pro
|
||||
roleId: normalizeStringId(response.roleId),
|
||||
roleName: response.roleName || '',
|
||||
roleCode: response.roleCode || '',
|
||||
additionalRoleNames: response.additionalRoleNames ?? [],
|
||||
managerFlag: Boolean(response.managerFlag),
|
||||
status: response.status,
|
||||
joinedTime: response.joinedTime,
|
||||
|
||||
11
src/typings/api/product.d.ts
vendored
11
src/typings/api/product.d.ts
vendored
@@ -99,10 +99,15 @@ declare namespace Api {
|
||||
userNickname: string;
|
||||
/** 角色 ID */
|
||||
roleId: string;
|
||||
/** 角色名称 */
|
||||
/** 角色名称(主角色) */
|
||||
roleName: string;
|
||||
/** 角色编码 */
|
||||
/** 角色编码(主角色) */
|
||||
roleCode: string;
|
||||
/**
|
||||
* 非主角色的中文名列表(多角色合并展示用,按字典序升序)
|
||||
* 单角色时为空数组 [];典型场景:创建者 + 经理重合时,主行 manager,creator 名进此列表
|
||||
*/
|
||||
additionalRoleNames: string[];
|
||||
/** 是否当前产品经理 */
|
||||
managerFlag: boolean;
|
||||
/** 成员状态 */
|
||||
@@ -218,6 +223,8 @@ declare namespace Api {
|
||||
interface CreateProductWithTeamParams {
|
||||
product: SaveProductParams;
|
||||
members: CreateProductMemberParams[];
|
||||
/** 关心人 user_id 数组(选填);后端按 (user, object, role) 三元组幂等写入 product_watcher 角色 */
|
||||
watcherUserIds?: string[];
|
||||
}
|
||||
|
||||
interface UpdateProductMemberParams {
|
||||
|
||||
11
src/typings/api/project.d.ts
vendored
11
src/typings/api/project.d.ts
vendored
@@ -519,10 +519,15 @@ declare namespace Api {
|
||||
userNickname: string;
|
||||
/** 角色 ID */
|
||||
roleId: string;
|
||||
/** 角色名称 */
|
||||
/** 角色名称(主角色) */
|
||||
roleName: string;
|
||||
/** 角色编码 */
|
||||
/** 角色编码(主角色) */
|
||||
roleCode: string;
|
||||
/**
|
||||
* 非主角色的中文名列表(多角色合并展示用,按字典序升序)
|
||||
* 单角色时为空数组 [];典型场景:创建者 + 负责人重合时,主行 manager,creator 名进此列表
|
||||
*/
|
||||
additionalRoleNames: string[];
|
||||
/** 是否项目负责人 */
|
||||
managerFlag: boolean;
|
||||
/** 成员状态 */
|
||||
@@ -628,6 +633,8 @@ declare namespace Api {
|
||||
interface CreateProjectWithTeamParams {
|
||||
project: SaveProjectParams;
|
||||
members: CreateProjectMemberParams[];
|
||||
/** 关心人 user_id 数组(选填);后端按 (user, object, role) 三元组幂等写入 project_watcher 角色 */
|
||||
watcherUserIds?: string[];
|
||||
}
|
||||
|
||||
// ========== 项目需求相关类型定义 ==========
|
||||
|
||||
1
src/typings/components.d.ts
vendored
1
src/typings/components.d.ts
vendored
@@ -104,6 +104,7 @@ declare module 'vue' {
|
||||
IconEpSuccessFilled: typeof import('~icons/ep/success-filled')['default']
|
||||
'IconF7:circleFill': typeof import('~icons/f7/circle-fill')['default']
|
||||
'IconF7:flagCircleFill': typeof import('~icons/f7/flag-circle-fill')['default']
|
||||
'IconFe:eye': typeof import('~icons/fe/eye')['default']
|
||||
'IconFe:question': typeof import('~icons/fe/question')['default']
|
||||
'IconFileIcons:microsoftExcel': typeof import('~icons/file-icons/microsoft-excel')['default']
|
||||
'IconGg:ratio': typeof import('~icons/gg/ratio')['default']
|
||||
|
||||
@@ -27,6 +27,7 @@ const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:members', members: Api.Product.CreateProductMemberParams[]): void;
|
||||
(e: 'update:watcherUserIds', watcherUserIds: string[]): void;
|
||||
}>();
|
||||
|
||||
const roleOptions = ref<Api.SystemManage.RoleSimple[]>([]);
|
||||
@@ -38,7 +39,15 @@ const memberDialogVisible = ref(false);
|
||||
const memberDialogMode = ref<'create' | 'edit'>('create');
|
||||
const editingKey = ref<string | null>(null);
|
||||
|
||||
const teamTableHeight = getProductTeamTableHeight(5);
|
||||
const watcherUserIds = ref<string[]>([]);
|
||||
|
||||
// 关心人候选用户:排除已在团队成员列表中的用户(包含产品经理本人)
|
||||
const watcherUserOptions = computed(() => {
|
||||
const memberUserIds = new Set(members.value.map(item => item.userId).filter(Boolean));
|
||||
return props.userOptions.filter(user => !memberUserIds.has(user.id));
|
||||
});
|
||||
|
||||
const teamTableHeight = getProductTeamTableHeight(4);
|
||||
|
||||
const userLabelMap = computed(() => new Map(props.userOptions.map(item => [String(item.id), item.nickname])));
|
||||
|
||||
@@ -205,6 +214,24 @@ async function runValidate(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleWatcherChange(ids: string[]) {
|
||||
watcherUserIds.value = ids;
|
||||
emit('update:watcherUserIds', ids);
|
||||
}
|
||||
|
||||
// 团队成员变化时,剔除已被加入团队的关心人,避免重叠
|
||||
watch(
|
||||
() => members.value.map(item => item.userId).join(','),
|
||||
() => {
|
||||
const memberUserIds = new Set(members.value.map(item => item.userId).filter(Boolean));
|
||||
const filtered = watcherUserIds.value.filter(id => !memberUserIds.has(id));
|
||||
|
||||
if (filtered.length !== watcherUserIds.value.length) {
|
||||
handleWatcherChange(filtered);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(loadRoles);
|
||||
|
||||
watch(
|
||||
@@ -261,6 +288,27 @@ defineExpose({ validate: runValidate });
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<div class="watcher-row">
|
||||
<span class="watcher-row__label">
|
||||
关心人
|
||||
<span class="watcher-row__optional">(选填)</span>
|
||||
</span>
|
||||
<ElSelect
|
||||
:model-value="watcherUserIds"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:max-collapse-tags="3"
|
||||
placeholder="可在列表 / 概览看到此产品的关注人"
|
||||
class="watcher-row__select"
|
||||
@update:model-value="handleWatcherChange"
|
||||
>
|
||||
<ElOption v-for="item in watcherUserOptions" :key="item.id" :value="item.id" :label="item.nickname" />
|
||||
</ElSelect>
|
||||
</div>
|
||||
|
||||
<ProductCreateTeamMemberDialog
|
||||
v-model:visible="memberDialogVisible"
|
||||
:mode="memberDialogMode"
|
||||
@@ -295,4 +343,27 @@ defineExpose({ validate: runValidate });
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.watcher-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.watcher-row__label {
|
||||
flex: 0 0 auto;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: rgb(60 70 95 / 96%);
|
||||
}
|
||||
|
||||
.watcher-row__optional {
|
||||
color: rgb(140 150 170 / 96%);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.watcher-row__select {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -114,6 +114,7 @@ const currentStep = ref<1 | 2>(1);
|
||||
|
||||
const createBaseModel = ref<ProductCreateBaseFormModel>(createBaseInfo());
|
||||
const draftMembers = ref<Api.Product.CreateProductMemberParams[]>([]);
|
||||
const draftWatcherUserIds = ref<string[]>([]);
|
||||
|
||||
function createBaseInfo(): ProductCreateBaseFormModel {
|
||||
return { code: '', name: '', directionCode: '', managerUserId: null, description: '' };
|
||||
@@ -157,7 +158,8 @@ async function handleCreateSubmit() {
|
||||
managerUserId: createBaseModel.value.managerUserId as string,
|
||||
description: getNullableText(createBaseModel.value.description)
|
||||
},
|
||||
members: draftMembers.value
|
||||
members: draftMembers.value,
|
||||
watcherUserIds: draftWatcherUserIds.value.length > 0 ? draftWatcherUserIds.value : undefined
|
||||
};
|
||||
|
||||
const { error, data } = await fetchCreateProductWithTeam(payload);
|
||||
@@ -186,6 +188,7 @@ watch(visible, async value => {
|
||||
editModel.value = createEditModel();
|
||||
createBaseModel.value = createBaseInfo();
|
||||
draftMembers.value = [];
|
||||
draftWatcherUserIds.value = [];
|
||||
await nextTick();
|
||||
editFormRef.value?.clearValidate();
|
||||
return;
|
||||
@@ -330,6 +333,7 @@ watch(visible, async value => {
|
||||
:base-info="createBaseModel"
|
||||
:user-options="managerUserOptions"
|
||||
@update:members="draftMembers = $event"
|
||||
@update:watcher-user-ids="draftWatcherUserIds = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -103,7 +103,23 @@ function getMemberStatusTagType(status: Api.Product.ProductMemberStatus) {
|
||||
>
|
||||
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
||||
<ElTableColumn prop="userNickname" label="成员姓名" min-width="140" />
|
||||
<ElTableColumn prop="roleName" label="当前角色" min-width="140" />
|
||||
<ElTableColumn label="当前角色" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="setting-team-panel__role-cell">
|
||||
<span class="setting-team-panel__role-main">{{ row.roleName || '--' }}</span>
|
||||
<ElTag
|
||||
v-for="extra in row.additionalRoleNames"
|
||||
:key="extra"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
class="setting-team-panel__role-extra"
|
||||
>
|
||||
{{ extra }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="成员状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getMemberStatusTagType(row.status)">{{ getMemberStatusLabel(row.status) }}</ElTag>
|
||||
@@ -180,6 +196,17 @@ function getMemberStatusTagType(status: Api.Product.ProductMemberStatus) {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-extra {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.setting-team-panel__header {
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -27,6 +27,7 @@ const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:members', members: Api.Project.CreateProjectMemberParams[]): void;
|
||||
(e: 'update:watcherUserIds', watcherUserIds: string[]): void;
|
||||
}>();
|
||||
|
||||
const roleOptions = ref<Api.SystemManage.RoleSimple[]>([]);
|
||||
@@ -38,7 +39,15 @@ const memberDialogVisible = ref(false);
|
||||
const memberDialogMode = ref<'create' | 'edit'>('create');
|
||||
const editingKey = ref<string | null>(null);
|
||||
|
||||
const teamTableHeight = getProjectTeamTableHeight(5);
|
||||
const watcherUserIds = ref<string[]>([]);
|
||||
|
||||
// 关心人候选用户:排除已在团队成员列表中的用户(包含项目负责人本人)
|
||||
const watcherUserOptions = computed(() => {
|
||||
const memberUserIds = new Set(members.value.map(item => item.userId).filter(Boolean));
|
||||
return props.userOptions.filter(user => !memberUserIds.has(user.id));
|
||||
});
|
||||
|
||||
const teamTableHeight = getProjectTeamTableHeight(4);
|
||||
|
||||
const userLabelMap = computed(() => new Map(props.userOptions.map(item => [String(item.id), item.nickname])));
|
||||
|
||||
@@ -205,6 +214,24 @@ async function runValidate(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleWatcherChange(ids: string[]) {
|
||||
watcherUserIds.value = ids;
|
||||
emit('update:watcherUserIds', ids);
|
||||
}
|
||||
|
||||
// 团队成员变化时,剔除已被加入团队的关心人,避免重叠
|
||||
watch(
|
||||
() => members.value.map(item => item.userId).join(','),
|
||||
() => {
|
||||
const memberUserIds = new Set(members.value.map(item => item.userId).filter(Boolean));
|
||||
const filtered = watcherUserIds.value.filter(id => !memberUserIds.has(id));
|
||||
|
||||
if (filtered.length !== watcherUserIds.value.length) {
|
||||
handleWatcherChange(filtered);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(loadRoles);
|
||||
|
||||
watch(
|
||||
@@ -261,6 +288,27 @@ defineExpose({ validate: runValidate });
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<div class="watcher-row">
|
||||
<span class="watcher-row__label">
|
||||
关心人
|
||||
<span class="watcher-row__optional">(选填)</span>
|
||||
</span>
|
||||
<ElSelect
|
||||
:model-value="watcherUserIds"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:max-collapse-tags="3"
|
||||
placeholder="可在列表 / 概览看到此项目的关注人"
|
||||
class="watcher-row__select"
|
||||
@update:model-value="handleWatcherChange"
|
||||
>
|
||||
<ElOption v-for="item in watcherUserOptions" :key="item.id" :value="item.id" :label="item.nickname" />
|
||||
</ElSelect>
|
||||
</div>
|
||||
|
||||
<ProjectCreateTeamMemberDialog
|
||||
v-model:visible="memberDialogVisible"
|
||||
:mode="memberDialogMode"
|
||||
@@ -295,4 +343,27 @@ defineExpose({ validate: runValidate });
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.watcher-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.watcher-row__label {
|
||||
flex: 0 0 auto;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: rgb(60 70 95 / 96%);
|
||||
}
|
||||
|
||||
.watcher-row__optional {
|
||||
color: rgb(140 150 170 / 96%);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.watcher-row__select {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -267,6 +267,7 @@ const currentStep = ref<1 | 2>(1);
|
||||
|
||||
const createBaseModel = ref<ProjectCreateBaseFormModel>(createBaseInfo());
|
||||
const draftMembers = ref<Api.Project.CreateProjectMemberParams[]>([]);
|
||||
const draftWatcherUserIds = ref<string[]>([]);
|
||||
|
||||
function createBaseInfo(): ProjectCreateBaseFormModel {
|
||||
return {
|
||||
@@ -324,7 +325,8 @@ async function handleCreateSubmit() {
|
||||
plannedEndDate: createBaseModel.value.plannedEndDate,
|
||||
projectDesc: getNullableText(createBaseModel.value.projectDesc)
|
||||
},
|
||||
members: draftMembers.value
|
||||
members: draftMembers.value,
|
||||
watcherUserIds: draftWatcherUserIds.value.length > 0 ? draftWatcherUserIds.value : undefined
|
||||
};
|
||||
|
||||
const { error, data } = await fetchCreateProjectWithTeam(payload);
|
||||
@@ -353,6 +355,7 @@ watch(visible, async value => {
|
||||
editModel.value = createEditModel();
|
||||
createBaseModel.value = createBaseInfo();
|
||||
draftMembers.value = [];
|
||||
draftWatcherUserIds.value = [];
|
||||
await nextTick();
|
||||
editFormRef.value?.clearValidate();
|
||||
return;
|
||||
@@ -556,6 +559,7 @@ watch(visible, async value => {
|
||||
:base-info="createBaseModel"
|
||||
:user-options="managerUserOptions"
|
||||
@update:members="draftMembers = $event"
|
||||
@update:watcher-user-ids="draftWatcherUserIds = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,23 @@ function getMemberStatusTagType(status: Api.Project.ProjectMemberStatus) {
|
||||
>
|
||||
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
||||
<ElTableColumn prop="userNickname" label="成员姓名" min-width="140" />
|
||||
<ElTableColumn prop="roleName" label="当前角色" min-width="140" />
|
||||
<ElTableColumn label="当前角色" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="setting-team-panel__role-cell">
|
||||
<span class="setting-team-panel__role-main">{{ row.roleName || '--' }}</span>
|
||||
<ElTag
|
||||
v-for="extra in row.additionalRoleNames"
|
||||
:key="extra"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
class="setting-team-panel__role-extra"
|
||||
>
|
||||
{{ extra }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="成员状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getMemberStatusTagType(row.status)">{{ getMemberStatusLabel(row.status) }}</ElTag>
|
||||
@@ -170,6 +186,17 @@ function getMemberStatusTagType(status: Api.Project.ProjectMemberStatus) {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-extra {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.setting-team-panel__header {
|
||||
align-items: flex-start;
|
||||
|
||||
1144
关心人功能-API接口文档.html
Normal file
1144
关心人功能-API接口文档.html
Normal file
File diff suppressed because it is too large
Load Diff
754
成员列表接口变更-前端对接说明.html
Normal file
754
成员列表接口变更-前端对接说明.html
Normal file
@@ -0,0 +1,754 @@
|
||||
<!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 行 — 主行 manager,creator 名字进
|
||||
<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><span class="role-main">{{ roleName }}</span>
|
||||
<span v-for="extra in additionalRoleNames" :key="extra" class="role-tag">
|
||||
{{ extra }}
|
||||
</span>
|
||||
//
|
||||
// CSS:role-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>
|
||||
Reference in New Issue
Block a user