.NET 中 Samsung Checkout BuyItem 返回错误回调
结论
这个回调只能表明 BuyItem 在结账界面启动前失败,不能据此判断商品数据解析失败。optionalData 是原样返回的订单业务数据,不包含错误详情。
结合 BillingPlugin.PaymentCallback() 和 purchase GUI 完全没有显示的现象,建议优先检查以下几项:
- 当前 Samsung Account、电视销售地区或商品销售地区没有可用的 payment method。
- 应用、商品或测试账号尚未在 Samsung Seller Office 中正确关联或启用。
- 测试环境是模拟器、非目标 Samsung TV、开发安装环境或不受支持的地区。
BuyItem使用的商品 ID、价格或币种与查询接口返回的数据不一致。- 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 和电视版本的要求可能不同。
同时确认:
- Billing/Checkout 服务已经完成初始化。
- 商品查询的成功回调已经完整结束。
BuyItem在 UI 线程或 SDK 指定的线程中调用。- 应用没有同时发起多个购买请求。
- 上一笔交易或 Checkout 回调已经处理完成。
- 传入的 JSON 没有重复编码、字段拼写错误或额外嵌套。
- 异步购买期间,回调对象没有被释放。
如果应用使用了代码混淆或裁剪,还要确认回调类型没有被裁剪,尤其是依赖反射或序列化绑定的类型。不过,这类问题通常表现为回调无法解析,与当前能够正常收到 FAIL 的现象不太一致。
推荐的定位顺序
建议按以下顺序排查:
- 在实体 Samsung TV 上测试,并登录有效的 Samsung Account。
- 检查电视地区、账号地区和商品销售地区是否一致。
- 在电视账号设置中确认有可用的 payment method。
- 使用商品查询返回的原始商品 ID、价格和币种调用
BuyItem。 - 核对 Seller Office 中应用、商品和测试账号之间的关联。
- 换用另一个已确认支持 Checkout 的账号或地区进行交叉测试。
- 保存完整请求、回调、电视型号、应用 ID 和商品 ID,并提交给 Samsung Checkout 技术支持检查服务端日志。
如果所有商品都返回相同的 FAIL,问题更可能出在账号、地区、支付渠道或应用级配置上。如果只有某个商品失败,应先检查该商品的状态、销售地区、价格和币种配置。仅凭当前回调,不能认定这是 TizenFX SDK 的缺陷。
备注:内容仅供参考。
