PAYATHON 2026

特定 Subscription 无法获取 Billing Data

支付老李

结论

特定 Subscription 返回 pricing costs: null,通常不是请求格式有误,而是该订阅在计费关系、报价类型、权限、生命周期状态或后端数据同步方面与其他订阅不同。

https://s2.billing.ext.azure.com/api/Billing/Subscription/GetSpecsCosts?api-version=2019-01-14 是 Azure Portal 使用的内部接口,不属于稳定、公开的计费 API。HTTP 请求成功,只能说明接口完成了处理,不能保证该 Subscription 有可返回的定价数据。null 也不代表费用为零。

可能原因

1. Subscription 使用了不同的计费模式

异常订阅可能属于不同的计费体系或 Offer,例如:

  • Enterprise Agreement(EA)
  • Microsoft Customer Agreement(MCA)
  • Cloud Solution Provider(CSP)
  • Microsoft Online Subscription Program(MOSP)
  • Visual Studio、Azure Sponsorship、Free Trial 等权益型订阅
  • 内部、测试或特殊报价订阅

部分报价的价格由企业协议、合作伙伴或特定 Billing Profile 管理,Portal 内部接口未必能在 Subscription 层直接返回价格。

可以对比正常订阅和异常订阅的以下信息:

  • Billing account
  • Billing profile
  • Invoice section
  • Offer 或 Subscription type
  • Enrollment account
  • Partner 管理关系
  • Billing currency 和市场区域

2. 当前用户只有资源权限,没有账单权限

Azure RBAC 权限与账单权限并不完全相同。用户即使是 Subscription 的 Owner 或 Contributor,也未必有权查看协议价格、发票或成本数据。

常见情况包括:

  • 只有 Subscription RBAC 角色,没有 Billing scope 角色
  • EA 中未启用 Account Owner 或 Department Administrator 的费用查看权限
  • MCA 中未获得 Billing account、Billing profile 或 Invoice section 的访问权限
  • CSP Subscription 的成本由合作伙伴管理
  • 租户策略禁止显示费用或协议价格

权限不足不一定会返回 403。Portal 的聚合接口可能仍然返回成功响应,只是将价格字段留空。

3. Subscription 没有可匹配的价格数据

以下状态可能导致暂时没有价格可返回:

  • Subscription 刚创建
  • Subscription 刚转移到其他租户或 Billing account
  • 最近更换了 Offer、协议或 Billing Profile
  • 订阅尚未产生计量用量
  • 用量数据已经产生,但尚未完成计费处理
  • 资源使用发生在尚未结算的时间窗口
  • Subscription 已禁用、取消、过期或处于异常状态

Azure 的资源部署、用量采集、价格匹配和成本计算并非同一条实时链路。因此,新订阅或刚完成计费关系变更的订阅可能出现同步延迟。具体延迟时间取决于计费类型,无法只通过这个内部接口判断。

4. 资源或计量项无法匹配价格

如果接口返回规格价格,而异常订阅使用了特殊资源、区域或计费条款,后端可能找不到适用的价格。例如:

  • 协议中存在自定义折扣或私有价格
  • 使用了预留实例、Savings Plan、Azure Hybrid Benefit 等优惠
  • Marketplace 项目由第三方定价
  • 资源位于特殊区域
  • Meter、SKU、currency 或 billing market 无法正确匹配
  • 费用由其他 Billing scope 承担

这种情况下,可能只有部分规格的价格为空,也可能整个 pricing costs 都为空。

5. Portal 内部接口存在兼容性或后端数据问题

该域名和 API 路径用于 Portal 场景,可能依赖:

  • Portal 当前上下文
  • 特定请求头
  • 登录令牌中的 tenant 和 billing claims
  • Portal 前端版本
  • 后端功能开关
  • Subscription 与 Billing scope 的内部映射

即使相同调用对其他 Subscription 有效,也不能据此认定所有计费类型都受支持。如果官方 Cost Management 页面也无法显示该订阅的成本,问题更可能出在权限、计费配置或后端数据上。如果官方页面显示正常,而该接口返回 null,则更可能是内部接口的行为差异。

排查步骤

1. 确认 Subscription 状态

在 Azure Portal 的 Subscriptions 页面检查:

  • Subscription 是否为 Enabled
  • 是否刚创建、转移或更改计费关系
  • Tenant ID 是否与当前登录租户一致
  • Billing account 或 Offer 是否与正常订阅相同

也可以通过 Azure CLI 查看基础信息:

az account show --subscription "<subscription-id>"

重点检查 id、tenantId、state 和用户上下文。该命令只能确认订阅状态,不能证明用户具有账单访问权限。

2. 在 Cost Management 中验证

