PAYATHON 2026

如何为 billing 注册自定义 audit events

支付老李

结论

如果完全不使用自定义持久化存储,就无法只靠进程内存可靠地登记可计费事件。应用重启、故障或扩缩容都会导致内存状态丢失,进程内存也无法满足计费所需的去重、追溯和审计要求。

业务事件需要写入持久化的审计目标。可以按以下顺序选择方案:

  1. 优先使用当前版本 Invantive Data Access Point 提供的 audit event 或 usage metering 接口。
  2. 如果没有可用的原生接口,可以将事件保存为 Exact Online 中合适的业务对象,例如日志、备注或文档。
  3. 如果以上方案都不适用,就必须引入外部事件存储,否则无法生成可靠的 billing 数据。

为什么技术指标不能代替业务事件

CPU、内存和 SQL 执行次数反映的是资源消耗,无法准确表示用户获得的业务价值。例如,一次上传可能包含 1 条银行交易,也可能包含 8 条,但两次操作执行的 SQL 数量未必有明显差别。

计费记录至少需要包含:

  • 稳定的事件类型,例如 bank_transactions_uploaded
  • 业务数量,例如 8
  • Exact Online division 或客户标识
  • 事件发生时间
  • 唯一事件 ID
  • 相关批次或请求 ID
  • 必要的业务上下文,但不能包含银行账号等敏感数据

建议采用以下事件结构:

{
  "eventId": "01J6H8V2QJ8P3K6Y7M9N4R2T1X",
  "eventType": "bank_transactions_uploaded",
  "quantity": 8,
  "unit": "bank_transaction",
  "division": 123456,
  "occurredAt": "2026-08-29T09:30:00Z",
  "correlationId": "upload-20260829-00042"
}

注册位置

应在上传操作成功提交后登记事件,而不是在用户点击按钮时登记。否则,因验证失败、事务回滚或网络故障而未完成的上传也可能产生费用。

处理顺序可以是:

验证上传内容
    ↓
向 Exact Online 提交交易
    ↓
确认提交成功
    ↓
登记 bank_transactions_uploaded,quantity = 8

如果上传可能部分成功,quantity 应记录实际成功写入的交易数量,而不是请求中原有的数量。

使用 Invantive 原生接口

如果部署版本提供 audit event 或 usage metering API,应优先调用该接口。这类接口通常比普通日志更适合计费汇总和审计。

不同 Invantive 产品和版本暴露的 API 名称及参数可能不同。在没有对应版本文档时,不能假定某个 SQL package、表名或 HTTP endpoint 一定存在。下面的代码只用于说明调用方式,其中 RegisterBillableEvent 是占位名称,必须替换为当前环境实际提供的接口:

var billableEvent = new
{
    EventId = eventId,
    EventType = "bank_transactions_uploaded",
    Quantity = successfulTransactionCount,
    Unit = "bank_transaction",
    Division = division,
    OccurredAt = DateTimeOffset.UtcNow,
    CorrelationId = uploadId
};

// 使用当前 Invantive Data Access Point 版本实际提供的
// audit event 或 usage metering API。
RegisterBillableEvent(billableEvent);

确认接口时,需要核实以下事项:

  • 接口是否会持久化事件,而非只写入诊断日志。
  • 是否允许自定义 eventType。
  • 是否支持 quantity 或类似的数量字段。
  • 重复提交同一个 eventId 时是否具有幂等性。
  • 数据保留期限是否覆盖账单和争议处理周期。
  • 是否可以按客户、division、时间段和事件类型导出数据。

普通 application log、trace 或 diagnostic log 不一定具备这些能力。即使日志中出现了事件文本,也不能据此认定它可以作为正式的计费依据。

使用 Exact Online 保存事件

如果没有原生计量接口,而且所有持久化状态都必须保留在 Exact Online 中,可以选择合适的标准对象承载事件。例如,每个上传批次创建一条文档或日志记录,并在描述或备注中写入结构化信息。

记录内容可以采用以下形式:

Event-ID: 01J6H8V2QJ8P3K6Y7M9N4R2T1X
Event-Type: bank_transactions_uploaded
Quantity: 8
Unit: bank_transaction
Correlation-ID: upload-20260829-00042
Occurred-At: 2026-08-29T09:30:00Z

这种做法不要求 Exact Online 支持 custom tables,但需要事先确认:

  • 集成应用可以创建所选对象。
  • 这种用途符合 Exact Online 的数据模型和 API 限制。
  • 记录不会干扰用户日常使用的业务列表。
  • 普通用户不会意外修改或删除这些对象。
  • API 限流和存储容量可以承受预计的事件数量。
  • 可以通过 Event-ID 检查重复事件。

不要只保存自然语言记录:

Performed an upload of 8 bank transactions

自然语言便于阅读,却不适合稳定汇总。记录中至少还要包含结构化的事件类型和数量。

可靠性与幂等性

这里无法避开“双写”问题:Exact Online 中的交易可能已经成功提交,但审计事件写入失败。建议为每次业务操作生成稳定的 eventId,以便安全重试。

try
{
    int uploadedCount = UploadBankTransactions(transactions);

    RegisterUsageEvent(
        eventId: eventId,
        eventType: "bank_transactions_uploaded",
        quantity: uploadedCount,
        correlationId: uploadId
    );
}
catch
{
    // 不应重复上传已经成功提交的银行交易。
    // 后续只重试登记具有相同 eventId 的 usage event。
    throw;
}

如果事件目标无法按 eventId 去重,重试可能造成重复计费。这种情况下需要额外的持久化 outbox 或去重存储,单靠内存无法彻底解决。

注意事项

  • 不要直接把诊断日志当作计费账本,除非已经确认其持久性、不可篡改性、保留期限和导出能力。
  • 不要将客户名称、银行账号、交易说明等敏感内容写入计量事件。
  • 需要明确数量口径,例如“成功创建的银行交易数”,并在所有版本中保持一致。
  • 批量上传通常记录一条数量为 8 的事件即可,不必写入八条文本日志,除非计费模型要求逐笔追踪。
  • 如果计费数据必须在故障后保持完整,就不能只保存在内存中。至少要在 Invantive、Exact Online 或外部系统中设置一个持久化审计目标。

备注:内容仅供参考。