新增: AI工单处理工作台 v1.0
- Go Gin 后端 (19个源文件): 认证、工单CRUD、GLM AI分析、状态流转、备注、操作日志 - Arco Design Vue 前端: 登录、工单列表/详情/创建、AI分析触发与确认 - MySQL 5表: ticket_user/ticket_info/ticket_ai_analysis/ticket_operation_log/ticket_note - 部署: tk.1216.top HTTPS, Nginx反代
This commit is contained in:
161
backend/internal/service/analysis_service.go
Normal file
161
backend/internal/service/analysis_service.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/casehub/ticket-workbench/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type GLMRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []Message `json:"messages"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type GLMResponse struct {
|
||||
Choices []Choice `json:"choices"`
|
||||
}
|
||||
|
||||
type Choice struct {
|
||||
Message Message `json:"message"`
|
||||
}
|
||||
|
||||
type AnalysisResult struct {
|
||||
Category string `json:"category"`
|
||||
Priority json.Number `json:"priority"`
|
||||
Summary string `json:"summary"`
|
||||
SuggestRole string `json:"suggest_role"`
|
||||
}
|
||||
|
||||
func AnalyzeTicket(db *gorm.DB, ticketid int, apikey, baseURL, glmModel, title, content, contactname, contactphone string) (*model.TicketAiAnalysis, error) {
|
||||
prompt := fmt.Sprintf(`你是一个客服工单分析助手。请分析以下工单内容,返回JSON格式的分析结果:
|
||||
{
|
||||
"category": "分类(refund/login/invoice/logistics/account/inquiry/other)",
|
||||
"priority": "优先级(0=P0紧急,1=P1高,2=P2中,3=P3低)",
|
||||
"summary": "一句话摘要",
|
||||
"suggest_role": "建议处理团队(refund_team/tech_support/finance_team/logistics_team/customer_service)"
|
||||
}
|
||||
|
||||
工单标题: %s
|
||||
工单内容: %s
|
||||
联系人: %s
|
||||
联系电话: %s
|
||||
|
||||
请只返回JSON,不要其他内容。`, title, content, contactname, contactphone)
|
||||
|
||||
reqBody := GLMRequest{
|
||||
Model: glmModel,
|
||||
Messages: []Message{
|
||||
{Role: "system", Content: "你是一个专业的客服工单分析助手,擅长对工单进行分类、优先级判断和智能分派。"},
|
||||
{Role: "user", Content: prompt},
|
||||
},
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(reqBody)
|
||||
|
||||
httpReq, err := http.NewRequest("POST", baseURL+"/chat/completions", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Authorization", "Bearer "+apikey)
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var glmResp GLMResponse
|
||||
if err := json.Unmarshal(body, &glmResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(glmResp.Choices) == 0 {
|
||||
return nil, fmt.Errorf("AI返回为空")
|
||||
}
|
||||
|
||||
// Clean markdown code block wrapping from AI response
|
||||
aiContent := strings.TrimSpace(glmResp.Choices[0].Message.Content)
|
||||
aiContent = strings.TrimPrefix(aiContent, "```json")
|
||||
aiContent = strings.TrimPrefix(aiContent, "```")
|
||||
aiContent = strings.TrimSuffix(aiContent, "```")
|
||||
aiContent = strings.TrimSpace(aiContent)
|
||||
|
||||
var result AnalysisResult
|
||||
if err := json.Unmarshal([]byte(aiContent), &result); err != nil {
|
||||
return nil, fmt.Errorf("解析AI响应失败: %v, 原始内容: %s", err, aiContent)
|
||||
}
|
||||
|
||||
priority := int16(2) // default P2
|
||||
if n, err := result.Priority.Int64(); err == nil {
|
||||
priority = int16(n)
|
||||
}
|
||||
|
||||
analysis := &model.TicketAiAnalysis{
|
||||
Ticketid: ticketid,
|
||||
Category: result.Category,
|
||||
Priority: priority,
|
||||
Summary: result.Summary,
|
||||
Suggestrole: result.SuggestRole,
|
||||
Rawresponse: glmResp.Choices[0].Message.Content,
|
||||
Confirmed: 0,
|
||||
Createtime: time.Now(),
|
||||
}
|
||||
|
||||
if err := db.Create(analysis).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
AddOperationLog(db, ticketid, 0, "analyze", "AI分析: "+result.Summary)
|
||||
|
||||
return analysis, nil
|
||||
}
|
||||
|
||||
func GetAnalysisByTicketID(db *gorm.DB, ticketid int) ([]model.TicketAiAnalysis, error) {
|
||||
var analyses []model.TicketAiAnalysis
|
||||
err := db.Where("ticketid = ?", ticketid).Order("createtime DESC").Find(&analyses).Error
|
||||
return analyses, err
|
||||
}
|
||||
|
||||
func ConfirmAnalysis(db *gorm.DB, analysisid int, category string, priority int16, summary string, confirmedby int) error {
|
||||
updates := map[string]interface{}{
|
||||
"confirmed": 1,
|
||||
"confirmedby": confirmedby,
|
||||
"confirmedat": time.Now(),
|
||||
}
|
||||
if category != "" {
|
||||
updates["category"] = category
|
||||
}
|
||||
if priority >= 0 {
|
||||
updates["priority"] = priority
|
||||
}
|
||||
if summary != "" {
|
||||
updates["summary"] = summary
|
||||
}
|
||||
|
||||
if err := db.Model(&model.TicketAiAnalysis{}).Where("analysisid = ?", analysisid).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var analysis model.TicketAiAnalysis
|
||||
db.Where("analysisid = ?", analysisid).First(&analysis)
|
||||
AddOperationLog(db, analysis.Ticketid, confirmedby, "confirm_analysis", "确认AI分析结果")
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user