feat(review): 支持同词不同曲和同曲不同词的合并类型审核
Showing
2 changed files
with
37 additions
and
10 deletions
| ... | @@ -991,6 +991,7 @@ | ... | @@ -991,6 +991,7 @@ |
| 991 | row.review_version = String(record.review_version ?? row.review_version ?? 0); | 991 | row.review_version = String(record.review_version ?? row.review_version ?? 0); |
| 992 | row.review_claimed_by = record.review_claimed_by ?? ''; | 992 | row.review_claimed_by = record.review_claimed_by ?? ''; |
| 993 | row.review_claim_expires_at = record.review_claim_expires_at ?? ''; | 993 | row.review_claim_expires_at = record.review_claim_expires_at ?? ''; |
| 994 | row.merge_diff_type = record.merge_diff_type ?? ''; | ||
| 994 | row.staging_status = record.staging_status ?? row.staging_status; | 995 | row.staging_status = record.staging_status ?? row.staging_status; |
| 995 | row.claimed_by_me = Boolean(record.claimed_by_me) || row.review_claimed_by === state.reviewer; | 996 | row.claimed_by_me = Boolean(record.claimed_by_me) || row.review_claimed_by === state.reviewer; |
| 996 | } | 997 | } |
| ... | @@ -1183,7 +1184,7 @@ | ... | @@ -1183,7 +1184,7 @@ |
| 1183 | </div>`; | 1184 | </div>`; |
| 1184 | } | 1185 | } |
| 1185 | 1186 | ||
| 1186 | async function updateStagingReview(query, decision, note, candidateId = '') { | 1187 | async function updateStagingReview(query, decision, note, candidateId = '', mergeDiffType = '') { |
| 1187 | return reviewPostJSON('/api/staging-review', { | 1188 | return reviewPostJSON('/api/staging-review', { |
| 1188 | staging_id: String(query.staging_id), | 1189 | staging_id: String(query.staging_id), |
| 1189 | expected_version: Number(query.review_version || 0), | 1190 | expected_version: Number(query.review_version || 0), |
| ... | @@ -1191,13 +1192,16 @@ | ... | @@ -1191,13 +1192,16 @@ |
| 1191 | reviewer: state.reviewer, | 1192 | reviewer: state.reviewer, |
| 1192 | decision, | 1193 | decision, |
| 1193 | note, | 1194 | note, |
| 1194 | candidate_id: candidateId | 1195 | candidate_id: candidateId, |
| 1196 | merge_diff_type: mergeDiffType | ||
| 1195 | }); | 1197 | }); |
| 1196 | } | 1198 | } |
| 1197 | 1199 | ||
| 1198 | const _DB_REVIEW_LABELS = { | 1200 | const _DB_REVIEW_LABELS = { |
| 1199 | approved_import: '确认不重复(待入库)', | 1201 | approved_import: '确认不重复(待入库)', |
| 1200 | rejected_duplicate: '确认重复(待合并作者)', | 1202 | rejected_duplicate: '确认重复(待合并作者)', |
| 1203 | same_lyric_diff_melody: '同词不同曲(待合并)', | ||
| 1204 | diff_lyric_same_melody: '同曲不同词(待合并)', | ||
| 1201 | unsure: '待确认', | 1205 | unsure: '待确认', |
| 1202 | deleted: '已删除', | 1206 | deleted: '已删除', |
| 1203 | }; | 1207 | }; |
| ... | @@ -1288,6 +1292,10 @@ | ... | @@ -1288,6 +1292,10 @@ |
| 1288 | const candidateComposer = candidate?.candidate_composer; | 1292 | const candidateComposer = candidate?.candidate_composer; |
| 1289 | const reason = candidate?.candidate_reason; | 1293 | const reason = candidate?.candidate_reason; |
| 1290 | const selectedReview = candidate ? getReview(candidate) : {}; | 1294 | const selectedReview = candidate ? getReview(candidate) : {}; |
| 1295 | // 从 DB 状态恢复 merge_diff_type 对应的按钮高亮 | ||
| 1296 | if (!selectedReview.final_decision && query.merge_diff_type) { | ||
| 1297 | selectedReview.final_decision = query.merge_diff_type; | ||
| 1298 | } | ||
| 1291 | const conflict = candidate ? isConflict(candidate) : false; | 1299 | const conflict = candidate ? isConflict(candidate) : false; |
| 1292 | const l1 = candidate ? l1Match(candidate) : false; | 1300 | const l1 = candidate ? l1Match(candidate) : false; |
| 1293 | const queryAudioUrl = query.audio_url || ''; | 1301 | const queryAudioUrl = query.audio_url || ''; |
| ... | @@ -1351,6 +1359,8 @@ | ... | @@ -1351,6 +1359,8 @@ |
| 1351 | <span style="flex:1"> | 1359 | <span style="flex:1"> |
| 1352 | <b>${esc(query.review_status === 'approved_import' && query.staging_status === 'imported' | 1360 | <b>${esc(query.review_status === 'approved_import' && query.staging_status === 'imported' |
| 1353 | ? '确认不重复(已入库)' | 1361 | ? '确认不重复(已入库)' |
| 1362 | : query.merge_diff_type | ||
| 1363 | ? `${_DB_REVIEW_LABELS[query.merge_diff_type] || query.merge_diff_type}` | ||
| 1354 | : query.review_status === 'rejected_duplicate' && query.staging_status === 'skipped' | 1364 | : query.review_status === 'rejected_duplicate' && query.staging_status === 'skipped' |
| 1355 | ? '确认重复(作者已合并)' | 1365 | ? '确认重复(作者已合并)' |
| 1356 | : (_DB_REVIEW_LABELS[query.review_status] || query.review_status))}</b> | 1366 | : (_DB_REVIEW_LABELS[query.review_status] || query.review_status))}</b> |
| ... | @@ -1362,9 +1372,9 @@ | ... | @@ -1362,9 +1372,9 @@ |
| 1362 | </div> | 1372 | </div> |
| 1363 | ` : needsManualReview ? ` | 1373 | ` : needsManualReview ? ` |
| 1364 | <div class="review-bar"> | 1374 | <div class="review-bar"> |
| 1365 | ${['duplicate', 'not_duplicate', 'unsure'].map(value => ` | 1375 | ${['duplicate', 'same_lyric_diff_melody', 'diff_lyric_same_melody', 'not_duplicate', 'unsure'].map(value => ` |
| 1366 | <button class="review-choice ${selectedReview.final_decision === value ? 'active' : ''}" data-review="${value}"> | 1376 | <button class="review-choice ${selectedReview.final_decision === value ? 'active' : ''}" data-review="${value}"> |
| 1367 | ${value === 'duplicate' ? '确认重复' : value === 'not_duplicate' ? '确认不重复' : '待确认'} | 1377 | ${{duplicate: '确认重复', same_lyric_diff_melody: '同词不同曲', diff_lyric_same_melody: '同曲不同词', not_duplicate: '确认不重复', unsure: '待确认'}[value]} |
| 1368 | </button>`).join('')} | 1378 | </button>`).join('')} |
| 1369 | <input id="reviewNote" value="${esc(selectedReview.note || '')}" placeholder="人工备注"> | 1379 | <input id="reviewNote" value="${esc(selectedReview.note || '')}" placeholder="人工备注"> |
| 1370 | <button id="deleteReviewBtn" class="secondary" style="color:#c0392b;border-color:#c0392b">删除</button> | 1380 | <button id="deleteReviewBtn" class="secondary" style="color:#c0392b;border-color:#c0392b">删除</button> |
| ... | @@ -1402,7 +1412,17 @@ | ... | @@ -1402,7 +1412,17 @@ |
| 1402 | renderDetail(); | 1412 | renderDetail(); |
| 1403 | }); | 1413 | }); |
| 1404 | } | 1414 | } |
| 1405 | const _REVIEW_DECISION_MAP = { duplicate: 'rejected_duplicate', not_duplicate: 'approved_import', unsure: 'unsure' }; | 1415 | const _REVIEW_DECISION_MAP = { |
| 1416 | duplicate: 'rejected_duplicate', | ||
| 1417 | same_lyric_diff_melody: 'rejected_duplicate', | ||
| 1418 | diff_lyric_same_melody: 'rejected_duplicate', | ||
| 1419 | not_duplicate: 'approved_import', | ||
| 1420 | unsure: 'unsure' | ||
| 1421 | }; | ||
| 1422 | const _MERGE_DIFF_TYPE_MAP = { | ||
| 1423 | same_lyric_diff_melody: 'same_lyric_diff_melody', | ||
| 1424 | diff_lyric_same_melody: 'diff_lyric_same_melody' | ||
| 1425 | }; | ||
| 1406 | for (const btn of els.detail.querySelectorAll('.review-choice')) { | 1426 | for (const btn of els.detail.querySelectorAll('.review-choice')) { |
| 1407 | btn.addEventListener('click', async () => { | 1427 | btn.addEventListener('click', async () => { |
| 1408 | if (!candidate) return; | 1428 | if (!candidate) return; |
| ... | @@ -1416,8 +1436,11 @@ | ... | @@ -1416,8 +1436,11 @@ |
| 1416 | renderDetail(); | 1436 | renderDetail(); |
| 1417 | return; | 1437 | return; |
| 1418 | } | 1438 | } |
| 1439 | const mergeDiffType = _MERGE_DIFF_TYPE_MAP[localDecision] || ''; | ||
| 1419 | const payload = await updateStagingReview( | 1440 | const payload = await updateStagingReview( |
| 1420 | query, dbDecision, note, localDecision === 'duplicate' ? (candidate?.candidate_id || '') : '' | 1441 | query, dbDecision, note, |
| 1442 | (localDecision === 'duplicate' || mergeDiffType) ? (candidate?.candidate_id || '') : '', | ||
| 1443 | mergeDiffType | ||
| 1421 | ); | 1444 | ); |
| 1422 | for (const row of state.queryRows) applyReviewSnapshot(row, payload.record); | 1445 | for (const row of state.queryRows) applyReviewSnapshot(row, payload.record); |
| 1423 | // 刷新统计和加载列表并行 | 1446 | // 刷新统计和加载列表并行 | ... | ... |
| ... | @@ -324,6 +324,7 @@ def _staging_dashboard_row(row: dict[str, object]) -> dict[str, str]: | ... | @@ -324,6 +324,7 @@ def _staging_dashboard_row(row: dict[str, object]) -> dict[str, str]: |
| 324 | "review_claimed_by": str(row.get("review_claimed_by") or ""), | 324 | "review_claimed_by": str(row.get("review_claimed_by") or ""), |
| 325 | "review_claim_expires_at": str(row.get("review_claim_expires_at") or ""), | 325 | "review_claim_expires_at": str(row.get("review_claim_expires_at") or ""), |
| 326 | "staging_status": str(row.get("staging_status") or ""), | 326 | "staging_status": str(row.get("staging_status") or ""), |
| 327 | "merge_diff_type": str(row.get("merge_diff_type") or ""), | ||
| 327 | "l1_metadata_match": "1" if row.get("l1_matched_id") else "0", | 328 | "l1_metadata_match": "1" if row.get("l1_matched_id") else "0", |
| 328 | "l1_l2_conflict": "0", | 329 | "l1_l2_conflict": "0", |
| 329 | "audio_url": str(row.get("audio_url") or ""), | 330 | "audio_url": str(row.get("audio_url") or ""), |
| ... | @@ -582,7 +583,7 @@ def _review_snapshot(cursor, staging_id: int) -> dict[str, object]: | ... | @@ -582,7 +583,7 @@ def _review_snapshot(cursor, staging_id: int) -> dict[str, object]: |
| 582 | f""" | 583 | f""" |
| 583 | SELECT staging_id, source_song_id, dedup_action, biz_review_status, biz_review_note, | 584 | SELECT staging_id, source_song_id, dedup_action, biz_review_status, biz_review_note, |
| 584 | reviewed_by, reviewed_at, review_version, review_claimed_by, | 585 | reviewed_by, reviewed_at, review_version, review_claimed_by, |
| 585 | review_claim_expires_at, staging_status | 586 | review_claim_expires_at, staging_status, merge_diff_type |
| 586 | FROM {TARGET_TABLE_NAME_TMP} | 587 | FROM {TARGET_TABLE_NAME_TMP} |
| 587 | WHERE staging_id = %s | 588 | WHERE staging_id = %s |
| 588 | """, | 589 | """, |
| ... | @@ -684,6 +685,7 @@ def _update_staging_review( | ... | @@ -684,6 +685,7 @@ def _update_staging_review( |
| 684 | expected_version: int, | 685 | expected_version: int, |
| 685 | client_token: str, | 686 | client_token: str, |
| 686 | candidate_id: str = "", | 687 | candidate_id: str = "", |
| 688 | merge_diff_type: str = "", | ||
| 687 | ) -> dict[str, object]: | 689 | ) -> dict[str, object]: |
| 688 | """Persist one claimed review using optimistic concurrency control.""" | 690 | """Persist one claimed review using optimistic concurrency control.""" |
| 689 | if decision == "pending": | 691 | if decision == "pending": |
| ... | @@ -699,7 +701,8 @@ def _update_staging_review( | ... | @@ -699,7 +701,8 @@ def _update_staging_review( |
| 699 | elif decision == "rejected_duplicate": | 701 | elif decision == "rejected_duplicate": |
| 700 | status_fields = """ | 702 | status_fields = """ |
| 701 | biz_review_status = %s, reviewed_by = %s, reviewed_at = NOW(), | 703 | biz_review_status = %s, reviewed_by = %s, reviewed_at = NOW(), |
| 702 | matched_song_id = %s | 704 | matched_song_id = %s, |
| 705 | merge_diff_type = NULLIF(%s, '') | ||
| 703 | """ | 706 | """ |
| 704 | else: | 707 | else: |
| 705 | status_fields = "biz_review_status = %s, reviewed_by = %s, reviewed_at = NOW()" | 708 | status_fields = "biz_review_status = %s, reviewed_by = %s, reviewed_at = NOW()" |
| ... | @@ -713,7 +716,7 @@ def _update_staging_review( | ... | @@ -713,7 +716,7 @@ def _update_staging_review( |
| 713 | candidate_id = candidate_id.strip() | 716 | candidate_id = candidate_id.strip() |
| 714 | if not candidate_id: | 717 | if not candidate_id: |
| 715 | raise ValueError("确认重复前请先选择一条召回候选") | 718 | raise ValueError("确认重复前请先选择一条召回候选") |
| 716 | params = [decision, reviewer, candidate_id, note, staging_id, expected_version, client_token, reviewer] | 719 | params = [decision, reviewer, candidate_id, merge_diff_type, note, staging_id, expected_version, client_token, reviewer] |
| 717 | else: | 720 | else: |
| 718 | params = [decision, reviewer, note, staging_id, expected_version, client_token, reviewer] | 721 | params = [decision, reviewer, note, staging_id, expected_version, client_token, reviewer] |
| 719 | with _target_conn() as conn, conn.cursor() as cursor: | 722 | with _target_conn() as conn, conn.cursor() as cursor: |
| ... | @@ -1447,6 +1450,7 @@ class Handler(BaseHTTPRequestHandler): | ... | @@ -1447,6 +1450,7 @@ class Handler(BaseHTTPRequestHandler): |
| 1447 | decision = str(payload.get("decision", "")).strip() | 1450 | decision = str(payload.get("decision", "")).strip() |
| 1448 | note = str(payload.get("note", "")).strip() | 1451 | note = str(payload.get("note", "")).strip() |
| 1449 | candidate_id = str(payload.get("candidate_id", "")).strip() | 1452 | candidate_id = str(payload.get("candidate_id", "")).strip() |
| 1453 | merge_diff_type = str(payload.get("merge_diff_type", "")).strip() | ||
| 1450 | client_token = str(payload.get("client_token") or "").strip() | 1454 | client_token = str(payload.get("client_token") or "").strip() |
| 1451 | expected_version = int(payload.get("expected_version", -1)) | 1455 | expected_version = int(payload.get("expected_version", -1)) |
| 1452 | if not staging_id: | 1456 | if not staging_id: |
| ... | @@ -1456,7 +1460,7 @@ class Handler(BaseHTTPRequestHandler): | ... | @@ -1456,7 +1460,7 @@ class Handler(BaseHTTPRequestHandler): |
| 1456 | if decision not in ("approved_import", "rejected_duplicate", "unsure", "pending", "deleted"): | 1460 | if decision not in ("approved_import", "rejected_duplicate", "unsure", "pending", "deleted"): |
| 1457 | raise ValueError(f"invalid decision: {decision}") | 1461 | raise ValueError(f"invalid decision: {decision}") |
| 1458 | result = _update_staging_review( | 1462 | result = _update_staging_review( |
| 1459 | staging_id, decision, note, reviewer, expected_version, client_token, candidate_id | 1463 | staging_id, decision, note, reviewer, expected_version, client_token, candidate_id, merge_diff_type |
| 1460 | ) | 1464 | ) |
| 1461 | _json_response(self, {"record": result}) | 1465 | _json_response(self, {"record": result}) |
| 1462 | except ReviewConflictError as exc: | 1466 | except ReviewConflictError as exc: | ... | ... |
-
Please register or sign in to post a comment