Files
tysdk-intergration/sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs
Liubing\LB de3ff3b7a4 feat: 支付验签框架 — 策略模式 + 装饰器重试
- 新增 IPaymentVerifier 接口 (Verify + Consume),渠道无关
- 新增 PaymentVerifierFactory 按 Channel 分发
- 新增 FlexionPaymentVerifier: HTTP 验签 + 线性 backoff 重试
- 新增 DummyPaymentVerifier: 占位验签实现
- 新增 RetryablePaymentVerifier: 装饰器,流程级重试
- TYSdkFacade_Pay: PayResult 增加验签分支,先验签后消费再回调
- TYSdkModel: PaymentInfo 增加 verified 字段
- UnityBridgeFunc: 移除 ConsumeFlexionPurchase(职责收归 Verifier)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 11:25:20 +08:00

190 lines
6.9 KiB
C#

using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
namespace tysdk
{
public interface IPaymentVerifier
{
Task<VerifyResult> Verify(PaymentInfo info);
void Consume(string token);
}
public class VerifyResult
{
public bool success;
public string msg;
}
public static class PaymentVerifierFactory
{
public static IPaymentVerifier GetVerifier()
{
return ChannelConfig.Channel switch
{
"Flexion" => new RetryablePaymentVerifier(new FlexionPaymentVerifier()),
"Dummy" => new RetryablePaymentVerifier(new DummyPaymentVerifier()),
_ => null
};
}
}
public class RetryablePaymentVerifier : IPaymentVerifier
{
private readonly IPaymentVerifier _inner;
private const int MaxRetries = 2;
private static readonly TimeSpan RetryInterval = TimeSpan.FromSeconds(2);
public RetryablePaymentVerifier(IPaymentVerifier inner)
{
_inner = inner;
}
public async Task<VerifyResult> Verify(PaymentInfo info)
{
VerifyResult lastResult = null;
for (int attempt = 0; attempt <= MaxRetries; attempt++)
{
if (attempt > 0)
{
Debug.Log($"[RetryableVerifier] Flow retry {attempt}/{MaxRetries}, waiting {RetryInterval.TotalSeconds}s");
await Task.Delay(RetryInterval);
}
lastResult = await _inner.Verify(info);
if (lastResult.success) return lastResult;
Debug.LogWarning($"[RetryableVerifier] Flow attempt {attempt + 1} failed: {lastResult.msg}");
}
return lastResult;
}
public void Consume(string token) => _inner.Consume(token);
}
public class FlexionPaymentVerifier : IPaymentVerifier
{
// TODO: 替换为实际游戏服务端验签 URL
private const string VERIFY_URL = "https://YOUR_GAME_SERVER/api/pay/verify";
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);
private const int MaxRetries = 2;
private static readonly TimeSpan RetryBackoff = TimeSpan.FromSeconds(2);
#if UNITY_EDITOR
public async Task<VerifyResult> Verify(PaymentInfo info)
{
Debug.LogWarning("[FlexionPaymentVerifier] Editor placeholder: auto success");
await Task.Yield();
return new VerifyResult { success = true, msg = "editor_placeholder" };
}
#else
public async Task<VerifyResult> Verify(PaymentInfo info)
{
VerifyResult lastResult = null;
for (int attempt = 0; attempt <= MaxRetries; attempt++)
{
if (attempt > 0)
{
var delay = TimeSpan.FromSeconds(RetryBackoff.TotalSeconds * attempt);
Debug.Log($"[FlexionPaymentVerifier] HTTP retry {attempt}/{MaxRetries}, waiting {delay.TotalSeconds}s");
await Task.Delay(delay);
}
lastResult = await SendVerifyRequest(BuildRequestBody(info));
if (lastResult.success) return lastResult;
Debug.LogWarning($"[FlexionPaymentVerifier] HTTP attempt {attempt + 1} failed: {lastResult.msg}");
}
return lastResult;
}
private async Task<VerifyResult> SendVerifyRequest(string body)
{
var cts = new CancellationTokenSource(Timeout);
try
{
Debug.Log("[FlexionPaymentVerifier] Sending verification request");
using var req = new UnityWebRequest(VERIFY_URL, "POST");
req.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body));
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/json");
var tcs = new TaskCompletionSource<VerifyResult>(TaskCreationOptions.RunContinuationsAsynchronously);
cts.Token.Register(() => tcs.TrySetResult(new VerifyResult { success = false, msg = "timeout" }));
req.SendWebRequest().completed += _ =>
{
if (req.result == UnityWebRequest.Result.Success)
{
Debug.Log("[FlexionPaymentVerifier] Response: " + req.downloadHandler.text);
// TODO: 按实际服务端响应格式解析
tcs.TrySetResult(new VerifyResult { success = true, msg = "ok" });
}
else
{
Debug.LogWarning("[FlexionPaymentVerifier] Failed: " + req.error);
tcs.TrySetResult(new VerifyResult { success = false, msg = req.error });
}
};
return await tcs.Task;
}
catch (Exception e)
{
Debug.LogError("[FlexionPaymentVerifier] Exception: " + e.Message);
return new VerifyResult { success = false, msg = e.Message };
}
finally
{
cts.Dispose();
}
}
private static string BuildRequestBody(PaymentInfo info)
{
var body = new StringBuilder();
body.Append('{');
body.Append("\"purchaseJson\":").Append(Escape(info.purchaseJson)).Append(',');
body.Append("\"signature\":").Append(Escape(info.signature)).Append(',');
body.Append("\"orderId\":").Append(Escape(info.orderId)).Append(',');
body.Append("\"productId\":").Append(Escape(info.productId)).Append(',');
body.Append("\"purchaseToken\":").Append(Escape(info.purchaseToken));
body.Append('}');
return body.ToString();
}
#endif
private static string Escape(string s)
{
if (s == null) return "null";
return '"' + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + '"';
}
#if UNITY_EDITOR
public void Consume(string token) { }
#elif UNITY_ANDROID
public void Consume(string token)
{
if (string.IsNullOrEmpty(token)) return;
using var sdkManager = new AndroidJavaClass("com.unity3d.player.SDKManager");
sdkManager.CallStatic("ConsumeFlexionPurchase", token);
}
#endif
}
public class DummyPaymentVerifier : IPaymentVerifier
{
public async Task<VerifyResult> Verify(PaymentInfo info)
{
Debug.Log("[DummyPaymentVerifier] Verifying purchase: " + info.orderId);
await Task.Delay(500);
return new VerifyResult { success = true, msg = "dummy_ok" };
}
public void Consume(string token)
{
Debug.Log("[DummyPaymentVerifier] Consume: " + token);
}
}
}