复制项目

This commit is contained in:
kim.dev.6789
2026-01-14 22:35:45 +08:00
parent 305d526110
commit b7f8db7d08
297 changed files with 81784 additions and 0 deletions

1143
internal/rpc/admin/admin.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,159 @@
// 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"
"strings"
"time"
"github.com/openimsdk/tools/utils/datautil"
"github.com/google/uuid"
"github.com/openimsdk/tools/errs"
"git.imall.cloud/openim/chat/pkg/common/constant"
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/protocol/admin"
"git.imall.cloud/openim/chat/pkg/protocol/common"
)
func (o *adminServer) AddApplet(ctx context.Context, req *admin.AddAppletReq) (*admin.AddAppletResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if req.Name == "" {
return nil, errs.ErrArgs.WrapMsg("name empty")
}
if req.AppID == "" {
return nil, errs.ErrArgs.WrapMsg("appid empty")
}
if !(req.Status == constant.StatusOnShelf || req.Status == constant.StatusUnShelf) {
return nil, errs.ErrArgs.WrapMsg("invalid status")
}
m := admindb.Applet{
ID: req.Id,
Name: req.Name,
AppID: req.AppID,
Icon: req.Icon,
URL: req.Url,
MD5: req.Md5,
Size: req.Size,
Version: req.Version,
Priority: req.Priority,
Status: uint8(req.Status),
CreateTime: time.Now(),
}
if m.ID == "" {
m.ID = uuid.New().String()
}
if err := o.Database.CreateApplet(ctx, []*admindb.Applet{&m}); err != nil {
return nil, err
}
return &admin.AddAppletResp{}, nil
}
func (o *adminServer) DelApplet(ctx context.Context, req *admin.DelAppletReq) (*admin.DelAppletResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if len(req.AppletIds) == 0 {
return nil, errs.ErrArgs.WrapMsg("AppletIds empty")
}
applets, err := o.Database.FindApplet(ctx, req.AppletIds)
if err != nil {
return nil, err
}
if ids := datautil.Single(req.AppletIds, datautil.Slice(applets, func(e *admindb.Applet) string { return e.ID })); len(ids) > 0 {
return nil, errs.ErrArgs.WrapMsg("ids not found: " + strings.Join(ids, ", "))
}
if err := o.Database.DelApplet(ctx, req.AppletIds); err != nil {
return nil, err
}
return &admin.DelAppletResp{}, nil
}
func (o *adminServer) UpdateApplet(ctx context.Context, req *admin.UpdateAppletReq) (*admin.UpdateAppletResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
_, err := o.Database.GetApplet(ctx, req.Id)
if err != nil {
return nil, err
}
update, err := ToDBAppletUpdate(req)
if err != nil {
return nil, err
}
if err := o.Database.UpdateApplet(ctx, req.Id, update); err != nil {
return nil, err
}
return &admin.UpdateAppletResp{}, nil
}
func (o *adminServer) FindApplet(ctx context.Context, req *admin.FindAppletReq) (*admin.FindAppletResp, error) {
if _, _, err := mctx.Check(ctx); err != nil {
return nil, err
}
applets, err := o.Database.FindOnShelf(ctx)
if err != nil {
return nil, err
}
resp := &admin.FindAppletResp{Applets: make([]*common.AppletInfo, 0, len(applets))}
for _, applet := range applets {
resp.Applets = append(resp.Applets, &common.AppletInfo{
Id: applet.ID,
Name: applet.Name,
AppID: applet.AppID,
Icon: applet.Icon,
Url: applet.URL,
Md5: applet.MD5,
Size: applet.Size,
Version: applet.Version,
Priority: applet.Priority,
Status: uint32(applet.Status),
CreateTime: applet.CreateTime.UnixMilli(),
})
}
return resp, nil
}
func (o *adminServer) SearchApplet(ctx context.Context, req *admin.SearchAppletReq) (*admin.SearchAppletResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
total, applets, err := o.Database.SearchApplet(ctx, req.Keyword, req.Pagination)
if err != nil {
return nil, err
}
resp := &admin.SearchAppletResp{Total: uint32(total), Applets: make([]*common.AppletInfo, 0, len(applets))}
for _, applet := range applets {
resp.Applets = append(resp.Applets, &common.AppletInfo{
Id: applet.ID,
Name: applet.Name,
AppID: applet.AppID,
Icon: applet.Icon,
Url: applet.URL,
Md5: applet.MD5,
Size: applet.Size,
Version: applet.Version,
Priority: applet.Priority,
Status: uint32(applet.Status),
CreateTime: applet.CreateTime.UnixMilli(),
})
}
return resp, nil
}

View File

@@ -0,0 +1,289 @@
package admin
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
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/protocol/admin"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"github.com/openimsdk/tools/utils/datautil"
"github.com/redis/go-redis/v9"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
func IsNotFound(err error) bool {
switch errs.Unwrap(err) {
case redis.Nil, mongo.ErrNoDocuments:
return true
default:
return false
}
}
func (o *adminServer) db2pbApplication(val *admindb.Application) *admin.ApplicationVersion {
return &admin.ApplicationVersion{
Id: val.ID.Hex(),
Platform: val.Platform,
Version: val.Version,
Url: val.Url,
Text: val.Text,
Force: val.Force,
Latest: val.Latest,
Hot: val.Hot,
CreateTime: val.CreateTime.UnixMilli(),
}
}
// LatestVersionAPIResponse 外部 API 响应结构
type LatestVersionAPIResponse struct {
Success bool `json:"success"`
ErrorCode string `json:"errorCode"`
ErrorMessage string `json:"errorMessage"`
ShowType int `json:"showType"`
Data struct {
APKPath string `json:"apk_path"`
APKSize int64 `json:"apk_size"`
AppLogo string `json:"app_logo"`
AppName string `json:"app_name"`
CreatedAt string `json:"created_at"`
Success bool `json:"success"`
Version string `json:"version"`
} `json:"data"`
}
func (o *adminServer) LatestApplicationVersion(ctx context.Context, req *admin.LatestApplicationVersionReq) (*admin.LatestApplicationVersionResp, error) {
// 从系统配置读取 build_app_id
buildAppIDConfig, err := o.ChatDatabase.GetSystemConfig(ctx, "build_app_id")
if err != nil {
log.ZWarn(ctx, "Failed to get build_app_id from system config, falling back to database", err)
// 如果获取配置失败,回退到从数据库读取
res, err := o.Database.LatestVersion(ctx, req.Platform)
if err == nil {
return &admin.LatestApplicationVersionResp{Version: o.db2pbApplication(res)}, nil
} else if IsNotFound(err) {
return &admin.LatestApplicationVersionResp{}, nil
} else {
return nil, err
}
}
buildAppID := buildAppIDConfig.Value
if buildAppID == "" {
log.ZWarn(ctx, "build_app_id is empty in system config, falling back to database", nil)
// 如果配置值为空,回退到从数据库读取
res, err := o.Database.LatestVersion(ctx, req.Platform)
if err == nil {
return &admin.LatestApplicationVersionResp{Version: o.db2pbApplication(res)}, nil
} else if IsNotFound(err) {
return &admin.LatestApplicationVersionResp{}, nil
} else {
return nil, err
}
}
// 调用外部 API
apiURL := "https://down.imall.cloud/api/download/latest"
requestBody := map[string]string{
"app_id": buildAppID,
}
jsonData, err := json.Marshal(requestBody)
if err != nil {
log.ZError(ctx, "Failed to marshal request body", err, "buildAppID", buildAppID)
return nil, errs.ErrInternalServer.WrapMsg("failed to prepare request")
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewBuffer(jsonData))
if err != nil {
log.ZError(ctx, "Failed to create HTTP request", err, "url", apiURL)
return nil, errs.ErrInternalServer.WrapMsg("failed to create request")
}
httpReq.Header.Set("Content-Type", "application/json")
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(httpReq)
if err != nil {
log.ZError(ctx, "Failed to call external API", err, "url", apiURL)
// API 调用失败,回退到从数据库读取
res, err := o.Database.LatestVersion(ctx, req.Platform)
if err == nil {
return &admin.LatestApplicationVersionResp{Version: o.db2pbApplication(res)}, nil
} else if IsNotFound(err) {
return &admin.LatestApplicationVersionResp{}, nil
} else {
return nil, err
}
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.ZError(ctx, "Failed to read response body", err)
// 读取响应失败,回退到从数据库读取
res, err := o.Database.LatestVersion(ctx, req.Platform)
if err == nil {
return &admin.LatestApplicationVersionResp{Version: o.db2pbApplication(res)}, nil
} else if IsNotFound(err) {
return &admin.LatestApplicationVersionResp{}, nil
} else {
return nil, err
}
}
if resp.StatusCode != http.StatusOK {
log.ZWarn(ctx, "External API returned non-200 status", nil, "statusCode", resp.StatusCode, "body", string(body))
// API 返回非 200回退到从数据库读取
res, err := o.Database.LatestVersion(ctx, req.Platform)
if err == nil {
return &admin.LatestApplicationVersionResp{Version: o.db2pbApplication(res)}, nil
} else if IsNotFound(err) {
return &admin.LatestApplicationVersionResp{}, nil
} else {
return nil, err
}
}
var apiResp LatestVersionAPIResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
log.ZError(ctx, "Failed to unmarshal API response", err, "body", string(body))
// 解析响应失败,回退到从数据库读取
res, err := o.Database.LatestVersion(ctx, req.Platform)
if err == nil {
return &admin.LatestApplicationVersionResp{Version: o.db2pbApplication(res)}, nil
} else if IsNotFound(err) {
return &admin.LatestApplicationVersionResp{}, nil
} else {
return nil, err
}
}
if !apiResp.Success {
log.ZWarn(ctx, "External API returned success=false", nil, "errorCode", apiResp.ErrorCode, "errorMessage", apiResp.ErrorMessage)
// API 返回失败,回退到从数据库读取
res, err := o.Database.LatestVersion(ctx, req.Platform)
if err == nil {
return &admin.LatestApplicationVersionResp{Version: o.db2pbApplication(res)}, nil
} else if IsNotFound(err) {
return &admin.LatestApplicationVersionResp{}, nil
} else {
return nil, err
}
}
// 解析创建时间
var createTime time.Time
if apiResp.Data.CreatedAt != "" {
createTime, err = time.Parse(time.RFC3339, apiResp.Data.CreatedAt)
if err != nil {
log.ZWarn(ctx, "Failed to parse created_at", err, "createdAt", apiResp.Data.CreatedAt)
createTime = time.Now()
}
} else {
createTime = time.Now()
}
// 转换为 ApplicationVersion 格式
version := &admin.ApplicationVersion{
Id: "", // 外部 API 没有 ID
Platform: req.Platform,
Version: apiResp.Data.Version,
Url: apiResp.Data.APKPath,
Text: fmt.Sprintf("应用名称: %s", apiResp.Data.AppName),
Force: false, // 外部 API 没有提供此字段
Latest: true, // 从 latest 接口获取的肯定是最新版本
Hot: false, // 外部 API 没有提供此字段
CreateTime: createTime.UnixMilli(),
}
return &admin.LatestApplicationVersionResp{Version: version}, nil
}
func (o *adminServer) AddApplicationVersion(ctx context.Context, req *admin.AddApplicationVersionReq) (*admin.AddApplicationVersionResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
val := &admindb.Application{
ID: primitive.NewObjectID(),
Platform: req.Platform,
Version: req.Version,
Url: req.Url,
Text: req.Text,
Force: req.Force,
Latest: req.Latest,
Hot: req.Hot,
CreateTime: time.Now(),
}
if err := o.Database.AddVersion(ctx, val); err != nil {
return nil, err
}
return &admin.AddApplicationVersionResp{}, nil
}
func (o *adminServer) UpdateApplicationVersion(ctx context.Context, req *admin.UpdateApplicationVersionReq) (*admin.UpdateApplicationVersionResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
oid, err := primitive.ObjectIDFromHex(req.Id)
if err != nil {
return nil, errs.ErrArgs.WrapMsg("invalid id " + err.Error())
}
update := make(map[string]any)
putUpdate(update, "platform", req.Platform)
putUpdate(update, "version", req.Version)
putUpdate(update, "url", req.Url)
putUpdate(update, "text", req.Text)
putUpdate(update, "force", req.Force)
putUpdate(update, "latest", req.Latest)
putUpdate(update, "hot", req.Hot)
if err := o.Database.UpdateVersion(ctx, oid, update); err != nil {
return nil, err
}
return &admin.UpdateApplicationVersionResp{}, nil
}
func (o *adminServer) DeleteApplicationVersion(ctx context.Context, req *admin.DeleteApplicationVersionReq) (*admin.DeleteApplicationVersionResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
ids := make([]primitive.ObjectID, 0, len(req.Id))
for _, id := range req.Id {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return nil, errs.ErrArgs.WrapMsg("invalid id " + err.Error())
}
ids = append(ids, oid)
}
if err := o.Database.DeleteVersion(ctx, ids); err != nil {
return nil, err
}
return &admin.DeleteApplicationVersionResp{}, nil
}
func (o *adminServer) PageApplicationVersion(ctx context.Context, req *admin.PageApplicationVersionReq) (*admin.PageApplicationVersionResp, error) {
total, res, err := o.Database.PageVersion(ctx, req.Platform, req.Pagination)
if err != nil {
return nil, err
}
return &admin.PageApplicationVersionResp{
Total: total,
Versions: datautil.Slice(res, o.db2pbApplication),
}, nil
}
func putUpdate[T any](update map[string]any, name string, val interface{ GetValuePtr() *T }) {
ptrVal := val.GetValuePtr()
if ptrVal == nil {
return
}
update[name] = *ptrVal
}

