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 }