Files
chat-deploy/internal/rpc/admin/admin.go
kim.dev.6789 b7f8db7d08 复制项目
2026-01-14 22:35:45 +08:00

1144 lines
34 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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,
}
}