View File

@@ -0,0 +1,70 @@
// 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"
"git.imall.cloud/openim/chat/pkg/common/db/dbutil"
"git.imall.cloud/openim/chat/pkg/eerrs"
"git.imall.cloud/openim/chat/pkg/protocol/admin"
)
func (o *adminServer) CheckRegisterForbidden(ctx context.Context, req *admin.CheckRegisterForbiddenReq) (*admin.CheckRegisterForbiddenResp, error) {
forbiddens, err := o.Database.FindIPForbidden(ctx, []string{req.Ip})
if err != nil {
return nil, err
}
for _, forbidden := range forbiddens {
if forbidden.LimitRegister {
return nil, eerrs.ErrForbidden.Wrap()
}
}
return &admin.CheckRegisterForbiddenResp{}, nil
}
func (o *adminServer) CheckLoginForbidden(ctx context.Context, req *admin.CheckLoginForbiddenReq) (*admin.CheckLoginForbiddenResp, error) {
forbiddens, err := o.Database.FindIPForbidden(ctx, []string{req.Ip})
if err != nil {
return nil, err
}
for _, forbidden := range forbiddens {
if forbidden.LimitLogin {
return nil, eerrs.ErrForbidden.WrapMsg("ip forbidden")
}
}
if _, err := o.Database.GetLimitUserLoginIP(ctx, req.UserID, req.Ip); err != nil {
if !dbutil.IsDBNotFound(err) {
return nil, err
}
count, err := o.Database.CountLimitUserLoginIP(ctx, req.UserID)
if err != nil {
return nil, err
}
if count > 0 {
return nil, eerrs.ErrForbidden.WrapMsg("user ip forbidden")
}
}
if forbiddenAccount, err := o.Database.GetBlockInfo(ctx, req.UserID); err == nil {
reason := "账户已被封禁"
if forbiddenAccount.Reason != "" {
reason = "账户已被封禁:" + forbiddenAccount.Reason
}
return nil, eerrs.ErrAccountBlocked.WrapMsg(reason)
} else if !dbutil.IsDBNotFound(err) {
return nil, err
}
return &admin.CheckLoginForbiddenResp{}, nil
}

View File

@@ -0,0 +1,55 @@
// 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"
"github.com/openimsdk/tools/errs"
"git.imall.cloud/openim/chat/pkg/common/mctx"
"git.imall.cloud/openim/chat/pkg/protocol/admin"
)
func (o *adminServer) GetClientConfig(ctx context.Context, req *admin.GetClientConfigReq) (*admin.GetClientConfigResp, error) {
conf, err := o.Database.GetConfig(ctx)
if err != nil {
return nil, err
}
return &admin.GetClientConfigResp{Config: conf}, nil
}
func (o *adminServer) SetClientConfig(ctx context.Context, req *admin.SetClientConfigReq) (*admin.SetClientConfigResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if len(req.Config) == 0 {
return nil, errs.ErrArgs.WrapMsg("update config empty")
}
if err := o.Database.SetConfig(ctx, req.Config); err != nil {
return nil, err
}
return &admin.SetClientConfigResp{}, nil
}
func (o *adminServer) DelClientConfig(ctx context.Context, req *admin.DelClientConfigReq) (*admin.DelClientConfigResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if err := o.Database.DelConfig(ctx, req.Keys); err != nil {
return nil, err
}
return &admin.DelClientConfigResp{}, nil
}

View File

@@ -0,0 +1,217 @@
// 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"
"math/rand"
"strings"
"time"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/tools/errs"
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"
)
func (o *adminServer) AddInvitationCode(ctx context.Context, req *admin.AddInvitationCodeReq) (*admin.AddInvitationCodeResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if len(req.Codes) == 0 {
return nil, errs.ErrArgs.WrapMsg("codes is empty")
}
if datautil.Duplicate(req.Codes) {
return nil, errs.ErrArgs.WrapMsg("codes is duplicate")
}
irs, err := o.Database.FindInvitationRegister(ctx, req.Codes)
if err != nil {
return nil, err
}
if len(irs) > 0 {
ids := datautil.Slice(irs, func(info *admindb.InvitationRegister) string { return info.InvitationCode })
return nil, errs.ErrArgs.WrapMsg("code existed", "ids", ids)
}
now := time.Now()
codes := make([]*admindb.InvitationRegister, 0, len(req.Codes))
for _, code := range req.Codes {
codes = append(codes, &admindb.InvitationRegister{
InvitationCode: code,
UsedByUserID: "",
CreateTime: now,
})
}
if err := o.Database.CreatInvitationRegister(ctx, codes); err != nil {
return nil, err
}
return &admin.AddInvitationCodeResp{}, nil
}
func (o *adminServer) GenInvitationCode(ctx context.Context, req *admin.GenInvitationCodeReq) (*admin.GenInvitationCodeResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if req.Num <= 0 || req.Len <= 0 {
return nil, errs.ErrArgs.WrapMsg("num or len <= 0")
}
if len(req.Chars) == 0 {
req.Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
}
now := time.Now()
invitationRegisters := make([]*admindb.InvitationRegister, 0, req.Num)
codes := make([]string, 0, req.Num)
for i := int32(0); i < req.Num; i++ {
buf := make([]byte, req.Len)
rand.Read(buf)
for i, b := range buf {
buf[i] = req.Chars[b%byte(len(req.Chars))]
}
codes = append(codes, string(buf))
invitationRegisters = append(invitationRegisters, &admindb.InvitationRegister{
InvitationCode: string(buf),
UsedByUserID: "",
CreateTime: now,
})
}
if datautil.Duplicate(codes) {
return nil, errs.ErrArgs.WrapMsg("gen duplicate codes")
}
irs, err := o.Database.FindInvitationRegister(ctx, codes)
if err != nil {
return nil, err
}
if len(irs) > 0 {
ids := datautil.Single(codes, datautil.Slice(irs, func(ir *admindb.InvitationRegister) string { return ir.InvitationCode }))
return nil, errs.ErrArgs.WrapMsg(strings.Join(ids, ", "))
}
if err := o.Database.CreatInvitationRegister(ctx, invitationRegisters); err != nil {
return nil, err
}
return &admin.GenInvitationCodeResp{}, nil
}
func (o *adminServer) FindInvitationCode(ctx context.Context, req *admin.FindInvitationCodeReq) (*admin.FindInvitationCodeResp, error) {
if _, _, err := mctx.Check(ctx); err != nil {
return nil, err
}
if len(req.Codes) == 0 {
return nil, errs.ErrArgs.WrapMsg("codes is empty")
}
invitationRegisters, err := o.Database.FindInvitationRegister(ctx, req.Codes)
if err != nil {
return nil, err
}
userIDs := make([]string, 0, len(invitationRegisters))
for _, register := range invitationRegisters {
if register.UsedByUserID != "" {
userIDs = append(userIDs, register.UsedByUserID)
}
}
userMap, err := o.Chat.MapUserPublicInfo(ctx, userIDs)
if err != nil {
return nil, err
}
resp := &admin.FindInvitationCodeResp{Codes: make([]*admin.InvitationRegister, 0, len(invitationRegisters))}
for _, register := range invitationRegisters {
resp.Codes = append(resp.Codes, &admin.InvitationRegister{
InvitationCode: register.InvitationCode,
CreateTime: register.CreateTime.UnixMilli(),
UsedUserID: register.UsedByUserID,
UsedUser: userMap[register.UsedByUserID],
})
}
return resp, nil
}
func (o *adminServer) UseInvitationCode(ctx context.Context, req *admin.UseInvitationCodeReq) (*admin.UseInvitationCodeResp, error) {
if _, _, err := mctx.Check(ctx); err != nil {
return nil, err
}
codes, err := o.Database.FindInvitationRegister(ctx, []string{req.Code})
if err != nil {
return nil, err
}
if len(codes) == 0 {
return nil, eerrs.ErrInvitationNotFound.Wrap()
}
if codes[0].UsedByUserID != "" {
return nil, eerrs.ErrInvitationCodeUsed.Wrap()
}
if err := o.Database.UpdateInvitationRegister(ctx, req.Code, ToDBInvitationRegisterUpdate(req.UserID)); err != nil {
return nil, err
}
return &admin.UseInvitationCodeResp{}, nil
}
func (o *adminServer) DelInvitationCode(ctx context.Context, req *admin.DelInvitationCodeReq) (*admin.DelInvitationCodeResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if len(req.Codes) == 0 {
return nil, errs.ErrArgs.WrapMsg("codes is empty")
}
if datautil.Duplicate(req.Codes) {
return nil, errs.ErrArgs.WrapMsg("codes is duplicate")
}
irs, err := o.Database.FindInvitationRegister(ctx, req.Codes)
if err != nil {
return nil, err
}
if len(irs) != len(req.Codes) {
ids := datautil.Single(req.Codes, datautil.Slice(irs, func(ir *admindb.InvitationRegister) string { return ir.InvitationCode }))
return nil, errs.ErrArgs.WrapMsg("code not found " + strings.Join(ids, ", "))
}
if err := o.Database.DelInvitationRegister(ctx, req.Codes); err != nil {
return nil, err
}
return &admin.DelInvitationCodeResp{}, nil
}
func (o *adminServer) SearchInvitationCode(ctx context.Context, req *admin.SearchInvitationCodeReq) (*admin.SearchInvitationCodeResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
total, list, err := o.Database.SearchInvitationRegister(ctx, req.Keyword, req.Status, req.UserIDs, req.Codes, req.Pagination)
if err != nil {
return nil, err
}
userIDs := make([]string, 0, len(list))
for _, register := range list {
if register.UsedByUserID != "" {
userIDs = append(userIDs, register.UsedByUserID)
}
}
userMap, err := o.Chat.MapUserPublicInfo(ctx, userIDs)
if err != nil {
return nil, err
}
invitationRegisters := make([]*admin.InvitationRegister, 0, len(list))
for _, register := range list {
invitationRegisters = append(invitationRegisters, &admin.InvitationRegister{
InvitationCode: register.InvitationCode,
CreateTime: register.CreateTime.UnixMilli(),
UsedUserID: register.UsedByUserID,
UsedUser: userMap[register.UsedByUserID],
})
}
return &admin.SearchInvitationCodeResp{
Total: uint32(total),
List: invitationRegisters,
}, nil
}

View File

