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>
This commit is contained in:
189
sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs
Normal file
189
sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1b7f5b1ccc26bf047b00f9b79d7fa3da
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -128,7 +128,47 @@ namespace tysdk
|
||||
}
|
||||
return;
|
||||
}
|
||||
TYSDKCallbackManager.Instance.TryTriggerCallback(result);
|
||||
var verifier = PaymentVerifierFactory.GetVerifier();
|
||||
if (verifier != null && !string.IsNullOrEmpty(result.purchaseJson))
|
||||
{
|
||||
_VerifyPurchase(result, verifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
TYSDKCallbackManager.Instance.TryTriggerCallback(result);
|
||||
}
|
||||
}
|
||||
|
||||
private async void _VerifyPurchase(PaymentInfo result, IPaymentVerifier verifier)
|
||||
{
|
||||
TYSDKCallbackManager.Instance.ResetCallbackTimeout<PaymentInfo>(TimeSpan.FromSeconds(60));
|
||||
try
|
||||
{
|
||||
var verifyResult = await verifier.Verify(result);
|
||||
if (verifyResult.success)
|
||||
{
|
||||
result.verified = true;
|
||||
if (!string.IsNullOrEmpty(result.purchaseToken))
|
||||
{
|
||||
verifier.Consume(result.purchaseToken);
|
||||
}
|
||||
TYSDKCallbackManager.Instance.TryTriggerCallback(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[TYSdkFacade] Payment verification failed: " + verifyResult.msg);
|
||||
result.code = "-1";
|
||||
result.msg = "verification_failed: " + verifyResult.msg;
|
||||
TYSDKCallbackManager.Instance.TryTriggerCallback(result);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError("[TYSdkFacade] Verification exception: " + e.Message);
|
||||
result.code = "-1";
|
||||
result.msg = "verification_error: " + e.Message;
|
||||
TYSDKCallbackManager.Instance.TryTriggerCallback(result);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ProductListInfo> GetSKUList()
|
||||
|
||||
@@ -24,6 +24,8 @@ namespace tysdk
|
||||
// Flexion: purchase token, used to consume after server verification
|
||||
public string purchaseToken;
|
||||
|
||||
public bool verified;
|
||||
|
||||
public bool Check(string orderId, string productId)
|
||||
{
|
||||
confirmed = orderId == this.orderId && productId == this.productId;
|
||||
|
||||
@@ -168,14 +168,6 @@ namespace tysdk
|
||||
}
|
||||
}
|
||||
|
||||
public static void ConsumeFlexionPurchase(string token)
|
||||
{
|
||||
using (var sdkManager = new AndroidJavaClass(SDK_CLASS))
|
||||
{
|
||||
sdkManager.CallStatic("ConsumeFlexionPurchase", token);
|
||||
}
|
||||
}
|
||||
|
||||
public static void UnityKnow(String productId, String productName, String productCount, String prodorderId, String appInfo)
|
||||
{
|
||||
using (var sdkManager = new AndroidJavaClass(SDK_CLASS))
|
||||
|
||||
Reference in New Issue
Block a user