R75-A4 新增自动化补盲用例
这个提交包含在:
@@ -10,6 +10,7 @@
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:smoke": "playwright test tests/e2e/login-portal-ux.spec.ts tests/e2e/wecom-entry.spec.ts",
|
||||
"test:e2e:full": "playwright test",
|
||||
"test:e2e:r75-gap-fill": "playwright test tests/e2e/r75-automation-gap-fill.spec.ts --workers=1 --reporter=list",
|
||||
"test:e2e:r70-assist": "R70_TRIALRUN_ASSIST=1 playwright test tests/e2e/manual/r70-trialrun-assist.spec.ts --workers=1 --reporter=list",
|
||||
"test:e2e:r70-deep-assist": "PLAYWRIGHT_MANUAL=1 R70_TRIALRUN_DEEP_ASSIST=1 playwright test tests/e2e/manual/r70-trialrun-deep-assist.spec.ts --workers=1 --reporter=list",
|
||||
"test:e2e:live-ocr-batch": "PLAYWRIGHT_MANUAL=1 PLAYWRIGHT_OCR_LIVE=1 PLAYWRIGHT_OCR_LIVE_BATCH=1 playwright test tests/e2e/manual/live-invoice-ocr-batch.spec.ts --workers=1 --reporter=list",
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
import { type APIResponse } from "@playwright/test";
|
||||
|
||||
import { expect, test } from "./helpers/fixtures";
|
||||
import {
|
||||
ADMIN,
|
||||
EMPLOYEE,
|
||||
FINANCE,
|
||||
MANAGER,
|
||||
apiGetData,
|
||||
apiPostData,
|
||||
createAuthenticatedApiClient,
|
||||
login,
|
||||
logout,
|
||||
} from "./helpers/auth";
|
||||
|
||||
type FeatureFlagItem = {
|
||||
flagKey: string;
|
||||
overrideMode: "inherit" | "force_on" | "force_off";
|
||||
rolloutType: "global" | "department_allowlist";
|
||||
departmentIds: string[];
|
||||
note: string;
|
||||
version: number;
|
||||
effectiveEnabled: boolean;
|
||||
};
|
||||
|
||||
type AttendanceTodayResponse = {
|
||||
workDate: string;
|
||||
needClockIn: boolean;
|
||||
exemptionReason: string | null;
|
||||
item: {
|
||||
clockInAt: string | null;
|
||||
clockOutAt: string | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type AttendanceTeamItem = {
|
||||
employeeId: string;
|
||||
employeeName: string;
|
||||
employeeNo: string;
|
||||
departmentId: string | null;
|
||||
attendanceStatus: string;
|
||||
needClockIn: boolean;
|
||||
clockInAt: string | null;
|
||||
clockOutAt: string | null;
|
||||
};
|
||||
|
||||
type NotificationListResponse = {
|
||||
unreadCount: number;
|
||||
items: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
body: string | null;
|
||||
targetType: string | null;
|
||||
targetId: string | null;
|
||||
readAt: string | null;
|
||||
createdAt: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
type DepartmentListResponse = {
|
||||
items: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
type EmployeeSummary = {
|
||||
id: string;
|
||||
employeeNo: string;
|
||||
name: string;
|
||||
email: string;
|
||||
employmentStatus: string | null;
|
||||
status: string;
|
||||
};
|
||||
|
||||
type ExpenseTypeListResponse = {
|
||||
items: Array<{
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
type InvoiceUploadResponse = {
|
||||
invoicePoolId: string;
|
||||
status: string;
|
||||
fileSha256: string;
|
||||
};
|
||||
|
||||
type InvoiceDetailResponse = {
|
||||
id: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
type SubmittedReimbursementSeed = {
|
||||
reimbursementId: string;
|
||||
formNumber: string;
|
||||
currentTaskId: string;
|
||||
};
|
||||
|
||||
type AuthedApi = Awaited<ReturnType<typeof createAuthenticatedApiClient>>["api"];
|
||||
|
||||
function formatDate(offsetDays: number) {
|
||||
const value = new Date();
|
||||
value.setUTCDate(value.getUTCDate() + offsetDays);
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function toDateTimeLocal(date: string, time: string) {
|
||||
return `${date}T${time}`;
|
||||
}
|
||||
|
||||
async function readApiData<T>(response: APIResponse) {
|
||||
const payload = (await response.json()) as { data?: T; error?: { code?: string; message?: string } };
|
||||
if (!response.ok()) {
|
||||
throw new Error(`${payload.error?.code ?? response.status()}: ${payload.error?.message ?? "request_failed"}`);
|
||||
}
|
||||
return payload.data as T;
|
||||
}
|
||||
|
||||
async function ensureFeatureFlags(api: Awaited<ReturnType<typeof createAuthenticatedApiClient>>["api"], keys: string[]) {
|
||||
const list = await apiGetData<{ items: FeatureFlagItem[] }>(api, "/api/system/feature-flags");
|
||||
for (const key of keys) {
|
||||
const item = list.items.find((flag) => flag.flagKey === key);
|
||||
if (!item) {
|
||||
throw new Error(`Missing feature flag: ${key}`);
|
||||
}
|
||||
if (item.overrideMode === "force_on" && item.effectiveEnabled) {
|
||||
continue;
|
||||
}
|
||||
await readApiData<{ item: FeatureFlagItem }>(
|
||||
await api.put(`/api/system/feature-flags/${key}`, {
|
||||
data: {
|
||||
overrideMode: "force_on",
|
||||
rolloutType: "global",
|
||||
departmentIds: [],
|
||||
note: "playwright r75",
|
||||
expectedVersion: item.version,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function findBackfillTarget(api: Awaited<ReturnType<typeof createAuthenticatedApiClient>>["api"]) {
|
||||
const offsets = [0, -1, -2, -3, -4, -5, -6, -7, 1, 2, 3, 4, 5];
|
||||
for (const offset of offsets) {
|
||||
const date = formatDate(offset);
|
||||
const payload = await apiGetData<{ date: string; items: AttendanceTeamItem[] }>(
|
||||
api,
|
||||
`/api/attendance/team?date=${date}`,
|
||||
);
|
||||
const target = payload.items.find((item) => item.needClockIn && (!item.clockInAt || !item.clockOutAt));
|
||||
if (target) {
|
||||
return { date, item: target };
|
||||
}
|
||||
}
|
||||
throw new Error("未找到可用于团队补卡的考勤缺口样本");
|
||||
}
|
||||
|
||||
async function createSubmittedReimbursement(api: AuthedApi, label: string): Promise<SubmittedReimbursementSeed> {
|
||||
const reimbursementDate = formatDate(30 + Math.floor(Math.random() * 45));
|
||||
const description = `Playwright 通知补盲 ${label}`;
|
||||
const created = await apiPostData<{ id: string }>(api, "/api/reimbursements", {
|
||||
reimbursementDate,
|
||||
description,
|
||||
});
|
||||
|
||||
const expenseTypes = await apiGetData<ExpenseTypeListResponse>(api, "/api/expense-types?enabled=true");
|
||||
const expenseTypeId = expenseTypes.items[0]?.id;
|
||||
if (!expenseTypeId) {
|
||||
throw new Error("未找到可用报销费用类型");
|
||||
}
|
||||
|
||||
await apiPostData(api, `/api/reimbursements/${created.id}/items`, {
|
||||
expenseTypeId,
|
||||
expenseDate: reimbursementDate,
|
||||
amount: "1683.00",
|
||||
description,
|
||||
});
|
||||
|
||||
const upload = await readApiData<InvoiceUploadResponse>(
|
||||
await api.post("/api/invoice-pool/upload", {
|
||||
multipart: {
|
||||
file: {
|
||||
name: `notify-${label}.png`,
|
||||
mimeType: "image/png",
|
||||
buffer: Buffer.from(`PW_R75_NOTIFY_${label}_${Date.now()}_${Math.random().toString(16).slice(2)}`, "utf8"),
|
||||
},
|
||||
ocr_type: "general",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const invoice = await apiGetData<InvoiceDetailResponse>(api, `/api/invoice-pool/${upload.invoicePoolId}`);
|
||||
return invoice.status;
|
||||
},
|
||||
{
|
||||
message: `等待票据 ${upload.invoicePoolId} 进入可绑定状态`,
|
||||
timeout: 90_000,
|
||||
},
|
||||
)
|
||||
.toBe("available");
|
||||
|
||||
await apiPostData(api, `/api/invoice-pool/${upload.invoicePoolId}/bind`, {
|
||||
reimbursementId: created.id,
|
||||
});
|
||||
|
||||
const submitted = await apiPostData<SubmittedReimbursementSeed>(api, `/api/reimbursements/${created.id}/submit`, {});
|
||||
return {
|
||||
reimbursementId: submitted.reimbursementId,
|
||||
formNumber: submitted.formNumber,
|
||||
currentTaskId: submitted.currentTaskId,
|
||||
};
|
||||
}
|
||||
|
||||
async function settleWorkflowTaskChain(managerApi: AuthedApi, financeApi: AuthedApi, taskId: string | null) {
|
||||
if (!taskId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const approved = await apiPostData<{ nextTaskId: string | null }>(managerApi, `/api/workflow/tasks/${taskId}/approve`, {});
|
||||
if (approved.nextTaskId) {
|
||||
await apiPostData(financeApi, `/api/workflow/tasks/${approved.nextTaskId}/approve`, {}).catch(() => undefined);
|
||||
}
|
||||
} catch {
|
||||
// 清理阶段不覆盖主断言结果。
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEmployeeByKeyword(
|
||||
api: Awaited<ReturnType<typeof createAuthenticatedApiClient>>["api"],
|
||||
keyword: string,
|
||||
) {
|
||||
const encoded = encodeURIComponent(keyword);
|
||||
const payload = await apiGetData<{ items: EmployeeSummary[] }>(
|
||||
api,
|
||||
`/api/employees?page=1&pageSize=20&q=${encoded}`,
|
||||
);
|
||||
return payload.items.find((item) => item.employeeNo === keyword || item.email === keyword) ?? null;
|
||||
}
|
||||
|
||||
test.describe("R75 自动化补盲", () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test("E2E R75 考勤补盲:员工今日状态 + 管理员团队补卡", async ({ page }) => {
|
||||
test.setTimeout(240_000);
|
||||
|
||||
const adminClient = await createAuthenticatedApiClient(ADMIN);
|
||||
const employeeClient = await createAuthenticatedApiClient(EMPLOYEE);
|
||||
|
||||
try {
|
||||
await ensureFeatureFlags(adminClient.api, ["attendance_v1"]);
|
||||
const backfillTarget = await findBackfillTarget(adminClient.api);
|
||||
|
||||
await login(page, EMPLOYEE);
|
||||
await page.goto("/attendance");
|
||||
await expect(page.getByRole("heading", { name: "考勤" })).toBeVisible();
|
||||
|
||||
const today = await apiGetData<AttendanceTodayResponse>(employeeClient.api, "/api/attendance/me/today");
|
||||
if (today.needClockIn) {
|
||||
if (!today.item?.clockInAt) {
|
||||
await page.getByRole("button", { name: "点击打上班卡" }).click();
|
||||
await expect(page.getByText("上班打卡成功")).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const latest = await apiGetData<AttendanceTodayResponse>(employeeClient.api, "/api/attendance/me/today");
|
||||
return Boolean(latest.item?.clockInAt);
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
await page.getByRole("button", { name: /点击下班|重新打下班卡/ }).click();
|
||||
await expect(page.getByText("下班打卡成功")).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const latest = await apiGetData<AttendanceTodayResponse>(employeeClient.api, "/api/attendance/me/today");
|
||||
return Boolean(latest.item?.clockOutAt);
|
||||
})
|
||||
.toBe(true);
|
||||
} else {
|
||||
await page.getByRole("button", { name: today.item.clockOutAt ? "重新打下班卡" : "点击下班" }).click();
|
||||
await expect(page.getByText("下班打卡成功")).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const latest = await apiGetData<AttendanceTodayResponse>(employeeClient.api, "/api/attendance/me/today");
|
||||
return Boolean(latest.item?.clockOutAt);
|
||||
})
|
||||
.toBe(true);
|
||||
}
|
||||
} else {
|
||||
await expect(page.getByText(/免打卡:/)).toBeVisible();
|
||||
}
|
||||
|
||||
await expect(page.getByText("本月打卡记录")).toBeVisible();
|
||||
await expect(page.getByText(today.workDate).first()).toBeVisible();
|
||||
|
||||
await logout(page, EMPLOYEE.name);
|
||||
|
||||
await login(page, ADMIN);
|
||||
await page.goto("/attendance");
|
||||
await page.getByRole("tab", { name: "团队考勤" }).click();
|
||||
await expect(page.getByText("查看指定日期团队上下班打卡状态")).toBeVisible();
|
||||
|
||||
await page.locator('input[type="date"]').fill(backfillTarget.date);
|
||||
|
||||
const targetRow = page.getByRole("row").filter({ hasText: backfillTarget.item.employeeNo }).first();
|
||||
await expect(targetRow).toBeVisible();
|
||||
await targetRow.getByRole("button", { name: "补卡" }).click();
|
||||
|
||||
await page.getByLabel("上班时间").fill(toDateTimeLocal(backfillTarget.date, "09:00"));
|
||||
await page.getByLabel("下班时间").fill(toDateTimeLocal(backfillTarget.date, "18:00"));
|
||||
await page.getByLabel("备注").fill("Playwright R75 团队补卡");
|
||||
await page.getByRole("button", { name: "保存补卡" }).click();
|
||||
await expect(page.getByText("补卡已保存")).toBeVisible();
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const payload = await apiGetData<{ items: AttendanceTeamItem[] }>(
|
||||
adminClient.api,
|
||||
`/api/attendance/team?date=${backfillTarget.date}`,
|
||||
);
|
||||
const item = payload.items.find((entry) => entry.employeeId === backfillTarget.item.employeeId);
|
||||
return Boolean(item?.clockInAt && item?.clockOutAt);
|
||||
})
|
||||
.toBe(true);
|
||||
} finally {
|
||||
await Promise.all([employeeClient.dispose(), adminClient.dispose()]);
|
||||
}
|
||||
});
|
||||
|
||||
test("E2E R75 通知补盲:未读筛选、单条已读、全部已读与跳转", async ({ page }) => {
|
||||
test.setTimeout(240_000);
|
||||
|
||||
const employeeClient = await createAuthenticatedApiClient(EMPLOYEE);
|
||||
const managerClient = await createAuthenticatedApiClient(MANAGER);
|
||||
const financeClient = await createAuthenticatedApiClient(FINANCE);
|
||||
const suffix = Date.now();
|
||||
let firstTaskId: string | null = null;
|
||||
let secondTaskId: string | null = null;
|
||||
|
||||
try {
|
||||
await apiPostData<{ updatedCount: number }>(managerClient.api, "/api/notifications/mark-all-read", {});
|
||||
|
||||
const firstReimbursement = await createSubmittedReimbursement(employeeClient.api, `A-${suffix}`);
|
||||
const secondReimbursement = await createSubmittedReimbursement(employeeClient.api, `B-${suffix}`);
|
||||
firstTaskId = firstReimbursement.currentTaskId;
|
||||
secondTaskId = secondReimbursement.currentTaskId;
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const payload = await apiGetData<NotificationListResponse>(
|
||||
managerClient.api,
|
||||
"/api/notifications?unreadOnly=1&type=workflow.task_assigned&page=1&pageSize=20",
|
||||
);
|
||||
return payload.items.filter(
|
||||
(item) =>
|
||||
item.title.includes(firstReimbursement.formNumber) ||
|
||||
item.body?.includes(firstReimbursement.formNumber) ||
|
||||
item.title.includes(secondReimbursement.formNumber) ||
|
||||
item.body?.includes(secondReimbursement.formNumber),
|
||||
).length;
|
||||
},
|
||||
{
|
||||
timeout: 60_000,
|
||||
},
|
||||
)
|
||||
.toBeGreaterThanOrEqual(2);
|
||||
|
||||
await login(page, MANAGER);
|
||||
await page.goto("/notifications?unreadOnly=1&type=workflow.task_assigned&page=1");
|
||||
await expect(page.getByRole("heading", { name: "通知" })).toBeVisible();
|
||||
|
||||
const firstRow = page.getByTestId("notification-row").filter({ hasText: firstReimbursement.formNumber }).first();
|
||||
const secondRow = page.getByTestId("notification-row").filter({ hasText: secondReimbursement.formNumber }).first();
|
||||
|
||||
await expect(firstRow).toContainText("未读");
|
||||
await expect(secondRow).toContainText("未读");
|
||||
|
||||
await firstRow.getByTestId("notification-target-link").click();
|
||||
await expect(page).toHaveURL(/\/workflow\/tasks\/[0-9a-f-]+/);
|
||||
|
||||
await page.goto("/notifications?unreadOnly=1&type=workflow.task_assigned&page=1");
|
||||
await expect(firstRow).toBeVisible();
|
||||
await firstRow.getByTestId("notification-mark-read").click();
|
||||
await expect(page.getByTestId("notification-row").filter({ hasText: firstReimbursement.formNumber })).toHaveCount(0);
|
||||
|
||||
await page.getByTestId("notifications-mark-all-read").click();
|
||||
await expect(page.getByText("已全部标为已读")).toBeVisible();
|
||||
await expect(page.getByTestId("notification-row").filter({ hasText: secondReimbursement.formNumber })).toHaveCount(0);
|
||||
|
||||
await page.getByTestId("notifications-filter-all").click();
|
||||
await expect(page.getByTestId("notification-row").filter({ hasText: secondReimbursement.formNumber }).first()).toContainText("已读");
|
||||
} finally {
|
||||
await settleWorkflowTaskChain(managerClient.api, financeClient.api, firstTaskId);
|
||||
await settleWorkflowTaskChain(managerClient.api, financeClient.api, secondTaskId);
|
||||
await Promise.all([employeeClient.dispose(), managerClient.dispose(), financeClient.dispose()]);
|
||||
}
|
||||
});
|
||||
|
||||
test("E2E R75 生命周期补盲:创建临时员工后发起入职并转正", async ({ page }) => {
|
||||
test.setTimeout(240_000);
|
||||
|
||||
const adminClient = await createAuthenticatedApiClient(ADMIN);
|
||||
const suffix = Date.now();
|
||||
const employeeNo = `PW-R75-${suffix}`;
|
||||
const email = `pw-r75-${suffix}@hyinda.com`;
|
||||
const phone = `139${String(suffix).slice(-8)}`;
|
||||
let createdEmployeeId: string | null = null;
|
||||
|
||||
try {
|
||||
await ensureFeatureFlags(adminClient.api, ["employee_lifecycle_v1"]);
|
||||
const departments = await apiGetData<DepartmentListResponse>(adminClient.api, "/api/departments?includeArchived=0");
|
||||
const departmentId = departments.items[0]?.id;
|
||||
if (!departmentId) {
|
||||
throw new Error("未找到可用部门,无法创建生命周期测试员工");
|
||||
}
|
||||
|
||||
const created = await apiPostData<EmployeeSummary>(adminClient.api, "/api/users", {
|
||||
employeeNo,
|
||||
name: `Playwright 生命周期 ${suffix}`,
|
||||
phone,
|
||||
email,
|
||||
password: "PlaywrightTemp123!",
|
||||
departmentId,
|
||||
roles: ["employee"],
|
||||
});
|
||||
createdEmployeeId = created.id;
|
||||
|
||||
await login(page, ADMIN);
|
||||
await page.goto("/people");
|
||||
await expect(page.getByRole("heading", { name: "人员管理" })).toBeVisible();
|
||||
await page.getByPlaceholder("搜索姓名、工号、手机号、邮箱").fill(employeeNo);
|
||||
|
||||
const row = page.getByRole("row").filter({ hasText: employeeNo }).first();
|
||||
await expect(row).toBeVisible();
|
||||
await row.getByRole("button", { name: "发起入职" }).click();
|
||||
await expect(page.getByText("已发起入职流程")).toBeVisible();
|
||||
await expect(row).toContainText("待入职");
|
||||
|
||||
await row.getByRole("button", { name: "转正" }).click();
|
||||
await expect(page.getByText("已完成转正")).toBeVisible();
|
||||
await expect(row).toContainText("在职");
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const employee = await fetchEmployeeByKeyword(adminClient.api, employeeNo);
|
||||
return employee?.employmentStatus ?? null;
|
||||
})
|
||||
.toBe("active");
|
||||
} finally {
|
||||
if (createdEmployeeId) {
|
||||
await apiPostData<{ id: string; status: string }>(adminClient.api, `/api/users/${createdEmployeeId}/archive`, {});
|
||||
}
|
||||
await adminClient.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test("E2E R75 治理补盲:功能开关备注可保存并恢复", async ({ page }) => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
const adminClient = await createAuthenticatedApiClient(ADMIN);
|
||||
const flagKey = "ai_assist";
|
||||
const updatedNote = `Playwright R75 配置写回 ${Date.now()}`;
|
||||
|
||||
let originalFlag: FeatureFlagItem | null = null;
|
||||
|
||||
try {
|
||||
const list = await apiGetData<{ items: FeatureFlagItem[] }>(adminClient.api, "/api/system/feature-flags");
|
||||
originalFlag = list.items.find((item) => item.flagKey === flagKey) ?? null;
|
||||
if (!originalFlag) {
|
||||
throw new Error(`未找到功能开关 ${flagKey}`);
|
||||
}
|
||||
|
||||
await login(page, ADMIN);
|
||||
await page.goto("/system/feature-flags");
|
||||
await expect(page.getByRole("heading", { name: "功能开关" })).toBeVisible();
|
||||
|
||||
const row = page.getByTestId(`feature-flag-row-${flagKey}`);
|
||||
await expect(row).toBeVisible();
|
||||
await row.locator("textarea").fill(updatedNote);
|
||||
|
||||
await row.getByTestId(`feature-flag-save-${flagKey}`).click();
|
||||
await expect(page.getByText("开关已更新")).toBeVisible();
|
||||
await expect(row.locator("textarea")).toHaveValue(updatedNote);
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const latest = await apiGetData<{ items: FeatureFlagItem[] }>(adminClient.api, "/api/system/feature-flags");
|
||||
return latest.items.find((item) => item.flagKey === flagKey)?.note ?? "";
|
||||
})
|
||||
.toBe(updatedNote);
|
||||
} finally {
|
||||
if (originalFlag) {
|
||||
const latest = await apiGetData<{ items: FeatureFlagItem[] }>(adminClient.api, "/api/system/feature-flags");
|
||||
const current = latest.items.find((item) => item.flagKey === flagKey);
|
||||
if (current) {
|
||||
await readApiData<{ item: FeatureFlagItem }>(
|
||||
await adminClient.api.put(`/api/system/feature-flags/${flagKey}`, {
|
||||
data: {
|
||||
overrideMode: originalFlag.overrideMode,
|
||||
rolloutType: originalFlag.rolloutType,
|
||||
departmentIds: originalFlag.departmentIds,
|
||||
note: originalFlag.note,
|
||||
expectedVersion: current.version,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
await adminClient.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户