Keep project_role as member/viewer, send document_duty=reviewer for USER_B, and clear the seed 李主管 reviewer so the assigned reviewer is the one that syncs. Co-authored-by: Cursor <cursoragent@cursor.com>
99 lines
3.5 KiB
TypeScript
99 lines
3.5 KiB
TypeScript
/**
|
|
* 立项成功后将非创建者初始成员持久化到受权 project_members API。
|
|
* 禁止仅写本地 allUsers/sessions 造成「页面勾选成功、库表无成员」假成功。
|
|
*/
|
|
|
|
import type { UpsertProjectMemberPayload } from '../api/projectMembers';
|
|
import { toApiProjectRole } from './projectMemberAdapter';
|
|
import { documentDutyFromMemberRole } from './projectDocumentDuty';
|
|
import type { ProjectMemberRole } from '../types/workbench';
|
|
|
|
export type InitialProjectMemberInput = {
|
|
userId: number;
|
|
userName: string;
|
|
department?: string;
|
|
position?: string;
|
|
role: ProjectMemberRole;
|
|
};
|
|
|
|
export function selectNonCreatorInitialMembers(
|
|
initialMembers: InitialProjectMemberInput[],
|
|
creatorUserId: number,
|
|
): InitialProjectMemberInput[] {
|
|
return initialMembers.filter((m) => m.userId !== creatorUserId);
|
|
}
|
|
|
|
export function buildInitialMemberUpsertPayload(
|
|
member: InitialProjectMemberInput,
|
|
creatorUserId: number,
|
|
): UpsertProjectMemberPayload {
|
|
return {
|
|
user_id: member.userId,
|
|
project_role: toApiProjectRole(member.role, member.userId, creatorUserId),
|
|
document_duty: documentDutyFromMemberRole(member.role),
|
|
};
|
|
}
|
|
|
|
export type AddProjectMemberFn = (
|
|
projectId: number,
|
|
payload: UpsertProjectMemberPayload,
|
|
) => Promise<void>;
|
|
|
|
/**
|
|
* 逐一 upsert 非创建者;任一失败抛错(调用方保留已创建 projectId 以便重试幂等)。
|
|
* Upsert 语义下重复确认不会因已存在成员而假失败。
|
|
*/
|
|
export async function persistInitialProjectMembers(params: {
|
|
projectId: number;
|
|
creatorUserId: number;
|
|
initialMembers: InitialProjectMemberInput[];
|
|
addMember: AddProjectMemberFn;
|
|
}): Promise<{ persistedUserIds: number[] }> {
|
|
const targets = selectNonCreatorInitialMembers(params.initialMembers, params.creatorUserId);
|
|
const persistedUserIds: number[] = [];
|
|
for (const member of targets) {
|
|
const payload = buildInitialMemberUpsertPayload(member, params.creatorUserId);
|
|
await params.addMember(params.projectId, payload);
|
|
persistedUserIds.push(member.userId);
|
|
}
|
|
return { persistedUserIds };
|
|
}
|
|
|
|
export type CreateProjectEnsurePhase = 'creating' | 'syncing_members';
|
|
|
|
/**
|
|
* 若已有 bootstrapProjectId(上次 create 成功但成员失败),跳过再次 createProject,仅补成员。
|
|
*/
|
|
export async function ensureCreatedProjectWithMembers(params: {
|
|
bootstrapProjectId: number | null;
|
|
createProject: () => Promise<{ id: number }>;
|
|
creatorUserId: number;
|
|
initialMembers: InitialProjectMemberInput[];
|
|
addMember: AddProjectMemberFn;
|
|
/** 可选:驱动 UI「立项中 / 同步成员中」文案(不参与控制流)。 */
|
|
onPhase?: (phase: CreateProjectEnsurePhase) => void;
|
|
}): Promise<{ projectId: number; createdNow: boolean; persistedUserIds: number[] }> {
|
|
let projectId = params.bootstrapProjectId;
|
|
let createdNow = false;
|
|
if (projectId == null || projectId <= 0) {
|
|
params.onPhase?.('creating');
|
|
const created = await params.createProject();
|
|
projectId = created.id;
|
|
createdNow = true;
|
|
}
|
|
params.onPhase?.('syncing_members');
|
|
try {
|
|
const { persistedUserIds } = await persistInitialProjectMembers({
|
|
projectId,
|
|
creatorUserId: params.creatorUserId,
|
|
initialMembers: params.initialMembers,
|
|
addMember: params.addMember,
|
|
});
|
|
return { projectId, createdNow, persistedUserIds };
|
|
} catch (err) {
|
|
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
(wrapped as Error & { projectId?: number }).projectId = projectId;
|
|
throw wrapped;
|
|
}
|
|
}
|