PAYATHON 2026

.NET 中 Samsung Checkout BuyItem 返回错误回调

支付老李

结论

这个回调只能表明 BuyItem 在结账界面启动前失败,不能据此判断商品数据解析失败。optionalData 是原样返回的订单业务数据,不包含错误详情。

结合 BillingPlugin.PaymentCallback() 和 purchase GUI 完全没有显示的现象,建议优先检查以下几项:

  1. 当前 Samsung Account、电视销售地区或商品销售地区没有可用的 payment method。
  2. 应用、商品或测试账号尚未在 Samsung Seller Office 中正确关联或启用。
  3. 测试环境是模拟器、非目标 Samsung TV、开发安装环境或不受支持的地区。
  4. BuyItem 使用的商品 ID、价格或币种与查询接口返回的数据不一致。
  5. Checkout 所需的权限、应用标识或证书配置不完整。

当前回调只有 {"result":"FAIL"},没有 errorCode 或 errorMessage,因此无法确定唯一原因。

为什么商品查询成功,购买仍然失败

商品查询和发起购买是两个不同的阶段。

商品查询成功通常只能说明:

  • Checkout 服务可以调用;
  • 应用能够访问商品目录;
  • 商品 ID 可以被查询;
  • 查询请求的基本配置有效。

调用 BuyItem 时,系统还会检查:

  • 当前设备和地区是否允许购买;
  • Samsung Account 是否满足购买条件;
  • 是否有可用的支付渠道;
  • 商品是否在当前国家或地区销售;
  • 订单价格、币种和商品信息是否一致;
  • 应用是否具备发起实际交易的资格。

如果检查在支付界面创建前失败,系统不会显示 purchase GUI,而会直接触发错误回调。调用栈中出现 PaymentCallback() 不一定说明 .NET 回调代码有问题。它也可能只是 Checkout 内部支付准备流程的返回入口。

首先检查运行环境

Samsung Checkout 应在受支持的实体 Samsung TV 上验证。不能只根据模拟器或普通 Tizen 设备的测试结果判断集成是否正确。

需要确认:

  • 使用的 Samsung TV 型号和 Tizen 版本受文档支持;
  • 电视已登录有效的 Samsung Account;
  • 电视的服务地区与 Samsung Account 地区一致;
  • 电视可以正常连接 Samsung 服务;
  • 系统时间、日期和时区正确;
  • 设备能够打开 Samsung Apps 等系统服务;
  • 账号所在地区支持 Checkout 和对应商品;
  • 账号至少有一种可用于测试的支付方式。

如果同一账号在电视系统中也无法配置支付方式,应先处理账号或地区问题。此时继续修改 BuyItem 参数通常没有作用。

检查 Seller Office 配置

除了确认商品可以查询,还需要核对应用和商品的后台状态:

  • Checkout 中登记的应用 ID 与实际运行的应用一致;
  • 商品关联到了正确的应用;
  • 商品已在当前测试国家或地区启用;
  • 商品币种是该地区支持的币种;
  • 商品目前可以购买,不是草稿、停用或超出售卖时间的状态;
  • 测试账号已加入对应的测试配置;
  • 没有混用测试商品和生产商品;
  • 测试应用的包名、证书与 Seller Office 中登记的信息一致。

开发安装的应用能否完成真实或测试购买,取决于 Samsung Checkout 当前的测试策略和后台配置。如果查询成功,但所有商品都在支付方式准备阶段购买失败,应请 Samsung 支持人员核对应用 ID、商品 ID、测试账号和电视地区的服务端状态。

不要重新拼装价格和币种

调用 BuyItem 时,尽量直接使用商品查询接口返回的商品标识、价格和币种。不要自行转换币种,也不要根据本地格式重新格式化价格。

例如,下面的写法容易引入问题:

// 不建议:价格经过浮点数和区域格式化后,可能变成 "7,45"
// 或产生与商品目录不一致的值。
double price = 7.45;
string orderTotal = price.ToString();

如果 API 要求传入字符串,可以直接使用查询响应中的原始价格。确实需要格式化时,应使用固定区域和 decimal:

using System.Globalization;

decimal price = 7.45m;
string orderTotal = price.ToString("0.00", CultureInfo.InvariantCulture);

提交前还可以校验数据是否一致:

using System;

