feat: 添加客服账号管理及WebSocket功能

This commit is contained in:
2026-04-03 17:52:09 +08:00
parent b7cce0befa
commit 7416bf8d96
20 changed files with 1065 additions and 74 deletions

View File

@@ -2,16 +2,19 @@ server:
address: ":3000"
name: "customer-server"
workerId: 1
cache:
localTTL: 60
redisTTL: 300
# Database.
database:
default:
- type: "pgsql"
host: "116.204.74.41"
port: "15432"
host: "localhost"
port: "5432"
user: "postgres"
pass: "Bjang09@686^*^"
pass: "123456"
name: "customer_server"
prefix: "customer_server_" # (可选)表名前缀
role: "master" # (可选)数据库主从角色(master/slave)默认为master。如果不使用应用主从机制请不配置或留空即可。
debug: false # (可选)开启调试模式
dryRun: false # (可选)ORM空跑(只读不写)
@@ -25,35 +28,15 @@ database:
updatedAt: "updated_at" # (可选)自动更新时间字段名称
deletedAt: "deleted_at" # (可选)软删除时间字段名称
timeMaintainDisabled: false # (可选)是否完全关闭时间更新特性为true时CreatedAt/UpdatedAt/DeletedAt都将失效
- type: "pgsql"
host: "116.204.74.41"
port: "15432"
user: "postgres"
pass: "Bjang09@686^*^"
name: "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都将失效
tenant-1:
- type: "pgsql"
host: "localhost"
port: "5432"
user: "postgres"
pass: "123456"
name: "tenant"
role: "master"
name: "customer_server"
prefix: "customer_server_" # (可选)表名前缀
debug: true # (可选)开启调试模式
role: "slave" # (可选)数据库主从角色(master/slave)默认为master。如果不使用应用主从机制请不配置或留空即可。
debug: false # (可选)开启调试模式
dryRun: false # (可选)ORM空跑(只读不写)
charset: "utf8" # (可选)数据库编码(如: utf8mb4/utf8/gbk/gb2312)一般设置为utf8mb4。默认为utf8。
timezone: "Asia/Shanghai" # (可选)时区配置,例如:Local

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

View File

@@ -2,12 +2,6 @@ package public
// sql 数据库表名
const (
TableNameAccount = "account"
TableNameDataset = "dataset"
TableNameKeyword = "keyword"
)
// es 索引名称
const (
IndexNameDocumentChunk = "document_chunk" // 文档分块索引
TableNameAccount = "account"
TableNameScriptedSpeech = "scripted_speech"
)

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

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

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

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

9
go.mod
View File

@@ -1,11 +1,12 @@
module customer-server
go 1.25.7
go 1.26.0
replace gitea.com/red-future/common v0.0.6 => ../common
replace gitea.com/red-future/common v0.0.7 => ../common
require (
gitea.com/red-future/common v0.0.6
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,6 +53,7 @@ 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.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect

6
go.sum
View File

@@ -21,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=
@@ -75,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=
@@ -194,6 +198,8 @@ 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.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=

View File

@@ -11,6 +11,7 @@ import (
"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"
@@ -30,6 +31,9 @@ func main() {
// 路由注册common/http init() 已异步启动服务器,这里注册路由不影响)
http.RouteRegister([]interface{}{
controller.Account,
controller.ScriptedSpeech,
controller.AccountWebsocket,
controller.Health,
controller.Archive,
controller.CustomerServiceAccount,

90
model/dto/account_dto.go Normal file
View 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:"客服账号名称"`
}

View 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:"平台"`
}

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

View File

@@ -6,19 +6,65 @@ import (
"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 []string `orm:"dataset_ids" json:"datasetIds" dc:"绑定的数据集ID列表"`
DocumentIds []string `orm:"document_ids" json:"documentIds" dc:"绑定的文档ID列表"`
SpeechcraftIds []string `orm:"speechcraftIds" json:"speechcraftIds" dc:"绑定的话术ID列表"`
AccountName string `orm:"account_name" json:"accountName" dc:"客服账号名称"`
Status account.Status `orm:"status" json:"status" dc:"客服账号状态"`
Greeting string `orm:"greeting" json:"greeting" dc:"开场白"`
Prompt []string `orm:"prompt" json:"prompt" dc:"提示词"`
SelfIdentity string `orm:"self_identity" json:"selfIdentity" dc:"AI身份描述"`
Platform string `orm:"platform" json:"platform" dc:"客服平台"`
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:"小红书AccessToken14天有效期"`
AppId int64 `orm:"app_id" json:"appId" dc:"小红书应用ID"`

View File

@@ -4,6 +4,22 @@ 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"`

View 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 - 添加成功后的客服账号IDerr - 错误信息
// 功能: 创建新的客服账号记录
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
}

View 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时发送ackGo直接返回的不需要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
}

View 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 - 添加成功后的预制话术IDerr - 错误信息
// 功能: 创建新的预制话术记录
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
}

View File

@@ -1,8 +1,8 @@
-----------2025-06-16 15:00:00--------------
--------------------pgsql创建rag_keyword表语句---------------------------
-- 关键词表(文档关键词+权重
CREATE TABLE IF NOT EXISTS rag_keyword (
--------------------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
@@ -13,31 +13,82 @@ CREATE TABLE IF NOT EXISTS rag_keyword (
deleted_at timestamp(6),
-- 业务字段
dataset_id BIGINT NOT NULL, -- 数据集ID
document_id BIGINT NOT NULL, -- 文件ID
word VARCHAR(255) NOT NULL, -- 关键词
weight SMALLINT NOT NULL DEFAULT 0 -- 权重
);
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_keyword_tenant_id ON rag_keyword(tenant_id);
CREATE INDEX idx_keyword_dataset_id ON rag_keyword(dataset_id);
CREATE INDEX idx_keyword_document_id ON rag_keyword(document_id);
CREATE INDEX idx_keyword_word ON rag_keyword(word);
CREATE INDEX idx_keyword_deleted_at ON rag_keyword(deleted_at);
-- 索引(高频查询)
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 rag_keyword IS 'RAG关键词表文档关键词+权重';
COMMENT ON COLUMN rag_keyword.id IS '主键ID非自增';
COMMENT ON COLUMN rag_keyword.tenant_id IS '租户ID';
COMMENT ON COLUMN rag_keyword.creator IS '创建人';
COMMENT ON COLUMN rag_keyword.created_at IS '创建时间';
COMMENT ON COLUMN rag_keyword.updater IS '更新人';
COMMENT ON COLUMN rag_keyword.updated_at IS '更新时间';
COMMENT ON COLUMN rag_keyword.deleted_at IS '删除时间(软删)';
COMMENT ON COLUMN rag_keyword.dataset_id IS '数据集ID';
COMMENT ON COLUMN rag_keyword.document_id IS '文档ID';
COMMENT ON COLUMN rag_keyword.word IS '关键词';
COMMENT ON COLUMN rag_keyword.weight IS '权重';
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创建rag_keyword表语句---------------------------
--------------------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表语句---------------------------