gongxiangp2/lib/work-review-metrics.ts
2026-09-21 17:49:40 +08:00

130 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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')
})
}