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