@@ -0,0 +1,77 @@
// 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"
"time"
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/protocol/admin"
)
func (o *adminServer) SearchIPForbidden(ctx context.Context, req *admin.SearchIPForbiddenReq) (*admin.SearchIPForbiddenResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
total, forbiddens, err := o.Database.SearchIPForbidden(ctx, req.Keyword, req.Status, req.Pagination)
if err != nil {
return nil, err
}
resp := &admin.SearchIPForbiddenResp{
Forbiddens: make([]*admin.IPForbidden, 0, len(forbiddens)),
Total: uint32(total),
}
for _, forbidden := range forbiddens {
resp.Forbiddens = append(resp.Forbiddens, &admin.IPForbidden{
Ip: forbidden.IP,
LimitLogin: forbidden.LimitLogin,
LimitRegister: forbidden.LimitRegister,
CreateTime: forbidden.CreateTime.UnixMilli(),
})
}
return resp, nil
}
func (o *adminServer) AddIPForbidden(ctx context.Context, req *admin.AddIPForbiddenReq) (*admin.AddIPForbiddenResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
now := time.Now()
tables := make([]*admindb.IPForbidden, 0, len(req.Forbiddens))
for _, forbidden := range req.Forbiddens {
tables = append(tables, &admindb.IPForbidden{
IP: forbidden.Ip,
LimitLogin: forbidden.LimitLogin,
LimitRegister: forbidden.LimitRegister,
CreateTime: now,
})
}
if err := o.Database.AddIPForbidden(ctx, tables); err != nil {
return nil, err
}
return &admin.AddIPForbiddenResp{}, nil
}
func (o *adminServer) DelIPForbidden(ctx context.Context, req *admin.DelIPForbiddenReq) (*admin.DelIPForbiddenResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if err := o.Database.DelIPForbidden(ctx, req.Ips); err != nil {
return nil, err
}
return &admin.DelIPForbiddenResp{}, nil
}

View File

@@ -0,0 +1,134 @@
// 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"
"time"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/tools/errs"
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/protocol/admin"
"git.imall.cloud/openim/chat/pkg/protocol/common"
)
func (o *adminServer) AddDefaultFriend(ctx context.Context, req *admin.AddDefaultFriendReq) (*admin.AddDefaultFriendResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if len(req.UserIDs) == 0 {
return nil, errs.ErrArgs.WrapMsg("user ids is empty")
}
if datautil.Duplicate(req.UserIDs) {
return nil, errs.ErrArgs.WrapMsg("user ids is duplicate")
}
users, err := o.Chat.FindUserPublicInfo(ctx, req.UserIDs)
if err != nil {
return nil, err
}
if ids := datautil.Single(req.UserIDs, datautil.Slice(users, func(user *common.UserPublicInfo) string { return user.UserID })); len(ids) > 0 {
return nil, errs.ErrRecordNotFound.WrapMsg("user id not found", "userID", ids)
}
exists, err := o.Database.FindDefaultFriend(ctx, req.UserIDs)
if err != nil {
return nil, err
}
if len(exists) > 0 {
return nil, errs.ErrDuplicateKey.WrapMsg("user id existed", "userID", exists)
}
now := time.Now()
ms := make([]*admindb.RegisterAddFriend, 0, len(req.UserIDs))
for _, userID := range req.UserIDs {
ms = append(ms, &admindb.RegisterAddFriend{
UserID: userID,
CreateTime: now,
})
}
if err := o.Database.AddDefaultFriend(ctx, ms); err != nil {
return nil, err
}
return &admin.AddDefaultFriendResp{}, nil
}
func (o *adminServer) DelDefaultFriend(ctx context.Context, req *admin.DelDefaultFriendReq) (*admin.DelDefaultFriendResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if len(req.UserIDs) == 0 {
return nil, errs.ErrArgs.WrapMsg("user ids is empty")
}
if datautil.Duplicate(req.UserIDs) {
return nil, errs.ErrArgs.WrapMsg("user ids is duplicate")
}
exists, err := o.Database.FindDefaultFriend(ctx, req.UserIDs)
if err != nil {
return nil, err
}
if ids := datautil.Single(req.UserIDs, exists); len(ids) > 0 {
return nil, errs.ErrRecordNotFound.WrapMsg("user id not found", "userID", ids)
}
now := time.Now()
ms := make([]*admindb.RegisterAddFriend, 0, len(req.UserIDs))
for _, userID := range req.UserIDs {
ms = append(ms, &admindb.RegisterAddFriend{
UserID: userID,
CreateTime: now,
})
}
if err := o.Database.DelDefaultFriend(ctx, req.UserIDs); err != nil {
return nil, err
}
return &admin.DelDefaultFriendResp{}, nil
}
func (o *adminServer) FindDefaultFriend(ctx context.Context, req *admin.FindDefaultFriendReq) (*admin.FindDefaultFriendResp, error) {
if _, _, err := mctx.Check(ctx); err != nil {
return nil, err
}
userIDs, err := o.Database.FindDefaultFriend(ctx, nil)
if err != nil {
return nil, err
}
return &admin.FindDefaultFriendResp{UserIDs: userIDs}, nil
}
func (o *adminServer) SearchDefaultFriend(ctx context.Context, req *admin.SearchDefaultFriendReq) (*admin.SearchDefaultFriendResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
total, infos, err := o.Database.SearchDefaultFriend(ctx, req.Keyword, req.Pagination)
if err != nil {
return nil, err
}
userIDs := datautil.Slice(infos, func(info *admindb.RegisterAddFriend) string { return info.UserID })
userMap, err := o.Chat.MapUserPublicInfo(ctx, userIDs)
if err != nil {
return nil, err
}
attributes := make([]*admin.DefaultFriendAttribute, 0, len(infos))
for _, info := range infos {
attribute := &admin.DefaultFriendAttribute{
UserID: info.UserID,
CreateTime: info.CreateTime.UnixMilli(),
User: userMap[info.UserID],
}
attributes = append(attributes, attribute)
}
return &admin.SearchDefaultFriendResp{Total: uint32(total), Users: attributes}, nil
}

View File

@@ -0,0 +1,112 @@
// 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"
"time"
"github.com/openimsdk/tools/utils/datautil"
"github.com/openimsdk/tools/errs"
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/protocol/admin"
)
func (o *adminServer) AddDefaultGroup(ctx context.Context, req *admin.AddDefaultGroupReq) (*admin.AddDefaultGroupResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if len(req.GroupIDs) == 0 {
return nil, errs.ErrArgs.WrapMsg("group ids is empty")
}
if datautil.Duplicate(req.GroupIDs) {
return nil, errs.ErrArgs.WrapMsg("group ids is duplicate")
}
exists, err := o.Database.FindDefaultGroup(ctx, req.GroupIDs)
if err != nil {
return nil, err
}
if len(exists) > 0 {
return nil, errs.ErrDuplicateKey.WrapMsg("group id existed", "groupID", exists)
}
now := time.Now()
ms := make([]*admindb.RegisterAddGroup, 0, len(req.GroupIDs))
for _, groupID := range req.GroupIDs {
ms = append(ms, &admindb.RegisterAddGroup{
GroupID: groupID,
CreateTime: now,
})
}
if err := o.Database.AddDefaultGroup(ctx, ms); err != nil {
return nil, err
}
return &admin.AddDefaultGroupResp{}, nil
}
func (o *adminServer) DelDefaultGroup(ctx context.Context, req *admin.DelDefaultGroupReq) (*admin.DelDefaultGroupResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if len(req.GroupIDs) == 0 {
return nil, errs.ErrArgs.WrapMsg("group ids is empty")
}
if datautil.Duplicate(req.GroupIDs) {
return nil, errs.ErrArgs.WrapMsg("group ids is duplicate")
}
exists, err := o.Database.FindDefaultGroup(ctx, req.GroupIDs)
if err != nil {
return nil, err
}
if ids := datautil.Single(req.GroupIDs, exists); len(ids) > 0 {
return nil, errs.ErrRecordNotFound.WrapMsg("group id not found", "groupID", ids)
}
now := time.Now()
ms := make([]*admindb.RegisterAddGroup, 0, len(req.GroupIDs))
for _, groupID := range req.GroupIDs {
ms = append(ms, &admindb.RegisterAddGroup{
GroupID: groupID,
CreateTime: now,
})
}
if err := o.Database.DelDefaultGroup(ctx, req.GroupIDs); err != nil {
return nil, err
}
return &admin.DelDefaultGroupResp{}, nil
}
func (o *adminServer) FindDefaultGroup(ctx context.Context, req *admin.FindDefaultGroupReq) (*admin.FindDefaultGroupResp, error) {
if _, _, err := mctx.Check(ctx); err != nil {
return nil, err
}
groupIDs, err := o.Database.FindDefaultGroup(ctx, nil)
if err != nil {
return nil, err
}
return &admin.FindDefaultGroupResp{GroupIDs: groupIDs}, nil
}
func (o *adminServer) SearchDefaultGroup(ctx context.Context, req *admin.SearchDefaultGroupReq) (*admin.SearchDefaultGroupResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
total, infos, err := o.Database.SearchDefaultGroup(ctx, req.Keyword, req.Pagination)
if err != nil {
return nil, err
}
return &admin.SearchDefaultGroupResp{Total: uint32(total), GroupIDs: datautil.Slice(infos, func(info *admindb.RegisterAddGroup) string { return info.GroupID })}, nil
}

View File

@@ -0,0 +1,102 @@
// 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"
chatdb "git.imall.cloud/openim/chat/pkg/common/db/table/chat"
"git.imall.cloud/openim/chat/pkg/common/mctx"
"git.imall.cloud/openim/chat/pkg/protocol/admin"
"github.com/openimsdk/tools/errs"
)
// ==================== 定时任务管理相关 RPC ====================
// GetScheduledTasks 获取定时任务列表(管理员接口,可查看所有任务)
func (o *adminServer) GetScheduledTasks(ctx context.Context, req *admin.GetScheduledTasksReq) (*admin.GetScheduledTasksResp, error) {
// 验证管理员权限
_, err := mctx.CheckAdmin(ctx)
if err != nil {
return nil, err
}
// 获取所有定时任务列表
total, tasks, err := o.ChatDatabase.GetAllScheduledTasks(ctx, req.Pagination)
if err != nil {
return nil, err
}
// 转换为响应格式
taskInfos := make([]*admin.ScheduledTaskInfo, 0, len(tasks))
for _, task := range tasks {
taskInfos = append(taskInfos, convertScheduledTaskToAdminProto(task))
}
return &admin.GetScheduledTasksResp{
Total: total,
Tasks: taskInfos,
}, nil
}
// DeleteScheduledTask 删除定时任务(管理员接口,可删除任何任务)
func (o *adminServer) DeleteScheduledTask(ctx context.Context, req *admin.DeleteScheduledTaskReq) (*admin.DeleteScheduledTaskResp, error) {
// 验证管理员权限
_, err := mctx.CheckAdmin(ctx)
if err != nil {
return nil, err
}
// 验证请求参数
if len(req.TaskIDs) == 0 {
return nil, errs.ErrArgs.WrapMsg("taskIDs is required")
}
// 删除任务
if err := o.ChatDatabase.DeleteScheduledTask(ctx, req.TaskIDs); err != nil {
return nil, err
}
return &admin.DeleteScheduledTaskResp{}, nil
}
// convertScheduledTaskToAdminProto 将数据库模型转换为 admin protobuf 消息
func convertScheduledTaskToAdminProto(task *chatdb.ScheduledTask) *admin.ScheduledTaskInfo {
messages := make([]*admin.ScheduledTaskMessage, 0, len(task.Messages))
for _, msg := range task.Messages {
messages = append(messages, &admin.ScheduledTaskMessage{
Type: msg.Type,
Content: msg.Content,
Thumbnail: msg.Thumbnail,
Duration: msg.Duration,
FileSize: msg.FileSize,
Width: msg.Width,
Height: msg.Height,
})
}
return &admin.ScheduledTaskInfo{
Id: task.ID,
UserID: task.UserID,
Name: task.Name,
CronExpression: task.CronExpression,
Messages: messages,
RecvIDs: task.RecvIDs,
GroupIDs: task.GroupIDs,
Status: task.Status,
CreateTime: task.CreateTime.UnixMilli(),
UpdateTime: task.UpdateTime.UnixMilli(),
}
}

110
internal/rpc/admin/start.go Normal file
View File

