From 93227d3316bfe0a330f0ae3d2ba9d8a2c99d648e Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Fri, 7 Aug 2026 23:02:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(ECR-020):=20QualityFeedback=20=E9=97=AE?= =?UTF-8?q?=E7=AD=94=E8=B4=A8=E9=87=8F=E5=8F=8D=E9=A6=88=E5=B9=B6=20Closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 ask_quality_feedback、运营/C端评分 API 与 admin-h5 问答反馈区;禁改消息/UGC/真支付。 Co-authored-by: Cursor --- .ai/domain/domain-map.md | 2 +- .ai/domain/entity-catalog.md | 2 +- .ai/product/feature-map.md | 2 +- .ai/product/feature-spec/README.md | 1 + .ai/product/feature-spec/ops-admin.md | 3 +- .../feature-spec/ops-quality-feedback.md | 42 +++++ apps/admin-h5/src/api/client.ts | 20 +++ apps/admin-h5/src/pages/AskPage.vue | 73 +++++++- apps/api/internal/handler/admin.go | 1 + .../handler/admin_quality_feedback.go | 76 ++++++++ apps/api/internal/handler/ask.go | 1 + apps/api/internal/handler/ask_feedback.go | 67 +++++++ .../integration/quality_feedback_test.go | 136 ++++++++++++++ .../repository/quality_feedback_repo.go | 170 ++++++++++++++++++ .../service/admin/quality_feedback.go | 60 +++++++ apps/api/internal/service/admin/rbac.go | 3 +- apps/api/internal/service/ask/feedback.go | 42 +++++ .../000021_ask_quality_feedback.down.sql | 4 + .../000021_ask_quality_feedback.up.sql | 23 +++ .../BD-2026-020-quality-feedback.md | 26 +++ docs/CHANGELOG.md | 1 + docs/CODE_REVIEW/ECR-020.md | 8 + docs/CONTRACT_DIFF/ECR-020.yaml | 26 +++ docs/ECR/ECR-020-quality-feedback.md | 19 ++ .../ECR-020-quality-feedback.md | 6 + docs/HANDOFF/ECR-020-architect-to-engineer.md | 3 + docs/HANDOFF/ECR-020-engineer-to-reviewer.md | 3 + docs/PRODUCT_SPEC/ECR-020-quality-feedback.md | 3 + docs/STATE/ECR-020.md | 6 + docs/TASKS/TASK-020-ECR020.yaml | 11 ++ docs/TEST_REPORT/ECR-020.md | 32 ++++ docs/TRACEABILITY.md | 1 + docs/WAVE0/LOOP_AUTHORIZATION.md | 2 +- proto/openapi.yaml | 66 +++++++ 34 files changed, 932 insertions(+), 9 deletions(-) create mode 100644 .ai/product/feature-spec/ops-quality-feedback.md create mode 100644 apps/api/internal/handler/admin_quality_feedback.go create mode 100644 apps/api/internal/handler/ask_feedback.go create mode 100644 apps/api/internal/integration/quality_feedback_test.go create mode 100644 apps/api/internal/repository/quality_feedback_repo.go create mode 100644 apps/api/internal/service/admin/quality_feedback.go create mode 100644 apps/api/internal/service/ask/feedback.go create mode 100644 apps/api/migrations/000021_ask_quality_feedback.down.sql create mode 100644 apps/api/migrations/000021_ask_quality_feedback.up.sql create mode 100644 docs/BACKEND_DESIGN/BD-2026-020-quality-feedback.md create mode 100644 docs/CODE_REVIEW/ECR-020.md create mode 100644 docs/CONTRACT_DIFF/ECR-020.yaml create mode 100644 docs/ECR/ECR-020-quality-feedback.md create mode 100644 docs/ENGINEERING_SPEC/ECR-020-quality-feedback.md create mode 100644 docs/HANDOFF/ECR-020-architect-to-engineer.md create mode 100644 docs/HANDOFF/ECR-020-engineer-to-reviewer.md create mode 100644 docs/PRODUCT_SPEC/ECR-020-quality-feedback.md create mode 100644 docs/STATE/ECR-020.md create mode 100644 docs/TASKS/TASK-020-ECR020.yaml create mode 100644 docs/TEST_REPORT/ECR-020.md diff --git a/.ai/domain/domain-map.md b/.ai/domain/domain-map.md index 82b4109..b27ebd8 100644 --- a/.ai/domain/domain-map.md +++ b/.ai/domain/domain-map.md @@ -37,7 +37,7 @@ | Ops_Content | ExploreConfig (partial) | Shipped Ops-C | | Account_Risk | AccountLifecycle | Spec via ECR-013B(Identity_Profile owns UserStatus) | | Content_Safety | ContentSafety · CrisisCare | ContentSafety **ECR-019**(FilterRule);CrisisCare Draft | -| Ask_Ops | AskOperations · AICoreConfig | AskOperations **ECR-017**(只读);AICoreConfig Draft | +| Ask_Ops | AskOperations · AICoreConfig | AskOperations **ECR-017/020**;AICoreConfig Draft | | Ops_CMS_NoUGC | OpsCMS | Draft | | Community | — | **Forbidden** | diff --git a/.ai/domain/entity-catalog.md b/.ai/domain/entity-catalog.md index c73fd6f..83cbd80 100644 --- a/.ai/domain/entity-catalog.md +++ b/.ai/domain/entity-catalog.md @@ -32,7 +32,7 @@ Status: `Draft` | ModerationCase | Content_Safety | ContentSafety | 后置 | | CrisisEvent / CrisisPolicy | Content_Safety | CrisisCare | 后置 | | SystemPrompt / Knowledge* / ToolDefinition | Ask_Ops | AICoreConfig | 后置 | -| AskSessionView / QualityFeedback | Ask_Ops | AskOperations | **ECR-017** AskSessionView 只读;QualityFeedback 后置 | +| AskSessionView / QualityFeedback | Ask_Ops | AskOperations | **ECR-017** AskSessionView;**ECR-020** QualityFeedback | | Banner / FeedSlot | Ops_CMS_NoUGC | OpsCMS | 后置 | | UGC* | Community | — | **Forbidden** | diff --git a/.ai/product/feature-map.md b/.ai/product/feature-map.md index ab00f2f..eca066d 100644 --- a/.ai/product/feature-map.md +++ b/.ai/product/feature-map.md @@ -235,7 +235,7 @@ UI **不出现「塔罗」**。禁止神谕吉凶、恐吓话术。 | Phase A `[Ops]` | 登录 · 用户/订单查询 · 会员授予 · 审计 · `apps/admin-h5`(ECR-006 Closed) | | Phase B `[Ops]` | 行为分析:自有埋点 + 管理端「数据」看板(**ECR-007 Closed** · Spec `ops-analytics.md`) | | Phase C `[Ops]` | 内容:首页宫格 CRUD · 测评上下架(**ECR-008 Closed** · Spec `ops-content.md`) | -| Phase D+ | **Contract-First**:**ECR-013A…018 Closed** → **ECR-019** ContentSafety(Loop)。`docs/WAVE0/LOOP_AUTHORIZATION.md`。 | +| Phase D+ | **Contract-First**:**ECR-013A…019 Closed** → **ECR-020** QualityFeedback(Loop)。`docs/WAVE0/LOOP_AUTHORIZATION.md`。 | | 排除 | **UGC / 社区广场**(M10.2)仍 `[No]`;真支付最后 | 不计入 P1 Complete;不进入五 Tab。 diff --git a/.ai/product/feature-spec/README.md b/.ai/product/feature-spec/README.md index af18cd7..2909af6 100644 --- a/.ai/product/feature-spec/README.md +++ b/.ai/product/feature-spec/README.md @@ -29,6 +29,7 @@ | [ops-ask-operations.md](ops-ask-operations.md) | 问答运营 AskOperations | §7 | `admin-h5` `/ask` · `GET /admin/ask/threads*` | Ops-D · **ECR-017 Closed** | | [ops-entitlement.md](ops-entitlement.md) | 用户权益 Entitlement | §7 | `admin-h5` 用户详情「权益」· `GET /admin/users/:id/entitlements` | Ops-D · **ECR-018 Closed** | | [ops-content-safety.md](ops-content-safety.md) | 内容安全 ContentSafety | §7 | `admin-h5` `/safety` · `GET /admin/content-safety/*` | Ops-D · **ECR-019 Closed** | +| [ops-quality-feedback.md](ops-quality-feedback.md) | 问答质量反馈 QualityFeedback | §7 | `admin-h5` `/ask` · `GET/POST /admin/ask/feedback*` | Ops-D · **ECR-020 Closed** | 新功能:复制 `_TEMPLATE.md` → 填满 → 在本表登记 → 再编码。 diff --git a/.ai/product/feature-spec/ops-admin.md b/.ai/product/feature-spec/ops-admin.md index c656b48..1f32a32 100644 --- a/.ai/product/feature-spec/ops-admin.md +++ b/.ai/product/feature-spec/ops-admin.md @@ -216,6 +216,7 @@ Phase A 可先 `console`/本地;不挡验收。 | **J(ECR-017 Closed)** | AskOperations — Spec `ops-ask-operations.md` | | **K(ECR-018 Closed)** | Entitlement — Spec `ops-entitlement.md` | | **L(ECR-019 Closed)** | ContentSafety FilterRule — Spec `ops-content-safety.md` | -| 后置 | QualityFeedback / AICoreConfig / CrisisCare(Loop 续跑) | +| **M(ECR-020 Closed)** | QualityFeedback — Spec `ops-quality-feedback.md` | +| 后置 | AICoreConfig / CrisisCare(Loop 续跑) | | D | 订单筛选 · 展示价 · 退款只读(另开 ECR) | | 后置 | 封禁加深(Account_Risk)· 推送占位 | diff --git a/.ai/product/feature-spec/ops-quality-feedback.md b/.ai/product/feature-spec/ops-quality-feedback.md new file mode 100644 index 0000000..ab2b192 --- /dev/null +++ b/.ai/product/feature-spec/ops-quality-feedback.md @@ -0,0 +1,42 @@ +# Feature Spec: QualityFeedback(Ops · ECR-020) + +> Status: `Active`(Loop continuous · **ECR-020 Closed**) +> Parent: WAVE0-FROZEN · Predecessor: ECR-019 Closed +> Capability: `AskOperations` · BC: `Ask_Ops` +> 授权:`docs/WAVE0/LOOP_AUTHORIZATION.md` + +## Non-goals + +改写消息正文 · SystemPrompt/Knowledge · HandoffCase 全量 · AICoreConfig · UGC 广场 · 真支付 + +## L2 Domain + +| 概念 | 语义 | +|------|------| +| `QualityFeedback` | 绑定 thread(可选 message);rating 1–5;tag ∈ {helpful,off_topic,unsafe,other};source ∈ {admin,user} | + +## L3 API + +| Method | Path | 权限 | 语义 | +|--------|------|------|------| +| GET | `/admin/ask/feedback` | `admin.ask.read` | 列表 | +| POST | `/admin/ask/threads/:id/feedback` | `admin.ask.feedback.write` | 运营提交 | +| POST | `/ask/threads/:id/feedback` | DeviceAuth 用户 | C 端评分 | + +## Migration + +`000021`:`ask_quality_feedback` + 授予 `admin.ask.feedback.write` + +## L4 AC + +| ID | Then | +|----|------| +| AC-F-01 | admin POST feedback → 200 · list 可见 | +| AC-F-02 | user POST feedback → 200 | +| AC-F-03 | 无效 rating → 400 | +| AC-S-01 | 无 Admin → admin list 401 | +| AC-S-02 | 无 feedback.write → admin POST 403 | +| AC-P-01 | list < 500ms | +| AC-O-01 | admin 提交写 AuditLog `ask.feedback.create` | + +contract_diff: `docs/CONTRACT_DIFF/ECR-020.yaml` diff --git a/apps/admin-h5/src/api/client.ts b/apps/admin-h5/src/api/client.ts index e67177d..8b772df 100644 --- a/apps/admin-h5/src/api/client.ts +++ b/apps/admin-h5/src/api/client.ts @@ -275,6 +275,26 @@ export const adminApi = { '/content-safety/evaluate', { text }, ), + askFeedback: () => + request<{ + items: Array<{ + id: string + thread_id: string + message_id?: string + source: string + rating: number + tag?: string + note?: string + created_at: string + }> + }>('GET', '/ask/feedback'), + createAskFeedback: (threadId: string, body: { rating: number; tag?: string; note?: string; message_id?: string }) => + request<{ + id: string + thread_id: string + rating: number + source: string + }>('POST', `/ask/threads/${threadId}/feedback`, body), orders: () => request<{ items: Array<{ diff --git a/apps/admin-h5/src/pages/AskPage.vue b/apps/admin-h5/src/pages/AskPage.vue index 699e268..333d58c 100644 --- a/apps/admin-h5/src/pages/AskPage.vue +++ b/apps/admin-h5/src/pages/AskPage.vue @@ -2,23 +2,34 @@ import { onMounted, ref } from 'vue' import { RouterLink } from 'vue-router' import { adminApi } from '@/api/client' +import { useAuthStore } from '@/stores/auth' type Thread = Awaited>['items'][number] type Detail = Awaited> +type Feedback = Awaited>['items'][number] +const auth = useAuthStore() const loading = ref(false) const error = ref('') const items = ref([]) const selected = ref(null) const detailErr = ref('') const detailLoading = ref(false) +const feedback = ref([]) +const rating = ref(4) +const tag = ref('helpful') +const note = ref('') +const fbMsg = ref('') + +const canWriteFeedback = () => auth.can('admin.ask.feedback.write') async function load() { loading.value = true error.value = '' try { - const res = await adminApi.askThreads() - items.value = res.items || [] + const [threads, fb] = await Promise.all([adminApi.askThreads(), adminApi.askFeedback()]) + items.value = threads.items || [] + feedback.value = fb.items || [] } catch (e) { error.value = e instanceof Error ? e.message : '加载失败' } finally { @@ -30,6 +41,7 @@ async function openThread(id: string) { detailLoading.value = true detailErr.value = '' selected.value = null + fbMsg.value = '' try { selected.value = await adminApi.askThread(id) } catch (e) { @@ -39,6 +51,24 @@ async function openThread(id: string) { } } +async function submitFeedback() { + if (!selected.value) return + fbMsg.value = '' + try { + await adminApi.createAskFeedback(selected.value.id, { + rating: rating.value, + tag: tag.value, + note: note.value || undefined, + }) + fbMsg.value = '已提交质量反馈' + note.value = '' + const fb = await adminApi.askFeedback() + feedback.value = fb.items || [] + } catch (e) { + fbMsg.value = e instanceof Error ? e.message : '提交失败' + } +} + function fmtTime(iso?: string) { if (!iso) return '—' try { @@ -54,7 +84,7 @@ onMounted(load)

选择左侧会话查看消息

@@ -103,6 +159,8 @@ onMounted(load)