// Copyright © 2023 OpenIM open source community. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package admin import ( "context" "crypto/hmac" cryptorand "crypto/rand" "crypto/sha1" "encoding/base32" "encoding/binary" "fmt" "math/rand" "net/url" "strconv" "strings" "time" "github.com/openimsdk/tools/errs" "github.com/openimsdk/tools/log" "github.com/openimsdk/tools/mcontext" "github.com/openimsdk/tools/utils/datautil" "git.imall.cloud/openim/chat/pkg/common/constant" "git.imall.cloud/openim/chat/pkg/common/db/dbutil" admindb "git.imall.cloud/openim/chat/pkg/common/db/table/admin" "git.imall.cloud/openim/chat/pkg/common/mctx" "git.imall.cloud/openim/chat/pkg/eerrs" "git.imall.cloud/openim/chat/pkg/protocol/admin" "git.imall.cloud/openim/chat/pkg/protocol/chat" "github.com/google/uuid" ) func (o *adminServer) GetAdminInfo(ctx context.Context, req *admin.GetAdminInfoReq) (*admin.GetAdminInfoResp, error) { userID, err := mctx.CheckAdmin(ctx) if err != nil { return nil, err } a, err := o.Database.GetAdminUserID(ctx, userID) if err != nil { return nil, err } // 生成完整的二维码URL,优先使用账号,如果没有则使用昵称 accountName := a.Account if accountName == "" { accountName = a.Nickname } googleAuthKey := o.generateGoogleAuthQRCodeURL(a.GoogleAuthKey, accountName) return &admin.GetAdminInfoResp{ Account: a.Account, Password: a.Password, OperationPassword: a.OperationPassword, FaceURL: a.FaceURL, Nickname: a.Nickname, UserID: a.UserID, Level: a.Level, GoogleAuthKey: googleAuthKey, CreateTime: a.CreateTime.UnixMilli(), }, nil } func (o *adminServer) ChangeAdminPassword(ctx context.Context, req *admin.ChangeAdminPasswordReq) (*admin.ChangeAdminPasswordResp, error) { user, err := o.Database.GetAdminUserID(ctx, req.UserID) if err != nil { return nil, err } if user.Password != req.CurrentPassword { return nil, errs.ErrInternalServer.WrapMsg("password error") } if err := o.Database.ChangePassword(ctx, req.UserID, req.NewPassword); err != nil { return nil, err } // 修改密码成功后,清除 Redis 中的 token,使所有登录会话失效 if err := o.Database.DeleteToken(ctx, req.UserID); err != nil { // 清除 token 失败不影响密码修改,只记录日志 log.ZWarn(ctx, "Failed to delete token after password change", err, "userID", req.UserID) } return &admin.ChangeAdminPasswordResp{}, nil } func (o *adminServer) ChangeOperationPassword(ctx context.Context, req *admin.ChangeOperationPasswordReq) (*admin.ChangeOperationPasswordResp, error) { // 获取当前登录的管理员ID userID, err := mctx.CheckAdmin(ctx) if err != nil { return nil, err } // 获取管理员信息 adminUser, err := o.Database.GetAdminUserID(ctx, userID) if err != nil { return nil, err } // 检查是否为超级管理员(level:100) if adminUser.Level != constant.AdvancedUserLevel { return nil, errs.ErrNoPermission.WrapMsg("only super admin (level:100) can set operation password") } // 验证新密码不能为空 if req.NewPassword == "" { return nil, errs.NewCodeError(errs.ErrArgs.Code(), "new password cannot be empty") } // 根据数据库中是否已有操作密码来判断是首次设置还是修改 hasOperationPassword := adminUser.OperationPassword != "" if hasOperationPassword { // 已设置过操作密码:必须校验旧密码且新旧不能相同 if req.CurrentPassword == "" { return nil, errs.NewCodeError(errs.ErrArgs.Code(), "current password is required when changing operation password") } if adminUser.OperationPassword != req.CurrentPassword { return nil, errs.NewCodeError(errs.ErrArgs.Code(), "current operation password is incorrect") } if req.NewPassword == req.CurrentPassword { return nil, errs.NewCodeError(errs.ErrArgs.Code(), "new password cannot be the same as current password") } } // 首次设置时,即使提供了 currentPassword 也不报错,直接忽略 // 更新操作密码 if err := o.Database.ChangeOperationPassword(ctx, userID, req.NewPassword); err != nil { return nil, err } return &admin.ChangeOperationPasswordResp{}, nil } func (o *adminServer) AddAdminAccount(ctx context.Context, req *admin.AddAdminAccountReq) (*admin.AddAdminAccountResp, error) { if err := o.CheckSuperAdmin(ctx); err != nil { return nil, err } _, err := o.Database.GetAdmin(ctx, req.Account) if err == nil { return nil, errs.ErrDuplicateKey.WrapMsg("the account is registered") } adm := &admindb.Admin{ Account: req.Account, Password: req.Password, FaceURL: req.FaceURL, Nickname: req.Nickname, UserID: o.genUserID(), Level: 80, CreateTime: time.Now(), } if err = o.Database.AddAdminAccount(ctx, []*admindb.Admin{adm}); err != nil { return nil, err } return &admin.AddAdminAccountResp{}, nil } func (o *adminServer) DelAdminAccount(ctx context.Context, req *admin.DelAdminAccountReq) (*admin.DelAdminAccountResp, error) { if err := o.CheckSuperAdmin(ctx); err != nil { return nil, err } if datautil.Duplicate(req.UserIDs) { return nil, errs.ErrArgs.WrapMsg("user ids is duplicate") } for _, userID := range req.UserIDs { superAdmin, err := o.Database.GetAdminUserID(ctx, userID) if err != nil { return nil, err } if superAdmin.Level == constant.AdvancedUserLevel { return nil, errs.ErrNoPermission.WrapMsg(fmt.Sprintf("%s is superAdminID", userID)) } } if err := o.Database.DelAdminAccount(ctx, req.UserIDs); err != nil { return nil, err } return &admin.DelAdminAccountResp{}, nil } func (o *adminServer) SearchAdminAccount(ctx context.Context, req *admin.SearchAdminAccountReq) (*admin.SearchAdminAccountResp, error) { if err := o.CheckSuperAdmin(ctx); err != nil { return nil, err } total, adminAccounts, err := o.Database.SearchAdminAccount(ctx, req.Keyword, req.Pagination) if err != nil { return nil, err } accounts := make([]*admin.GetAdminInfoResp, 0, len(adminAccounts)) for _, v := range adminAccounts { // 生成完整的二维码URL,优先使用账号,如果没有则使用昵称 accountName := v.Account if accountName == "" { accountName = v.Nickname } googleAuthKey := o.generateGoogleAuthQRCodeURL(v.GoogleAuthKey, accountName) temp := &admin.GetAdminInfoResp{ Account: v.Account, OperationPassword: v.OperationPassword, FaceURL: v.FaceURL, Nickname: v.Nickname, UserID: v.UserID, Level: v.Level, GoogleAuthKey: googleAuthKey, CreateTime: v.CreateTime.Unix(), } accounts = append(accounts, temp) } return &admin.SearchAdminAccountResp{Total: uint32(total), AdminAccounts: accounts}, nil } func (o *adminServer) AdminUpdateInfo(ctx context.Context, req *admin.AdminUpdateInfoReq) (*admin.AdminUpdateInfoResp, error) { userID, err := mctx.CheckAdmin(ctx) if err != nil { return nil, err } // 如果请求中包含操作密码,禁止通过此接口设置 // 操作密码只能通过 ChangeOperationPassword 接口修改 if req.OperationPassword != nil { return nil, errs.ErrArgs.WrapMsg("operation password can only be changed through ChangeOperationPassword interface") } update, err := ToDBAdminUpdate(req) if err != nil { return nil, err } info, err := o.Database.GetAdminUserID(ctx, mcontext.GetOpUserID(ctx)) if err != nil { return nil, err } if err := o.Database.UpdateAdmin(ctx, userID, update); err != nil { return nil, err } resp := &admin.AdminUpdateInfoResp{UserID: info.UserID} if req.Nickname == nil { resp.Nickname = info.Nickname } else { resp.Nickname = req.Nickname.Value } if req.FaceURL == nil { resp.FaceURL = info.FaceURL } else { resp.FaceURL = req.FaceURL.Value } return resp, nil } func (o *adminServer) Login(ctx context.Context, req *admin.LoginReq) (*admin.LoginResp, error) { a, err := o.Database.GetAdmin(ctx, req.Account) if err != nil { if dbutil.IsDBNotFound(err) { return nil, eerrs.ErrAccountNotFound.Wrap() } return nil, err } if a.Password != req.Password { return nil, eerrs.ErrPassword.Wrap() } // 如果设置了 Google Authenticator key,则必须验证 Google 验证码 if a.GoogleAuthKey != "" { if req.GoogleAuthCode == "" { return nil, eerrs.ErrGoogleAuthCodeRequired.WrapMsg("Google Authenticator 验证码不能为空") } // 验证 Google 验证码 if !o.verifyTOTP(a.GoogleAuthKey, req.GoogleAuthCode) { return nil, eerrs.ErrGoogleAuthCodeNotMatch.WrapMsg("Google Authenticator 验证码错误") } } adminToken, err := o.CreateToken(ctx, &admin.CreateTokenReq{UserID: a.UserID, UserType: constant.AdminUser}) if err != nil { return nil, err } return &admin.LoginResp{ AdminUserID: a.UserID, AdminAccount: a.Account, AdminToken: adminToken.Token, Nickname: a.Nickname, FaceURL: a.FaceURL, Level: a.Level, }, nil } func (o *adminServer) ChangePassword(ctx context.Context, req *admin.ChangePasswordReq) (*admin.ChangePasswordResp, error) { userID, err := mctx.CheckAdmin(ctx) if err != nil { return nil, err } a, err := o.Database.GetAdminUserID(ctx, userID) if err != nil { return nil, err } // 准备更新字段 update := make(map[string]any) // 修改登录密码 if req.Password != "" { passwordUpdate, err := ToDBAdminUpdatePassword(req.Password) if err != nil { return nil, err } for k, v := range passwordUpdate { update[k] = v } } // 修改操作密码(如果提供了新操作密码) if req.NewOperationPassword != "" { // 检查是否为超级管理员(level:100) if a.Level != constant.AdvancedUserLevel { return nil, errs.ErrNoPermission.WrapMsg("only super admin (level:100) can set operation password") } // 如果已设置操作密码,需要验证旧密码 if a.OperationPassword != "" { if req.CurrentOperationPassword == "" { return nil, errs.ErrArgs.WrapMsg("current operation password is required when changing operation password") } if a.OperationPassword != req.CurrentOperationPassword { return nil, errs.ErrNoPermission.WrapMsg("current operation password is incorrect") } if req.NewOperationPassword == req.CurrentOperationPassword { return nil, errs.ErrArgs.WrapMsg("new operation password cannot be the same as current password") } } else { // 首次设置操作密码,不需要提供旧密码 if req.CurrentOperationPassword != "" { return nil, errs.ErrArgs.WrapMsg("current operation password should not be provided when setting operation password for the first time") } } // 更新操作密码 update["operation_password"] = req.NewOperationPassword } // 执行更新 passwordChanged := false if len(update) > 0 { // 检查是否修改了登录密码 if req.Password != "" { passwordChanged = true } if err := o.Database.UpdateAdmin(ctx, a.UserID, update); err != nil { return nil, err } } // 如果修改了登录密码,清除 Redis 中的 token,使所有登录会话失效 if passwordChanged { if err := o.Database.DeleteToken(ctx, a.UserID); err != nil { // 清除 token 失败不影响密码修改,只记录日志 log.ZWarn(ctx, "Failed to delete token after password change", err, "userID", a.UserID) } } return &admin.ChangePasswordResp{}, nil } func (o *adminServer) SetGoogleAuthKey(ctx context.Context, req *admin.SetGoogleAuthKeyReq) (*admin.SetGoogleAuthKeyResp, error) { // 获取当前登录的管理员信息 currentUserID, err := mctx.CheckAdmin(ctx) if err != nil { return nil, err } currentAdmin, err := o.Database.GetAdminUserID(ctx, currentUserID) if err != nil { return nil, err } // 确定要操作的目标管理员:默认当前登录者;如果传入 userID 且为超级管理员,则操作指定管理员 targetUserID := currentUserID targetAdmin := currentAdmin if req.UserID != "" && req.UserID != currentUserID { // 仅超级管理员可以为其他管理员操作 if currentAdmin.Level != constant.AdvancedUserLevel { return nil, errs.ErrNoPermission.WrapMsg("only super admin (level:100) can operate other admins' google auth key") } targetUserID = req.UserID targetAdmin, err = o.Database.GetAdminUserID(ctx, targetUserID) if err != nil { return nil, err } } // 验证操作类型 if req.OperationType < 1 || req.OperationType > 3 { return nil, errs.ErrArgs.WrapMsg("operationType must be 1, 2, or 3") } var newKey string var qrCodeURL string operationType := req.OperationType switch req.OperationType { case 1: // 新设置:如果为空则设置,如果已存在则返回错误 if targetAdmin.GoogleAuthKey != "" { return nil, errs.ErrArgs.WrapMsg("Google Auth key already exists, use operationType=2 to regenerate or operationType=3 to clear") } // 生成新的密钥 generatedKey, err := o.generateGoogleAuthKey() if err != nil { return nil, errs.ErrInternalServer.WrapMsg("failed to generate Google Auth key: " + err.Error()) } newKey = generatedKey case 2: // 强制覆盖旧的:即使存在也生成新的 // 生成新的密钥 generatedKey, err := o.generateGoogleAuthKey() if err != nil { return nil, errs.ErrInternalServer.WrapMsg("failed to generate Google Auth key: " + err.Error()) } newKey = generatedKey case 3: // 清空:删除现有的密钥 newKey = "" qrCodeURL = "" // 更新数据库,清空密钥(使用 $unset 删除字段) if err := o.Database.ClearGoogleAuthKey(ctx, targetUserID); err != nil { return nil, err } return &admin.SetGoogleAuthKeyResp{ GoogleAuthKey: "", QrCodeURL: "", OperationType: 3, }, nil } // 更新数据库(操作类型1和2) if req.OperationType == 1 || req.OperationType == 2 { update := map[string]any{ "google_auth_key": newKey, } if err := o.Database.UpdateAdmin(ctx, targetUserID, update); err != nil { return nil, err } // 生成二维码URL accountName := targetAdmin.Account if accountName == "" { accountName = targetAdmin.Nickname } qrCodeURL = o.generateGoogleAuthQRCodeURL(newKey, accountName) } return &admin.SetGoogleAuthKeyResp{ GoogleAuthKey: newKey, QrCodeURL: qrCodeURL, OperationType: operationType, }, nil } // generateGoogleAuthKey 生成Google身份验证码密钥(Base32编码,16字节) func (o *adminServer) generateGoogleAuthKey() (string, error) { // 生成16字节的随机数据(Google Authenticator标准) keyBytes := make([]byte, 16) if _, err := cryptorand.Read(keyBytes); err != nil { return "", err } // 使用Base32编码(无填充),这是Google Authenticator使用的格式 return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(keyBytes), nil } // verifyTOTP 验证 TOTP (Time-based One-Time Password) 验证码 // 基于 RFC 6238 标准,使用 HMAC-SHA1 算法 func (o *adminServer) verifyTOTP(secret string, code string) bool { // 验证码必须是 6 位数字 if len(code) != 6 { return false } // 验证码必须全部是数字 expectedCode, err := strconv.Atoi(code) if err != nil { return false } // 如果 secret 是 otpauth URL 格式,从中提取密钥 // 格式: otpauth://totp/...?secret=XXX&... if strings.HasPrefix(secret, "otpauth://") { // 解析 URL 提取 secret 参数 parsedURL, err := url.Parse(secret) if err != nil { return false } secretParam := parsedURL.Query().Get("secret") if secretParam == "" { return false } secret = secretParam } // 解码 Base32 编码的密钥 key, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(strings.ToUpper(secret)) if err != nil { return false } // 获取当前时间戳(秒),除以 30 得到时间步数 timestamp := time.Now().Unix() timeStep := timestamp / 30 // 验证当前时间步和前后各一个时间步(允许时间偏差,共 3 个时间窗口,90 秒) for i := -1; i <= 1; i++ { step := timeStep + int64(i) // 将时间步转换为 8 字节的大端序字节数组 counter := make([]byte, 8) binary.BigEndian.PutUint64(counter, uint64(step)) // 使用 HMAC-SHA1 计算哈希 h := hmac.New(sha1.New, key) h.Write(counter) hash := h.Sum(nil) // 动态截取(RFC 6238) offset := hash[19] & 0x0f binaryCode := binary.BigEndian.Uint32(hash[offset:offset+4]) & 0x7fffffff // 取最后 6 位数字 calculatedCode := int(binaryCode % 1000000) // 如果匹配,返回 true if calculatedCode == expectedCode { return true } } return false } // generateGoogleAuthQRCodeURL 生成Google Authenticator二维码URL func (o *adminServer) generateGoogleAuthQRCodeURL(secret string, accountName string) string { if secret == "" { return "" } // 使用账号或昵称作为账户标识,优先使用账号 account := accountName if account == "" { account = "Admin" } // 服务提供商名称 issuer := "OpenIM Admin" // 构建 otpauth URL // Google Authenticator 标准格式: otpauth://totp/{label}?secret={secret}&issuer={issuer}&algorithm=SHA1&digits=6&period=30 // label 格式: {issuer}:{account},路径中的空格不需要编码 // 参数值(secret 和 issuer)需要 URL 编码 // 构建 label,格式为 issuer:account // 注意:路径部分(label)中的空格不需要编码,直接使用空格 label := fmt.Sprintf("%s:%s", issuer, account) // 构建完整的 URL // 参数值使用 QueryEscape 编码,但空格在参数值中会被编码为 %20 otpauthURL := fmt.Sprintf("otpauth://totp/%s?secret=%s&issuer=%s&algorithm=SHA1&digits=6&period=30", label, url.QueryEscape(secret), url.QueryEscape(issuer), ) return otpauthURL } func (o *adminServer) GetStatistics(ctx context.Context, req *admin.GetStatisticsReq) (*admin.GetStatisticsResp, error) { // 普通管理员也可以查看统计数据 if _, err := mctx.CheckAdmin(ctx); err != nil { return nil, err } // 用户总数 totalUsers, err := o.ChatDatabase.NewUserCountTotal(ctx, nil) if err != nil { return nil, err } // 今天注册的用户数 todayRegisteredUsers, err := o.ChatDatabase.CountTodayRegisteredUsers(ctx) if err != nil { return nil, err } // 今天活跃用户数(今天登录的不同用户数) todayActiveUsers, err := o.ChatDatabase.CountTodayActiveUsers(ctx) if err != nil { return nil, err } // 从数据库查询群组和好友统计数据 totalGroups, err := o.Database.CountTotalGroups(ctx) if err != nil { // 如果查询失败,返回 0,不阻塞其他统计数据的返回 totalGroups = 0 } todayNewGroups, err := o.Database.CountTodayNewGroups(ctx) if err != nil { todayNewGroups = 0 } totalFriends, err := o.Database.CountTotalFriends(ctx) if err != nil { totalFriends = 0 } // 消息和在线用户统计数据 // 注意:这些数据可能存储在 OpenIM 的核心服务中,如果数据库中没有,需要通过 OpenIM API 获取 // 目前先返回 0,后续可以根据实际情况实现 var todayMessages, totalMessages, onlineUsers int64 // TODO: 实现消息统计和在线用户统计 // 如果数据库中有消息表,可以在这里查询 // 如果 OpenIM 有统计接口,可以通过 ImApiCaller 调用 _ = o.ImApiCaller // 暂时保留,后续可以添加具体的统计接口调用 return &admin.GetStatisticsResp{ TotalUsers: totalUsers, TodayRegisteredUsers: todayRegisteredUsers, TodayActiveUsers: todayActiveUsers, TodayMessages: todayMessages, TotalMessages: totalMessages, TotalGroups: totalGroups, TotalFriends: totalFriends, OnlineUsers: onlineUsers, TodayNewGroups: todayNewGroups, }, nil } func (o *adminServer) genUserID() string { const l = 10 data := make([]byte, l) rand.Read(data) chars := []byte("0123456789") for i := 0; i < len(data); i++ { if i == 0 { data[i] = chars[1:][data[i]%9] } else { data[i] = chars[data[i]%10] } } return string(data) } func (o *adminServer) CheckSuperAdmin(ctx context.Context) error { userID, err := mctx.CheckAdmin(ctx) if err != nil { return err } adminUser, err := o.Database.GetAdminUserID(ctx, userID) if err != nil { return err } if adminUser.Level != constant.AdvancedUserLevel { return errs.ErrNoPermission.Wrap() } return nil } // ==================== 敏感词管理相关 RPC ==================== // 敏感词管理 func (o *adminServer) AddSensitiveWord(ctx context.Context, req *admin.AddSensitiveWordReq) (*admin.AddSensitiveWordResp, error) { // 调用Chat RPC chatReq := &chat.AddSensitiveWordReq{ Word: req.Word, Level: req.Level, Type: req.Type, Action: req.Action, Status: req.Status, Remark: req.Remark, } _, err := o.Chat.AddSensitiveWord(ctx, chatReq) if err != nil { return nil, err } return &admin.AddSensitiveWordResp{}, nil } func (o *adminServer) UpdateSensitiveWord(ctx context.Context, req *admin.UpdateSensitiveWordReq) (*admin.UpdateSensitiveWordResp, error) { // 调用Chat RPC chatReq := &chat.UpdateSensitiveWordReq{ Id: req.Id, Word: req.Word, Level: req.Level, Type: req.Type, Action: req.Action, Status: req.Status, Remark: req.Remark, } _, err := o.Chat.UpdateSensitiveWord(ctx, chatReq) if err != nil { return nil, err } return &admin.UpdateSensitiveWordResp{}, nil } func (o *adminServer) DeleteSensitiveWord(ctx context.Context, req *admin.DeleteSensitiveWordReq) (*admin.DeleteSensitiveWordResp, error) { // 调用Chat RPC chatReq := &chat.DeleteSensitiveWordReq{ Ids: req.Ids, } _, err := o.Chat.DeleteSensitiveWord(ctx, chatReq) if err != nil { return nil, err } return &admin.DeleteSensitiveWordResp{}, nil } func (o *adminServer) GetSensitiveWord(ctx context.Context, req *admin.GetSensitiveWordReq) (*admin.GetSensitiveWordResp, error) { // 调用Chat RPC chatReq := &chat.GetSensitiveWordReq{ Id: req.Id, } chatResp, err := o.Chat.GetSensitiveWord(ctx, chatReq) if err != nil { return nil, err } // 转换响应 return &admin.GetSensitiveWordResp{ Word: convertToAdminSensitiveWordInfo(chatResp.Word), }, nil } func (o *adminServer) SearchSensitiveWords(ctx context.Context, req *admin.SearchSensitiveWordsReq) (*admin.SearchSensitiveWordsResp, error) { // 调用Chat RPC chatReq := &chat.SearchSensitiveWordsReq{ Keyword: req.Keyword, Action: req.Action, Status: req.Status, Pagination: req.Pagination, } chatResp, err := o.Chat.SearchSensitiveWords(ctx, chatReq) if err != nil { return nil, err } // 转换响应 var words []*admin.SensitiveWordInfo for _, word := range chatResp.Words { words = append(words, convertToAdminSensitiveWordInfo(word)) } return &admin.SearchSensitiveWordsResp{ Total: int64(chatResp.Total), Words: words, }, nil } func (o *adminServer) BatchAddSensitiveWords(ctx context.Context, req *admin.BatchAddSensitiveWordsReq) (*admin.BatchAddSensitiveWordsResp, error) { // 调用Chat RPC chatReq := &chat.BatchAddSensitiveWordsReq{ Words: convertToChatSensitiveWordDetailInfos(req.Words), } _, err := o.Chat.BatchAddSensitiveWords(ctx, chatReq) if err != nil { return nil, err } return &admin.BatchAddSensitiveWordsResp{}, nil } func (o *adminServer) BatchUpdateSensitiveWords(ctx context.Context, req *admin.BatchUpdateSensitiveWordsReq) (*admin.BatchUpdateSensitiveWordsResp, error) { // 调用Chat RPC chatReq := &chat.BatchUpdateSensitiveWordsReq{ Updates: convertToChatSensitiveWordDetailInfoMap(req.Updates), } _, err := o.Chat.BatchUpdateSensitiveWords(ctx, chatReq) if err != nil { return nil, err } return &admin.BatchUpdateSensitiveWordsResp{}, nil } func (o *adminServer) BatchDeleteSensitiveWords(ctx context.Context, req *admin.BatchDeleteSensitiveWordsReq) (*admin.BatchDeleteSensitiveWordsResp, error) { // 调用Chat RPC chatReq := &chat.BatchDeleteSensitiveWordsReq{ Ids: req.Ids, } _, err := o.Chat.BatchDeleteSensitiveWords(ctx, chatReq) if err != nil { return nil, err } return &admin.BatchDeleteSensitiveWordsResp{}, nil } // 敏感词分组管理 func (o *adminServer) AddSensitiveWordGroup(ctx context.Context, req *admin.AddSensitiveWordGroupReq) (*admin.AddSensitiveWordGroupResp, error) { // 调用Chat RPC chatReq := &chat.AddSensitiveWordGroupReq{ Name: req.Name, Remark: req.Remark, } _, err := o.Chat.AddSensitiveWordGroup(ctx, chatReq) if err != nil { return nil, err } return &admin.AddSensitiveWordGroupResp{}, nil } func (o *adminServer) UpdateSensitiveWordGroup(ctx context.Context, req *admin.UpdateSensitiveWordGroupReq) (*admin.UpdateSensitiveWordGroupResp, error) { // 调用Chat RPC chatReq := &chat.UpdateSensitiveWordGroupReq{ Id: req.Id, Name: req.Name, Remark: req.Remark, } _, err := o.Chat.UpdateSensitiveWordGroup(ctx, chatReq) if err != nil { return nil, err } return &admin.UpdateSensitiveWordGroupResp{}, nil } func (o *adminServer) DeleteSensitiveWordGroup(ctx context.Context, req *admin.DeleteSensitiveWordGroupReq) (*admin.DeleteSensitiveWordGroupResp, error) { // 调用Chat RPC chatReq := &chat.DeleteSensitiveWordGroupReq{ Ids: req.Ids, } _, err := o.Chat.DeleteSensitiveWordGroup(ctx, chatReq) if err != nil { return nil, err } return &admin.DeleteSensitiveWordGroupResp{}, nil } func (o *adminServer) GetSensitiveWordGroup(ctx context.Context, req *admin.GetSensitiveWordGroupReq) (*admin.GetSensitiveWordGroupResp, error) { // 调用Chat RPC chatReq := &chat.GetSensitiveWordGroupReq{ Id: req.Id, } chatResp, err := o.Chat.GetSensitiveWordGroup(ctx, chatReq) if err != nil { return nil, err } // 转换响应 return &admin.GetSensitiveWordGroupResp{ Group: convertToAdminSensitiveWordGroupInfo(chatResp.Group), }, nil } func (o *adminServer) GetAllSensitiveWordGroups(ctx context.Context, req *admin.GetAllSensitiveWordGroupsReq) (*admin.GetAllSensitiveWordGroupsResp, error) { // 调用Chat RPC chatReq := &chat.GetAllSensitiveWordGroupsReq{} chatResp, err := o.Chat.GetAllSensitiveWordGroups(ctx, chatReq) if err != nil { return nil, err } // 转换响应 var groups []*admin.SensitiveWordGroupInfo for _, group := range chatResp.Groups { groups = append(groups, convertToAdminSensitiveWordGroupInfo(group)) } return &admin.GetAllSensitiveWordGroupsResp{ Groups: groups, }, nil } // 敏感词配置管理 func (o *adminServer) GetSensitiveWordConfig(ctx context.Context, req *admin.GetSensitiveWordConfigReq) (*admin.GetSensitiveWordConfigResp, error) { fmt.Println("GetSensitiveWordConfig", "_________11", req) // 调用Chat RPC获取敏感词配置 chatResp, err := o.Chat.GetSensitiveWordConfig(ctx, &chat.GetSensitiveWordConfigReq{}) if err != nil { fmt.Println("GetSensitiveWordConfig", "_________22", err) return nil, err } fmt.Println("GetSensitiveWordConfig", "_________33", chatResp) // 转换响应 return &admin.GetSensitiveWordConfigResp{ Config: &admin.SensitiveWordConfigInfo{ Id: chatResp.Config.Id, EnableFilter: chatResp.Config.EnableFilter, FilterMode: chatResp.Config.FilterMode, ReplaceChar: chatResp.Config.ReplaceChar, WhitelistUsers: chatResp.Config.WhitelistUsers, WhitelistGroups: chatResp.Config.WhitelistGroups, LogEnabled: chatResp.Config.LogEnabled, AutoApprove: chatResp.Config.AutoApprove, UpdateTime: chatResp.Config.UpdateTime, }, }, nil } func (o *adminServer) UpdateSensitiveWordConfig(ctx context.Context, req *admin.UpdateSensitiveWordConfigReq) (*admin.UpdateSensitiveWordConfigResp, error) { // 调用Chat RPC更新敏感词配置 chatReq := &chat.UpdateSensitiveWordConfigReq{ Config: &chat.SensitiveWordConfigInfo{ Id: req.Config.Id, EnableFilter: req.Config.EnableFilter, FilterMode: req.Config.FilterMode, ReplaceChar: req.Config.ReplaceChar, WhitelistUsers: req.Config.WhitelistUsers, WhitelistGroups: req.Config.WhitelistGroups, LogEnabled: req.Config.LogEnabled, AutoApprove: req.Config.AutoApprove, UpdateTime: req.Config.UpdateTime, }, } _, err := o.Chat.UpdateSensitiveWordConfig(ctx, chatReq) if err != nil { return nil, err } return &admin.UpdateSensitiveWordConfigResp{}, nil } // 敏感词日志管理 func (o *adminServer) GetSensitiveWordLogs(ctx context.Context, req *admin.GetSensitiveWordLogsReq) (*admin.GetSensitiveWordLogsResp, error) { // 调用Chat RPC chatReq := &chat.GetSensitiveWordLogsReq{ UserId: req.UserId, GroupId: req.GroupId, Pagination: req.Pagination, } chatResp, err := o.Chat.GetSensitiveWordLogs(ctx, chatReq) if err != nil { return nil, err } // 转换响应 var logs []*admin.SensitiveWordLogInfo for _, log := range chatResp.Logs { logs = append(logs, convertToAdminSensitiveWordLogInfo(log)) } return &admin.GetSensitiveWordLogsResp{ Total: int64(chatResp.Total), Logs: logs, }, nil } // GetUserLoginRecords 查询用户登录记录 func (o *adminServer) GetUserLoginRecords(ctx context.Context, req *admin.GetUserLoginRecordsReq) (*admin.GetUserLoginRecordsResp, error) { // 调用Chat RPC chatReq := &chat.GetUserLoginRecordsReq{ UserId: req.UserId, Ip: req.Ip, Pagination: req.Pagination, } chatResp, err := o.Chat.GetUserLoginRecords(ctx, chatReq) if err != nil { return nil, err } // 转换响应 var records []*admin.UserLoginRecordInfo for _, record := range chatResp.Records { records = append(records, &admin.UserLoginRecordInfo{ UserId: record.UserId, LoginTime: record.LoginTime, Ip: record.Ip, DeviceId: record.DeviceId, Platform: record.Platform, FaceUrl: record.FaceUrl, Nickname: record.Nickname, }) } return &admin.GetUserLoginRecordsResp{ Total: int64(chatResp.Total), Records: records, }, nil } func (o *adminServer) DeleteSensitiveWordLogs(ctx context.Context, req *admin.DeleteSensitiveWordLogsReq) (*admin.DeleteSensitiveWordLogsResp, error) { // 调用Chat RPC chatReq := &chat.DeleteSensitiveWordLogsReq{ Ids: req.Ids, } _, err := o.Chat.DeleteSensitiveWordLogs(ctx, chatReq) if err != nil { return nil, err } return &admin.DeleteSensitiveWordLogsResp{}, nil } // 敏感词统计 func (o *adminServer) GetSensitiveWordStats(ctx context.Context, req *admin.GetSensitiveWordStatsReq) (*admin.GetSensitiveWordStatsResp, error) { // 调用Chat RPC获取敏感词统计 chatResp, err := o.Chat.GetSensitiveWordStats(ctx, &chat.GetSensitiveWordStatsReq{}) if err != nil { return nil, err } // 转换响应 return &admin.GetSensitiveWordStatsResp{ Stats: &admin.SensitiveWordStatsInfo{ Total: chatResp.Stats.Total, Enabled: chatResp.Stats.Enabled, Disabled: chatResp.Stats.Disabled, Replace: chatResp.Stats.Replace, Block: chatResp.Stats.Block, }, }, nil } func (o *adminServer) GetSensitiveWordLogStats(ctx context.Context, req *admin.GetSensitiveWordLogStatsReq) (*admin.GetSensitiveWordLogStatsResp, error) { // 调用Chat RPC chatReq := &chat.GetSensitiveWordLogStatsReq{ StartTime: req.StartTime, EndTime: req.EndTime, } chatResp, err := o.Chat.GetSensitiveWordLogStats(ctx, chatReq) if err != nil { return nil, err } // 转换响应 return &admin.GetSensitiveWordLogStatsResp{ Stats: &admin.SensitiveWordLogStatsInfo{ Total: chatResp.Stats.Total, Replace: chatResp.Stats.Replace, Block: chatResp.Stats.Block, }, }, nil } // ==================== 辅助函数 ==================== // generateID 生成唯一ID func generateID() string { return uuid.New().String() } // getAdminUserID 获取当前管理员用户ID func getAdminUserID(ctx context.Context) string { userID, _ := mctx.CheckAdmin(ctx) return userID } // convertToAdminSensitiveWordInfo 转换为Admin敏感词信息 func convertToAdminSensitiveWordInfo(word *chat.SensitiveWordDetailInfo) *admin.SensitiveWordInfo { return &admin.SensitiveWordInfo{ Id: word.Id, Word: word.Word, Level: word.Level, Type: word.Type, Action: word.Action, Status: word.Status, Creator: word.Creator, Updater: word.Updater, CreateTime: word.CreateTime, UpdateTime: word.UpdateTime, Remark: word.Remark, } } // convertToChatSensitiveWordDetailInfos 转换为Chat敏感词详细信息列表 func convertToChatSensitiveWordDetailInfos(words []*admin.SensitiveWordInfo) []*chat.SensitiveWordDetailInfo { var result []*chat.SensitiveWordDetailInfo for _, word := range words { result = append(result, &chat.SensitiveWordDetailInfo{ Id: word.Id, Word: word.Word, Level: word.Level, Type: word.Type, Action: word.Action, Status: word.Status, Creator: word.Creator, Updater: word.Updater, CreateTime: word.CreateTime, UpdateTime: word.UpdateTime, Remark: word.Remark, }) } return result } // convertToChatSensitiveWordDetailInfoMap 转换为Chat敏感词详细信息映射 func convertToChatSensitiveWordDetailInfoMap(updates map[string]*admin.SensitiveWordInfo) map[string]*chat.SensitiveWordDetailInfo { result := make(map[string]*chat.SensitiveWordDetailInfo) for id, word := range updates { result[id] = &chat.SensitiveWordDetailInfo{ Id: word.Id, Word: word.Word, Level: word.Level, Type: word.Type, Action: word.Action, Status: word.Status, Creator: word.Creator, Updater: word.Updater, CreateTime: word.CreateTime, UpdateTime: word.UpdateTime, Remark: word.Remark, } } return result } // convertToAdminSensitiveWordGroupInfo 转换为Admin敏感词分组信息 func convertToAdminSensitiveWordGroupInfo(group *chat.SensitiveWordGroupInfo) *admin.SensitiveWordGroupInfo { return &admin.SensitiveWordGroupInfo{ Id: group.Id, Name: group.Name, Remark: group.Remark, CreateTime: group.CreateTime, UpdateTime: group.UpdateTime, } } // convertToAdminSensitiveWordLogInfo 转换为Admin敏感词日志信息 func convertToAdminSensitiveWordLogInfo(log *chat.SensitiveWordLogInfo) *admin.SensitiveWordLogInfo { return &admin.SensitiveWordLogInfo{ Id: log.Id, UserId: log.UserId, GroupId: log.GroupId, Content: log.Content, MatchedWords: log.MatchedWords, Action: log.Action, ProcessedText: log.ProcessedText, CreateTime: log.CreateTime, } }