@@ -0,0 +1,110 @@
package admin
import (
"context"
"crypto/md5"
"encoding/hex"
"math/rand"
"time"
"git.imall.cloud/openim/chat/pkg/common/config"
"git.imall.cloud/openim/chat/pkg/common/constant"
"git.imall.cloud/openim/chat/pkg/common/db/database"
"git.imall.cloud/openim/chat/pkg/common/db/dbutil"
"git.imall.cloud/openim/chat/pkg/common/db/table/admin"
"git.imall.cloud/openim/chat/pkg/common/imapi"
"git.imall.cloud/openim/chat/pkg/common/tokenverify"
adminpb "git.imall.cloud/openim/chat/pkg/protocol/admin"
"git.imall.cloud/openim/chat/pkg/protocol/chat"
chatClient "git.imall.cloud/openim/chat/pkg/rpclient/chat"
"github.com/openimsdk/tools/db/mongoutil"
"github.com/openimsdk/tools/db/redisutil"
"github.com/openimsdk/tools/discovery"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/mw"
"github.com/openimsdk/tools/utils/runtimeenv"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type Config struct {
RpcConfig config.Admin
RedisConfig config.Redis
MongodbConfig config.Mongo
Discovery config.Discovery
Share config.Share
RuntimeEnv string
}
func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server *grpc.Server) error {
config.RuntimeEnv = runtimeenv.PrintRuntimeEnvironment()
if len(config.Share.ChatAdmin) == 0 {
return errs.New("share chat admin not configured")
}
rand.Seed(time.Now().UnixNano())
rdb, err := redisutil.NewRedisClient(ctx, config.RedisConfig.Build())
if err != nil {
return err
}
mgocli, err := mongoutil.NewMongoDB(ctx, config.MongodbConfig.Build())
if err != nil {
return err
}
var srv adminServer
srv.Database, err = database.NewAdminDatabase(mgocli, rdb)
if err != nil {
return err
}
srv.ChatDatabase, err = database.NewChatDatabase(mgocli, rdb)
if err != nil {
return err
}
conn, err := client.GetConn(ctx, config.Discovery.RpcService.Chat, grpc.WithTransportCredentials(insecure.NewCredentials()), mw.GrpcClient())
if err != nil {
return err
}
srv.Chat = chatClient.NewChatClient(chat.NewChatClient(conn))
srv.Token = &tokenverify.Token{
Expires: time.Duration(config.RpcConfig.TokenPolicy.Expire) * time.Hour * 24,
Secret: config.RpcConfig.Secret,
}
srv.ImApiCaller = imapi.New(config.Share.OpenIM.ApiURL, config.Share.OpenIM.Secret, config.Share.OpenIM.AdminUserID)
if err := srv.initAdmin(ctx, config.Share.ChatAdmin, config.Share.OpenIM.AdminUserID); err != nil {
return err
}
adminpb.RegisterAdminServer(server, &srv)
return nil
}
type adminServer struct {
adminpb.UnimplementedAdminServer
Database database.AdminDatabaseInterface
ChatDatabase database.ChatDatabaseInterface
Chat *chatClient.ChatClient
Token *tokenverify.Token
ImApiCaller imapi.CallerInterface
}
func (o *adminServer) initAdmin(ctx context.Context, admins []string, imUserID string) error {
for _, account := range admins {
if _, err := o.Database.GetAdmin(ctx, account); err == nil {
continue
} else if !dbutil.IsDBNotFound(err) {
return err
}
sum := md5.Sum([]byte(account))
a := admin.Admin{
Account: account,
UserID: imUserID,
Password: hex.EncodeToString(sum[:]),
Level: constant.DefaultAdminLevel,
CreateTime: time.Now(),
}
if err := o.Database.AddAdminAccount(ctx, []*admin.Admin{&a}); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,439 @@
// 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"
"encoding/json"
"fmt"
"strconv"
"time"
chatdb "git.imall.cloud/openim/chat/pkg/common/db/table/chat"
"git.imall.cloud/openim/chat/pkg/common/mctx"
adminpb "git.imall.cloud/openim/chat/pkg/protocol/admin"
"github.com/openimsdk/tools/errs"
)
// ==================== 系统配置管理相关 RPC ====================
// convertValueToString 将任意类型的值转换为字符串(根据 ValueType
// 支持接收字符串、数字、布尔值、JSON对象
func convertValueToString(value interface{}, valueType int32) (string, error) {
if value == nil {
return "", errs.ErrArgs.WrapMsg("value cannot be nil")
}
switch valueType {
case chatdb.ConfigValueTypeString:
// 字符串类型:直接转换为字符串
switch v := value.(type) {
case string:
return v, nil
default:
// 其他类型转为字符串
return fmt.Sprintf("%v", v), nil
}
case chatdb.ConfigValueTypeNumber:
// 数字类型:转换为数字字符串
switch v := value.(type) {
case string:
// 验证是否为有效数字
if _, err := strconv.ParseFloat(v, 64); err != nil {
return "", errs.ErrArgs.WrapMsg("value must be a valid number")
}
return v, nil
case float64:
return strconv.FormatFloat(v, 'f', -1, 64), nil
case float32:
return strconv.FormatFloat(float64(v), 'f', -1, 32), nil
case int:
return strconv.Itoa(v), nil
case int64:
return strconv.FormatInt(v, 10), nil
case int32:
return strconv.FormatInt(int64(v), 10), nil
default:
// 尝试转换为数字
if num, ok := v.(float64); ok {
return strconv.FormatFloat(num, 'f', -1, 64), nil
}
return "", errs.ErrArgs.WrapMsg("value must be a number")
}
case chatdb.ConfigValueTypeBool:
// 布尔类型:转换为 "true" 或 "false"
switch v := value.(type) {
case string:
// 验证是否为有效的布尔字符串
if _, err := strconv.ParseBool(v); err != nil {
return "", errs.ErrArgs.WrapMsg("value must be 'true' or 'false'")
}
return v, nil
case bool:
return strconv.FormatBool(v), nil
default:
return "", errs.ErrArgs.WrapMsg("value must be a boolean")
}
case chatdb.ConfigValueTypeJSON:
// JSON类型转换为 JSON 字符串
switch v := value.(type) {
case string:
// 验证是否为有效的 JSON
var js interface{}
if err := json.Unmarshal([]byte(v), &js); err != nil {
return "", errs.ErrArgs.WrapMsg("value must be a valid JSON string")
}
return v, nil
default:
// 将对象序列化为 JSON 字符串
jsonBytes, err := json.Marshal(v)
if err != nil {
return "", errs.ErrArgs.WrapMsg("value must be a valid JSON object")
}
return string(jsonBytes), nil
}
default:
return "", errs.ErrArgs.WrapMsg("invalid value type")
}
}
// convertValueFromString 将字符串值转换为对应类型(用于返回给前端)
func convertValueFromString(value string, valueType int32) (interface{}, error) {
switch valueType {
case chatdb.ConfigValueTypeString:
return value, nil
case chatdb.ConfigValueTypeNumber:
// 尝试解析为数字
if num, err := strconv.ParseFloat(value, 64); err == nil {
// 如果是整数,返回整数;否则返回浮点数
if num == float64(int64(num)) {
return int64(num), nil
}
return num, nil
}
return nil, errs.ErrArgs.WrapMsg("invalid number format")
case chatdb.ConfigValueTypeBool:
return strconv.ParseBool(value)
case chatdb.ConfigValueTypeJSON:
var js interface{}
if err := json.Unmarshal([]byte(value), &js); err != nil {
return nil, err
}
return js, nil
default:
return value, nil
}
}
// validateValueByType 根据 ValueType 验证 Value 的格式
func validateValueByType(value string, valueType int32) error {
switch valueType {
case chatdb.ConfigValueTypeString:
// 字符串类型:任何字符串都可以,无需验证
return nil
case chatdb.ConfigValueTypeNumber:
// 数字类型:必须是有效的数字字符串
if value == "" {
return errs.ErrArgs.WrapMsg("value cannot be empty for number type")
}
_, err := strconv.ParseFloat(value, 64)
if err != nil {
return errs.ErrArgs.WrapMsg("value must be a valid number string")
}
return nil
case chatdb.ConfigValueTypeBool:
// 布尔类型:必须是 "true" 或 "false"
if value == "" {
return errs.ErrArgs.WrapMsg("value cannot be empty for bool type")
}
_, err := strconv.ParseBool(value)
if err != nil {
return errs.ErrArgs.WrapMsg("value must be 'true' or 'false' for bool type")
}
return nil
case chatdb.ConfigValueTypeJSON:
// JSON类型必须是有效的 JSON 字符串
if value == "" {
return errs.ErrArgs.WrapMsg("value cannot be empty for JSON type")
}
var js interface{}
if err := json.Unmarshal([]byte(value), &js); err != nil {
return errs.ErrArgs.WrapMsg("value must be a valid JSON string")
}
return nil
default:
return errs.ErrArgs.WrapMsg("invalid value type")
}
}
// CreateSystemConfig 创建系统配置
func (o *adminServer) CreateSystemConfig(ctx context.Context, req *adminpb.CreateSystemConfigReq) (*adminpb.CreateSystemConfigResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
// 验证必填字段
if req.Key == "" {
return nil, errs.ErrArgs.WrapMsg("config key is required")
}
// 检查配置键是否已存在
_, err := o.ChatDatabase.GetSystemConfig(ctx, req.Key)
if err == nil {
return nil, errs.ErrDuplicateKey.WrapMsg("config key already exists")
}
// 创建配置对象
config := &chatdb.SystemConfig{
Key: req.Key,
Title: req.Title,
Value: req.Value,
ValueType: req.ValueType,
Description: req.Description,
Enabled: req.Enabled,
ShowInApp: req.ShowInApp,
CreateTime: time.Now(),
UpdateTime: time.Now(),
}
// 如果未设置值类型,默认为字符串类型
if config.ValueType == 0 {
config.ValueType = chatdb.ConfigValueTypeString
}
// 根据 ValueType 验证 Value 的格式
if err := validateValueByType(config.Value, config.ValueType); err != nil {
return nil, err
}
// 保存到数据库
if err := o.ChatDatabase.CreateSystemConfig(ctx, config); err != nil {
return nil, err
}
return &adminpb.CreateSystemConfigResp{}, nil
}
// GetSystemConfig 获取系统配置详情
func (o *adminServer) GetSystemConfig(ctx context.Context, req *adminpb.GetSystemConfigReq) (*adminpb.GetSystemConfigResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
// 获取配置
config, err := o.ChatDatabase.GetSystemConfig(ctx, req.Key)
if err != nil {
return nil, err
}
return &adminpb.GetSystemConfigResp{
Config: convertSystemConfigToProto(config),
}, nil
}
// GetAllSystemConfigs 获取所有系统配置(分页)
func (o *adminServer) GetAllSystemConfigs(ctx context.Context, req *adminpb.GetAllSystemConfigsReq) (*adminpb.GetAllSystemConfigsResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
// 获取配置列表
total, configs, err := o.ChatDatabase.GetAllSystemConfigs(ctx, req.Pagination)
if err != nil {
return nil, err
}
// 转换为响应格式
configInfos := make([]*adminpb.SystemConfigInfo, 0, len(configs))
for _, config := range configs {
configInfos = append(configInfos, convertSystemConfigToProto(config))
}
return &adminpb.GetAllSystemConfigsResp{
Total: uint32(total),
List: configInfos,
}, nil
}
// UpdateSystemConfig 更新系统配置
func (o *adminServer) UpdateSystemConfig(ctx context.Context, req *adminpb.UpdateSystemConfigReq) (*adminpb.UpdateSystemConfigResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
// 验证配置是否存在
_, err := o.ChatDatabase.GetSystemConfig(ctx, req.Key)
if err != nil {
return nil, err
}
// 获取当前配置,用于验证
currentConfig, err := o.ChatDatabase.GetSystemConfig(ctx, req.Key)
if err != nil {
return nil, err
}
// 确定要使用的 ValueType如果更新了 ValueType使用新的否则使用当前的
newValueType := currentConfig.ValueType
if req.ValueType != nil {
newValueType = req.ValueType.Value
}
// 确定要使用的 Value如果更新了 Value使用新的否则使用当前的
newValue := currentConfig.Value
if req.Value != nil {
newValue = req.Value.Value
// 如果更新了 Value需要根据 ValueType 验证
if err := validateValueByType(newValue, newValueType); err != nil {
return nil, err
}
} else if req.ValueType != nil {
// 如果只更新了 ValueType需要验证当前 Value 是否符合新的 ValueType
if err := validateValueByType(currentConfig.Value, newValueType); err != nil {
return nil, err
}
}
// 构建更新数据
updateData := make(map[string]any)
if req.Title != nil {
updateData["title"] = req.Title.Value
}
if req.Value != nil {
updateData["value"] = req.Value.Value
}
if req.ValueType != nil {
updateData["value_type"] = req.ValueType.Value
}
if req.Description != nil {
updateData["description"] = req.Description.Value
}
if req.Enabled != nil {
updateData["enabled"] = req.Enabled.Value
}
if req.ShowInApp != nil {
updateData["show_in_app"] = req.ShowInApp.Value
}
// 更新配置
if err := o.ChatDatabase.UpdateSystemConfig(ctx, req.Key, updateData); err != nil {
return nil, err
}
return &adminpb.UpdateSystemConfigResp{}, nil
}
// UpdateSystemConfigValue 更新系统配置值
func (o *adminServer) UpdateSystemConfigValue(ctx context.Context, req *adminpb.UpdateSystemConfigValueReq) (*adminpb.UpdateSystemConfigValueResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
// 获取当前配置,用于获取 ValueType 进行验证
config, err := o.ChatDatabase.GetSystemConfig(ctx, req.Key)
if err != nil {
return nil, err
}
// 根据当前 ValueType 验证新 Value 的格式
if err := validateValueByType(req.Value, config.ValueType); err != nil {
return nil, err
}
// 更新配置值
if err := o.ChatDatabase.UpdateSystemConfigValue(ctx, req.Key, req.Value); err != nil {
return nil, err
}
return &adminpb.UpdateSystemConfigValueResp{}, nil
}
// UpdateSystemConfigEnabled 更新系统配置启用状态
func (o *adminServer) UpdateSystemConfigEnabled(ctx context.Context, req *adminpb.UpdateSystemConfigEnabledReq) (*adminpb.UpdateSystemConfigEnabledResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
// 更新启用状态
if err := o.ChatDatabase.UpdateSystemConfigEnabled(ctx, req.Key, req.Enabled); err != nil {
return nil, err
}
return &adminpb.UpdateSystemConfigEnabledResp{}, nil
}
// DeleteSystemConfig 删除系统配置
func (o *adminServer) DeleteSystemConfig(ctx context.Context, req *adminpb.DeleteSystemConfigReq) (*adminpb.DeleteSystemConfigResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
// 删除配置
if err := o.ChatDatabase.DeleteSystemConfig(ctx, req.Keys); err != nil {
return nil, err
}
return &adminpb.DeleteSystemConfigResp{}, nil
}
// GetEnabledSystemConfigs 获取所有已启用的配置
func (o *adminServer) GetEnabledSystemConfigs(ctx context.Context, req *adminpb.GetEnabledSystemConfigsReq) (*adminpb.GetEnabledSystemConfigsResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
// 获取已启用的配置
configs, err := o.ChatDatabase.GetEnabledSystemConfigs(ctx)
if err != nil {
return nil, err
}
// 转换为响应格式
configInfos := make([]*adminpb.SystemConfigInfo, 0, len(configs))
for _, config := range configs {
configInfos = append(configInfos, convertSystemConfigToProto(config))
}
return &adminpb.GetEnabledSystemConfigsResp{
List: configInfos,
}, nil
}
// convertSystemConfigToProto 将数据库模型转换为 protobuf 消息
func convertSystemConfigToProto(config *chatdb.SystemConfig) *adminpb.SystemConfigInfo {
return &adminpb.SystemConfigInfo{
Key: config.Key,
Title: config.Title,
Value: config.Value,
ValueType: config.ValueType,
Description: config.Description,
Enabled: config.Enabled,
ShowInApp: config.ShowInApp,
CreateTime: config.CreateTime.UnixMilli(),
UpdateTime: config.UpdateTime.UnixMilli(),
}
}

View File

@@ -0,0 +1,79 @@
// 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"
"github.com/redis/go-redis/v9"
"git.imall.cloud/openim/chat/pkg/eerrs"
adminpb "git.imall.cloud/openim/chat/pkg/protocol/admin"
"github.com/openimsdk/tools/log"
)
func (o *adminServer) CreateToken(ctx context.Context, req *adminpb.CreateTokenReq) (*adminpb.CreateTokenResp, error) {
token, expire, err := o.Token.CreateToken(req.UserID, req.UserType)
if err != nil {
return nil, err
}
err = o.Database.CacheToken(ctx, req.UserID, token, expire)
if err != nil {
return nil, err
}
return &adminpb.CreateTokenResp{
Token: token,
}, nil
}
func (o *adminServer) ParseToken(ctx context.Context, req *adminpb.ParseTokenReq) (*adminpb.ParseTokenResp, error) {
userID, userType, err := o.Token.GetToken(req.Token)
if err != nil {
return nil, err
}
m, err := o.Database.GetTokens(ctx, userID)
if err != nil && err != redis.Nil {
return nil, err
}
if len(m) == 0 {
return nil, eerrs.ErrTokenNotExist.Wrap()
}
if _, ok := m[req.Token]; !ok {
return nil, eerrs.ErrTokenNotExist.Wrap()
}
return &adminpb.ParseTokenResp{
UserID: userID,
UserType: userType,
}, nil
}
func (o *adminServer) GetUserToken(ctx context.Context, req *adminpb.GetUserTokenReq) (*adminpb.GetUserTokenResp, error) {
tokensMap, err := o.Database.GetTokens(ctx, req.UserID)
if err != nil {
return nil, err
}
return &adminpb.GetUserTokenResp{TokensMap: tokensMap}, nil
}
func (o *adminServer) InvalidateToken(ctx context.Context, req *adminpb.InvalidateTokenReq) (*adminpb.InvalidateTokenResp, error) {
err := o.Database.DeleteToken(ctx, req.UserID)
if err != nil && err != redis.Nil {
return nil, err
}
log.ZDebug(ctx, "delete token from redis", "userID", req.UserID)
return &adminpb.InvalidateTokenResp{}, nil
}

