refactor: remove Subject field from Conversation model and related logic, replace with CustomerName

This commit is contained in:
mlogclub
2026-04-27 19:22:40 +08:00
parent 2d2d2d2051
commit 3a9bc4914a
24 changed files with 49 additions and 64 deletions
@@ -84,9 +84,6 @@ func buildAnalyzeConversationResult(conversation models.Conversation, messages [
func buildConversationSummary(conversation models.Conversation, messages []models.Message, input AnalyzeConversationInput) string {
parts := make([]string, 0, 4)
if strings.TrimSpace(conversation.Subject) != "" {
parts = append(parts, "会话主题:"+strings.TrimSpace(conversation.Subject))
}
if input.ObservedIssue != "" {
parts = append(parts, "当前问题:"+input.ObservedIssue)
} else if strings.TrimSpace(conversation.LastMessageSummary) != "" {
@@ -103,7 +100,6 @@ func buildConversationSummary(conversation models.Conversation, messages []model
func buildConversationCorpus(conversation models.Conversation, messages []models.Message, input AnalyzeConversationInput) string {
parts := make([]string, 0, len(messages)+4)
parts = append(parts, strings.TrimSpace(conversation.Subject))
parts = append(parts, strings.TrimSpace(conversation.LastMessageSummary))
parts = append(parts, input.Goal, input.ObservedIssue, input.AdditionalContext)
for i := range messages {
@@ -200,9 +196,6 @@ func recommendQuestions(intent string, signals []string, input AnalyzeConversati
func buildConversationAnalysisFacts(conversation models.Conversation, messages []models.Message) []string {
facts := make([]string, 0, 4)
if strings.TrimSpace(conversation.Subject) != "" {
facts = append(facts, "会话主题:"+strings.TrimSpace(conversation.Subject))
}
if strings.TrimSpace(conversation.LastMessageSummary) != "" {
facts = append(facts, "最近摘要:"+strings.TrimSpace(conversation.LastMessageSummary))
}
@@ -9,7 +9,6 @@ import (
func TestBuildAnalyzeConversationResult_RecommendsHandoffForComplaint(t *testing.T) {
conversation := models.Conversation{
Subject: "用户投诉扣费异常",
LastMessageSummary: "用户反馈被重复扣费,并要求人工处理",
}
messages := []models.Message{
@@ -33,7 +32,6 @@ func TestBuildAnalyzeConversationResult_RecommendsHandoffForComplaint(t *testing
func TestBuildAnalyzeConversationResult_RecommendsPrepareTicket(t *testing.T) {
conversation := models.Conversation{
Subject: "订单无法支付",
LastMessageSummary: "用户要求登记问题并尽快处理",
}
messages := []models.Message{
@@ -114,7 +114,7 @@ func (g *CreateTicketGraph) buildCreateRequest(argumentsInJSON string) (request.
req.Priority = args.Priority
req.Severity = args.Severity
if req.Title == "" {
req.Title = strings.TrimSpace(g.conversation.Subject)
req.Title = strings.TrimSpace(g.conversation.LastMessageSummary)
}
if req.Description == "" {
req.Description = strings.TrimSpace(g.conversation.LastMessageSummary)
@@ -100,8 +100,6 @@ func buildDraftTitle(conversation models.Conversation, input PrepareTicketDraftI
return limitText(input.Title, 80)
case input.Issue != "":
return limitText(input.Issue, 80)
case strings.TrimSpace(conversation.Subject) != "":
return limitText(conversation.Subject, 80)
case strings.TrimSpace(conversation.LastMessageSummary) != "":
return limitText(conversation.LastMessageSummary, 80)
default:
@@ -144,9 +142,6 @@ func hasSufficientIssueContext(input PrepareTicketDraftInput, description string
func buildConversationFacts(conversation models.Conversation, messages []models.Message) []string {
facts := make([]string, 0, 4)
if strings.TrimSpace(conversation.Subject) != "" {
facts = append(facts, "会话主题:"+strings.TrimSpace(conversation.Subject))
}
if strings.TrimSpace(conversation.LastMessageSummary) != "" {
facts = append(facts, "最近摘要:"+strings.TrimSpace(conversation.LastMessageSummary))
}
@@ -9,7 +9,6 @@ import (
func TestBuildPrepareTicketDraftResult_UsesConversationFallbacks(t *testing.T) {
conversation := models.Conversation{
Subject: "企业微信登录异常",
LastMessageSummary: "用户反馈企业微信扫码后页面空白,无法进入工作台",
}
messages := []models.Message{
@@ -38,7 +37,6 @@ func TestBuildPrepareTicketDraftResult_UsesConversationFallbacks(t *testing.T) {
func TestBuildPrepareTicketDraftResult_ReadyWithExplicitIssue(t *testing.T) {
conversation := models.Conversation{
Subject: "订单支付失败",
LastMessageSummary: "用户反馈连续支付失败",
}
@@ -9,7 +9,6 @@ import (
func TestTriageServiceRequestResult_PrepareTicket(t *testing.T) {
conversation := models.Conversation{
Subject: "支付失败需要登记工单",
LastMessageSummary: "用户要求建单跟进支付失败问题",
}
messages := []models.Message{
@@ -31,7 +30,6 @@ func TestTriageServiceRequestResult_PrepareTicket(t *testing.T) {
func TestTriageServiceRequestResult_Handoff(t *testing.T) {
conversation := models.Conversation{
Subject: "投诉转人工",
LastMessageSummary: "用户要求人工处理扣费投诉",
}
messages := []models.Message{
+11 -1
View File
@@ -19,7 +19,7 @@ func BuildConversation(item *models.Conversation) response.ConversationResponse
AIAgentID: item.AIAgentID,
ChannelID: item.ChannelID,
CustomerID: item.CustomerID,
Subject: item.Subject,
CustomerName: buildConversationCustomerName(item.CustomerID),
Status: item.Status,
ServiceMode: item.ServiceMode,
Priority: item.Priority,
@@ -62,6 +62,16 @@ func BuildConversation(item *models.Conversation) response.ConversationResponse
return ret
}
func buildConversationCustomerName(customerID int64) string {
if customerID <= 0 {
return ""
}
if customer := services.CustomerService.Get(customerID); customer != nil {
return strings.TrimSpace(customer.Name)
}
return ""
}
func BuildParticipantResponses(conversationID int64) []response.ConversationParticipantResponse {
list := services.ConversationParticipantService.Find(sqls.NewCnd().Eq("conversation_id", conversationID).Asc("id"))
if len(list) == 0 {
@@ -36,7 +36,7 @@ func (c *ConversationController) AnyList() *web.JsonResult {
if keyword, _ := params.Get(c.Ctx, "keyword"); strs.IsNotBlank(keyword) {
keywordLike := "%" + strings.TrimSpace(keyword) + "%"
cnd.Where("subject LIKE ? OR last_message_summary LIKE ?", keywordLike, keywordLike)
cnd.Where("last_message_summary LIKE ? OR customer_id IN (SELECT id FROM t_customer WHERE name LIKE ?)", keywordLike, keywordLike)
}
// 标签搜索
-1
View File
@@ -327,7 +327,6 @@ type Conversation struct {
AIAgentID int64 `gorm:"type:bigint;not null;default:0;index"` // AIAgentID 为当前会话绑定的 AI Agent ID。
ChannelID int64 `gorm:"type:bigint;not null;default:0;index"` // ChannelID 为该会话来源接入渠道ID。
CustomerID int64 `gorm:"type:bigint;not null;default:0;index"` // CustomerID 为会话所属客户 ID。
Subject string `gorm:"type:varchar(255);not null;default:''"` // Subject 为会话标题或摘要。
Status enums.IMConversationStatus `gorm:"type:int;not null;default:1;index"` // Status 为会话状态,如待接入、处理中、已关闭。
ServiceMode enums.IMConversationServiceMode `gorm:"type:int;not null;default:3;index"` // ServiceMode 为服务模式,如仅AI、仅人工、AI优先人工接管。
Priority int `gorm:"type:int;not null;default:0;index"` // Priority 为会话优先级。
@@ -22,7 +22,7 @@ type ConversationResponse struct {
AIAgentID int64 `json:"aiAgentId"`
ChannelID int64 `json:"channelId"`
CustomerID int64 `json:"customerId"`
Subject string `json:"subject"`
CustomerName string `json:"customerName"`
Status enums.IMConversationStatus `json:"status"`
ServiceMode enums.IMConversationServiceMode `json:"serviceMode"`
Priority int `json:"priority"`
+6 -24
View File
@@ -2,10 +2,7 @@ package services
import (
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"log/slog"
"cs-agent/internal/events"
@@ -61,7 +58,8 @@ func (s *conversationService) ListConversations(userID int64, filter request.Age
if strs.IsNotBlank(keyword) {
keyword = strings.TrimSpace(keyword)
cnd.Where("subject LIKE ? OR last_message_summary LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
keywordLike := "%" + keyword + "%"
cnd.Where("last_message_summary LIKE ? OR customer_id IN (SELECT id FROM t_customer WHERE name LIKE ?)", keywordLike, keywordLike)
}
switch filter {
@@ -103,8 +101,6 @@ func (s *conversationService) getLatestNotFinishedByCustomerID(db *gorm.DB, cust
}
func (s *conversationService) Create(externalInfo openidentity.ExternalInfo, channelID, aiAgentID int64) (*models.Conversation, error) {
subject := s.buildDefaultSubject(externalInfo)
aiAgent := AIAgentService.Get(aiAgentID)
if aiAgent == nil || aiAgent.Status != enums.StatusOk {
return nil, errorsx.InvalidParam("AI Agent not found")
@@ -127,7 +123,6 @@ func (s *conversationService) Create(externalInfo openidentity.ExternalInfo, cha
AIAgentID: aiAgentID,
ChannelID: channelID,
CustomerID: customerID,
Subject: subject,
Status: s.resolveInitialStatus(aiAgent.ServiceMode),
ServiceMode: aiAgent.ServiceMode,
Priority: 0,
@@ -667,7 +662,10 @@ func (s *conversationService) BuildConversationSummary(conversation *models.Conv
if strings.TrimSpace(conversation.LastMessageSummary) != "" {
return conversation.LastMessageSummary
}
return strings.TrimSpace(conversation.Subject)
if customer := CustomerService.Get(conversation.CustomerID); customer != nil {
return strings.TrimSpace(customer.Name)
}
return ""
}
func (s *conversationService) canCloseConversation(conversation *models.Conversation, operator *dto.AuthPrincipal) bool {
@@ -806,22 +804,6 @@ func (s *conversationService) canLinkConversationCustomer(conv *models.Conversat
}
}
func (s *conversationService) buildDefaultSubject(externalInfo openidentity.ExternalInfo) string {
if strs.IsNotBlank(externalInfo.ExternalName) {
return externalInfo.ExternalName
}
return fmt.Sprintf("访客%s", hashUUID(externalInfo.ExternalID))
}
func hashUUID(uuid string) string {
if uuid == "" {
return "unknown"
}
h := md5.Sum([]byte(uuid))
return hex.EncodeToString(h[:])[:8]
}
func (s *conversationService) resolveInitialStatus(serviceMode enums.IMConversationServiceMode) enums.IMConversationStatus {
switch serviceMode {
case enums.IMConversationServiceModeHumanOnly:
+11
View File
@@ -1,6 +1,8 @@
package services
import (
"crypto/md5"
"encoding/hex"
"log/slog"
"cs-agent/internal/models"
@@ -154,6 +156,15 @@ func buildExternalCustomerName(externalInfo openidentity.ExternalInfo) string {
return "访客" + hashUUID(externalInfo.ExternalID)
}
func hashUUID(uuid string) string {
if uuid == "" {
return "unknown"
}
h := md5.Sum([]byte(uuid))
return hex.EncodeToString(h[:])[:8]
}
func (s *customerService) CreateCustomer(req request.CreateCustomerRequest, operator *dto.AuthPrincipal) (*models.Customer, error) {
if operator == nil {
return nil, errorsx.Unauthorized("未登录或登录已过期")
@@ -55,7 +55,7 @@ func buildConversationAssignedNotifyBody(conversation *models.Conversation, assi
}
lines := []string{
fmt.Sprintf("会话ID: #%d", conversation.ID),
fmt.Sprintf("会话主题: %s", strs.DefaultIfBlank(conversation.Subject, "-")),
fmt.Sprintf("会话摘要: %s", strs.DefaultIfBlank(services.ConversationService.BuildConversationSummary(conversation), "-")),
fmt.Sprintf("接入渠道: %s", resolveConversationChannelLabel(conversation)),
fmt.Sprintf("当前状态: %s", enums.GetIMConversationStatusLabel(conversation.Status)),
fmt.Sprintf("处理人: %s", resolveNotifyUserLabel(assigneeID)),
+1 -1
View File
@@ -852,7 +852,7 @@ func (s *ticketService) CreateFromConversation(req request.CreateTicketFromConve
}
title := strings.TrimSpace(req.Title)
if title == "" {
title = strings.TrimSpace(conversation.Subject)
title = ConversationService.BuildConversationSummary(conversation)
}
description := strings.TrimSpace(req.Description)
if description == "" {
@@ -249,7 +249,7 @@ export function ConversationDetailDialog({
onOpenChange={onOpenChange}
title={
<div className="flex items-center gap-3">
<span>{currentConversation?.subject || "会话详情"}</span>
<span>{currentConversation?.customerName || "会话详情"}</span>
<div>
{statusMeta ? (
@@ -597,7 +597,7 @@ export default function DashboardConversationsPage() {
setActionLoadingId(item.id)
try {
await markConversationRead(item.id)
toast.success(`已标记已读:${item.subject || `#${item.id}`}`)
toast.success(`已标记已读:${item.customerName || `#${item.id}`}`)
await loadConversations()
if (detailItem?.id === item.id) {
await refreshDetail()
@@ -613,7 +613,7 @@ export default function DashboardConversationsPage() {
setActionLoadingId(item.id)
try {
await dispatchConversation(item.id)
toast.success(`已触发自动分配:${item.subject || `#${item.id}`}`)
toast.success(`已触发自动分配:${item.customerName || `#${item.id}`}`)
await loadConversations()
if (detailItem?.id === item.id) {
await refreshDetail()
@@ -724,7 +724,7 @@ export default function DashboardConversationsPage() {
<TableRow key={item.id}>
<TableCell className="max-w-60">
<div className="min-w-0">
<div className="font-medium">{item.subject || `会话 #${item.id}`}</div>
<div className="font-medium">{item.customerName || `客户 #${item.customerId || item.id}`}</div>
<div className="mt-1 text-sm text-muted-foreground">
ID{item.channelId || "-"}
</div>
@@ -755,7 +755,7 @@ export default function DashboardConversationsPage() {
<DropdownMenu>
<DropdownMenuTrigger
render={<Button variant="outline" size="icon-sm" />}
aria-label={`更多操作 ${item.subject || item.id}`}
aria-label={`更多操作 ${item.customerName || item.id}`}
>
<MoreHorizontalIcon />
</DropdownMenuTrigger>
@@ -480,7 +480,7 @@ export function ChatPanel() {
<DialogTitle></DialogTitle>
<DialogDescription>
{conversation
? `确认认领“${conversation.subject}”吗?认领后会话会进入我的列表。`
? `确认认领“${conversation.customerName || `客户 #${conversation.customerId || conversation.id}`}”吗?认领后会话会进入我的列表。`
: "确认认领当前会话吗?"}
</DialogDescription>
</DialogHeader>
@@ -73,7 +73,7 @@ export function ConversationList({ onAfterSelect }: ConversationListProps) {
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<span className="min-w-0 flex-1 truncate font-medium text-sm leading-4">
{conversation.subject}
{conversation.customerName || `客户 #${conversation.customerId || conversation.id}`}
</span>
{conversation.agentUnreadCount > 0 ? (
<div className="flex size-4.5 shrink-0 items-center justify-center rounded-full bg-primary text-[10px] text-primary-foreground">
+2 -2
View File
@@ -272,7 +272,7 @@ export default function ConversationsPage() {
<div className="min-w-0">
<div className="flex items-center gap-2">
<p className="min-w-0 truncate font-medium leading-tight">
{conversation.subject}
{conversation.customerName || `客户 #${conversation.customerId || conversation.id}`}
</p>
<span
className={`inline-flex shrink-0 items-center gap-1 rounded-full border px-2 text-[11px] ${
@@ -474,7 +474,7 @@ export default function ConversationsPage() {
conversation
? {
id: conversation.id,
subject: conversation.subject,
customerName: conversation.customerName,
customerId: conversation.customerId ?? 0,
lastMessageSummary: conversation.lastMessageSummary,
currentAssigneeId: conversation.currentAssigneeId,
@@ -7,7 +7,7 @@ import { EditDialog } from "./edit"
type ConversationSeed = {
id: number
subject: string
customerName: string
customerId?: number
lastMessageSummary?: string
currentAssigneeId?: number
@@ -28,7 +28,7 @@ export function CreateTicketFromConversationDialog({
}: CreateTicketFromConversationDialogProps) {
const initialValues = conversation
? {
title: conversation.subject || "",
title: conversation.customerName || "",
description: conversation.lastMessageSummary || "",
priority: 2,
severity: 1,
+1 -1
View File
@@ -880,7 +880,7 @@ export default function TicketDetailPage() {
{sourceConversation ? (
<SurfacePanel className="p-2.5">
<div className="text-sm font-medium">
{sourceConversation.subject || "未命名会话"}
{sourceConversation.customerName || "未命名客户"}
</div>
<div className="mt-2 flex flex-wrap gap-2 text-xs text-muted-foreground">
<span>
+2 -1
View File
@@ -109,7 +109,8 @@ export type ConversationParticipant = {
export type AdminConversation = {
id: number
channelId: number
subject: string
customerId: number
customerName: string
status: number
serviceMode: number
priority: number
+1 -1
View File
@@ -37,7 +37,7 @@ export type AgentConversation = {
aiAgentId?: number
channelId?: number
customerId?: number
subject: string
customerName: string
status: number
serviceMode: number
priority: number
+1 -1
View File
@@ -34,7 +34,7 @@ export type ImConversationParticipant = {
export type ImConversation = {
id: number
channelId: number
subject: string
customerName: string
status: number
serviceMode: number
priority: number