feat: 新增账号编码和HTTP连接功能

This commit is contained in:
2026-04-11 18:22:52 +08:00
parent 2f5c4f7e54
commit f8927afa9c
94 changed files with 1213 additions and 10230 deletions

View File

@@ -28,7 +28,7 @@ func (d *account) Insert(ctx context.Context, req *dto.AddAccountReq) (id int64,
}
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()
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAccount).Data(&req).Where(entity.AccountCol.Id, req.Id).OmitEmpty().Update()
if err != nil {
return
}
@@ -44,7 +44,7 @@ func (d *account) Delete(ctx context.Context, req *dto.DeleteAccountReq) (rows i
}
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()
count, err = gfdb.DB(ctx).Model(ctx, public.TableNameAccount).OmitEmpty().Where(entity.AccountCol.AccountCode, req.AccountCode).Count()
return
}
@@ -64,6 +64,7 @@ func (d *account) List(ctx context.Context, req *dto.ListAccountReq, fields ...s
if !g.IsEmpty(req.Keyword) {
model.WhereLike(entity.AccountCol.AccountName, "%"+req.Keyword+"%")
}
model.Where(entity.AccountCol.AccountCode, req.AccountCode)
model.Where(entity.AccountCol.Status, req.Status)
model.Where(entity.AccountCol.Platform, req.Platform)
model.OrderDesc(entity.AccountCol.CreatedAt)
@@ -79,9 +80,9 @@ func (d *account) List(ctx context.Context, req *dto.ListAccountReq, fields ...s
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()
// GetByAccountCode 根据客服账号编码查询不带租户id
func (d *account) GetByAccountCode(ctx context.Context, req *dto.GetByAccountCodeReq, fields ...string) (res *entity.Account, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAccount).NoTenantId(ctx).Where(entity.AccountCol.AccountCode, req.AccountCode).Fields(fields).One()
if err != nil {
return
}

View File

@@ -0,0 +1,60 @@
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/database/gdb"
"github.com/gogf/gf/v2/util/gconv"
)
var AccountUserDialog = new(accountUserDialog)
type accountUserDialog struct{}
func (d *accountUserDialog) Insert(ctx context.Context, req *dto.AddAccountUserDialogReq) (id int64, err error) {
var e *entity.AccountUserDialog
if err = gconv.Struct(req, &e); err != nil {
return
}
result, err := gfdb.DB(ctx).Model(ctx, public.TableNameAccountUserDialog).Insert(e)
if err != nil {
return
}
return result.LastInsertId()
}
func (d *accountUserDialog) Update(ctx context.Context, req *dto.UpdateAccountUserDialogReq) (rows int64, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAccountUserDialog).Data(&req).Where(entity.AccountUserDialogCol.Id, req.Id).OmitEmpty().
Data(entity.AccountUserDialogCol.DialogCount, &gdb.Counter{
Field: entity.AccountUserDialogCol.DialogCount,
Value: gconv.Float64(req.DialogCount),
}).Update()
if err != nil {
return
}
return r.RowsAffected()
}
func (d *accountUserDialog) Delete(ctx context.Context, req *dto.DeleteAccountUserDialogReq) (rows int64, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAccountUserDialog).Where(entity.AccountUserDialogCol.Id, req.Id).Delete()
if err != nil {
return
}
return r.RowsAffected()
}
func (d *accountUserDialog) Get(ctx context.Context, req *dto.GetAccountUserDialogReq) (res *entity.AccountUserDialog, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameAccountUserDialog).OmitEmpty().
Where(entity.AccountUserDialogCol.UserId, req.UserId).
Where(entity.AccountUserDialogCol.AccountId, req.AccountId).
One()
if err != nil {
return
}
err = gconv.Struct(r, &res)
return
}

View File

@@ -1,158 +0,0 @@
package dao
import (
"context"
"customer-server/model/entity"
"time"
"gitea.com/red-future/common/db/mongo"
"github.com/gogf/gf/v2/frame/g"
"go.mongodb.org/mongo-driver/v2/bson"
)
// archive 归档 DAO
type archive struct{}
// Archive 归档 DAO 单例
var Archive = new(archive)
// CopyToTempByRange 将指定时间范围的数据复制到临时表
// startTime: 开始时间包含endTime: 结束时间(不包含)
func (d *archive) CopyToTempByRange(ctx context.Context, startTime, endTime time.Time) (count int64, err error) {
db := mongo.GetDB()
// 查询指定时间范围的数据
filter := bson.M{
"createdAt": bson.M{
"$gte": startTime,
"$lt": endTime,
},
"isDeleted": false,
}
cursor, err := db.Collection(entity.ConversationCollection).Find(ctx, filter)
if err != nil {
return
}
defer cursor.Close(ctx)
// 批量插入临时表
batchSize := g.Cfg().MustGet(ctx, "archive.mongoBatchSize", 1000).Int()
var docs []interface{}
for cursor.Next(ctx) {
var conv entity.Conversation
if err = cursor.Decode(&conv); err != nil {
return
}
// 转换为临时表结构
temp := entity.ConversationArchiveTemp{
MongoBaseDO: conv.MongoBaseDO,
UserId: conv.UserId,
Platform: conv.Platform,
SessionId: conv.SessionId,
Question: conv.Question,
Answer: conv.Answer,
MessageId: conv.MessageId,
MsgTime: conv.MsgTime,
OriginalId: conv.Id.Hex(), // 保存原始 ID
}
// 清空 ID让 MongoDB 自动生成新 ID
temp.Id = nil
docs = append(docs, temp)
// 批量插入
if len(docs) >= batchSize {
if _, err = db.Collection(entity.ConversationArchiveTempCollection).InsertMany(ctx, docs); err != nil {
return
}
count += int64(len(docs))
docs = docs[:0]
}
}
// 插入剩余数据
if len(docs) > 0 {
if _, err = db.Collection(entity.ConversationArchiveTempCollection).InsertMany(ctx, docs); err != nil {
return
}
count += int64(len(docs))
}
return
}
// DeleteByTempIds 根据临时表中的 originalId 删除原表数据
func (d *archive) DeleteByTempIds(ctx context.Context) (count int64, err error) {
db := mongo.GetDB()
// 从临时表获取所有 originalId
cursor, err := db.Collection(entity.ConversationArchiveTempCollection).Find(ctx, bson.M{})
if err != nil {
return
}
defer cursor.Close(ctx)
var ids []bson.ObjectID
for cursor.Next(ctx) {
var temp entity.ConversationArchiveTemp
if err = cursor.Decode(&temp); err != nil {
return
}
if oid, parseErr := bson.ObjectIDFromHex(temp.OriginalId); parseErr == nil {
ids = append(ids, oid)
}
// 每 1000 条批量删除一次
if len(ids) >= 1000 {
result, delErr := db.Collection(entity.ConversationCollection).DeleteMany(ctx, bson.M{
"_id": bson.M{"$in": ids},
})
if delErr != nil {
err = delErr
return
}
count += result.DeletedCount
ids = ids[:0]
}
}
// 删除剩余数据
if len(ids) > 0 {
result, delErr := db.Collection(entity.ConversationCollection).DeleteMany(ctx, bson.M{
"_id": bson.M{"$in": ids},
})
if delErr != nil {
err = delErr
return
}
count += result.DeletedCount
}
return
}
// GetTempData 获取临时表数据(用于写入 ES
func (d *archive) GetTempData(ctx context.Context) (data []*entity.ConversationArchiveTemp, err error) {
db := mongo.GetDB()
cursor, err := db.Collection(entity.ConversationArchiveTempCollection).Find(ctx, bson.M{})
if err != nil {
return
}
defer cursor.Close(ctx)
err = cursor.All(ctx, &data)
return
}
// DropTempCollection 删除临时表
func (d *archive) DropTempCollection(ctx context.Context) (err error) {
return mongo.GetDB().Collection(entity.ConversationArchiveTempCollection).Drop(ctx)
}
// CountTemp 统计临时表记录数
func (d *archive) CountTemp(ctx context.Context) (count int64, err error) {
return mongo.GetDB().Collection(entity.ConversationArchiveTempCollection).CountDocuments(ctx, bson.M{})
}