View File

@@ -0,0 +1,140 @@
// 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 (
"crypto/md5"
"encoding/hex"
"time"
"github.com/openimsdk/tools/errs"
"git.imall.cloud/openim/chat/pkg/protocol/admin"
)
type Admin struct {
Account string `gorm:"column:account;primary_key;type:char(64)"`
Password string `gorm:"column:password;type:char(64)"`
FaceURL string `gorm:"column:face_url;type:char(64)"`
Nickname string `gorm:"column:nickname;type:char(64)"`
UserID string `gorm:"column:user_id;type:char(64)"` // openIM userID
Level int32 `gorm:"column:level;default:1" `
CreateTime time.Time `gorm:"column:create_time"`
}
func ToDBAdminUpdate(req *admin.AdminUpdateInfoReq) (map[string]any, error) {
update := make(map[string]any)
if req.Account != nil {
if req.Account.Value == "" {
return nil, errs.ErrArgs.WrapMsg("account is empty")
}
update["account"] = req.Account.Value
}
if req.Password != nil {
if req.Password.Value == "" {
return nil, errs.ErrArgs.WrapMsg("password is empty")
}
update["password"] = req.Password.Value
}
if req.FaceURL != nil {
update["face_url"] = req.FaceURL.Value
}
if req.Nickname != nil {
if req.Nickname.Value == "" {
return nil, errs.ErrArgs.WrapMsg("nickname is empty")
}
update["nickname"] = req.Nickname.Value
}
//if req.UserID != nil {
// update["user_id"] = req.UserID.Value
//}
if req.Level != nil {
update["level"] = req.Level.Value
}
if req.GoogleAuthKey != nil {
update["google_auth_key"] = req.GoogleAuthKey.Value
}
if req.OperationPassword != nil {
update["operation_password"] = req.OperationPassword.Value
}
if len(update) == 0 {
return nil, errs.ErrArgs.WrapMsg("no update info")
}
return update, nil
}
func ToDBAdminUpdatePassword(password string) (map[string]any, error) {
if password == "" {
return nil, errs.ErrArgs.WrapMsg("password is empty")
}
return map[string]any{"password": password}, nil
}
func ToDBAppletUpdate(req *admin.UpdateAppletReq) (map[string]any, error) {
update := make(map[string]any)
if req.Name != nil {
if req.Name.Value == "" {
return nil, errs.ErrArgs.WrapMsg("name is empty")
}
update["name"] = req.Name.Value
}
if req.AppID != nil {
if req.AppID.Value == "" {
return nil, errs.ErrArgs.WrapMsg("appID is empty")
}
update["app_id"] = req.AppID.Value
}
if req.Icon != nil {
update["icon"] = req.Icon.Value
}
if req.Url != nil {
if req.Url.Value == "" {
return nil, errs.ErrArgs.WrapMsg("url is empty")
}
update["url"] = req.Url.Value
}
if req.Md5 != nil {
if hash, _ := hex.DecodeString(req.Md5.Value); len(hash) != md5.Size {
return nil, errs.ErrArgs.WrapMsg("md5 is invalid")
}
update["md5"] = req.Md5.Value
}
if req.Size != nil {
if req.Size.Value <= 0 {
return nil, errs.ErrArgs.WrapMsg("size is invalid")
}
update["size"] = req.Size.Value
}
if req.Version != nil {
if req.Version.Value == "" {
return nil, errs.ErrArgs.WrapMsg("version is empty")
}
update["version"] = req.Version.Value
}
if req.Priority != nil {
update["priority"] = req.Priority.Value
}
if req.Status != nil {
update["status"] = req.Status.Value
}
if len(update) == 0 {
return nil, errs.ErrArgs.WrapMsg("no update info")
}
return update, nil
}
func ToDBInvitationRegisterUpdate(userID string) map[string]any {
return map[string]any{"user_id": userID}
}

147
internal/rpc/admin/user.go Normal file
View File

@@ -0,0 +1,147 @@
// 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"
"strings"
"time"
"git.imall.cloud/openim/protocol/wrapperspb"
"github.com/openimsdk/tools/utils/datautil"
"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/protocol/admin"
"git.imall.cloud/openim/chat/pkg/protocol/chat"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/mcontext"
)
func (o *adminServer) CancellationUser(ctx context.Context, req *admin.CancellationUserReq) (*admin.CancellationUserResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
empty := wrapperspb.String("")
update := &chat.UpdateUserInfoReq{UserID: req.UserID, Account: empty, AreaCode: empty, PhoneNumber: empty, Email: empty}
if err := o.Chat.UpdateUser(ctx, update); err != nil {
return nil, err
}
return &admin.CancellationUserResp{}, nil
}
func (o *adminServer) BlockUser(ctx context.Context, req *admin.BlockUserReq) (*admin.BlockUserResp, error) {
_, err := mctx.CheckAdmin(ctx)
if err != nil {
return nil, err
}
_, err = o.Database.GetBlockInfo(ctx, req.UserID)
if err == nil {
return nil, errs.ErrArgs.WrapMsg("user already blocked")
} else if !dbutil.IsDBNotFound(err) {
return nil, err
}
t := &admindb.ForbiddenAccount{
UserID: req.UserID,
Reason: req.Reason,
OperatorUserID: mcontext.GetOpUserID(ctx),
CreateTime: time.Now(),
}
if err := o.Database.BlockUser(ctx, []*admindb.ForbiddenAccount{t}); err != nil {
return nil, err
}
return &admin.BlockUserResp{}, nil
}
func (o *adminServer) UnblockUser(ctx context.Context, req *admin.UnblockUserReq) (*admin.UnblockUserResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if len(req.UserIDs) == 0 {
return nil, errs.ErrArgs.WrapMsg("empty user id")
}
if datautil.Duplicate(req.UserIDs) {
return nil, errs.ErrArgs.WrapMsg("duplicate user id")
}
bs, err := o.Database.FindBlockInfo(ctx, req.UserIDs)
if err != nil {
return nil, err
}
if len(req.UserIDs) != len(bs) {
ids := datautil.Single(req.UserIDs, datautil.Slice(bs, func(info *admindb.ForbiddenAccount) string { return info.UserID }))
return nil, errs.ErrArgs.WrapMsg("user not blocked " + strings.Join(ids, ", "))
}
if err := o.Database.DelBlockUser(ctx, req.UserIDs); err != nil {
return nil, err
}
return &admin.UnblockUserResp{}, nil
}
func (o *adminServer) SearchBlockUser(ctx context.Context, req *admin.SearchBlockUserReq) (*admin.SearchBlockUserResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
total, infos, err := o.Database.SearchBlockUser(ctx, req.Keyword, req.Pagination)
if err != nil {
return nil, err
}
userIDs := datautil.Slice(infos, func(info *admindb.ForbiddenAccount) string { return info.UserID })
userMap, err := o.Chat.MapUserFullInfo(ctx, userIDs)
if err != nil {
return nil, err
}
users := make([]*admin.BlockUserInfo, 0, len(infos))
for _, info := range infos {
user := &admin.BlockUserInfo{
UserID: info.UserID,
Reason: info.Reason,
OpUserID: info.OperatorUserID,
CreateTime: info.CreateTime.UnixMilli(),
}
if userFull := userMap[info.UserID]; userFull != nil {
user.Account = userFull.Account
user.PhoneNumber = userFull.PhoneNumber
user.AreaCode = userFull.AreaCode
user.Email = userFull.Email
user.Nickname = userFull.Nickname
user.FaceURL = userFull.FaceURL
user.Gender = userFull.Gender
}
users = append(users, user)
}
return &admin.SearchBlockUserResp{Total: uint32(total), Users: users}, nil
}
func (o *adminServer) FindUserBlockInfo(ctx context.Context, req *admin.FindUserBlockInfoReq) (*admin.FindUserBlockInfoResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
list, err := o.Database.FindBlockUser(ctx, req.UserIDs)
if err != nil {
return nil, err
}
blocks := make([]*admin.BlockInfo, 0, len(list))
for _, info := range list {
blocks = append(blocks, &admin.BlockInfo{
UserID: info.UserID,
Reason: info.Reason,
OpUserID: info.OperatorUserID,
CreateTime: info.CreateTime.UnixMilli(),
})
}
return &admin.FindUserBlockInfoResp{Blocks: blocks}, nil
}

