Compare commits
11 Commits
master
...
3180fd065e
| Author | SHA1 | Date | |
|---|---|---|---|
| 3180fd065e | |||
| 9598aec184 | |||
| 7416bf8d96 | |||
| f389065798 | |||
| c87cde60d2 | |||
| 425728b22f | |||
| 65dc04322c | |||
| 6008ddf643 | |||
| eae825726e | |||
| 98c1c6d31f | |||
| b7cce0befa |
49
Dockerfile
Normal file
49
Dockerfile
Normal file
@@ -0,0 +1,49 @@
|
||||
# 阶段1: 构建
|
||||
FROM golang:1.26-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git ca-certificates tzdata
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
ENV GO111MODULE=on
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOTOOLCHAIN=auto
|
||||
ENV GOPRIVATE=gitea.com/red-future/common
|
||||
|
||||
# 配置git使用私有Gitea仓库
|
||||
RUN git config --global url."http://x-token-auth:9b31146aa8c10a7cb4f2e49dcee0934a223be1076289810e1ad98b968066c2bc@116.204.74.41:3000/red-future/common.git".insteadOf "https://gitea.com/red-future/common.git" && \
|
||||
git config --global credential.helper store
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go mod download && go mod tidy
|
||||
|
||||
RUN go build -ldflags="-s -w" -o main ./main.go
|
||||
|
||||
# 阶段2: 运行
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
ENV TZ=Asia/Shanghai
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /build/main .
|
||||
COPY --from=builder /build/config.yml ./
|
||||
|
||||
RUN mkdir -p /app/resource/log/run \
|
||||
/app/resource/log/server \
|
||||
&& adduser -D -u 1000 appuser \
|
||||
&& chown -R appuser:appuser /app
|
||||
|
||||
USER appuser
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["./main"]
|
||||
67
config.yml
67
config.yml
@@ -1,7 +1,53 @@
|
||||
server:
|
||||
address: ":3000"
|
||||
name: "customer-server"
|
||||
|
||||
workerId: 1
|
||||
cache:
|
||||
localTTL: 60
|
||||
redisTTL: 300
|
||||
# Database.
|
||||
database:
|
||||
default:
|
||||
- type: "pgsql"
|
||||
host: "localhost"
|
||||
port: "5432"
|
||||
user: "postgres"
|
||||
pass: "123456"
|
||||
name: "customer_server"
|
||||
prefix: "customer_server_" # (可选)表名前缀
|
||||
role: "master" # (可选)数据库主从角色(master/slave),默认为master。如果不使用应用主从机制请不配置或留空即可。
|
||||
debug: false # (可选)开启调试模式
|
||||
dryRun: false # (可选)ORM空跑(只读不写)
|
||||
charset: "utf8" # (可选)数据库编码(如: utf8mb4/utf8/gbk/gb2312),一般设置为utf8mb4。默认为utf8。
|
||||
timezone: "Asia/Shanghai" # (可选)时区配置,例如:Local
|
||||
maxIdle: 5 # (可选)连接池最大闲置的连接数(默认10)
|
||||
maxOpen: 20 # (可选)连接池最大打开的连接数(默认无限制)
|
||||
maxLifetime: "30s" # (可选)连接对象可重复使用的时间长度(默认30秒)
|
||||
maxIdleConnTime: "30s" # (可选,v2.10新增)连接池中空闲连接的最大生存时间(默认30秒)。可以通过配置文件或SetConnMaxIdleTime方法设置,避免长时间空闲连接占用资源。
|
||||
createdAt: "created_at" # (可选)自动创建时间字段名称
|
||||
updatedAt: "updated_at" # (可选)自动更新时间字段名称
|
||||
deletedAt: "deleted_at" # (可选)软删除时间字段名称
|
||||
timeMaintainDisabled: false # (可选)是否完全关闭时间更新特性,为true时CreatedAt/UpdatedAt/DeletedAt都将失效
|
||||
- type: "pgsql"
|
||||
host: "localhost"
|
||||
port: "5432"
|
||||
user: "postgres"
|
||||
pass: "123456"
|
||||
name: "customer_server"
|
||||
prefix: "customer_server_" # (可选)表名前缀
|
||||
role: "slave" # (可选)数据库主从角色(master/slave),默认为master。如果不使用应用主从机制请不配置或留空即可。
|
||||
debug: false # (可选)开启调试模式
|
||||
dryRun: false # (可选)ORM空跑(只读不写)
|
||||
charset: "utf8" # (可选)数据库编码(如: utf8mb4/utf8/gbk/gb2312),一般设置为utf8mb4。默认为utf8。
|
||||
timezone: "Asia/Shanghai" # (可选)时区配置,例如:Local
|
||||
maxIdle: 5 # (可选)连接池最大闲置的连接数(默认10)
|
||||
maxOpen: 20 # (可选)连接池最大打开的连接数(默认无限制)
|
||||
maxLifetime: "30s" # (可选)连接对象可重复使用的时间长度(默认30秒)
|
||||
maxIdleConnTime: "30s" # (可选,v2.10新增)连接池中空闲连接的最大生存时间(默认30秒)。可以通过配置文件或SetConnMaxIdleTime方法设置,避免长时间空闲连接占用资源。
|
||||
createdAt: "created_at" # (可选)自动创建时间字段名称
|
||||
updatedAt: "updated_at" # (可选)自动更新时间字段名称
|
||||
deletedAt: "deleted_at" # (可选)软删除时间字段名称
|
||||
timeMaintainDisabled: false # (可选)是否完全关闭时间更新特性,为true时CreatedAt/UpdatedAt/DeletedAt都将失效
|
||||
|
||||
jwt:
|
||||
secret: "abcdefghijklmnopqrstuvwxyz"
|
||||
@@ -22,7 +68,7 @@ rate:
|
||||
# --- 内网服务器(192.168.3.200:27017,无认证) ---
|
||||
mongo:
|
||||
default:
|
||||
address: "mongodb://192.168.3.200:27017/?directConnection=true"
|
||||
address: "localhost:27017/?directConnection=true"
|
||||
database: "customer_service"
|
||||
logger:
|
||||
level: "all"
|
||||
@@ -30,7 +76,7 @@ mongo:
|
||||
|
||||
redis:
|
||||
default:
|
||||
address: 116.204.74.41:6379
|
||||
address: localhost:6379
|
||||
db: 0
|
||||
idleTimeout: "60s"
|
||||
maxConnLifetime: "90s"
|
||||
@@ -41,13 +87,13 @@ redis:
|
||||
maxActive: 100
|
||||
|
||||
consul:
|
||||
address: 116.204.74.41:8500
|
||||
address: localhost:8500
|
||||
|
||||
rabbitmq:
|
||||
host: 116.204.74.41
|
||||
host: localhost
|
||||
port: 5672
|
||||
username: root
|
||||
password: root
|
||||
username: admin
|
||||
password: 123456
|
||||
# 响应队列配置(从message迁移)
|
||||
responseExchange: "ragflow.response"
|
||||
responseQueue: "ragflow.response.queue"
|
||||
@@ -55,7 +101,7 @@ rabbitmq:
|
||||
# 不配置instanceName,直接使用os.Hostname()获取容器名/主机名作为实例ID
|
||||
|
||||
jaeger:
|
||||
addr: 116.204.74.41:4318
|
||||
addr: localhost:4318
|
||||
|
||||
# RAGFlow配置(customerservice只需要base_url和api_key用于更新prompt接口)
|
||||
ragflow:
|
||||
@@ -102,8 +148,3 @@ elasticsearch:
|
||||
- "http://116.204.74.41:9200"
|
||||
username: ""
|
||||
password: ""
|
||||
|
||||
database:
|
||||
logger:
|
||||
level: "none" # 关闭数据库日志
|
||||
stdout: false
|
||||
|
||||
27
consts/account/platform.go
Normal file
27
consts/account/platform.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package account
|
||||
|
||||
import "github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
var (
|
||||
PlatformXHS = newPlatform(gconv.PtrString("xiaohongshu"), "小红书")
|
||||
PlatformDY = newPlatform(gconv.PtrString("douyin"), "抖音")
|
||||
PlatformKS = newPlatform(gconv.PtrString("kuaishou"), "快手")
|
||||
)
|
||||
|
||||
type Platform *string
|
||||
|
||||
type platform struct {
|
||||
code Platform
|
||||
desc string
|
||||
}
|
||||
|
||||
func (s platform) Code() Platform {
|
||||
return s.code
|
||||
}
|
||||
func (s platform) Desc() string {
|
||||
return s.desc
|
||||
}
|
||||
|
||||
func newPlatform(code Platform, desc string) platform {
|
||||
return platform{code: code, desc: desc}
|
||||
}
|
||||
26
consts/account/status.go
Normal file
26
consts/account/status.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package account
|
||||
|
||||
import "github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
var (
|
||||
StatusDisable = newStatus(gconv.PtrInt8(0), "disable")
|
||||
StatusEnable = newStatus(gconv.PtrInt8(1), "enable")
|
||||
)
|
||||
|
||||
type Status *int8
|
||||
|
||||
type status struct {
|
||||
code Status
|
||||
desc string
|
||||
}
|
||||
|
||||
func (s status) Code() Status {
|
||||
return s.code
|
||||
}
|
||||
func (s status) Desc() string {
|
||||
return s.desc
|
||||
}
|
||||
|
||||
func newStatus(code Status, desc string) status {
|
||||
return status{code: code, desc: desc}
|
||||
}
|
||||
28
consts/account/vector_status.go
Normal file
28
consts/account/vector_status.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package account
|
||||
|
||||
import "github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
var (
|
||||
VectorStatusPending = newVectorStatus(gconv.PtrInt8(1), "pending")
|
||||
VectorStatusProcessing = newVectorStatus(gconv.PtrInt8(2), "processing")
|
||||
VectorStatusCompleted = newVectorStatus(gconv.PtrInt8(3), "completed")
|
||||
VectorStatusFailed = newVectorStatus(gconv.PtrInt8(4), "failed")
|
||||
)
|
||||
|
||||
type VectorStatus *int8
|
||||
|
||||
type vectorStatus struct {
|
||||
code VectorStatus
|
||||
desc string
|
||||
}
|
||||
|
||||
func (s vectorStatus) Code() VectorStatus {
|
||||
return s.code
|
||||
}
|
||||
func (s vectorStatus) Desc() string {
|
||||
return s.desc
|
||||
}
|
||||
|
||||
func newVectorStatus(code VectorStatus, desc string) vectorStatus {
|
||||
return vectorStatus{code: code, desc: desc}
|
||||
}
|
||||
15
consts/public/redis_key.go
Normal file
15
consts/public/redis_key.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package public
|
||||
|
||||
const KnowledgeLockEsKey = "rag:knowledge:lock:knowledgeIdEs-%v"
|
||||
const KnowledgeLockSqlKey = "rag:knowledge:lock:knowledgeIdSql-%v"
|
||||
const KnowledgeContentHashEsKey = "rag:knowledge:knowledgeId:contentHashEs-%v"
|
||||
const KnowledgeContentHashSqlKey = "rag:knowledge:knowledgeId:contentHashSql-%v"
|
||||
|
||||
const KnowledgeDocumentChunkTopic = "knowledge:document:chunk:stream" // 请求 Stream 键名(与发消息的key一致)
|
||||
|
||||
const (
|
||||
KnowledgeDocumentVectorStatusTopic = "knowledge:document:vector:status:stream"
|
||||
KnowledgeDocumentVectorStatusConsumer = "knowledge-document-vector-status-consumer"
|
||||
KnowledgeDocumentVectorStatusBatchSize = 1
|
||||
KnowledgeDocumentVectorStatusAutoAck = false
|
||||
)
|
||||
7
consts/public/table_name.go
Normal file
7
consts/public/table_name.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package public
|
||||
|
||||
// sql 数据库表名
|
||||
const (
|
||||
TableNameAccount = "account"
|
||||
TableNameScriptedSpeech = "scripted_speech"
|
||||
)
|
||||
64
controller/account_controller.go
Normal file
64
controller/account_controller.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Package controller - 客服账号控制器
|
||||
// 功能:客服账号的增删改查、状态切换
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"customer-server/model/dto"
|
||||
"customer-server/service"
|
||||
|
||||
"gitea.com/red-future/common/beans"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
var Account = new(account)
|
||||
|
||||
type account struct{}
|
||||
|
||||
// Add 添加客服账号
|
||||
// 参数: req - 添加客服账号请求,包含客服账号名称、平台等信息
|
||||
// 返回: res - 添加成功后的客服账号ID等信息
|
||||
// 功能: 创建新的客服账号记录
|
||||
func (c *account) Add(ctx context.Context, req *dto.AddAccountReq) (res *dto.AddAccountRes, err error) {
|
||||
res, err = service.AccountService.Add(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Update 更新客服账号
|
||||
// 参数: req - 更新客服账号请求,包含客服账号ID和待更新字段
|
||||
// 返回: res - 空响应(成功则err为nil)
|
||||
// 功能: 更新客服账号信息
|
||||
func (c *account) Update(ctx context.Context, req *dto.UpdateAccountReq) (res *beans.ResponseEmpty, err error) {
|
||||
err = service.AccountService.Update(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除客服账号
|
||||
// 参数: req - 删除客服账号请求,包含客服账号ID
|
||||
// 返回: res - 空响应(成功则err为nil)
|
||||
// 功能: 逻辑删除客服账号记录
|
||||
func (c *account) Delete(ctx context.Context, req *dto.DeleteAccountReq) (res *beans.ResponseEmpty, err error) {
|
||||
err = service.AccountService.Delete(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Get 获取单个客服账号
|
||||
// 参数: req - 获取客服账号请求,包含客服账号ID
|
||||
// 返回: res - 客服账号信息
|
||||
// 功能: 根据ID获取单个客服账号详情
|
||||
func (c *account) Get(ctx context.Context, req *dto.GetAccountReq) (res *dto.AccountVO, err error) {
|
||||
res, err = service.AccountService.Get(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取客服账号列表
|
||||
// 参数: req - 列表查询请求,支持分页、账号名称、状态、平台筛选
|
||||
// 返回: res - 客服账号列表及分页信息
|
||||
// 功能: 分页查询客服账号记录
|
||||
func (c *account) List(ctx context.Context, req *dto.ListAccountReq) (res *dto.ListAccountRes, err error) {
|
||||
if !g.IsEmpty(req.Page) {
|
||||
req.Page = &beans.Page{PageNum: 1, PageSize: 20}
|
||||
}
|
||||
res, err = service.AccountService.List(ctx, req)
|
||||
return
|
||||
}
|
||||
26
controller/account_websocket_controller.go
Normal file
26
controller/account_websocket_controller.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Package controller - WebSocket控制器
|
||||
// 功能:WebSocket连接管理、实时消息推送
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"customer-server/model/dto"
|
||||
"customer-server/service"
|
||||
|
||||
"gitea.com/red-future/common/beans"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
var AccountWebsocket = new(accountWebSocket)
|
||||
|
||||
type accountWebSocket struct{}
|
||||
|
||||
// Connect WebSocket连接
|
||||
// 参数: req - WebSocket连接请求,包含用户ID和平台信息
|
||||
// 返回: res - 连接结果(实际通过WebSocket协议通信)
|
||||
// 功能: 升级HTTP连接为WebSocket,建立实时通信通道
|
||||
func (c *accountWebSocket) Connect(ctx context.Context, req *dto.AccountWebSocketConnectReq) (res *beans.ResponseEmpty, err error) {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
err = service.AccountWebSocket.Connect(ctx, r, req.AccountName, req.Platform)
|
||||
return
|
||||
}
|
||||
64
controller/scripted_speech_controller.go
Normal file
64
controller/scripted_speech_controller.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Package controller - 预制话术控制器
|
||||
// 功能:预制话术的增删改查
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"customer-server/model/dto"
|
||||
"customer-server/service"
|
||||
|
||||
"gitea.com/red-future/common/beans"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
var ScriptedSpeech = new(scriptedSpeech)
|
||||
|
||||
type scriptedSpeech struct{}
|
||||
|
||||
// Add 添加预制话术
|
||||
// 参数: req - 添加预制话术请求,包含账号ID、数据集ID、问题、回答等信息
|
||||
// 返回: res - 添加成功后的预制话术ID等信息
|
||||
// 功能: 创建新的预制话术记录
|
||||
func (c *scriptedSpeech) Add(ctx context.Context, req *dto.AddScriptedSpeechReq) (res *dto.AddScriptedSpeechRes, err error) {
|
||||
res, err = service.ScriptedSpeech.Add(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Update 更新预制话术
|
||||
// 参数: req - 更新预制话术请求,包含预制话术ID和待更新字段
|
||||
// 返回: res - 空响应(成功则err为nil)
|
||||
// 功能: 更新预制话术信息
|
||||
func (c *scriptedSpeech) Update(ctx context.Context, req *dto.UpdateScriptedSpeechReq) (res *beans.ResponseEmpty, err error) {
|
||||
err = service.ScriptedSpeech.Update(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除预制话术
|
||||
// 参数: req - 删除预制话术请求,包含预制话术ID
|
||||
// 返回: res - 空响应(成功则err为nil)
|
||||
// 功能: 逻辑删除预制话术记录
|
||||
func (c *scriptedSpeech) Delete(ctx context.Context, req *dto.DeleteScriptedSpeechReq) (res *beans.ResponseEmpty, err error) {
|
||||
err = service.ScriptedSpeech.Delete(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Get 获取单个预制话术
|
||||
// 参数: req - 获取预制话术请求,包含预制话术ID
|
||||
// 返回: res - 预制话术信息
|
||||
// 功能: 根据ID获取单个预制话术详情
|
||||
func (c *scriptedSpeech) Get(ctx context.Context, req *dto.GetScriptedSpeechReq) (res *dto.ScriptedSpeechVO, err error) {
|
||||
res, err = service.ScriptedSpeech.Get(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取预制话术列表
|
||||
// 参数: req - 列表查询请求,支持分页、账号ID、数据集ID筛选
|
||||
// 返回: res - 预制话术列表及分页信息
|
||||
// 功能: 分页查询预制话术记录
|
||||
func (c *scriptedSpeech) List(ctx context.Context, req *dto.ListScriptedSpeechReq) (res *dto.ListScriptedSpeechRes, err error) {
|
||||
if g.IsEmpty(req.Page) {
|
||||
req.Page = &beans.Page{PageNum: 1, PageSize: 20}
|
||||
}
|
||||
res, err = service.ScriptedSpeech.List(ctx, req)
|
||||
return
|
||||
}
|
||||
90
dao/account_dao.go
Normal file
90
dao/account_dao.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"customer-server/consts/public"
|
||||
"customer-server/model/dto"
|
||||
"customer-server/model/entity"
|
||||
|
||||
"gitea.com/red-future/common/db/gfdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var Account = new(account)
|
||||
|
||||
type account struct{}
|
||||
|
||||
func (d *account) Insert(ctx context.Context, req *dto.AddAccountReq) (id int64, err error) {
|
||||
var e *entity.Account
|
||||
if err = gconv.Struct(req, &e); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := gfdb.DB(ctx).Model(ctx, public.TableNameAccount).Insert(e)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return result.LastInsertId()
|
||||
}
|
||||
|
||||
func (d *account) Update(ctx context.Context, req *dto.UpdateAccountReq) (rows int64, err error) {
|
||||
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAccount).Data(&req).Where(entity.AccountCol.Id, req.Id).Update()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return r.RowsAffected()
|
||||
}
|
||||
|
||||
func (d *account) Delete(ctx context.Context, req *dto.DeleteAccountReq) (rows int64, err error) {
|
||||
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAccount).Where(entity.AccountCol.Id, req.Id).Delete()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return r.RowsAffected()
|
||||
}
|
||||
|
||||
func (d *account) Count(ctx context.Context, req *dto.ListAccountReq) (count int, err error) {
|
||||
count, err = gfdb.DB(ctx).Model(ctx, public.TableNameAccount).OmitEmpty().Where(entity.AccountCol.AccountName, req.AccountName).Count()
|
||||
return
|
||||
}
|
||||
|
||||
// GetById 根据ID查询客服账号
|
||||
func (d *account) GetById(ctx context.Context, req *dto.GetAccountReq, fields ...string) (res *entity.Account, err error) {
|
||||
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAccount).Where(entity.AccountCol.Id, req.Id).Fields(fields).One()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = r.Struct(&res)
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取客服账号列表
|
||||
func (d *account) List(ctx context.Context, req *dto.ListAccountReq, fields ...string) (res []*entity.Account, total int, err error) {
|
||||
model := gfdb.DB(ctx).Model(ctx, public.TableNameAccount).Fields(fields).OmitEmpty()
|
||||
if !g.IsEmpty(req.Keyword) {
|
||||
model.WhereLike(entity.AccountCol.AccountName, "%"+req.Keyword+"%")
|
||||
}
|
||||
model.Where(entity.AccountCol.Status, req.Status)
|
||||
model.Where(entity.AccountCol.Platform, req.Platform)
|
||||
model.OrderDesc(entity.AccountCol.CreatedAt)
|
||||
if req.Page != nil {
|
||||
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||
}
|
||||
r, total, err := model.AllAndCount(false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = r.Structs(&res)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetByAccountName 根据账号名称查询客服账号(GoFrame框架原声,绕过用户信息校验)
|
||||
func (d *account) GetByAccountName(ctx context.Context, req *dto.GetByAccountNameReq, fields ...string) (res *entity.Account, err error) {
|
||||
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAccount).NoTenantId(ctx).Where(entity.AccountCol.AccountName, req.AccountName).Fields(fields).One()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = r.Struct(&res)
|
||||
return
|
||||
}
|
||||
71
dao/scripted_speech_dao.go
Normal file
71
dao/scripted_speech_dao.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"customer-server/consts/public"
|
||||
"customer-server/model/dto"
|
||||
"customer-server/model/entity"
|
||||
|
||||
"gitea.com/red-future/common/db/gfdb"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var ScriptedSpeech = new(scriptedSpeech)
|
||||
|
||||
type scriptedSpeech struct{}
|
||||
|
||||
func (d *scriptedSpeech) Insert(ctx context.Context, req *dto.AddScriptedSpeechReq) (id int64, err error) {
|
||||
var e *entity.ScriptedSpeech
|
||||
if err = gconv.Struct(req, &e); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := gfdb.DB(ctx).Model(ctx, public.TableNameScriptedSpeech).Insert(e)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return result.LastInsertId()
|
||||
}
|
||||
|
||||
func (d *scriptedSpeech) Update(ctx context.Context, req *dto.UpdateScriptedSpeechReq) (rows int64, err error) {
|
||||
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameScriptedSpeech).Data(&req).Where(entity.ScriptedSpeechCol.Id, req.Id).Update()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return r.RowsAffected()
|
||||
}
|
||||
|
||||
func (d *scriptedSpeech) Delete(ctx context.Context, req *dto.DeleteScriptedSpeechReq) (rows int64, err error) {
|
||||
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameScriptedSpeech).Where(entity.ScriptedSpeechCol.Id, req.Id).Delete()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return r.RowsAffected()
|
||||
}
|
||||
|
||||
// GetById 根据ID查询预制话术
|
||||
func (d *scriptedSpeech) GetById(ctx context.Context, req *dto.GetScriptedSpeechReq, fields ...string) (res *entity.ScriptedSpeech, err error) {
|
||||
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameScriptedSpeech).Where(entity.ScriptedSpeechCol.Id, req.Id).Fields(fields).One()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = r.Struct(&res)
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取预制话术列表
|
||||
func (d *scriptedSpeech) List(ctx context.Context, req *dto.ListScriptedSpeechReq, fields ...string) (res []*entity.ScriptedSpeech, total int, err error) {
|
||||
model := gfdb.DB(ctx).Model(ctx, public.TableNameScriptedSpeech).Fields(fields).OmitEmpty()
|
||||
model.Where(entity.ScriptedSpeechCol.AccountId, req.AccountId)
|
||||
model.Where(entity.ScriptedSpeechCol.DatasetId, req.DatasetId)
|
||||
model.OrderDesc(entity.ScriptedSpeechCol.CreatedAt)
|
||||
if req.Page != nil {
|
||||
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))
|
||||
}
|
||||
r, total, err := model.AllAndCount(false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = r.Structs(&res)
|
||||
|
||||
return
|
||||
}
|
||||
11
go.mod
11
go.mod
@@ -1,11 +1,12 @@
|
||||
module customer-server
|
||||
|
||||
go 1.25.7
|
||||
go 1.26.0
|
||||
|
||||
//replace gitea.com/red-future/common => ../common
|
||||
replace gitea.com/red-future/common v0.0.7 => ../common
|
||||
|
||||
require (
|
||||
gitea.com/red-future/common v0.0.4
|
||||
gitea.com/red-future/common v0.0.7
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0
|
||||
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0
|
||||
github.com/gogf/gf/v2 v2.10.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
@@ -16,6 +17,7 @@ require (
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/bwmarrin/snowflake v0.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
@@ -51,8 +53,9 @@ require (
|
||||
github.com/hashicorp/serf v0.10.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
|
||||
12
go.sum
12
go.sum
@@ -1,6 +1,4 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
gitea.com/red-future/common v0.0.3/go.mod h1:mq4smQZFI5nYul6gvLH7ScnC/26bAOcTvR3hP625NYY=
|
||||
gitea.com/red-future/common v0.0.4/go.mod h1:UI9N5UUjilbMPF7+/lypZSnqDVHigt14300oSRrAyZg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
@@ -23,6 +21,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
|
||||
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -77,6 +77,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 h1:39+jbTenm7KBj4hO2C8ANAxVHpX/7OuRDs1VcGC9ylA=
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0/go.mod h1:B0s0fVzn0W220E8UTpSGzrrGKsop5KcB90twBeLCiz0=
|
||||
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0 h1:N/F9CuDdUZLoM1nVRqrDE/33pDZuhVxpNY4wYdeIaBs=
|
||||
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0/go.mod h1:x6uoJGfZOtirIRQls8xUlYzC6f7T/eULPUa9er368X0=
|
||||
github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 h1:eUqwJ/qNH8lJ6yssiqskazgp1ACQuNU6zXlLOZVuXTQ=
|
||||
@@ -196,10 +198,12 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
|
||||
77
main.go
77
main.go
@@ -8,10 +8,10 @@ import (
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"gitea.com/red-future/common/elasticsearch"
|
||||
"gitea.com/red-future/common/http"
|
||||
"gitea.com/red-future/common/jaeger"
|
||||
"gitea.com/red-future/common/rabbitmq"
|
||||
_ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
|
||||
_ "github.com/gogf/gf/contrib/nosql/redis/v2"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
@@ -31,6 +31,9 @@ func main() {
|
||||
|
||||
// 路由注册(common/http init() 已异步启动服务器,这里注册路由不影响)
|
||||
http.RouteRegister([]interface{}{
|
||||
controller.Account,
|
||||
controller.ScriptedSpeech,
|
||||
controller.AccountWebsocket,
|
||||
controller.Health,
|
||||
controller.Archive,
|
||||
controller.CustomerServiceAccount,
|
||||
@@ -45,41 +48,41 @@ func main() {
|
||||
controller.XiaohongshuController, // 小红书Webhook接口
|
||||
})
|
||||
|
||||
// 初始化消费者管理器并注册所有消费者
|
||||
mgr := rabbitmq.NewConsumerManager(ctx)
|
||||
|
||||
// 注册响应消费者
|
||||
responseConsumer := service.NewResponseConsumer(ctx)
|
||||
mgr.Register("响应消费者", responseConsumer.Start, responseConsumer.Stop)
|
||||
|
||||
// 注册追问消费者
|
||||
followUpConsumer := service.NewFollowUpConsumer(ctx)
|
||||
mgr.Register("追问消费者", followUpConsumer.Start, followUpConsumer.Stop)
|
||||
|
||||
// 注册会话归档消费者
|
||||
sessionArchiveConsumer := service.NewSessionArchiveConsumer(ctx)
|
||||
mgr.Register("会话归档消费者", sessionArchiveConsumer.Start, sessionArchiveConsumer.Stop)
|
||||
|
||||
// 注册延时落库消费者
|
||||
delayedFlushConsumer := service.NewDelayedFlushConsumer(ctx)
|
||||
mgr.Register("延时落库消费者", delayedFlushConsumer.Start, delayedFlushConsumer.Stop)
|
||||
|
||||
// 初始化并启动所有消费者
|
||||
if err := mgr.Init(); err != nil {
|
||||
glog.Fatalf(ctx, "消费者管理器初始化失败: %v", err)
|
||||
}
|
||||
|
||||
// 初始化 ES 客户端
|
||||
if !g.Cfg().MustGet(ctx, "elasticsearch").IsEmpty() {
|
||||
if err := elasticsearch.Init(ctx); err != nil {
|
||||
glog.Warningf(ctx, "ES 初始化失败(月度归档功能不可用): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 启动月度归档定时任务
|
||||
if !g.Cfg().MustGet(ctx, "mongo").IsEmpty() && !g.Cfg().MustGet(ctx, "elasticsearch").IsEmpty() {
|
||||
service.ArchiveService.StartCron(ctx)
|
||||
}
|
||||
//// 初始化消费者管理器并注册所有消费者
|
||||
//mgr := rabbitmq.NewConsumerManager(ctx)
|
||||
//
|
||||
//// 注册响应消费者
|
||||
//responseConsumer := service.NewResponseConsumer(ctx)
|
||||
//mgr.Register("响应消费者", responseConsumer.Start, responseConsumer.Stop)
|
||||
//
|
||||
//// 注册追问消费者
|
||||
//followUpConsumer := service.NewFollowUpConsumer(ctx)
|
||||
//mgr.Register("追问消费者", followUpConsumer.Start, followUpConsumer.Stop)
|
||||
//
|
||||
//// 注册会话归档消费者
|
||||
//sessionArchiveConsumer := service.NewSessionArchiveConsumer(ctx)
|
||||
//mgr.Register("会话归档消费者", sessionArchiveConsumer.Start, sessionArchiveConsumer.Stop)
|
||||
//
|
||||
//// 注册延时落库消费者
|
||||
//delayedFlushConsumer := service.NewDelayedFlushConsumer(ctx)
|
||||
//mgr.Register("延时落库消费者", delayedFlushConsumer.Start, delayedFlushConsumer.Stop)
|
||||
//
|
||||
//// 初始化并启动所有消费者
|
||||
//if err := mgr.Init(); err != nil {
|
||||
// glog.Fatalf(ctx, "消费者管理器初始化失败: %v", err)
|
||||
//}
|
||||
//
|
||||
//// 初始化 ES 客户端
|
||||
//if !g.Cfg().MustGet(ctx, "elasticsearch").IsEmpty() {
|
||||
// if err := elasticsearch.Init(ctx); err != nil {
|
||||
// glog.Warningf(ctx, "ES 初始化失败(月度归档功能不可用): %v", err)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// 启动月度归档定时任务
|
||||
//if !g.Cfg().MustGet(ctx, "mongo").IsEmpty() && !g.Cfg().MustGet(ctx, "elasticsearch").IsEmpty() {
|
||||
// service.ArchiveService.StartCron(ctx)
|
||||
//}
|
||||
|
||||
// 监听系统信号,支持 Ctrl+C 优雅退出
|
||||
quit := make(chan os.Signal, 1)
|
||||
@@ -89,7 +92,7 @@ func main() {
|
||||
glog.Info(ctx, "收到停止信号,正在关闭服务...")
|
||||
|
||||
// 停止所有消费者并关闭RabbitMQ连接
|
||||
mgr.Stop()
|
||||
//mgr.Stop()
|
||||
|
||||
glog.Info(ctx, "customerservice 服务已停止")
|
||||
}
|
||||
|
||||
90
model/dto/account_dto.go
Normal file
90
model/dto/account_dto.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Package dto - 客服账号DTO
|
||||
// 功能:客服账号的增删改查请求响应结构体
|
||||
package dto
|
||||
|
||||
import (
|
||||
"customer-server/consts/account"
|
||||
|
||||
"gitea.com/red-future/common/beans"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// AddAccountReq 添加客服账号
|
||||
type AddAccountReq struct {
|
||||
g.Meta `path:"/add" method:"post" tags:"客服账号管理" summary:"添加客服账号" dc:"创建新的客服账号"`
|
||||
|
||||
DatasetIds []int64 `json:"datasetIds" v:"required#数据集ID不能为空" dc:"数据集ID列表"`
|
||||
DocumentIds []int64 `json:"documentIds" v:"required#文档ID不能为空" dc:"文档ID列表"`
|
||||
AccountName string `json:"accountName" v:"required#客服账号名称不能为空"`
|
||||
Status account.Status `json:"status" dc:"客服账号状态"`
|
||||
Greeting string `json:"greeting" dc:"开场白"`
|
||||
Prompt []string `json:"prompt" dc:"提示词"`
|
||||
SelfIdentity string `json:"selfIdentity" dc:"AI身份描述"`
|
||||
Platform account.Platform `json:"platform" v:"required#客服平台不能为空" dc:"客服平台"`
|
||||
}
|
||||
|
||||
type AddAccountRes struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
// UpdateAccountReq 更新客服账号
|
||||
type UpdateAccountReq struct {
|
||||
g.Meta `path:"/update" method:"post" tags:"客服账号管理" summary:"更新客服账号" dc:"更新客服账号信息"`
|
||||
|
||||
Id int64 `json:"id" v:"required#客服账号ID不能为空" dc:"客服账号ID"`
|
||||
DatasetIds []int64 `json:"datasetIds" dc:"数据集ID列表"`
|
||||
DocumentIds []int64 `json:"documentIds" dc:"文档ID列表"`
|
||||
AccountName string `json:"accountName" dc:"客服账号名称"`
|
||||
Status account.Status `json:"status" dc:"客服账号状态"`
|
||||
Greeting string `json:"greeting" dc:"开场白"`
|
||||
Prompt []string `json:"prompt" dc:"提示词"`
|
||||
SelfIdentity string `json:"selfIdentity" dc:"AI身份描述"`
|
||||
Platform account.Platform `json:"platform" dc:"客服平台"`
|
||||
}
|
||||
|
||||
// DeleteAccountReq 删除客服账号
|
||||
type DeleteAccountReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"客服账号管理" summary:"删除客服账号" dc:"删除指定客服账号"`
|
||||
Id int64 `json:"id" v:"required#客服账号ID不能为空" dc:"客服账号ID"`
|
||||
}
|
||||
|
||||
// GetAccountReq 获取单个客服账号
|
||||
type GetAccountReq struct {
|
||||
g.Meta `path:"/getOne" method:"get" tags:"客服账号管理" summary:"获取客服账号详情" dc:"根据ID获取单个客服账号信息"`
|
||||
Id int64 `json:"id" v:"required#客服账号ID不能为空" dc:"客服账号ID"`
|
||||
}
|
||||
|
||||
// ListAccountReq 获取客服账号列表
|
||||
type ListAccountReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"客服账号管理" summary:"获取客服账号列表" dc:"分页查询客服账号,支持按账号名称、状态、平台筛选"`
|
||||
Page *beans.Page `json:"page"`
|
||||
Keyword string `json:"keyword" dc:"关键字"`
|
||||
AccountName string `json:"accountName" dc:"客服账号名称"`
|
||||
Status account.Status `json:"status" dc:"客服账号状态"`
|
||||
Platform account.Platform `json:"platform" dc:"客服平台"`
|
||||
}
|
||||
|
||||
type ListAccountRes struct {
|
||||
List []*AccountVO `json:"list"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// AccountVO 客服账号视图对象
|
||||
type AccountVO struct {
|
||||
Id int64 `json:"id,string"`
|
||||
DatasetIds []int64 `json:"datasetIds,string"`
|
||||
DocumentIds []int64 `json:"documentIds,string"`
|
||||
AccountName string `json:"accountName"`
|
||||
Status account.Status `json:"status"`
|
||||
Greeting string `json:"greeting"`
|
||||
Prompt []string `json:"prompt"`
|
||||
SelfIdentity string `json:"selfIdentity"`
|
||||
Platform account.Platform `json:"platform"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// GetByAccountNameReq 根据账号名称获取客服账号
|
||||
type GetByAccountNameReq struct {
|
||||
AccountName string `json:"accountName" dc:"客服账号名称"`
|
||||
}
|
||||
14
model/dto/account_websocket_dto.go
Normal file
14
model/dto/account_websocket_dto.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"customer-server/consts/account"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// AccountWebSocketConnectReq WebSocket 连接请求
|
||||
type AccountWebSocketConnectReq struct {
|
||||
g.Meta `path:"/accountConnect" method:"get" tags:"AccountWebSocket" summary:"WebSocket连接" dc:"建立WebSocket连接,用于实时消息推送"`
|
||||
AccountName string `json:"accountName" v:"required#客服账号不能为空" dc:"客服账号"`
|
||||
Platform account.Platform `json:"platform" v:"required#平台不能为空" dc:"平台"`
|
||||
}
|
||||
73
model/dto/scripted_speech_dto.go
Normal file
73
model/dto/scripted_speech_dto.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Package dto - 预制话术DTO
|
||||
// 功能:预制话术的增删改查请求响应结构体
|
||||
package dto
|
||||
|
||||
import (
|
||||
"gitea.com/red-future/common/beans"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// AddScriptedSpeechReq 添加预制话术
|
||||
type AddScriptedSpeechReq struct {
|
||||
g.Meta `path:"/add" method:"post" tags:"预制话术管理" summary:"添加预制话术" dc:"创建新的预制话术"`
|
||||
|
||||
AccountId int64 `json:"accountId" v:"required#账号ID不能为空" dc:"账号ID"`
|
||||
DatasetId int64 `json:"datasetId" v:"required#数据集ID不能为空" dc:"数据集ID"`
|
||||
QuestionContent string `json:"questionContent" v:"required#问题内容不能为空" dc:"问题内容"`
|
||||
AnswerContent string `json:"answerContent" v:"required#回答内容不能为空" dc:"回答内容"`
|
||||
}
|
||||
|
||||
type AddScriptedSpeechRes struct {
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
// UpdateScriptedSpeechReq 更新预制话术
|
||||
type UpdateScriptedSpeechReq struct {
|
||||
g.Meta `path:"/update" method:"post" tags:"预制话术管理" summary:"更新预制话术" dc:"更新预制话术内容"`
|
||||
|
||||
Id int64 `json:"id" v:"required#预制话术ID不能为空" dc:"预制话术ID"`
|
||||
AccountId *int64 `json:"accountId" dc:"账号ID"`
|
||||
DatasetId *int64 `json:"datasetId" dc:"数据集ID"`
|
||||
QuestionContent string `json:"questionContent" dc:"问题内容"`
|
||||
AnswerContent string `json:"answerContent" dc:"回答内容"`
|
||||
}
|
||||
|
||||
// DeleteScriptedSpeechReq 删除预制话术
|
||||
type DeleteScriptedSpeechReq struct {
|
||||
g.Meta `path:"/delete" method:"post" tags:"预制话术管理" summary:"删除预制话术" dc:"删除指定预制话术"`
|
||||
|
||||
Id int64 `json:"id" v:"required#预制话术ID不能为空" dc:"预制话术ID"`
|
||||
}
|
||||
|
||||
// GetScriptedSpeechReq 获取单个预制话术
|
||||
type GetScriptedSpeechReq struct {
|
||||
g.Meta `path:"/getOne" method:"get" tags:"预制话术管理" summary:"获取预制话术详情" dc:"根据ID获取单个预制话术"`
|
||||
|
||||
Id int64 `json:"id" v:"required#预制话术ID不能为空" dc:"预制话术ID"`
|
||||
}
|
||||
|
||||
// ListScriptedSpeechReq 获取预制话术列表
|
||||
type ListScriptedSpeechReq struct {
|
||||
g.Meta `path:"/list" method:"get" tags:"预制话术管理" summary:"获取预制话术列表" dc:"分页查询预制话术,支持按账号ID、数据集ID筛选"`
|
||||
|
||||
Page *beans.Page `json:"page"`
|
||||
AccountId int64 `json:"accountId" dc:"账号ID"`
|
||||
DatasetId int64 `json:"datasetId" dc:"数据集ID"`
|
||||
}
|
||||
|
||||
type ListScriptedSpeechRes struct {
|
||||
List []*ScriptedSpeechVO `json:"list"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// ScriptedSpeechVO 预制话术视图对象
|
||||
type ScriptedSpeechVO struct {
|
||||
Id int64 `json:"id,string"`
|
||||
TenantId uint64 `json:"tenantId,string"`
|
||||
AccountId int64 `json:"accountId,string"`
|
||||
DatasetId int64 `json:"datasetId,string"`
|
||||
QuestionContent string `json:"questionContent"`
|
||||
AnswerContent string `json:"answerContent"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
76
model/entity/account.go
Normal file
76
model/entity/account.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"customer-server/consts/account"
|
||||
|
||||
"gitea.com/red-future/common/beans"
|
||||
)
|
||||
|
||||
type accountCol struct {
|
||||
beans.SQLBaseCol
|
||||
DatasetIds string
|
||||
DocumentIds string
|
||||
SpeechcraftIds string
|
||||
AccountName string
|
||||
Status string
|
||||
Greeting string
|
||||
Prompt string
|
||||
SelfIdentity string
|
||||
Platform string
|
||||
AccessToken string
|
||||
AppId string
|
||||
SecretKey string
|
||||
XhsUserId string
|
||||
ContactCardMessage string
|
||||
NameCardMessage string
|
||||
CardTriggerCount string
|
||||
}
|
||||
|
||||
var AccountCol = accountCol{
|
||||
SQLBaseCol: beans.DefSQLBaseCol,
|
||||
DatasetIds: "dataset_ids",
|
||||
DocumentIds: "document_ids",
|
||||
SpeechcraftIds: "speechcraft_ids",
|
||||
AccountName: "account_name",
|
||||
Status: "status",
|
||||
Greeting: "greeting",
|
||||
Prompt: "prompt",
|
||||
SelfIdentity: "self_identity",
|
||||
Platform: "platform",
|
||||
AccessToken: "access_token",
|
||||
AppId: "app_id",
|
||||
SecretKey: "secret_key",
|
||||
XhsUserId: "xhs_user_id",
|
||||
ContactCardMessage: "contact_card_message",
|
||||
NameCardMessage: "name_card_message",
|
||||
CardTriggerCount: "card_trigger_count",
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
beans.SQLBaseDO `orm:",inline"`
|
||||
|
||||
DatasetIds []int64 `orm:"dataset_ids" json:"datasetIds" dc:"绑定的数据集ID列表"`
|
||||
DocumentIds []int64 `orm:"document_ids" json:"documentIds" dc:"绑定的文档ID列表"`
|
||||
AccountName string `orm:"account_name" json:"accountName" dc:"客服账号名称"`
|
||||
Status account.Status `orm:"status" json:"status" dc:"客服账号状态"`
|
||||
Platform account.Platform `orm:"platform" json:"platform" dc:"客服平台"`
|
||||
Greeting string `orm:"greeting" json:"greeting" dc:"开场白"`
|
||||
Prompt []string `orm:"prompt" json:"prompt" dc:"提示词"`
|
||||
SelfIdentity string `orm:"self_identity" json:"selfIdentity" dc:"AI身份描述"`
|
||||
ExpandData *AccountExpandData `orm:"expand_data" json:"expandData" description:"扩展数据(JSONB)"`
|
||||
}
|
||||
|
||||
type AccountExpandData struct {
|
||||
Xhs XhsExpandData `orm:",inline" json:"xhs"`
|
||||
}
|
||||
|
||||
type XhsExpandData struct {
|
||||
// 小红书平台专属字段(仅platform=xiaohongshu时有效)
|
||||
AccessToken string `orm:"access_token" json:"accessToken" dc:"小红书AccessToken(14天有效期)"`
|
||||
AppId int64 `orm:"app_id" json:"appId" dc:"小红书应用ID"`
|
||||
SecretKey string `orm:"secret_key" json:"secretKey" dc:"小红书加解密密钥"`
|
||||
XhsUserId string `orm:"xhs_user_id" json:"xhsUserId" dc:"小红书用户ID"`
|
||||
ContactCardMessage string `orm:"contact_card_message" json:"contactCardMessage" dc:"留资卡文案"`
|
||||
NameCardMessage string `orm:"name_card_message" json:"nameCardMessage" dc:"名片文案"`
|
||||
CardTriggerCount int `orm:"card_trigger_count" json:"cardTriggerCount" dc:"卡片触发次数"`
|
||||
}
|
||||
30
model/entity/scripted_speech.go
Normal file
30
model/entity/scripted_speech.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"gitea.com/red-future/common/beans"
|
||||
)
|
||||
|
||||
type scriptedSpeechCol struct {
|
||||
beans.SQLBaseCol
|
||||
AccountId string
|
||||
DatasetId string
|
||||
QuestionContent string
|
||||
AnswerContent string
|
||||
}
|
||||
|
||||
var ScriptedSpeechCol = scriptedSpeechCol{
|
||||
SQLBaseCol: beans.DefSQLBaseCol,
|
||||
AccountId: "account_id",
|
||||
DatasetId: "dataset_id",
|
||||
QuestionContent: "question_content",
|
||||
AnswerContent: "answer_content",
|
||||
}
|
||||
|
||||
type ScriptedSpeech struct {
|
||||
beans.SQLBaseDO `orm:",inline"`
|
||||
|
||||
AccountId int64 `orm:"account_id" json:"accountId" dc:"账号ID"`
|
||||
DatasetId int64 `orm:"dataset_id" json:"datasetId" dc:"数据集ID"`
|
||||
QuestionContent string `orm:"question_content" json:"questionContent" dc:"问题内容"`
|
||||
AnswerContent string `orm:"answer_content" json:"answerContent" dc:"回答内容"`
|
||||
}
|
||||
92
service/account_service.go
Normal file
92
service/account_service.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// Package service - 客服账号服务
|
||||
// 功能:客服账号的增删改查、状态切换
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"customer-server/dao"
|
||||
"customer-server/model/dto"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var (
|
||||
AccountService = new(accountService)
|
||||
)
|
||||
|
||||
type accountService struct{}
|
||||
|
||||
// Add 添加客服账号
|
||||
// 参数: ctx - 上下文,req - 添加客服账号请求
|
||||
// 返回: res - 添加成功后的客服账号ID,err - 错误信息
|
||||
// 功能: 创建新的客服账号记录
|
||||
func (s *accountService) Add(ctx context.Context, req *dto.AddAccountReq) (res *dto.AddAccountRes, err error) {
|
||||
// 检查账号名称是否已存在
|
||||
count, err := dao.Account.Count(ctx, &dto.ListAccountReq{
|
||||
AccountName: req.AccountName,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
err = gerror.Newf("客服账号名称已存在:%s", req.AccountName)
|
||||
return
|
||||
}
|
||||
|
||||
// 插入数据库
|
||||
id, err := dao.Account.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.AddAccountRes{Id: id}
|
||||
return
|
||||
}
|
||||
|
||||
// Update 更新客服账号
|
||||
// 参数: ctx - 上下文,req - 更新客服账号请求
|
||||
// 返回: err - 错误信息
|
||||
// 功能: 更新客服账号信息
|
||||
func (s *accountService) Update(ctx context.Context, req *dto.UpdateAccountReq) (err error) {
|
||||
_, err = dao.Account.Update(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除客服账号
|
||||
// 参数: ctx - 上下文,req - 删除客服账号请求
|
||||
// 返回: err - 错误信息
|
||||
// 功能: 逻辑删除客服账号记录
|
||||
func (s *accountService) Delete(ctx context.Context, req *dto.DeleteAccountReq) (err error) {
|
||||
_, err = dao.Account.Delete(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Get 获取单个客服账号
|
||||
// 参数: ctx - 上下文,req - 获取客服账号请求
|
||||
// 返回: res - 客服账号信息,err - 错误信息
|
||||
// 功能: 根据ID获取单个客服账号详情
|
||||
func (s *accountService) Get(ctx context.Context, req *dto.GetAccountReq) (res *dto.AccountVO, err error) {
|
||||
r, err := dao.Account.GetById(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = gconv.Struct(r, &res)
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取客服账号列表
|
||||
// 参数: ctx - 上下文,req - 列表查询请求
|
||||
// 返回: res - 客服账号列表及分页信息,err - 错误信息
|
||||
// 功能: 分页查询客服账号记录
|
||||
func (s *accountService) List(ctx context.Context, req *dto.ListAccountReq) (res *dto.ListAccountRes, err error) {
|
||||
list, total, err := dao.Account.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListAccountRes{
|
||||
Total: total,
|
||||
}
|
||||
err = gconv.Struct(list, &res.List)
|
||||
|
||||
return
|
||||
}
|
||||
187
service/account_websocket_service.go
Normal file
187
service/account_websocket_service.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"customer-server/consts/account"
|
||||
"customer-server/dao"
|
||||
"customer-server/model/dto"
|
||||
"customer-server/model/entity"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"gitea.com/red-future/common/jaeger"
|
||||
"github.com/gogf/gf/v2/container/gmap"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// AccountWebSocket 全局单例
|
||||
var AccountWebSocket = &accountWebsocketService{
|
||||
connections: gmap.NewStrAnyMap(true),
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // 允许跨域
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type accountWebsocketService struct {
|
||||
connections *gmap.StrAnyMap
|
||||
upgrader websocket.Upgrader
|
||||
}
|
||||
|
||||
// key: userId_platform
|
||||
// accountWsConnection WebSocket 连接信息
|
||||
type accountWsConnection struct {
|
||||
UserId string
|
||||
Platform account.Platform
|
||||
TenantId uint64
|
||||
AccountName string // 客服账号ID
|
||||
Conn *websocket.Conn
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
// Connect 建立 WebSocket 连接
|
||||
func (s *accountWebsocketService) Connect(ctx context.Context, r *ghttp.Request, accountName string, platform account.Platform) error {
|
||||
// 使用原生upgrader升级WebSocket连接
|
||||
ws, err := s.upgrader.Upgrade(r.Response.Writer, r.Request, nil)
|
||||
if err != nil {
|
||||
jaeger.RecordError(ctx, err, "WebSocket 升级失败")
|
||||
return err
|
||||
}
|
||||
defer ws.Close()
|
||||
if g.IsEmpty(accountName) {
|
||||
return errors.New("accountName is empty")
|
||||
}
|
||||
res, err := s.getGreeting(ctx, accountName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if g.IsEmpty(&res) {
|
||||
return errors.New("account is empty")
|
||||
}
|
||||
if !g.IsEmpty(res.Greeting) {
|
||||
s.writeJSON(ws, &dto.WebSocketPushMsg{
|
||||
Type: "message",
|
||||
Message: res.Greeting,
|
||||
})
|
||||
glog.Infof(ctx, "已发送开场白 - 用户: %v, 客服账号: %s, 长度: %d", res.Id, accountName, len(res.Greeting))
|
||||
} else {
|
||||
glog.Warningf(ctx, "客服账号未配置开场白 - accountName: %s, tenantId: %v", accountName, res.TenantId)
|
||||
}
|
||||
|
||||
// key格式: tenantId:userId_platform (确保租户隔离)
|
||||
key := gconv.String(res.TenantId) + ":" + gconv.String(res.Creator) + ":" + gconv.String(platform)
|
||||
|
||||
// 关闭旧连接
|
||||
if old := s.connections.Get(key); old != nil {
|
||||
old.(*accountWsConnection).Conn.Close()
|
||||
}
|
||||
|
||||
// 注册新连接(携带 TenantId 和 AccountName)
|
||||
s.connections.Set(key, &accountWsConnection{
|
||||
UserId: res.Creator,
|
||||
Platform: platform,
|
||||
TenantId: res.TenantId,
|
||||
AccountName: accountName,
|
||||
Conn: ws,
|
||||
CreatedAt: gtime.Now().Timestamp(),
|
||||
})
|
||||
|
||||
// 处理消息(阻塞)
|
||||
s.handleConnection(ctx, key, ws)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleConnection 处理 WebSocket 连接
|
||||
func (s *accountWebsocketService) handleConnection(ctx context.Context, key string, conn *websocket.Conn) {
|
||||
defer func() {
|
||||
s.connections.Remove(key)
|
||||
conn.Close()
|
||||
glog.Infof(ctx, "WebSocket 连接断开 - %s", key)
|
||||
}()
|
||||
|
||||
for {
|
||||
msgType, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
// 排除正常关闭情况:正常关闭、离开页面、无状态码关闭
|
||||
if websocket.IsUnexpectedCloseError(err,
|
||||
websocket.CloseNormalClosure,
|
||||
websocket.CloseGoingAway,
|
||||
websocket.CloseNoStatusReceived,
|
||||
) {
|
||||
jaeger.RecordError(ctx, err, "WebSocket 读取错误")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if msgType != websocket.TextMessage {
|
||||
continue
|
||||
}
|
||||
|
||||
content := gconv.String(message)
|
||||
glog.Infof(ctx, "收到 WebSocket 消息 - %s: %s", key, content)
|
||||
|
||||
// 解析 userId
|
||||
//connInfo := s.connections.Get(key)
|
||||
//if connInfo == nil {
|
||||
// break
|
||||
//}
|
||||
//wsConn := connInfo.(*accountWsConnection)
|
||||
|
||||
// 先检查对话轮数,>5 则只发卡片,跳过话术
|
||||
//checkCardBeforeProcess 已推送卡片消息,无需ack
|
||||
//if handled, err := checkCardBeforeProcess(ctx, wsConn.TenantId, wsConn.UserId, wsConn.Platform); err != nil {
|
||||
// jaeger.RecordError(ctx, err, "卡片检查失败")
|
||||
//} else if handled {
|
||||
// continue
|
||||
//}
|
||||
|
||||
// 话术匹配并发布响应
|
||||
// status 暂时为空,表示任意行为匹配
|
||||
// isPushed=true表示已直接推送响应(话术匹配),无需ack
|
||||
// isPushed=false表示转发到RAGFlow,需要ack告知用户正在处理
|
||||
|
||||
// 创建带有accountName的context,供GetTenantInfo使用
|
||||
//newCtx := ctx
|
||||
//if wsConn.AccountName != "" {
|
||||
// newCtx = context.WithValue(ctx, "accountName", wsConn.AccountName)
|
||||
//}
|
||||
|
||||
//isPushed, err := Speechcraft.ProcessAndPublish(newCtx, wsConn.UserId, wsConn.Platform, wsConn.TenantId, content, "", wsConn.AccountName)
|
||||
//if err != nil {
|
||||
// jaeger.RecordError(ctx, err, "话术处理失败")
|
||||
// s.writeJSON(conn, &dto.WebSocketPushMsg{Type: "error", Message: "消息处理失败"})
|
||||
// continue
|
||||
//}
|
||||
|
||||
// 只在转发到RAGFlow时发送ack(Go直接返回的不需要ack)
|
||||
//if !isPushed {
|
||||
// s.writeJSON(conn, &dto.WebSocketPushMsg{Type: "ack", Message: "消息已接收,正在处理..."})
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
// writeJSON 发送 JSON 消息
|
||||
func (s *accountWebsocketService) writeJSON(conn *websocket.Conn, data interface{}) {
|
||||
jsonBytes, _ := gjson.Encode(data)
|
||||
conn.WriteMessage(websocket.TextMessage, jsonBytes)
|
||||
}
|
||||
|
||||
// getGreeting 获取客服账号的开场白
|
||||
func (s *accountWebsocketService) getGreeting(ctx context.Context, accountName string) (res *entity.Account, err error) {
|
||||
res, err = dao.Account.GetByAccountName(ctx, &dto.GetByAccountNameReq{
|
||||
AccountName: accountName,
|
||||
})
|
||||
if err != nil {
|
||||
jaeger.RecordError(ctx, err, "查询客服账号开场白失败")
|
||||
glog.Errorf(ctx, "查询开场白失败: %v", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
90
service/scripted_speech_service.go
Normal file
90
service/scripted_speech_service.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Package service - 预制话术服务
|
||||
// 功能:预制话术的增删改查
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"customer-server/dao"
|
||||
"customer-server/model/dto"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var (
|
||||
ScriptedSpeech = new(scriptedSpeech)
|
||||
)
|
||||
|
||||
type scriptedSpeech struct{}
|
||||
|
||||
// Add 添加预制话术
|
||||
// 参数: ctx - 上下文,req - 添加预制话术请求
|
||||
// 返回: res - 添加成功后的预制话术ID,err - 错误信息
|
||||
// 功能: 创建新的预制话术记录
|
||||
func (s *scriptedSpeech) Add(ctx context.Context, req *dto.AddScriptedSpeechReq) (res *dto.AddScriptedSpeechRes, err error) {
|
||||
// 检查账号是否存在
|
||||
account, err := dao.Account.GetById(ctx, &dto.GetAccountReq{Id: req.AccountId})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if account == nil {
|
||||
err = gerror.New("客服账号不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 插入数据库
|
||||
id, err := dao.ScriptedSpeech.Insert(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.AddScriptedSpeechRes{Id: id}
|
||||
return
|
||||
}
|
||||
|
||||
// Update 更新预制话术
|
||||
// 参数: ctx - 上下文,req - 更新预制话术请求
|
||||
// 返回: err - 错误信息
|
||||
// 功能: 更新预制话术信息
|
||||
func (s *scriptedSpeech) Update(ctx context.Context, req *dto.UpdateScriptedSpeechReq) (err error) {
|
||||
_, err = dao.ScriptedSpeech.Update(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除预制话术
|
||||
// 参数: ctx - 上下文,req - 删除预制话术请求
|
||||
// 返回: err - 错误信息
|
||||
// 功能: 逻辑删除预制话术记录
|
||||
func (s *scriptedSpeech) Delete(ctx context.Context, req *dto.DeleteScriptedSpeechReq) (err error) {
|
||||
_, err = dao.ScriptedSpeech.Delete(ctx, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Get 获取单个预制话术
|
||||
// 参数: ctx - 上下文,req - 获取预制话术请求
|
||||
// 返回: res - 预制话术信息,err - 错误信息
|
||||
// 功能: 根据ID获取单个预制话术详情
|
||||
func (s *scriptedSpeech) Get(ctx context.Context, req *dto.GetScriptedSpeechReq) (res *dto.ScriptedSpeechVO, err error) {
|
||||
r, err := dao.ScriptedSpeech.GetById(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = gconv.Struct(r, &res)
|
||||
return
|
||||
}
|
||||
|
||||
// List 获取预制话术列表
|
||||
// 参数: ctx - 上下文,req - 列表查询请求
|
||||
// 返回: res - 预制话术列表及分页信息,err - 错误信息
|
||||
// 功能: 分页查询预制话术记录
|
||||
func (s *scriptedSpeech) List(ctx context.Context, req *dto.ListScriptedSpeechReq) (res *dto.ListScriptedSpeechRes, err error) {
|
||||
list, total, err := dao.ScriptedSpeech.List(ctx, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = &dto.ListScriptedSpeechRes{
|
||||
Total: total,
|
||||
}
|
||||
err = gconv.Struct(list, &res.List)
|
||||
|
||||
return
|
||||
}
|
||||
95
update.sql
95
update.sql
@@ -1 +1,94 @@
|
||||
-----------张斌2025-06-16 15:00:00--------------
|
||||
-----------张斌2025-06-16 15:00:00--------------
|
||||
|
||||
--------------------pgsql创建customer_server_account表语句---------------------------
|
||||
-- 客服账号表(RAG智能客服)
|
||||
CREATE TABLE IF NOT EXISTS customer_server_account (
|
||||
-- 基础字段(完全对齐项目规范)
|
||||
id BIGINT PRIMARY KEY, -- 主键ID(非自增)
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID int8
|
||||
creator VARCHAR(64) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at timestamp(6),
|
||||
|
||||
-- 业务字段
|
||||
account_name VARCHAR(128) NOT NULL, -- 客服账号名称
|
||||
status SMALLINT NOT NULL DEFAULT 1, -- 状态:1启用/0停用
|
||||
platform VARCHAR(32) NOT NULL, -- 客服平台
|
||||
greeting TEXT DEFAULT '', -- 开场白
|
||||
prompt TEXT[] DEFAULT '{}', -- 提示词数组
|
||||
self_identity TEXT DEFAULT '', -- AI身份描述
|
||||
dataset_ids BIGINT[] DEFAULT '{}', -- 绑定的数据集ID列表
|
||||
document_ids BIGINT[] DEFAULT '{}', -- 绑定的文档ID列表
|
||||
expand_data JSONB DEFAULT '{}'::JSONB -- 扩展数据(JSONB)
|
||||
);
|
||||
|
||||
-- 索引(高频查询)
|
||||
CREATE INDEX idx_csa_tenant_id ON customer_server_account(tenant_id);
|
||||
CREATE INDEX idx_csa_account_name ON customer_server_account(account_name);
|
||||
CREATE INDEX idx_csa_status ON customer_server_account(status);
|
||||
CREATE INDEX idx_csa_platform ON customer_server_account(platform);
|
||||
CREATE INDEX idx_csa_deleted_at ON customer_server_account(deleted_at);
|
||||
|
||||
-- 表和字段注释
|
||||
COMMENT ON TABLE customer_server_account IS '客服账号表(RAG智能客服配置)';
|
||||
COMMENT ON COLUMN customer_server_account.id IS '主键ID(非自增)';
|
||||
COMMENT ON COLUMN customer_server_account.tenant_id IS '租户ID';
|
||||
COMMENT ON COLUMN customer_server_account.creator IS '创建人';
|
||||
COMMENT ON COLUMN customer_server_account.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN customer_server_account.updater IS '更新人';
|
||||
COMMENT ON COLUMN customer_server_account.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN customer_server_account.deleted_at IS '删除时间(软删)';
|
||||
COMMENT ON COLUMN customer_server_account.account_name IS '客服账号名称';
|
||||
COMMENT ON COLUMN customer_server_account.status IS '客服账号状态';
|
||||
COMMENT ON COLUMN customer_server_account.platform IS '客服平台';
|
||||
COMMENT ON COLUMN customer_server_account.greeting IS '开场白';
|
||||
COMMENT ON COLUMN customer_server_account.prompt IS '提示词数组';
|
||||
COMMENT ON COLUMN customer_server_account.self_identity IS 'AI身份描述';
|
||||
COMMENT ON COLUMN customer_server_account.dataset_ids IS '绑定的数据集ID列表';
|
||||
COMMENT ON COLUMN customer_server_account.document_ids IS '绑定的文档ID列表';
|
||||
COMMENT ON COLUMN customer_server_account.expand_data IS '扩展数据(JSONB)';
|
||||
|
||||
--------------------pgsql创建customer_server_account表语句---------------------------
|
||||
|
||||
--------------------pgsql创建customer_server_scripted_speech表语句---------------------------
|
||||
-- 客服话术表(自定义问答话术)
|
||||
CREATE TABLE IF NOT EXISTS customer_server_scripted_speech (
|
||||
-- 基础字段(完全对齐项目规范)
|
||||
id BIGINT PRIMARY KEY, -- 主键ID(非自增)
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID int8
|
||||
creator VARCHAR(64) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at timestamp(6),
|
||||
|
||||
-- 业务字段
|
||||
account_id BIGINT NOT NULL, -- 账号ID
|
||||
dataset_id BIGINT NOT NULL, -- 数据集ID
|
||||
question_content TEXT NOT NULL, -- 问题内容
|
||||
answer_content TEXT NOT NULL -- 回答内容
|
||||
);
|
||||
|
||||
-- 索引(高频查询)
|
||||
CREATE INDEX idx_csss_tenant_id ON customer_server_scripted_speech(tenant_id);
|
||||
CREATE INDEX idx_csss_account_id ON customer_server_scripted_speech(account_id);
|
||||
CREATE INDEX idx_csss_dataset_id ON customer_server_scripted_speech(dataset_id);
|
||||
CREATE INDEX idx_csss_deleted_at ON customer_server_scripted_speech(deleted_at);
|
||||
|
||||
-- 表和字段注释
|
||||
COMMENT ON TABLE customer_server_scripted_speech IS '客服话术表(自定义问答话术)';
|
||||
COMMENT ON COLUMN customer_server_scripted_speech.id IS '主键ID(非自增)';
|
||||
COMMENT ON COLUMN customer_server_scripted_speech.tenant_id IS '租户ID';
|
||||
COMMENT ON COLUMN customer_server_scripted_speech.creator IS '创建人';
|
||||
COMMENT ON COLUMN customer_server_scripted_speech.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN customer_server_scripted_speech.updater IS '更新人';
|
||||
COMMENT ON COLUMN customer_server_scripted_speech.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN customer_server_scripted_speech.deleted_at IS '删除时间(软删)';
|
||||
COMMENT ON COLUMN customer_server_scripted_speech.account_id IS '账号ID';
|
||||
COMMENT ON COLUMN customer_server_scripted_speech.dataset_id IS '数据集ID';
|
||||
COMMENT ON COLUMN customer_server_scripted_speech.question_content IS '问题内容';
|
||||
COMMENT ON COLUMN customer_server_scripted_speech.answer_content IS '回答内容';
|
||||
|
||||
--------------------pgsql创建customer_server_scripted_speech表语句---------------------------
|
||||
@@ -10,12 +10,13 @@ import (
|
||||
"gitea.com/red-future/common/utils"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// GetTenantInfo 获取租户信息
|
||||
// 优先从 token 获取,失败则从请求参数 customerServiceId 查询 customer_service_account 表
|
||||
func GetTenantInfo(ctx context.Context) (user beans.User, err error) {
|
||||
func GetTenantInfo(ctx context.Context) (user *beans.User, err error) {
|
||||
// 1. 优先从 token 获取
|
||||
user, err = utils.GetUserInfo(ctx)
|
||||
if err == nil {
|
||||
@@ -51,7 +52,7 @@ func GetTenantInfo(ctx context.Context) (user beans.User, err error) {
|
||||
cacheKey := fmt.Sprintf("tenant:account:%s", accountName)
|
||||
cached, cacheErr := redis.RedisClient().Get(ctx, cacheKey)
|
||||
if cacheErr == nil && !g.IsEmpty(cached) {
|
||||
user.TenantId = cached.Interface()
|
||||
user.TenantId = gconv.Uint64(cached.Interface())
|
||||
user.UserName = accountName
|
||||
return user, nil
|
||||
}
|
||||
@@ -59,7 +60,7 @@ func GetTenantInfo(ctx context.Context) (user beans.User, err error) {
|
||||
// 4. 缓存未命中,查询 customer_service_account 表
|
||||
filter := bson.M{"accountName": accountName, "isDeleted": false}
|
||||
var account struct {
|
||||
TenantId interface{} `bson:"tenantId"`
|
||||
TenantId uint64 `bson:"tenantId"`
|
||||
}
|
||||
if findErr := mongo.GetDB().Collection("customer_service_account").FindOne(ctx, filter).Decode(&account); findErr != nil {
|
||||
return user, gerror.Newf("通过 accountName 查询租户失败: %v", findErr)
|
||||
|
||||
Reference in New Issue
Block a user