Files
oci-portal/internal/oci/account.go
T
Wang Defa dbba1f4905
CI / test (push) Successful in 30s
Release / release (push) Successful in 49s
发布 0.1.0:通知渠道、告警规则、令牌版本与安全加固
2026-07-10 17:38:34 +08:00

134 lines
5.0 KiB
Go

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
}