切换到异常 Subscription 对应的计费范围,检查:

  • Cost analysis 是否能显示成本
  • Invoices 或 Usage 是否可访问
  • 时间范围内是否存在实际用量
  • 是否出现权限或数据不可用提示
  • 切换到 Billing account、Billing profile 或 Invoice section 后是否能够查看成本

如果 Cost Management 同样没有数据,应先处理计费范围、权限或数据同步问题,不要继续依赖 GetSpecsCosts。

3. 对比正常和异常请求

在浏览器开发者工具的 Network 面板中,分别捕获两个 Subscription 的请求,然后对比:

  • Subscription ID
  • Tenant 上下文
  • 请求头
  • 响应正文
  • Billing scope
  • Currency
  • Locale
  • Portal 是否发起了额外的前置请求
  • 是否存在被忽略的警告或错误字段

不要在工单、日志或聊天中公开 Authorization、Cookie、访问令牌或完整账户信息。

4. 检查账单访问权限

请 Billing account 管理员确认当前用户是否具备对应范围的成本查看权限。仅在 Subscription 上添加 Reader、Contributor 或 Owner,不一定能解决问题。

如果启用了成本可见性策略,还需确认该 Subscription 所属协议允许用户查看费用。具体角色名称和配置入口会因 EA、MCA、CSP 等计费体系而异。

5. 使用受支持的 API 验证成本数据

生产集成应优先使用 Azure Cost Management 的公开 API,不要使用 Portal 内部接口。例如,可以通过 Cost Management Query API 查询 Subscription 范围内的实际成本:

POST https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.CostManagement/query?api-version={supported-api-version}
Authorization: Bearer <access-token>
Content-Type: application/json

请求体示例:

{
  "type": "ActualCost",
  "timeframe": "MonthToDate",
  "dataset": {
    "granularity": "Daily",
    "aggregation": {
      "totalCost": {
        "name": "Cost",
        "function": "Sum"
      }
    }
  }
}

api-version 应从当前 Azure Cost Management 官方文档或资源提供程序元数据中选择受支持的版本,不要直接照搬 Portal 内部接口使用的 2019-01-14。

调用后需要区分空数据、权限错误和接口失败:

async function querySubscriptionCost(subscriptionId, apiVersion, token) {
  const url =
    `https://management.azure.com/subscriptions/${encodeURIComponent(subscriptionId)}` +
    `/providers/Microsoft.CostManagement/query?api-version=${encodeURIComponent(apiVersion)}`;

  const response = await fetch(url, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      type: "ActualCost",
      timeframe: "MonthToDate",
      dataset: {
        granularity: "Daily",
        aggregation: {
          totalCost: {
            name: "Cost",
            function: "Sum"
          }
        }
      }
    })
  });

  const body = await response.json();

  if (!response.ok) {
    throw new Error(
      `Cost Management query failed: ${response.status} ${JSON.stringify(body)}`
    );
  }

  const rows = body?.properties?.rows;

  if (!Array.isArray(rows) || rows.length === 0) {
    return {
      status: "no-data",
      message: "请求成功,但当前范围和时间段没有可返回的成本数据。"
    };
  }

  return {
    status: "ok",
    data: body
  };
}

该查询返回的是已经处理过的成本数据,不是所有 SKU 的公开目录价格。如果需要查询零售价格,应使用 Azure Retail Prices API。如果需要查询客户协议价格,则应使用与相应 Billing account 类型匹配且受支持的计费 API。

6. 必要时提交 Azure 支持请求

如果同时满足以下条件,建议提交 Billing/Cost Management 支持请求:

  • Subscription 状态正常
  • 当前用户已获得正确的 Billing scope 权限
  • Cost Management 在合理时间后仍无数据
  • 相同计费类型的其他 Subscription 工作正常
  • 异常订阅存在实际用量
  • 问题在不同浏览器或用户下均能复现

提交时提供以下脱敏信息:

  • Subscription ID
  • Tenant ID
  • Billing account 类型
  • 问题首次出现的时间及时区
  • Portal Correlation ID、Request ID
  • 正常与异常响应的结构差异
  • 是否发生过 Subscription 转移或协议变更

注意事项

  • 不要把 pricing costs: null 当作费用为 0,两者含义不同。
  • 不要在生产程序中依赖 billing.ext.azure.com 下的 Portal 内部 API,其结构、权限要求和可用性都可能变化。
  • Retail Prices、协议价格、实际成本和预测成本是不同的数据,不能相互替代。
  • 比较 Subscription 时,应确保时间范围、币种、Billing scope、权限和计费模式一致。
  • 如果 Subscription 由 CSP 合作伙伴管理,最终客户可能无法直接取得合作伙伴结算价格,需要联系合作伙伴确认。
  • 如果问题只出现在刚创建或刚转移的 Subscription 上,可以先等待计费数据完成处理。如果长时间未恢复,应通过 Azure 支持核查后端映射。

备注:内容仅供参考。