新增: 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:
2026-05-13 17:05:49 +08:00
commit 4793b1a533
51 changed files with 3650 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
package config
import (
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"db"`
GLM GLMConfig `mapstructure:"glm"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
}
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
}
type GLMConfig struct {
APIKey string `mapstructure:"api_key"`
BaseURL string `mapstructure:"base_url"`
Model string `mapstructure:"model"`
}
func Load(path string) (*Config, error) {
v := viper.New()
v.SetConfigFile(path)
v.SetConfigType("yaml")
if err := v.ReadInConfig(); err != nil {
return nil, err
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}

View File

@@ -0,0 +1,28 @@
package dto
type Response struct {
Success bool `json:"success"`
Retcode int `json:"retcode"`
Retinfo string `json:"retinfo"`
Result interface{} `json:"result,omitempty"`
}
func Success(data interface{}) Response {
return Response{Success: true, Retcode: 0, Retinfo: "", Result: data}
}
func Error(code int, msg string) Response {
return Response{Success: false, Retcode: code, Retinfo: msg}
}
func Fail(msg string) Response {
return Response{Success: false, Retcode: -1, Retinfo: msg}
}
type UserSession struct {
Userid int `json:"userid"`
Username string `json:"username"`
Account string `json:"account"`
Role int16 `json:"role"`
Team string `json:"team"`
}

View File

@@ -0,0 +1,49 @@
package dto
type LoginRequest struct {
Account string `json:"account" binding:"required"`
Password string `json:"password" binding:"required"`
}
type CreateTicketRequest struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
Contactname string `json:"contactname" binding:"required"`
Contactphone string `json:"contactphone" binding:"required"`
Source string `json:"source"`
Category string `json:"category"`
Priority int16 `json:"priority"`
}
type UpdateTicketRequest struct {
Title string `json:"title"`
Content string `json:"content"`
Contactname string `json:"contactname"`
Contactphone string `json:"contactphone"`
Category string `json:"category"`
Priority int16 `json:"priority"`
Handlerid *int `json:"handlerid"`
}
type UpdateStatusRequest struct {
Status int16 `json:"status" binding:"required"`
}
type TicketListQuery struct {
Status int16 `form:"status"`
Category string `form:"category"`
Priority int16 `form:"priority"`
Keyword string `form:"keyword"`
Page int `form:"page"`
PageSize int `form:"pageSize"`
}
type ConfirmAnalysisRequest struct {
Category string `json:"category"`
Priority int16 `json:"priority"`
Summary string `json:"summary"`
}
type AddNoteRequest struct {
Content string `json:"content" binding:"required"`
}

View File

@@ -0,0 +1,95 @@
package handler
import (
"strconv"
"github.com/casehub/ticket-workbench/internal/config"
"github.com/casehub/ticket-workbench/internal/dto"
"github.com/casehub/ticket-workbench/internal/middleware"
"github.com/casehub/ticket-workbench/internal/model"
"github.com/casehub/ticket-workbench/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func AnalyzeTicket(db *gorm.DB, cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
var ticket model.TicketInfo
if err := db.Where("ticketid = ?", id).First(&ticket).Error; err != nil {
c.JSON(200, dto.Fail("工单不存在"))
return
}
analysis, err := service.AnalyzeTicket(db, id, cfg.GLM.APIKey, cfg.GLM.BaseURL, cfg.GLM.Model,
ticket.Title, ticket.Content, ticket.Contactname, ticket.Contactphone)
if err != nil {
c.JSON(200, dto.Fail("AI分析失败: "+err.Error()))
return
}
c.JSON(200, dto.Success(analysis))
}
}
func GetAnalysis(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
analyses, err := service.GetAnalysisByTicketID(db, id)
if err != nil {
c.JSON(200, dto.Fail("查询失败"))
return
}
c.JSON(200, dto.Success(analyses))
}
}
func ConfirmAnalysis(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
idStr := c.Param("id")
ticketid, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
var req dto.ConfirmAnalysisRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
user := middleware.GetCurrentUser(c)
if user == nil {
c.JSON(200, dto.Fail("未登录"))
return
}
var analysis model.TicketAiAnalysis
if err := db.Where("ticketid = ? AND confirmed = 0", ticketid).Order("createtime DESC").First(&analysis).Error; err != nil {
c.JSON(200, dto.Fail("未找到待确认的分析结果"))
return
}
err = service.ConfirmAnalysis(db, analysis.Analysisid, req.Category, req.Priority, req.Summary, user.Userid)
if err != nil {
c.JSON(200, dto.Fail("确认失败"))
return
}
c.JSON(200, dto.Success(nil))
}
}

View File