View File

@@ -0,0 +1,97 @@
// 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"
"time"
"github.com/openimsdk/tools/utils/datautil"
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/protocol/admin"
"github.com/openimsdk/tools/errs"
)
func (o *adminServer) SearchUserIPLimitLogin(ctx context.Context, req *admin.SearchUserIPLimitLoginReq) (*admin.SearchUserIPLimitLoginResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
total, list, err := o.Database.SearchUserLimitLogin(ctx, req.Keyword, req.Pagination)
if err != nil {
return nil, err
}
userIDs := datautil.Slice(list, func(info *admindb.LimitUserLoginIP) string { return info.UserID })
userMap, err := o.Chat.MapUserPublicInfo(ctx, datautil.Distinct(userIDs))
if err != nil {
return nil, err
}
limits := make([]*admin.LimitUserLoginIP, 0, len(list))
for _, info := range list {
limits = append(limits, &admin.LimitUserLoginIP{
UserID: info.UserID,
Ip: info.IP,
CreateTime: info.CreateTime.UnixMilli(),
User: userMap[info.UserID],
})
}
return &admin.SearchUserIPLimitLoginResp{Total: uint32(total), Limits: limits}, nil
}
func (o *adminServer) AddUserIPLimitLogin(ctx context.Context, req *admin.AddUserIPLimitLoginReq) (*admin.AddUserIPLimitLoginResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if len(req.Limits) == 0 {
return nil, errs.ErrArgs.WrapMsg("limits is empty")
}
now := time.Now()
ts := make([]*admindb.LimitUserLoginIP, 0, len(req.Limits))
for _, limit := range req.Limits {
ts = append(ts, &admindb.LimitUserLoginIP{
UserID: limit.UserID,
IP: limit.Ip,
CreateTime: now,
})
}
if err := o.Database.AddUserLimitLogin(ctx, ts); err != nil {
return nil, err
}
return &admin.AddUserIPLimitLoginResp{}, nil
}
func (o *adminServer) DelUserIPLimitLogin(ctx context.Context, req *admin.DelUserIPLimitLoginReq) (*admin.DelUserIPLimitLoginResp, error) {
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
if len(req.Limits) == 0 {
return nil, errs.ErrArgs.WrapMsg("limits is empty")
}
ts := make([]*admindb.LimitUserLoginIP, 0, len(req.Limits))
for _, limit := range req.Limits {
if limit.UserID == "" || limit.Ip == "" {
return nil, errs.ErrArgs.WrapMsg("user_id or ip is empty")
}
ts = append(ts, &admindb.LimitUserLoginIP{
UserID: limit.UserID,
IP: limit.Ip,
})
}
if err := o.Database.DelUserLimitLogin(ctx, ts); err != nil {
return nil, err
}
return &admin.DelUserIPLimitLoginResp{}, nil
}

View File

