限制考核调研发起人参与评分

This commit is contained in:
chenpei 2026-07-15 11:52:28 +08:00
parent 8f7ba177e3
commit 62ca61d9e2
3 changed files with 128 additions and 7 deletions

View File

@ -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({
<DepartmentPicker
departments={departments}
selected={evaluatorDepartmentValues}
excludedEmployeeIds={currentEmployee?.id ? [currentEmployee.id] : []}
onChange={setEvaluatorDepartmentValues}
onDepartmentEmployeesChange={(department, checked, employees) => {
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<Employee[]>(`/employees?${params.toString()}`)
const excludedIds = new Set(excludedEmployeeIds)
const employees = (await apiRequest<Employee[]>(`/employees?${params.toString()}`))
.filter((employee) => !excludedIds.has(employee.id))
employeeCacheRef.current.set(value, employees)
return employees
}

View File

@ -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级人员',

View File

@ -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