@@ -0,0 +1,64 @@
package handler
import (
"github.com/casehub/ticket-workbench/internal/dto"
"github.com/casehub/ticket-workbench/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func Login(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
var req dto.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
user, sessionID, err := service.Login(db, req.Account, req.Password)
if err != nil {
c.JSON(200, dto.Fail(err.Error()))
return
}
c.JSON(200, dto.Success(map[string]interface{}{
"token": sessionID,
"user": map[string]interface{}{
"userid": user.Userid,
"username": user.Username,
"account": user.Account,
"role": user.Role,
"team": user.Team,
},
}))
}
}
func Logout() gin.HandlerFunc {
return func(c *gin.Context) {
sessionID := c.GetHeader("Authorization")
if sessionID == "" {
sessionID = c.GetHeader("jsessionid")
}
service.Logout(sessionID)
c.JSON(200, dto.Success(nil))
}
}
func UserInfo() gin.HandlerFunc {
return func(c *gin.Context) {
userid, _ := c.Get("userid")
username, _ := c.Get("username")
account := c.GetString("account")
role, _ := c.Get("role")
team, _ := c.Get("team")
c.JSON(200, dto.Success(map[string]interface{}{
"userid": userid,
"username": username,
"account": account,
"role": role,
"team": team,
}))
}
}

View File

@@ -0,0 +1,61 @@
package handler
import (
"strconv"
"github.com/casehub/ticket-workbench/internal/dto"
"github.com/casehub/ticket-workbench/internal/middleware"
"github.com/casehub/ticket-workbench/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func ListNotes(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
notes, err := service.ListNotes(db, id)
if err != nil {
c.JSON(200, dto.Fail("查询失败"))
return
}
c.JSON(200, dto.Success(notes))
}
}
func AddNote(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
var req dto.AddNoteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
user := middleware.GetCurrentUser(c)
if user == nil {
c.JSON(200, dto.Fail("未登录"))
return
}
note, err := service.AddNote(db, id, user.Userid, req.Content)
if err != nil {
c.JSON(200, dto.Fail("添加失败"))
return
}
c.JSON(200, dto.Success(note))
}
}

View File

@@ -0,0 +1,160 @@
package handler
import (
"strconv"
"github.com/casehub/ticket-workbench/internal/dto"
"github.com/casehub/ticket-workbench/internal/middleware"
"github.com/casehub/ticket-workbench/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func ListTickets(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
var query dto.TicketListQuery
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
if query.Page <= 0 {
query.Page = 1
}
if query.PageSize <= 0 {
query.PageSize = 20
}
result, err := service.ListTickets(db, query.Status, query.Category, query.Priority, query.Keyword, query.Page, query.PageSize)
if err != nil {
c.JSON(200, dto.Fail("查询失败"))
return
}
c.JSON(200, dto.Success(result))
}
}
func GetTicket(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
ticket, err := service.GetTicketByID(db, id)
if err != nil {
c.JSON(200, dto.Fail("工单不存在"))
return
}
c.JSON(200, dto.Success(ticket))
}
}
func CreateTicket(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
var req dto.CreateTicketRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
user := middleware.GetCurrentUser(c)
if user == nil {
c.JSON(200, dto.Fail("未登录"))
return
}
ticket, err := service.CreateTicket(db, req.Title, req.Content, req.Contactname, req.Contactphone, req.Source, req.Category, req.Priority, user.Userid)
if err != nil {
c.JSON(200, dto.Fail("创建失败"))
return
}
c.JSON(200, dto.Success(ticket))
}
}
func UpdateTicket(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
var req dto.UpdateTicketRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
user := middleware.GetCurrentUser(c)
if user == nil {
c.JSON(200, dto.Fail("未登录"))
return
}
err = service.UpdateTicket(db, id, req.Title, req.Content, req.Contactname, req.Contactphone, req.Category, req.Priority, req.Handlerid, user.Userid)
if err != nil {
c.JSON(200, dto.Fail("更新失败"))
return
}
c.JSON(200, dto.Success(nil))
}
}
func UpdateTicketStatus(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
var req dto.UpdateStatusRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
user := middleware.GetCurrentUser(c)
if user == nil {
c.JSON(200, dto.Fail("未登录"))
return
}
err = service.UpdateTicketStatus(db, id, req.Status, user.Userid)
if err != nil {
c.JSON(200, dto.Fail("更新失败"))
return
}
c.JSON(200, dto.Success(nil))
}
}
func GetOperationLogs(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(200, dto.Fail("参数错误"))
return
}
logs, err := service.GetOperationLogs(db, id)
if err != nil {
c.JSON(200, dto.Fail("查询失败"))
return
}
c.JSON(200, dto.Success(logs))
}
}

View File