@@ -0,0 +1,982 @@
// 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"
"errors"
"time"
"git.imall.cloud/openim/chat/pkg/common/constant"
chatdb "git.imall.cloud/openim/chat/pkg/common/db/table/chat"
"git.imall.cloud/openim/chat/pkg/common/mctx"
"git.imall.cloud/openim/chat/pkg/eerrs"
adminpb "git.imall.cloud/openim/chat/pkg/protocol/admin"
"git.imall.cloud/openim/protocol/sdkws"
"github.com/google/uuid"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
"go.mongodb.org/mongo-driver/mongo"
)
// ==================== 钱包管理相关 RPC ====================
// GetUserWallet 获取用户钱包信息
func (o *adminServer) GetUserWallet(ctx context.Context, req *adminpb.GetUserWalletReq) (*adminpb.GetUserWalletResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
// 验证必填字段
if req.UserID == "" {
return nil, errs.ErrArgs.WrapMsg("userID is required")
}
// 获取钱包信息
wallet, err := o.ChatDatabase.GetWallet(ctx, req.UserID)
if err != nil {
// 如果钱包不存在,返回默认值
if errors.Is(err, mongo.ErrNoDocuments) {
return &adminpb.GetUserWalletResp{
Wallet: &adminpb.WalletInfo{
UserID: req.UserID,
Balance: 0,
WithdrawAccount: "",
RealNameAuth: nil,
WithdrawReceiveAccount: "",
HasPaymentPassword: false,
CreateTime: 0,
UpdateTime: 0,
},
}, nil
}
return nil, err
}
// 转换实名认证信息
var realNameAuth *adminpb.RealNameAuthInfo
if wallet.RealNameAuth.IDCard != "" {
realNameAuth = &adminpb.RealNameAuthInfo{
IdCard: wallet.RealNameAuth.IDCard,
Name: wallet.RealNameAuth.Name,
IdCardPhotoFront: wallet.RealNameAuth.IDCardPhotoFront,
IdCardPhotoBack: wallet.RealNameAuth.IDCardPhotoBack,
AuditStatus: wallet.RealNameAuth.AuditStatus, // 审核状态0-未审核1-审核通过2-审核拒绝
}
}
return &adminpb.GetUserWalletResp{
Wallet: &adminpb.WalletInfo{
UserID: wallet.UserID,
Balance: wallet.Balance,
WithdrawAccount: wallet.WithdrawAccount,
RealNameAuth: realNameAuth,
WithdrawReceiveAccount: wallet.WithdrawReceiveAccount,
HasPaymentPassword: wallet.PaymentPassword != "",
CreateTime: wallet.CreateTime.UnixMilli(),
UpdateTime: wallet.UpdateTime.UnixMilli(),
},
}, nil
}
// UpdateUserWalletBalance 更新用户余额(后台充值/扣款)
// 使用原子操作防止并发问题
func (o *adminServer) UpdateUserWalletBalance(ctx context.Context, req *adminpb.UpdateUserWalletBalanceReq) (*adminpb.UpdateUserWalletBalanceResp, error) {
// 检查管理员权限
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 update wallet balance")
}
// 检查是否设置了操作密码
if adminUser.OperationPassword == "" {
return nil, errs.ErrNoPermission.WrapMsg("operation password must be set before updating wallet balance")
}
// 验证操作密码
if req.OperationPassword == "" {
return nil, eerrs.ErrPassword.WrapMsg("operation password is required")
}
if adminUser.OperationPassword != req.OperationPassword {
return nil, eerrs.ErrPassword.WrapMsg("operation password is incorrect")
}
// 验证必填字段
if req.UserID == "" {
return nil, errs.ErrArgs.WrapMsg("userID is required")
}
if req.Amount == 0 {
return nil, errs.ErrArgs.WrapMsg("amount cannot be zero")
}
// 使用原子操作更新余额(防止并发问题)
// IncrementBalance 方法已经处理了钱包不存在的情况(通过 upsert
// 如果是扣款且余额不足,会返回明确的错误信息
beforeBalance, afterBalance, err := o.ChatDatabase.IncrementWalletBalance(ctx, req.UserID, req.Amount)
if err != nil {
// 所有错误都直接返回IncrementBalance 已经处理了各种情况:
// 1. 余额不足(扣款时):返回明确的错误信息
// 2. 钱包不存在且充值upsert 会自动创建
// 3. 钱包不存在且扣款:返回余额不足错误
return nil, err
}
// 创建余额变动记录
record := &chatdb.WalletBalanceRecord{
ID: uuid.New().String(),
UserID: req.UserID,
Amount: req.Amount,
Type: req.Type,
BeforeBalance: beforeBalance,
AfterBalance: afterBalance,
Remark: req.Remark,
CreateTime: time.Now(),
}
if err := o.ChatDatabase.CreateWalletBalanceRecord(ctx, record); err != nil {
// 余额变动记录创建失败,记录错误日志
// 注意:余额已经更新,但记录创建失败,这是一个严重的数据不一致问题
// 不返回错误,因为余额已经更新,返回错误会让调用方误以为余额未更新
log.ZError(ctx, "Failed to create wallet balance record", err,
"userID", req.UserID,
"amount", req.Amount,
"beforeBalance", beforeBalance,
"afterBalance", afterBalance,
"type", req.Type,
"remark", req.Remark)
}
return &adminpb.UpdateUserWalletBalanceResp{
Balance: afterBalance,
}, nil
}
// GetUserWalletBalanceRecords 获取用户余额变动记录列表
func (o *adminServer) GetUserWalletBalanceRecords(ctx context.Context, req *adminpb.GetUserWalletBalanceRecordsReq) (*adminpb.GetUserWalletBalanceRecordsResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
// 验证必填字段
if req.UserID == "" {
return nil, errs.ErrArgs.WrapMsg("userID is required")
}
// 获取余额变动记录列表
total, records, err := o.ChatDatabase.GetWalletBalanceRecords(ctx, req.UserID, req.Pagination)
if err != nil {
return nil, err
}
// 转换为响应格式
recordInfos := make([]*adminpb.WalletBalanceRecordInfo, 0, len(records))
for _, record := range records {
recordInfos = append(recordInfos, &adminpb.WalletBalanceRecordInfo{
Id: record.ID,
UserID: record.UserID,
Amount: record.Amount,
Type: record.Type,
BeforeBalance: record.BeforeBalance,
AfterBalance: record.AfterBalance,
OrderID: record.OrderID,
TransactionID: record.TransactionID,
RedPacketID: record.RedPacketID,
Remark: record.Remark,
CreateTime: record.CreateTime.UnixMilli(),
})
}
return &adminpb.GetUserWalletBalanceRecordsResp{
Total: uint32(total),
List: recordInfos,
}, nil
}
// UpdateUserPaymentPassword 修改用户支付密码(后台)
func (o *adminServer) UpdateUserPaymentPassword(ctx context.Context, req *adminpb.UpdateUserPaymentPasswordReq) (*adminpb.UpdateUserPaymentPasswordResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
// 验证必填字段
if req.UserID == "" {
return nil, errs.ErrArgs.WrapMsg("userID is required")
}
if req.PaymentPassword == "" {
return nil, errs.ErrArgs.WrapMsg("paymentPassword is required")
}
// 更新支付密码
if err := o.ChatDatabase.UpdateWalletPaymentPassword(ctx, req.UserID, req.PaymentPassword); err != nil {
return nil, err
}
return &adminpb.UpdateUserPaymentPasswordResp{}, nil
}
// SetUserWithdrawAccount 设置用户提款账号(后台)
func (o *adminServer) SetUserWithdrawAccount(ctx context.Context, req *adminpb.SetUserWithdrawAccountReq) (*adminpb.SetUserWithdrawAccountResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
// 验证必填字段
if req.UserID == "" {
return nil, errs.ErrArgs.WrapMsg("userID is required")
}
if req.WithdrawAccount == "" {
return nil, errs.ErrArgs.WrapMsg("withdrawAccount is required")
}
// 更新提款账号
if err := o.ChatDatabase.UpdateWalletWithdrawAccount(ctx, req.UserID, req.WithdrawAccount); err != nil {
return nil, err
}
return &adminpb.SetUserWithdrawAccountResp{}, nil
}
// ==================== 提现管理相关 RPC操作 withdraw_applications====================
// GetWithdraw 获取提现申请详情
func (o *adminServer) GetWithdraw(ctx context.Context, req *adminpb.GetWithdrawReq) (*adminpb.GetWithdrawResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
// 验证必填字段
if req.WithdrawID == "" {
return nil, errs.ErrArgs.WrapMsg("applicationID is required")
}
// 获取提现申请
application, err := o.ChatDatabase.GetWithdrawApplication(ctx, req.WithdrawID)
if err != nil {
return nil, err
}
// 转换为响应格式
withdrawInfo := &adminpb.WithdrawInfo{
Id: application.ID,
UserID: application.UserID,
Amount: application.Amount,
WithdrawAccount: application.WithdrawAccount,
Status: application.Status,
AuditorID: application.AuditorID,
AuditTime: application.AuditTime.UnixMilli(),
AuditRemark: application.AuditRemark,
Ip: application.IP,
DeviceID: application.DeviceID,
Platform: application.Platform,
DeviceModel: application.DeviceModel,
DeviceBrand: application.DeviceBrand,
OsVersion: application.OSVersion,
AppVersion: application.AppVersion,
CreateTime: application.CreateTime.UnixMilli(),
UpdateTime: application.UpdateTime.UnixMilli(),
}
return &adminpb.GetWithdrawResp{
Withdraw: withdrawInfo,
}, nil
}
// GetUserWithdraws 获取用户的提现申请列表
func (o *adminServer) GetUserWithdraws(ctx context.Context, req *adminpb.GetUserWithdrawsReq) (*adminpb.GetUserWithdrawsResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
// 验证必填字段
if req.UserID == "" {
return nil, errs.ErrArgs.WrapMsg("userID is required")
}
// 获取用户的提现申请列表
total, applications, err := o.ChatDatabase.GetWithdrawApplicationsByUserID(ctx, req.UserID, req.Pagination)
if err != nil {
return nil, err
}
// 查询用户钱包信息(用于获取实名信息)
var wallet *chatdb.Wallet
wallet, err = o.ChatDatabase.GetWallet(ctx, req.UserID)
if err != nil && !errors.Is(err, mongo.ErrNoDocuments) {
log.ZWarn(ctx, "Failed to get wallet for user withdraws", err, "userID", req.UserID)
}
// 转换为响应格式
withdrawInfos := make([]*adminpb.WithdrawInfo, 0, len(applications))
for _, application := range applications {
withdrawInfo := &adminpb.WithdrawInfo{
Id: application.ID,
UserID: application.UserID,
Amount: application.Amount,
WithdrawAccount: application.WithdrawAccount,
Status: application.Status,
AuditorID: application.AuditorID,
AuditTime: application.AuditTime.UnixMilli(),
AuditRemark: application.AuditRemark,
Ip: application.IP,
DeviceID: application.DeviceID,
Platform: application.Platform,
DeviceModel: application.DeviceModel,
DeviceBrand: application.DeviceBrand,
OsVersion: application.OSVersion,
AppVersion: application.AppVersion,
CreateTime: application.CreateTime.UnixMilli(),
UpdateTime: application.UpdateTime.UnixMilli(),
}
// 填充用户实名认证信息
if wallet != nil && wallet.RealNameAuth.IDCard != "" {
withdrawInfo.RealNameAuth = &adminpb.RealNameAuthInfo{
IdCard: wallet.RealNameAuth.IDCard,
Name: wallet.RealNameAuth.Name,
IdCardPhotoFront: wallet.RealNameAuth.IDCardPhotoFront,
IdCardPhotoBack: wallet.RealNameAuth.IDCardPhotoBack,
AuditStatus: wallet.RealNameAuth.AuditStatus, // 审核状态0-未审核1-审核通过2-审核拒绝
}
}
withdrawInfos = append(withdrawInfos, withdrawInfo)
}
return &adminpb.GetUserWithdrawsResp{
Total: uint32(total),
List: withdrawInfos,
}, nil
}
// GetWithdraws 获取提现申请列表(后台,支持按状态筛选)
func (o *adminServer) GetWithdraws(ctx context.Context, req *adminpb.GetWithdrawsReq) (*adminpb.GetWithdrawsResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
var total int64
var applications []*chatdb.WithdrawApplication
var err error
// 如果指定了状态,按状态筛选;否则获取全部
if req.Status > 0 {
total, applications, err = o.ChatDatabase.GetWithdrawApplicationsByStatus(ctx, req.Status, req.Pagination)
} else {
total, applications, err = o.ChatDatabase.GetWithdrawApplicationsPage(ctx, req.Pagination)
}
if err != nil {
return nil, err
}
// 收集所有用户ID批量查询钱包信息用于获取实名信息
userIDs := make([]string, 0, len(applications))
userIDSet := make(map[string]bool)
for _, application := range applications {
if !userIDSet[application.UserID] {
userIDs = append(userIDs, application.UserID)
userIDSet[application.UserID] = true
}
}
// 批量查询钱包信息
walletMap := make(map[string]*chatdb.Wallet)
if len(userIDs) > 0 {
wallets, err := o.ChatDatabase.GetWalletsByUserIDs(ctx, userIDs)
if err != nil {
log.ZWarn(ctx, "Failed to get wallets for withdraw list", err, "userIDs", userIDs)
} else {
for _, wallet := range wallets {
walletMap[wallet.UserID] = wallet
}
}
}
// 转换为响应格式
withdrawInfos := make([]*adminpb.WithdrawInfo, 0, len(applications))
for _, application := range applications {
withdrawInfo := &adminpb.WithdrawInfo{
Id: application.ID,
UserID: application.UserID,
Amount: application.Amount,
WithdrawAccount: application.WithdrawAccount,
Status: application.Status,
AuditorID: application.AuditorID,
AuditTime: application.AuditTime.UnixMilli(),
AuditRemark: application.AuditRemark,
Ip: application.IP,
DeviceID: application.DeviceID,
Platform: application.Platform,
DeviceModel: application.DeviceModel,
DeviceBrand: application.DeviceBrand,
OsVersion: application.OSVersion,
AppVersion: application.AppVersion,
CreateTime: application.CreateTime.UnixMilli(),
UpdateTime: application.UpdateTime.UnixMilli(),
}
// 填充用户实名认证信息
if wallet, ok := walletMap[application.UserID]; ok && wallet.RealNameAuth.IDCard != "" {
withdrawInfo.RealNameAuth = &adminpb.RealNameAuthInfo{
IdCard: wallet.RealNameAuth.IDCard,
Name: wallet.RealNameAuth.Name,
IdCardPhotoFront: wallet.RealNameAuth.IDCardPhotoFront,
IdCardPhotoBack: wallet.RealNameAuth.IDCardPhotoBack,
AuditStatus: wallet.RealNameAuth.AuditStatus, // 审核状态0-未审核1-审核通过2-审核拒绝
}
}
withdrawInfos = append(withdrawInfos, withdrawInfo)
}
return &adminpb.GetWithdrawsResp{
Total: uint32(total),
List: withdrawInfos,
}, nil
}
// AuditWithdraw 批量审核提现申请
func (o *adminServer) AuditWithdraw(ctx context.Context, req *adminpb.AuditWithdrawReq) (*adminpb.AuditWithdrawResp, error) {
// 检查管理员权限
auditorID, err := mctx.CheckAdmin(ctx)
if err != nil {
return nil, err
}
// 验证必填字段
if len(req.WithdrawIDs) == 0 {
return nil, errs.ErrArgs.WrapMsg("withdrawIDs is required and cannot be empty")
}
if req.Status != chatdb.WithdrawApplicationStatusApproved && req.Status != chatdb.WithdrawApplicationStatusRejected {
return nil, errs.ErrArgs.WrapMsg("status must be 2 (approved) or 3 (rejected)")
}
var successCount uint32
var failCount uint32
var failedIDs []string
// 批量处理每个提现申请
for _, withdrawID := range req.WithdrawIDs {
// 获取提现申请
application, err := o.ChatDatabase.GetWithdrawApplication(ctx, withdrawID)
if err != nil {
failCount++
failedIDs = append(failedIDs, withdrawID)
log.ZWarn(ctx, "Get withdraw application failed", err, "withdrawID", withdrawID)
continue
}
// 检查提现申请状态:允许"待审核"和"已通过"状态的提现申请可以被审核
// 已通过的提现申请可以重新审核为拒绝
if application.Status != chatdb.WithdrawApplicationStatusPending && application.Status != chatdb.WithdrawApplicationStatusApproved {
failCount++
failedIDs = append(failedIDs, withdrawID)
log.ZWarn(ctx, "Withdraw application status is not pending or approved", nil, "withdrawID", withdrawID, "status", application.Status)
continue
}
// 更新提现申请状态
if err := o.ChatDatabase.UpdateWithdrawApplicationStatus(ctx, withdrawID, req.Status, auditorID, req.AuditRemark); err != nil {
failCount++
failedIDs = append(failedIDs, withdrawID)
log.ZWarn(ctx, "Update withdraw application status failed", err, "withdrawID", withdrawID)
continue
}
// 如果审核通过,不需要额外操作(因为用户申请时已经扣除了余额)
// 如果审核拒绝(包括从"待审核"改为"已拒绝",或从"已通过"改为"已拒绝"),需要将余额退回给用户
if req.Status == chatdb.WithdrawApplicationStatusRejected {
// 退回余额
beforeBalance, afterBalance, err := o.ChatDatabase.IncrementWalletBalance(ctx, application.UserID, application.Amount)
if err != nil {
// 记录错误但不影响审核状态更新
log.ZError(ctx, "Refund balance failed", err, "withdrawID", withdrawID, "userID", application.UserID, "amount", application.Amount)
} else {
// 创建余额变动记录(退款)
record := &chatdb.WalletBalanceRecord{
ID: uuid.New().String(),
UserID: application.UserID,
Amount: application.Amount,
Type: 4, // 4-退款
BeforeBalance: beforeBalance,
AfterBalance: afterBalance,
Remark: "提现审核拒绝,退回余额",
CreateTime: time.Now(),
}
if err := o.ChatDatabase.CreateWalletBalanceRecord(ctx, record); err != nil {
log.ZWarn(ctx, "Create wallet balance record failed", err, "withdrawID", withdrawID)
}
}
}
successCount++
}
return &adminpb.AuditWithdrawResp{
SuccessCount: successCount,
FailCount: failCount,
FailedIDs: failedIDs,
}, nil
}
// BatchUpdateWalletBalance 批量更新用户余额
func (o *adminServer) BatchUpdateWalletBalance(ctx context.Context, req *adminpb.BatchUpdateWalletBalanceReq) (*adminpb.BatchUpdateWalletBalanceResp, error) {
// 检查管理员权限
adminID, err := mctx.CheckAdmin(ctx)
if err != nil {
return nil, err
}
// 获取管理员信息
adminUser, err := o.Database.GetAdminUserID(ctx, adminID)
if err != nil {
return nil, err
}
// 检查是否为超级管理员level:100
if adminUser.Level != constant.AdvancedUserLevel {
return nil, errs.ErrNoPermission.WrapMsg("only super admin (level:100) can batch update wallet balance")
}
// 检查是否设置了操作密码
if adminUser.OperationPassword == "" {
return nil, errs.ErrNoPermission.WrapMsg("operation password must be set before batch updating wallet balance")
}
// 验证操作密码
if req.OperationPassword == "" {
return nil, eerrs.ErrPassword.WrapMsg("operation password is required")
}
if adminUser.OperationPassword != req.OperationPassword {
return nil, eerrs.ErrPassword.WrapMsg("operation password is incorrect")
}
// 验证必填字段
if len(req.Users) == 0 {
return nil, errs.ErrArgs.WrapMsg("users list cannot be empty")
}
// 验证默认操作类型
defaultOperation := req.Operation
if defaultOperation == "" {
defaultOperation = "add" // 默认为增加
}
if defaultOperation != "set" && defaultOperation != "add" && defaultOperation != "subtract" {
return nil, errs.ErrArgs.WrapMsg("default operation must be one of: set, add, subtract")
}
var results []*adminpb.BatchUpdateResultItem
var successCount uint32
var failedCount uint32
// 批量处理每个用户
for _, userItem := range req.Users {
result := &adminpb.BatchUpdateResultItem{
UserID: userItem.UserID,
PhoneNumber: userItem.PhoneNumber,
Account: userItem.Account,
Remark: userItem.Remark,
}
// 1. 根据提供的标识符查找用户
var targetUserID string
if userItem.UserID != "" {
// 直接使用 userID
targetUserID = userItem.UserID
} else if userItem.PhoneNumber != "" {
// 通过手机号查找用户(假设区号为空或默认)
attr, err := o.ChatDatabase.TakeAttributeByPhone(ctx, "", userItem.PhoneNumber)
if err != nil {
result.Success = false
result.Message = "user not found by phone number"
results = append(results, result)
failedCount++
continue
}
targetUserID = attr.UserID
result.UserID = targetUserID
} else if userItem.Account != "" {
// 通过账号查找用户
attr, err := o.ChatDatabase.TakeAttributeByAccount(ctx, userItem.Account)
if err != nil {
result.Success = false
result.Message = "user not found by account"
results = append(results, result)
failedCount++
continue
}
targetUserID = attr.UserID
result.UserID = targetUserID
} else {
result.Success = false
result.Message = "at least one of userID, phoneNumber, or account must be provided"
results = append(results, result)
failedCount++
continue
}
// 2. 确定使用的金额和操作类型
amount := userItem.Amount
if amount == 0 {
amount = req.Amount
}
operation := userItem.Operation
if operation == "" {
operation = defaultOperation
}
if operation != "set" && operation != "add" && operation != "subtract" {
result.Success = false
result.Message = "operation must be one of: set, add, subtract"
results = append(results, result)
failedCount++
continue
}
// 3. 获取当前余额
wallet, err := o.ChatDatabase.GetWallet(ctx, targetUserID)
var oldBalance int64
if err != nil {
if errors.Is(err, mongo.ErrNoDocuments) {
oldBalance = 0
} else {
result.Success = false
result.Message = "failed to get wallet: " + err.Error()
results = append(results, result)
failedCount++
continue
}
} else {
oldBalance = wallet.Balance
}
result.OldBalance = oldBalance
// 4. 根据操作类型计算新余额和变动金额
var newBalance int64
var incrementAmount int64
var balanceChangeType int32 = 99 // 99-其他(后台批量操作)
switch operation {
case "set":
newBalance = amount
incrementAmount = amount - oldBalance
case "add":
incrementAmount = amount
newBalance = oldBalance + amount
case "subtract":
incrementAmount = -amount
newBalance = oldBalance - amount
}
// 5. 检查余额是否会变为负数
if newBalance < 0 {
result.Success = false
result.Message = "insufficient balance: cannot be negative"
result.NewBalance = oldBalance
results = append(results, result)
failedCount++
continue
}
// 6. 更新余额
beforeBalance, afterBalance, err := o.ChatDatabase.IncrementWalletBalance(ctx, targetUserID, incrementAmount)
if err != nil {
result.Success = false
result.Message = "failed to update balance: " + err.Error()
result.NewBalance = oldBalance
results = append(results, result)
failedCount++
continue
}
result.OldBalance = beforeBalance
result.NewBalance = afterBalance
// 7. 创建余额变动记录
record := &chatdb.WalletBalanceRecord{
ID: uuid.New().String(),
UserID: targetUserID,
Amount: incrementAmount,
Type: balanceChangeType,
BeforeBalance: beforeBalance,
AfterBalance: afterBalance,
Remark: userItem.Remark,
CreateTime: time.Now(),
}
if err := o.ChatDatabase.CreateWalletBalanceRecord(ctx, record); err != nil {
log.ZWarn(ctx, "Create wallet balance record failed", err, "userID", targetUserID)
}
result.Success = true
result.Message = "success"
results = append(results, result)
successCount++
}
return &adminpb.BatchUpdateWalletBalanceResp{
Total: uint32(len(req.Users)),
Success: successCount,
Failed: failedCount,
Results: results,
}, nil
}
// GetWallets 获取钱包列表
func (o *adminServer) GetWallets(ctx context.Context, req *adminpb.GetWalletsReq) (*adminpb.GetWalletsResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
var total int64
var wallets []*chatdb.Wallet
var err error
// 如果提供了查询条件,先查找用户
if req.UserID != "" || req.PhoneNumber != "" || req.Account != "" {
var userIDs []string
if req.UserID != "" {
// 直接使用 userID
userIDs = []string{req.UserID}
} else if req.PhoneNumber != "" {
// 通过手机号模糊查询用户
_, attributes, err := o.ChatDatabase.SearchUser(ctx, req.PhoneNumber, nil, nil, &sdkws.RequestPagination{PageNumber: 1, ShowNumber: 1000})
if err != nil {
return nil, err
}
for _, attr := range attributes {
userIDs = append(userIDs, attr.UserID)
}
} else if req.Account != "" {
// 通过账号模糊查询用户
_, attributes, err := o.ChatDatabase.SearchUser(ctx, req.Account, nil, nil, &sdkws.RequestPagination{PageNumber: 1, ShowNumber: 1000})
if err != nil {
return nil, err
}
for _, attr := range attributes {
userIDs = append(userIDs, attr.UserID)
}
}
// 根据 userIDs 查询钱包
if len(userIDs) > 0 {
wallets, err = o.ChatDatabase.GetWalletsByUserIDs(ctx, userIDs)
if err != nil {
return nil, err
}
total = int64(len(wallets))
} else {
total = 0
wallets = []*chatdb.Wallet{}
}
} else {
// 没有查询条件,获取所有钱包(分页)
total, wallets, err = o.ChatDatabase.GetWalletsPage(ctx, req.Pagination)
if err != nil {
return nil, err
}
}
// 提取所有 userIDs
userIDs := make([]string, 0, len(wallets))
for _, wallet := range wallets {
userIDs = append(userIDs, wallet.UserID)
}
// 批量获取用户属性(昵称、头像等)
userAttrMap := make(map[string]*chatdb.Attribute)
if len(userIDs) > 0 {
attributes, err := o.ChatDatabase.FindAttribute(ctx, userIDs)
if err != nil {
log.ZWarn(ctx, "Find user attributes failed", err, "userIDs", userIDs)
} else {
for _, attr := range attributes {
userAttrMap[attr.UserID] = attr
}
}
}
// 转换为响应格式
walletInfos := make([]*adminpb.WalletListItemInfo, 0, len(wallets))
for _, wallet := range wallets {
info := &adminpb.WalletListItemInfo{
UserID: wallet.UserID,
Balance: wallet.Balance,
CreateTime: wallet.CreateTime.UnixMilli(),
UpdateTime: wallet.UpdateTime.UnixMilli(),
}
// 填充用户昵称和头像
if attr, ok := userAttrMap[wallet.UserID]; ok {
info.Nickname = attr.Nickname
info.FaceURL = attr.FaceURL
}
walletInfos = append(walletInfos, info)
}
return &adminpb.GetWalletsResp{
Total: uint32(total),
Wallets: walletInfos,
}, nil
}
// GetRealNameAuths 获取实名认证列表(支持按审核状态筛选)
func (o *adminServer) GetRealNameAuths(ctx context.Context, req *adminpb.GetRealNameAuthsReq) (*adminpb.GetRealNameAuthsResp, error) {
// 检查管理员权限
if _, err := mctx.CheckAdmin(ctx); err != nil {
return nil, err
}
var total int64
var wallets []*chatdb.Wallet
var err error
// 查询逻辑:过滤身份证号不为空的(已完成实名认证)
// auditStatus: 0-待审核1-审核通过2-审核拒绝,<0 表示不过滤状态(全部)
// userID: 用户ID搜索可选为空时不过滤
total, wallets, err = o.ChatDatabase.GetWalletsPageByRealNameAuthAuditStatus(ctx, req.AuditStatus, req.UserID, req.Pagination)
if err != nil {
return nil, err
}
// 提取所有 userIDs
userIDs := make([]string, 0, len(wallets))
for _, wallet := range wallets {
userIDs = append(userIDs, wallet.UserID)
}
// 批量获取用户属性(昵称、头像等)
userAttrMap := make(map[string]*chatdb.Attribute)
if len(userIDs) > 0 {
attributes, err := o.ChatDatabase.FindAttribute(ctx, userIDs)
if err != nil {
log.ZWarn(ctx, "Find user attributes failed", err, "userIDs", userIDs)
} else {
for _, attr := range attributes {
userAttrMap[attr.UserID] = attr
}
}
}
// 转换为响应格式
authInfos := make([]*adminpb.RealNameAuthListItemInfo, 0, len(wallets))
for _, wallet := range wallets {
// 注意:数据库查询已经过滤了身份证号不为空的记录,这里不需要再次检查
// 如果在这里再次过滤,会导致返回数量不一致
// 处理创建时间:如果为零值(负数时间戳),使用更新时间或当前时间
createTime := wallet.CreateTime
if createTime.IsZero() || createTime.UnixMilli() < 0 {
if !wallet.UpdateTime.IsZero() {
createTime = wallet.UpdateTime
} else {
createTime = time.Now()
}
}
// 处理更新时间:如果为零值(负数时间戳),使用当前时间
updateTime := wallet.UpdateTime
if updateTime.IsZero() || updateTime.UnixMilli() < 0 {
updateTime = time.Now()
}
info := &adminpb.RealNameAuthListItemInfo{
UserID: wallet.UserID,
IdCard: wallet.RealNameAuth.IDCard,
Name: wallet.RealNameAuth.Name,
IdCardPhotoFront: wallet.RealNameAuth.IDCardPhotoFront,
IdCardPhotoBack: wallet.RealNameAuth.IDCardPhotoBack,
AuditStatus: wallet.RealNameAuth.AuditStatus,
CreateTime: createTime.UnixMilli(),
UpdateTime: updateTime.UnixMilli(),
}
// 填充用户昵称和头像
if attr, ok := userAttrMap[wallet.UserID]; ok {
info.Nickname = attr.Nickname
info.FaceURL = attr.FaceURL
}
authInfos = append(authInfos, info)
}
// 注意:不在应用层排序,因为数据库查询时已经按 create_time 倒序排序并分页
// 如果在应用层重新排序,会导致分页结果不准确
return &adminpb.GetRealNameAuthsResp{
Total: uint32(total),
List: authInfos,
}, nil
}
// AuditRealNameAuth 审核实名认证(通过/拒绝)
func (o *adminServer) AuditRealNameAuth(ctx context.Context, req *adminpb.AuditRealNameAuthReq) (*adminpb.AuditRealNameAuthResp, error) {
// 检查管理员权限
auditorID, err := mctx.CheckAdmin(ctx)
if err != nil {
return nil, err
}
// 验证必填字段
if req.UserID == "" {
return nil, errs.ErrArgs.WrapMsg("userID is required")
}
if req.AuditStatus != 1 && req.AuditStatus != 2 {
return nil, errs.ErrArgs.WrapMsg("auditStatus must be 1 (approved) or 2 (rejected)")
}
// 获取钱包信息
wallet, err := o.ChatDatabase.GetWallet(ctx, req.UserID)
if err != nil {
if errors.Is(err, mongo.ErrNoDocuments) {
return nil, errs.ErrArgs.WrapMsg("wallet not found")
}
return nil, err
}
// 检查是否已完成实名认证
if wallet.RealNameAuth.IDCard == "" || wallet.RealNameAuth.Name == "" {
return nil, errs.ErrArgs.WrapMsg("user has not completed real name authentication")
}
// 更新审核状态
wallet.RealNameAuth.AuditStatus = req.AuditStatus
if err := o.ChatDatabase.UpdateWalletRealNameAuth(ctx, req.UserID, wallet.RealNameAuth); err != nil {
return nil, errs.WrapMsg(err, "failed to update real name auth audit status")
}
log.ZInfo(ctx, "Real name auth audited", "userID", req.UserID, "auditorID", auditorID, "auditStatus", req.AuditStatus, "auditRemark", req.AuditRemark)
return &adminpb.AuditRealNameAuthResp{}, nil
}