1144 lines
34 KiB
Go
1144 lines
34 KiB
Go
// 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,
|
||
}
|
||
}
|