新增: 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
|
||||
}
|
||||
46
backend/internal/service/auth_service.go
Normal file
46
backend/internal/service/auth_service.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/casehub/ticket-workbench/internal/model"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var sessions = make(map[string]*model.TicketUser)
|
||||
|
||||
func MD5Hash(text string) string {
|
||||
hash := md5.Sum([]byte(text))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func Login(db *gorm.DB, account, password string) (*model.TicketUser, string, error) {
|
||||
var user model.TicketUser
|
||||
err := db.Where("account = ? AND status = 1", account).First(&user).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, "", fmt.Errorf("账号或密码错误")
|
||||
}
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
if user.Password != MD5Hash(password) {
|
||||
return nil, "", fmt.Errorf("账号或密码错误")
|
||||
}
|
||||
|
||||
sessionID := uuid.New().String()
|
||||
sessions[sessionID] = &user
|
||||
|
||||
return &user, sessionID, nil
|
||||
}
|
||||
|
||||
func Logout(sessionID string) {
|
||||
delete(sessions, sessionID)
|
||||
}
|
||||
|
||||
func GetUserBySession(sessionID string) *model.TicketUser {
|
||||
return sessions[sessionID]
|
||||
}
|
||||
31
backend/internal/service/note_service.go
Normal file
31
backend/internal/service/note_service.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/casehub/ticket-workbench/internal/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func ListNotes(db *gorm.DB, ticketid int) ([]model.TicketNote, error) {
|
||||
var notes []model.TicketNote
|
||||
err := db.Where("ticketid = ?", ticketid).Order("createtime ASC").Find(¬es).Error
|
||||
return notes, err
|
||||
}
|
||||
|
||||
func AddNote(db *gorm.DB, ticketid, authorid int, content string) (*model.TicketNote, error) {
|
||||
note := &model.TicketNote{
|
||||
Ticketid: ticketid,
|
||||
Authorid: authorid,
|
||||
Content: content,
|
||||
Createtime: time.Now(),
|
||||
}
|
||||
|
||||
if err := db.Create(note).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
AddOperationLog(db, ticketid, authorid, "note", "添加备注: "+content)
|
||||
|
||||
return note, nil
|
||||
}
|
||||
149
backend/internal/service/ticket_service.go
Normal file
149
backend/internal/service/ticket_service.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/casehub/ticket-workbench/internal/model"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TicketListResult struct {
|
||||
Total int64 `json:"total"`
|
||||
Rows []model.TicketInfo `json:"rows"`
|
||||
}
|
||||
|
||||
func ListTickets(db *gorm.DB, status int16, category string, priority int16, keyword string, page, pageSize int) (*TicketListResult, error) {
|
||||
var total int64
|
||||
var tickets []model.TicketInfo
|
||||
|
||||
query := db.Model(&model.TicketInfo{})
|
||||
|
||||
if status >= 0 {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
if category != "" {
|
||||
query = query.Where("category = ?", category)
|
||||
}
|
||||
if priority >= 0 {
|
||||
query = query.Where("priority = ?", priority)
|
||||
}
|
||||
if keyword != "" {
|
||||
query = query.Where("title LIKE ? OR content LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Offset(offset).Limit(pageSize).Order("createtime DESC").Find(&tickets).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TicketListResult{Total: total, Rows: tickets}, nil
|
||||
}
|
||||
|
||||
func GetTicketByID(db *gorm.DB, ticketid int) (*model.TicketInfo, error) {
|
||||
var ticket model.TicketInfo
|
||||
err := db.Where("ticketid = ?", ticketid).First(&ticket).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ticket, nil
|
||||
}
|
||||
|
||||
func CreateTicket(db *gorm.DB, title, content, contactname, contactphone, source, category string, priority int16, submitterid int) (*model.TicketInfo, error) {
|
||||
ticketno := fmt.Sprintf("TKT%s", uuid.New().String()[:8])
|
||||
now := time.Now()
|
||||
|
||||
ticket := &model.TicketInfo{
|
||||
Ticketno: ticketno,
|
||||
Title: title,
|
||||
Content: content,
|
||||
Contactname: contactname,
|
||||
Contactphone: contactphone,
|
||||
Source: source,
|
||||
Category: category,
|
||||
Priority: priority,
|
||||
Submitterid: submitterid,
|
||||
Status: 0,
|
||||
Createtime: now,
|
||||
Updatetime: now,
|
||||
}
|
||||
|
||||
if err := db.Create(ticket).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
AddOperationLog(db, ticket.Ticketid, submitterid, "create", "创建工单")
|
||||
|
||||
return ticket, nil
|
||||
}
|
||||
|
||||
func UpdateTicket(db *gorm.DB, ticketid int, title, content, contactname, contactphone, category string, priority int16, handlerid *int, operatorid int) error {
|
||||
updates := map[string]interface{}{
|
||||
"updatetime": time.Now(),
|
||||
}
|
||||
if title != "" {
|
||||
updates["title"] = title
|
||||
}
|
||||
if content != "" {
|
||||
updates["content"] = content
|
||||
}
|
||||
if contactname != "" {
|
||||
updates["contactname"] = contactname
|
||||
}
|
||||
if contactphone != "" {
|
||||
updates["contactphone"] = contactphone
|
||||
}
|
||||
if category != "" {
|
||||
updates["category"] = category
|
||||
}
|
||||
if priority >= 0 {
|
||||
updates["priority"] = priority
|
||||
}
|
||||
if handlerid != nil {
|
||||
updates["handlerid"] = handlerid
|
||||
}
|
||||
|
||||
if err := db.Model(&model.TicketInfo{}).Where("ticketid = ?", ticketid).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
AddOperationLog(db, ticketid, operatorid, "update", "更新工单信息")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateTicketStatus(db *gorm.DB, ticketid int, status int16, operatorid int) error {
|
||||
if err := db.Model(&model.TicketInfo{}).Where("ticketid = ?", ticketid).Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"updatetime": time.Now(),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
statusText := map[int16]string{0: "待处理", 1: "分析中", 2: "已确认", 3: "处理中", 4: "已关闭"}
|
||||
AddOperationLog(db, ticketid, operatorid, "status", "状态变更为: "+statusText[status])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AddOperationLog(db *gorm.DB, ticketid, operatorid int, action, detail string) error {
|
||||
log := &model.TicketOperationLog{
|
||||
Ticketid: ticketid,
|
||||
Operatorid: operatorid,
|
||||
Action: action,
|
||||
Detail: detail,
|
||||
Createtime: time.Now(),
|
||||
}
|
||||
return db.Create(log).Error
|
||||
}
|
||||
|
||||
func GetOperationLogs(db *gorm.DB, ticketid int) ([]model.TicketOperationLog, error) {
|
||||
var logs []model.TicketOperationLog
|
||||
err := db.Where("ticketid = ?", ticketid).Order("createtime ASC").Find(&logs).Error
|
||||
return logs, err
|
||||
}
|
||||
Reference in New Issue
Block a user