// 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" "io" "strings" "time" apiuserutil "git.imall.cloud/openim/chat/internal/api/util" "strconv" "git.imall.cloud/openim/chat/pkg/common/apistruct" "git.imall.cloud/openim/chat/pkg/common/imapi" "git.imall.cloud/openim/chat/pkg/common/mctx" "git.imall.cloud/openim/chat/pkg/eerrs" "git.imall.cloud/openim/chat/pkg/protocol/admin" chatpb "git.imall.cloud/openim/chat/pkg/protocol/chat" constantpb "git.imall.cloud/openim/protocol/constant" "git.imall.cloud/openim/protocol/sdkws" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/openimsdk/tools/a2r" "github.com/openimsdk/tools/apiresp" "github.com/openimsdk/tools/errs" "github.com/openimsdk/tools/log" ) func New(chatClient chatpb.ChatClient, adminClient admin.AdminClient, imApiCaller imapi.CallerInterface, api *apiuserutil.Api) *Api { return &Api{ Api: api, chatClient: chatClient, adminClient: adminClient, imApiCaller: imApiCaller, } } type Api struct { *apiuserutil.Api chatClient chatpb.ChatClient adminClient admin.AdminClient imApiCaller imapi.CallerInterface } // operationIDKey 用于 context.WithValue 的自定义类型,避免使用字符串导致类型冲突 type operationIDKey struct{} // ################## ACCOUNT ################## func (o *Api) SendVerifyCode(c *gin.Context) { req, err := a2r.ParseRequest[chatpb.SendVerifyCodeReq](c) if err != nil { apiresp.GinError(c, err) return } ip, err := o.GetClientIP(c) if err != nil { apiresp.GinError(c, err) return } req.Ip = ip resp, err := o.chatClient.SendVerifyCode(c, req) if err != nil { apiresp.GinError(c, err) return } apiresp.GinSuccess(c, resp) } func (o *Api) VerifyCode(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.VerifyCode, o.chatClient) } func (o *Api) GetCaptchaImage(c *gin.Context) { // 获取或生成operationID(图片请求可能没有operationID header) operationID := c.GetHeader("operationID") if operationID == "" { // 如果没有operationID,生成一个UUID operationID = uuid.New().String() } // 将operationID设置到context中,确保RPC调用能获取到 // 使用自定义类型作为 key,避免使用字符串导致类型冲突 ctx := context.WithValue(c.Request.Context(), operationIDKey{}, operationID) // 调用RPC方法获取验证码(6位数字) resp, err := o.chatClient.GetCaptchaImage(ctx, &chatpb.GetCaptchaImageReq{}) if err != nil { apiresp.GinError(c, err) return } // 在API层生成验证码图片 imgBytes, err := apiuserutil.GenerateCaptchaImageFromCode(resp.Code) if err != nil { apiresp.GinError(c, err) return } // 验证图片数据 if len(imgBytes) == 0 { apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("generated captcha image is empty")) return } // 检查GIF文件头 if len(imgBytes) < 6 { apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("generated captcha image data too short")) return } // 设置响应头,返回GIF图片 c.Header("Content-Type", "image/gif") c.Header("Content-Length", fmt.Sprintf("%d", len(imgBytes))) c.Header("Cache-Control", "no-cache, no-store, must-revalidate") c.Header("Pragma", "no-cache") c.Header("Expires", "0") // 将验证码ID放在响应头中,客户端可用于后续验证 c.Header("X-Captcha-ID", resp.CaptchaID) // 返回图片数据 c.Data(200, "image/gif", imgBytes) } func (o *Api) RegisterUser(c *gin.Context) { req, err := a2r.ParseRequest[chatpb.RegisterUserReq](c) if err != nil { apiresp.GinError(c, err) return } ip, err := o.GetClientIP(c) if err != nil { apiresp.GinError(c, err) return } req.Ip = ip imToken, err := o.imApiCaller.ImAdminTokenWithDefaultAdmin(c) if err != nil { apiresp.GinError(c, err) return } apiCtx := mctx.WithApiToken(c, imToken) rpcCtx := o.WithAdminUser(c) checkResp, err := o.chatClient.CheckUserExist(rpcCtx, &chatpb.CheckUserExistReq{User: req.User}) if err != nil { apiresp.GinError(c, err) return } if checkResp.IsRegistered { isUserNotExist, err := o.imApiCaller.AccountCheckSingle(apiCtx, checkResp.Userid) if err != nil { apiresp.GinError(c, err) return } // if User is not exist in SDK server. You need delete this user and register new user again. if isUserNotExist { _, err := o.chatClient.DelUserAccount(rpcCtx, &chatpb.DelUserAccountReq{UserIDs: []string{checkResp.Userid}}) if err != nil { apiresp.GinError(c, err) return } } } // H5注册场景(提供了registerToken)默认自动登录 if req.RegisterToken != "" && !req.AutoLogin { req.AutoLogin = true } respRegisterUser, err := o.chatClient.RegisterUser(c, req) if err != nil { apiresp.GinError(c, err) return } userInfo := &sdkws.UserInfo{ UserID: respRegisterUser.UserID, Nickname: req.User.Nickname, FaceURL: req.User.FaceURL, CreateTime: time.Now().UnixMilli(), UserType: 0, // 设置默认用户类型 UserFlag: "", } err = o.imApiCaller.RegisterUser(apiCtx, []*sdkws.UserInfo{userInfo}) if err != nil { apiresp.GinError(c, err) return } if resp, err := o.adminClient.FindDefaultFriend(rpcCtx, &admin.FindDefaultFriendReq{}); err == nil { _ = o.imApiCaller.ImportFriend(apiCtx, respRegisterUser.UserID, resp.UserIDs) } if resp, err := o.adminClient.FindDefaultGroup(rpcCtx, &admin.FindDefaultGroupReq{}); err == nil { _ = o.imApiCaller.InviteToGroup(apiCtx, respRegisterUser.UserID, resp.GroupIDs) } var resp apistruct.UserRegisterResp if req.AutoLogin { resp.ImToken, err = o.imApiCaller.GetUserToken(apiCtx, respRegisterUser.UserID, req.Platform) if err != nil { apiresp.GinError(c, err) return } } resp.ChatToken = respRegisterUser.ChatToken resp.UserID = respRegisterUser.UserID apiresp.GinSuccess(c, &resp) } func (o *Api) Login(c *gin.Context) { req, err := a2r.ParseRequest[chatpb.LoginReq](c) if err != nil { apiresp.GinError(c, err) return } ip, err := o.GetClientIP(c) if err != nil { apiresp.GinError(c, err) return } req.Ip = ip resp, err := o.chatClient.Login(c, req) if err != nil { // 调试日志:打印错误的详细信息 errStr := err.Error() log.ZError(c, "Login error detected", err, "errorString", errStr, "errorType", fmt.Sprintf("%T", err), "contains20015", strings.Contains(errStr, "20015"), "containsAccountBlocked", strings.Contains(errStr, "AccountBlocked"), "contains封禁", strings.Contains(errStr, "账户已被封禁")) // 检查是否是账户被封禁的错误,确保返回明确的错误提示 // 需要检查错误字符串中是否包含封禁相关的关键词,即使错误被包装了 if strings.Contains(errStr, "20015") || strings.Contains(errStr, "AccountBlocked") || strings.Contains(errStr, "账户已被封禁") { log.ZInfo(c, "Detected AccountBlocked error, returning explicit error", "originalError", errStr) apiresp.GinError(c, eerrs.ErrAccountBlocked.WrapMsg("账户已被封禁")) return } // 如果错误被包装成 ServerInternalError,尝试从错误链中提取原始错误 // 检查错误消息中是否包含封禁相关的堆栈信息 if strings.Contains(errStr, "ServerInternalError") && (strings.Contains(errStr, "20015") || strings.Contains(errStr, "AccountBlocked")) { log.ZInfo(c, "Detected AccountBlocked error in wrapped ServerInternalError, returning explicit error", "originalError", errStr) apiresp.GinError(c, eerrs.ErrAccountBlocked.WrapMsg("账户已被封禁")) return } log.ZError(c, "Login error not AccountBlocked, returning original error", err) apiresp.GinError(c, err) return } adminToken, err := o.imApiCaller.ImAdminTokenWithDefaultAdmin(c) if err != nil { apiresp.GinError(c, err) return } apiCtx := mctx.WithApiToken(c, adminToken) imToken, err := o.imApiCaller.GetUserToken(apiCtx, resp.UserID, req.Platform) if err != nil { apiresp.GinError(c, err) return } apiresp.GinSuccess(c, &apistruct.LoginResp{ ImToken: imToken, UserID: resp.UserID, ChatToken: resp.ChatToken, }) } func (o *Api) ResetPassword(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.ResetPassword, o.chatClient) } func (o *Api) ChangePassword(c *gin.Context) { req, err := a2r.ParseRequest[chatpb.ChangePasswordReq](c) if err != nil { apiresp.GinError(c, err) return } resp, err := o.chatClient.ChangePassword(c, req) if err != nil { apiresp.GinError(c, err) return } imToken, err := o.imApiCaller.ImAdminTokenWithDefaultAdmin(c) if err != nil { apiresp.GinError(c, err) return } err = o.imApiCaller.ForceOffLine(mctx.WithApiToken(c, imToken), req.UserID) if err != nil { apiresp.GinError(c, err) return } apiresp.GinSuccess(c, resp) } // ################## USER ################## func (o *Api) UpdateUserInfo(c *gin.Context) { req, err := a2r.ParseRequest[chatpb.UpdateUserInfoReq](c) if err != nil { apiresp.GinError(c, err) return } // 检查req是否为nil if req == nil { apiresp.GinError(c, errs.ErrArgs.WrapMsg("request is nil")) return } // 检查必要参数 if req.UserID == "" { apiresp.GinError(c, errs.ErrArgs.WrapMsg("userID is required")) return } respUpdate, err := o.chatClient.UpdateUserInfo(c, req) if err != nil { apiresp.GinError(c, err) return } var imToken string imToken, err = o.imApiCaller.ImAdminTokenWithDefaultAdmin(c) if err != nil { apiresp.GinError(c, err) return } // 检查token是否为空 if imToken == "" { apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to get admin token")) return } // 检查imApiCaller是否为nil if o.imApiCaller == nil { apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("imApiCaller is nil")) return } var ( nickName string faceURL string userFlag string ) if req.Nickname != nil { nickName = req.Nickname.Value } else { nickName = respUpdate.NickName } if req.FaceURL != nil { faceURL = req.FaceURL.Value } else { faceURL = respUpdate.FaceUrl } // 处理UserFlag字段 if req.UserFlag != nil { userFlag = req.UserFlag.Value } // 确保字符串参数不为nil(虽然这里应该是安全的,但添加额外保护) if nickName == "" { nickName = "" } if faceURL == "" { faceURL = "" } err = o.imApiCaller.UpdateUserInfo(mctx.WithApiToken(c, imToken), req.UserID, nickName, faceURL, req.UserType, userFlag) if err != nil { apiresp.GinError(c, err) return } // 构造 ex 字段的 JSON 数据(包含 userFlag 和 userType) type UserEx struct { UserFlag string `json:"userFlag"` UserType int32 `json:"userType"` } userEx := UserEx{ UserFlag: userFlag, UserType: req.UserType, } exBytes, err := json.Marshal(userEx) if err != nil { apiresp.GinError(c, errs.ErrInternalServer.WrapMsg("failed to marshal user ex")) return } // 调用 UpdateUserInfoEx 更新 ex 字段 err = o.imApiCaller.UpdateUserInfoEx(mctx.WithApiToken(c, imToken), req.UserID, string(exBytes)) if err != nil { apiresp.GinError(c, err) return } apiresp.GinSuccess(c, apistruct.UpdateUserInfoResp{}) } func (o *Api) FindUserPublicInfo(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.FindUserPublicInfo, o.chatClient) } func (o *Api) FindUserFullInfo(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.FindUserFullInfo, o.chatClient) } func (o *Api) SearchUserFullInfo(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.SearchUserFullInfo, o.chatClient) } func (o *Api) SearchUserPublicInfo(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.SearchUserPublicInfo, o.chatClient) } func (o *Api) GetTokenForVideoMeeting(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.GetTokenForVideoMeeting, o.chatClient) } // ################## APPLET ################## func (o *Api) FindApplet(c *gin.Context) { a2r.Call(c, admin.AdminClient.FindApplet, o.adminClient) } // ################## CONFIG ################## func (o *Api) GetClientConfig(c *gin.Context) { a2r.Call(c, admin.AdminClient.GetClientConfig, o.adminClient) } // ################## CALLBACK ################## func (o *Api) OpenIMCallback(c *gin.Context) { body, err := io.ReadAll(c.Request.Body) if err != nil { apiresp.GinError(c, err) return } req := &chatpb.OpenIMCallbackReq{ Command: c.Query(constantpb.CallbackCommand), Body: string(body), } if _, err := o.chatClient.OpenIMCallback(c, req); err != nil { apiresp.GinError(c, err) return } apiresp.GinSuccess(c, nil) } func (o *Api) SearchFriend(c *gin.Context) { req, err := a2r.ParseRequest[struct { UserID string `json:"userID"` chatpb.SearchUserInfoReq }](c) if err != nil { apiresp.GinError(c, err) return } if req.UserID == "" { req.UserID = mctx.GetOpUserID(c) } imToken, err := o.imApiCaller.ImAdminTokenWithDefaultAdmin(c) if err != nil { apiresp.GinError(c, err) return } userIDs, err := o.imApiCaller.FriendUserIDs(mctx.WithApiToken(c, imToken), req.UserID) if err != nil { apiresp.GinError(c, err) return } if len(userIDs) == 0 { apiresp.GinSuccess(c, &chatpb.SearchUserInfoResp{}) return } req.SearchUserInfoReq.UserIDs = userIDs resp, err := o.chatClient.SearchUserInfo(c, &req.SearchUserInfoReq) if err != nil { apiresp.GinError(c, err) return } apiresp.GinSuccess(c, resp) } func (o *Api) AddFriend(c *gin.Context) { req, err := a2r.ParseRequest[chatpb.AddFriendReq](c) if err != nil { apiresp.GinError(c, err) return } // 获取当前用户ID(发起添加好友的用户) ownerUserID := mctx.GetOpUserID(c) // 获取用户信息,检查 userType userInfoResp, err := o.chatClient.FindUserFullInfo(c, &chatpb.FindUserFullInfoReq{UserIDs: []string{ownerUserID}}) if err != nil { apiresp.GinError(c, err) return } if len(userInfoResp.Users) == 0 || userInfoResp.Users[0].UserType != 1 { apiresp.GinError(c, errs.ErrNoPermission.WrapMsg("only userType=1 users can add friends directly")) return } // 调用 chat-deploy 的 ImportFriends imToken, err := o.imApiCaller.ImAdminTokenWithDefaultAdmin(c) if err != nil { apiresp.GinError(c, err) return } // 调用 ImportFriend,传入要添加的好友ID err = o.imApiCaller.ImportFriend(mctx.WithApiToken(c, imToken), ownerUserID, []string{req.UserID}) if err != nil { apiresp.GinError(c, err) return } apiresp.GinSuccess(c, nil) } func (o *Api) LatestApplicationVersion(c *gin.Context) { a2r.Call(c, admin.AdminClient.LatestApplicationVersion, o.adminClient) } func (o *Api) PageApplicationVersion(c *gin.Context) { a2r.Call(c, admin.AdminClient.PageApplicationVersion, o.adminClient) } // ==================== 敏感词检测相关 API ==================== // GetSensitiveWords 获取敏感词列表 func (o *Api) GetSensitiveWords(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.GetSensitiveWords, o.chatClient) } // CheckSensitiveWords 检测敏感词 func (o *Api) CheckSensitiveWords(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.CheckSensitiveWords, o.chatClient) } // ==================== 收藏相关 API ==================== // CreateFavorite 创建收藏 func (o *Api) CreateFavorite(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.CreateFavorite, o.chatClient) } // GetFavorite 获取收藏详情 func (o *Api) GetFavorite(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.GetFavorite, o.chatClient) } // GetFavorites 获取收藏列表 func (o *Api) GetFavorites(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.GetFavorites, o.chatClient) } // SearchFavorites 搜索收藏 func (o *Api) SearchFavorites(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.SearchFavorites, o.chatClient) } // UpdateFavorite 更新收藏 func (o *Api) UpdateFavorite(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.UpdateFavorite, o.chatClient) } // DeleteFavorite 删除收藏 func (o *Api) DeleteFavorite(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.DeleteFavorite, o.chatClient) } // GetFavoritesByTags 根据标签获取收藏 func (o *Api) GetFavoritesByTags(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.GetFavoritesByTags, o.chatClient) } // GetFavoriteCount 获取收藏数量 func (o *Api) GetFavoriteCount(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.GetFavoriteCount, o.chatClient) } // ==================== 定时任务相关 API ==================== // CreateScheduledTask 创建定时任务 func (o *Api) CreateScheduledTask(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.CreateScheduledTask, o.chatClient) } // GetScheduledTask 获取定时任务详情 func (o *Api) GetScheduledTask(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.GetScheduledTask, o.chatClient) } // GetScheduledTasks 获取定时任务列表 func (o *Api) GetScheduledTasks(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.GetScheduledTasks, o.chatClient) } // UpdateScheduledTask 更新定时任务 func (o *Api) UpdateScheduledTask(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.UpdateScheduledTask, o.chatClient) } // DeleteScheduledTask 删除定时任务 func (o *Api) DeleteScheduledTask(c *gin.Context) { a2r.Call(c, chatpb.ChatClient.DeleteScheduledTask, o.chatClient) } // ==================== 系统配置相关 API ==================== // GetAppSystemConfigs 获取APP端配置(返回所有 show_in_app=true 且 enabled=true 的配置) func (o *Api) GetAppSystemConfigs(c *gin.Context) { resp, err := o.chatClient.GetAppSystemConfigs(c, &chatpb.GetAppSystemConfigsReq{}) if err != nil { apiresp.GinError(c, err) return } // 转换为对象格式,key 作为对象的键,value 作为值 configMap := make(map[string]interface{}) for _, config := range resp.Configs { convertedValue := o.convertValueFromString(config.Value, config.ValueType) configMap[config.Key] = convertedValue } apiresp.GinSuccess(c, configMap) } // convertValueFromString 将字符串值转换为对应类型(用于返回给前端) func (o *Api) convertValueFromString(value string, valueType int32) interface{} { switch valueType { case 1: // String return value case 2: // Number // 尝试解析为数字 if num, err := strconv.ParseFloat(value, 64); err == nil { // 如果是整数,返回整数;否则返回浮点数 if num == float64(int64(num)) { return int64(num) } return num } return value case 3: // Bool if b, err := strconv.ParseBool(value); err == nil { return b } return value case 4: // JSON var js interface{} if err := json.Unmarshal([]byte(value), &js); err == nil { return js } return value default: return value } } // ==================== 钱包相关 API ==================== // GetWalletBalance 获取钱包余额 func (o *Api) GetWalletBalance(c *gin.Context) { resp, err := o.chatClient.GetWalletBalance(c, &chatpb.GetWalletBalanceReq{}) if err != nil { apiresp.GinError(c, err) return } // 将余额从分转换为元(前端显示用) apiresp.GinSuccess(c, map[string]interface{}{ "balance": resp.Balance, // 余额(单位:分) }) } // GetWalletInfo 获取钱包详细信息 func (o *Api) GetWalletInfo(c *gin.Context) { resp, err := o.chatClient.GetWalletInfo(c, &chatpb.GetWalletInfoReq{}) if err != nil { apiresp.GinError(c, err) return } // 构建响应 result := map[string]interface{}{ "balance": resp.Balance, // 余额(单位:分) "withdrawAccount": resp.WithdrawAccount, "withdrawAccountType": resp.WithdrawAccountType, "withdrawReceiveAccount": resp.WithdrawReceiveAccount, "hasPaymentPassword": resp.HasPaymentPassword, } // 添加实名认证信息(如果存在) if resp.RealNameAuth != nil { result["realNameAuth"] = map[string]interface{}{ "idCard": resp.RealNameAuth.IdCard, "idCardPhotoFront": resp.RealNameAuth.IdCardPhotoFront, "idCardPhotoBack": resp.RealNameAuth.IdCardPhotoBack, "name": resp.RealNameAuth.Name, "auditStatus": resp.RealNameAuth.AuditStatus, } } apiresp.GinSuccess(c, result) } // GetWalletBalanceRecords 获取余额明细 func (o *Api) GetWalletBalanceRecords(c *gin.Context) { var req chatpb.GetWalletBalanceRecordsReq if err := c.ShouldBindJSON(&req); err != nil { apiresp.GinError(c, errs.ErrArgs.WrapMsg("invalid request")) return } resp, err := o.chatClient.GetWalletBalanceRecords(c, &req) if err != nil { apiresp.GinError(c, err) return } // 转换为响应格式 records := make([]map[string]interface{}, 0, len(resp.Records)) for _, record := range resp.Records { records = append(records, map[string]interface{}{ "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, }) } apiresp.GinSuccess(c, map[string]interface{}{ "total": resp.Total, "records": records, }) } // SetPaymentPassword 设置支付密码 func (o *Api) SetPaymentPassword(c *gin.Context) { var req chatpb.SetPaymentPasswordReq if err := c.ShouldBindJSON(&req); err != nil { apiresp.GinError(c, errs.ErrArgs.WrapMsg("invalid request")) return } resp, err := o.chatClient.SetPaymentPassword(c, &req) if err != nil { apiresp.GinError(c, err) return } apiresp.GinSuccess(c, resp) } // SetWithdrawAccount 设置提现账号 func (o *Api) SetWithdrawAccount(c *gin.Context) { var req chatpb.SetWithdrawAccountReq if err := c.ShouldBindJSON(&req); err != nil { apiresp.GinError(c, errs.ErrArgs.WrapMsg("invalid request")) return } resp, err := o.chatClient.SetWithdrawAccount(c, &req) if err != nil { apiresp.GinError(c, err) return } apiresp.GinSuccess(c, resp) } // CreateWithdrawApplication 申请提现 func (o *Api) CreateWithdrawApplication(c *gin.Context) { req, err := a2r.ParseRequest[chatpb.CreateWithdrawApplicationReq](c) if err != nil { apiresp.GinError(c, err) return } // 从请求中获取客户端IP ip, err := o.GetClientIP(c) if err != nil { apiresp.GinError(c, errs.ErrArgs.WrapMsg("无法获取客户端IP")) return } req.Ip = ip resp, err := o.chatClient.CreateWithdrawApplication(c, req) if err != nil { apiresp.GinError(c, err) return } apiresp.GinSuccess(c, map[string]interface{}{ "applicationID": resp.ApplicationID, }) } // GetWithdrawApplications 获取提现申请列表 func (o *Api) GetWithdrawApplications(c *gin.Context) { var req chatpb.GetWithdrawApplicationsReq if err := c.ShouldBindJSON(&req); err != nil { apiresp.GinError(c, errs.ErrArgs.WrapMsg("invalid request")) return } resp, err := o.chatClient.GetWithdrawApplications(c, &req) if err != nil { apiresp.GinError(c, err) return } // 转换为响应格式 applications := make([]map[string]interface{}, 0, len(resp.Applications)) for _, app := range resp.Applications { applications = append(applications, map[string]interface{}{ "id": app.Id, "userID": app.UserID, "amount": app.Amount, "withdrawAccount": app.WithdrawAccount, "withdrawAccountType": app.WithdrawAccountType, "status": app.Status, "auditorID": app.AuditorID, "auditTime": app.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, "updateTime": app.UpdateTime, }) } apiresp.GinSuccess(c, map[string]interface{}{ "total": resp.Total, "applications": applications, }) } // RealNameAuth 实名认证 func (o *Api) RealNameAuth(c *gin.Context) { req, err := a2r.ParseRequest[chatpb.RealNameAuthReq](c) if err != nil { apiresp.GinError(c, err) return } resp, err := o.chatClient.RealNameAuth(c, req) if err != nil { apiresp.GinError(c, err) return } apiresp.GinSuccess(c, map[string]interface{}{ "success": resp.Success, "message": resp.Message, "idCardPhotoFront": resp.IdCardPhotoFront, "idCardPhotoBack": resp.IdCardPhotoBack, }) }