复制项目
This commit is contained in:
1143
internal/rpc/admin/admin.go
Normal file
1143
internal/rpc/admin/admin.go
Normal file
File diff suppressed because it is too large
Load Diff
159
internal/rpc/admin/applet.go
Normal file
159
internal/rpc/admin/applet.go
Normal 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
|
||||
}
|
||||
289
internal/rpc/admin/application.go
Normal file
289
internal/rpc/admin/application.go
Normal 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
|
||||
}
|
||||
70
internal/rpc/admin/check.go
Normal file
70
internal/rpc/admin/check.go
Normal 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
|
||||
}
|
||||
55
internal/rpc/admin/client_config.go
Normal file
55
internal/rpc/admin/client_config.go
Normal 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
|
||||
}
|
||||
217
internal/rpc/admin/invitation.go
Normal file
217
internal/rpc/admin/invitation.go
Normal 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
|
||||
}
|
||||
77
internal/rpc/admin/ip_forbidden.go
Normal file
77
internal/rpc/admin/ip_forbidden.go
Normal 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
|
||||
}
|
||||
134
internal/rpc/admin/register_add_friend.go
Normal file
134
internal/rpc/admin/register_add_friend.go
Normal 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
|
||||
}
|
||||
112
internal/rpc/admin/register_add_group.go
Normal file
112
internal/rpc/admin/register_add_group.go
Normal 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
|
||||
}
|
||||
102
internal/rpc/admin/scheduled_task.go
Normal file
102
internal/rpc/admin/scheduled_task.go
Normal 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
110
internal/rpc/admin/start.go
Normal 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
|
||||
}
|
||||
439
internal/rpc/admin/system_config.go
Normal file
439
internal/rpc/admin/system_config.go
Normal 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(),
|
||||
}
|
||||
}
|
||||
79
internal/rpc/admin/token.go
Normal file
79
internal/rpc/admin/token.go
Normal 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
|
||||
}
|
||||
140
internal/rpc/admin/update.go
Normal file
140
internal/rpc/admin/update.go
Normal 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
147
internal/rpc/admin/user.go
Normal 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
|
||||
}
|
||||
97
internal/rpc/admin/user_ip_limit_login.go
Normal file
97
internal/rpc/admin/user_ip_limit_login.go
Normal 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
|
||||
}
|
||||
982
internal/rpc/admin/wallet.go
Normal file
982
internal/rpc/admin/wallet.go
Normal 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
|
||||
}
|
||||
153
internal/rpc/bot/agent.go
Normal file
153
internal/rpc/bot/agent.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/common/constant"
|
||||
"git.imall.cloud/openim/chat/pkg/common/convert"
|
||||
"git.imall.cloud/openim/chat/pkg/common/mctx"
|
||||
"git.imall.cloud/openim/chat/pkg/protocol/bot"
|
||||
pbconstant "git.imall.cloud/openim/protocol/constant"
|
||||
"git.imall.cloud/openim/protocol/sdkws"
|
||||
"git.imall.cloud/openim/protocol/user"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
)
|
||||
|
||||
func (b *botSvr) CreateAgent(ctx context.Context, req *bot.CreateAgentReq) (*bot.CreateAgentResp, error) {
|
||||
if req.Agent == nil {
|
||||
return nil, errs.ErrArgs.WrapMsg("req.Agent is nil")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
imToken, err := b.imCaller.ImAdminTokenWithDefaultAdmin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx = mctx.WithApiToken(ctx, imToken)
|
||||
if req.Agent.UserID != "" {
|
||||
req.Agent.UserID = constant.AgentUserIDPrefix + req.Agent.UserID
|
||||
users, err := b.imCaller.GetUsersInfo(ctx, []string{req.Agent.UserID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(users) > 0 {
|
||||
return nil, errs.ErrDuplicateKey.WrapMsg("agent userID already exists")
|
||||
}
|
||||
} else {
|
||||
randUserIDs := make([]string, 5)
|
||||
for i := range randUserIDs {
|
||||
randUserIDs[i] = constant.AgentUserIDPrefix + genID(10)
|
||||
}
|
||||
users, err := b.imCaller.GetUsersInfo(ctx, randUserIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(users) == len(randUserIDs) {
|
||||
return nil, errs.ErrDuplicateKey.WrapMsg("gen agent userID already exists, please try again")
|
||||
}
|
||||
userIDs := datautil.Batch(func(u *sdkws.UserInfo) string { return u.UserID }, users)
|
||||
for _, uid := range randUserIDs {
|
||||
if datautil.Contain(uid, userIDs...) {
|
||||
continue
|
||||
}
|
||||
req.Agent.UserID = uid
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.imCaller.AddNotificationAccount(ctx, &user.AddNotificationAccountReq{
|
||||
UserID: req.Agent.UserID,
|
||||
NickName: req.Agent.Nickname,
|
||||
FaceURL: req.Agent.FaceURL,
|
||||
AppMangerLevel: pbconstant.AppRobotAdmin,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbagent := convert.PB2DBAgent(req.Agent)
|
||||
dbagent.CreateTime = now
|
||||
err = b.database.CreateAgent(ctx, dbagent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &bot.CreateAgentResp{}, nil
|
||||
}
|
||||
|
||||
func (b *botSvr) UpdateAgent(ctx context.Context, req *bot.UpdateAgentReq) (*bot.UpdateAgentResp, error) {
|
||||
if _, err := b.database.TakeAgent(ctx, req.UserID); err != nil {
|
||||
return nil, errs.ErrArgs.Wrap()
|
||||
}
|
||||
|
||||
if req.FaceURL != nil || req.Nickname != nil {
|
||||
imReq := &user.UpdateNotificationAccountInfoReq{
|
||||
UserID: req.UserID,
|
||||
}
|
||||
if req.Nickname != nil {
|
||||
imReq.NickName = *req.Nickname
|
||||
}
|
||||
if req.FaceURL != nil {
|
||||
imReq.FaceURL = *req.FaceURL
|
||||
}
|
||||
imToken, err := b.imCaller.ImAdminTokenWithDefaultAdmin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx = mctx.WithApiToken(ctx, imToken)
|
||||
err = b.imCaller.UpdateNotificationAccount(ctx, imReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
update := ToDBAgentUpdate(req)
|
||||
err := b.database.UpdateAgent(ctx, req.UserID, update)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &bot.UpdateAgentResp{}, nil
|
||||
}
|
||||
|
||||
func (b *botSvr) PageFindAgent(ctx context.Context, req *bot.PageFindAgentReq) (*bot.PageFindAgentResp, error) {
|
||||
total, agents, err := b.database.PageAgents(ctx, req.UserIDs, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
//_, userType, err := mctx.Check(ctx)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
//if userType != constant.AdminUser {
|
||||
for i := range agents {
|
||||
agents[i].Key = ""
|
||||
}
|
||||
//}
|
||||
return &bot.PageFindAgentResp{
|
||||
Total: total,
|
||||
Agents: convert.BatchDB2PBAgent(agents),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *botSvr) DeleteAgent(ctx context.Context, req *bot.DeleteAgentReq) (*bot.DeleteAgentResp, error) {
|
||||
err := b.database.DeleteAgents(ctx, req.UserIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &bot.DeleteAgentResp{}, nil
|
||||
}
|
||||
|
||||
func genID(l int) string {
|
||||
data := make([]byte, l)
|
||||
_, _ = rand.Read(data)
|
||||
chars := []byte("0123456789")
|
||||
for i := 0; i < len(data); i++ {
|
||||
if i == 0 {
|
||||
data[i] = chars[1:][data[i]%9]
|
||||
} else {
|
||||
data[i] = chars[data[i]%10]
|
||||
}
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
92
internal/rpc/bot/send.go
Normal file
92
internal/rpc/bot/send.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/botstruct"
|
||||
"git.imall.cloud/openim/chat/pkg/common/imapi"
|
||||
"git.imall.cloud/openim/chat/pkg/common/mctx"
|
||||
"git.imall.cloud/openim/chat/pkg/protocol/bot"
|
||||
"git.imall.cloud/openim/protocol/constant"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func (b *botSvr) SendBotMessage(ctx context.Context, req *bot.SendBotMessageReq) (*bot.SendBotMessageResp, error) {
|
||||
agent, err := b.database.TakeAgent(ctx, req.AgentID)
|
||||
if err != nil {
|
||||
return nil, errs.ErrArgs.WrapMsg("agent not found")
|
||||
}
|
||||
//convRespID, err := b.database.TakeConversationRespID(ctx, req.ConversationID, req.AgentID)
|
||||
//if err != nil && !errors.Is(err, mongo.ErrNoDocuments) {
|
||||
// return nil, err
|
||||
//}
|
||||
//var respID string
|
||||
//if convRespID != nil {
|
||||
// respID = convRespID.PreviousResponseID
|
||||
//}
|
||||
|
||||
aiCfg := openai.DefaultConfig(agent.Key)
|
||||
aiCfg.BaseURL = agent.Url
|
||||
aiCfg.HTTPClient = b.httpClient
|
||||
client := openai.NewClientWithConfig(aiCfg)
|
||||
aiReq := openai.ChatCompletionRequest{
|
||||
Model: agent.Model,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleSystem,
|
||||
Content: agent.Prompts,
|
||||
},
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: req.Content,
|
||||
},
|
||||
},
|
||||
}
|
||||
aiCtx, cancel := context.WithTimeout(ctx, time.Duration(b.timeout)*time.Second)
|
||||
defer cancel()
|
||||
completion, err := client.CreateChatCompletion(aiCtx, aiReq)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
|
||||
imToken, err := b.imCaller.ImAdminTokenWithDefaultAdmin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx = mctx.WithApiToken(ctx, imToken)
|
||||
|
||||
content := "no response"
|
||||
if len(completion.Choices) > 0 {
|
||||
content = completion.Choices[0].Message.Content
|
||||
}
|
||||
err = b.imCaller.SendSimpleMsg(ctx, &imapi.SendSingleMsgReq{
|
||||
SendID: agent.UserID,
|
||||
Content: content,
|
||||
}, req.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//err = b.database.UpdateConversationRespID(ctx, req.ConversationID, agent.UserID, ToDBConversationRespIDUpdate(completion.ID))
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
return &bot.SendBotMessageResp{}, nil
|
||||
}
|
||||
|
||||
func getContent(contentType int32, content string) (string, error) {
|
||||
switch contentType {
|
||||
case constant.Text:
|
||||
var elem botstruct.TextElem
|
||||
err := json.Unmarshal([]byte(content), &elem)
|
||||
if err != nil {
|
||||
return "", errs.ErrArgs.WrapMsg(err.Error())
|
||||
}
|
||||
return elem.Content, nil
|
||||
default:
|
||||
return "", errs.New("un support contentType").Wrap()
|
||||
}
|
||||
}
|
||||
53
internal/rpc/bot/start.go
Normal file
53
internal/rpc/bot/start.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/common/config"
|
||||
"git.imall.cloud/openim/chat/pkg/common/db/database"
|
||||
"git.imall.cloud/openim/chat/pkg/common/imapi"
|
||||
"git.imall.cloud/openim/chat/pkg/protocol/bot"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/discovery"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
RpcConfig config.Bot
|
||||
RedisConfig config.Redis
|
||||
MongodbConfig config.Mongo
|
||||
Discovery config.Discovery
|
||||
Share config.Share
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server *grpc.Server) error {
|
||||
mgocli, err := mongoutil.NewMongoDB(ctx, config.MongodbConfig.Build())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var srv botSvr
|
||||
|
||||
srv.database, err = database.NewBotDatabase(mgocli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.timeout = config.RpcConfig.Timeout
|
||||
srv.httpClient = &http.Client{
|
||||
Timeout: time.Duration(config.RpcConfig.Timeout) * time.Second,
|
||||
}
|
||||
im := imapi.New(config.Share.OpenIM.ApiURL, config.Share.OpenIM.Secret, config.Share.OpenIM.AdminUserID)
|
||||
srv.imCaller = im
|
||||
bot.RegisterBotServer(server, &srv)
|
||||
return nil
|
||||
}
|
||||
|
||||
type botSvr struct {
|
||||
bot.UnimplementedBotServer
|
||||
database database.BotDatabase
|
||||
httpClient *http.Client
|
||||
timeout int
|
||||
imCaller imapi.CallerInterface
|
||||
//Admin *chatClient.AdminClient
|
||||
}
|
||||
37
internal/rpc/bot/update.go
Normal file
37
internal/rpc/bot/update.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package bot
|
||||
|
||||
import "git.imall.cloud/openim/chat/pkg/protocol/bot"
|
||||
|
||||
func ToDBAgentUpdate(req *bot.UpdateAgentReq) map[string]any {
|
||||
update := make(map[string]any)
|
||||
if req.Key != nil {
|
||||
update["key"] = req.Key
|
||||
}
|
||||
if req.Prompts != nil {
|
||||
update["prompts"] = req.Prompts
|
||||
}
|
||||
if req.Model != nil {
|
||||
update["model"] = req.Model
|
||||
}
|
||||
if req.FaceURL != nil {
|
||||
update["face_url"] = req.FaceURL
|
||||
}
|
||||
if req.Nickname != nil {
|
||||
update["nick_name"] = req.Nickname
|
||||
}
|
||||
if req.Identity != nil {
|
||||
update["identity"] = req.Identity
|
||||
}
|
||||
if req.Url != nil {
|
||||
update["url"] = req.Url
|
||||
}
|
||||
|
||||
return update
|
||||
}
|
||||
|
||||
func ToDBConversationRespIDUpdate(respID string) map[string]any {
|
||||
update := map[string]any{
|
||||
"previous_response_id": respID,
|
||||
}
|
||||
return update
|
||||
}
|
||||
61
internal/rpc/chat/callback.go
Normal file
61
internal/rpc/chat/callback.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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 chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/common/constant"
|
||||
"git.imall.cloud/openim/chat/pkg/eerrs"
|
||||
"git.imall.cloud/openim/chat/pkg/protocol/chat"
|
||||
constantpb "git.imall.cloud/openim/protocol/constant"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
)
|
||||
|
||||
type CallbackBeforeAddFriendReq struct {
|
||||
CallbackCommand `json:"callbackCommand"`
|
||||
FromUserID string `json:"fromUserID" `
|
||||
ToUserID string `json:"toUserID"`
|
||||
ReqMsg string `json:"reqMsg"`
|
||||
OperationID string `json:"operationID"`
|
||||
}
|
||||
|
||||
type CallbackCommand string
|
||||
|
||||
func (c CallbackCommand) GetCallbackCommand() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
func (o *chatSvr) OpenIMCallback(ctx context.Context, req *chat.OpenIMCallbackReq) (*chat.OpenIMCallbackResp, error) {
|
||||
switch req.Command {
|
||||
case constantpb.CallbackBeforeAddFriendCommand:
|
||||
var data CallbackBeforeAddFriendReq
|
||||
if err := json.Unmarshal([]byte(req.Body), &data); err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
user, err := o.Database.TakeAttributeByUserID(ctx, data.ToUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.AllowAddFriend != constant.OrdinaryUserAddFriendEnable {
|
||||
return nil, eerrs.ErrRefuseFriend.WrapMsg(fmt.Sprintf("state %d", user.AllowAddFriend))
|
||||
}
|
||||
return &chat.OpenIMCallbackResp{}, nil
|
||||
default:
|
||||
return nil, errs.ErrArgs.WrapMsg(fmt.Sprintf("invalid command %s", req.Command))
|
||||
}
|
||||
}
|
||||
292
internal/rpc/chat/favorite.go
Normal file
292
internal/rpc/chat/favorite.go
Normal file
@@ -0,0 +1,292 @@
|
||||
// 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 chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
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/chat"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
)
|
||||
|
||||
// ==================== 收藏相关 RPC ====================
|
||||
|
||||
// CreateFavorite 创建收藏
|
||||
func (o *chatSvr) CreateFavorite(ctx context.Context, req *chat.CreateFavoriteReq) (*chat.CreateFavoriteResp, error) {
|
||||
// 获取当前用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证收藏类型
|
||||
if req.Type < 1 || req.Type > 7 {
|
||||
return nil, errs.ErrArgs.WrapMsg("invalid favorite type")
|
||||
}
|
||||
|
||||
// 创建收藏对象
|
||||
favorite := &chatdb.Favorite{
|
||||
UserID: userID,
|
||||
Type: req.Type,
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
Description: req.Description,
|
||||
Thumbnail: req.Thumbnail,
|
||||
LinkURL: req.LinkURL,
|
||||
FileSize: req.FileSize,
|
||||
Duration: req.Duration,
|
||||
Location: req.Location,
|
||||
Tags: req.Tags,
|
||||
Remark: req.Remark,
|
||||
Status: 1, // 正常状态
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
if err := o.Database.CreateFavorite(ctx, favorite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chat.CreateFavoriteResp{
|
||||
FavoriteID: favorite.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetFavorite 获取收藏详情
|
||||
func (o *chatSvr) GetFavorite(ctx context.Context, req *chat.GetFavoriteReq) (*chat.GetFavoriteResp, error) {
|
||||
// 获取当前用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取收藏
|
||||
favorite, err := o.Database.GetFavorite(ctx, req.FavoriteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证是否为当前用户的收藏
|
||||
if favorite.UserID != userID {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("not your favorite")
|
||||
}
|
||||
|
||||
return &chat.GetFavoriteResp{
|
||||
Favorite: convertFavoriteToProto(favorite),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetFavorites 获取收藏列表
|
||||
func (o *chatSvr) GetFavorites(ctx context.Context, req *chat.GetFavoritesReq) (*chat.GetFavoritesResp, error) {
|
||||
// 获取当前用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var total int64
|
||||
var favorites []*chatdb.Favorite
|
||||
|
||||
if req.Type > 0 {
|
||||
// 按类型查询
|
||||
total, favorites, err = o.Database.GetFavoritesByUserIDAndType(ctx, userID, req.Type, req.Pagination)
|
||||
} else {
|
||||
// 查询所有
|
||||
total, favorites, err = o.Database.GetFavoritesByUserID(ctx, userID, req.Pagination)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
favoriteInfos := make([]*chat.FavoriteInfo, 0, len(favorites))
|
||||
for _, fav := range favorites {
|
||||
favoriteInfos = append(favoriteInfos, convertFavoriteToProto(fav))
|
||||
}
|
||||
|
||||
return &chat.GetFavoritesResp{
|
||||
Total: uint32(total),
|
||||
Favorites: favoriteInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchFavorites 搜索收藏
|
||||
func (o *chatSvr) SearchFavorites(ctx context.Context, req *chat.SearchFavoritesReq) (*chat.SearchFavoritesResp, error) {
|
||||
// 获取当前用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 搜索收藏
|
||||
total, favorites, err := o.Database.SearchFavoritesByKeyword(ctx, userID, req.Keyword, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
favoriteInfos := make([]*chat.FavoriteInfo, 0, len(favorites))
|
||||
for _, fav := range favorites {
|
||||
favoriteInfos = append(favoriteInfos, convertFavoriteToProto(fav))
|
||||
}
|
||||
|
||||
return &chat.SearchFavoritesResp{
|
||||
Total: uint32(total),
|
||||
Favorites: favoriteInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateFavorite 更新收藏
|
||||
func (o *chatSvr) UpdateFavorite(ctx context.Context, req *chat.UpdateFavoriteReq) (*chat.UpdateFavoriteResp, error) {
|
||||
// 获取当前用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取收藏,验证所有权
|
||||
favorite, err := o.Database.GetFavorite(ctx, req.FavoriteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if favorite.UserID != userID {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("not your favorite")
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
updateData := make(map[string]any)
|
||||
if req.Title != "" {
|
||||
updateData["title"] = req.Title
|
||||
}
|
||||
if req.Description != "" {
|
||||
updateData["description"] = req.Description
|
||||
}
|
||||
if req.Remark != "" {
|
||||
updateData["remark"] = req.Remark
|
||||
}
|
||||
if len(req.Tags) > 0 {
|
||||
updateData["tags"] = req.Tags
|
||||
}
|
||||
|
||||
// 更新收藏
|
||||
if err := o.Database.UpdateFavorite(ctx, req.FavoriteID, updateData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chat.UpdateFavoriteResp{}, nil
|
||||
}
|
||||
|
||||
// DeleteFavorite 删除收藏
|
||||
func (o *chatSvr) DeleteFavorite(ctx context.Context, req *chat.DeleteFavoriteReq) (*chat.DeleteFavoriteResp, error) {
|
||||
// 获取当前用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证所有权(批量验证)
|
||||
for _, favoriteID := range req.FavoriteIDs {
|
||||
favorite, err := o.Database.GetFavorite(ctx, favoriteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if favorite.UserID != userID {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("not your favorite")
|
||||
}
|
||||
}
|
||||
|
||||
// 删除收藏
|
||||
if err := o.Database.DeleteFavorite(ctx, req.FavoriteIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chat.DeleteFavoriteResp{}, nil
|
||||
}
|
||||
|
||||
// GetFavoritesByTags 根据标签获取收藏
|
||||
func (o *chatSvr) GetFavoritesByTags(ctx context.Context, req *chat.GetFavoritesByTagsReq) (*chat.GetFavoritesByTagsResp, error) {
|
||||
// 获取当前用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(req.Tags) == 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("tags is empty")
|
||||
}
|
||||
|
||||
// 根据标签查询
|
||||
total, favorites, err := o.Database.GetFavoritesByTags(ctx, userID, req.Tags, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
favoriteInfos := make([]*chat.FavoriteInfo, 0, len(favorites))
|
||||
for _, fav := range favorites {
|
||||
favoriteInfos = append(favoriteInfos, convertFavoriteToProto(fav))
|
||||
}
|
||||
|
||||
return &chat.GetFavoritesByTagsResp{
|
||||
Total: uint32(total),
|
||||
Favorites: favoriteInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetFavoriteCount 获取收藏数量
|
||||
func (o *chatSvr) GetFavoriteCount(ctx context.Context, req *chat.GetFavoriteCountReq) (*chat.GetFavoriteCountResp, error) {
|
||||
// 获取当前用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取收藏数量
|
||||
count, err := o.Database.CountFavoritesByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chat.GetFavoriteCountResp{
|
||||
Count: count,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// convertFavoriteToProto 将数据库模型转换为 protobuf 消息
|
||||
func convertFavoriteToProto(fav *chatdb.Favorite) *chat.FavoriteInfo {
|
||||
return &chat.FavoriteInfo{
|
||||
Id: fav.ID,
|
||||
UserID: fav.UserID,
|
||||
Type: fav.Type,
|
||||
Title: fav.Title,
|
||||
Content: fav.Content,
|
||||
Description: fav.Description,
|
||||
Thumbnail: fav.Thumbnail,
|
||||
LinkURL: fav.LinkURL,
|
||||
FileSize: fav.FileSize,
|
||||
Duration: fav.Duration,
|
||||
Location: fav.Location,
|
||||
Tags: fav.Tags,
|
||||
Remark: fav.Remark,
|
||||
CreateTime: fav.CreateTime.UnixMilli(),
|
||||
UpdateTime: fav.UpdateTime.UnixMilli(),
|
||||
}
|
||||
}
|
||||
1495
internal/rpc/chat/login.go
Normal file
1495
internal/rpc/chat/login.go
Normal file
File diff suppressed because it is too large
Load Diff
109
internal/rpc/chat/password.go
Normal file
109
internal/rpc/chat/password.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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 chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/openimsdk/tools/errs"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/common/constant"
|
||||
"git.imall.cloud/openim/chat/pkg/common/mctx"
|
||||
"git.imall.cloud/openim/chat/pkg/protocol/chat"
|
||||
)
|
||||
|
||||
func (o *chatSvr) ResetPassword(ctx context.Context, req *chat.ResetPasswordReq) (*chat.ResetPasswordResp, error) {
|
||||
if req.Password == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("password must be set")
|
||||
}
|
||||
if req.AreaCode == "" || req.PhoneNumber == "" {
|
||||
if !(req.AreaCode == "" && req.PhoneNumber == "") {
|
||||
return nil, errs.ErrArgs.WrapMsg("area code and phone number must set together")
|
||||
}
|
||||
}
|
||||
var verifyCodeID string
|
||||
var err error
|
||||
if req.Email == "" {
|
||||
verifyCodeID, err = o.verifyCode(ctx, o.verifyCodeJoin(req.AreaCode, req.PhoneNumber), req.VerifyCode, phone)
|
||||
} else {
|
||||
verifyCodeID, err = o.verifyCode(ctx, req.Email, req.VerifyCode, mail)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var account string
|
||||
if req.Email == "" {
|
||||
account = BuildCredentialPhone(req.AreaCode, req.PhoneNumber)
|
||||
} else {
|
||||
account = req.Email
|
||||
}
|
||||
cred, err := o.Database.TakeCredentialByAccount(ctx, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = o.Database.UpdatePasswordAndDeleteVerifyCode(ctx, cred.UserID, req.Password, verifyCodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &chat.ResetPasswordResp{}, nil
|
||||
}
|
||||
|
||||
func (o *chatSvr) ChangePassword(ctx context.Context, req *chat.ChangePasswordReq) (*chat.ChangePasswordResp, error) {
|
||||
if req.NewPassword == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("new password must be set")
|
||||
}
|
||||
if req.NewPassword == req.CurrentPassword {
|
||||
return nil, errs.ErrArgs.WrapMsg("new password == current password")
|
||||
}
|
||||
opUserID, userType, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch userType {
|
||||
case constant.NormalUser:
|
||||
if req.UserID == "" {
|
||||
req.UserID = opUserID
|
||||
}
|
||||
if req.UserID != opUserID {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("no permission change other user password")
|
||||
}
|
||||
case constant.AdminUser:
|
||||
if req.UserID == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("user id must be set")
|
||||
}
|
||||
default:
|
||||
return nil, errs.ErrInternalServer.WrapMsg("invalid user type")
|
||||
}
|
||||
user, err := o.Database.GetUser(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userType != constant.AdminUser {
|
||||
if user.Password != req.CurrentPassword {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("current password is wrong")
|
||||
}
|
||||
}
|
||||
if user.Password != req.NewPassword {
|
||||
if err := o.Database.UpdatePassword(ctx, req.UserID, req.NewPassword); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := o.Admin.InvalidateToken(ctx, req.UserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chat.ChangePasswordResp{}, nil
|
||||
}
|
||||
16
internal/rpc/chat/register.go
Normal file
16
internal/rpc/chat/register.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/protocol/chat"
|
||||
)
|
||||
|
||||
func (o *chatSvr) SetAllowRegister(ctx context.Context, req *chat.SetAllowRegisterReq) (*chat.SetAllowRegisterResp, error) {
|
||||
o.AllowRegister = req.AllowRegister
|
||||
return &chat.SetAllowRegisterResp{}, nil
|
||||
}
|
||||
|
||||
func (o *chatSvr) GetAllowRegister(ctx context.Context, req *chat.GetAllowRegisterReq) (*chat.GetAllowRegisterResp, error) {
|
||||
return &chat.GetAllowRegisterResp{AllowRegister: o.AllowRegister}, nil
|
||||
}
|
||||
22
internal/rpc/chat/rtc.go
Normal file
22
internal/rpc/chat/rtc.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/common/mctx"
|
||||
"git.imall.cloud/openim/chat/pkg/protocol/chat"
|
||||
)
|
||||
|
||||
func (o *chatSvr) GetTokenForVideoMeeting(ctx context.Context, req *chat.GetTokenForVideoMeetingReq) (*chat.GetTokenForVideoMeetingResp, error) {
|
||||
if _, _, err := mctx.Check(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := o.Livekit.GetLiveKitToken(req.Room, req.Identity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &chat.GetTokenForVideoMeetingResp{
|
||||
ServerUrl: o.Livekit.GetLiveKitURL(),
|
||||
Token: token,
|
||||
}, err
|
||||
}
|
||||
288
internal/rpc/chat/scheduled_task.go
Normal file
288
internal/rpc/chat/scheduled_task.go
Normal file
@@ -0,0 +1,288 @@
|
||||
// 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 chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
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/chat"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
)
|
||||
|
||||
// ==================== 定时任务相关 RPC ====================
|
||||
|
||||
// CreateScheduledTask 创建定时任务
|
||||
func (o *chatSvr) CreateScheduledTask(ctx context.Context, req *chat.CreateScheduledTaskReq) (*chat.CreateScheduledTaskResp, error) {
|
||||
// 获取当前用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.Name == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("task name is required")
|
||||
}
|
||||
if req.CronExpression == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("cron expression is required")
|
||||
}
|
||||
if len(req.Messages) == 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("messages is required")
|
||||
}
|
||||
if len(req.RecvIDs) == 0 && len(req.GroupIDs) == 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("recvIDs or groupIDs is required")
|
||||
}
|
||||
|
||||
// 验证消息类型
|
||||
for _, msg := range req.Messages {
|
||||
if msg.Type < 1 || msg.Type > 3 {
|
||||
return nil, errs.ErrArgs.WrapMsg("invalid message type")
|
||||
}
|
||||
}
|
||||
|
||||
// 转换消息列表
|
||||
messages := make([]chatdb.Message, 0, len(req.Messages))
|
||||
for _, msg := range req.Messages {
|
||||
messages = append(messages, chatdb.Message{
|
||||
Type: msg.Type,
|
||||
Content: msg.Content,
|
||||
Thumbnail: msg.Thumbnail,
|
||||
Duration: msg.Duration,
|
||||
FileSize: msg.FileSize,
|
||||
Width: msg.Width,
|
||||
Height: msg.Height,
|
||||
})
|
||||
}
|
||||
|
||||
// 创建定时任务对象
|
||||
task := &chatdb.ScheduledTask{
|
||||
UserID: userID,
|
||||
Name: req.Name,
|
||||
CronExpression: req.CronExpression,
|
||||
Messages: messages,
|
||||
RecvIDs: req.RecvIDs,
|
||||
GroupIDs: req.GroupIDs,
|
||||
Status: req.Status,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
|
||||
// 如果状态未设置,默认为启用
|
||||
if task.Status == 0 {
|
||||
task.Status = 1
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
if err := o.Database.CreateScheduledTask(ctx, task); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chat.CreateScheduledTaskResp{
|
||||
TaskID: task.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetScheduledTask 获取定时任务详情
|
||||
func (o *chatSvr) GetScheduledTask(ctx context.Context, req *chat.GetScheduledTaskReq) (*chat.GetScheduledTaskResp, error) {
|
||||
// 获取当前用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取任务
|
||||
task, err := o.Database.GetScheduledTask(ctx, req.TaskID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证是否为当前用户的任务
|
||||
if task.UserID != userID {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("not your task")
|
||||
}
|
||||
|
||||
return &chat.GetScheduledTaskResp{
|
||||
Task: convertScheduledTaskToProto(task),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetScheduledTasks 获取定时任务列表
|
||||
func (o *chatSvr) GetScheduledTasks(ctx context.Context, req *chat.GetScheduledTasksReq) (*chat.GetScheduledTasksResp, error) {
|
||||
// 获取当前用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取任务列表
|
||||
total, tasks, err := o.Database.GetScheduledTasksByUserID(ctx, userID, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
taskInfos := make([]*chat.ScheduledTaskInfo, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
taskInfos = append(taskInfos, convertScheduledTaskToProto(task))
|
||||
}
|
||||
|
||||
return &chat.GetScheduledTasksResp{
|
||||
Total: uint32(total),
|
||||
Tasks: taskInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateScheduledTask 更新定时任务
|
||||
func (o *chatSvr) UpdateScheduledTask(ctx context.Context, req *chat.UpdateScheduledTaskReq) (*chat.UpdateScheduledTaskResp, error) {
|
||||
// 获取当前用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取任务,验证所有权
|
||||
task, err := o.Database.GetScheduledTask(ctx, req.TaskID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if task.UserID != userID {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("not your task")
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
updateData := make(map[string]any)
|
||||
if req.Name != "" {
|
||||
updateData["name"] = req.Name
|
||||
}
|
||||
if req.CronExpression != "" {
|
||||
updateData["cron_expression"] = req.CronExpression
|
||||
}
|
||||
if len(req.Messages) > 0 {
|
||||
// 验证消息类型
|
||||
for _, msg := range req.Messages {
|
||||
if msg.Type < 1 || msg.Type > 3 {
|
||||
return nil, errs.ErrArgs.WrapMsg("invalid message type")
|
||||
}
|
||||
}
|
||||
// 转换消息列表
|
||||
messages := make([]chatdb.Message, 0, len(req.Messages))
|
||||
for _, msg := range req.Messages {
|
||||
messages = append(messages, chatdb.Message{
|
||||
Type: msg.Type,
|
||||
Content: msg.Content,
|
||||
Thumbnail: msg.Thumbnail,
|
||||
Duration: msg.Duration,
|
||||
FileSize: msg.FileSize,
|
||||
Width: msg.Width,
|
||||
Height: msg.Height,
|
||||
})
|
||||
}
|
||||
updateData["messages"] = messages
|
||||
}
|
||||
if req.RecvIDs != nil {
|
||||
updateData["recv_ids"] = req.RecvIDs
|
||||
}
|
||||
if req.GroupIDs != nil {
|
||||
updateData["group_ids"] = req.GroupIDs
|
||||
}
|
||||
// status字段:0-已禁用,1-已启用,允许设置为0
|
||||
if req.Status == 0 || req.Status == 1 {
|
||||
updateData["status"] = req.Status
|
||||
}
|
||||
|
||||
// 验证:如果更新后没有接收者,返回错误
|
||||
if req.RecvIDs != nil && req.GroupIDs != nil {
|
||||
if len(req.RecvIDs) == 0 && len(req.GroupIDs) == 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("recvIDs or groupIDs is required")
|
||||
}
|
||||
} else if req.RecvIDs != nil && len(req.RecvIDs) == 0 {
|
||||
// 如果只更新了RecvIDs且为空,检查GroupIDs是否也为空
|
||||
if len(task.GroupIDs) == 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("recvIDs or groupIDs is required")
|
||||
}
|
||||
} else if req.GroupIDs != nil && len(req.GroupIDs) == 0 {
|
||||
// 如果只更新了GroupIDs且为空,检查RecvIDs是否也为空
|
||||
if len(task.RecvIDs) == 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("recvIDs or groupIDs is required")
|
||||
}
|
||||
}
|
||||
|
||||
// 更新任务
|
||||
if err := o.Database.UpdateScheduledTask(ctx, req.TaskID, updateData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chat.UpdateScheduledTaskResp{}, nil
|
||||
}
|
||||
|
||||
// DeleteScheduledTask 删除定时任务
|
||||
func (o *chatSvr) DeleteScheduledTask(ctx context.Context, req *chat.DeleteScheduledTaskReq) (*chat.DeleteScheduledTaskResp, error) {
|
||||
// 获取当前用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证所有权(批量验证)
|
||||
for _, taskID := range req.TaskIDs {
|
||||
task, err := o.Database.GetScheduledTask(ctx, taskID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if task.UserID != userID {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("not your task")
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
if err := o.Database.DeleteScheduledTask(ctx, req.TaskIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chat.DeleteScheduledTaskResp{}, nil
|
||||
}
|
||||
|
||||
// convertScheduledTaskToProto 将数据库模型转换为 protobuf 消息
|
||||
func convertScheduledTaskToProto(task *chatdb.ScheduledTask) *chat.ScheduledTaskInfo {
|
||||
messages := make([]*chat.ScheduledTaskMessage, 0, len(task.Messages))
|
||||
for _, msg := range task.Messages {
|
||||
messages = append(messages, &chat.ScheduledTaskMessage{
|
||||
Type: msg.Type,
|
||||
Content: msg.Content,
|
||||
Thumbnail: msg.Thumbnail,
|
||||
Duration: msg.Duration,
|
||||
FileSize: msg.FileSize,
|
||||
Width: msg.Width,
|
||||
Height: msg.Height,
|
||||
})
|
||||
}
|
||||
|
||||
return &chat.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(),
|
||||
}
|
||||
}
|
||||
107
internal/rpc/chat/sensitive_word.go
Normal file
107
internal/rpc/chat/sensitive_word.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// 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 chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
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/chat"
|
||||
)
|
||||
|
||||
// ==================== 敏感词检测相关 RPC ====================
|
||||
|
||||
// GetSensitiveWords 获取敏感词列表
|
||||
func (o *chatSvr) GetSensitiveWords(ctx context.Context, req *chat.GetSensitiveWordsReq) (*chat.GetSensitiveWordsResp, error) {
|
||||
// 验证用户身份
|
||||
if _, _, err := mctx.Check(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取启用的敏感词列表
|
||||
words, err := o.Database.GetSensitiveWords(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应格式(客户端只需要基本信息)
|
||||
var wordInfos []*chat.SensitiveWordInfo
|
||||
for _, word := range words {
|
||||
wordInfos = append(wordInfos, &chat.SensitiveWordInfo{
|
||||
Word: word.Word,
|
||||
Action: word.Action,
|
||||
ReplaceChar: "", // 敏感词本身没有替换字符,使用配置中的默认值
|
||||
})
|
||||
}
|
||||
|
||||
// 获取敏感词配置
|
||||
config, err := o.Database.GetSensitiveWordConfig(ctx)
|
||||
if err != nil {
|
||||
// 如果配置不存在,使用默认值
|
||||
config = &chatdb.SensitiveWordConfig{
|
||||
EnableFilter: true,
|
||||
ReplaceChar: "***",
|
||||
}
|
||||
}
|
||||
|
||||
return &chat.GetSensitiveWordsResp{
|
||||
Words: wordInfos,
|
||||
EnableFilter: config.EnableFilter,
|
||||
DefaultReplaceChar: config.ReplaceChar,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CheckSensitiveWords 检测敏感词
|
||||
func (o *chatSvr) CheckSensitiveWords(ctx context.Context, req *chat.CheckSensitiveWordsReq) (*chat.CheckSensitiveWordsResp, error) {
|
||||
// 验证用户身份
|
||||
if _, _, err := mctx.Check(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检测敏感词
|
||||
matchedWords, hasSensitive, err := o.Database.CheckSensitiveWords(ctx, req.Content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果检测到敏感词,进行内容过滤
|
||||
filteredContent := req.Content
|
||||
var matchedWordStrings []string
|
||||
|
||||
if hasSensitive {
|
||||
for _, word := range matchedWords {
|
||||
matchedWordStrings = append(matchedWordStrings, word.Word)
|
||||
|
||||
// 根据处理动作进行替换
|
||||
if word.Action == 1 { // 替换模式
|
||||
// 获取配置中的替换字符
|
||||
config, err := o.Database.GetSensitiveWordConfig(ctx)
|
||||
replaceChar := "***"
|
||||
if err == nil && config.ReplaceChar != "" {
|
||||
replaceChar = config.ReplaceChar
|
||||
}
|
||||
filteredContent = strings.ReplaceAll(filteredContent, word.Word, replaceChar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &chat.CheckSensitiveWordsResp{
|
||||
HasSensitive: hasSensitive,
|
||||
FilteredContent: filteredContent,
|
||||
MatchedWords: matchedWordStrings,
|
||||
}, nil
|
||||
}
|
||||
573
internal/rpc/chat/sensitive_word_admin.go
Normal file
573
internal/rpc/chat/sensitive_word_admin.go
Normal file
@@ -0,0 +1,573 @@
|
||||
// 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 chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/common/db/table/chat"
|
||||
"git.imall.cloud/openim/chat/pkg/common/mctx"
|
||||
chatpb "git.imall.cloud/openim/chat/pkg/protocol/chat"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ==================== 敏感词管理相关 RPC ====================
|
||||
|
||||
// AddSensitiveWord 添加敏感词
|
||||
func (o *chatSvr) AddSensitiveWord(ctx context.Context, req *chatpb.AddSensitiveWordReq) (*chatpb.AddSensitiveWordResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建敏感词对象
|
||||
word := &chat.SensitiveWord{
|
||||
ID: uuid.New().String(),
|
||||
Word: req.Word,
|
||||
Level: req.Level,
|
||||
Type: req.Type,
|
||||
Action: req.Action,
|
||||
Status: req.Status,
|
||||
Creator: getAdminUserID(ctx),
|
||||
Updater: getAdminUserID(ctx),
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
Remark: req.Remark,
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
err := o.Database.CreateSensitiveWord(ctx, word)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.AddSensitiveWordResp{}, nil
|
||||
}
|
||||
|
||||
// UpdateSensitiveWord 更新敏感词
|
||||
func (o *chatSvr) UpdateSensitiveWord(ctx context.Context, req *chatpb.UpdateSensitiveWordReq) (*chatpb.UpdateSensitiveWordResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
data := make(map[string]any)
|
||||
if req.Word != "" {
|
||||
data["word"] = req.Word
|
||||
}
|
||||
if req.Level > 0 {
|
||||
data["level"] = req.Level
|
||||
}
|
||||
if req.Type > 0 {
|
||||
data["type"] = req.Type
|
||||
}
|
||||
if req.Action > 0 {
|
||||
data["action"] = req.Action
|
||||
}
|
||||
if req.Status >= 0 {
|
||||
data["status"] = req.Status
|
||||
}
|
||||
if req.Remark != "" {
|
||||
data["remark"] = req.Remark
|
||||
}
|
||||
data["updater"] = getAdminUserID(ctx)
|
||||
|
||||
// 更新数据库
|
||||
err := o.Database.UpdateSensitiveWord(ctx, req.Id, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.UpdateSensitiveWordResp{}, nil
|
||||
}
|
||||
|
||||
// DeleteSensitiveWord 删除敏感词
|
||||
func (o *chatSvr) DeleteSensitiveWord(ctx context.Context, req *chatpb.DeleteSensitiveWordReq) (*chatpb.DeleteSensitiveWordResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 删除数据
|
||||
err := o.Database.DeleteSensitiveWord(ctx, req.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.DeleteSensitiveWordResp{}, nil
|
||||
}
|
||||
|
||||
// GetSensitiveWord 获取敏感词
|
||||
func (o *chatSvr) GetSensitiveWord(ctx context.Context, req *chatpb.GetSensitiveWordReq) (*chatpb.GetSensitiveWordResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
word, err := o.Database.GetSensitiveWord(ctx, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.GetSensitiveWordResp{
|
||||
Word: convertToSensitiveWordDetailInfo(word),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchSensitiveWords 搜索敏感词
|
||||
func (o *chatSvr) SearchSensitiveWords(ctx context.Context, req *chatpb.SearchSensitiveWordsReq) (*chatpb.SearchSensitiveWordsResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 搜索数据
|
||||
total, words, err := o.Database.SearchSensitiveWords(ctx, req.Keyword, req.Action, req.Status, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换结果
|
||||
var wordInfos []*chatpb.SensitiveWordDetailInfo
|
||||
for _, word := range words {
|
||||
wordInfos = append(wordInfos, convertToSensitiveWordDetailInfo(word))
|
||||
}
|
||||
|
||||
return &chatpb.SearchSensitiveWordsResp{
|
||||
Total: uint32(total),
|
||||
Words: wordInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BatchAddSensitiveWords 批量添加敏感词
|
||||
func (o *chatSvr) BatchAddSensitiveWords(ctx context.Context, req *chatpb.BatchAddSensitiveWordsReq) (*chatpb.BatchAddSensitiveWordsResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为数据库模型
|
||||
var words []*chat.SensitiveWord
|
||||
now := time.Now()
|
||||
adminID := getAdminUserID(ctx)
|
||||
for _, wordInfo := range req.Words {
|
||||
words = append(words, &chat.SensitiveWord{
|
||||
ID: uuid.New().String(),
|
||||
Word: wordInfo.Word,
|
||||
Level: wordInfo.Level,
|
||||
Type: wordInfo.Type,
|
||||
Action: wordInfo.Action,
|
||||
Status: wordInfo.Status,
|
||||
Creator: adminID,
|
||||
Updater: adminID,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
Remark: wordInfo.Remark,
|
||||
})
|
||||
}
|
||||
|
||||
// 批量保存
|
||||
err := o.Database.BatchAddSensitiveWords(ctx, words)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.BatchAddSensitiveWordsResp{}, nil
|
||||
}
|
||||
|
||||
// BatchUpdateSensitiveWords 批量更新敏感词
|
||||
func (o *chatSvr) BatchUpdateSensitiveWords(ctx context.Context, req *chatpb.BatchUpdateSensitiveWordsReq) (*chatpb.BatchUpdateSensitiveWordsResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为数据库模型
|
||||
updates := make(map[string]map[string]any)
|
||||
adminID := getAdminUserID(ctx)
|
||||
for id, wordInfo := range req.Updates {
|
||||
data := make(map[string]any)
|
||||
if wordInfo.Word != "" {
|
||||
data["word"] = wordInfo.Word
|
||||
}
|
||||
if wordInfo.Level > 0 {
|
||||
data["level"] = wordInfo.Level
|
||||
}
|
||||
if wordInfo.Type > 0 {
|
||||
data["type"] = wordInfo.Type
|
||||
}
|
||||
if wordInfo.Action > 0 {
|
||||
data["action"] = wordInfo.Action
|
||||
}
|
||||
if wordInfo.Status >= 0 {
|
||||
data["status"] = wordInfo.Status
|
||||
}
|
||||
if wordInfo.Remark != "" {
|
||||
data["remark"] = wordInfo.Remark
|
||||
}
|
||||
data["updater"] = adminID
|
||||
updates[id] = data
|
||||
}
|
||||
|
||||
// 批量更新
|
||||
err := o.Database.BatchUpdateSensitiveWords(ctx, updates)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.BatchUpdateSensitiveWordsResp{}, nil
|
||||
}
|
||||
|
||||
// BatchDeleteSensitiveWords 批量删除敏感词
|
||||
func (o *chatSvr) BatchDeleteSensitiveWords(ctx context.Context, req *chatpb.BatchDeleteSensitiveWordsReq) (*chatpb.BatchDeleteSensitiveWordsResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
err := o.Database.BatchDeleteSensitiveWords(ctx, req.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.BatchDeleteSensitiveWordsResp{}, nil
|
||||
}
|
||||
|
||||
// ==================== 敏感词分组管理相关 RPC ====================
|
||||
|
||||
// AddSensitiveWordGroup 添加敏感词分组
|
||||
func (o *chatSvr) AddSensitiveWordGroup(ctx context.Context, req *chatpb.AddSensitiveWordGroupReq) (*chatpb.AddSensitiveWordGroupResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建分组对象
|
||||
group := &chat.SensitiveWordGroup{
|
||||
ID: primitive.NewObjectID(),
|
||||
Name: req.Name,
|
||||
Remark: req.Remark,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
err := o.Database.CreateSensitiveWordGroup(ctx, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.AddSensitiveWordGroupResp{}, nil
|
||||
}
|
||||
|
||||
// UpdateSensitiveWordGroup 更新敏感词分组
|
||||
func (o *chatSvr) UpdateSensitiveWordGroup(ctx context.Context, req *chatpb.UpdateSensitiveWordGroupReq) (*chatpb.UpdateSensitiveWordGroupResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
data := make(map[string]any)
|
||||
if req.Name != "" {
|
||||
data["name"] = req.Name
|
||||
}
|
||||
if req.Remark != "" {
|
||||
data["remark"] = req.Remark
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
err := o.Database.UpdateSensitiveWordGroup(ctx, req.Id, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.UpdateSensitiveWordGroupResp{}, nil
|
||||
}
|
||||
|
||||
// DeleteSensitiveWordGroup 删除敏感词分组
|
||||
func (o *chatSvr) DeleteSensitiveWordGroup(ctx context.Context, req *chatpb.DeleteSensitiveWordGroupReq) (*chatpb.DeleteSensitiveWordGroupResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 删除数据
|
||||
err := o.Database.DeleteSensitiveWordGroup(ctx, req.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.DeleteSensitiveWordGroupResp{}, nil
|
||||
}
|
||||
|
||||
// GetSensitiveWordGroup 获取敏感词分组
|
||||
func (o *chatSvr) GetSensitiveWordGroup(ctx context.Context, req *chatpb.GetSensitiveWordGroupReq) (*chatpb.GetSensitiveWordGroupResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
group, err := o.Database.GetSensitiveWordGroup(ctx, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.GetSensitiveWordGroupResp{
|
||||
Group: convertToSensitiveWordGroupInfo(group),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAllSensitiveWordGroups 获取所有敏感词分组
|
||||
func (o *chatSvr) GetAllSensitiveWordGroups(ctx context.Context, req *chatpb.GetAllSensitiveWordGroupsReq) (*chatpb.GetAllSensitiveWordGroupsResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
groups, err := o.Database.GetAllSensitiveWordGroups(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换结果
|
||||
var groupInfos []*chatpb.SensitiveWordGroupInfo
|
||||
for _, group := range groups {
|
||||
groupInfos = append(groupInfos, convertToSensitiveWordGroupInfo(group))
|
||||
}
|
||||
|
||||
return &chatpb.GetAllSensitiveWordGroupsResp{
|
||||
Groups: groupInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ==================== 敏感词配置管理相关 RPC ====================
|
||||
|
||||
// GetSensitiveWordConfig 获取敏感词配置
|
||||
func (o *chatSvr) GetSensitiveWordConfig(ctx context.Context, req *chatpb.GetSensitiveWordConfigReq) (*chatpb.GetSensitiveWordConfigResp, error) {
|
||||
fmt.Println("GetSensitiveWordConfig", "_________11", req)
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
fmt.Println("GetSensitiveWordConfig", "_________22", err)
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println("GetSensitiveWordConfig", "_________33")
|
||||
// 查询数据
|
||||
config, err := o.Database.GetSensitiveWordConfig(ctx)
|
||||
if err != nil {
|
||||
fmt.Println("GetSensitiveWordConfig", "_________44", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.GetSensitiveWordConfigResp{
|
||||
Config: convertToSensitiveWordConfigInfo(config),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateSensitiveWordConfig 更新敏感词配置
|
||||
func (o *chatSvr) UpdateSensitiveWordConfig(ctx context.Context, req *chatpb.UpdateSensitiveWordConfigReq) (*chatpb.UpdateSensitiveWordConfigResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为数据库模型
|
||||
config := &chat.SensitiveWordConfig{
|
||||
ID: req.Config.Id,
|
||||
EnableFilter: req.Config.EnableFilter,
|
||||
FilterMode: req.Config.FilterMode,
|
||||
ReplaceChar: req.Config.ReplaceChar,
|
||||
WhitelistUsers: req.Config.WhitelistUsers,
|
||||
WhitelistGroups: req.Config.WhitelistGroups,
|
||||
LogEnabled: req.Config.LogEnabled,
|
||||
AutoApprove: req.Config.AutoApprove,
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
|
||||
// 更新数据库
|
||||
err := o.Database.UpdateSensitiveWordConfig(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.UpdateSensitiveWordConfigResp{}, nil
|
||||
}
|
||||
|
||||
// ==================== 敏感词日志管理相关 RPC ====================
|
||||
|
||||
// GetSensitiveWordLogs 获取敏感词日志
|
||||
func (o *chatSvr) GetSensitiveWordLogs(ctx context.Context, req *chatpb.GetSensitiveWordLogsReq) (*chatpb.GetSensitiveWordLogsResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
total, logs, err := o.Database.GetSensitiveWordLogs(ctx, req.UserId, req.GroupId, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换结果
|
||||
var logInfos []*chatpb.SensitiveWordLogInfo
|
||||
for _, log := range logs {
|
||||
logInfos = append(logInfos, convertToSensitiveWordLogInfo(log))
|
||||
}
|
||||
|
||||
return &chatpb.GetSensitiveWordLogsResp{
|
||||
Total: uint32(total),
|
||||
Logs: logInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteSensitiveWordLogs 删除敏感词日志
|
||||
func (o *chatSvr) DeleteSensitiveWordLogs(ctx context.Context, req *chatpb.DeleteSensitiveWordLogsReq) (*chatpb.DeleteSensitiveWordLogsResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 删除数据
|
||||
err := o.Database.DeleteSensitiveWordLogs(ctx, req.Ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.DeleteSensitiveWordLogsResp{}, nil
|
||||
}
|
||||
|
||||
// ==================== 敏感词统计相关 RPC ====================
|
||||
|
||||
// GetSensitiveWordStats 获取敏感词统计
|
||||
func (o *chatSvr) GetSensitiveWordStats(ctx context.Context, req *chatpb.GetSensitiveWordStatsReq) (*chatpb.GetSensitiveWordStatsResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
stats, err := o.Database.GetSensitiveWordStats(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.GetSensitiveWordStatsResp{
|
||||
Stats: &chatpb.SensitiveWordStatsInfo{
|
||||
Total: stats["total"],
|
||||
Enabled: stats["enabled"],
|
||||
Disabled: stats["disabled"],
|
||||
Replace: stats["replace"],
|
||||
Block: stats["block"],
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSensitiveWordLogStats 获取敏感词日志统计
|
||||
func (o *chatSvr) GetSensitiveWordLogStats(ctx context.Context, req *chatpb.GetSensitiveWordLogStatsReq) (*chatpb.GetSensitiveWordLogStatsResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
startTime := time.Unix(req.StartTime, 0)
|
||||
endTime := time.Unix(req.EndTime, 0)
|
||||
stats, err := o.Database.GetSensitiveWordLogStats(ctx, startTime, endTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.GetSensitiveWordLogStatsResp{
|
||||
Stats: &chatpb.SensitiveWordLogStatsInfo{
|
||||
Total: stats["total"],
|
||||
Replace: stats["replace"],
|
||||
Block: stats["block"],
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
// getAdminUserID 获取当前管理员用户ID
|
||||
func getAdminUserID(ctx context.Context) string {
|
||||
userID, _ := mctx.CheckAdmin(ctx)
|
||||
return userID
|
||||
}
|
||||
|
||||
// convertToSensitiveWordDetailInfo 转换为敏感词详细信息
|
||||
func convertToSensitiveWordDetailInfo(word *chat.SensitiveWord) *chatpb.SensitiveWordDetailInfo {
|
||||
return &chatpb.SensitiveWordDetailInfo{
|
||||
Id: word.ID,
|
||||
Word: word.Word,
|
||||
Level: word.Level,
|
||||
Type: word.Type,
|
||||
Action: word.Action,
|
||||
Status: word.Status,
|
||||
Creator: word.Creator,
|
||||
Updater: word.Updater,
|
||||
CreateTime: word.CreateTime.UnixMilli(),
|
||||
UpdateTime: word.UpdateTime.UnixMilli(),
|
||||
Remark: word.Remark,
|
||||
}
|
||||
}
|
||||
|
||||
// convertToSensitiveWordGroupInfo 转换为敏感词分组信息
|
||||
func convertToSensitiveWordGroupInfo(group *chat.SensitiveWordGroup) *chatpb.SensitiveWordGroupInfo {
|
||||
return &chatpb.SensitiveWordGroupInfo{
|
||||
Id: group.ID.Hex(),
|
||||
Name: group.Name,
|
||||
Remark: group.Remark,
|
||||
CreateTime: group.CreateTime.UnixMilli(),
|
||||
UpdateTime: group.UpdateTime.UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
// convertToSensitiveWordConfigInfo 转换为敏感词配置信息
|
||||
func convertToSensitiveWordConfigInfo(config *chat.SensitiveWordConfig) *chatpb.SensitiveWordConfigInfo {
|
||||
return &chatpb.SensitiveWordConfigInfo{
|
||||
Id: config.ID,
|
||||
EnableFilter: config.EnableFilter,
|
||||
FilterMode: config.FilterMode,
|
||||
ReplaceChar: config.ReplaceChar,
|
||||
WhitelistUsers: config.WhitelistUsers,
|
||||
WhitelistGroups: config.WhitelistGroups,
|
||||
LogEnabled: config.LogEnabled,
|
||||
AutoApprove: config.AutoApprove,
|
||||
UpdateTime: config.UpdateTime.UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
// convertToSensitiveWordLogInfo 转换为敏感词日志信息
|
||||
func convertToSensitiveWordLogInfo(log *chat.SensitiveWordLog) *chatpb.SensitiveWordLogInfo {
|
||||
return &chatpb.SensitiveWordLogInfo{
|
||||
Id: log.ID.Hex(),
|
||||
UserId: log.UserID,
|
||||
GroupId: log.GroupID,
|
||||
Content: log.Content,
|
||||
MatchedWords: log.MatchedWords,
|
||||
Action: log.Action,
|
||||
ProcessedText: log.ProcessedText,
|
||||
CreateTime: log.CreateTime.UnixMilli(),
|
||||
}
|
||||
}
|
||||
119
internal/rpc/chat/start.go
Normal file
119
internal/rpc/chat/start.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/common/constant"
|
||||
"git.imall.cloud/openim/chat/pkg/common/mctx"
|
||||
"git.imall.cloud/openim/chat/pkg/common/rtc"
|
||||
"git.imall.cloud/openim/chat/pkg/protocol/admin"
|
||||
"git.imall.cloud/openim/chat/pkg/protocol/chat"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/discovery"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/mw"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/common/config"
|
||||
"git.imall.cloud/openim/chat/pkg/common/db/database"
|
||||
"git.imall.cloud/openim/chat/pkg/email"
|
||||
chatClient "git.imall.cloud/openim/chat/pkg/rpclient/chat"
|
||||
"git.imall.cloud/openim/chat/pkg/sms"
|
||||
"github.com/openimsdk/tools/db/redisutil"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
RpcConfig config.Chat
|
||||
RedisConfig config.Redis
|
||||
MongodbConfig config.Mongo
|
||||
Discovery config.Discovery
|
||||
Share config.Share
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, config *Config, client discovery.SvcDiscoveryRegistry, server *grpc.Server) error {
|
||||
if len(config.Share.ChatAdmin) == 0 {
|
||||
return errs.New("share chat admin not configured")
|
||||
}
|
||||
mgocli, err := mongoutil.NewMongoDB(ctx, config.MongodbConfig.Build())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rdb, err := redisutil.NewRedisClient(ctx, config.RedisConfig.Build())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var srv chatSvr
|
||||
srv.rdb = rdb
|
||||
config.RpcConfig.VerifyCode.Phone.Use = strings.ToLower(config.RpcConfig.VerifyCode.Phone.Use)
|
||||
config.RpcConfig.VerifyCode.Mail.Use = strings.ToLower(config.RpcConfig.VerifyCode.Mail.Use)
|
||||
srv.conf = config.RpcConfig.VerifyCode
|
||||
switch config.RpcConfig.VerifyCode.Phone.Use {
|
||||
case "ali":
|
||||
ali := config.RpcConfig.VerifyCode.Phone.Ali
|
||||
srv.SMS, err = sms.NewAli(ali.Endpoint, ali.AccessKeyID, ali.AccessKeySecret, ali.SignName, ali.VerificationCodeTemplateCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "bao":
|
||||
bao := config.RpcConfig.VerifyCode.Phone.Bao
|
||||
srv.SMS, err = sms.NewBao(bao.Endpoint, bao.AccessKeyID, bao.AccessKeySecret, bao.SignName, bao.VerificationCodeTemplateCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if mail := config.RpcConfig.VerifyCode.Mail; mail.Use == constant.VerifyMail {
|
||||
srv.Mail = email.NewMail(mail.SMTPAddr, mail.SMTPPort, mail.SenderMail, mail.SenderAuthorizationCode, mail.Title)
|
||||
}
|
||||
srv.Database, err = database.NewChatDatabase(mgocli, rdb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn, err := client.GetConn(ctx, config.Discovery.RpcService.Admin, grpc.WithTransportCredentials(insecure.NewCredentials()), mw.GrpcClient())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srv.Admin = chatClient.NewAdminClient(admin.NewAdminClient(conn))
|
||||
srv.Code = verifyCode{
|
||||
UintTime: time.Duration(config.RpcConfig.VerifyCode.UintTime) * time.Second,
|
||||
MaxCount: config.RpcConfig.VerifyCode.MaxCount,
|
||||
ValidCount: config.RpcConfig.VerifyCode.ValidCount,
|
||||
SuperCode: config.RpcConfig.VerifyCode.SuperCode,
|
||||
ValidTime: time.Duration(config.RpcConfig.VerifyCode.ValidTime) * time.Second,
|
||||
Len: config.RpcConfig.VerifyCode.Len,
|
||||
}
|
||||
srv.Livekit = rtc.NewLiveKit(config.RpcConfig.LiveKit.Key, config.RpcConfig.LiveKit.Secret, config.RpcConfig.LiveKit.URL)
|
||||
srv.AllowRegister = config.RpcConfig.AllowRegister
|
||||
chat.RegisterChatServer(server, &srv)
|
||||
return nil
|
||||
}
|
||||
|
||||
type chatSvr struct {
|
||||
chat.UnimplementedChatServer
|
||||
conf config.VerifyCode
|
||||
Database database.ChatDatabaseInterface
|
||||
Admin *chatClient.AdminClient
|
||||
SMS sms.SMS
|
||||
Mail email.Mail
|
||||
Code verifyCode
|
||||
Livekit *rtc.LiveKit
|
||||
ChatAdminUserID string
|
||||
AllowRegister bool
|
||||
rdb redis.UniversalClient
|
||||
}
|
||||
|
||||
func (o *chatSvr) WithAdminUser(ctx context.Context) context.Context {
|
||||
return mctx.WithAdminUser(ctx, o.ChatAdminUserID)
|
||||
}
|
||||
|
||||
type verifyCode struct {
|
||||
UintTime time.Duration // sec
|
||||
MaxCount int
|
||||
ValidCount int
|
||||
SuperCode string
|
||||
ValidTime time.Duration
|
||||
Len int
|
||||
}
|
||||
44
internal/rpc/chat/statistic.go
Normal file
44
internal/rpc/chat/statistic.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// 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 chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/protocol/chat"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
)
|
||||
|
||||
func (o *chatSvr) UserLoginCount(ctx context.Context, req *chat.UserLoginCountReq) (*chat.UserLoginCountResp, error) {
|
||||
resp := &chat.UserLoginCountResp{}
|
||||
if req.Start > req.End {
|
||||
return nil, errs.ErrArgs.WrapMsg("start > end")
|
||||
}
|
||||
total, err := o.Database.NewUserCountTotal(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start := time.UnixMilli(req.Start)
|
||||
end := time.UnixMilli(req.End)
|
||||
count, loginCount, err := o.Database.UserLoginCountRangeEverydayTotal(ctx, &start, &end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.LoginCount = loginCount
|
||||
resp.UnloginCount = total - loginCount
|
||||
resp.Count = count
|
||||
return resp, nil
|
||||
}
|
||||
46
internal/rpc/chat/system_config.go
Normal file
46
internal/rpc/chat/system_config.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// 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 chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
chatpb "git.imall.cloud/openim/chat/pkg/protocol/chat"
|
||||
)
|
||||
|
||||
// GetAppSystemConfigs 获取APP端配置(返回所有 show_in_app=true 且 enabled=true 的配置)
|
||||
func (o *chatSvr) GetAppSystemConfigs(ctx context.Context, req *chatpb.GetAppSystemConfigsReq) (*chatpb.GetAppSystemConfigsResp, error) {
|
||||
// 获取所有 show_in_app=true 且 enabled=true 的配置
|
||||
configs, err := o.Database.GetAppSystemConfigs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
configInfos := make([]*chatpb.SystemConfigInfo, 0, len(configs))
|
||||
for _, config := range configs {
|
||||
configInfos = append(configInfos, &chatpb.SystemConfigInfo{
|
||||
Key: config.Key,
|
||||
Title: config.Title,
|
||||
Value: config.Value,
|
||||
ValueType: config.ValueType,
|
||||
Description: config.Description,
|
||||
})
|
||||
}
|
||||
|
||||
return &chatpb.GetAppSystemConfigsResp{
|
||||
Configs: configInfos,
|
||||
}, nil
|
||||
}
|
||||
132
internal/rpc/chat/update.go
Normal file
132
internal/rpc/chat/update.go
Normal file
@@ -0,0 +1,132 @@
|
||||
// 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 chat
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/common/constant"
|
||||
chatdb "git.imall.cloud/openim/chat/pkg/common/db/table/chat"
|
||||
|
||||
"github.com/openimsdk/tools/errs"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/protocol/chat"
|
||||
)
|
||||
|
||||
func ToDBAttributeUpdate(req *chat.UpdateUserInfoReq) (map[string]any, error) {
|
||||
update := make(map[string]any)
|
||||
if req.Account != nil {
|
||||
update["account"] = req.Account.Value
|
||||
}
|
||||
if req.AreaCode != nil {
|
||||
update["area_code"] = req.AreaCode.Value
|
||||
}
|
||||
if req.Email != nil {
|
||||
update["email"] = req.Email.Value
|
||||
}
|
||||
if req.Nickname != nil {
|
||||
if req.Nickname.Value == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("nickname can not be empty")
|
||||
}
|
||||
update["nickname"] = req.Nickname.Value
|
||||
}
|
||||
if req.FaceURL != nil {
|
||||
update["face_url"] = req.FaceURL.Value
|
||||
}
|
||||
if req.Gender != nil {
|
||||
update["gender"] = req.Gender.Value
|
||||
}
|
||||
if req.Level != nil {
|
||||
update["level"] = req.Level.Value
|
||||
}
|
||||
// userType 现在是 int32 类型,直接使用值
|
||||
update["user_type"] = req.UserType
|
||||
if req.UserFlag != nil {
|
||||
update["user_flag"] = req.UserFlag.Value
|
||||
}
|
||||
|
||||
if req.Birth != nil {
|
||||
update["birth_time"] = time.UnixMilli(req.Birth.Value)
|
||||
}
|
||||
if req.AllowAddFriend != nil {
|
||||
update["allow_add_friend"] = req.AllowAddFriend.Value
|
||||
}
|
||||
if req.AllowBeep != nil {
|
||||
update["allow_beep"] = req.AllowBeep.Value
|
||||
}
|
||||
if req.AllowVibration != nil {
|
||||
update["allow_vibration"] = req.AllowVibration.Value
|
||||
}
|
||||
if req.GlobalRecvMsgOpt != nil {
|
||||
update["global_recv_msg_opt"] = req.GlobalRecvMsgOpt.Value
|
||||
}
|
||||
//if len(update) == 0 {
|
||||
// return nil, errs.ErrArgs.WrapMsg("no update info")
|
||||
//}
|
||||
return update, nil
|
||||
}
|
||||
|
||||
func ToDBCredentialUpdate(req *chat.UpdateUserInfoReq, allowChange bool) ([]*chatdb.Credential, []*chatdb.Credential, error) {
|
||||
update := make([]*chatdb.Credential, 0)
|
||||
del := make([]*chatdb.Credential, 0)
|
||||
if req.Account != nil {
|
||||
if req.Account.GetValue() == "" {
|
||||
del = append(del, &chatdb.Credential{
|
||||
UserID: req.UserID,
|
||||
Type: constant.CredentialAccount,
|
||||
})
|
||||
} else {
|
||||
update = append(update, &chatdb.Credential{
|
||||
UserID: req.UserID,
|
||||
Account: req.Account.GetValue(),
|
||||
Type: constant.CredentialAccount,
|
||||
AllowChange: allowChange,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if req.Email != nil {
|
||||
if req.Email.GetValue() == "" {
|
||||
del = append(del, &chatdb.Credential{
|
||||
UserID: req.UserID,
|
||||
Type: constant.CredentialEmail,
|
||||
})
|
||||
} else {
|
||||
update = append(update, &chatdb.Credential{
|
||||
UserID: req.UserID,
|
||||
Account: req.Email.GetValue(),
|
||||
Type: constant.CredentialEmail,
|
||||
AllowChange: allowChange,
|
||||
})
|
||||
}
|
||||
}
|
||||
if req.PhoneNumber != nil {
|
||||
if req.PhoneNumber.GetValue() == "" {
|
||||
del = append(del, &chatdb.Credential{
|
||||
UserID: req.UserID,
|
||||
Type: constant.CredentialPhone,
|
||||
})
|
||||
} else {
|
||||
update = append(update, &chatdb.Credential{
|
||||
UserID: req.UserID,
|
||||
Account: BuildCredentialPhone(req.AreaCode.GetValue(), req.PhoneNumber.GetValue()),
|
||||
Type: constant.CredentialPhone,
|
||||
AllowChange: allowChange,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return update, del, nil
|
||||
}
|
||||
623
internal/rpc/chat/user.go
Normal file
623
internal/rpc/chat/user.go
Normal file
@@ -0,0 +1,623 @@
|
||||
// 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 chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.imall.cloud/openim/protocol/wrapperspb"
|
||||
"github.com/openimsdk/tools/utils/stringutil"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/common/db/dbutil"
|
||||
chatdb "git.imall.cloud/openim/chat/pkg/common/db/table/chat"
|
||||
constantpb "git.imall.cloud/openim/protocol/constant"
|
||||
"github.com/openimsdk/tools/mcontext"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/common/constant"
|
||||
"git.imall.cloud/openim/chat/pkg/common/mctx"
|
||||
"git.imall.cloud/openim/chat/pkg/eerrs"
|
||||
"git.imall.cloud/openim/chat/pkg/protocol/chat"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
)
|
||||
|
||||
func (o *chatSvr) checkUpdateInfo(ctx context.Context, req *chat.UpdateUserInfoReq) error {
|
||||
if req.AreaCode != nil || req.PhoneNumber != nil {
|
||||
if !(req.AreaCode != nil && req.PhoneNumber != nil) {
|
||||
return errs.ErrArgs.WrapMsg("areaCode and phoneNumber must be set together")
|
||||
}
|
||||
if req.AreaCode.Value == "" || req.PhoneNumber.Value == "" {
|
||||
if req.AreaCode.Value != req.PhoneNumber.Value {
|
||||
return errs.ErrArgs.WrapMsg("areaCode and phoneNumber must be set together")
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.UserID == "" {
|
||||
return errs.ErrArgs.WrapMsg("user id is empty")
|
||||
}
|
||||
|
||||
credentials, err := o.Database.TakeCredentialsByUserID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(credentials) == 0 {
|
||||
return errs.ErrArgs.WrapMsg("user not found")
|
||||
}
|
||||
var (
|
||||
credNum, delNum, addNum = len(credentials), 0, 0
|
||||
)
|
||||
|
||||
addFunc := func(s *wrapperspb.StringValue) {
|
||||
if s != nil {
|
||||
if s.Value != "" {
|
||||
addNum++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range []*wrapperspb.StringValue{req.Account, req.PhoneNumber, req.Email} {
|
||||
addFunc(s)
|
||||
}
|
||||
|
||||
for _, credential := range credentials {
|
||||
switch credential.Type {
|
||||
case constant.CredentialAccount:
|
||||
if req.Account != nil {
|
||||
if req.Account.Value == credential.Account {
|
||||
req.Account = nil
|
||||
} else if req.Account.Value == "" {
|
||||
delNum += 1
|
||||
}
|
||||
}
|
||||
case constant.CredentialPhone:
|
||||
if req.PhoneNumber != nil {
|
||||
phoneAccount := BuildCredentialPhone(req.AreaCode.Value, req.PhoneNumber.Value)
|
||||
if phoneAccount == credential.Account {
|
||||
req.AreaCode = nil
|
||||
req.PhoneNumber = nil
|
||||
} else if req.PhoneNumber.Value == "" {
|
||||
delNum += 1
|
||||
}
|
||||
}
|
||||
case constant.CredentialEmail:
|
||||
if req.Email != nil {
|
||||
if req.Email.Value == credential.Account {
|
||||
req.Email = nil
|
||||
} else if req.Email.Value == "" {
|
||||
delNum += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if addNum+credNum-delNum <= 0 {
|
||||
return errs.ErrArgs.WrapMsg("a login method must exist")
|
||||
}
|
||||
|
||||
if req.PhoneNumber.GetValue() != "" {
|
||||
if !strings.HasPrefix(req.AreaCode.GetValue(), "+") {
|
||||
req.AreaCode.Value = "+" + req.AreaCode.Value
|
||||
}
|
||||
if _, err := strconv.ParseUint(req.AreaCode.Value[1:], 10, 64); err != nil {
|
||||
return errs.ErrArgs.WrapMsg("area code must be number")
|
||||
}
|
||||
if _, err := strconv.ParseUint(req.PhoneNumber.GetValue(), 10, 64); err != nil {
|
||||
return errs.ErrArgs.WrapMsg("phone number must be number")
|
||||
}
|
||||
phoneAccount := BuildCredentialPhone(req.AreaCode.GetValue(), req.PhoneNumber.GetValue())
|
||||
existingCredential, err := o.Database.TakeCredentialByAccount(ctx, phoneAccount)
|
||||
if err == nil {
|
||||
// 如果手机号已存在,检查是否是当前用户的手机号
|
||||
if existingCredential.UserID == req.UserID {
|
||||
// 是当前用户的手机号,允许更新(实际上是相同值,不需要更新)
|
||||
req.AreaCode = nil
|
||||
req.PhoneNumber = nil
|
||||
} else {
|
||||
// 是其他用户的手机号,返回错误
|
||||
return eerrs.ErrPhoneAlreadyRegister.Wrap()
|
||||
}
|
||||
} else if !dbutil.IsDBNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if req.Account.GetValue() != "" {
|
||||
accountValue := req.Account.GetValue()
|
||||
// 验证长度:6到20位
|
||||
if len(accountValue) < 6 || len(accountValue) > 20 {
|
||||
return errs.ErrArgs.WrapMsg("account must be between 6 and 20 characters")
|
||||
}
|
||||
// 验证格式:只能包含数字、字母、下划线(_)、横线(-)
|
||||
pattern := `^[a-zA-Z0-9_-]+$`
|
||||
matched, err := regexp.MatchString(pattern, accountValue)
|
||||
if err != nil || !matched {
|
||||
return errs.ErrArgs.WrapMsg("account must contain only letters, numbers, underscores, and hyphens")
|
||||
}
|
||||
existingCredential, err := o.Database.TakeCredentialByAccount(ctx, accountValue)
|
||||
if err == nil {
|
||||
// 如果账户已存在,检查是否是当前用户的账户
|
||||
if existingCredential.UserID == req.UserID {
|
||||
// 是当前用户的账户,允许更新(实际上是相同值,不需要更新)
|
||||
req.Account = nil
|
||||
|
||||
} else {
|
||||
// 是其他用户的账户,返回错误
|
||||
return eerrs.ErrAccountAlreadyRegister.Wrap()
|
||||
}
|
||||
} else if !dbutil.IsDBNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if req.Email.GetValue() != "" {
|
||||
if !stringutil.IsValidEmail(req.Email.GetValue()) {
|
||||
return errs.ErrArgs.WrapMsg("invalid email")
|
||||
}
|
||||
existingCredential, err := o.Database.TakeCredentialByAccount(ctx, req.Email.GetValue())
|
||||
if err == nil {
|
||||
// 如果邮箱已存在,检查是否是当前用户的邮箱
|
||||
if existingCredential.UserID == req.UserID {
|
||||
// 是当前用户的邮箱,允许更新(实际上是相同值,不需要更新)
|
||||
req.Email = nil
|
||||
} else {
|
||||
// 是其他用户的邮箱,返回错误
|
||||
return eerrs.ErrEmailAlreadyRegister.Wrap()
|
||||
}
|
||||
} else if !dbutil.IsDBNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *chatSvr) UpdateUserInfo(ctx context.Context, req *chat.UpdateUserInfoReq) (*chat.UpdateUserInfoResp, error) {
|
||||
opUserID, userType, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = o.checkUpdateInfo(ctx, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch userType {
|
||||
case constant.NormalUser:
|
||||
if req.RegisterType != nil {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("registerType can not be updated")
|
||||
}
|
||||
if req.UserID != opUserID {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("only admin can update other user info")
|
||||
}
|
||||
// 普通用户不能修改自己的用户类型
|
||||
if req.UserType != 0 {
|
||||
return nil, errs.ErrNoPermission.WrapMsg("normal user can not update userType")
|
||||
}
|
||||
|
||||
case constant.AdminUser:
|
||||
// 管理员可以修改用户类型,但需要验证值
|
||||
if req.UserType < 0 || req.UserType > 3 {
|
||||
return nil, errs.ErrArgs.WrapMsg("invalid userType: must be 0-3")
|
||||
}
|
||||
default:
|
||||
return nil, errs.ErrNoPermission.WrapMsg("user type error")
|
||||
}
|
||||
|
||||
update, err := ToDBAttributeUpdate(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if userType == constant.NormalUser {
|
||||
delete(update, "user_flag")
|
||||
delete(update, "user_type")
|
||||
}
|
||||
|
||||
credUpdate, credDel, err := ToDBCredentialUpdate(req, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(update) > 0 {
|
||||
if err := o.Database.UpdateUseInfo(ctx, req.UserID, update, credUpdate, credDel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &chat.UpdateUserInfoResp{}, nil
|
||||
}
|
||||
|
||||
func (o *chatSvr) FindUserPublicInfo(ctx context.Context, req *chat.FindUserPublicInfoReq) (*chat.FindUserPublicInfoResp, error) {
|
||||
if len(req.UserIDs) == 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("UserIDs is empty")
|
||||
}
|
||||
attributes, err := o.Database.FindAttribute(ctx, req.UserIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &chat.FindUserPublicInfoResp{
|
||||
Users: DbToPbAttributes(attributes),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (o *chatSvr) AddUserAccount(ctx context.Context, req *chat.AddUserAccountReq) (*chat.AddUserAccountResp, error) {
|
||||
if _, _, err := mctx.Check(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := o.checkRegisterInfo(ctx, req.User, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.User.UserID == "" {
|
||||
for i := 0; i < 20; i++ {
|
||||
userID := o.genUserID()
|
||||
_, err := o.Database.GetUser(ctx, userID)
|
||||
if err == nil {
|
||||
continue
|
||||
} else if dbutil.IsDBNotFound(err) {
|
||||
req.User.UserID = userID
|
||||
break
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if req.User.UserID == "" {
|
||||
return nil, errs.ErrInternalServer.WrapMsg("gen user id failed")
|
||||
}
|
||||
} else {
|
||||
_, err := o.Database.GetUser(ctx, req.User.UserID)
|
||||
if err == nil {
|
||||
return nil, errs.ErrArgs.WrapMsg("appoint user id already register")
|
||||
} else if !dbutil.IsDBNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
credentials []*chatdb.Credential
|
||||
)
|
||||
|
||||
if req.User.PhoneNumber != "" {
|
||||
credentials = append(credentials, &chatdb.Credential{
|
||||
UserID: req.User.UserID,
|
||||
Account: BuildCredentialPhone(req.User.AreaCode, req.User.PhoneNumber),
|
||||
Type: constant.CredentialPhone,
|
||||
AllowChange: true,
|
||||
})
|
||||
}
|
||||
|
||||
if req.User.Account != "" {
|
||||
credentials = append(credentials, &chatdb.Credential{
|
||||
UserID: req.User.UserID,
|
||||
Account: req.User.Account,
|
||||
Type: constant.CredentialAccount,
|
||||
AllowChange: true,
|
||||
})
|
||||
}
|
||||
|
||||
if req.User.Email != "" {
|
||||
credentials = append(credentials, &chatdb.Credential{
|
||||
UserID: req.User.UserID,
|
||||
Account: req.User.Email,
|
||||
Type: constant.CredentialEmail,
|
||||
AllowChange: true,
|
||||
})
|
||||
}
|
||||
|
||||
register := &chatdb.Register{
|
||||
UserID: req.User.UserID,
|
||||
DeviceID: req.DeviceID,
|
||||
IP: req.Ip,
|
||||
Platform: constantpb.PlatformID2Name[int(req.Platform)],
|
||||
AccountType: "",
|
||||
Mode: constant.UserMode,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
account := &chatdb.Account{
|
||||
UserID: req.User.UserID,
|
||||
Password: req.User.Password,
|
||||
OperatorUserID: mcontext.GetOpUserID(ctx),
|
||||
ChangeTime: register.CreateTime,
|
||||
CreateTime: register.CreateTime,
|
||||
}
|
||||
attribute := &chatdb.Attribute{
|
||||
UserID: req.User.UserID,
|
||||
Account: req.User.Account,
|
||||
PhoneNumber: req.User.PhoneNumber,
|
||||
AreaCode: req.User.AreaCode,
|
||||
Email: req.User.Email,
|
||||
Nickname: req.User.Nickname,
|
||||
FaceURL: req.User.FaceURL,
|
||||
Gender: req.User.Gender,
|
||||
BirthTime: time.UnixMilli(req.User.Birth),
|
||||
ChangeTime: register.CreateTime,
|
||||
CreateTime: register.CreateTime,
|
||||
AllowVibration: constant.DefaultAllowVibration,
|
||||
AllowBeep: constant.DefaultAllowBeep,
|
||||
AllowAddFriend: constant.DefaultAllowAddFriend,
|
||||
}
|
||||
|
||||
if err := o.Database.RegisterUser(ctx, register, account, attribute, credentials); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &chat.AddUserAccountResp{}, nil
|
||||
}
|
||||
|
||||
func (o *chatSvr) SearchUserPublicInfo(ctx context.Context, req *chat.SearchUserPublicInfoReq) (*chat.SearchUserPublicInfoResp, error) {
|
||||
if _, _, err := mctx.Check(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
total, list, err := o.Database.Search(ctx, constant.FinDAllUser, req.Keyword, req.Genders, nil, nil, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &chat.SearchUserPublicInfoResp{
|
||||
Total: uint32(total),
|
||||
Users: DbToPbAttributes(list),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (o *chatSvr) FindUserFullInfo(ctx context.Context, req *chat.FindUserFullInfoReq) (*chat.FindUserFullInfoResp, error) {
|
||||
if _, _, err := mctx.Check(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(req.UserIDs) == 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("UserIDs is empty")
|
||||
}
|
||||
attributes, err := o.Database.FindAttribute(ctx, req.UserIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取每个用户的最新登录IP
|
||||
userIPMap := make(map[string]string)
|
||||
for _, attr := range attributes {
|
||||
ip, err := o.Database.GetLatestLoginIP(ctx, attr.UserID)
|
||||
if err != nil {
|
||||
// 如果获取IP失败,记录错误但继续处理其他用户
|
||||
continue
|
||||
}
|
||||
userIPMap[attr.UserID] = ip
|
||||
}
|
||||
|
||||
return &chat.FindUserFullInfoResp{Users: DbToPbUserFullInfosWithIP(attributes, userIPMap)}, nil
|
||||
}
|
||||
|
||||
func (o *chatSvr) SearchUserFullInfo(ctx context.Context, req *chat.SearchUserFullInfoReq) (*chat.SearchUserFullInfoResp, error) {
|
||||
if _, _, err := mctx.Check(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 解析时间戳为 time.Time(毫秒时间戳)
|
||||
var startTime, endTime *time.Time
|
||||
if req.StartTime > 0 {
|
||||
st := time.UnixMilli(req.StartTime)
|
||||
startTime = &st
|
||||
}
|
||||
if req.EndTime > 0 {
|
||||
// 将endTime加1000毫秒,确保包含到当天的最后一毫秒
|
||||
// 例如:endTime=1727740799000 (2025-11-01 23:59:59) 会被转换为 1727740800000 (2025-11-02 00:00:00)
|
||||
// 这样使用 $lt 查询时,会包含 2025-11-01 23:59:59.999 但不包含 2025-11-02 00:00:00
|
||||
et := time.UnixMilli(req.EndTime + 1000)
|
||||
endTime = &et
|
||||
}
|
||||
// 使用支持实名信息搜索的方法
|
||||
total, list, err := o.Database.SearchWithRealNameAuth(ctx, req.Normal, req.Keyword, req.Genders, startTime, endTime, req.RealNameKeyword, req.IdCardKeyword, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 批量获取钱包信息(用于填充实名信息)
|
||||
userIDs := make([]string, 0, len(list))
|
||||
for _, attr := range list {
|
||||
userIDs = append(userIDs, attr.UserID)
|
||||
}
|
||||
walletMap := make(map[string]*chatdb.Wallet)
|
||||
if len(userIDs) > 0 {
|
||||
wallets, err := o.Database.GetWalletsByUserIDs(ctx, userIDs)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "Failed to get wallets for user search", err, "userIDs", userIDs)
|
||||
} else {
|
||||
for _, wallet := range wallets {
|
||||
walletMap[wallet.UserID] = wallet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取每个用户的最新登录IP
|
||||
userIPMap := make(map[string]string)
|
||||
for _, attr := range list {
|
||||
ip, err := o.Database.GetLatestLoginIP(ctx, attr.UserID)
|
||||
if err != nil {
|
||||
// 如果获取IP失败,记录错误但继续处理其他用户
|
||||
log.ZWarn(ctx, "Failed to get latest login IP for user", err, "userID", attr.UserID)
|
||||
// 即使出错也设置空字符串,确保map中有该用户的记录
|
||||
userIPMap[attr.UserID] = ""
|
||||
continue
|
||||
}
|
||||
// 记录获取到的IP(用于调试)
|
||||
if ip != "" {
|
||||
log.ZDebug(ctx, "Got latest login IP for user", "userID", attr.UserID, "ip", ip)
|
||||
} else {
|
||||
log.ZDebug(ctx, "No login IP found for user (empty string)", "userID", attr.UserID)
|
||||
}
|
||||
userIPMap[attr.UserID] = ip
|
||||
}
|
||||
// 统计有IP的用户数量
|
||||
usersWithIP := 0
|
||||
for _, ip := range userIPMap {
|
||||
if ip != "" {
|
||||
usersWithIP++
|
||||
}
|
||||
}
|
||||
log.ZInfo(ctx, "User IP map summary", "totalUsers", len(list), "ipMapSize", len(userIPMap), "usersWithIP", usersWithIP)
|
||||
|
||||
return &chat.SearchUserFullInfoResp{
|
||||
Total: uint32(total),
|
||||
Users: DbToPbUserFullInfosWithRealNameAuthAndIP(list, walletMap, userIPMap),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserLoginRecords 查询用户登录记录
|
||||
func (o *chatSvr) GetUserLoginRecords(ctx context.Context, req *chat.GetUserLoginRecordsReq) (*chat.GetUserLoginRecordsResp, error) {
|
||||
// 检查管理员权限
|
||||
if _, err := mctx.CheckAdmin(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询登录记录
|
||||
total, records, err := o.Database.SearchUserLoginRecords(ctx, req.UserId, req.Ip, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 收集所有用户ID
|
||||
userIDs := make([]string, 0, len(records))
|
||||
userIDSet := make(map[string]bool)
|
||||
for _, record := range records {
|
||||
if !userIDSet[record.UserID] {
|
||||
userIDs = append(userIDs, record.UserID)
|
||||
userIDSet[record.UserID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 批量获取用户属性(头像和昵称)
|
||||
userAttrMap := make(map[string]*chatdb.Attribute)
|
||||
if len(userIDs) > 0 {
|
||||
attributes, err := o.Database.FindAttribute(ctx, userIDs)
|
||||
if err != nil {
|
||||
log.ZWarn(ctx, "Failed to get user attributes for login records", err, "userIDs", userIDs)
|
||||
} else {
|
||||
for _, attr := range attributes {
|
||||
userAttrMap[attr.UserID] = attr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 转换结果
|
||||
var recordInfos []*chat.UserLoginRecordInfo
|
||||
for _, record := range records {
|
||||
recordInfo := &chat.UserLoginRecordInfo{
|
||||
UserId: record.UserID,
|
||||
LoginTime: record.LoginTime.UnixMilli(),
|
||||
Ip: record.IP,
|
||||
DeviceId: record.DeviceID,
|
||||
Platform: record.Platform,
|
||||
}
|
||||
// 填充用户头像和昵称
|
||||
if attr, ok := userAttrMap[record.UserID]; ok {
|
||||
recordInfo.FaceUrl = attr.FaceURL
|
||||
recordInfo.Nickname = attr.Nickname
|
||||
}
|
||||
recordInfos = append(recordInfos, recordInfo)
|
||||
}
|
||||
|
||||
return &chat.GetUserLoginRecordsResp{
|
||||
Total: uint32(total),
|
||||
Records: recordInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (o *chatSvr) FindUserAccount(ctx context.Context, req *chat.FindUserAccountReq) (*chat.FindUserAccountResp, error) {
|
||||
if len(req.UserIDs) == 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("user id list must be set")
|
||||
}
|
||||
if _, _, err := mctx.CheckAdminOrUser(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attributes, err := o.Database.FindAttribute(ctx, req.UserIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userAccountMap := make(map[string]string)
|
||||
for _, attribute := range attributes {
|
||||
userAccountMap[attribute.UserID] = attribute.Account
|
||||
}
|
||||
return &chat.FindUserAccountResp{UserAccountMap: userAccountMap}, nil
|
||||
}
|
||||
|
||||
func (o *chatSvr) FindAccountUser(ctx context.Context, req *chat.FindAccountUserReq) (*chat.FindAccountUserResp, error) {
|
||||
if len(req.Accounts) == 0 {
|
||||
return nil, errs.ErrArgs.WrapMsg("account list must be set")
|
||||
}
|
||||
if _, _, err := mctx.CheckAdminOrUser(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attributes, err := o.Database.FindAttribute(ctx, req.Accounts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accountUserMap := make(map[string]string)
|
||||
for _, attribute := range attributes {
|
||||
accountUserMap[attribute.Account] = attribute.UserID
|
||||
}
|
||||
return &chat.FindAccountUserResp{AccountUserMap: accountUserMap}, nil
|
||||
}
|
||||
|
||||
func (o *chatSvr) SearchUserInfo(ctx context.Context, req *chat.SearchUserInfoReq) (*chat.SearchUserInfoResp, error) {
|
||||
if _, _, err := mctx.Check(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
total, list, err := o.Database.SearchUser(ctx, req.Keyword, req.UserIDs, req.Genders, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &chat.SearchUserInfoResp{
|
||||
Total: uint32(total),
|
||||
Users: DbToPbUserFullInfos(list),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (o *chatSvr) CheckUserExist(ctx context.Context, req *chat.CheckUserExistReq) (resp *chat.CheckUserExistResp, err error) {
|
||||
if req.User == nil {
|
||||
return nil, errs.ErrArgs.WrapMsg("user is nil")
|
||||
}
|
||||
if req.User.PhoneNumber != "" {
|
||||
account, err := o.Database.TakeCredentialByAccount(ctx, BuildCredentialPhone(req.User.AreaCode, req.User.PhoneNumber))
|
||||
// err != nil is not found User
|
||||
if err != nil && !errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return nil, err
|
||||
}
|
||||
if account != nil {
|
||||
return &chat.CheckUserExistResp{Userid: account.UserID, IsRegistered: true}, nil
|
||||
}
|
||||
}
|
||||
if req.User.Email != "" {
|
||||
account, err := o.Database.TakeCredentialByAccount(ctx, req.User.AreaCode)
|
||||
if err != nil && !errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return nil, err
|
||||
}
|
||||
if account != nil {
|
||||
return &chat.CheckUserExistResp{Userid: account.UserID, IsRegistered: true}, nil
|
||||
}
|
||||
}
|
||||
if req.User.Account != "" {
|
||||
account, err := o.Database.TakeCredentialByAccount(ctx, req.User.Account)
|
||||
if err != nil && !errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return nil, err
|
||||
}
|
||||
if account != nil {
|
||||
return &chat.CheckUserExistResp{Userid: account.UserID, IsRegistered: true}, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (o *chatSvr) DelUserAccount(ctx context.Context, req *chat.DelUserAccountReq) (resp *chat.DelUserAccountResp, err error) {
|
||||
if err := o.Database.DelUserAccount(ctx, req.UserIDs); err != nil && errs.Unwrap(err) != mongo.ErrNoDocuments {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
202
internal/rpc/chat/utils.go
Normal file
202
internal/rpc/chat/utils.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.imall.cloud/openim/chat/pkg/common/db/dbutil"
|
||||
table "git.imall.cloud/openim/chat/pkg/common/db/table/chat"
|
||||
"git.imall.cloud/openim/chat/pkg/eerrs"
|
||||
"git.imall.cloud/openim/chat/pkg/protocol/chat"
|
||||
"git.imall.cloud/openim/chat/pkg/protocol/common"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/utils/datautil"
|
||||
"github.com/openimsdk/tools/utils/stringutil"
|
||||
)
|
||||
|
||||
func DbToPbAttribute(attribute *table.Attribute) *common.UserPublicInfo {
|
||||
if attribute == nil {
|
||||
return nil
|
||||
}
|
||||
return &common.UserPublicInfo{
|
||||
UserID: attribute.UserID,
|
||||
Account: attribute.Account,
|
||||
Email: attribute.Email,
|
||||
Nickname: attribute.Nickname,
|
||||
FaceURL: attribute.FaceURL,
|
||||
Gender: attribute.Gender,
|
||||
Level: attribute.Level,
|
||||
UserType: attribute.UserType,
|
||||
}
|
||||
}
|
||||
|
||||
func DbToPbAttributes(attributes []*table.Attribute) []*common.UserPublicInfo {
|
||||
return datautil.Slice(attributes, DbToPbAttribute)
|
||||
}
|
||||
|
||||
func DbToPbUserFullInfo(attribute *table.Attribute) *common.UserFullInfo {
|
||||
return &common.UserFullInfo{
|
||||
UserID: attribute.UserID,
|
||||
Password: "",
|
||||
Account: attribute.Account,
|
||||
PhoneNumber: attribute.PhoneNumber,
|
||||
AreaCode: attribute.AreaCode,
|
||||
Email: attribute.Email,
|
||||
Nickname: attribute.Nickname,
|
||||
FaceURL: attribute.FaceURL,
|
||||
Gender: attribute.Gender,
|
||||
Level: attribute.Level,
|
||||
UserType: attribute.UserType,
|
||||
Birth: attribute.BirthTime.UnixMilli(),
|
||||
AllowAddFriend: attribute.AllowAddFriend,
|
||||
AllowBeep: attribute.AllowBeep,
|
||||
AllowVibration: attribute.AllowVibration,
|
||||
GlobalRecvMsgOpt: attribute.GlobalRecvMsgOpt,
|
||||
RegisterType: attribute.RegisterType,
|
||||
UserFlag: attribute.UserFlag,
|
||||
CreateTime: attribute.CreateTime.UnixMilli(),
|
||||
Ip: "", // 默认空字符串
|
||||
}
|
||||
}
|
||||
|
||||
func DbToPbUserFullInfos(attributes []*table.Attribute) []*common.UserFullInfo {
|
||||
return datautil.Slice(attributes, DbToPbUserFullInfo)
|
||||
}
|
||||
|
||||
func DbToPbUserFullInfoWithIP(attribute *table.Attribute, ip string) *common.UserFullInfo {
|
||||
return &common.UserFullInfo{
|
||||
UserID: attribute.UserID,
|
||||
Password: "",
|
||||
Account: attribute.Account,
|
||||
PhoneNumber: attribute.PhoneNumber,
|
||||
AreaCode: attribute.AreaCode,
|
||||
Email: attribute.Email,
|
||||
Nickname: attribute.Nickname,
|
||||
FaceURL: attribute.FaceURL,
|
||||
Gender: attribute.Gender,
|
||||
Level: attribute.Level,
|
||||
UserType: attribute.UserType,
|
||||
Birth: attribute.BirthTime.UnixMilli(),
|
||||
AllowAddFriend: attribute.AllowAddFriend,
|
||||
AllowBeep: attribute.AllowBeep,
|
||||
AllowVibration: attribute.AllowVibration,
|
||||
GlobalRecvMsgOpt: attribute.GlobalRecvMsgOpt,
|
||||
RegisterType: attribute.RegisterType,
|
||||
UserFlag: attribute.UserFlag,
|
||||
CreateTime: attribute.CreateTime.UnixMilli(),
|
||||
Ip: ip,
|
||||
}
|
||||
}
|
||||
|
||||
func DbToPbUserFullInfosWithIP(attributes []*table.Attribute, userIPMap map[string]string) []*common.UserFullInfo {
|
||||
result := make([]*common.UserFullInfo, 0, len(attributes))
|
||||
for _, attr := range attributes {
|
||||
ip := userIPMap[attr.UserID]
|
||||
result = append(result, DbToPbUserFullInfoWithIP(attr, ip))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// DbToPbUserFullInfosWithRealNameAuth 转换用户信息(包含实名认证信息)
|
||||
func DbToPbUserFullInfosWithRealNameAuth(attributes []*table.Attribute, walletMap map[string]*table.Wallet) []*common.UserFullInfo {
|
||||
result := make([]*common.UserFullInfo, 0, len(attributes))
|
||||
for _, attr := range attributes {
|
||||
userInfo := DbToPbUserFullInfo(attr)
|
||||
// 填充实名认证信息
|
||||
if wallet, ok := walletMap[attr.UserID]; ok {
|
||||
userInfo.IdCard = wallet.RealNameAuth.IDCard
|
||||
userInfo.RealName = wallet.RealNameAuth.Name
|
||||
userInfo.IdCardPhotoFront = wallet.RealNameAuth.IDCardPhotoFront
|
||||
userInfo.IdCardPhotoBack = wallet.RealNameAuth.IDCardPhotoBack
|
||||
userInfo.AuditStatus = wallet.RealNameAuth.AuditStatus
|
||||
}
|
||||
result = append(result, userInfo)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// DbToPbUserFullInfosWithRealNameAuthAndIP 转换用户信息(包含实名认证信息和IP)
|
||||
func DbToPbUserFullInfosWithRealNameAuthAndIP(attributes []*table.Attribute, walletMap map[string]*table.Wallet, userIPMap map[string]string) []*common.UserFullInfo {
|
||||
result := make([]*common.UserFullInfo, 0, len(attributes))
|
||||
for _, attr := range attributes {
|
||||
ip := userIPMap[attr.UserID]
|
||||
userInfo := DbToPbUserFullInfoWithIP(attr, ip)
|
||||
// 填充实名认证信息
|
||||
if wallet, ok := walletMap[attr.UserID]; ok {
|
||||
userInfo.IdCard = wallet.RealNameAuth.IDCard
|
||||
userInfo.RealName = wallet.RealNameAuth.Name
|
||||
userInfo.IdCardPhotoFront = wallet.RealNameAuth.IDCardPhotoFront
|
||||
userInfo.IdCardPhotoBack = wallet.RealNameAuth.IDCardPhotoBack
|
||||
userInfo.AuditStatus = wallet.RealNameAuth.AuditStatus
|
||||
}
|
||||
result = append(result, userInfo)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func BuildCredentialPhone(areaCode, phone string) string {
|
||||
return areaCode + " " + phone
|
||||
}
|
||||
|
||||
func (o *chatSvr) checkRegisterInfo(ctx context.Context, user *chat.RegisterUserInfo, isAdmin bool) error {
|
||||
if user == nil {
|
||||
return errs.ErrArgs.WrapMsg("user is nil")
|
||||
}
|
||||
user.Account = strings.TrimSpace(user.Account)
|
||||
// 如果提供了account,则不需要验证phone和email
|
||||
if user.Account != "" {
|
||||
// account验证逻辑在后面,这里直接跳过"至少需要一个账号"的检查
|
||||
} else if user.Email == "" && !(user.PhoneNumber != "" && user.AreaCode != "") && !isAdmin {
|
||||
// 如果没有account,也没有email,也没有完整的phone(phoneNumber和areaCode都提供),且不是管理员,则报错
|
||||
return errs.ErrArgs.WrapMsg("at least one valid account is required")
|
||||
}
|
||||
if user.PhoneNumber != "" {
|
||||
if !strings.HasPrefix(user.AreaCode, "+") {
|
||||
user.AreaCode = "+" + user.AreaCode
|
||||
}
|
||||
if _, err := strconv.ParseUint(user.AreaCode[1:], 10, 64); err != nil {
|
||||
return errs.ErrArgs.WrapMsg("area code must be number")
|
||||
}
|
||||
if _, err := strconv.ParseUint(user.PhoneNumber, 10, 64); err != nil {
|
||||
return errs.ErrArgs.WrapMsg("phone number must be number")
|
||||
}
|
||||
_, err := o.Database.TakeAttributeByPhone(ctx, user.AreaCode, user.PhoneNumber)
|
||||
if err == nil {
|
||||
return eerrs.ErrPhoneAlreadyRegister.Wrap()
|
||||
} else if !dbutil.IsDBNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if user.Account != "" {
|
||||
// 验证长度:6到20位
|
||||
if len(user.Account) < 6 || len(user.Account) > 20 {
|
||||
return errs.ErrArgs.WrapMsg("account must be between 6 and 20 characters")
|
||||
}
|
||||
// 验证格式:只能包含数字、字母、下划线(_)、横线(-)
|
||||
pattern := `^[a-zA-Z0-9_-]+$`
|
||||
matched, err := regexp.MatchString(pattern, user.Account)
|
||||
if err != nil || !matched {
|
||||
return errs.ErrArgs.WrapMsg("account must contain only letters, numbers, underscores, and hyphens")
|
||||
}
|
||||
_, err = o.Database.TakeAttributeByAccount(ctx, user.Account)
|
||||
if err == nil {
|
||||
return eerrs.ErrAccountAlreadyRegister.Wrap()
|
||||
} else if !dbutil.IsDBNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if user.Email != "" {
|
||||
if !stringutil.IsValidEmail(user.Email) {
|
||||
return errs.ErrArgs.WrapMsg("invalid email")
|
||||
}
|
||||
_, err := o.Database.TakeAttributeByAccount(ctx, user.Email)
|
||||
if err == nil {
|
||||
return eerrs.ErrEmailAlreadyRegister.Wrap()
|
||||
} else if !dbutil.IsDBNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
753
internal/rpc/chat/wallet.go
Normal file
753
internal/rpc/chat/wallet.go
Normal file
@@ -0,0 +1,753 @@
|
||||
// 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 chat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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/common/util"
|
||||
"git.imall.cloud/openim/chat/pkg/eerrs"
|
||||
chatpb "git.imall.cloud/openim/chat/pkg/protocol/chat"
|
||||
"github.com/google/uuid"
|
||||
"github.com/openimsdk/tools/errs"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
// GetWalletBalance 获取钱包余额
|
||||
func (o *chatSvr) GetWalletBalance(ctx context.Context, req *chatpb.GetWalletBalanceReq) (*chatpb.GetWalletBalanceResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取钱包信息
|
||||
wallet, err := o.Database.GetWallet(ctx, userID)
|
||||
if err != nil {
|
||||
// 如果钱包不存在,返回余额为0
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return &chatpb.GetWalletBalanceResp{
|
||||
Balance: 0,
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.GetWalletBalanceResp{
|
||||
Balance: wallet.Balance,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetWalletInfo 获取钱包详细信息
|
||||
func (o *chatSvr) GetWalletInfo(ctx context.Context, req *chatpb.GetWalletInfoReq) (*chatpb.GetWalletInfoResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取钱包信息
|
||||
wallet, err := o.Database.GetWallet(ctx, userID)
|
||||
if err != nil {
|
||||
// 如果钱包不存在,返回默认值
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return &chatpb.GetWalletInfoResp{
|
||||
Balance: 0,
|
||||
WithdrawAccount: "",
|
||||
WithdrawAccountType: 0,
|
||||
RealNameAuth: nil,
|
||||
WithdrawReceiveAccount: "",
|
||||
HasPaymentPassword: false,
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换实名认证信息
|
||||
var realNameAuth *chatpb.RealNameAuthInfo
|
||||
if wallet.RealNameAuth.IDCard != "" {
|
||||
realNameAuth = &chatpb.RealNameAuthInfo{
|
||||
IdCard: wallet.RealNameAuth.IDCard,
|
||||
IdCardPhotoFront: wallet.RealNameAuth.IDCardPhotoFront,
|
||||
IdCardPhotoBack: wallet.RealNameAuth.IDCardPhotoBack,
|
||||
Name: wallet.RealNameAuth.Name,
|
||||
AuditStatus: wallet.RealNameAuth.AuditStatus,
|
||||
}
|
||||
}
|
||||
|
||||
return &chatpb.GetWalletInfoResp{
|
||||
Balance: wallet.Balance,
|
||||
WithdrawAccount: wallet.WithdrawAccount,
|
||||
WithdrawAccountType: wallet.WithdrawAccountType,
|
||||
RealNameAuth: realNameAuth,
|
||||
WithdrawReceiveAccount: wallet.WithdrawReceiveAccount,
|
||||
HasPaymentPassword: wallet.PaymentPassword != "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetWalletBalanceRecords 获取余额明细(余额变动记录)
|
||||
func (o *chatSvr) GetWalletBalanceRecords(ctx context.Context, req *chatpb.GetWalletBalanceRecordsReq) (*chatpb.GetWalletBalanceRecordsResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var total int64
|
||||
var records []*chatdb.WalletBalanceRecord
|
||||
|
||||
// 根据类型查询或查询所有
|
||||
if req.Type > 0 {
|
||||
// 按类型查询
|
||||
total, records, err = o.Database.GetWalletBalanceRecordsByUserIDAndType(ctx, userID, req.Type, req.Pagination)
|
||||
} else {
|
||||
// 查询所有
|
||||
total, records, err = o.Database.GetWalletBalanceRecords(ctx, userID, req.Pagination)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
recordInfos := make([]*chatpb.WalletBalanceRecordInfo, 0, len(records))
|
||||
for _, record := range records {
|
||||
recordInfos = append(recordInfos, &chatpb.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 &chatpb.GetWalletBalanceRecordsResp{
|
||||
Total: uint32(total),
|
||||
Records: recordInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetWithdrawAccount 设置提现账号
|
||||
func (o *chatSvr) SetWithdrawAccount(ctx context.Context, req *chatpb.SetWithdrawAccountReq) (*chatpb.SetWithdrawAccountResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.Account == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("提现账号不能为空")
|
||||
}
|
||||
if req.AccountType <= 0 || req.AccountType > 3 {
|
||||
return nil, errs.ErrArgs.WrapMsg("账号类型无效,必须是1-支付宝,2-微信,3-银行卡")
|
||||
}
|
||||
|
||||
// 更新提现账号
|
||||
if err := o.Database.UpdateWalletWithdrawAccountWithType(ctx, userID, req.Account, req.AccountType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.SetWithdrawAccountResp{}, nil
|
||||
}
|
||||
|
||||
// SetPaymentPassword 设置支付密码(首次设置或修改)
|
||||
func (o *chatSvr) SetPaymentPassword(ctx context.Context, req *chatpb.SetPaymentPasswordReq) (*chatpb.SetPaymentPasswordResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.NewPassword == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("新支付密码不能为空")
|
||||
}
|
||||
|
||||
// 清理新密码(去除首尾空格)
|
||||
newPassword := strings.TrimSpace(req.NewPassword)
|
||||
if newPassword == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("新支付密码不能为空")
|
||||
}
|
||||
|
||||
// 获取钱包信息
|
||||
wallet, err := o.Database.GetWallet(ctx, userID)
|
||||
if err != nil {
|
||||
// 如果钱包不存在,创建钱包并设置支付密码
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
// 首次设置,不需要验证旧密码
|
||||
if req.OldPassword != "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("首次设置支付密码不需要提供旧密码")
|
||||
}
|
||||
|
||||
// 创建钱包并设置支付密码
|
||||
now := time.Now()
|
||||
newWallet := &chatdb.Wallet{
|
||||
UserID: userID,
|
||||
Balance: 0,
|
||||
PaymentPassword: newPassword,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
if err := o.Database.CreateWallet(ctx, newWallet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.SetPaymentPasswordResp{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 钱包已存在,判断是首次设置还是修改
|
||||
hasPaymentPassword := wallet.PaymentPassword != ""
|
||||
|
||||
if hasPaymentPassword {
|
||||
// 修改支付密码,需要验证旧密码
|
||||
if req.OldPassword == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("修改支付密码需要提供旧密码")
|
||||
}
|
||||
// 清理旧密码和存储的密码(去除首尾空格)
|
||||
oldPassword := strings.TrimSpace(req.OldPassword)
|
||||
storedPassword := strings.TrimSpace(wallet.PaymentPassword)
|
||||
if storedPassword != oldPassword {
|
||||
return nil, errs.ErrArgs.WrapMsg("旧支付密码错误")
|
||||
}
|
||||
if newPassword == oldPassword {
|
||||
return nil, errs.ErrArgs.WrapMsg("新密码不能与旧密码相同")
|
||||
}
|
||||
} else {
|
||||
// 首次设置支付密码,不需要验证旧密码
|
||||
if req.OldPassword != "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("首次设置支付密码不需要提供旧密码")
|
||||
}
|
||||
}
|
||||
|
||||
// 更新支付密码
|
||||
if err := o.Database.UpdateWalletPaymentPassword(ctx, userID, newPassword); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chatpb.SetPaymentPasswordResp{}, nil
|
||||
}
|
||||
|
||||
// CreateWithdrawApplication 申请提现
|
||||
func (o *chatSvr) CreateWithdrawApplication(ctx context.Context, req *chatpb.CreateWithdrawApplicationReq) (*chatpb.CreateWithdrawApplicationResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.Amount <= 0 {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "提现金额必须大于0")
|
||||
}
|
||||
if req.PaymentPassword == "" {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "支付密码不能为空")
|
||||
}
|
||||
|
||||
// 从数据库 SystemConfig 集合读取 withdraw_limit 配置并验证提现限额
|
||||
withdrawLimitConfig, _ := o.Database.GetSystemConfig(ctx, "withdraw_limit")
|
||||
if withdrawLimitConfig != nil {
|
||||
// 如果配置存在但未启用,跳过验证
|
||||
if !withdrawLimitConfig.Enabled {
|
||||
log.ZInfo(ctx, "withdraw_limit config is disabled, skipping validation")
|
||||
} else {
|
||||
// 配置存在且启用,必须验证
|
||||
limitValue := strings.TrimSpace(withdrawLimitConfig.Value)
|
||||
if limitValue == "" {
|
||||
log.ZWarn(ctx, "withdraw_limit config value is empty", nil)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "提现限额配置错误,请联系管理员")
|
||||
}
|
||||
|
||||
// 解析提现限制配置(格式:最低限制-最高限制,单位:元,需要转换为分)
|
||||
parts := strings.Split(limitValue, "-")
|
||||
if len(parts) != 2 {
|
||||
log.ZWarn(ctx, "Invalid withdraw_limit config format, expected 'min-max'", nil,
|
||||
"value", limitValue)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "提现限额配置格式错误,请联系管理员")
|
||||
}
|
||||
|
||||
minLimitStr := strings.TrimSpace(parts[0])
|
||||
maxLimitStr := strings.TrimSpace(parts[1])
|
||||
minLimitYuan, err1 := strconv.ParseFloat(minLimitStr, 64)
|
||||
maxLimitYuan, err2 := strconv.ParseFloat(maxLimitStr, 64)
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
log.ZWarn(ctx, "Failed to parse withdraw_limit config values", nil,
|
||||
"minLimitStr", minLimitStr,
|
||||
"maxLimitStr", maxLimitStr,
|
||||
"minLimitErr", err1,
|
||||
"maxLimitErr", err2)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "提现限额配置解析失败,请联系管理员")
|
||||
}
|
||||
|
||||
// 将元转换为分(乘以100)
|
||||
minLimit := int64(minLimitYuan * 100)
|
||||
maxLimit := int64(maxLimitYuan * 100)
|
||||
|
||||
// 验证配置值的有效性
|
||||
if minLimit <= 0 {
|
||||
log.ZWarn(ctx, "Invalid withdraw_limit minLimit, must be greater than 0", nil,
|
||||
"minLimitYuan", minLimitYuan,
|
||||
"minLimit", minLimit)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "提现最低限额配置无效,请联系管理员")
|
||||
}
|
||||
if maxLimit <= 0 {
|
||||
log.ZWarn(ctx, "Invalid withdraw_limit maxLimit, must be greater than 0", nil,
|
||||
"maxLimitYuan", maxLimitYuan,
|
||||
"maxLimit", maxLimit)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "提现最高限额配置无效,请联系管理员")
|
||||
}
|
||||
if minLimit > maxLimit {
|
||||
log.ZWarn(ctx, "Invalid withdraw_limit config, minLimit > maxLimit", nil,
|
||||
"minLimitYuan", minLimitYuan,
|
||||
"maxLimitYuan", maxLimitYuan,
|
||||
"minLimit", minLimit,
|
||||
"maxLimit", maxLimit)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "提现限额配置错误(最低限额不能大于最高限额),请联系管理员")
|
||||
}
|
||||
|
||||
// 验证提现金额是否在限制范围内(req.Amount 单位是分)
|
||||
if req.Amount < minLimit {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), fmt.Sprintf("提现金额不能少于 %.2f 元(%d 分)", minLimitYuan, minLimit))
|
||||
}
|
||||
if req.Amount > maxLimit {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), fmt.Sprintf("提现金额不能超过 %.2f 元(%d 分)", maxLimitYuan, maxLimit))
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "Withdraw amount validated against withdraw_limit config",
|
||||
"amount", req.Amount,
|
||||
"minLimit", minLimit,
|
||||
"maxLimit", maxLimit)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理支付密码(去除首尾空格)
|
||||
paymentPassword := strings.TrimSpace(req.PaymentPassword)
|
||||
if paymentPassword == "" {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "支付密码不能为空")
|
||||
}
|
||||
|
||||
// 获取钱包信息,验证余额是否足够
|
||||
wallet, err := o.Database.GetWallet(ctx, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "钱包不存在,无法申请提现")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查是否已完成实名认证
|
||||
if wallet.RealNameAuth.IDCard == "" || wallet.RealNameAuth.Name == "" {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "请先完成实名认证才能申请提现")
|
||||
}
|
||||
|
||||
// 检查实名认证审核状态,必须为审核通过(1)才能提现
|
||||
if wallet.RealNameAuth.AuditStatus != 1 {
|
||||
switch wallet.RealNameAuth.AuditStatus {
|
||||
case 0:
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "实名认证正在审核中,请等待审核通过后再申请提现")
|
||||
case 2:
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "实名认证审核未通过,无法申请提现")
|
||||
default:
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "实名认证状态异常,无法申请提现")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否设置了支付密码
|
||||
if wallet.PaymentPassword == "" {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "请先设置支付密码")
|
||||
}
|
||||
|
||||
// 清理数据库中存储的支付密码(去除首尾空格)
|
||||
storedPassword := strings.TrimSpace(wallet.PaymentPassword)
|
||||
|
||||
// 调试日志:打印支付密码验证信息
|
||||
log.ZInfo(ctx, "支付密码验证调试",
|
||||
"userID", userID,
|
||||
"inputPassword", paymentPassword,
|
||||
"inputPasswordLen", len(paymentPassword),
|
||||
"storedPassword", storedPassword,
|
||||
"storedPasswordLen", len(storedPassword),
|
||||
"storedPasswordRaw", wallet.PaymentPassword,
|
||||
"storedPasswordRawLen", len(wallet.PaymentPassword),
|
||||
"match", storedPassword == paymentPassword,
|
||||
)
|
||||
|
||||
// 验证支付密码
|
||||
if storedPassword != paymentPassword {
|
||||
log.ZWarn(ctx, "支付密码验证失败", nil,
|
||||
"userID", userID,
|
||||
"inputPassword", paymentPassword,
|
||||
"storedPassword", storedPassword,
|
||||
)
|
||||
return nil, eerrs.ErrPaymentPassword.WrapMsg("支付密码错误")
|
||||
}
|
||||
|
||||
// 检查余额是否足够
|
||||
if wallet.Balance < req.Amount {
|
||||
return nil, eerrs.ErrInsufficientBalance.WrapMsg("余额不足,无法申请提现")
|
||||
}
|
||||
|
||||
// 从钱包中获取提现账号
|
||||
if wallet.WithdrawAccount == "" {
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "请先在钱包中设置提现账号")
|
||||
}
|
||||
|
||||
// 使用事务:扣减余额、创建余额变动记录、创建提现申请
|
||||
applicationID := uuid.New().String()
|
||||
now := time.Now()
|
||||
|
||||
// 扣减余额(使用负数表示扣款)
|
||||
beforeBalance, afterBalance, err := o.Database.IncrementWalletBalance(ctx, userID, -req.Amount)
|
||||
if err != nil {
|
||||
// IncrementWalletBalance 已经返回了具体的错误信息(如余额不足),直接返回
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建余额变动记录
|
||||
balanceRecord := &chatdb.WalletBalanceRecord{
|
||||
ID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
Amount: -req.Amount, // 负数表示减少
|
||||
Type: chatdb.BalanceRecordTypeWithdraw, // 提现/提款
|
||||
BeforeBalance: beforeBalance,
|
||||
AfterBalance: afterBalance,
|
||||
Remark: "提现申请",
|
||||
CreateTime: now,
|
||||
}
|
||||
if err := o.Database.CreateWalletBalanceRecord(ctx, balanceRecord); err != nil {
|
||||
// 如果创建记录失败,回滚余额(增加回去)
|
||||
_, _, _ = o.Database.IncrementWalletBalance(ctx, userID, req.Amount)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "创建余额变动记录失败")
|
||||
}
|
||||
|
||||
// 创建提现申请
|
||||
application := &chatdb.WithdrawApplication{
|
||||
ID: applicationID,
|
||||
UserID: userID,
|
||||
Amount: req.Amount,
|
||||
WithdrawAccount: wallet.WithdrawAccount,
|
||||
WithdrawAccountType: wallet.WithdrawAccountType,
|
||||
Status: chatdb.WithdrawApplicationStatusPending, // 待审核
|
||||
IP: req.Ip,
|
||||
DeviceID: req.DeviceID,
|
||||
Platform: req.Platform,
|
||||
DeviceModel: req.DeviceModel,
|
||||
DeviceBrand: req.DeviceBrand,
|
||||
OSVersion: req.OsVersion,
|
||||
AppVersion: req.AppVersion,
|
||||
Remark: "", // 备注由后台管理员填写
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
|
||||
// 保存提现申请
|
||||
if err := o.Database.CreateWithdrawApplication(ctx, application); err != nil {
|
||||
// 如果创建申请失败,回滚余额(增加回去)
|
||||
// 注意:余额变动记录保留,因为余额确实已经扣减了
|
||||
// 如果后续需要,可以通过记录ID删除余额变动记录
|
||||
_, _, _ = o.Database.IncrementWalletBalance(ctx, userID, req.Amount)
|
||||
return nil, errs.NewCodeError(errs.ErrArgs.Code(), "创建提现申请失败,余额已回滚")
|
||||
}
|
||||
|
||||
return &chatpb.CreateWithdrawApplicationResp{
|
||||
ApplicationID: applicationID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetWithdrawApplications 获取用户的提现申请列表
|
||||
func (o *chatSvr) GetWithdrawApplications(ctx context.Context, req *chatpb.GetWithdrawApplicationsReq) (*chatpb.GetWithdrawApplicationsResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取提现申请列表
|
||||
total, applications, err := o.Database.GetWithdrawApplicationsByUserID(ctx, userID, req.Pagination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
applicationInfos := make([]*chatpb.WithdrawApplicationInfo, 0, len(applications))
|
||||
for _, app := range applications {
|
||||
var auditTime int64
|
||||
if !app.AuditTime.IsZero() {
|
||||
auditTime = app.AuditTime.UnixMilli()
|
||||
}
|
||||
applicationInfos = append(applicationInfos, &chatpb.WithdrawApplicationInfo{
|
||||
Id: app.ID,
|
||||
UserID: app.UserID,
|
||||
Amount: app.Amount,
|
||||
WithdrawAccount: app.WithdrawAccount,
|
||||
WithdrawAccountType: app.WithdrawAccountType,
|
||||
Status: app.Status,
|
||||
AuditorID: app.AuditorID,
|
||||
AuditTime: auditTime,
|
||||
AuditRemark: app.AuditRemark,
|
||||
Ip: app.IP,
|
||||
DeviceID: app.DeviceID,
|
||||
Platform: app.Platform,
|
||||
DeviceModel: app.DeviceModel,
|
||||
DeviceBrand: app.DeviceBrand,
|
||||
OsVersion: app.OSVersion,
|
||||
AppVersion: app.AppVersion,
|
||||
Remark: app.Remark,
|
||||
CreateTime: app.CreateTime.UnixMilli(),
|
||||
UpdateTime: app.UpdateTime.UnixMilli(),
|
||||
})
|
||||
}
|
||||
|
||||
return &chatpb.GetWithdrawApplicationsResp{
|
||||
Total: uint32(total),
|
||||
Applications: applicationInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RealNameAuth 实名认证
|
||||
func (o *chatSvr) RealNameAuth(ctx context.Context, req *chatpb.RealNameAuthReq) (*chatpb.RealNameAuthResp, error) {
|
||||
// 获取用户ID
|
||||
userID, _, err := mctx.Check(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查用户是否已经实名认证且审核通过
|
||||
wallet, err := o.Database.GetWallet(ctx, userID)
|
||||
if err == nil && wallet != nil {
|
||||
// 如果已经实名认证且审核状态为通过(1),不允许重新认证
|
||||
if wallet.RealNameAuth.IDCard != "" && wallet.RealNameAuth.AuditStatus == 1 {
|
||||
return nil, errs.ErrArgs.WrapMsg("您已经完成实名认证,不能重新认证")
|
||||
}
|
||||
} else if err != nil && !errors.Is(err, mongo.ErrNoDocuments) {
|
||||
// 如果不是"文档不存在"的错误,返回错误
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.IdCard == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("身份证号不能为空")
|
||||
}
|
||||
if req.Name == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("真实姓名不能为空")
|
||||
}
|
||||
if req.IdCardPhotoFront == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("身份证正面照片不能为空")
|
||||
}
|
||||
if req.IdCardPhotoBack == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("身份证反面照片不能为空")
|
||||
}
|
||||
|
||||
// 清理输入(去除首尾空格)
|
||||
idCard := strings.TrimSpace(req.IdCard)
|
||||
name := strings.TrimSpace(req.Name)
|
||||
idCardPhotoFront := strings.TrimSpace(req.IdCardPhotoFront)
|
||||
idCardPhotoBack := strings.TrimSpace(req.IdCardPhotoBack)
|
||||
|
||||
if idCard == "" || name == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("身份证号和姓名不能为空")
|
||||
}
|
||||
if idCardPhotoFront == "" || idCardPhotoBack == "" {
|
||||
return nil, errs.ErrArgs.WrapMsg("身份证正面照片和反面照片不能为空")
|
||||
}
|
||||
|
||||
// 验证姓名只能包含中文字符(不允许英文、数字和标点符号)
|
||||
chineseRegex := regexp.MustCompile(`^[\p{Han}]+$`)
|
||||
if !chineseRegex.MatchString(name) {
|
||||
return nil, errs.ErrArgs.WrapMsg("真实姓名只能包含中文,不能包含英文、数字或标点符号")
|
||||
}
|
||||
|
||||
// 构建原始数据 JSON
|
||||
rawData := map[string]string{
|
||||
"cardNo": idCard,
|
||||
"realName": name,
|
||||
}
|
||||
rawDataJSON, err := json.Marshal(rawData)
|
||||
if err != nil {
|
||||
return nil, errs.WrapMsg(err, "构建数据失败")
|
||||
}
|
||||
|
||||
// AES 密钥(32字节的十六进制字符串)
|
||||
aesKey := "a7f3c9e2b8d4f1a6c3e9b2d7f4a1c8e5b2d9f6a3c8e1b4d7f2a9c5e8b1d4f7a2"
|
||||
|
||||
// 在客户端本地加密数据(使用 AES-GCM 模式)
|
||||
log.ZInfo(ctx, "开始本地加密实名认证数据", "userID", userID, "rawData", string(rawDataJSON), "idCard", idCard, "name", name)
|
||||
encryptedData, err := util.EncryptRealNameAuthData(string(rawDataJSON), aesKey)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "本地加密失败", err, "userID", userID)
|
||||
return nil, errs.WrapMsg(err, "加密数据失败")
|
||||
}
|
||||
log.ZInfo(ctx, "本地加密成功", "userID", userID, "encryptedLength", len(encryptedData), "encryptedData", encryptedData)
|
||||
|
||||
// 调用验证接口(直接发送加密后的字符串)
|
||||
baseURL := "http://95.40.154.128"
|
||||
verifyURL := baseURL + "/idcheck"
|
||||
|
||||
log.ZInfo(ctx, "准备调用验证接口", "userID", userID, "url", verifyURL, "encryptedLength", len(encryptedData))
|
||||
|
||||
// 创建请求,请求体直接是加密后的字符串
|
||||
httpReq, err := http.NewRequest("POST", verifyURL, bytes.NewBufferString(encryptedData))
|
||||
if err != nil {
|
||||
log.ZError(ctx, "创建验证请求失败", err, "userID", userID, "url", verifyURL)
|
||||
return nil, errs.WrapMsg(err, "创建验证请求失败")
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
verifyResp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "调用验证接口失败", err, "userID", userID, "url", verifyURL)
|
||||
return nil, errs.WrapMsg(err, "调用验证接口失败")
|
||||
}
|
||||
defer verifyResp.Body.Close()
|
||||
|
||||
verifyRespBody, err := io.ReadAll(verifyResp.Body)
|
||||
if err != nil {
|
||||
return nil, errs.WrapMsg(err, "读取验证接口响应失败")
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "验证接口响应", "userID", userID, "statusCode", verifyResp.StatusCode, "responseBody", string(verifyRespBody), "responseLength", len(verifyRespBody))
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if verifyResp.StatusCode != http.StatusOK {
|
||||
log.ZWarn(ctx, "验证接口返回错误状态码", nil, "userID", userID, "statusCode", verifyResp.StatusCode, "response", string(verifyRespBody))
|
||||
return &chatpb.RealNameAuthResp{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("验证请求失败,状态码: %d, 响应: %s", verifyResp.StatusCode, string(verifyRespBody)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 解析响应(格式:{"success": bool, "data": interface{}, "error": string, "message": string})
|
||||
var verifyResult struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(verifyRespBody, &verifyResult); err != nil {
|
||||
log.ZWarn(ctx, "解析验证接口响应失败", err, "userID", userID, "response", string(verifyRespBody))
|
||||
return &chatpb.RealNameAuthResp{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("解析验证结果失败: %s, 响应: %s", err.Error(), string(verifyRespBody)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 检查验证结果
|
||||
if !verifyResult.Success {
|
||||
errorMsg := verifyResult.Error
|
||||
if errorMsg == "" {
|
||||
errorMsg = verifyResult.Message
|
||||
}
|
||||
if errorMsg == "" {
|
||||
errorMsg = "验证失败"
|
||||
}
|
||||
|
||||
return &chatpb.RealNameAuthResp{
|
||||
Success: false,
|
||||
Message: errorMsg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 验证成功,保存实名认证信息到数据库
|
||||
if verifyResult.Success {
|
||||
// 获取或创建钱包
|
||||
_, err := o.Database.GetWallet(ctx, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
// 钱包不存在,创建钱包
|
||||
now := time.Now()
|
||||
newWallet := &chatdb.Wallet{
|
||||
UserID: userID,
|
||||
Balance: 0,
|
||||
RealNameAuth: chatdb.RealNameAuth{
|
||||
IDCard: idCard,
|
||||
Name: name,
|
||||
IDCardPhotoFront: idCardPhotoFront,
|
||||
IDCardPhotoBack: idCardPhotoBack,
|
||||
AuditStatus: 0, // 0-未审核
|
||||
},
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
if err := o.Database.CreateWallet(ctx, newWallet); err != nil {
|
||||
return nil, errs.WrapMsg(err, "创建钱包失败")
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// 更新实名认证信息(重新提交后,审核状态重置为待审核)
|
||||
realNameAuth := chatdb.RealNameAuth{
|
||||
IDCard: idCard,
|
||||
Name: name,
|
||||
IDCardPhotoFront: idCardPhotoFront,
|
||||
IDCardPhotoBack: idCardPhotoBack,
|
||||
AuditStatus: 0, // 0-未审核(重新提交后重置为待审核状态)
|
||||
}
|
||||
if err := o.Database.UpdateWalletRealNameAuth(ctx, userID, realNameAuth); err != nil {
|
||||
return nil, errs.WrapMsg(err, "更新实名认证信息失败")
|
||||
}
|
||||
log.ZInfo(ctx, "实名认证信息已更新,审核状态重置为待审核", "userID", userID, "idCard", idCard, "name", name, "auditStatus", 0)
|
||||
}
|
||||
|
||||
log.ZInfo(ctx, "实名认证成功并已保存", "userID", userID, "idCard", idCard, "name", name)
|
||||
|
||||
// 获取更新后的钱包信息,返回身份证照片URL
|
||||
updatedWallet, err := o.Database.GetWallet(ctx, userID)
|
||||
var idCardPhotoFront, idCardPhotoBack string
|
||||
if err == nil && updatedWallet != nil {
|
||||
idCardPhotoFront = updatedWallet.RealNameAuth.IDCardPhotoFront
|
||||
idCardPhotoBack = updatedWallet.RealNameAuth.IDCardPhotoBack
|
||||
}
|
||||
|
||||
return &chatpb.RealNameAuthResp{
|
||||
Success: true,
|
||||
Message: "提交成功了,请等待审核",
|
||||
IdCardPhotoFront: idCardPhotoFront,
|
||||
IdCardPhotoBack: idCardPhotoBack,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 这行代码永远不会执行到,因为如果 verifyResult.Success 为 false,已经在前面返回了
|
||||
// 但为了代码完整性保留
|
||||
log.ZError(ctx, "代码逻辑错误:验证失败但未返回", nil, "userID", userID, "verifyResult", verifyResult)
|
||||
return &chatpb.RealNameAuthResp{
|
||||
Success: false,
|
||||
Message: "验证失败",
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user