package oci import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" "github.com/oracle/oci-go-sdk/v65/common" "oci-portal/internal/model" ) // AccountProfile 是一次云端订阅查询得到的账户信息快照。 type AccountProfile struct { AccountType string SubscriptionID string ServiceName string PaymentModel string SubscriptionTier string PromotionStatus string PromotionAmount float32 PromotionExpires *time.Time } // AccountCapabilities 是账户能力接口的关键字段。来源为 Limits 服务 // 20230301 版 `/compartments/{tenancy}/capabilities?accountInfoOnly=true` // (与控制台同源;oci-go-sdk v65 未收录,自签名裸调)。 // 该接口对暂停/已终止租户仍可访问,借此把「暂停」从「失联」中区分出来。 type AccountCapabilities struct { Suspended bool `json:"suspended"` AccountStatus string `json:"accountStatus"` FreeTierEnabled bool `json:"freeTierEnabled"` FreeTierOnly bool `json:"freeTierOnly"` PromotionStatus string `json:"promotionStatus"` ProgramType string `json:"programType"` HasSaasSubscription bool `json:"hasSaasSubscription"` Deletable bool `json:"deletable"` } // capabilitiesEnvelope 对应接口原始响应:顶层与 accountInfo 各有一个 // suspended,任一为 true 即视为暂停。 type capabilitiesEnvelope struct { Capabilities struct { Suspended bool `json:"suspended"` AccountInfo struct { FreeTierEnabled bool `json:"freeTierEnabled"` FreeTierOnly bool `json:"freeTierOnly"` PromotionStatus string `json:"promotionStatus"` Suspended bool `json:"suspended"` ProgramType string `json:"programType"` HasSaasSubscription bool `json:"hasSaasSubscription"` AccountStatus string `json:"accountStatus"` Deletable bool `json:"deletable"` } `json:"accountInfo"` } `json:"capabilities"` } // parseAccountCapabilities 把原始响应体压平为关键字段视图。 func parseAccountCapabilities(body []byte) (AccountCapabilities, error) { var env capabilitiesEnvelope if err := json.Unmarshal(body, &env); err != nil { return AccountCapabilities{}, fmt.Errorf("decode account capabilities: %w", err) } info := env.Capabilities.AccountInfo return AccountCapabilities{ Suspended: env.Capabilities.Suspended || info.Suspended, AccountStatus: info.AccountStatus, FreeTierEnabled: info.FreeTierEnabled, FreeTierOnly: info.FreeTierOnly, PromotionStatus: info.PromotionStatus, ProgramType: info.ProgramType, HasSaasSubscription: info.HasSaasSubscription, Deletable: info.Deletable, }, nil } // GetAccountCapabilities 实现 Client:查询账户能力(暂停/账户状态/免费层)。 // region 应传 home region;请求带 API Key 签名,并沿用租户出站代理。 func (c *RealClient) GetAccountCapabilities(ctx context.Context, cred Credentials, region string) (AccountCapabilities, error) { url := fmt.Sprintf("https://limits.%s.oci.oraclecloud.com/20230301/compartments/%s/capabilities?accountInfoOnly=true", normalizeRegion(region), cred.TenancyOCID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return AccountCapabilities{}, fmt.Errorf("new capabilities request: %w", err) } if err := common.DefaultRequestSigner(provider(cred)).Sign(req); err != nil { return AccountCapabilities{}, fmt.Errorf("sign capabilities request: %w", err) } hc := proxyHTTPClient(cred.Proxy) if hc == nil { hc = &http.Client{Timeout: proxyClientTimeout} } resp, err := hc.Do(req) if err != nil { return AccountCapabilities{}, fmt.Errorf("get account capabilities: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return AccountCapabilities{}, fmt.Errorf("read account capabilities: %w", err) } if resp.StatusCode != http.StatusOK { return AccountCapabilities{}, fmt.Errorf("get account capabilities: status %d", resp.StatusCode) } return parseAccountCapabilities(body) } // ClassifyAccount 按订阅字段判定账户类别: // - paymentModel 非空且不是 FREE_TRIAL:付费; // - paymentModel 为 FREE_TRIAL 时,promotion ACTIVE 或 tier FREE_AND_TRIAL // 判为试用;promotion EXPIRED 或 tier FREE 判为免费(试用权益已结束); // - 其余情况无法判定。 func ClassifyAccount(paymentModel, subscriptionTier, promotionStatus string) string { pm := strings.ToUpper(strings.TrimSpace(paymentModel)) tier := strings.ToUpper(strings.TrimSpace(subscriptionTier)) promo := strings.ToUpper(strings.TrimSpace(promotionStatus)) if pm != "" && pm != "FREE_TRIAL" { return model.AccountTypePaid } if pm == "FREE_TRIAL" { switch { case promo == "ACTIVE" || tier == "FREE_AND_TRIAL": return model.AccountTypeTrial case promo == "EXPIRED" || tier == "FREE": return model.AccountTypeFree } } return model.AccountTypeUnknown }