Exciting news! TCMS official website is live! Offering full-stack software services including enterprise-level custom R&D, App and mini-program development, multi-system integration, AI, blockchain, and embedded development, empowering digital-intelligent transformation across industries. Visit dev.tekin.cn to discuss cooperation!
Build an enterprise-grade AI customer service system from zero in 7 days! Leveraging the Vue3+Go+Gin+K8s+Llama3 tech stack, this solution covers intelligent Q&A, work order management, human agent transfer, and data statistics. It includes complete runnable source code, detailed operation steps, and deployment documentation—even beginners can follow along to launch...

Build an enterprise-grade AI customer service system from zero in 7 days! Leveraging the Vue3+Go+Gin+K8s+Llama3 tech stack, this solution covers intelligent Q&A, work order management, human agent transfer, and data statistics. It includes complete runnable source code, detailed operation steps, and deployment documentation—even beginners can follow along to launch. The Go backend delivers exceptional performance, while the Vue3 frontend offers responsive adaptation. Supporting multimodal interaction (text/voice) and streaming output, it features enterprise-level permission control and containerized deployment to drive cost reduction and efficiency gains.
This is the Go/Gin implementation of the Java/Spring Boot version: https://dev.tekin.cn/en/blog/7day-enterprise-ai-cs-vue3-springboot-k8s-source-deploy
| Module | Technology Stack | Version Requirements | Core Adaptation Scenarios |
|---|---|---|---|
| Frontend | Vue3+Element Plus+Axios+ECharts | Vue3.2+, Element Plus2.3+ | Cross-device responsive adaptation, streaming component rendering, chart visualization |
| Backend | Go1.22+Gin1.10+GORM2.0+JWT | Go1.22+, Gin1.10+ | High-performance API response, lightweight deployment, enterprise-grade permission control |
| AI Capabilities | Llama 3 (Open-Source LLM)+Langchaingo | Llama3-8B, Langchaingo0.12+ | Lightweight on-premises deployment, ≥85% intent recognition accuracy, low hardware requirements |
| Database | MySQL 8.0+Redis 7.0 | MySQL8.0.30+, Redis7.0.10+ | Business data persistence, high-frequency data caching, session storage |
| Containerization & Deployment | Docker+Kubernetes | Docker24.0+, K8s1.24+ | Environment consistency, auto-scaling, enterprise-grade cluster deployment |
| Speech Recognition | Baidu Speech Recognition API (Optional) | V1 Version | Ample free quota (50,000 requests/day), ≥95% recognition accuracy |
| Real-Time Communication | SSE (Server-Sent Events) | Browser-native support | AI streaming output, lightweight real-time communication without WebSocket |
| File Storage | Local Storage (Basic)/MinIO (Advanced) | MinIO8.5+ | Work order attachment storage, scalable distributed deployment |
| Auxiliary Tools | Viper (Config Parsing)+Zap (Logging)+Gorm-gen (Code Generation) | Viper1.18+, Zap1.27+ | Unified config management, high-performance logging, simplified database operations |
# Verify Node.js and npm versions
node -v # Required: v16.18.0+
npm -v # Required: 8.19.2+
# Install Vue CLI globally
npm install -g @vue/cli@5.0.8 # Specify stable version to avoid compatibility issues
vue --version # Verify installation (Required: 5.0.8+)
vue create ai-customer-service-frontend
# Select "Manually select features" and check Babel, Router, Vuex, CSS Pre-processors, etc.
# Follow subsequent steps as in the original plan to install core dependencies and configure project structure
# Install Go (Required: 1.22+ version)
wget https://dl.google.com/go/go1.22.5.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz
echo "export PATH=\$PATH:/usr/local/go/bin" >> ~/.bashrc
source ~/.bashrc
# Verify Go version
go version # Required: go1.22.x
# Configure Go module proxy (accelerate dependency downloads)
go env -w GOPROXY=https://goproxy.cn,direct
# Create project directory
mkdir -p ai-customer-service-backend
cd ai-customer-service-backend
# Initialize Go module (replace with your module name)
go mod init github.com/your-username/ai-cs-backend
# Install core dependencies
go get github.com/gin-gonic/gin@v1.10.0
go get gorm.io/gorm@v1.25.4
go get gorm.io/driver/mysql@v1.5.2
go get github.com/go-redis/redis/v8@v8.11.5
go get github.com/golang-jwt/jwt/v5@v5.2.1
go get github.com/spf13/viper@v1.18.2
go get go.uber.org/zap@v1.27.0
go get github.com/langchaingo/langchaingo@v0.12.0
go get github.com/langchaingo/llms/ollama@v0.12.0
go get github.com/google/uuid@v1.6.0
go get github.com/tealeg/xlsx/v3@v3.3.1 # Excel export
ai-customer-service-backend/
├── cmd/
│ └── server/
│└── main.go # Program entry
├── config/
│ ├── config.go # Config initialization
│ └── app.yaml # Config file
├── internal/
│ ├── api/
│ │ ├── handler/ # Route handlers (equivalent to Controllers)
│ │ │ ├── auth_handler.go # Login authentication
│ │ │ ├── ai_handler.go # AI Q&A
│ │ │ ├── workorder_handler.go # Work order management
│ │ │ └── stat_handler.go # Data statistics
│ │ ├── middleware/ # Middleware
│ │ │ ├── jwt_middleware.go # JWT authentication middleware
│ │ │ ├── cors_middleware.go # CORS middleware
│ │ │ └── logger_middleware.go # Logging middleware
│ │ └── router/ # Route registration
│ │└── router.go
│ ├── model/ # Data models (equivalent to Entities)
│ │ ├── user.go
│ │ ├── conversation.go
│ │ ├── workorder.go
│ │ ├── faq.go
│ │ └── sys_config.go
│ ├── repository/ # Data access layer (equivalent to Mappers)
│ │ ├── user_repo.go
│ │ ├── workorder_repo.go
│ │ └── faq_repo.go
│ ├── service/ # Business logic layer (equivalent to Services)
│ │ ├── auth_service.go
│ │ ├── ai_service.go
│ │ ├── workorder_service.go
│ │ └── stat_service.go
│ └── util/ # Utility classes
│├── jwt_util.go
│├── redis_util.go
│├── sse_util.go # SSE streaming tool
│└── file_util.go # File handling
├── pkg/
│ ├── logger/ # Logging tools
│ └── resp/ # Unified response format
├── go.mod
├── go.sum
└── Dockerfile
app:
name: ai-customer-service
port: 8080
context-path: /api # API prefix
mode: debug # Running mode: debug/release
mysql:
host: localhost
port: 3306
username: root
password: 123456
db-name: ai_customer_service
max-open-conns: 10
max-idle-conns: 5
conn-max-lifetime: 3600 # Connection max lifetime (seconds)
redis:
host: localhost
port: 6379
password: ""
db: 1
pool-size: 10
min-idle-conns: 2
idle-timeout: 3600 # Idle connection timeout (seconds)
jwt:
secret: aiCustomerService2025@Example.com
expiration: 86400 # Token validity (seconds, 24 hours)
issuer: ai-cs-backend
ai:
ollama:base-url: http://localhost:11434model-name: llama3:8b-instructmax-tokens: 1024temperature: 0.6timeout: 60 # Timeout (seconds)
cache:enabled: trueexpire-seconds: 3600 # Cache expiration (1 hour)threshold: 5 # Cache after 5 hits
work-order:
assign-auto: true
remind-time: 30 # Reminder for unprocessed orders (minutes)
file:
upload-path: ./uploads/
max-file-size: 10 # Max single file size (MB)
max-request-size: 50 # Max single request size (MB)
package config
import (
"github.com/spf13/viper"
"go.uber.org/zap"
"os"
"path/filepath"
)
// Config Global config structure
type Config struct {
AppAppConfig`yaml:"app"`
MySQLMySQLConfig`yaml:"mysql"`
RedisRedisConfig`yaml:"redis"`
JWTJWTConfig`yaml:"jwt"`
AIAIConfig`yaml:"ai"`
WorkOrder WorkOrderConfig `yaml:"work-order"`
FileFileConfig`yaml:"file"`
}
// Sub-config structures
type AppConfig struct {
Namestring `yaml:"name"`
Portint `yaml:"port"`
ContextPath string `yaml:"context-path"`
Modestring `yaml:"mode"`
}
type MySQLConfig struct {
Hoststring `yaml:"host"`
Portint `yaml:"port"`
Usernamestring `yaml:"username"`
Passwordstring `yaml:"password"`
DBNamestring `yaml:"db-name"`
MaxOpenConns int `yaml:"max-open-conns"`
MaxIdleConns int `yaml:"max-idle-conns"`
ConnMaxLifetime int `yaml:"conn-max-lifetime"`
}
type RedisConfig struct {
Hoststring `yaml:"host"`
Portint `yaml:"port"`
Passwordstring `yaml:"password"`
DBint `yaml:"db"`
PoolSizeint `yaml:"pool-size"`
MinIdleConns int `yaml:"min-idle-conns"`
IdleTimeout int `yaml:"idle-timeout"`
}
type JWTConfig struct {
Secretstring `yaml:"secret"`
Expiration int64 `yaml:"expiration"` // Seconds
Issuerstring `yaml:"issuer"`
}
type AIConfig struct {
Ollama OllamaConfig `yaml:"ollama"`
Cache CacheConfig `yaml:"cache"`
}
type OllamaConfig struct {
BaseURLstring `yaml:"base-url"`
ModelName string `yaml:"model-name"`
MaxTokens int`yaml:"max-tokens"`
Temperature float64 `yaml:"temperature"`
Timeoutint`yaml:"timeout"`
}
type CacheConfig struct {
Enabledbool `yaml:"enabled"`
ExpireSeconds int `yaml:"expire-seconds"`
Thresholdint `yaml:"threshold"`
}
type WorkOrderConfig struct {
AssignAuto bool `yaml:"assign-auto"`
RemindTime int `yaml:"remind-time"`
}
type FileConfig struct {
UploadPathstring `yaml:"upload-path"`
MaxFileSize int64 `yaml:"max-file-size"` // MB
MaxRequestSize int64 `yaml:"max-request-size"` // MB
}
var GlobalConfig Config
// Init Initialize config
func Init() {
// Config file path
configPath := filepath.Join("config", "app.yaml")
if _, err := os.Stat(configPath); os.IsNotExist(err) {zap.L().Fatal("Config file does not exist", zap.String("path", configPath))
}// Read config file
viper.SetConfigFile(configPath)
viper.SetConfigType("yaml")
if err := viper.ReadInConfig(); err != nil {zap.L().Fatal("Failed to read config file", zap.Error(err))
}// Deserialize to structure
if err := viper.Unmarshal(&GlobalConfig); err != nil {zap.L().Fatal("Failed to parse config file", zap.Error(err))
}// Convert file size units (MB → Byte)
GlobalConfig.File.MaxFileSize *= 1024 * 1024
GlobalConfig.File.MaxRequestSize *= 1024 * 1024zap.L().Info("Config initialized successfully")
}
-- Create database if not exists
CREATE DATABASE IF NOT EXISTS ai_customer_service DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE ai_customer_service;
-- 1. System User Table (regular users, agents, admins)
CREATE TABLE IF NOT EXISTS `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'Primary Key ID',
`username` varchar(50) NOT NULL COMMENT 'Username (unique)',
`password` varchar(100) NOT NULL COMMENT 'Encrypted password (BCrypt)',
`role` varchar(20) NOT NULL COMMENT 'Role: USER/CUSTOMER_SERVICE/ADMIN',
`nickname` varchar(50) DEFAULT NULL COMMENT 'Nickname',
`phone` varchar(20) DEFAULT NULL COMMENT 'Phone number',
`email` varchar(100) DEFAULT NULL COMMENT 'Email',
`avatar` varchar(255) DEFAULT NULL COMMENT 'Avatar URL',
`status` tinyint NOT NULL DEFAULT 1 COMMENT 'Status: 0=Disabled, 1=Normal',
`online_status` tinyint NOT NULL DEFAULT 0 COMMENT 'Online status: 0=Offline, 1=Online',
`last_login_time` datetime DEFAULT NULL COMMENT 'Last login time',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation time',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`) COMMENT 'Unique index for username',
KEY `idx_role` (`role`) COMMENT 'Role index',
KEY `idx_status` (`status`) COMMENT 'Status index'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='System User Table';
-- Initialize data (same as original plan)
INSERT INTO `sys_user` (`username`, `password`, `role`, `nickname`, `status`)
VALUES ('admin', '$2a$10$Z8H4k4U6f7G3j2i1l0K9m8N7O6P5Q4R3S2T1U0V9W8X7Y6Z5A4B', 'ADMIN', 'System Administrator', 1);
INSERT INTO `sys_user` (`username`, `password`, `role`, `nickname`, `status`)
VALUES ('customer_service1', '$2a$10$A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X', 'CUSTOMER_SERVICE', 'Agent 1', 1);
-- Other tables (conversation, work_order, faq, sys_config) creation scripts are the same as the original plan
package util
import (
"errors"
"github.com/golang-jwt/jwt/v5"
"github.com/your-username/ai-cs-backend/config"
"time"
)
// Claims JWT payload structure
type Claims struct {
Username string `json:"username"`
Rolestring `json:"role"`
UserID int64 `json:"user_id"`
jwt.RegisteredClaims
}
// GenerateToken Generate JWT Token
func GenerateToken(userID int64, username, role string) (string, error) {
// Build payload
claims := Claims{Username: username,Role:role,UserID: userID,RegisteredClaims: jwt.RegisteredClaims{Issuer: config.GlobalConfig.JWT.Issuer,ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(config.GlobalConfig.JWT.Expiration) * time.Second)),IssuedAt: jwt.NewNumericDate(time.Now()),},
}// Generate Token (HS256 algorithm)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.GlobalConfig.JWT.Secret))
}
// ParseToken Parse JWT Token
func ParseToken(tokenString string) (*Claims, error) {
// Parse Token
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {// Verify signing algorithmif _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {return nil, errors.New("unsupported signing algorithm")}return []byte(config.GlobalConfig.JWT.Secret), nil
})if err != nil {return nil, err
}// Verify Token validity and return payload
if claims, ok := token.Claims.(*Claims); ok && token.Valid {return claims, nil
}
return nil, errors.New("invalid or expired token")
}
package middleware
import (
"github.com/gin-gonic/gin"
"github.com/your-username/ai-cs-backend/internal/util"
"github.com/your-username/ai-cs-backend/pkg/resp"
"net/http"
"strings"
)
// JWTMiddleware JWT authentication middleware
func JWTMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {// Get Token from request headerauthHeader := c.GetHeader("Authorization")if authHeader == "" {resp.Error(c, http.StatusUnauthorized, "Authorization token not provided")c.Abort()return}// Parse Token format (Bearer <token>)parts := strings.SplitN(authHeader, " ", 2)if !(len(parts) == 2 && parts[0] == "Bearer") {resp.Error(c, http.StatusUnauthorized, "Invalid Authorization token format")c.Abort()return}// Parse Tokenclaims, err := util.ParseToken(parts[1])if err != nil {resp.Error(c, http.StatusUnauthorized, "Invalid or expired token")c.Abort()return}// Store user info in contextc.Set("userID", claims.UserID)c.Set("username", claims.Username)c.Set("role", claims.Role)c.Next()
}
}
// RoleAuth Role-based permission control middleware
func RoleAuth(roles ...string) gin.HandlerFunc {
return func(c *gin.Context) {// Get role from contextrole, exists := c.Get("role")if !exists {resp.Error(c, http.StatusForbidden, "Insufficient permissions")c.Abort()return}// Verify if role is in allowed listhasPermission := falsefor _, r := range roles {if role == r {hasPermission = truebreak}}if !hasPermission {resp.Error(c, http.StatusForbidden, "No permission for this operation")c.Abort()return}c.Next()
}
}
package util
import (
"context"
"github.com/go-redis/redis/v8"
"github.com/your-username/ai-cs-backend/config"
"go.uber.org/zap"
"time"
)
var redisClient *redis.Client
var ctx = context.Background()
// InitRedis Initialize Redis client
func InitRedis() {
conf := config.GlobalConfig.Redis
redisClient = redis.NewClient(&redis.Options{Addr:conf.Host + ":" + string(rune(conf.Port)),Password:conf.Password,DB:conf.DB,PoolSize:conf.PoolSize,MinIdleConns: conf.MinIdleConns,IdleTimeout: time.Duration(conf.IdleTimeout) * time.Second,
})// Test connection
if err := redisClient.Ping(ctx).Err(); err != nil {zap.L().Fatal("Redis connection failed", zap.Error(err))
}
zap.L().Info("Redis initialized successfully")
}
// Set Store key-value pair (with expiration)
func RedisSet(key string, value interface{}, expire time.Duration) error {
return redisClient.Set(ctx, key, value, expire).Err()
}
// Get Retrieve value by key
func RedisGet(key string) (string, error) {
return redisClient.Get(ctx, key).Result()
}
// Incr Increment by 1
func RedisIncr(key string) (int64, error) {
return redisClient.Incr(ctx, key).Result()
}
// Del Delete key
func RedisDel(key string) error {
return redisClient.Del(ctx, key).Err()
}
// Exists Check if key exists
func RedisExists(key string) (bool, error) {
count, err := redisClient.Exists(ctx, key).Result()
return count > 0, err
}
package model
import "time"
// SysUser System User Table model
type SysUser struct {
IDint64`gorm:"column:id;type:bigint;primaryKey;autoIncrement" json:"id"`
Usernamestring `gorm:"column:username;type:varchar(50);uniqueIndex;not null" json:"username"`
Passwordstring `gorm:"column:password;type:varchar(100);not null" json:"-"` // Exclude password from serialization
Rolestring `gorm:"column:role;type:varchar(20);not null;index" json:"role"`
Nicknamestring `gorm:"column:nickname;type:varchar(50)" json:"nickname"`
Phonestring `gorm:"column:phone;type:varchar(20)" json:"phone"`
Emailstring `gorm:"column:email;type:varchar(100)" json:"email"`
Avatarstring `gorm:"column:avatar;type:varchar(255)" json:"avatar"`
Statusint8`gorm:"column:status;type:tinyint;not null;default:1;index" json:"status"`
OnlineStatus int8`gorm:"column:online_status;type:tinyint;not null;default:0" json:"online_status"`
LastLoginTime *time.Time `gorm:"column:last_login_time;type:datetime" json:"last_login_time"`
CreateTimetime.Time `gorm:"column:create_time;type:datetime;not null;default:current_timestamp" json:"create_time"`
UpdateTimetime.Time `gorm:"column:update_time;type:datetime;not null;default:current_timestamp;autoUpdateTime" json:"update_time"`
}
// TableName Table name mapping
func (SysUser) TableName() string {
return "sys_user"
}
package repository
import (
"context"
"github.com/your-username/ai-cs-backend/internal/model"
"gorm.io/gorm"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
// GetByUsername Query user by username
func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*model.SysUser, error) {
var user model.SysUser
err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error
if err != nil {return nil, err
}
return &user, nil
}
// GetOnlineCustomerService Get online agents
func (r *UserRepository) GetOnlineCustomerService(ctx context.Context) (*model.SysUser, error) {
var cs model.SysUser
err := r.db.WithContext(ctx).Where("role = ?", "CUSTOMER_SERVICE").Where("status = ?", 1).Where("online_status = ?", 1).Order("id ASC").First(&cs).Error
if err != nil {return nil, err
}
return &cs, nil
}
// UpdateOnlineStatus Update online status
func (r *UserRepository) UpdateOnlineStatus(ctx context.Context, userID int64, status int8) error {
return r.db.WithContext(ctx).Model(&model.SysUser{}).Where("id = ?", userID).Update("online_status", status).Error
}
package service
import (
"context"
"errors"
"fmt"
"github.com/langchaingo/langchaingo/llms"
"github.com/langchaingo/llms/ollama"
"github.com/your-username/ai-cs-backend/config"
"github.com/your-username/ai-cs-backend/internal/model"
"github.com/your-username/ai-cs-backend/internal/repository"
"github.com/your-username/ai-cs-backend/internal/util"
"go.uber.org/zap"
"strconv"
"time"
)
type AIService struct {
faqRepo*repository.FaqRepository
convRepo*repository.ConversationRepository
sysConfigRepo *repository.SysConfigRepository
llmllms.Model
}
func NewAIService(
faqRepo *repository.FaqRepository,
convRepo *repository.ConversationRepository,
sysConfigRepo *repository.SysConfigRepository,
) (*AIService, error) {
// Initialize Ollama client
conf := config.GlobalConfig.AI.Ollama
llm, err := ollama.New(ollama.WithBaseURL(conf.BaseURL),ollama.WithModel(conf.ModelName),ollama.WithTimeout(time.Duration(conf.Timeout)*time.Second),
)
if err != nil {return nil, fmt.Errorf("failed to initialize Ollama: %w", err)
}return &AIService{faqRepo:faqRepo,convRepo:convRepo,sysConfigRepo: sysConfigRepo,llm:llm,
}, nil
}
// Answer Regular Q&A (non-streaming)
func (s *AIService) Answer(ctx context.Context, userID int64, question string) (string, error) {
if question == "" {return "Hello! How can I assist you today?", nil
}// 1. Check Redis cache first
cacheKey := "ai:answer:" + strconv.Itoa(int(hashString(question)))
if config.GlobalConfig.AI.Cache.Enabled {cacheVal, err := util.RedisGet(cacheKey)if err == nil && cacheVal != "" {// Update FAQ hit count on cache hit_ = s.faqRepo.IncrHitCountByQuestion(ctx, question)return cacheVal, nil}
}// 2. Query FAQ (fuzzy match)
faqAnswer, err := s.faqRepo.SearchFaq(ctx, question)
if err == nil && faqAnswer != "" {// 3. Store in Redis if cache threshold is metif config.GlobalConfig.AI.Cache.Enabled {hitCount, _ := s.faqRepo.GetHitCountByQuestion(ctx, question)if hitCount >= config.GlobalConfig.AI.Cache.Threshold {_ = util.RedisSet(cacheKey,faqAnswer,time.Duration(config.GlobalConfig.AI.Cache.ExpireSeconds)*time.Second,)}}// Save conversation record_ = s.saveConversation(ctx, userID, "", question, faqAnswer, "AI")return faqAnswer, nil
}// 4. Call LLM to generate answer
systemPrompt := s.buildSystemPrompt()
fullPrompt := fmt.Sprintf("%s\nUser question: %s", systemPrompt, question)
completion, err := llms.GenerateFromSinglePrompt(ctx, s.llm, fullPrompt,llms.WithMaxTokens(config.GlobalConfig.AI.Ollama.MaxTokens),llms.WithTemperature(config.GlobalConfig.AI.Ollama.Temperature),
)
if err != nil {zap.L().Error("LLM call failed", zap.Error(err))return "Sorry, there was a system error. Please try again later~", nil
}// Save conversation record
_ = s.saveConversation(ctx, userID, "", question, completion.Content, "AI")
return completion.Content, nil
}
// AnswerStream Streaming Q&A (real-time return)
func (s *AIService) AnswerStream(ctx context.Context, userID int64, sessionID, question string, streamChan chan<- string) error {
defer close(streamChan)if question == "" {streamChan <- "Hello! How can I assist you today?"streamChan <- "[END]"return nil
}// 1. Check cache
cacheKey := "ai:answer:" + strconv.Itoa(int(hashString(question)))
if config.GlobalConfig.AI.Cache.Enabled {cacheVal, err := util.RedisGet(cacheKey)if err == nil && cacheVal != "" {streamChan <- cacheValstreamChan <- "[END]"_ = s.saveConversation(ctx, userID, sessionID, question, cacheVal, "AI")return nil}
}// 2. Query FAQ
faqAnswer, err := s.faqRepo.SearchFaq(ctx, question)
if err == nil && faqAnswer != "" {streamChan <- faqAnswerstreamChan <- "[END]"_ = s.saveConversation(ctx, userID, sessionID, question, faqAnswer, "AI")return nil
}// 3. LLM streaming generation
systemPrompt := s.buildSystemPrompt()
fullPrompt := fmt.Sprintf("%s\nUser question: %s", systemPrompt, question)stream, err := s.llm.GenerateContentStream(ctx, []llms.Message{llms.NewSystemMessage(systemPrompt),llms.NewHumanMessage(question),
},llms.WithMaxTokens(config.GlobalConfig.AI.Ollama.MaxTokens),llms.WithTemperature(config.GlobalConfig.AI.Ollama.Temperature),
)
if err != nil {zap.L().Error("LLM streaming call failed", zap.Error(err))streamChan <- "Sorry, there was a system error. Please try again later~"streamChan <- "[END]"return err
}// Collect full answer
var fullAnswer string
for {resp, err := stream.Recv()if err != nil {break}for _, choice := range resp.Choices {content := choice.ContentfullAnswer += contentstreamChan <- content}
}streamChan <- "[END]"
// Save conversation record
_ = s.saveConversation(ctx, userID, sessionID, question, fullAnswer, "AI")// Cache answer (if threshold is met)
if config.GlobalConfig.AI.Cache.Enabled {hitCount, _ := s.faqRepo.GetHitCountByQuestion(ctx, question)if hitCount >= config.GlobalConfig.AI.Cache.Threshold {_ = util.RedisSet(cacheKey,fullAnswer,time.Duration(config.GlobalConfig.AI.Cache.ExpireSeconds)*time.Second,)}
}return nil
}
// NeedTransferToHuman Check if human agent transfer is required
func (s *AIService) NeedTransferToHuman(ctx context.Context, question, answer string) (bool, error) {
// 1. Keyword match (direct transfer)
transferKeywords := []string{"human", "agent", "transfer", "human service", "online agent"}
for _, kw := range transferKeywords {if containsString(question, kw) {return true, nil}
}// 2. Keywords indicating AI inability to answer
unableKeywords := []string{"cannot answer", "do not know", "please consult", "transfer to human"}
for _, kw := range unableKeywords {if containsString(answer, kw) {return true, nil}
}// 3. Read confidence threshold configuration
configVal, err := s.sysConfigRepo.GetConfigByKey(ctx, "AI_AUTO_TRANSFER_THRESHOLD")
if err != nil {return false, err
}
threshold, _ := strconv.ParseFloat(configVal, 64)
if threshold <= 0 {threshold = 0.3
}// Simplified: Integrate specialized intent recognition models for confidence calculation in production
return false, nil
}
// Build system prompt
func (s *AIService) buildSystemPrompt() string {
return `You are an enterprise-grade intelligent customer service assistant. Follow these rules:
1. Only answer questions related to enterprise business (accounts, orders, work orders, product inquiries, etc.);
2. Keep answers concise and clear. Prioritize using standard answers from the FAQ;
3. For unanswerable questions, reply: "Sorry, I cannot answer this question. We recommend transferring to a human agent or submitting a work order~";
4. Refuse to answer non-business-related questions (e.g., weather, news, entertainment);
5. Maintain a friendly and professional tone, and reply in Chinese.`
}
// Save conversation record
func (s *AIService) saveConversation(ctx context.Context, userID int64, sessionID, question, answer, sender string) error {
if sessionID == "" {sessionID = generateSessionID(userID)
}// Save user message
userConv := &model.Conversation{UserID:userID,SessionID: sessionID,Content: question,Sender:"USER",SenderID: userID,MessageType: "TEXT",CreateTime: time.Now(),
}
if err := s.convRepo.Create(ctx, userConv); err != nil {zap.L().Error("Failed to save user conversation", zap.Error(err))return err
}// Save AI message
aiConv := &model.Conversation{UserID:userID,SessionID: sessionID,Content: answer,Sender:sender,SenderID: 0, // AI SenderID is 0MessageType: "TEXT",CreateTime: time.Now(),
}
if err := s.convRepo.Create(ctx, aiConv); err != nil {zap.L().Error("Failed to save AI conversation", zap.Error(err))return err
}return nil
}
// Generate session ID (userID + date)
func generateSessionID(userID int64) string {
dateStr := time.Now().Format("20060102")
return fmt.Sprintf("%d_%s", userID, dateStr)
}
// String contains check
func containsString(str, substr string) bool {
return len(str) >= len(substr) && indexString(str, substr) != -1
}
// String index (simplified implementation)
func indexString(str, substr string) int {
for i := 0; i <= len(str)-len(substr); i++ {if str[i:i+len(substr)] == substr {return i}
}
return -1
}
// String hash (for cache keys)
func hashString(s string) uint64 {
var h uint64
for i := 0; i < len(s); i++ {h = h*31 + uint64(s[i])
}
return h
}
package handler
import (
"github.com/gin-gonic/gin"
"github.com/your-username/ai-cs-backend/internal/service"
"github.com/your-username/ai-cs-backend/internal/util"
"github.com/your-username/ai-cs-backend/pkg/resp"
"net/http"
"strconv"
)
type AIHandler struct {
aiService *service.AIService
}
func NewAIHandler(aiService *service.AIService) *AIHandler {
return &AIHandler{aiService: aiService}
}
// Answer Regular Q&A API
func (h *AIHandler) Answer(c *gin.Context) {
var req struct {Question string `json:"question" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {resp.Error(c, http.StatusBadRequest, "Invalid parameters: "+err.Error())return
}// Get userID from context
userID, _ := c.Get("userID")answer, err := h.aiService.Answer(c.Request.Context(), userID.(int64), req.Question)
if err != nil {resp.Error(c, http.StatusInternalServerError, "Q&A failed: "+err.Error())return
}resp.Success(c, answer)
}
// AnswerStream Streaming Q&A API (SSE)
func (h *AIHandler) AnswerStream(c *gin.Context) {
// Set SSE response headers
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no") // Disable nginx buffering// Get parameters
question := c.Query("question")
sessionID := c.Query("session_id")
if question == "" {util.SendSSE(c, "Please enter your question")util.SendSSE(c, "[END]")return
}// Get userID from context
userID, _ := c.Get("userID")// Create streaming channel
streamChan := make(chan string, 10)
defer close(streamChan)// Call AI service asynchronously
go func() {_ = h.aiService.AnswerStream(c.Request.Context(), userID.(int64), sessionID, question, streamChan)
}()// Push stream data to client
for msg := range streamChan {util.SendSSE(c, msg)// Flush response immediatelyc.Writer.Flush()if msg == "[END]" {break}
}
}
// CheckTransfer Check if human transfer is needed
func (h *AIHandler) CheckTransfer(c *gin.Context) {
question := c.Query("question")
answer := c.Query("answer")if question == "" || answer == "" {resp.Error(c, http.StatusBadRequest, "Invalid parameters: question and answer cannot be empty")return
}needTransfer, err := h.aiService.NeedTransferToHuman(c.Request.Context(), question, answer)
if err != nil {resp.Error(c, http.StatusInternalServerError, "Check failed: "+err.Error())return
}resp.Success(c, gin.H{"need_transfer": needTransfer})
}
package model
import "time"
// WorkOrder Work Order Table model
type WorkOrder struct {
IDint64`gorm:"column:id;type:bigint;primaryKey;autoIncrement" json:"id"`
OrderNostring`gorm:"column:order_no;type:varchar(32);uniqueIndex;not null" json:"order_no"`
UserIDint64`gorm:"column:user_id;type:bigint;not null;index" json:"user_id"`
Titlestring`gorm:"column:title;type:varchar(200);not null" json:"title"`
Contentstring`gorm:"column:content;type:text;not null" json:"content"`
Statusstring`gorm:"column:status;type:varchar(20);not null;index" json:"status"` // PENDING/PROCESSING/CLOSED/REJECTED
HandlerID*int64`gorm:"column:handler_id;type:bigint;index" json:"handler_id,omitempty"`
Prioritystring`gorm:"column:priority;type:varchar(10);not null;default:'NORMAL'" json:"priority"` // LOW/NORMAL/HIGH
Replystring`gorm:"column:reply;type:text" json:"reply,omitempty"`
AttachmentUrlsstring`gorm:"column:attachment_urls;type:varchar(512)" json:"attachment_urls,omitempty"`
UserFeedback*int`gorm:"column:user_feedback;type:int" json:"user_feedback,omitempty"`
UserFeedbackContent string`gorm:"column:user_feedback_content;type:text" json:"user_feedback_content,omitempty"`
CreateTimetime.Time `gorm:"column:create_time;type:datetime;not null;default:current_timestamp" json:"create_time"`
AssignTime*time.Time `gorm:"column:assign_time;type:datetime" json:"assign_time,omitempty"`
HandleTime*time.Time `gorm:"column:handle_time;type:datetime" json:"handle_time,omitempty"`
CloseTime*time.Time `gorm:"column:close_time;type:datetime" json:"close_time,omitempty"`
UpdateTimetime.Time `gorm:"column:update_time;type:datetime;not null;default:current_timestamp;autoUpdateTime" json:"update_time"`
}
// TableName Table name mapping
func (WorkOrder) TableName() string {
return "work_order"
}
package service
import (
"context"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/your-username/ai-cs-backend/config"
"github.com/your-username/ai-cs-backend/internal/model"
"github.com/your-username/ai-cs-backend/internal/repository"
"go.uber.org/zap"
"time"
)
type WorkOrderService struct {
woRepo *repository.WorkOrderRepository
userRepo *repository.UserRepository
}
func NewWorkOrderService(
woRepo *repository.WorkOrderRepository,
userRepo *repository.UserRepository,
) *WorkOrderService {
return &WorkOrderService{woRepo: woRepo,userRepo: userRepo,
}
}
// CreateWorkOrder Create work order
func (s *WorkOrderService) CreateWorkOrder(ctx context.Context, req CreateWorkOrderReq) (bool, error) {
// Generate unique work order number (WO+timestamp+last 6 digits of UUID)
orderNo := fmt.Sprintf("WO%d%s", time.Now().UnixMilli(), uuid.NewString()[:6])wo := &model.WorkOrder{OrderNo:orderNo,UserID:req.UserID,Title:req.Title,Content:req.Content,Status:"PENDING",Priority:req.Priority,AttachmentUrls: req.AttachmentUrls,CreateTime:time.Now(),UpdateTime:time.Now(),
}// Auto-assign work order (if enabled)
if config.GlobalConfig.WorkOrder.AssignAuto {cs, err := s.userRepo.GetOnlineCustomerService(ctx)if err == nil && cs != nil {wo.HandlerID = &cs.IDwo.Status = "PROCESSING"now := time.Now()wo.AssignTime = &now}
}if err := s.woRepo.Create(ctx, wo); err != nil {zap.L().Error("Failed to create work order", zap.Error(err), zap.Any("req", req))return false, err
}return true, nil
}
// AssignWorkOrder Assign work order
func (s *WorkOrderService) AssignWorkOrder(ctx context.Context, orderID, handlerID int64) (bool, error) {
// Verify work order exists and is pending
wo, err := s.woRepo.GetByID(ctx, orderID)
if err != nil {return false, fmt.Errorf("work order does not exist: %w", err)
}
if wo.Status != "PENDING" {return false, errors.New("work order is not in pending status, cannot assign")
}// Verify agent is online
cs, err := s.userRepo.GetByID(ctx, handlerID)
if err != nil || cs.Role != "CUSTOMER_SERVICE" || cs.Status != 1 || cs.OnlineStatus != 1 {return false, errors.New("agent does not exist or is not online")
}// Update work order
now := time.Now()
err = s.woRepo.Update(ctx, &model.WorkOrder{ID:orderID,HandlerID: &handlerID,Status:"PROCESSING",AssignTime: &now,UpdateTime: now,
})
if err != nil {zap.L().Error("Failed to assign work order", zap.Error(err), zap.Int64("orderID", orderID), zap.Int64("handlerID", handlerID))return false, err
}return true, nil
}
// HandleWorkOrder Process work order
func (s *WorkOrderService) HandleWorkOrder(ctx context.Context, orderID, handlerID int64, reply string) (bool, error) {
// Verify work order
wo, err := s.woRepo.GetByID(ctx, orderID)
if err != nil {return false, fmt.Errorf("work order does not exist: %w", err)
}
if wo.Status != "PROCESSING" {return false, errors.New("work order is not in processing status")
}
if wo.HandlerID == nil || *wo.HandlerID != handlerID {return false, errors.New("current agent is not the work order handler")
}// Update work order
now := time.Now()
err = s.woRepo.Update(ctx, &model.WorkOrder{ID:orderID,Reply:reply,Status:"CLOSED",HandleTime: &now,CloseTime: &now,UpdateTime: now,
})
if err != nil {zap.L().Error("Failed to process work order", zap.Error(err), zap.Int64("orderID", orderID))return false, err
}return true, nil
}
// CreateWorkOrderReq Work order creation request structure
type CreateWorkOrderReq struct {
UserIDint64 `json:"user_id"`
Titlestring `json:"title"`
Contentstring `json:"content"`
Prioritystring `json:"priority"`
AttachmentUrls string `json:"attachment_urls,omitempty"`
}
The frontend tech stack (Vue3+Element Plus+Axios) remains unchanged. The API request format and response structure are fully compatible with the original Java backend. Ensure the frontend’s Content-Type, API paths, and parameter names match the Go backend.
Core frontend modules (streaming output components, speech input components, work order lists, data statistics pages) use the same code as the original plan—no repetition here.
Original plan reference: https://dev.tekin.cn/blog/7day-enterprise-ai-cs-vue3-springboot-k8s-source-deploy
# Initialize config and dependencies
cd ai-customer-service-backend
go mod tidy
# Start service (development mode)
go run cmd/server/main.go
# Service listens on port 8080 with API prefix "/api" after startup
gin.Context.Writer.Flush()—frontend streaming components are fully compatible.package middleware
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"time"
)
// CorsMiddleware CORS middleware
func CorsMiddleware() gin.HandlerFunc {
return cors.New(cors.Config{AllowOrigins:[]string{"http://localhost:8081"}, // Frontend development addressAllowMethods:[]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},AllowHeaders:[]string{"Origin", "Content-Type", "Authorization"},ExposeHeaders: []string{"Content-Length"},AllowCredentials: true,MaxAge:12 * time.Hour,
})
}
package main
import (
"github.com/gin-gonic/gin"
"github.com/your-username/ai-cs-backend/config"
"github.com/your-username/ai-cs-backend/internal/api/middleware"
"github.com/your-username/ai-cs-backend/internal/api/router"
"github.com/your-username/ai-cs-backend/internal/util"
"github.com/your-username/ai-cs-backend/pkg/logger"
"net/http"
"strconv"
"go.uber.org/zap"
)
func main() {
// Initialize logging
logger.Init()
// Initialize config
config.Init()
// Initialize Redis
util.InitRedis()// Set Gin mode
gin.SetMode(config.GlobalConfig.App.Mode)
r := gin.Default()// Register middleware
r.Use(middleware.LoggerMiddleware()) // Logging middleware
r.Use(middleware.CorsMiddleware()) // CORS middleware
r.Use(gin.Recovery())// Panic recovery middleware// Set file upload size limit
r.MaxMultipartMemory = config.GlobalConfig.File.MaxRequestSize// Register routes
router.RegisterRoutes(r)// Start service
addr := ":" + strconv.Itoa(config.GlobalConfig.App.Port)
logger.ZapLogger.Info("Service started successfully", zap.String("addr", addr))
if err := r.Run(addr); err != nil && err != http.ErrServerClosed {logger.ZapLogger.Fatal("Failed to start service", zap.Error(err))
}
}
# Multi-stage build: Build stage
FROM golang:1.22-alpine AS builder
# Set working directory
WORKDIR /app
# Copy go.mod and go.sum
COPY go.mod go.sum ./
# Download dependencies
RUN go mod tidy
# Copy source code
COPY . .
# Build Go application (static linking, no system library dependencies)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o ai-cs-backend cmd/server/main.go
# Run stage
FROM alpine:3.19
# Set working directory
WORKDIR /app
# Copy build artifact
COPY --from=builder /app/ai-cs-backend .
# Copy config files
COPY --from=builder /app/config ./config
# Create upload directory
RUN mkdir -p uploads
# Set time zone
RUN apk add --no-cache tzdata && ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo "Asia/Shanghai" > /etc/timezone
# Expose port
EXPOSE 8080
# Start application
ENTRYPOINT ["./ai-cs-backend"]
# Backend service deployment (Go version)
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai-cs-backend
spec:
replicas: 2
selector:matchLabels:app: ai-cs-backend
template:metadata:labels:app: ai-cs-backendspec:containers:- name: ai-cs-backendimage: ai-cs-backend:v1.0 # Go version imageports:- containerPort: 8080env:- name: MYSQL_HOSTvalue: "mysql-service"- name: MYSQL_PORTvalue: "3306"- name: MYSQL_USERNAMEvalue: "root"- name: MYSQL_PASSWORDvalue: "123456"- name: MYSQL_DBNAMEvalue: "ai_customer_service"- name: REDIS_HOSTvalue: "redis-service"- name: AI_OLLAMA_BASE_URLvalue: "http://ollama-service:11434"resources:requests:cpu: "500m" # Go backend uses fewer resources—can be reduced appropriatelymemory: "1Gi"limits:cpu: "1"memory: "2Gi"livenessProbe:httpGet:path: /api/healthport: 8080initialDelaySeconds: 30periodSeconds: 10readinessProbe:httpGet:path: /api/healthport: 8080initialDelaySeconds: 10periodSeconds: 5
---
# Backend Service (same as original plan)
apiVersion: v1
kind: Service
metadata:
name: ai-cs-backend-service
spec:
selector:app: ai-cs-backend
ports:
- port: 8080targetPort: 8080
type: ClusterIP
kubectl logs -f deployment/ai-cs-backend to view Go application logs (Zap logs have a clear format).Full source code (including Go backend, Vue frontend, database scripts, deployment configurations): https://dev.tekin.cn/blog/7day-enterprise-ai-customer-service-vue3-go-gin
Technical Support QQ: 932256355
Java SpringBoot Version Reference: https://dev.tekin.cn/blog/7day-enterprise-ai-cs-vue3-springboot-k8s-source-deploy
#AI Application Development #Enterprise-Grade Practice #Go Language #Gin #K8s #Docker #Full-Stack Development