diff --git a/config.yml b/config.yml index 2d83bf6..1f1122c 100644 --- a/config.yml +++ b/config.yml @@ -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 diff --git a/consts/account/platform.go b/consts/account/platform.go new file mode 100644 index 0000000..af266a7 --- /dev/null +++ b/consts/account/platform.go @@ -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} +} diff --git a/consts/public/table_name.go b/consts/public/table_name.go index 08421d5..e5b4019 100644 --- a/consts/public/table_name.go +++ b/consts/public/table_name.go @@ -2,12 +2,6 @@ package public // sql 数据库表名 const ( - TableNameAccount = "account" - TableNameDataset = "dataset" - TableNameKeyword = "keyword" -) - -// es 索引名称 -const ( - IndexNameDocumentChunk = "document_chunk" // 文档分块索引 + TableNameAccount = "account" + TableNameScriptedSpeech = "scripted_speech" ) diff --git a/controller/account_controller.go b/controller/account_controller.go new file mode 100644 index 0000000..f6d7699 --- /dev/null +++ b/controller/account_controller.go @@ -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 +} diff --git a/controller/account_websocket_controller.go b/controller/account_websocket_controller.go new file mode 100644 index 0000000..8264f5f --- /dev/null +++ b/controller/account_websocket_controller.go @@ -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 +} diff --git a/controller/scripted_speech_controller.go b/controller/scripted_speech_controller.go new file mode 100644 index 0000000..7fd287c --- /dev/null +++ b/controller/scripted_speech_controller.go @@ -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 +} diff --git a/dao/account_dao.go b/dao/account_dao.go new file mode 100644 index 0000000..500be27 --- /dev/null +++ b/dao/account_dao.go @@ -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 +} diff --git a/dao/scripted_speech_dao.go b/dao/scripted_speech_dao.go new file mode 100644 index 0000000..f3593dc --- /dev/null +++ b/dao/scripted_speech_dao.go @@ -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 +} diff --git a/go.mod b/go.mod index 2a6e4a6..0dbc70a 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index dc431f3..9840c96 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 2a2735a..97e2db1 100644 --- a/main.go +++ b/main.go @@ -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, diff --git a/model/dto/account_dto.go b/model/dto/account_dto.go new file mode 100644 index 0000000..5a6234d --- /dev/null +++ b/model/dto/account_dto.go @@ -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:"客服账号名称"` +} diff --git a/model/dto/account_websocket_dto.go b/model/dto/account_websocket_dto.go new file mode 100644 index 0000000..572fc10 --- /dev/null +++ b/model/dto/account_websocket_dto.go @@ -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:"平台"` +} diff --git a/model/dto/scripted_speech_dto.go b/model/dto/scripted_speech_dto.go new file mode 100644 index 0000000..beb38c6 --- /dev/null +++ b/model/dto/scripted_speech_dto.go @@ -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"` +} diff --git a/model/entity/account.go b/model/entity/account.go index 05b32bf..a6ef8d8 100644 --- a/model/entity/account.go +++ b/model/entity/account.go @@ -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:"小红书AccessToken(14天有效期)"` AppId int64 `orm:"app_id" json:"appId" dc:"小红书应用ID"` diff --git a/model/entity/scripted_speech.go b/model/entity/scripted_speech.go index 4c801dc..13502af 100644 --- a/model/entity/scripted_speech.go +++ b/model/entity/scripted_speech.go @@ -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"` diff --git a/service/account_service.go b/service/account_service.go new file mode 100644 index 0000000..19201ce --- /dev/null +++ b/service/account_service.go @@ -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 +} diff --git a/service/account_websocket_service.go b/service/account_websocket_service.go new file mode 100644 index 0000000..be23003 --- /dev/null +++ b/service/account_websocket_service.go @@ -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 +} diff --git a/service/scripted_speech_service.go b/service/scripted_speech_service.go new file mode 100644 index 0000000..72477a3 --- /dev/null +++ b/service/scripted_speech_service.go @@ -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 +} diff --git a/update.sql b/update.sql index 2486ee3..6b2a4a8 100644 --- a/update.sql +++ b/update.sql @@ -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表语句--------------------------- \ No newline at end of file +--------------------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表语句--------------------------- \ No newline at end of file