18 changed files with 1526 additions and 40 deletions
-
4frontend/src/api/faq.ts
-
9frontend/src/api/feedback.ts
-
2frontend/src/router/index.ts
-
1frontend/src/stores/navigation.ts
-
98frontend/src/views/ChatPanel.vue
-
187frontend/src/views/ConversationManager.vue
-
12frontend/src/views/FaqManager.vue
-
299frontend/src/views/FeedbackOps.vue
-
140src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java
-
145src/main/java/com/wok/supportbot/controller/FaqController.java
-
82src/main/java/com/wok/supportbot/controller/MessageFeedbackController.java
-
34src/main/java/com/wok/supportbot/dao/FaqFeedbackLinkMapper.java
-
94src/main/java/com/wok/supportbot/entity/FaqFeedbackLink.java
-
9src/main/java/com/wok/supportbot/entity/KnowledgeFaq.java
-
19src/main/java/com/wok/supportbot/entity/MessageFeedback.java
-
198src/main/java/com/wok/supportbot/service/FaqService.java
-
184src/main/java/com/wok/supportbot/service/MessageFeedbackService.java
-
49src/main/resources/init-database.sql
@ -0,0 +1,299 @@ |
|||||
|
<template> |
||||
|
<t-card title="💡 反馈运营看板" :bordered="false"> |
||||
|
<!-- 筛选栏 --> |
||||
|
<div class="toolbar"> |
||||
|
<t-select v-model="filterFeedbackType" :options="feedbackTypeOptions" placeholder="全部类型" clearable size="small" style="width:140px;" /> |
||||
|
<t-select v-model="filterReasonCategory" :options="reasonCategoryOptions" placeholder="全部原因" clearable size="small" style="width:120px;" /> |
||||
|
<t-select v-model="filterProcessed" :options="processedOptions" placeholder="全部状态" clearable size="small" style="width:120px;" /> |
||||
|
<t-date-picker v-model="filterDate" size="small" style="width:240px;" placeholder="选择日期范围" /> |
||||
|
<t-button size="small" @click="loadList(1)">搜索</t-button> |
||||
|
<t-button variant="outline" size="small" @click="loadList(1)">刷新</t-button> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 反馈列表 --> |
||||
|
<t-table :data="feedbacks" :columns="columns" row-key="conversationId" :loading="loading" |
||||
|
:pagination="{ current: page, total: total, pageSize: pageSize, showJumper: true }" @page-change="onPageChange" |
||||
|
empty="暂无反馈数据,用户在 SDK 端对 AI 回复点赞或点踩后将在此展示。"> |
||||
|
<template #feedbackType="{ row }"> |
||||
|
<t-tag :theme="row.feedbackType === 'THUMBS_UP' ? 'success' : 'danger'" variant="light"> |
||||
|
{{ row.feedbackType === 'THUMBS_UP' ? '👍 有帮助' : '👎 没帮助' }} |
||||
|
</t-tag> |
||||
|
</template> |
||||
|
<template #reasonCategory="{ row }"> |
||||
|
<t-tag v-if="row.reasonCategory" theme="warning" variant="light">{{ reasonLabel(row.reasonCategory) }}</t-tag> |
||||
|
<span v-else>-</span> |
||||
|
</template> |
||||
|
<template #processed="{ row }"> |
||||
|
<t-tag :theme="row.processed ? 'success' : 'default'" variant="light">{{ row.processed ? '已处理' : '待处理' }}</t-tag> |
||||
|
</template> |
||||
|
<template #userQuestion="{ row }"> |
||||
|
<span :title="row.userQuestion" style="max-width:250px;overflow:hidden;text-overflow:ellipsis;display:inline-block;">{{ truncate(row.userQuestion, 60) }}</span> |
||||
|
</template> |
||||
|
<template #assistantAnswer="{ row }"> |
||||
|
<span :title="row.assistantAnswer" style="max-width:250px;overflow:hidden;text-overflow:ellipsis;display:inline-block;">{{ truncate(row.assistantAnswer, 60) }}</span> |
||||
|
</template> |
||||
|
<template #op="{ row }"> |
||||
|
<t-space :size="4"> |
||||
|
<t-button size="small" variant="text" @click="viewDetail(row)">查看</t-button> |
||||
|
<t-button size="small" variant="text" theme="primary" @click="createFaqFromRow(row)" v-if="row.feedbackType === 'THUMBS_UP'">生成 FAQ</t-button> |
||||
|
<t-button size="small" variant="text" theme="warning" @click="openFaqAction(row)" v-if="row.feedbackType === 'THUMBS_DOWN'">关联 FAQ</t-button> |
||||
|
<t-button size="small" variant="text" v-if="!row.processed" @click="markHandled(row)">标记已处理</t-button> |
||||
|
</t-space> |
||||
|
</template> |
||||
|
</t-table> |
||||
|
|
||||
|
<!-- 消息详情弹窗 --> |
||||
|
<t-dialog v-model:visible="detailModal.visible" header="会话消息详情" width="800px" :footer="false"> |
||||
|
<div v-if="!detailModal.messages?.length"><t-empty /></div> |
||||
|
<div v-else class="msg-list" style="max-height:400px;overflow-y:auto;"> |
||||
|
<div v-for="(msg, i) in detailModal.messages" :key="i" :class="['msg-item', msg.messageType === 'USER' ? 'msg-user' : msg.messageType === 'ASSISTANT' ? 'msg-assistant' : 'msg-system']"> |
||||
|
<div class="msg-meta"> |
||||
|
<t-tag size="small" :theme="msg.messageType === 'USER' ? 'primary' : msg.messageType === 'ASSISTANT' ? 'success' : 'default'" variant="light"> |
||||
|
{{ msg.messageType === 'USER' ? '用户' : msg.messageType === 'ASSISTANT' ? 'AI助手' : '系统' }} |
||||
|
</t-tag> |
||||
|
<t-tag v-if="msg.feedback" size="small" :theme="msg.feedback.feedbackType === 'THUMBS_UP' ? 'success' : 'danger'" variant="light"> |
||||
|
{{ msg.feedback.feedbackType === 'THUMBS_UP' ? '👍 有帮助' : '👎 没帮助' }} |
||||
|
<span v-if="msg.feedback.reasonCategory"> · {{ reasonLabel(msg.feedback.reasonCategory) }}</span> |
||||
|
</t-tag> |
||||
|
</div> |
||||
|
<div class="msg-body" v-html="msg.messageType === 'ASSISTANT' ? renderMarkdown(msg.content) : msg.content"></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</t-dialog> |
||||
|
|
||||
|
<!-- FAQ 操作弹窗(复用) --> |
||||
|
<t-dialog v-model:visible="faqModal.visible" :header="faqModal.title" width="600px" :footer="false"> |
||||
|
<t-form label-align="top"> |
||||
|
<t-form-item v-if="faqModal.mode === 'create'" label="标准问题 *"><t-input v-model="faqModal.question" /></t-form-item> |
||||
|
<t-form-item v-if="faqModal.mode === 'create'" label="标准答案 *"><t-textarea v-model="faqModal.answer" :autosize="{ minRows: 3, maxRows: 6 }" /></t-form-item> |
||||
|
<t-form-item v-if="faqModal.mode === 'append'" label="选择目标 FAQ"><t-select v-model="faqModal.targetFaqId" :options="faqOptions" placeholder="搜索并选择" filterable /></t-form-item> |
||||
|
<t-form-item v-if="faqModal.mode === 'append'" label="相似问法"><t-input v-model="faqModal.similarQuestion" /></t-form-item> |
||||
|
<t-form-item v-if="faqModal.mode === 'edit'" label="选择目标 FAQ"><t-select v-model="faqModal.targetFaqId" :options="faqOptions" placeholder="搜索并选择" filterable /></t-form-item> |
||||
|
<t-form-item v-if="faqModal.mode === 'edit'" label="修正后的答案"><t-textarea v-model="faqModal.answer" :autosize="{ minRows: 3, maxRows: 6 }" /></t-form-item> |
||||
|
<t-form-item v-if="faqModal.mode === 'create'" label="分类"><t-select v-model="faqModal.categoryId" :options="categorySelectOptions" placeholder="不分类" clearable /></t-form-item> |
||||
|
<t-form-item v-if="faqModal.mode === 'create'" label="优先级"><t-input-number v-model="faqModal.priority" :min="0" style="width:100%;" /></t-form-item> |
||||
|
</t-form> |
||||
|
<div class="dialog-footer"> |
||||
|
<t-button variant="outline" @click="faqModal.visible = false">取消</t-button> |
||||
|
<t-button theme="primary" @click="submitFaqAction" :disabled="!canSubmit">确定</t-button> |
||||
|
</div> |
||||
|
</t-dialog> |
||||
|
</t-card> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref, onMounted, computed } from 'vue' |
||||
|
import { listFeedbackConversations, getFeedbackMessages, markFeedbackProcessed } from '@/api/feedback' |
||||
|
import { listFaqs, createFaqFromFeedback, appendFaqFromFeedback, editFaqFromFeedback } from '@/api/faq' |
||||
|
import { getCategoryTree } from '@/api/category' |
||||
|
import { toast } from '@/utils/toast' |
||||
|
import { renderMarkdown } from '@/utils/markdown' |
||||
|
|
||||
|
const filterFeedbackType = ref(''); const filterReasonCategory = ref(''); const filterProcessed = ref(''); const filterDate = ref('') |
||||
|
const feedbacks = ref<any[]>([]); const loading = ref(false) |
||||
|
const page = ref(1); const pageSize = ref(20); const total = ref(0) |
||||
|
|
||||
|
const detailModal = ref({ visible: false, messages: [] as any[] }) |
||||
|
const faqModal = ref<{ |
||||
|
visible: boolean; title: string; mode: 'create' | 'append' | 'edit'; |
||||
|
question: string; answer: string; similarQuestion: string; |
||||
|
targetFaqId: string; categoryId: any; priority: number; |
||||
|
sourceRow: any; |
||||
|
}>({ visible: false, title: '', mode: 'create', question: '', answer: '', similarQuestion: '', targetFaqId: '', categoryId: null, priority: 0, sourceRow: null }) |
||||
|
|
||||
|
const faqList = ref<any[]>([]); const flatCategories = ref<any[]>([]) |
||||
|
|
||||
|
const feedbackTypeOptions = [ |
||||
|
{ label: '👍 有帮助', value: 'THUMBS_UP' }, |
||||
|
{ label: '👎 没帮助', value: 'THUMBS_DOWN' }, |
||||
|
] |
||||
|
|
||||
|
const reasonCategoryOptions = [ |
||||
|
{ label: '不准确', value: 'inaccurate' }, |
||||
|
{ label: '不相关', value: 'irrelevant' }, |
||||
|
{ label: '不完整', value: 'incomplete' }, |
||||
|
{ label: '其他', value: 'other' }, |
||||
|
] |
||||
|
|
||||
|
const processedOptions = [ |
||||
|
{ label: '待处理', value: 'false' }, |
||||
|
{ label: '已处理', value: 'true' }, |
||||
|
] |
||||
|
|
||||
|
const columns = [ |
||||
|
{ colKey: 'feedbackType', title: '反馈', width: 90, cell: 'feedbackType' }, |
||||
|
{ colKey: 'reasonCategory', title: '原因', width: 80, cell: 'reasonCategory' }, |
||||
|
{ colKey: 'userQuestion', title: '用户问题', width: 200, ellipsis: true, cell: 'userQuestion' }, |
||||
|
{ colKey: 'assistantAnswer', title: 'AI 回复', width: 200, ellipsis: true, cell: 'assistantAnswer' }, |
||||
|
{ colKey: 'processed', title: '状态', width: 70, cell: 'processed' }, |
||||
|
{ colKey: 'feedbackTime', title: '时间', width: 150 }, |
||||
|
{ colKey: 'op', title: '操作', width: 240, cell: 'op' }, |
||||
|
] |
||||
|
|
||||
|
const canSubmit = computed(() => { |
||||
|
const m = faqModal.value |
||||
|
if (m.mode === 'create') return !!(m.question && m.answer) |
||||
|
if (m.mode === 'append') return !!(m.targetFaqId && m.similarQuestion) |
||||
|
if (m.mode === 'edit') return !!(m.targetFaqId && m.answer) |
||||
|
return false |
||||
|
}) |
||||
|
|
||||
|
const categorySelectOptions = computed(() => |
||||
|
flatCategories.value.map((c: any) => ({ label: c.path || c.name, value: c.id })) |
||||
|
) |
||||
|
|
||||
|
const faqOptions = computed(() => |
||||
|
faqList.value.map((f: any) => ({ label: f.question, value: String(f.id) })) |
||||
|
) |
||||
|
|
||||
|
function reasonLabel(key: string): string { |
||||
|
const map: Record<string, string> = { inaccurate: '不准确', irrelevant: '不相关', incomplete: '不完整', other: '其他' } |
||||
|
return map[key] || key || '-' |
||||
|
} |
||||
|
|
||||
|
function truncate(text: string, maxLen: number): string { |
||||
|
if (!text) return '' |
||||
|
return text.length > maxLen ? text.substring(0, maxLen) + '…' : text |
||||
|
} |
||||
|
|
||||
|
/** 将后端返回的 snake_case 字段归一化为 camelCase,统一后续访问方式 */ |
||||
|
function normalizeFeedbackRow(item: any): any { |
||||
|
return { |
||||
|
...item, |
||||
|
feedbackId: item.feedback_id || item.feedbackId, |
||||
|
feedbackType: item.feedback_type || item.feedbackType, |
||||
|
reasonCategory: item.reason_category || item.reasonCategory, |
||||
|
reasonComment: item.reason_comment || item.reasonComment, |
||||
|
feedbackTime: item.feedback_time || item.feedbackTime, |
||||
|
userQuestion: item.user_question || item.userQuestion, |
||||
|
assistantAnswer: item.assistant_answer || item.assistantAnswer, |
||||
|
conversationId: item.conversation_id || item.conversationId, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMounted(() => { loadList(1); loadFaqList(); loadCategories() }) |
||||
|
|
||||
|
async function loadFaqList() { try { const r = await listFaqs(1, 100); if (r.success) faqList.value = r.data?.records || r.data || [] } catch {/* */} } |
||||
|
|
||||
|
async function loadCategories() { |
||||
|
try { const r = await getCategoryTree(); if (r.success) { flatCategories.value = flattenTree(r.data) } } catch {/* */} |
||||
|
} |
||||
|
function flattenTree(tree: any[], prefix = ''): any[] { |
||||
|
const r: any[] = [] |
||||
|
for (const n of (tree || [])) { const p = prefix ? `${prefix}/${n.name}` : n.name; r.push({ id: n.id, name: n.name, path: p }); if (n.children?.length) r.push(...flattenTree(n.children, p)) } |
||||
|
return r |
||||
|
} |
||||
|
|
||||
|
async function loadList(p: number) { |
||||
|
page.value = p; loading.value = true |
||||
|
try { |
||||
|
const params: Record<string, any> = { page: p, size: pageSize.value } |
||||
|
if (filterFeedbackType.value) params.feedbackType = filterFeedbackType.value |
||||
|
if (filterReasonCategory.value) params.reasonCategory = filterReasonCategory.value |
||||
|
if (filterProcessed.value) params.processed = filterProcessed.value === 'true' |
||||
|
if (filterDate.value) { |
||||
|
// TDesign DatePicker 返回格式视配置而定,这里兼容数组和字符串 |
||||
|
if (Array.isArray(filterDate.value) && filterDate.value.length >= 2) { |
||||
|
params.startDate = filterDate.value[0] |
||||
|
params.endDate = filterDate.value[1] |
||||
|
} |
||||
|
} |
||||
|
const r = await listFeedbackConversations(params) |
||||
|
if (r.success) { |
||||
|
feedbacks.value = (r.data?.records || []).map(normalizeFeedbackRow) |
||||
|
total.value = r.data?.total || 0 |
||||
|
} |
||||
|
else toast(r.message || '查询失败', 'error') |
||||
|
} catch (e: any) { toast('加载失败:' + e.message, 'error') } |
||||
|
finally { loading.value = false } |
||||
|
} |
||||
|
|
||||
|
function onPageChange(i: { current: number; pageSize: number }) { pageSize.value = i.pageSize; loadList(i.current) } |
||||
|
|
||||
|
async function viewDetail(row: any) { |
||||
|
try { |
||||
|
const r = await getFeedbackMessages(row.conversationId || row.conversation_id) |
||||
|
detailModal.value.visible = true; detailModal.value.messages = r.data || [] |
||||
|
} catch (e: any) { toast('加载消息失败:' + e.message, 'error') } |
||||
|
} |
||||
|
|
||||
|
async function markHandled(row: any) { |
||||
|
const fbId = row.feedbackId || row.feedback_id |
||||
|
if (!fbId) { toast('未找到反馈记录', 'error'); return } |
||||
|
try { |
||||
|
await markFeedbackProcessed(fbId, { remark: '手动标记已处理' }) |
||||
|
toast('已标记', 'success') |
||||
|
loadList(page.value) |
||||
|
} catch (e: any) { toast('操作失败:' + e.message, 'error') } |
||||
|
} |
||||
|
|
||||
|
function createFaqFromRow(row: any) { |
||||
|
// 预填问题来自后端 SQL 中的 user_question / userQuestion 字段名 |
||||
|
faqModal.value = { |
||||
|
visible: true, title: '从点赞反馈生成 FAQ', mode: 'create', |
||||
|
question: truncate(row.userQuestion || row.user_question || '', 500), |
||||
|
answer: row.assistantAnswer || row.assistant_answer || '', |
||||
|
similarQuestion: '', targetFaqId: '', categoryId: null, priority: 0, sourceRow: row, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function openFaqAction(row: any) { |
||||
|
loadFaqList() |
||||
|
const q = truncate(row.userQuestion || row.user_question || '', 500) |
||||
|
if (row.reasonCategory === 'inaccurate' || row.reasonCategory === 'incomplete') { |
||||
|
faqModal.value = { |
||||
|
visible: true, title: '修正 FAQ 答案', mode: 'edit', |
||||
|
question: '', answer: row.assistantAnswer || row.assistant_answer || '', |
||||
|
similarQuestion: '', targetFaqId: '', categoryId: null, priority: 0, sourceRow: row, |
||||
|
} |
||||
|
} else { |
||||
|
faqModal.value = { |
||||
|
visible: true, title: '补充到已有 FAQ', mode: 'append', |
||||
|
question: '', answer: '', similarQuestion: q, |
||||
|
targetFaqId: '', categoryId: null, priority: 0, sourceRow: row, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function submitFaqAction() { |
||||
|
const m = faqModal.value |
||||
|
const row = m.sourceRow |
||||
|
const userQ = (row.userQuestion || row.user_question || '').substring(0, 500) |
||||
|
const aiA = (row.assistantAnswer || row.assistant_answer || '').substring(0, 10000) |
||||
|
const fbId = row.feedbackId || row.feedback_id |
||||
|
try { |
||||
|
if (m.mode === 'create') { |
||||
|
const r = await createFaqFromFeedback({ |
||||
|
feedbackId: fbId, |
||||
|
question: m.question, answer: m.answer, |
||||
|
categoryId: m.categoryId, priority: m.priority, |
||||
|
source: 'feedback_positive', |
||||
|
originalQuestion: userQ, originalAnswer: aiA, |
||||
|
}) |
||||
|
if (r.success) { toast('FAQ 创建成功', 'success'); faqModal.value.visible = false; loadList(page.value) } |
||||
|
else toast(r.message || '创建失败', 'error') |
||||
|
} else if (m.mode === 'append') { |
||||
|
const r = await appendFaqFromFeedback(m.targetFaqId, { |
||||
|
feedbackId: fbId, similarQuestion: m.similarQuestion, originalAnswer: aiA, |
||||
|
}) |
||||
|
if (r.success) { toast('补充成功', 'success'); faqModal.value.visible = false; loadList(page.value) } |
||||
|
else toast(r.message || '补充失败', 'error') |
||||
|
} else if (m.mode === 'edit') { |
||||
|
const r = await editFaqFromFeedback(m.targetFaqId, { |
||||
|
feedbackId: fbId, answer: m.answer, originalQuestion: userQ, |
||||
|
}) |
||||
|
if (r.success) { toast('修正成功', 'success'); faqModal.value.visible = false; loadList(page.value) } |
||||
|
else toast(r.message || '修正失败', 'error') |
||||
|
} |
||||
|
} catch (e: any) { toast('操作失败:' + e.message, 'error') } |
||||
|
} |
||||
|
</script> |
||||
|
<style scoped> |
||||
|
.msg-item { padding: 12px; border-radius: 8px; margin-bottom: 12px; } |
||||
|
.msg-user { background: #eff6ff; } |
||||
|
.msg-assistant { background: #f9fafb; } |
||||
|
.msg-system { background: #fef3c7; } |
||||
|
.msg-meta { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; } |
||||
|
.msg-body { font-size: 13px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; } |
||||
|
.dialog-footer { margin-top: 16px; display: flex; justify-content: flex-end; gap: 8px; } |
||||
|
</style> |
||||
@ -0,0 +1,34 @@ |
|||||
|
package com.wok.supportbot.dao; |
||||
|
|
||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
||||
|
import com.wok.supportbot.entity.FaqFeedbackLink; |
||||
|
import org.apache.ibatis.annotations.Mapper; |
||||
|
import org.apache.ibatis.annotations.Param; |
||||
|
import org.apache.ibatis.annotations.Select; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* FAQ 与反馈维护关联表 Mapper |
||||
|
*/ |
||||
|
@Mapper |
||||
|
public interface FaqFeedbackLinkMapper extends BaseMapper<FaqFeedbackLink> { |
||||
|
|
||||
|
/** |
||||
|
* 根据 FAQ ID 查询关联的反馈维护记录 |
||||
|
* |
||||
|
* @param faqId FAQ ID |
||||
|
* @return 关联记录列表 |
||||
|
*/ |
||||
|
@Select("SELECT * FROM faq_feedback_link WHERE faq_id = #{faqId} AND is_delete = false ORDER BY create_time DESC") |
||||
|
List<FaqFeedbackLink> selectByFaqId(@Param("faqId") Long faqId); |
||||
|
|
||||
|
/** |
||||
|
* 根据反馈 ID 查询关联的维护记录 |
||||
|
* |
||||
|
* @param feedbackId 反馈 ID |
||||
|
* @return 关联记录列表 |
||||
|
*/ |
||||
|
@Select("SELECT * FROM faq_feedback_link WHERE feedback_id = #{feedbackId} AND is_delete = false ORDER BY create_time DESC") |
||||
|
List<FaqFeedbackLink> selectByFeedbackId(@Param("feedbackId") Long feedbackId); |
||||
|
} |
||||
@ -0,0 +1,94 @@ |
|||||
|
package com.wok.supportbot.entity; |
||||
|
|
||||
|
import com.baomidou.mybatisplus.annotation.*; |
||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize; |
||||
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Builder; |
||||
|
import lombok.Data; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
import java.io.Serial; |
||||
|
import java.io.Serializable; |
||||
|
import java.util.Date; |
||||
|
|
||||
|
/** |
||||
|
* FAQ 与反馈维护关联实体 |
||||
|
* 记录哪条反馈通过何种动作维护到了哪条 FAQ |
||||
|
*/ |
||||
|
@Data |
||||
|
@Builder |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
@TableName("faq_feedback_link") |
||||
|
public class FaqFeedbackLink implements Serializable { |
||||
|
|
||||
|
@Serial |
||||
|
@TableField(exist = false) |
||||
|
private static final long serialVersionUID = 1L; |
||||
|
|
||||
|
/** |
||||
|
* 主键 ID(雪花算法) |
||||
|
*/ |
||||
|
@TableId(value = "id", type = IdType.ASSIGN_ID) |
||||
|
@JsonSerialize(using = ToStringSerializer.class) |
||||
|
private Long id; |
||||
|
|
||||
|
/** |
||||
|
* 关联反馈 ID |
||||
|
*/ |
||||
|
@TableField("feedback_id") |
||||
|
@JsonSerialize(using = ToStringSerializer.class) |
||||
|
private Long feedbackId; |
||||
|
|
||||
|
/** |
||||
|
* 关联 FAQ ID(创建前可能为空) |
||||
|
*/ |
||||
|
@TableField("faq_id") |
||||
|
@JsonSerialize(using = ToStringSerializer.class) |
||||
|
private Long faqId; |
||||
|
|
||||
|
/** |
||||
|
* 维护动作类型: create / append_similar / edit_answer / mark_resolved |
||||
|
*/ |
||||
|
@TableField("action_type") |
||||
|
private String actionType; |
||||
|
|
||||
|
/** |
||||
|
* 用户原始问题快照 |
||||
|
*/ |
||||
|
@TableField("original_question") |
||||
|
private String originalQuestion; |
||||
|
|
||||
|
/** |
||||
|
* AI 原回复快照 |
||||
|
*/ |
||||
|
@TableField("original_answer") |
||||
|
private String originalAnswer; |
||||
|
|
||||
|
/** |
||||
|
* 运营人员 ID |
||||
|
*/ |
||||
|
@TableField("operator_id") |
||||
|
@JsonSerialize(using = ToStringSerializer.class) |
||||
|
private Long operatorId; |
||||
|
|
||||
|
/** |
||||
|
* 备注 |
||||
|
*/ |
||||
|
@TableField("remark") |
||||
|
private String remark; |
||||
|
|
||||
|
/** |
||||
|
* 创建时间 |
||||
|
*/ |
||||
|
@TableField(value = "create_time", fill = FieldFill.INSERT) |
||||
|
private Date createTime; |
||||
|
|
||||
|
/** |
||||
|
* 是否删除 false-未删除 true-已删除 |
||||
|
*/ |
||||
|
@TableField("is_delete") |
||||
|
@TableLogic |
||||
|
private boolean isDelete; |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue