fix: add temporary single-review test flow
This commit is contained in:
parent
50ea94ce3c
commit
878906c52f
@ -319,7 +319,7 @@ export default function WorkReviewBoard() {
|
||||
<div>
|
||||
<h1 className="text-[24px] font-bold tracking-tight text-[#101828]">作品与评审记录</h1>
|
||||
<p className="mt-1.5 text-[13px] font-medium text-[#667085]">
|
||||
同一作品的所有提交版本与双评审意见会归档在一张卡片中
|
||||
同一作品的所有提交版本与评审意见会归档在一张卡片中
|
||||
</p>
|
||||
</div>
|
||||
{data && (
|
||||
@ -443,7 +443,7 @@ export default function WorkReviewBoard() {
|
||||
{debouncedQuery || statusFilter ? '没有符合条件的作品' : '还没有作品记录'}
|
||||
</h2>
|
||||
<p className="mt-1.5 text-[12px] font-medium text-[#7b8494]">
|
||||
{debouncedQuery || statusFilter ? '可以调整搜索词或状态筛选' : canSubmit ? '提交第一份作品,开始双评审流程' : '有作品分配给你后会显示在这里'}
|
||||
{debouncedQuery || statusFilter ? '可以调整搜索词或状态筛选' : canSubmit ? '提交第一份作品,开始评审流程' : '有作品分配给你后会显示在这里'}
|
||||
</p>
|
||||
{(debouncedQuery || statusFilter) ? (
|
||||
<button
|
||||
|
||||
@ -402,7 +402,9 @@ export function WorkReviewSubmissionDialog({
|
||||
</div>
|
||||
</dl>
|
||||
<div className="rounded-lg border border-[#f3d29b] bg-[#fffaf0] px-4 py-3 text-[12px] font-medium leading-5 text-[#8a5200]">
|
||||
请确认已处理 V{project?.latestVersion || 1} 中的评审建议。提交后,原评审人与元泽会收到 V{nextVersion} 的新一轮评审通知。
|
||||
请确认已处理 V{project?.latestVersion || 1} 中的评审建议。提交后,{project?.reviewMode === 'single_reviewer_test'
|
||||
? '原评审人会收到'
|
||||
: '原评审人与元泽会收到'} V{nextVersion} 的新一轮评审通知。
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -49,10 +49,20 @@ function getVersionReview(version: WorkReviewVersion, role: WorkReviewRole) {
|
||||
return version.reviews.find((review) => review.reviewerRole === role) || null
|
||||
}
|
||||
|
||||
function getVersionAnnotations(version: WorkReviewVersion): WorkReviewDisplayAnnotation[][] {
|
||||
function getRequiredReviewRoles(project: WorkReviewProject): WorkReviewRole[] {
|
||||
return project.requiredReviewRoles?.length
|
||||
? project.requiredReviewRoles
|
||||
: ['manager', 'final_reviewer']
|
||||
}
|
||||
|
||||
function getVersionAnnotations(
|
||||
version: WorkReviewVersion,
|
||||
requiredRoles: WorkReviewRole[],
|
||||
): WorkReviewDisplayAnnotation[][] {
|
||||
const imageCount = version.imageGroups?.length || version.imageUrls?.length || 0
|
||||
const result = Array.from({ length: imageCount }, () => [] as WorkReviewDisplayAnnotation[])
|
||||
for (const review of version.reviews || []) {
|
||||
if (!requiredRoles.includes(review.reviewerRole)) continue
|
||||
for (const imageComment of review.imageComments || []) {
|
||||
if (!result[imageComment.imageIndex]) continue
|
||||
result[imageComment.imageIndex].push(...(imageComment.annotations || []).map((annotation, index) => ({
|
||||
@ -73,10 +83,12 @@ function ReviewProgress({
|
||||
project: WorkReviewProject
|
||||
version: WorkReviewVersion
|
||||
}) {
|
||||
const roles: Array<{ role: WorkReviewRole; label: string; name: string }> = [
|
||||
const requiredRoles = getRequiredReviewRoles(project)
|
||||
const allRoles: Array<{ role: WorkReviewRole; label: string; name: string }> = [
|
||||
{ role: 'manager', label: '评审人', name: project.manager.name },
|
||||
{ role: 'final_reviewer', label: '元泽', name: project.finalReviewer.name },
|
||||
]
|
||||
const roles = allRoles.filter((item) => requiredRoles.includes(item.role))
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5">
|
||||
@ -121,9 +133,11 @@ export function WorkReviewProjectCard({
|
||||
)
|
||||
const cover = latestVersion?.imageGroups?.[0]?.previewUrl || latestVersion?.imageUrls?.[0] || ''
|
||||
const isLatestSelected = selectedVersion?.id === latestVersion?.id
|
||||
const requiredReviewRoles = getRequiredReviewRoles(project)
|
||||
const singleReviewerTest = project.reviewMode === 'single_reviewer_test'
|
||||
const annotationsByImageIndex = useMemo(
|
||||
() => selectedVersion ? getVersionAnnotations(selectedVersion) : [],
|
||||
[selectedVersion],
|
||||
() => selectedVersion ? getVersionAnnotations(selectedVersion, requiredReviewRoles) : [],
|
||||
[requiredReviewRoles, selectedVersion],
|
||||
)
|
||||
|
||||
if (!latestVersion || !selectedVersion) return null
|
||||
@ -270,30 +284,34 @@ export function WorkReviewProjectCard({
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<h3 className="text-[15px] font-bold text-[#101828]">双评审意见</h3>
|
||||
<h3 className="text-[15px] font-bold text-[#101828]">{singleReviewerTest ? '评审意见' : '双评审意见'}</h3>
|
||||
<ReviewProgress project={project} version={selectedVersion} />
|
||||
</div>
|
||||
<div className="grid gap-3 xl:grid-cols-2">
|
||||
<ReviewPanel
|
||||
label="指定评审人评审"
|
||||
reviewerName={project.manager.name}
|
||||
review={getVersionReview(selectedVersion, 'manager')}
|
||||
canReview={Boolean(
|
||||
isLatestSelected
|
||||
&& project.permissions.pendingReviewRoles.includes('manager')
|
||||
)}
|
||||
onReview={onReview}
|
||||
/>
|
||||
<ReviewPanel
|
||||
label="元泽评审"
|
||||
reviewerName={project.finalReviewer.name}
|
||||
review={getVersionReview(selectedVersion, 'final_reviewer')}
|
||||
canReview={Boolean(
|
||||
isLatestSelected
|
||||
&& project.permissions.pendingReviewRoles.includes('final_reviewer')
|
||||
)}
|
||||
onReview={onReview}
|
||||
/>
|
||||
<div className={`grid gap-3 ${singleReviewerTest ? '' : 'xl:grid-cols-2'}`}>
|
||||
{requiredReviewRoles.includes('manager') && (
|
||||
<ReviewPanel
|
||||
label="指定评审人评审"
|
||||
reviewerName={project.manager.name}
|
||||
review={getVersionReview(selectedVersion, 'manager')}
|
||||
canReview={Boolean(
|
||||
isLatestSelected
|
||||
&& project.permissions.pendingReviewRoles.includes('manager')
|
||||
)}
|
||||
onReview={onReview}
|
||||
/>
|
||||
)}
|
||||
{requiredReviewRoles.includes('final_reviewer') && (
|
||||
<ReviewPanel
|
||||
label="元泽评审"
|
||||
reviewerName={project.finalReviewer.name}
|
||||
review={getVersionReview(selectedVersion, 'final_reviewer')}
|
||||
canReview={Boolean(
|
||||
isLatestSelected
|
||||
&& project.permissions.pendingReviewRoles.includes('final_reviewer')
|
||||
)}
|
||||
onReview={onReview}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -7,6 +7,7 @@ import type {
|
||||
WorkReviewImageGroup,
|
||||
WorkReviewProject,
|
||||
WorkReviewReviewInput,
|
||||
WorkReviewRole,
|
||||
WorkReviewVersion,
|
||||
} from '@/types/work-review'
|
||||
import {
|
||||
@ -404,6 +405,13 @@ export function WorkReviewReviewerWorkspace({
|
||||
if (!selectedVersion || !latestVersion) return null
|
||||
|
||||
const isLatestSelected = selectedVersion.id === latestVersion.id
|
||||
const requiredReviewRoles: WorkReviewRole[] = project.requiredReviewRoles?.length
|
||||
? project.requiredReviewRoles
|
||||
: ['manager', 'final_reviewer']
|
||||
const singleReviewerTest = project.reviewMode === 'single_reviewer_test'
|
||||
const relevantReviews = selectedVersion.reviews.filter((review) => (
|
||||
requiredReviewRoles.includes(review.reviewerRole)
|
||||
))
|
||||
const pendingRole = isLatestSelected ? project.permissions.pendingReviewRoles[0] : undefined
|
||||
const currentDrafts = draftAnnotations[activeWorkImageIndex] || []
|
||||
const selectedDraft = selectedAnnotation
|
||||
@ -413,13 +421,13 @@ export function WorkReviewReviewerWorkspace({
|
||||
? draftAnnotations[positionEditableAnnotation.imageIndex]?.find((item) => item.id === positionEditableAnnotation.id)
|
||||
: undefined
|
||||
const totalDraftCount = draftAnnotations.reduce((sum, annotations) => sum + annotations.length, 0)
|
||||
const totalSubmittedAnnotationCount = selectedVersion.reviews.reduce((reviewSum, review) => (
|
||||
const totalSubmittedAnnotationCount = relevantReviews.reduce((reviewSum, review) => (
|
||||
reviewSum + (review.imageComments || []).reduce((imageSum, imageComment) => (
|
||||
imageSum + (imageComment.annotations?.length || 0)
|
||||
), 0)
|
||||
), 0)
|
||||
const totalAnnotationCount = totalSubmittedAnnotationCount + totalDraftCount
|
||||
const submittedAnnotations: WorkReviewDisplayAnnotation[] = selectedVersion.reviews.flatMap((review) => {
|
||||
const submittedAnnotations: WorkReviewDisplayAnnotation[] = relevantReviews.flatMap((review) => {
|
||||
const imageComment = (review.imageComments || []).find((item) => item.imageIndex === activeWorkImageIndex)
|
||||
return (imageComment?.annotations || []).map((annotation, index) => ({
|
||||
annotation,
|
||||
@ -627,6 +635,11 @@ export function WorkReviewReviewerWorkspace({
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-col">
|
||||
{singleReviewerTest && (
|
||||
<div className="mb-3 rounded border border-[#f3d29b] bg-[#fffaf0] px-3 py-2 text-[12px] font-semibold leading-5 text-[#8a5200]" role="status">
|
||||
测试模式:当前作品由指定评审人单独完成评审,已跳过元泽。
|
||||
</div>
|
||||
)}
|
||||
<nav
|
||||
className="mb-3 flex min-h-12 items-center gap-2 overflow-x-auto border-b border-[#dfe5ee] bg-white px-1 pb-2"
|
||||
aria-label="选择作品版本"
|
||||
@ -941,7 +954,9 @@ export function WorkReviewReviewerWorkspace({
|
||||
? '当前版本需要修改,请通过顶部“提交修改”上传新版本'
|
||||
: project.status === 'approved'
|
||||
? '作品评审已通过,本作品已完结'
|
||||
: '你的本轮评审已提交;两位评审完成后系统会通知发起人'}
|
||||
: singleReviewerTest
|
||||
? '当前测试流程等待指定评审人完成'
|
||||
: '你的本轮评审已提交;两位评审完成后系统会通知发起人'}
|
||||
</div>
|
||||
)}
|
||||
<div className="sr-only" aria-live="polite">{liveMessage}</div>
|
||||
|
||||
@ -100,6 +100,15 @@ describe('work review policy', () => {
|
||||
])).toBe('revision_required')
|
||||
})
|
||||
|
||||
test('completes a manager-only test round immediately after the manager review', () => {
|
||||
expect(deriveCompletedWorkReviewStatus([
|
||||
{ reviewerRole: 'manager', decision: 'approved' },
|
||||
], ['manager'])).toBe('approved')
|
||||
expect(deriveCompletedWorkReviewStatus([
|
||||
{ reviewerRole: 'manager', decision: 'revision_required' },
|
||||
], ['manager'])).toBe('revision_required')
|
||||
})
|
||||
|
||||
test('normalizes image annotations without storing viewport pixels', () => {
|
||||
expect(validateWorkReviewAnnotations([
|
||||
{ id: 'point-1', x: 0.123456789, y: 0.75, comment: ' 调整人物位置 ' },
|
||||
|
||||
@ -234,4 +234,38 @@ describe('WorkReviewReviewerWorkspace', () => {
|
||||
expect(screen.getByText('作品评审已通过,本作品已完结')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: '提交给修改人' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('shows the single-review test mode and hides skipped final-reviewer annotations', () => {
|
||||
render(
|
||||
<WorkReviewReviewerWorkspace
|
||||
project={{
|
||||
...project,
|
||||
reviewMode: 'single_reviewer_test',
|
||||
requiredReviewRoles: ['manager'],
|
||||
versions: [{
|
||||
...project.versions[0],
|
||||
reviews: [{
|
||||
id: 'skipped-review',
|
||||
reviewerRole: 'final_reviewer',
|
||||
reviewerName: '元泽',
|
||||
decision: 'revision_required',
|
||||
imageComments: [{
|
||||
imageIndex: 0,
|
||||
decision: 'revision_required',
|
||||
comment: '不应显示的历史意见',
|
||||
annotations: [{ id: 'skipped-point', x: 0.5, y: 0.5, comment: '跳过此标注' }],
|
||||
}],
|
||||
generalComment: null,
|
||||
reviewedAt: '2026-08-01T08:00:00.000Z',
|
||||
}],
|
||||
}],
|
||||
}}
|
||||
onSubmitReview={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('status')).toHaveTextContent('已跳过元泽')
|
||||
expect(screen.queryByText('跳过此标注')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('不应显示的历史意见')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -99,6 +99,9 @@ const OPTIONAL_ENV_VARS = [
|
||||
'PERFORMANCE_SURVEY_DAILY_SYNC_INITIAL_DELAY_SECONDS',
|
||||
'PERFORMANCE_SURVEY_DAILY_SYNC_LIMIT',
|
||||
'DOWNLOAD_TOKEN_SECRET',
|
||||
'WORK_REVIEW_SINGLE_REVIEW_TEST_PROJECT_ID',
|
||||
'WORK_REVIEW_SINGLE_REVIEW_TEST_MANAGER_EMPLOYEE_ID',
|
||||
'WORK_REVIEW_SINGLE_REVIEW_TEST_EXPIRES_AT',
|
||||
'DISABLE_CACHE',
|
||||
'ADMIN_API_KEY', // 管理员API密钥(用于外部API调用验证)
|
||||
]
|
||||
@ -179,6 +182,28 @@ export function validateEnvFormats(): void {
|
||||
}
|
||||
}
|
||||
|
||||
const singleReviewVars = [
|
||||
'WORK_REVIEW_SINGLE_REVIEW_TEST_PROJECT_ID',
|
||||
'WORK_REVIEW_SINGLE_REVIEW_TEST_MANAGER_EMPLOYEE_ID',
|
||||
'WORK_REVIEW_SINGLE_REVIEW_TEST_EXPIRES_AT',
|
||||
] as const
|
||||
const configuredSingleReviewVars = singleReviewVars.filter((name) => Boolean(process.env[name]?.trim()))
|
||||
if (configuredSingleReviewVars.length > 0 && configuredSingleReviewVars.length !== singleReviewVars.length) {
|
||||
errors.push(`作品过稿单评审测试配置必须同时设置:${singleReviewVars.join(', ')}`)
|
||||
}
|
||||
if (configuredSingleReviewVars.length === singleReviewVars.length) {
|
||||
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
if (!uuidPattern.test(process.env.WORK_REVIEW_SINGLE_REVIEW_TEST_PROJECT_ID!.trim())) {
|
||||
errors.push('WORK_REVIEW_SINGLE_REVIEW_TEST_PROJECT_ID 必须是有效 UUID')
|
||||
}
|
||||
if (!uuidPattern.test(process.env.WORK_REVIEW_SINGLE_REVIEW_TEST_MANAGER_EMPLOYEE_ID!.trim())) {
|
||||
errors.push('WORK_REVIEW_SINGLE_REVIEW_TEST_MANAGER_EMPLOYEE_ID 必须是有效 UUID')
|
||||
}
|
||||
if (!Number.isFinite(Date.parse(process.env.WORK_REVIEW_SINGLE_REVIEW_TEST_EXPIRES_AT!.trim()))) {
|
||||
errors.push('WORK_REVIEW_SINGLE_REVIEW_TEST_EXPIRES_AT 必须是有效日期时间')
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
const errorList = errors.map(e => ` - ${e}`).join('\n')
|
||||
throw new Error(
|
||||
|
||||
@ -165,9 +165,11 @@ export function deriveCompletedWorkReviewStatus(
|
||||
reviewerRole: string
|
||||
decision: WorkReviewDecision
|
||||
}>,
|
||||
requiredRoles: ReadonlyArray<WorkReviewReviewerRole> = ['manager', 'final_reviewer'],
|
||||
): WorkReviewDecision | null {
|
||||
const manager = reviews.find((review) => review.reviewerRole === 'manager')
|
||||
const finalReviewer = reviews.find((review) => review.reviewerRole === 'final_reviewer')
|
||||
if (!manager || !finalReviewer) return null
|
||||
return deriveWorkReviewDecision([manager, finalReviewer])
|
||||
const requiredReviews = requiredRoles.map((role) => (
|
||||
reviews.find((review) => review.reviewerRole === role)
|
||||
))
|
||||
if (requiredReviews.some((review) => !review)) return null
|
||||
return deriveWorkReviewDecision(requiredReviews as Array<{ decision: WorkReviewDecision }>)
|
||||
}
|
||||
|
||||
@ -135,6 +135,33 @@ function clean(value: unknown) {
|
||||
return text || null
|
||||
}
|
||||
|
||||
type WorkReviewReviewMode = 'dual' | 'single_reviewer_test'
|
||||
|
||||
function getSingleReviewerTestConfig() {
|
||||
const projectId = clean(process.env.WORK_REVIEW_SINGLE_REVIEW_TEST_PROJECT_ID)
|
||||
const managerEmployeeId = clean(process.env.WORK_REVIEW_SINGLE_REVIEW_TEST_MANAGER_EMPLOYEE_ID)
|
||||
const expiresAt = clean(process.env.WORK_REVIEW_SINGLE_REVIEW_TEST_EXPIRES_AT)
|
||||
if (!projectId || !managerEmployeeId || !expiresAt) return null
|
||||
const expiresAtMs = Date.parse(expiresAt)
|
||||
if (!Number.isFinite(expiresAtMs) || Date.now() >= expiresAtMs) return null
|
||||
return { projectId, managerEmployeeId, expiresAt }
|
||||
}
|
||||
|
||||
function getWorkReviewReviewMode(project: { id: string; managerEmployeeId: string }): WorkReviewReviewMode {
|
||||
const config = getSingleReviewerTestConfig()
|
||||
return config
|
||||
&& project.id === config.projectId
|
||||
&& project.managerEmployeeId === config.managerEmployeeId
|
||||
? 'single_reviewer_test'
|
||||
: 'dual'
|
||||
}
|
||||
|
||||
function getRequiredReviewRoles(project: { id: string; managerEmployeeId: string }): ReviewRole[] {
|
||||
return getWorkReviewReviewMode(project) === 'single_reviewer_test'
|
||||
? [REVIEW_ROLES.MANAGER]
|
||||
: [REVIEW_ROLES.MANAGER, REVIEW_ROLES.FINAL_REVIEWER]
|
||||
}
|
||||
|
||||
function escapeDingtalkMarkdown(value: unknown) {
|
||||
return String(value ?? '')
|
||||
.replace(/\\/g, '\\\\')
|
||||
@ -385,12 +412,14 @@ function getPendingRoles(
|
||||
version: any,
|
||||
) {
|
||||
const submittedRoles = new Set((version?.reviews || []).map((review: any) => review.reviewerRole))
|
||||
return [REVIEW_ROLES.MANAGER, REVIEW_ROLES.FINAL_REVIEWER].filter((role) => (
|
||||
return getRequiredReviewRoles(project).filter((role) => (
|
||||
!submittedRoles.has(role) && canReviewRole(ctx, project, role)
|
||||
))
|
||||
}
|
||||
|
||||
function serializeProject(ctx: Awaited<ReturnType<typeof getContext>>, project: any) {
|
||||
const reviewMode = getWorkReviewReviewMode(project)
|
||||
const requiredReviewRoles = getRequiredReviewRoles(project)
|
||||
const versions = [...(project.versions || [])]
|
||||
.sort((left, right) => right.version - left.version)
|
||||
.map((version) => {
|
||||
@ -407,6 +436,7 @@ function serializeProject(ctx: Awaited<ReturnType<typeof getContext>>, project:
|
||||
referenceImageUrls: images.referenceImageUrls,
|
||||
referenceImageGroups: images.referenceImageGroups,
|
||||
reviews: [...(version.reviews || [])]
|
||||
.filter((review) => requiredReviewRoles.includes(review.reviewerRole))
|
||||
.sort((left, right) => left.reviewerRole.localeCompare(right.reviewerRole))
|
||||
.map((review) => ({
|
||||
id: review.id,
|
||||
@ -443,6 +473,8 @@ function serializeProject(ctx: Awaited<ReturnType<typeof getContext>>, project:
|
||||
},
|
||||
createdAt: project.createdAt,
|
||||
updatedAt: project.updatedAt,
|
||||
reviewMode,
|
||||
requiredReviewRoles,
|
||||
versions,
|
||||
permissions: {
|
||||
canResubmit: Boolean(
|
||||
@ -759,7 +791,7 @@ async function queueSubmittedNotifications(project: any, version: any) {
|
||||
'> 请逐张查看作品图片并填写评审修改建议。',
|
||||
].join('\n')
|
||||
|
||||
return Promise.all([
|
||||
const notifications = [
|
||||
queueWorkReviewNotification({
|
||||
projectId: project.id,
|
||||
versionId: version.id,
|
||||
@ -773,7 +805,9 @@ async function queueSubmittedNotifications(project: any, version: any) {
|
||||
content,
|
||||
actionUrl,
|
||||
}),
|
||||
queueWorkReviewNotification({
|
||||
]
|
||||
if (getRequiredReviewRoles(project).includes(REVIEW_ROLES.FINAL_REVIEWER)) {
|
||||
notifications.push(queueWorkReviewNotification({
|
||||
projectId: project.id,
|
||||
versionId: version.id,
|
||||
recipientEmployeeId: project.finalReviewerEmployeeId,
|
||||
@ -785,12 +819,14 @@ async function queueSubmittedNotifications(project: any, version: any) {
|
||||
title: '作品过稿看板',
|
||||
content,
|
||||
actionUrl,
|
||||
}),
|
||||
])
|
||||
}))
|
||||
}
|
||||
return Promise.all(notifications)
|
||||
}
|
||||
|
||||
async function queueCompletedNotification(project: any, version: any) {
|
||||
const approved = version.status === VERSION_STATUS.APPROVED
|
||||
const singleReviewerTest = getWorkReviewReviewMode(project) === 'single_reviewer_test'
|
||||
return queueWorkReviewNotification({
|
||||
projectId: project.id,
|
||||
versionId: version.id,
|
||||
@ -807,7 +843,9 @@ async function queueCompletedNotification(project: any, version: any) {
|
||||
`**${escapeDingtalkMarkdown(project.title)}**`,
|
||||
'',
|
||||
`**版本:** V${version.version} `,
|
||||
`**评审结果:** ${approved ? '指定评审人与元泽均已通过' : '请按评审建议修改后再次提交'} `,
|
||||
`**评审结果:** ${approved
|
||||
? (singleReviewerTest ? '指定评审人已通过' : '指定评审人与元泽均已通过')
|
||||
: '请按评审建议修改后再次提交'} `,
|
||||
'',
|
||||
approved ? '> 本轮作品已完成过稿。' : '> 点击下方按钮查看每张图的修改建议。',
|
||||
].join('\n'),
|
||||
@ -1038,6 +1076,10 @@ export async function submitWorkReview(session: Session | null, versionId: strin
|
||||
if (version.status !== VERSION_STATUS.PENDING_REVIEW) {
|
||||
throw new WorkReviewApiError('该版本评审已经结束', 409)
|
||||
}
|
||||
const requiredReviewRoles = getRequiredReviewRoles(version.project)
|
||||
if (!requiredReviewRoles.includes(role)) {
|
||||
throw new WorkReviewApiError('当前测试流程已跳过元泽评审', 409)
|
||||
}
|
||||
if (!canReviewRole(ctx, version.project, role)) {
|
||||
throw new WorkReviewApiError(role === REVIEW_ROLES.MANAGER ? '无权填写指定评审人区域' : '无权填写元泽评审', 403)
|
||||
}
|
||||
@ -1084,7 +1126,7 @@ export async function submitWorkReview(session: Session | null, versionId: strin
|
||||
const finalStatus = deriveCompletedWorkReviewStatus(reviews as Array<{
|
||||
reviewerRole: string
|
||||
decision: 'approved' | 'revision_required'
|
||||
}>)
|
||||
}>, requiredReviewRoles)
|
||||
if (!finalStatus) return { completed: false, status: VERSION_STATUS.PENDING_REVIEW }
|
||||
const completedAt = new Date()
|
||||
await tx.workReviewVersion.update({
|
||||
|
||||
@ -2,6 +2,8 @@ export type WorkReviewStatus = 'pending_review' | 'revision_required' | 'approve
|
||||
|
||||
export type WorkReviewRole = 'manager' | 'final_reviewer'
|
||||
|
||||
export type WorkReviewReviewMode = 'dual' | 'single_reviewer_test'
|
||||
|
||||
export type WorkReviewDecision = 'approved' | 'revision_required'
|
||||
|
||||
export type WorkReviewImageAnnotation = {
|
||||
@ -89,6 +91,8 @@ export type WorkReviewProject = {
|
||||
finalReviewer: WorkReviewPerson
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
reviewMode?: WorkReviewReviewMode
|
||||
requiredReviewRoles?: WorkReviewRole[]
|
||||
versions: WorkReviewVersion[]
|
||||
permissions: {
|
||||
canResubmit: boolean
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user