static void ValidatePurchase(
    string productId,
    string orderItemId,
    string productCurrency,
    string orderCurrency,
    decimal productPrice,
    decimal orderTotal)
{
    if (string.IsNullOrWhiteSpace(productId))
        throw new ArgumentException("Product ID is missing.");

    if (!string.Equals(productId, orderItemId, StringComparison.Ordinal))
        throw new InvalidOperationException(
            "OrderItemID does not match the queried product ID.");

    if (!string.Equals(
            productCurrency,
            orderCurrency,
            StringComparison.OrdinalIgnoreCase))
    {
        throw new InvalidOperationException(
            "OrderCurrencyID does not match the product currency.");
    }

    if (productPrice != orderTotal)
        throw new InvalidOperationException(
            "OrderTotal does not match the queried product price.");
}

其中的字段名需要根据实际 SDK 返回对象进行映射。示例使用了 productId,并不代表 SDK 中的属性一定使用这个名称。

正确记录回调内容

当前回调中的 optionalData 是经过 JSON 转义的字符串,需要按第二层 JSON 解析。它只能用来关联订单,不能作为错误原因。

using System;
using System.Text.Json;

public sealed class BillingCallback
{
    public string Result { get; set; }
    public string OptionalData { get; set; }
}

public sealed class OrderData
{
    public string OrderTotal { get; set; }
    public string OrderItemID { get; set; }
    public string OrderTitle { get; set; }
    public string OrderCurrencyID { get; set; }
    public string OrderCustomID { get; set; }
}

static void HandleBillingCallback(string callbackJson)
{
    var callback = JsonSerializer.Deserialize<BillingCallback>(
        callbackJson,
        new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        });

    Console.WriteLine($"Checkout result: {callback?.Result}");
    Console.WriteLine($"Checkout raw callback: {callbackJson}");

    if (!string.IsNullOrWhiteSpace(callback?.OptionalData))
    {
        try
        {
            var order = JsonSerializer.Deserialize<OrderData>(
                callback.OptionalData,
                new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true
                });

            Console.WriteLine($"OrderItemID: {order?.OrderItemID}");
            Console.WriteLine($"OrderCurrencyID: {order?.OrderCurrencyID}");
            Console.WriteLine($"OrderTotal: {order?.OrderTotal}");
        }
        catch (JsonException ex)
        {
            Console.WriteLine($"optionalData parse failed: {ex.Message}");
        }
    }
}

排查时还应记录:

  • BuyItem 的完整原始请求,但不要记录密钥或账号凭据;
  • 商品查询接口的原始响应;
  • 设备型号和 Tizen 版本;
  • 电视所在的国家或地区;
  • Samsung Account 所属的国家或地区;
  • 应用 ID、商品 ID 和测试时间;
  • SDK 返回的全部字段,包括可能存在的错误码和错误消息。

检查权限和初始化顺序

请按照当前目标电视版本对应的 Samsung 文档核对 manifest privilege。不要从其他 Tizen profile 或旧版示例中复制权限名称,因为不同 SDK 和电视版本的要求可能不同。

同时确认:

  1. Billing/Checkout 服务已经完成初始化。
  2. 商品查询的成功回调已经完整结束。
  3. BuyItem 在 UI 线程或 SDK 指定的线程中调用。
  4. 应用没有同时发起多个购买请求。
  5. 上一笔交易或 Checkout 回调已经处理完成。
  6. 传入的 JSON 没有重复编码、字段拼写错误或额外嵌套。
  7. 异步购买期间,回调对象没有被释放。

如果应用使用了代码混淆或裁剪,还要确认回调类型没有被裁剪,尤其是依赖反射或序列化绑定的类型。不过,这类问题通常表现为回调无法解析,与当前能够正常收到 FAIL 的现象不太一致。

推荐的定位顺序

建议按以下顺序排查:

  1. 在实体 Samsung TV 上测试,并登录有效的 Samsung Account。
  2. 检查电视地区、账号地区和商品销售地区是否一致。
  3. 在电视账号设置中确认有可用的 payment method。
  4. 使用商品查询返回的原始商品 ID、价格和币种调用 BuyItem。
  5. 核对 Seller Office 中应用、商品和测试账号之间的关联。
  6. 换用另一个已确认支持 Checkout 的账号或地区进行交叉测试。
  7. 保存完整请求、回调、电视型号、应用 ID 和商品 ID,并提交给 Samsung Checkout 技术支持检查服务端日志。

如果所有商品都返回相同的 FAIL,问题更可能出在账号、地区、支付渠道或应用级配置上。如果只有某个商品失败,应先检查该商品的状态、销售地区、价格和币种配置。仅凭当前回调,不能认定这是 TizenFX SDK 的缺陷。

备注:内容仅供参考。