View File

@@ -1,140 +0,0 @@
package dao
import (
"context"
"customer-server/model/entity"
"gitea.com/red-future/common/db/mongo"
"gitea.com/red-future/common/redis"
"github.com/gogf/gf/v2/os/gtime"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
var Conversation = new(conversation)
type conversation struct{}
// BatchInsert 批量插入对话记录
func (d *conversation) BatchInsert(ctx context.Context, list []*entity.Conversation) (err error) {
if len(list) == 0 {
return
}
now := gtime.Now().Time
docs := make([]interface{}, 0, len(list))
for _, data := range list {
docs = append(docs, bson.M{
"userId": data.UserId,
"platform": data.Platform,
"sessionId": data.SessionId,
"question": data.Question,
"answer": data.Answer,
"messageId": data.MessageId,
"msgTime": data.MsgTime,
"tenantId": data.TenantId,
"creator": "system",
"createdAt": now,
"updater": "system",
"updatedAt": now,
"isDeleted": false,
})
}
_, err = mongo.GetDB().Collection(entity.ConversationCollection).InsertMany(ctx, docs)
return
}
// UpsertByMessageId 幂等插入对话记录(使用 message_id 做唯一键,防止重复消费)
func (d *conversation) UpsertByMessageId(ctx context.Context, data *entity.Conversation) (inserted bool, err error) {
filter := bson.M{"messageId": data.MessageId}
now := gtime.Now().Time
update := bson.M{
"$setOnInsert": bson.M{
"userId": data.UserId,
"platform": data.Platform,
"sessionId": data.SessionId,
"question": data.Question,
"answer": data.Answer,
"messageId": data.MessageId,
"msgTime": data.MsgTime,
"creator": "system",
"createdAt": now,
"tenantId": data.TenantId, // 使用传入的租户ID
"isDeleted": false,
},
"$set": bson.M{
"updater": "system",
"updatedAt": now,
},
}
opts := options.UpdateOne().SetUpsert(true)
result, err := mongo.GetDB().Collection(entity.ConversationCollection).UpdateOne(ctx, filter, update, opts)
if err != nil {
return
}
// UpsertedCount > 0 表示是新插入,否则是已存在(幂等跳过)
inserted = result.UpsertedCount > 0
return
}
// FindByUserId 根据用户ID查询对话记录
func (d *conversation) FindByUserId(ctx context.Context, userId string, limit int64) (list []*entity.Conversation, err error) {
filter := bson.M{"userId": userId, "isDeleted": false}
opts := options.Find().SetSort(bson.D{{Key: "msgTime", Value: -1}}).SetLimit(limit)
cursor, err := mongo.GetDB().Collection(entity.ConversationCollection).Find(ctx, filter, opts)
if err != nil {
return
}
defer cursor.Close(ctx)
err = cursor.All(ctx, &list)
return
}
// FindBySessionId 根据 Session ID 查询对话记录
func (d *conversation) FindBySessionId(ctx context.Context, sessionId string) (list []*entity.Conversation, err error) {
filter := bson.M{"sessionId": sessionId, "isDeleted": false}
opts := options.Find().SetSort(bson.D{{Key: "msgTime", Value: 1}})
cursor, err := mongo.GetDB().Collection(entity.ConversationCollection).Find(ctx, filter, opts)
if err != nil {
return
}
defer cursor.Close(ctx)
err = cursor.All(ctx, &list)
return
}
// GetRecentHistory 获取用户最近 N 轮历史对话(用于上下文注入)
// 返回 redis.HistoryMessage 切片,按时间正序排列
func (d *conversation) GetRecentHistory(ctx context.Context, userId string, limit int64) (history []redis.HistoryMessage, err error) {
filter := bson.M{"userId": userId, "isDeleted": false}
// 先按时间倒序取最近 N 条,再反转为正序
opts := options.Find().SetSort(bson.D{{Key: "msgTime", Value: -1}}).SetLimit(limit)
cursor, err := mongo.GetDB().Collection(entity.ConversationCollection).Find(ctx, filter, opts)
if err != nil {
return
}
defer cursor.Close(ctx)
var list []*entity.Conversation
if err = cursor.All(ctx, &list); err != nil {
return
}
// 反转为时间正序
history = make([]redis.HistoryMessage, len(list))
for i, conv := range list {
history[len(list)-1-i] = redis.HistoryMessage{
Question: conv.Question,
Answer: conv.Answer,
}
}
return
}

View File

@@ -1,197 +0,0 @@
package dao
import (
"context"
"customer-server/model/dto"
"customer-server/model/entity"
"fmt"
"gitea.com/red-future/common/beans"
"gitea.com/red-future/common/db/mongo"
"gitea.com/red-future/common/redis"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"go.mongodb.org/mongo-driver/v2/bson"
)
var CustomerServiceAccount = new(customerServiceAccount)
type customerServiceAccount struct{}
// FindByAccountName 根据accountName查询
// 使用 MongoDAO不需要token验证
func (d *customerServiceAccount) FindByAccountName(ctx context.Context, accountName string) (account *entity.CustomerServiceAccount, err error) {
filter := bson.M{"accountName": accountName, "isDeleted": false}
var result entity.CustomerServiceAccount
err = MongoDAO.FindOne(ctx, filter, &result, entity.CustomerServiceAccountCollection)
if err != nil {
return nil, err
}
// 如果未找到记录result 是零值
if result.Id.IsZero() {
return nil, nil
}
account = &result
return
}
// Insert 插入客服账号
func (d *customerServiceAccount) Insert(ctx context.Context, data *entity.CustomerServiceAccount) (err error) {
// 统一使用commonmongo.DB().Insert自动清除缓存
// service层已经设置了TenantIdcommonmongo.DB().Insert不会覆盖已有值
ids, err := mongo.DB().Insert(ctx, []interface{}{data}, entity.CustomerServiceAccountCollection)
if err != nil {
return
}
if len(ids) > 0 {
if oid, ok := ids[0].(bson.ObjectID); ok {
data.Id = &oid // 取地址赋值给指针类型
}
}
return
}
// Update 更新客服账号
func (d *customerServiceAccount) Update(ctx context.Context, req *dto.UpdateCustomerServiceAccountReq) (err error) {
objectId, err := bson.ObjectIDFromHex(req.Id)
if err != nil {
return
}
filter := bson.M{"_id": objectId}
// 如果accountName变更需要清理旧的缓存
if !g.IsEmpty(req.AccountName) {
// 先查出旧的accountName
var oldAccount entity.CustomerServiceAccount
if findErr := MongoDAO.FindOne(ctx, filter, &oldAccount, entity.CustomerServiceAccountCollection); findErr == nil {
// 清理旧accountName的缓存
oldCacheKey := fmt.Sprintf("tenant:account:%s", oldAccount.AccountName)
redis.RedisClient().Del(ctx, oldCacheKey)
}
}
updateFields := bson.M{}
if !g.IsEmpty(req.AccountName) {
updateFields["accountName"] = req.AccountName
}
if !g.IsEmpty(req.Platform) {
updateFields["platform"] = req.Platform
}
if req.SelfIdentity != nil {
updateFields["selfIdentity"] = *req.SelfIdentity
}
// 如果有字段需要更新,则执行 MongoDB 更新操作
if len(updateFields) > 0 {
_, err = mongo.DB().Update(ctx, filter, bson.M{"$set": updateFields}, entity.CustomerServiceAccountCollection)
}
return
}
// Delete 软删除客服账号
func (d *customerServiceAccount) Delete(ctx context.Context, id string) (err error) {
objectId, err := bson.ObjectIDFromHex(id)
if err != nil {
return
}
filter := bson.M{"_id": objectId, "isDeleted": false}
// 删除前先查出accountName清理缓存
var account entity.CustomerServiceAccount
if findErr := MongoDAO.FindOne(ctx, filter, &account, entity.CustomerServiceAccountCollection); findErr == nil {
cacheKey := fmt.Sprintf("tenant:account:%s", account.AccountName)
redis.RedisClient().Del(ctx, cacheKey)
}
update := bson.M{"$set": bson.M{"isDeleted": true, "updatedAt": gtime.Now().Time}}
_, err = mongo.DB().Update(ctx, filter, update, entity.CustomerServiceAccountCollection)
if err != nil {
return gerror.Wrap(err, "删除客服账号失败")
}
return
}
// ToggleStatus 切换客服账号状态1 启用0 禁用)
func (d *customerServiceAccount) ToggleStatus(ctx context.Context, req *dto.ToggleCustomerServiceAccountStatusReq) (err error) {
objectId, err := bson.ObjectIDFromHex(req.Id)
if err != nil {
return
}
filter := bson.M{"_id": objectId}
// 先查出当前状态
var account entity.CustomerServiceAccount
if err = MongoDAO.FindOne(ctx, filter, &account, entity.CustomerServiceAccountCollection); err != nil {
return
}
// 计算新的状态true->falsefalse->true切换禁用状态
newIsDisabled := true
if !account.Id.IsZero() && account.IsDisabled {
newIsDisabled = false
}
_, err = mongo.DB().Update(ctx, filter, bson.M{"$set": bson.M{"isDisabled": newIsDisabled}}, entity.CustomerServiceAccountCollection)
return
}
// buildListFilter 构建列表查询的过滤条件
func (d *customerServiceAccount) buildListFilter(req *dto.ListCustomerServiceAccountReq) bson.M {
filter := bson.M{}
if !g.IsEmpty(req.AccountName) {
filter["accountName"] = bson.M{"$regex": req.AccountName, "$options": "i"} // $regex模糊查询忽略大小写
}
if req.IsDisabled != nil {
filter["isDisabled"] = *req.IsDisabled
}
if !g.IsEmpty(req.Platform) {
filter["platform"] = req.Platform
}
return filter
}
// checkTotalCount 检查总数
func (d *customerServiceAccount) checkTotalCount(ctx context.Context, filter bson.M) (total int64, err error) {
total, err = mongo.DB().Count(ctx, filter, entity.CustomerServiceAccountCollection)
return
}
// List 获取客服账号列表(包含所有账号,含已禁用账号)
func (d *customerServiceAccount) List(ctx context.Context, req *dto.ListCustomerServiceAccountReq) (list []*entity.CustomerServiceAccount, total int64, err error) {
// 构建查询过滤条件
filter := d.buildListFilter(req)
// 检查总数
total, err = d.checkTotalCount(ctx, filter)
if err != nil {
return
}
// 分页参数处理
pageNum := req.PageNum
if pageNum <= 0 {
pageNum = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
// 使用统一的mongo.DB().Find方法支持分页和排序
page := &beans.Page{
PageNum: int64(pageNum),
PageSize: int64(pageSize),
}
orderBy := []beans.OrderBy{
{Field: "createdAt", Order: beans.Desc}, // 按创建时间倒序
}
_, err = mongo.DB().Find(ctx, filter, &list, entity.CustomerServiceAccountCollection, page, orderBy)
return
}

View File

@@ -1,167 +0,0 @@
package dao
import (
"context"
"customer-server/model/dto"
"customer-server/model/entity"
"gitea.com/red-future/common/beans"
"gitea.com/red-future/common/db/mongo"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"go.mongodb.org/mongo-driver/v2/bson"
)
var Data = new(data)
type data struct{}
// Insert 插入数据
func (d *data) Insert(ctx context.Context, data *entity.Data) (err error) {
_, err = mongo.DB().Insert(ctx, []interface{}{data}, entity.DataCollection)
return
}
// Update 更新数据
func (d *data) Update(ctx context.Context, req *dto.UpdateDataReq) (err error) {
objectId, err := bson.ObjectIDFromHex(req.Id)
if err != nil {
return
}
filter := bson.M{"_id": objectId}
// 构建动态更新字段
updateFields := bson.M{}
if !g.IsEmpty(req.CustomerId) {
updateFields["customerId"] = req.CustomerId
}
if !g.IsEmpty(req.AccountName) {
updateFields["accountName"] = req.AccountName
}
if req.IsInbound != nil {
updateFields["isInbound"] = *req.IsInbound
}
if req.IsActive != nil {
updateFields["isActive"] = *req.IsActive
}
if req.IsServed != nil {
updateFields["isServed"] = *req.IsServed
}
if req.HasSentContactCard != nil {
updateFields["hasSentContactCard"] = *req.HasSentContactCard
}
if req.HasSentNameCard != nil {
updateFields["hasSentNameCard"] = *req.HasSentNameCard
}
if req.HasLeftContactInfo != nil {
updateFields["hasLeftContactInfo"] = *req.HasLeftContactInfo
}
if len(updateFields) > 0 {
_, err = mongo.DB().Update(ctx, filter, bson.M{"$set": updateFields}, entity.DataCollection)
}
return
}
// // Delete 删除数据
// func (d *data) Delete(ctx context.Context, req *dto.DeleteDataReq) (err error) {
// objectId, err := bson.ObjectIDFromHex(req.Id)
// if err != nil {
// return
// }
// filter := bson.M{"_id": objectId}
// _, err = mongo.DB().Delete(ctx, filter, d.collection)
// return
// }
// buildListFilter 构建列表查询的过滤条件
func (d *data) buildListFilter(req *dto.ListDataReq) bson.M {
filter := bson.M{}
if !g.IsEmpty(req.CustomerId) {
filter["customerId"] = req.CustomerId
}
if !g.IsEmpty(req.AccountName) {
filter["accountName"] = *req.AccountName
}
// 处理时间范围筛选
if !g.IsEmpty(req.StartDate) || !g.IsEmpty(req.EndDate) {
timeFilter := bson.M{}
// 开始日期:大于等于当天 00:00:00时间戳秒
if !g.IsEmpty(req.StartDate) {
// 将日期字符串转换为时间戳(秒)
startTime, err := parseDate(req.StartDate)
if err == nil {
timeFilter["$gte"] = startTime
}
}
// 结束日期:小于等于当天 23:59:59时间戳秒
if !g.IsEmpty(req.EndDate) {
// 将日期字符串转换为时间戳(秒)+ 86399一天的最后一秒
endTime, err := parseDate(req.EndDate)
if err == nil {
timeFilter["$lte"] = endTime + 86399 // 加上一天的秒数 - 1
}
}
if len(timeFilter) > 0 {
filter["sessionStartTime"] = timeFilter
}
}
return filter
}
// checkTotalCount 检查总数
func (d *data) checkTotalCount(ctx context.Context, filter bson.M) (total int64, err error) {
total, err = mongo.DB().Count(ctx, filter, entity.DataCollection)
return
}
// List 获取数据列表
func (d *data) List(ctx context.Context, req *dto.ListDataReq) (list []*entity.Data, total int64, err error) {
// 构建查询过滤条件
filter := d.buildListFilter(req)
// 检查总数
total, err = d.checkTotalCount(ctx, filter)
if err != nil {
return
}
// 分页参数处理
pageNum := req.PageNum
if pageNum <= 0 {
pageNum = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
// 使用统一的mongo.DB().Find方法支持分页和排序
page := &beans.Page{
PageNum: int64(pageNum),
PageSize: int64(pageSize),
}
orderBy := []beans.OrderBy{
{Field: "sessionStartTime", Order: beans.Desc}, // 按会话开始时间倒序
}
_, err = mongo.DB().Find(ctx, filter, &list, entity.DataCollection, page, orderBy)
return
}
// parseDate 将日期字符串YYYY-MM-DD转换为时间戳
func parseDate(dateStr string) (int64, error) {
// 使用 gtime 解析日期字符串
t := gtime.NewFromStr(dateStr)
if t == nil {
return 0, gerror.New("日期格式错误")
}
// 返回时间戳(秒)
return t.Timestamp(), nil
}

View File

@@ -1,199 +0,0 @@
package dao
import (
"context"
"customer-server/model/dto"
"customer-server/model/entity"
"gitea.com/red-future/common/beans"
"gitea.com/red-future/common/db/mongo"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"go.mongodb.org/mongo-driver/v2/bson"
)
// dataStatistics DAO 单例
var DataStatistics = new(dataStatistics)
type dataStatistics struct{}
// Insert 插入数据统计
func (d *dataStatistics) Insert(ctx context.Context, data *entity.DataStatistics) (err error) {
// 如果 ID 为空,生成一个新的 ObjectID
if data.Id == nil || data.Id.IsZero() {
newId := bson.NewObjectID()
data.Id = &newId // 取地址赋值给指针类型
}
// 使用 common/db/mongo.DB().Insert自动添加 tenantId、creator、updater 等字段
// 确保查询时能通过 tenantId 正确过滤数据
_, err = mongo.DB().Insert(ctx, []interface{}{data}, entity.DataStatisticsCollection)
return
}
// Update 更新数据统计
func (d *dataStatistics) Update(ctx context.Context, req *dto.UpdateDataStatisticsReq) (err error) {
objectId, err := bson.ObjectIDFromHex(req.Id)
if err != nil {
return
}
filter := bson.M{"_id": objectId}
updateFields := bson.M{}
// 使用 gconv 和 gtime 转换日期
if !g.IsEmpty(req.Date) {
if dateTime := gtime.NewFromStr(req.Date); dateTime != nil {
updateFields["date"] = dateTime.Time
}
}
if !g.IsEmpty(req.AccountName) {
updateFields["accountName"] = req.AccountName
}
if !g.IsEmpty(req.CustomerServiceName) {
updateFields["customerServiceName"] = req.CustomerServiceName
}
if !g.IsEmpty(req.CustomerServicePlatform) {
updateFields["customerServicePlatform"] = req.CustomerServicePlatform
}
if req.InboundCount != nil {
updateFields["inboundCount"] = *req.InboundCount
}
if req.ActiveCount != nil {
updateFields["activeCount"] = *req.ActiveCount
}
if req.ServedCount != nil {
updateFields["servedCount"] = *req.ServedCount
}
if req.ContactCardSentCount != nil {
updateFields["contactCardSentCount"] = *req.ContactCardSentCount
}
if req.NameCardSentCount != nil {
updateFields["nameCardSentCount"] = *req.NameCardSentCount
}
if req.LeftContactInfoCount != nil {
updateFields["leftContactInfoCount"] = *req.LeftContactInfoCount
}
if req.ResponseRate30s != nil {
updateFields["responseRate30s"] = *req.ResponseRate30s
}
if req.ResponseRate60s != nil {
updateFields["responseRate60s"] = *req.ResponseRate60s
}
if req.ResponseRate360s != nil {
updateFields["responseRate360s"] = *req.ResponseRate360s
}
if len(updateFields) > 0 {
_, err = mongo.DB().Update(ctx, filter, bson.M{"$set": updateFields}, entity.DataStatisticsCollection)
}
return
}
// buildListFilter 构建列表查询的过滤条件
func (d *dataStatistics) buildListFilter(req *dto.ListDataStatisticsReq) bson.M {
filter := bson.M{}
// 客服平台筛选
if !g.IsEmpty(req.CustomerServicePlatform) {
filter["customerServicePlatform"] = req.CustomerServicePlatform
}
// 日期范围筛选:支持单独传 StartDate 或 EndDate或同时传两者
// 前端传入字符串格式YYYY-MM-DD需要转换为 time.Time 进行比较
if !g.IsEmpty(req.StartDate) || !g.IsEmpty(req.EndDate) {
dateFilter := bson.M{}
if !g.IsEmpty(req.StartDate) {
// 使用 gtime 转换,设置为当天 00:00:00
if startTime := gtime.NewFromStr(req.StartDate); startTime != nil {
dateFilter["$gte"] = startTime.Time
}
}
if !g.IsEmpty(req.EndDate) {
// 使用 gtime 转换,设置为当天 23:59:59
if endTime := gtime.NewFromStr(req.EndDate + " 23:59:59"); endTime != nil {
dateFilter["$lte"] = endTime.Time
}
}
filter["date"] = dateFilter
}
return filter
}
// checkTotalCount 检查总数
func (d *dataStatistics) checkTotalCount(ctx context.Context, filter bson.M) (total int64, err error) {
total, err = mongo.DB().Count(ctx, filter, entity.DataStatisticsCollection)
return
}
// List 获取数据统计列表
func (d *dataStatistics) List(ctx context.Context, req *dto.ListDataStatisticsReq) (list []*entity.DataStatistics, total int64, err error) {
// 构建查询过滤条件
filter := d.buildListFilter(req)
// 检查总数
total, err = d.checkTotalCount(ctx, filter)
if err != nil {
return
}
pageNum := req.PageNum
if pageNum <= 0 {
pageNum = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
// 使用统一的mongo.DB().Find方法支持分页和排序
page := &beans.Page{
PageNum: int64(pageNum),
PageSize: int64(pageSize),
}
orderBy := []beans.OrderBy{
{Field: "date", Order: beans.Desc}, // 按日期倒序
}
_, err = mongo.DB().Find(ctx, filter, &list, entity.DataStatisticsCollection, page, orderBy)
return
}
// FindAllForExport 查询所有符合条件的数据统计(用于导出,不分页,需要租户过滤)
func (d *dataStatistics) FindAllForExport(ctx context.Context, req *dto.ExportDataStatisticsReq) (list []*entity.DataStatistics, err error) {
// 构建查询过滤条件(复用 buildListFilter 逻辑)
filter := bson.M{}
// 客服平台筛选
if !g.IsEmpty(req.CustomerServicePlatform) {
filter["customerServicePlatform"] = req.CustomerServicePlatform
}
// 日期范围筛选
if !g.IsEmpty(req.StartDate) || !g.IsEmpty(req.EndDate) {
dateFilter := bson.M{}
if !g.IsEmpty(req.StartDate) {
if startTime := gtime.NewFromStr(req.StartDate); startTime != nil {
dateFilter["$gte"] = startTime.Time
}
}
if !g.IsEmpty(req.EndDate) {
if endTime := gtime.NewFromStr(req.EndDate + " 23:59:59"); endTime != nil {
dateFilter["$lte"] = endTime.Time
}
}
filter["date"] = dateFilter
}
// 使用 mongo.DB().Find 查询,自动添加 tenantId 过滤,确保租户数据隔离
// 不分页设置一个足够大的PageSize
page := &beans.Page{
PageNum: 1,
PageSize: 100000, // 导出场景设置足够大的PageSize
}
orderBy := []beans.OrderBy{
{Field: "date", Order: beans.Desc}, // 按日期倒序
}
_, err = mongo.DB().Find(ctx, filter, &list, entity.DataStatisticsCollection, page, orderBy)
return list, err
}

View File

@@ -1,75 +0,0 @@
package dao
import (
"context"
commonMongo "gitea.com/red-future/common/db/mongo"
"go.mongodb.org/mongo-driver/v2/bson"
)
// MongoDAO MongoDB原生查询不需要token验证
var MongoDAO = new(mongoDAO)
type mongoDAO struct{}
// FindOne 原生查询单条记录不需要token验证
// 未找到记录时返回 nil error调用方需检查 result 是否为零值
func (m *mongoDAO) FindOne(ctx context.Context, filter bson.M, result interface{}, collectionName string) error {
db := commonMongo.GetDB()
collection := db.Collection(collectionName)
err := collection.FindOne(ctx, filter).Decode(result)
if err != nil {
if err.Error() == "mongo: no documents in result" {
return nil // 未找到记录返回nil而不是错误
}
return err
}
return nil
}
// InsertOne 原生插入单条记录不需要token验证
func (m *mongoDAO) InsertOne(ctx context.Context, document interface{}, collectionName string) (interface{}, error) {
db := commonMongo.GetDB()
collection := db.Collection(collectionName)
result, err := collection.InsertOne(ctx, document)
if err != nil {
return nil, err
}
return result.InsertedID, nil
}
// UpdateOne 原生更新单条记录不需要token验证
// 返回 matchedCount匹配到的记录数和 modifiedCount实际修改的记录数
func (m *mongoDAO) UpdateOne(ctx context.Context, filter bson.M, update bson.M, collectionName string) (matchedCount int64, modifiedCount int64, err error) {
db := commonMongo.GetDB()
collection := db.Collection(collectionName)
result, err := collection.UpdateOne(ctx, filter, update)
if err != nil {
return 0, 0, err
}
return result.MatchedCount, result.ModifiedCount, nil
}
// UpdateMany 原生批量更新记录不需要token验证
func (m *mongoDAO) UpdateMany(ctx context.Context, filter bson.M, update bson.M, collectionName string) (matchedCount int64, modifiedCount int64, err error) {
db := commonMongo.GetDB()
collection := db.Collection(collectionName)
result, err := collection.UpdateMany(ctx, filter, update)
if err != nil {
return 0, 0, err
}
return result.MatchedCount, result.ModifiedCount, nil
}
// Delete 原生删除记录不需要token验证用于回滚操作
func (m *mongoDAO) Delete(ctx context.Context, filter bson.M, collectionName string) error {
db := commonMongo.GetDB()
collection := db.Collection(collectionName)
_, err := collection.DeleteOne(ctx, filter)
return err
}

View File

@@ -1,185 +0,0 @@
package dao
import (
"context"
"customer-server/model/dto"
"customer-server/model/entity"
"gitea.com/red-future/common/beans"
"gitea.com/red-future/common/db/mongo"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"go.mongodb.org/mongo-driver/v2/bson"
)
var Product = new(product)
type product struct{}
// Insert 插入产品
// 注意mongo.DB().Insert不会自动将生成的ID回写到原始对象
// 必须手动从返回的InsertedIDs中提取并赋值给data.Id否则后续访问data.Id会触发空指针异常
func (d *product) Insert(ctx context.Context, data *entity.Product) (id bson.ObjectID, err error) {
ids, err := mongo.DB().Insert(ctx, []interface{}{data}, entity.ProductCollection)
if err != nil {
return
}
// 从返回的ID列表中提取ObjectID并回写到data.Id
if len(ids) > 0 {
if oid, ok := ids[0].(bson.ObjectID); ok {
id = oid
data.Id = &oid // 回写ID到原始对象防止后续访问时空指针异常
}
}
return
}
// Update 更新产品
func (d *product) Update(ctx context.Context, req *dto.UpdateProductReq) (err error) {
objectId, err := bson.ObjectIDFromHex(req.Id)
if err != nil {
return
}
filter := bson.M{"_id": objectId, "isDeleted": false}
updateFields := bson.M{}
if !g.IsEmpty(req.Name) {
updateFields["name"] = req.Name
}
if !g.IsEmpty(req.Description) {
updateFields["description"] = req.Description
}
// 自动更新时间为当前时间
updateFields["updatedAt"] = gtime.Now().Time
if len(updateFields) > 0 {
_, err = mongo.DB().Update(ctx, filter, bson.M{"$set": updateFields}, entity.ProductCollection)
}
return
}
// Delete 软删除产品(设置 IsDeleted=true
func (d *product) Delete(ctx context.Context, req *dto.DeleteProductReq) (err error) {
objectId, err := bson.ObjectIDFromHex(req.Id)
if err != nil {
return
}
filter := bson.M{"_id": objectId, "isDeleted": false}
update := bson.M{"$set": bson.M{"isDeleted": true, "updatedAt": gtime.Now().Time}}
_, err = mongo.DB().Update(ctx, filter, update, entity.ProductCollection)
if err != nil {
return gerror.Wrap(err, "删除产品失败")
}
return
}
// buildListFilter 构建列表查询的过滤条件
func (d *product) buildListFilter(req *dto.ListProductReq) bson.M {
filter := bson.M{"isDeleted": false}
if !g.IsEmpty(req.Name) {
filter["name"] = bson.M{"$regex": req.Name}
}
return filter
}
// checkTotalCount 检查总数
func (d *product) checkTotalCount(ctx context.Context, filter bson.M) (total int64, err error) {
total, err = mongo.DB().Count(ctx, filter, entity.ProductCollection)
return
}
// FindByName 根据名称查询产品(用于去重检查)
func (d *product) FindByName(ctx context.Context, name string) (product *entity.Product, err error) {
filter := bson.M{
"name": name,
"isDeleted": false,
}
err = mongo.DB().FindOne(ctx, filter, &product, entity.ProductCollection)
if err != nil {
if err.Error() == "mongo: no documents in result" {
return nil, nil
}
return nil, err
}
return
}
// List 获取产品列表(排除已删除)
func (d *product) List(ctx context.Context, req *dto.ListProductReq) (list []*entity.Product, total int64, err error) {
// 构建查询过滤条件
filter := d.buildListFilter(req)
// 检查总数
total, err = d.checkTotalCount(ctx, filter)
if err != nil {
return
}
// 分页参数处理
pageNum := req.PageNum
if pageNum <= 0 {
pageNum = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
// 使用统一的mongo.DB().Find方法支持分页和排序
page := &beans.Page{
PageNum: int64(pageNum),
PageSize: int64(pageSize),
}
orderBy := []beans.OrderBy{
{Field: "createdAt", Order: beans.Desc}, // 按创建时间倒序
}
_, err = mongo.DB().Find(ctx, filter, &list, entity.ProductCollection, page, orderBy)
return
}
// GetById 根据ID获取产品
func (d *product) GetById(ctx context.Context, id string) (product *entity.Product, err error) {
objectId, err := bson.ObjectIDFromHex(id)
if err != nil {
return
}
filter := bson.M{"_id": objectId, "isDeleted": false}
err = mongo.DB().FindOne(ctx, filter, &product, entity.ProductCollection)
return
}
// FindAllForExport 查询所有产品用于导出(不分页)
func (d *product) FindAllForExport(ctx context.Context, name string) (list []*entity.Product, err error) {
filter := bson.M{}
if !g.IsEmpty(name) {
filter["name"] = bson.M{"$regex": name, "$options": "i"} // 模糊查询,忽略大小写
}
// 使用 mongo.DB().Find会自动过滤租户和已删除数据
// 导出场景不分页设置足够大的PageSize
page := &beans.Page{
PageNum: 1,
PageSize: 100000, // 导出场景设置足够大的PageSize
}
orderBy := []beans.OrderBy{
{Field: "createdAt", Order: beans.Desc}, // 按创建时间倒序
}
_, err = mongo.DB().Find(ctx, filter, &list, entity.ProductCollection, page, orderBy)
return
}
// UpdateEntity 更新产品实体(用于绑定/解绑/同步等场景)
func (d *product) UpdateEntity(ctx context.Context, product *entity.Product) (err error) {
filter := bson.M{"_id": product.Id, "isDeleted": false}
// 将实体转换为bson.M
updateDoc := bson.M{}
data, _ := bson.Marshal(product)
bson.Unmarshal(data, &updateDoc)
_, err = mongo.DB().Update(ctx, filter, bson.M{"$set": updateDoc}, entity.ProductCollection)
return
}

View File

@@ -1,135 +0,0 @@
package dao
import (
"context"
"customer-server/model/entity"
"gitea.com/red-future/common/db/mongo"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/util/gconv"
"go.mongodb.org/mongo-driver/v2/bson"
)
var RAGFlowConfig = new(ragflowConfig)
type ragflowConfig struct{}
// FindByAccountName 根据客服账号名称查询配置带租户隔离兼容tenantId类型不一致
func (d *ragflowConfig) FindByAccountName(ctx context.Context, accountName string) (*entity.RAGFlowConfig, error) {
// 先查询客服账号获取tenantId
account, err := CustomerServiceAccount.FindByAccountName(ctx, accountName)
if err != nil {
return nil, err
}
if account == nil {
return nil, nil
}
// 使用accountName + tenantId查询RAGFlow配置租户隔离
// 先尝试原始类型查询
filter := bson.M{"accountName": accountName, "tenantId": account.TenantId, "isDeleted": false}
var config entity.RAGFlowConfig
err = MongoDAO.FindOne(ctx, filter, &config, entity.RAGFlowConfigCollection)
// 如果未找到且tenantId可以转为string尝试用string查询兼容性处理
if (err != nil || config.Id == nil || config.Id.IsZero()) && account.TenantId != nil {
tenantIdStr := gconv.String(account.TenantId)
if tenantIdStr != "" {
filter = bson.M{"accountName": accountName, "tenantId": tenantIdStr, "isDeleted": false}
err = MongoDAO.FindOne(ctx, filter, &config, entity.RAGFlowConfigCollection)
if err != nil {
return nil, err
}
}
}
if config.Id.IsZero() {
return nil, nil
}
return &config, nil
}
// FindDatasetIdByTenant 根据租户ID查询知识库ID从任意一条RAGFlowConfig记录中获取
func (d *ragflowConfig) FindDatasetIdByTenant(ctx context.Context, tenantId string) (datasetId string, err error) {
// 先尝试字符串查询
filter := bson.M{"tenantId": tenantId, "isDeleted": false}
var config entity.RAGFlowConfig
err = MongoDAO.FindOne(ctx, filter, &config, entity.RAGFlowConfigCollection)
// 如果未找到且tenantId可以转为数字尝试用数字查询兼容MongoDB中可能存储为int的情况
if err != nil || config.Id.IsZero() {
tenantIdInt := gconv.Int(tenantId)
if tenantIdInt > 0 {
filter = bson.M{"tenantId": tenantIdInt, "isDeleted": false}
err = MongoDAO.FindOne(ctx, filter, &config, entity.RAGFlowConfigCollection)
if err != nil {
return "", err
}
}
}
if config.Id.IsZero() {
return "", nil // 未找到记录
}
return config.DatasetId, nil
}
// Insert 插入配置
func (d *ragflowConfig) Insert(ctx context.Context, config *entity.RAGFlowConfig) error {
_, err := mongo.DB().Insert(ctx, []interface{}{config}, entity.RAGFlowConfigCollection)
return err
}
// UpdateEntity 更新配置避免双重token验证冲突
func (d *ragflowConfig) UpdateEntity(ctx context.Context, config *entity.RAGFlowConfig) error {
filter := bson.M{"_id": config.Id, "isDeleted": false}
// 将实体转换为bson.M
updateDoc := bson.M{}
data, _ := bson.Marshal(config)
bson.Unmarshal(data, &updateDoc)
update := bson.M{"$set": updateDoc}
// 使用MongoDAO.UpdateOne不需要token验证
_, _, err := MongoDAO.UpdateOne(ctx, filter, update, entity.RAGFlowConfigCollection)
if err != nil {
return gerror.Wrap(err, "更新RAGFlow配置失败")
}
return nil
}
// UpdateDocumentIds 更新文档ID列表避免双重token验证冲突
func (d *ragflowConfig) UpdateDocumentIds(ctx context.Context, accountName string, documentIds []string) error {
filter := bson.M{"accountName": accountName, "isDeleted": false}
update := bson.M{"$set": bson.M{"documentIds": documentIds}}
// 使用MongoDAO.UpdateOne不需要token验证
_, _, err := MongoDAO.UpdateOne(ctx, filter, update, entity.RAGFlowConfigCollection)
return err
}
// UpdateDatasetIdByTenant 批量更新租户的所有datasetId记录兼容tenantId类型不一致
func (d *ragflowConfig) UpdateDatasetIdByTenant(ctx context.Context, tenantId, newDatasetId string) error {
// 先尝试字符串查询
filter := bson.M{"tenantId": tenantId, "isDeleted": false}
update := bson.M{"$set": bson.M{"datasetId": newDatasetId}}
matchedCount, _, err := MongoDAO.UpdateMany(ctx, filter, update, entity.RAGFlowConfigCollection)
// 如果未匹配到且tenantId可以转为数字尝试用数字查询兼容MongoDB中可能存储为int的情况
if (err != nil || matchedCount == 0) && gconv.Int(tenantId) > 0 {
filter = bson.M{"tenantId": gconv.Int(tenantId), "isDeleted": false}
matchedCount, _, err = MongoDAO.UpdateMany(ctx, filter, update, entity.RAGFlowConfigCollection)
if err != nil {
return gerror.Wrap(err, "批量更新datasetId失败数字类型尝试")
}
}
if err != nil {
return gerror.Wrap(err, "批量更新datasetId失败")
}
if matchedCount == 0 {
return gerror.Newf("未找到租户%s的记录", tenantId)
}
return nil
}

View File

@@ -27,7 +27,7 @@ func (d *scriptedSpeech) Insert(ctx context.Context, req *dto.AddScriptedSpeechR
}
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()
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameScriptedSpeech).Data(&req).Where(entity.ScriptedSpeechCol.Id, req.Id).OmitEmpty().Update()
if err != nil {
return
}
@@ -42,6 +42,14 @@ func (d *scriptedSpeech) Delete(ctx context.Context, req *dto.DeleteScriptedSpee
return r.RowsAffected()
}
func (d *scriptedSpeech) Count(ctx context.Context, req *dto.ListScriptedSpeechReq) (count int, err error) {
count, err = gfdb.DB(ctx).Model(ctx, public.TableNameScriptedSpeech).OmitEmpty().
Where(entity.ScriptedSpeechCol.DatasetId, req.DatasetId).
Where(entity.ScriptedSpeechCol.SceneType, req.SceneType).
Count()
return
}
// 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()
@@ -52,11 +60,24 @@ func (d *scriptedSpeech) GetById(ctx context.Context, req *dto.GetScriptedSpeech
return
}
// GetByDatasetIdAndSceneType 根据数据集ID和场景类型查询预制话术
func (d *scriptedSpeech) GetByDatasetIdAndSceneType(ctx context.Context, req *dto.ListScriptedSpeechReq, fields ...string) (res *entity.ScriptedSpeech, err error) {
r, err := gfdb.DB(ctx).Model(ctx, public.TableNameScriptedSpeech).Fields(fields).
Where(entity.ScriptedSpeechCol.DatasetId, req.DatasetId).
Where(entity.ScriptedSpeechCol.SceneType, req.SceneType).
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.Where(entity.ScriptedSpeechCol.SceneType, req.SceneType)
model.OrderDesc(entity.ScriptedSpeechCol.CreatedAt)
if req.Page != nil {
model.Page(int(req.Page.PageNum), int(req.Page.PageSize))

View File

@@ -1,99 +0,0 @@
package dao
import (
"context"
"customer-server/model/entity"
"gitea.com/red-future/common/db/mongo"
"github.com/gogf/gf/v2/os/gtime"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
var Session = new(session)
type session struct{}
// Upsert 更新或插入会话(根据 userId + sessionId
// 注意:消费者调用,无 HTTP 上下文,直接使用原生 MongoDB 操作
func (d *session) Upsert(ctx context.Context, data *entity.Session) (err error) {
filter := bson.M{
"userId": data.UserId,
"sessionId": data.SessionId,
"isDeleted": false,
}
now := gtime.Now().Time
update := bson.M{
"$set": bson.M{
"platform": data.Platform,
"status": data.Status,
"lastActiveAt": data.LastActiveAt,
"updater": "system",
"updatedAt": now,
},
"$inc": bson.M{
"messageCount": 1,
},
"$setOnInsert": bson.M{
"creator": "system",
"createdAt": now,
"isDeleted": false,
},
}
opts := options.UpdateOne().SetUpsert(true)
_, err = mongo.GetDB().Collection(entity.SessionCollection).UpdateOne(ctx, filter, update, opts)
return
}
// Archive 归档会话
// 注意:消费者调用,无 HTTP 上下文,直接使用原生 MongoDB 操作
func (d *session) Archive(ctx context.Context, userId, sessionId string) (err error) {
filter := bson.M{
"userId": userId,
"sessionId": sessionId,
"isDeleted": false,
}
now := gtime.Now().Time
update := bson.M{
"$set": bson.M{
"status": entity.SessionStatusArchived,
"archivedAt": now,
"updater": "system",
"updatedAt": now,
},
}
_, err = mongo.GetDB().Collection(entity.SessionCollection).UpdateOne(ctx, filter, update)
return
}
// FindByUserId 根据用户ID查询会话列表
func (d *session) FindByUserId(ctx context.Context, userId string, limit int64) (list []*entity.Session, err error) {
filter := bson.M{"userId": userId, "isDeleted": false}
opts := options.Find().SetSort(bson.D{{Key: "lastActiveAt", Value: -1}}).SetLimit(limit)
cursor, err := mongo.GetDB().Collection(entity.SessionCollection).Find(ctx, filter, opts)
if err != nil {
return
}
defer cursor.Close(ctx)
err = cursor.All(ctx, &list)
return
}
// FindActiveByUserId 查询用户活跃会话
func (d *session) FindActiveByUserId(ctx context.Context, userId string) (data *entity.Session, err error) {
filter := bson.M{
"userId": userId,
"status": entity.SessionStatusActive,
"isDeleted": false,
}
err = mongo.GetDB().Collection(entity.SessionCollection).FindOne(ctx, filter).Decode(&data)
return
}

View File

@@ -1,301 +0,0 @@
package dao
import (
"context"
"customer-server/model/dto"
"customer-server/model/entity"
"strings"
"gitea.com/red-future/common/beans"
"gitea.com/red-future/common/db/mongo"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/grand"
"go.mongodb.org/mongo-driver/v2/bson"
)
var Speechcraft = new(speechcraft)
type speechcraft struct{}
// Insert 插入话术
func (d *speechcraft) Insert(ctx context.Context, data *entity.Speechcraft) (id bson.ObjectID, err error) {
// 统一使用mongo.DB().Insert自动清除缓存
// service层已经设置了TenantIdmongo.DB().Insert不会覆盖已有值
ids, err := mongo.DB().Insert(ctx, []interface{}{data}, entity.SpeechcraftCollection)
if err != nil {
return
}
if len(ids) > 0 {
if oid, ok := ids[0].(bson.ObjectID); ok {
id = oid
data.Id = &oid // 取地址赋值给指针类型
}
}
return
}
// Update 更新话术
func (d *speechcraft) Update(ctx context.Context, req *dto.UpdateSpeechcraftReq) (err error) {
objectId, err := bson.ObjectIDFromHex(req.Id)
if err != nil {
return
}
updateFields := bson.M{}
if !g.IsEmpty(req.Tag) {
updateFields["tag"] = req.Tag
}
if !g.IsEmpty(req.Content) {
updateFields["content"] = req.Content
}
// 状态机字段
if req.Stage != nil {
updateFields["stage"] = *req.Stage
}
if req.Status != nil {
updateFields["status"] = *req.Status
}
if req.Keywords != nil {
updateFields["keywords"] = req.Keywords
}
if req.NextStage != nil {
updateFields["nextStage"] = *req.NextStage
}
if req.Platform != nil {
updateFields["platform"] = *req.Platform
}
if len(updateFields) > 0 {
_, err = mongo.DB().Update(ctx, bson.M{"_id": objectId, "isDeleted": false}, bson.M{"$set": updateFields}, entity.SpeechcraftCollection)
}
return
}
// Delete 软删除话术
func (d *speechcraft) Delete(ctx context.Context, req *dto.DeleteSpeechcraftReq) (err error) {
objectId, err := bson.ObjectIDFromHex(req.Id)
if err != nil {
return
}
filter := bson.M{"_id": objectId, "isDeleted": false}
update := bson.M{"$set": bson.M{"isDeleted": true, "updatedAt": gtime.Now().Time}}
_, err = mongo.DB().Update(ctx, filter, update, entity.SpeechcraftCollection)
if err != nil {
return gerror.Wrap(err, "删除话术失败")
}
return
}
// buildListFilter 构建列表查询的过滤条件
func (d *speechcraft) buildListFilter(req *dto.ListSpeechcraftReq) bson.M {
filter := bson.M{"isDeleted": false}
if !g.IsEmpty(req.Tag) {
filter["tag"] = bson.M{"$regex": req.Tag}
}
if !g.IsEmpty(req.Content) {
filter["content"] = bson.M{"$regex": req.Content}
}
if req.Stage != nil {
filter["stage"] = *req.Stage
}
if !g.IsEmpty(req.Platform) {
filter["platform"] = req.Platform
}
return filter
}
// checkTotalCount 检查总数
func (d *speechcraft) checkTotalCount(ctx context.Context, filter bson.M) (total int64, err error) {
total, err = mongo.DB().Count(ctx, filter, entity.SpeechcraftCollection)
return
}
// List 获取话术列表(排除已删除)
func (d *speechcraft) List(ctx context.Context, req *dto.ListSpeechcraftReq) (list []*entity.Speechcraft, total int64, err error) {
// 构建查询过滤条件
filter := d.buildListFilter(req)
// 检查总数
total, err = d.checkTotalCount(ctx, filter)
if err != nil {
return
}
// 分页参数处理
pageNum := req.PageNum
if pageNum <= 0 {
pageNum = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
// 使用统一的mongo.DB().Find方法支持分页和排序
page := &beans.Page{
PageNum: int64(pageNum),
PageSize: int64(pageSize),
}
orderBy := []beans.OrderBy{
{Field: "createdAt", Order: beans.Desc}, // 按创建时间倒序
}
_, err = mongo.DB().Find(ctx, filter, &list, entity.SpeechcraftCollection, page, orderBy)
return
}
// MatchByStage 根据阶段和用户输入匹配话术
// 匹配逻辑:阶段匹配 + 行为匹配(可选)+ 关键字匹配(可选)
// 从匹配结果中随机选择一条(话术池随机)
func (d *speechcraft) MatchByStage(ctx context.Context, stage int, status, content, platform string) (script *entity.Speechcraft, err error) {
// 查询该阶段的所有话术
filter := bson.M{
"stage": stage,
"isDeleted": false,
}
if !g.IsEmpty(platform) {
filter["platform"] = platform
}
var list []*entity.Speechcraft
// 使用mongo.DB().Find会自动从token或accountName获取tenantId并过滤
// 查询所有匹配的话术(不分页)
page := &beans.Page{
PageNum: 1,
PageSize: 10000, // 话术匹配场景设置足够大的PageSize
}
orderBy := []beans.OrderBy{} // 无需排序,后续会随机选择
if _, err = mongo.DB().Find(ctx, filter, &list, entity.SpeechcraftCollection, page, orderBy); err != nil {
return nil, err
}
// 收集所有匹配的话术
matched := make([]*entity.Speechcraft, 0, len(list))
for _, item := range list {
// 行为匹配(空=任意行为都匹配)
if !g.IsEmpty(item.Status) && item.Status != status {
continue
}
// 关键字匹配(空=任意内容都匹配)
if len(item.Keywords) > 0 && !d.matchKeywords(content, item.Keywords) {
continue
}
// 匹配成功,加入候选池
matched = append(matched, item)
}
// 从候选池随机选择一条
if len(matched) > 0 {
script = matched[grand.Intn(len(matched))]
}
return
}
// matchKeywords 检查内容是否包含任一关键字
func (d *speechcraft) matchKeywords(content string, keywords []string) bool {
for _, kw := range keywords {
if g.IsEmpty(kw) {
continue
}
if strings.Contains(content, kw) {
return true
}
}
return false
}
// FindByTag 根据tag查询话术用于去重检查同一租户下tag唯一
func (d *speechcraft) FindByTag(ctx context.Context, tag string) (speechcraft *entity.Speechcraft, err error) {
filter := bson.M{
"tag": tag,
"isDeleted": false,
}
err = mongo.DB().FindOne(ctx, filter, &speechcraft, entity.SpeechcraftCollection)
if err != nil {
if err.Error() == "mongo: no documents in result" {
return nil, nil
}
return nil, err
}
return
}
// FindByTagAndPlatform 根据tag和platform查询话术用于去重检查
func (d *speechcraft) FindByTagAndPlatform(ctx context.Context, tag, platform string) (speechcraft *entity.Speechcraft, err error) {
filter := bson.M{
"tag": tag,
"platform": platform,
"isDeleted": false,
}
err = mongo.DB().FindOne(ctx, filter, &speechcraft, entity.SpeechcraftCollection)
if err != nil {
if err.Error() == "mongo: no documents in result" {
return nil, nil
}
return nil, err
}
return
}
// GetById 根据ID查询话术
// 使用 MongoDAO不需要token验证
func (d *speechcraft) GetById(ctx context.Context, id string) (speechcraft *entity.Speechcraft, err error) {
objectId, err := bson.ObjectIDFromHex(id)
if err != nil {
return
}
filter := bson.M{"_id": objectId, "isDeleted": false}
var result entity.Speechcraft
err = MongoDAO.FindOne(ctx, filter, &result, entity.SpeechcraftCollection)
if err != nil {
return nil, err
}
// 如果未找到记录result 是零值
if result.Id.IsZero() {
return nil, nil
}
speechcraft = &result
return
}
// UpdateEntity 更新话术实体(用于绑定/解绑/同步等场景)
func (d *speechcraft) UpdateEntity(ctx context.Context, speechcraft *entity.Speechcraft) (err error) {
filter := bson.M{"_id": speechcraft.Id, "isDeleted": false}
// 将实体转换为bson.M
updateDoc := bson.M{}
data, _ := bson.Marshal(speechcraft)
bson.Unmarshal(data, &updateDoc)
_, err = mongo.DB().Update(ctx, filter, bson.M{"$set": updateDoc}, entity.SpeechcraftCollection)
return
}
// FindByStage 查询指定阶段的所有话术
func (d *speechcraft) FindByStage(ctx context.Context, stage int, platform string) (list []*entity.Speechcraft, err error) {
filter := bson.M{
"stage": stage,
"isDeleted": false,
}
if !g.IsEmpty(platform) {
filter["platform"] = platform
}
// 使用统一的mongo.DB().Find方法支持分页和排序
page := &beans.Page{
PageNum: 1,
PageSize: 10000, // 查询所有话术(不分页)
}
orderBy := []beans.OrderBy{
{Field: "priority", Order: beans.Desc}, // 按优先级倒序
}
_, err = mongo.DB().Find(ctx, filter, &list, entity.SpeechcraftCollection, page, orderBy)
return
}

View File

@@ -1,143 +0,0 @@
package dao
import (
"context"
"customer-server/model/entity"
"gitea.com/red-future/common/db/mongo"
"github.com/gogf/gf/v2/os/gtime"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
var UserStage = new(userStage)
type userStage struct{}
// GetOrCreate 获取用户阶段,不存在则创建
func (d *userStage) GetOrCreate(ctx context.Context, userId, platform string) (state *entity.UserStage, err error) {
filter := bson.M{
"userId": userId,
"platform": platform,
"isDeleted": false,
}
state = &entity.UserStage{}
if err = mongo.DB().FindOne(ctx, filter, state, entity.UserStageCollection); err == nil {
return
}
// 不存在则创建
now := gtime.Now().Time
state = &entity.UserStage{
UserId: userId,
Platform: platform,
Stage: entity.StageInit,
Status: entity.StatusIdle,
LastMsgAt: &now, // 取地址赋值给指针类型
}
state.CreatedAt = &now // 取地址赋值给指针类型
state.UpdatedAt = &now // 取地址赋值给指针类型
state.IsDeleted = false
_, err = mongo.DB().Insert(ctx, []interface{}{state}, entity.UserStageCollection)
return
}
// UpdateStage 更新用户阶段
func (d *userStage) UpdateStage(ctx context.Context, userId, platform string, stage int) (err error) {
filter := bson.M{
"userId": userId,
"platform": platform,
"isDeleted": false,
}
update := bson.M{
"$set": bson.M{
"stage": stage,
"lastMsgAt": gtime.Now().Time,
"updatedAt": gtime.Now().Time,
},
}
_, err = mongo.DB().Update(ctx, filter, update, entity.UserStageCollection)
return
}
// UpdateStatus 更新用户行为
func (d *userStage) UpdateStatus(ctx context.Context, userId, platform, status string) (err error) {
filter := bson.M{
"userId": userId,
"platform": platform,
"isDeleted": false,
}
update := bson.M{
"$set": bson.M{
"status": status,
"lastMsgAt": gtime.Now().Time,
"updatedAt": gtime.Now().Time,
},
}
_, err = mongo.DB().Update(ctx, filter, update, entity.UserStageCollection)
return
}
// UpdateStageAndStatus 同时更新阶段和行为
func (d *userStage) UpdateStageAndStatus(ctx context.Context, userId, platform string, stage int, status string) (err error) {
filter := bson.M{
"userId": userId,
"platform": platform,
"isDeleted": false,
}
update := bson.M{
"$set": bson.M{
"stage": stage,
"status": status,
"lastMsgAt": gtime.Now().Time,
"updatedAt": gtime.Now().Time,
},
}
_, err = mongo.DB().Update(ctx, filter, update, entity.UserStageCollection)
return
}
// Reset 重置用户阶段到初始状态
func (d *userStage) Reset(ctx context.Context, userId, platform string) (err error) {
return d.UpdateStageAndStatus(ctx, userId, platform, entity.StageInit, entity.StatusIdle)
}
// FindByUser 查询用户阶段
func (d *userStage) FindByUser(ctx context.Context, userId, platform string) (state *entity.UserStage, err error) {
filter := bson.M{
"userId": userId,
"platform": platform,
"isDeleted": false,
}
state = &entity.UserStage{}
err = mongo.DB().FindOne(ctx, filter, state, entity.UserStageCollection)
return
}
// Upsert 更新或插入用户阶段
func (d *userStage) Upsert(ctx context.Context, userId, platform string, stage int, status string) (err error) {
filter := bson.M{
"userId": userId,
"platform": platform,
}
now := gtime.Now().Time
update := bson.M{
"$set": bson.M{
"stage": stage,
"status": status,
"lastMsgAt": now,
"updatedAt": now,
"isDeleted": false,
},
"$setOnInsert": bson.M{
"userId": userId,
"platform": platform,
"createdAt": now,
},
}
opts := options.UpdateOne().SetUpsert(true)
_, err = mongo.GetDB().Collection(entity.UserStageCollection).UpdateOne(ctx, filter, update, opts)
return
}