130 lines
4.1 KiB
TypeScript
130 lines
4.1 KiB
TypeScript
export type FirstPassReviewStatus = 'approved' | 'revision_required' | 'pending_review' | 'unknown'
|
||
|
||
export type FirstPassReviewStatusCount = {
|
||
status: string
|
||
count: number
|
||
}
|
||
|
||
export type FirstPassReviewSummary = {
|
||
totalUploads: number
|
||
totalApproved: number
|
||
firstPassApproved: number
|
||
rate: number | null
|
||
statusBreakdown: {
|
||
approved: number
|
||
revisionRequired: number
|
||
pendingReview: number
|
||
unknown: number
|
||
}
|
||
readAt: string
|
||
/** 作品过稿后台的增量检查状态;统计本身仍直接读取该后台。 */
|
||
syncStatus?: 'idle' | 'syncing' | 'ready' | 'error'
|
||
lastSyncedAt?: string | null
|
||
syncRowsChecked?: number
|
||
}
|
||
|
||
export type FirstPassSubmitterRate = {
|
||
submitterName: string
|
||
totalUploads: number
|
||
totalApproved: number
|
||
firstPassApproved: number
|
||
rate: number | null
|
||
}
|
||
|
||
export type FirstPassSubmitterProject = {
|
||
submitterName?: string | null
|
||
currentStatus: string
|
||
firstVersionStatus?: string | null
|
||
}
|
||
|
||
export function normalizeFirstPassReviewStatus(status: string): FirstPassReviewStatus {
|
||
if (status === 'approved' || status === 'revision_required' || status === 'pending_review') {
|
||
return status
|
||
}
|
||
return 'unknown'
|
||
}
|
||
|
||
/**
|
||
* 首轮通过率 = V1 审核通过作品数 / 当前已通过作品数。
|
||
*
|
||
* totalUploads 和状态拆分用于展示全部 V1 明细,不参与比率分母。
|
||
*/
|
||
export function buildFirstPassReviewSummary(
|
||
rows: FirstPassReviewStatusCount[],
|
||
totalApproved: number,
|
||
readAt = new Date(),
|
||
): FirstPassReviewSummary {
|
||
const statusBreakdown = {
|
||
approved: 0,
|
||
revisionRequired: 0,
|
||
pendingReview: 0,
|
||
unknown: 0,
|
||
}
|
||
|
||
for (const row of rows) {
|
||
const count = Math.max(0, Number(row.count) || 0)
|
||
const normalizedStatus = normalizeFirstPassReviewStatus(row.status)
|
||
if (normalizedStatus === 'approved') statusBreakdown.approved += count
|
||
else if (normalizedStatus === 'revision_required') statusBreakdown.revisionRequired += count
|
||
else if (normalizedStatus === 'pending_review') statusBreakdown.pendingReview += count
|
||
else statusBreakdown.unknown += count
|
||
}
|
||
|
||
const totalUploads = Object.values(statusBreakdown).reduce((total, count) => total + count, 0)
|
||
const approvedDenominator = Math.max(0, Number(totalApproved) || 0)
|
||
const firstPassApproved = statusBreakdown.approved
|
||
|
||
return {
|
||
totalUploads,
|
||
totalApproved: approvedDenominator,
|
||
firstPassApproved,
|
||
rate: approvedDenominator === 0
|
||
? null
|
||
: Math.round((firstPassApproved / approvedDenominator) * 1000) / 10,
|
||
statusBreakdown,
|
||
readAt: readAt.toISOString(),
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 按提交人复用总指标口径:V1 审核通过作品数 / 当前已通过作品数。
|
||
* 调用方应只传入包含 V1 的作品,与页面明细的统计范围保持一致。
|
||
*/
|
||
export function buildFirstPassSubmitterRates(
|
||
projects: FirstPassSubmitterProject[],
|
||
): FirstPassSubmitterRate[] {
|
||
const ratesBySubmitter = new Map<string, Omit<FirstPassSubmitterRate, 'rate'>>()
|
||
|
||
for (const project of projects) {
|
||
const submitterName = project.submitterName?.trim() || '未填写提交人'
|
||
const current = ratesBySubmitter.get(submitterName) || {
|
||
submitterName,
|
||
totalUploads: 0,
|
||
totalApproved: 0,
|
||
firstPassApproved: 0,
|
||
}
|
||
|
||
if (project.firstVersionStatus) {
|
||
current.totalUploads += 1
|
||
if (normalizeFirstPassReviewStatus(project.firstVersionStatus) === 'approved') {
|
||
current.firstPassApproved += 1
|
||
}
|
||
}
|
||
if (project.currentStatus === 'approved') current.totalApproved += 1
|
||
ratesBySubmitter.set(submitterName, current)
|
||
}
|
||
|
||
return Array.from(ratesBySubmitter.values(), (item) => ({
|
||
...item,
|
||
rate: item.totalApproved === 0
|
||
? null
|
||
: Math.round((item.firstPassApproved / item.totalApproved) * 1000) / 10,
|
||
})).sort((left, right) => {
|
||
if (left.rate === null && right.rate !== null) return 1
|
||
if (left.rate !== null && right.rate === null) return -1
|
||
if (left.rate !== right.rate) return (right.rate || 0) - (left.rate || 0)
|
||
if (left.totalApproved !== right.totalApproved) return right.totalApproved - left.totalApproved
|
||
return left.submitterName.localeCompare(right.submitterName, 'zh-CN')
|
||
})
|
||
}
|