Commit 96799bc7 96799bc7a3005e121a90e208d4e77522720522cc by 沈秋雨

fix(review): 优化复核逻辑及同步流程

- 调整已审核记录的识别条件,区分不同审核状态
- 移除撤销审核权限中的审核人限制,允许所有审核人撤销
- 修正 SQL 领取语句,避免错误筛选已审核记录的审核人字段
- 更新前端领取逻辑,避免新建/合并/跳过等记录被错误领取
- 增加多人审核首次启用时的数据库迁移说明
- 前端页面增加审核任务领取、数据同步及冲突处理的详细说明
- 添加单元测试对更新代码的覆盖和验证
- 新增 title-norm Python 包相关项目配置及说明文档
1 parent 98d1d92a
...@@ -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: 新入库歌词 + 召回候选(各含音频播放器)
......
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 需同步更新本包,保持两侧行为一致。
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
......