fix(review): 优化复核逻辑及同步流程
- 调整已审核记录的识别条件,区分不同审核状态 - 移除撤销审核权限中的审核人限制,允许所有审核人撤销 - 修正 SQL 领取语句,避免错误筛选已审核记录的审核人字段 - 更新前端领取逻辑,避免新建/合并/跳过等记录被错误领取 - 增加多人审核首次启用时的数据库迁移说明 - 前端页面增加审核任务领取、数据同步及冲突处理的详细说明 - 添加单元测试对更新代码的覆盖和验证 - 新增 title-norm Python 包相关项目配置及说明文档
Showing
6 changed files
with
82 additions
and
7 deletions
| ... | @@ -144,11 +144,19 @@ python -m pytest test_dedup.py::TestL2Benchmark::test_l2_recall_topk_efficiency_ | ... | @@ -144,11 +144,19 @@ python -m pytest test_dedup.py::TestL2Benchmark::test_l2_recall_topk_efficiency_ |
| 144 | 144 | ||
| 145 | ### 7. 启动复核前端 | 145 | ### 7. 启动复核前端 |
| 146 | 146 | ||
| 147 | 首次启用多人审核时,先让服务补齐领取和乐观锁字段(已有审核结果不会被修改): | ||
| 148 | |||
| 149 | ```bash | ||
| 150 | python serve_l2_dashboard.py --migrate-review-schema --host 127.0.0.1 --port 8765 | ||
| 151 | ``` | ||
| 152 | |||
| 153 | 后续正常启动: | ||
| 154 | |||
| 147 | ```bash | 155 | ```bash |
| 148 | python serve_l2_dashboard.py --host 127.0.0.1 --port 8765 | 156 | python serve_l2_dashboard.py --host 127.0.0.1 --port 8765 |
| 149 | ``` | 157 | ``` |
| 150 | 158 | ||
| 151 | 然后访问 http://127.0.0.1:8765 查看测试报告、比对歌词、人工标注。 | 159 | 然后访问 http://127.0.0.1:8765,填写审核人姓名或工号后开始审核。打开 `pending/unsure` 的人工复核记录时会自动领取;`new/merge/skip/已审核` 记录仅供浏览,不会领取。领取冲突只会将当前记录设为只读,不会刷新列表或跳转。其他审核人的结果约 15 秒内轻量同步,统计信息每分钟刷新一次,页面处于后台时暂停轮询。 |
| 152 | 160 | ||
| 153 | ## 单元测试 | 161 | ## 单元测试 |
| 154 | 162 | ... | ... |
| ... | @@ -840,9 +840,10 @@ | ... | @@ -840,9 +840,10 @@ |
| 840 | if (!query || state.run?.type !== 'staging' || !query.staging_id) return true; | 840 | if (!query || state.run?.type !== 'staging' || !query.staging_id) return true; |
| 841 | const status = query.review_status || ''; | 841 | const status = query.review_status || ''; |
| 842 | const isPendingReview = rowDecision(query) === 'review' && ['pending', 'unsure', ''].includes(status); | 842 | const isPendingReview = rowDecision(query) === 'review' && ['pending', 'unsure', ''].includes(status); |
| 843 | const isOwnReviewedRecord = allowReviewed && query.reviewed_by === state.reviewer; | 843 | const isReviewedRecord = allowReviewed && |
| 844 | ['approved_import', 'rejected_duplicate', 'deleted'].includes(status); | ||
| 844 | const isNonReviewRecord = allowNonReview && status === 'not_required'; | 845 | const isNonReviewRecord = allowNonReview && status === 'not_required'; |
| 845 | const reviewable = isPendingReview || isOwnReviewedRecord || isNonReviewRecord; | 846 | const reviewable = isPendingReview || isReviewedRecord || isNonReviewRecord; |
| 846 | // new / merge / skip 等记录只浏览,不应触发领取请求。 | 847 | // new / merge / skip 等记录只浏览,不应触发领取请求。 |
| 847 | if (!reviewable) return true; | 848 | if (!reviewable) return true; |
| 848 | try { | 849 | try { |
| ... | @@ -1103,8 +1104,7 @@ | ... | @@ -1103,8 +1104,7 @@ |
| 1103 | const needsManualReview = rowDecision(query) === 'review'; | 1104 | const needsManualReview = rowDecision(query) === 'review'; |
| 1104 | const hasFinalReview = Boolean(query.review_status) && | 1105 | const hasFinalReview = Boolean(query.review_status) && |
| 1105 | !['pending', 'not_required', 'unsure'].includes(query.review_status); | 1106 | !['pending', 'not_required', 'unsure'].includes(query.review_status); |
| 1106 | const canUndoReview = hasFinalReview && query.staging_status !== 'imported' && | 1107 | const canUndoReview = hasFinalReview && query.staging_status !== 'imported'; |
| 1107 | query.reviewed_by === state.reviewer; | ||
| 1108 | const canDeleteRecord = query.staging_status !== 'imported'; | 1108 | const canDeleteRecord = query.staging_status !== 'imported'; |
| 1109 | 1109 | ||
| 1110 | // Sticky area: 新入库歌词 + 召回候选(各含音频播放器) | 1110 | // Sticky area: 新入库歌词 + 召回候选(各含音频播放器) | ... | ... |
python/title-norm/README.md
0 → 100644
| 1 | # title-norm | ||
| 2 | |||
| 3 | `archive-bridge` 链路B `internal/subject/normalize.go` 中 | ||
| 4 | `title -> (title_norm, version_type)` 算法的 Python 移植包,供跨语言/离线批处理场景复用。 | ||
| 5 | |||
| 6 | 移植范围(与 Go 源保持处理顺序与规则一致): | ||
| 7 | |||
| 8 | - `normalize_name`:姓名/文本归一化(全角转半角、繁转简、空白折叠、转小写) | ||
| 9 | - `normalize_title`:歌名 -> `(title_norm, version)` 主算法 | ||
| 10 | - 书名号剥离 → 破折号后缀版本感知剥离 → 多重括号循环剥离 → 无括号版本后缀词剥离 | ||
| 11 | → `normalize_name` → 版本枚举归一 | ||
| 12 | - `strip_clip_markers`:削去片段标题(如抖音切片)的版本/切片标记后缀 | ||
| 13 | - `is_version_marker`:判定内容是否为版本标记(版本别名/固定词表/速度模式/黑名单) | ||
| 14 | - 其余辅助:`split_names` / `normalize_name_set` / `normalize_set` / | ||
| 15 | `singer_identity_key` / `author_identity_key` / `org_identity_key` / | ||
| 16 | `platform_from_singer_table` | ||
| 17 | |||
| 18 | ## 安装 | ||
| 19 | |||
| 20 | ```bash | ||
| 21 | pip install -e . | ||
| 22 | ``` | ||
| 23 | |||
| 24 | 繁简转换依赖 `opencc-python-reimplemented`(纯查表,非 AI)。若安装失败或加载出错, | ||
| 25 | `normalize_name` 会跳过繁简转换,不影响其余归一逻辑(与 Go 源行为一致)。 | ||
| 26 | |||
| 27 | ## 用法 | ||
| 28 | |||
| 29 | ```python | ||
| 30 | from title_norm import normalize_title | ||
| 31 | |||
| 32 | title_norm, version = normalize_title("七里香(现场版)") | ||
| 33 | # title_norm == "七里香", version == "live" | ||
| 34 | ``` | ||
| 35 | |||
| 36 | ## 测试 | ||
| 37 | |||
| 38 | ```bash | ||
| 39 | pip install pytest | ||
| 40 | pytest | ||
| 41 | ``` | ||
| 42 | |||
| 43 | 测试用例(`tests/test_title_norm.py`)与 Go 源 `normalize_test.go` 中的用例逐一对齐。 | ||
| 44 | |||
| 45 | ## 维护 | ||
| 46 | |||
| 47 | Go 源是权威实现;若 `internal/subject/normalize.go` 的规则(词表、正则、处理顺序)有变更, | ||
| 48 | 需同步更新本包,保持两侧行为一致。 |
python/title-norm/pyproject.toml
0 → 100644
| 1 | [build-system] | ||
| 2 | requires = ["setuptools>=61.0"] | ||
| 3 | build-backend = "setuptools.build_meta" | ||
| 4 | |||
| 5 | [project] | ||
| 6 | name = "title-norm" | ||
| 7 | version = "0.1.0" | ||
| 8 | description = "歌名归一化与版本提取算法(archive-bridge internal/subject/normalize.go 的 Python 移植)" | ||
| 9 | readme = "README.md" | ||
| 10 | requires-python = ">=3.8" | ||
| 11 | dependencies = [ | ||
| 12 | "opencc-python-reimplemented>=0.1.4", | ||
| 13 | ] | ||
| 14 | |||
| 15 | [tool.setuptools.packages.find] | ||
| 16 | include = ["title_norm*"] |
| ... | @@ -516,7 +516,7 @@ def _claim_staging_review(staging_id: int, reviewer: str, client_token: str) -> | ... | @@ -516,7 +516,7 @@ def _claim_staging_review(staging_id: int, reviewer: str, client_token: str) -> |
| 516 | AND ( | 516 | AND ( |
| 517 | biz_review_status IN ('pending', 'unsure') | 517 | biz_review_status IN ('pending', 'unsure') |
| 518 | OR biz_review_status = 'not_required' | 518 | OR biz_review_status = 'not_required' |
| 519 | OR (reviewed_by = %s AND biz_review_status IN ('approved_import', 'rejected_duplicate', 'deleted')) | 519 | OR biz_review_status IN ('approved_import', 'rejected_duplicate', 'deleted') |
| 520 | ) | 520 | ) |
| 521 | AND ( | 521 | AND ( |
| 522 | review_claim_expires_at IS NULL | 522 | review_claim_expires_at IS NULL |
| ... | @@ -524,7 +524,7 @@ def _claim_staging_review(staging_id: int, reviewer: str, client_token: str) -> | ... | @@ -524,7 +524,7 @@ def _claim_staging_review(staging_id: int, reviewer: str, client_token: str) -> |
| 524 | OR review_claim_token = %s | 524 | OR review_claim_token = %s |
| 525 | ) | 525 | ) |
| 526 | """, | 526 | """, |
| 527 | (reviewer, client_token, REVIEW_CLAIM_TTL_SECONDS, staging_id, reviewer, client_token), | 527 | (reviewer, client_token, REVIEW_CLAIM_TTL_SECONDS, staging_id, client_token), |
| 528 | ) | 528 | ) |
| 529 | if cursor.rowcount != 1: | 529 | if cursor.rowcount != 1: |
| 530 | current = _review_snapshot(cursor, staging_id) | 530 | current = _review_snapshot(cursor, staging_id) | ... | ... |
| ... | @@ -109,6 +109,8 @@ def test_claim_conflict_exposes_current_owner(): | ... | @@ -109,6 +109,8 @@ def test_claim_conflict_exposes_current_owner(): |
| 109 | with pytest.raises(dashboard.ReviewConflictError) as raised: | 109 | with pytest.raises(dashboard.ReviewConflictError) as raised: |
| 110 | dashboard._claim_staging_review(42, "alice", "alice-token") | 110 | dashboard._claim_staging_review(42, "alice", "alice-token") |
| 111 | 111 | ||
| 112 | claim_sql, _params = cursor.executions[0] | ||
| 113 | assert "reviewed_by = %s" not in claim_sql | ||
| 112 | assert raised.value.current_record["review_claimed_by"] == "bob" | 114 | assert raised.value.current_record["review_claimed_by"] == "bob" |
| 113 | assert conn.rolled_back | 115 | assert conn.rolled_back |
| 114 | 116 | ||
| ... | @@ -191,3 +193,4 @@ def test_dashboard_distinguishes_reviewed_from_submitted(): | ... | @@ -191,3 +193,4 @@ def test_dashboard_distinguishes_reviewed_from_submitted(): |
| 191 | assert row["staging_status"] == "imported" | 193 | assert row["staging_status"] == "imported" |
| 192 | assert '<option value="reviewed">已审核</option>' in html | 194 | assert '<option value="reviewed">已审核</option>' in html |
| 193 | assert '<option value="submitted">已提交</option>' in html | 195 | assert '<option value="submitted">已提交</option>' in html |
| 196 | assert "hasFinalReview && query.staging_status !== 'imported'" in html | ... | ... |
-
Please register or sign in to post a comment