@@ -0,0 +1,58 @@
package middleware
import (
"github.com/casehub/ticket-workbench/internal/model"
"github.com/casehub/ticket-workbench/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func Auth(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
sessionID := extractSessionID(c)
if sessionID == "" {
c.JSON(200, map[string]interface{}{"success": false, "retcode": -1, "retinfo": "未登录"})
c.Abort()
return
}
user := service.GetUserBySession(sessionID)
if user == nil {
c.JSON(200, map[string]interface{}{"success": false, "retcode": -1, "retinfo": "登录已过期"})
c.Abort()
return
}
c.Set("userid", user.Userid)
c.Set("username", user.Username)
c.Set("role", user.Role)
c.Set("team", user.Team)
c.Next()
}
}
func extractSessionID(c *gin.Context) string {
if s := c.GetHeader("Authorization"); s != "" {
return s
}
if s := c.GetHeader("jsessionid"); s != "" {
return s
}
return ""
}
func GetCurrentUser(c *gin.Context) *model.TicketUser {
userid, exists := c.Get("userid")
if !exists {
return nil
}
username, _ := c.Get("username")
role, _ := c.Get("role")
team, _ := c.Get("team")
return &model.TicketUser{
Userid: userid.(int),
Username: username.(string),
Role: role.(int16),
Team: team.(string),
}
}

View File

@@ -0,0 +1,21 @@
package middleware
import (
"github.com/gin-gonic/gin"
)
func CORS() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, jsessionid")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}

View File

@@ -0,0 +1,21 @@
package model
import "time"
type TicketAiAnalysis struct {
Analysisid int `gorm:"primaryKey;autoIncrement" json:"analysisid"`
Ticketid int `gorm:"not null" json:"ticketid"`
Category string `gorm:"size:32" json:"category"`
Priority int16 `json:"priority"`
Summary string `gorm:"type:text" json:"summary"`
Suggestrole string `gorm:"size:64" json:"suggestrole"`
Rawresponse string `gorm:"type:text" json:"rawresponse"`
Confirmed int8 `gorm:"default:0" json:"confirmed"`
Confirmedby *int `json:"confirmedby"`
Confirmedat *time.Time `json:"confirmedat"`
Createtime time.Time `json:"createtime"`
}
func (TicketAiAnalysis) TableName() string {
return "ticket_ai_analysis"
}

View File

@@ -0,0 +1,15 @@
package model
import "time"
type TicketNote struct {
Noteid int `gorm:"primaryKey;autoIncrement" json:"noteid"`
Ticketid int `gorm:"not null" json:"ticketid"`
Authorid int `json:"authorid"`
Content string `gorm:"type:text;not null" json:"content"`
Createtime time.Time `json:"createtime"`
}
func (TicketNote) TableName() string {
return "ticket_note"
}

View File

@@ -0,0 +1,16 @@
package model
import "time"
type TicketOperationLog struct {
Logid int `gorm:"primaryKey;autoIncrement" json:"logid"`
Ticketid int `gorm:"not null" json:"ticketid"`
Operatorid int `json:"operatorid"`
Action string `gorm:"size:32;not null" json:"action"`
Detail string `gorm:"type:text" json:"detail"`
Createtime time.Time `json:"createtime"`
}
func (TicketOperationLog) TableName() string {
return "ticket_operation_log"
}

View File

@@ -0,0 +1,24 @@
package model
import "time"
type TicketInfo struct {
Ticketid int `gorm:"primaryKey;autoIncrement" json:"ticketid"`
Ticketno string `gorm:"size:32;uniqueIndex" json:"ticketno"`
Title string `gorm:"size:255;not null" json:"title"`
Content string `gorm:"type:text" json:"content"`
Contactname string `gorm:"size:64" json:"contactname"`
Contactphone string `gorm:"size:20" json:"contactphone"`
Source string `gorm:"size:20;default:web" json:"source"`
Submitterid int `json:"submitterid"`
Category string `gorm:"size:32" json:"category"`
Priority int16 `gorm:"default:2" json:"priority"`
Handlerid *int `json:"handlerid"`
Status int16 `gorm:"default:0" json:"status"`
Createtime time.Time `json:"createtime"`
Updatetime time.Time `json:"updatetime"`
}
func (TicketInfo) TableName() string {
return "ticket_info"
}

View File

@@ -0,0 +1,19 @@
package model
import "time"
type TicketUser struct {
Userid int `gorm:"primaryKey;autoIncrement" json:"userid"`
Username string `gorm:"size:64" json:"username"`
Account string `gorm:"size:64;uniqueIndex" json:"account"`
Password string `gorm:"size:64" json:"-"`
Role int16 `gorm:"default:20" json:"role"`
Team string `gorm:"size:32" json:"team"`
Status int16 `gorm:"default:1" json:"status"`
Createtime time.Time `json:"createtime"`
Updatetime time.Time `json:"updatetime"`
}
func (TicketUser) TableName() string {
return "ticket_user"
}

View 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
}

View 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]
}

View 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(&notes).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
}

View 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
}