From 62ca61d9e25164a23e6dd899f734e53eef55da88 Mon Sep 17 00:00:00 2001 From: chenpei Date: Wed, 15 Jul 2026 11:52:28 +0800 Subject: [PATCH] =?UTF-8?q?=E9=99=90=E5=88=B6=E8=80=83=E6=A0=B8=E8=B0=83?= =?UTF-8?q?=E7=A0=94=E5=8F=91=E8=B5=B7=E4=BA=BA=E5=8F=82=E4=B8=8E=E8=AF=84?= =?UTF-8?q?=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/performance-survey/[[...path]]/page.tsx | 13 ++- .../performance-survey-service-policy.test.ts | 93 +++++++++++++++++++ lib/performance-survey-service.ts | 29 +++++- 3 files changed, 128 insertions(+), 7 deletions(-) diff --git a/app/performance-survey/[[...path]]/page.tsx b/app/performance-survey/[[...path]]/page.tsx index e2d2005..147448a 100644 --- a/app/performance-survey/[[...path]]/page.tsx +++ b/app/performance-survey/[[...path]]/page.tsx @@ -2734,8 +2734,9 @@ function CreateSurveyForm({ ? 0 : effectiveTargetEmployees.filter((employee) => employee.id !== currentEmployee?.id).length const evaluatorEmployees = useMemo( - () => mergeEmployeesById(manualEvaluatorEmployees, ...Object.values(evaluatorDepartmentEmployees)), - [manualEvaluatorEmployees, evaluatorDepartmentEmployees], + () => mergeEmployeesById(manualEvaluatorEmployees, ...Object.values(evaluatorDepartmentEmployees)) + .filter((employee) => employee.id !== currentEmployee?.id), + [currentEmployee?.id, manualEvaluatorEmployees, evaluatorDepartmentEmployees], ) const publishValidationIssues: string[] = [] if (!selectedTemplate) publishValidationIssues.push('选择考评模板') @@ -3012,6 +3013,7 @@ function CreateSurveyForm({ title={automaticWorkCooperationEvaluators ? '手动调整评分人员(可选)' : '评分人员'} purpose="evaluator" selected={evaluatorEmployees} + excludedIds={currentEmployee?.id ? [currentEmployee.id] : []} onChange={setManualEvaluatorEmployees} onAddEmployee={(employee) => setManualEvaluatorEmployees((current) => mergeEmployeesById(current, [employee]))} onRemoveEmployee={removeEvaluatorEmployee} @@ -3021,6 +3023,7 @@ function CreateSurveyForm({ { setEvaluatorDepartmentEmployees((current) => { @@ -3508,11 +3511,13 @@ function EmployeePicker({ function DepartmentPicker({ departments, selected, + excludedEmployeeIds = [], onChange, onDepartmentEmployeesChange, }: { departments: DepartmentOption[] selected: string[] + excludedEmployeeIds?: string[] onChange: (items: string[]) => void onDepartmentEmployeesChange?: (value: string, checked: boolean, employees: Employee[]) => void }) { @@ -3601,7 +3606,9 @@ function DepartmentPicker({ limit: '1000', purpose: 'evaluator', }) - const employees = await apiRequest(`/employees?${params.toString()}`) + const excludedIds = new Set(excludedEmployeeIds) + const employees = (await apiRequest(`/employees?${params.toString()}`)) + .filter((employee) => !excludedIds.has(employee.id)) employeeCacheRef.current.set(value, employees) return employees } diff --git a/lib/__tests__/performance-survey-service-policy.test.ts b/lib/__tests__/performance-survey-service-policy.test.ts index 25e0e81..511d090 100644 --- a/lib/__tests__/performance-survey-service-policy.test.ts +++ b/lib/__tests__/performance-survey-service-policy.test.ts @@ -11,6 +11,7 @@ import { getPerformanceSurveyMe, getPerformanceSurveyResults, listPerformanceSurveys, + searchPerformanceSurveyEmployees, updatePerformanceSurvey, } from '@/lib/performance-survey-service' import { prisma } from '@/lib/prisma' @@ -160,6 +161,7 @@ describe('performance survey service policies', () => { beforeEach(() => { jest.clearAllMocks() ;(prisma.user.findUnique as jest.Mock).mockResolvedValue(baseUser) + ;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockReset() ;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(baseEmployee) ;(prisma.performanceSurvey.findUnique as jest.Mock).mockResolvedValue(buildSurvey()) ;(prisma.performanceSurvey.findMany as jest.Mock).mockResolvedValue([]) @@ -168,11 +170,14 @@ describe('performance survey service policies', () => { ;(prisma.performanceSurveyResponse.findMany as jest.Mock).mockResolvedValue([]) ;(prisma.performanceSurvey.update as jest.Mock).mockResolvedValue({}) ;(prisma.performanceSurveyEmployeeProfile.findFirst as jest.Mock).mockResolvedValue(null) + ;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockReset() ;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValue([]) ;(prisma.performanceSurveyEmployeeProfile.count as jest.Mock).mockResolvedValue(0) ;(prisma.performanceSurveyTarget.findMany as jest.Mock).mockResolvedValue([]) + ;(prisma.user.findMany as jest.Mock).mockReset() ;(prisma.user.findMany as jest.Mock).mockResolvedValue([]) ;(prisma.performanceSurvey.count as jest.Mock).mockResolvedValue(0) + ;(getPerformanceSurveyTemplate as jest.Mock).mockReset() ;(getPerformanceSurveyTemplate as jest.Mock).mockResolvedValue({ code: 'course_share', name: '课程分享月度考核', @@ -613,6 +618,31 @@ describe('performance survey service policies', () => { ) }) + test('发起人不能手动选择自己作为评分人员', async () => { + await expect(createPerformanceSurvey(session, { + templateCode: 'course_share', + assessmentMonth: '2026-07', + targetEmployeeIds: ['employee-1'], + evaluatorEmployeeIds: ['employee-1'], + openNow: false, + })).rejects.toEqual( + new PerformanceSurveyApiError('发起人不能作为评分人员,请选择其他人员', 409), + ) + + expect(prisma.$transaction).not.toHaveBeenCalled() + }) + + test('评分人员搜索和部门批量加载都排除当前发起人', async () => { + await searchPerformanceSurveyEmployees(session, new URLSearchParams({ + purpose: 'evaluator', + department: '集团A/品牌中心/策划部', + limit: '1000', + })) + + const query = (prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mock.calls[0][0] + expect(query.where.AND).toContainEqual({ id: { not: 'employee-1' } }) + }) + test('创建接口拒绝直接传入组织范围外的被考核人 ID', async () => { ;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValueOnce([baseEmployee]) @@ -769,6 +799,69 @@ describe('performance survey service policies', () => { })) }) + test('自动匹配的非自评人员如果是发起人则拒绝创建', async () => { + const creator = { + ...baseEmployee, + dingtalkUserId: baseUser.dingtalkUserId, + departmentName: '品牌创意A部', + departmentPath: '集团A/品牌中心/品牌创意A部', + managerDingtalkUserId: 'ding-other-manager', + } + const target = { + ...creator, + id: 'employee-target', + displayName: '其他被考核人', + dingtalkUserId: 'ding-target', + managerDingtalkUserId: creator.dingtalkUserId, + } + const creatorManager = { + ...creator, + id: 'employee-creator-manager', + displayName: '发起人的主管', + dingtalkUserId: 'ding-other-manager', + } + ;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockReset() + ;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue(creator) + ;(getPerformanceSurveyTemplate as jest.Mock).mockResolvedValue({ + code: 'work_cooperation', + name: '工作配合度满意度调研', + shortName: '工作配合度', + surveyType: 'person_period', + weight: 0.2, + questions: [], + scoreBands: [], + }) + ;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockReset() + ;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockImplementation(async ({ where }: any) => { + const clauses = where?.AND || [] + const requestedIds = clauses.find((clause: any) => clause?.id?.in)?.id?.in + if (requestedIds) return [creator, target] + const evaluatorDingtalkUserId = clauses.find((clause: any) => clause?.dingtalkUserId)?.dingtalkUserId + if (evaluatorDingtalkUserId === creatorManager.dingtalkUserId) return [creatorManager] + if (evaluatorDingtalkUserId === creator.dingtalkUserId) return [creator] + return [] + }) + ;(prisma.user.findMany as jest.Mock).mockReset() + ;(prisma.user.findMany as jest.Mock).mockImplementation(async ({ where }: any) => { + const dingtalkUserId = where?.dingtalkUserId + if (dingtalkUserId === creatorManager.dingtalkUserId) { + return [{ id: 'user-creator-manager', dingtalkUserId }] + } + return [{ id: baseUser.id, dingtalkUserId: baseUser.dingtalkUserId }] + }) + + await expect(createPerformanceSurvey(session, { + templateCode: 'work_cooperation', + assessmentMonth: '2026-07', + targetEmployeeIds: [target.id], + openNow: false, + })).rejects.toEqual( + new PerformanceSurveyApiError('发起人不能作为评分人员,请选择其他人员', 409), + ) + + expect(prisma.$transaction).not.toHaveBeenCalled() + }) + test.each([ { label: '品牌创意A部P级人员', diff --git a/lib/performance-survey-service.ts b/lib/performance-survey-service.ts index 677025d..3bd04a2 100644 --- a/lib/performance-survey-service.ts +++ b/lib/performance-survey-service.ts @@ -963,6 +963,9 @@ export async function searchPerformanceSurveyEmployees(session: Session | null, if (!ctx.isSystemAdmin && !isEvaluatorSearch) { and.push(getContextScopeDepartmentWhere(ctx) || { id: '__none__' }) } + if (isEvaluatorSearch && ctx.employee?.id) { + and.push({ id: { not: ctx.employee.id } }) + } if (includeKeyword && search) { and.push({ OR: [ { displayName: { contains: search, mode: 'insensitive' } }, @@ -2083,7 +2086,11 @@ export async function createPerformanceSurvey(session: Session | null, body: any } catch (error) { throw new PerformanceSurveyApiError(error instanceof Error ? error.message : '考评模板版本无效') } - const hasRequestedEvaluators = normalizeList(body?.evaluatorEmployeeIds).length > 0 + const requestedEvaluatorEmployeeIds = normalizeList(body?.evaluatorEmployeeIds) + if (ctx.employee?.id && requestedEvaluatorEmployeeIds.includes(ctx.employee.id)) { + throw new PerformanceSurveyApiError('发起人不能作为评分人员,请选择其他人员', 409) + } + const hasRequestedEvaluators = requestedEvaluatorEmployeeIds.length > 0 || normalizeList(body?.evaluatorDepartmentNames).length > 0 const useDefaultWorkCooperationEvaluators = usesAutomaticWorkCooperationEvaluators( template.code, @@ -2158,6 +2165,19 @@ export async function createPerformanceSurvey(session: Session | null, body: any ? await resolveWorkCooperationEvaluators(targetSource?.employee || target.targetSnapshot) : evaluators for (const evaluator of targetEvaluators) { + const evaluatorDingtalkUserId = clean(evaluator.dingtalkUserId) + || clean(evaluator.employee?.dingtalkUserId) + const isCreatorEvaluator = Boolean( + (ctx.employee?.id && evaluator.employee?.id === ctx.employee.id) + || (evaluator.userId && evaluator.userId === ctx.user.id) + || ( + ctx.user.dingtalkUserId + && evaluatorDingtalkUserId === clean(ctx.user.dingtalkUserId) + ), + ) + if (isCreatorEvaluator && !isSelfWorkflowRole(evaluator.workflowRole || null)) { + throw new PerformanceSurveyApiError('发起人不能作为评分人员,请选择其他人员', 409) + } if ( surveyKind !== 'item_course' && target.employeeProfileId && @@ -3495,14 +3515,15 @@ async function resolveSurveyEvaluators(body: any, ctx: UserContext) { or.push(...getDepartmentSelectionWhere(evaluatorDepartmentNames)) } - if (or.length === 0 && ctx.employee) { - or.push({ id: ctx.employee.id }) - } + if (or.length === 0) return [] const and: any[] = [ await getPerformanceSurveyAccountProfileWhere(), { OR: or }, ] + if (ctx.employee?.id) { + and.push({ id: { not: ctx.employee.id } }) + } const where: any = { AND: and } const employees = or.length > 0