From de3ff3b7a42763686f9fe33ac1b5a06ce48ac941 Mon Sep 17 00:00:00 2001 From: "Liubing\\LB" Date: Mon, 11 May 2026 11:25:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E4=BB=98=E9=AA=8C=E7=AD=BE?= =?UTF-8?q?=E6=A1=86=E6=9E=B6=20=E2=80=94=20=E7=AD=96=E7=95=A5=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=20+=20=E8=A3=85=E9=A5=B0=E5=99=A8=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 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 --- .../Packages/tysdk/Runtime/PaymentVerifier.cs | 189 ++++++++++++++++++ .../tysdk/Runtime/PaymentVerifier.cs.meta | 11 + .../Packages/tysdk/Runtime/TYSdkFacade_Pay.cs | 42 +++- .../Packages/tysdk/Runtime/TYSdkModel.cs | 2 + .../Packages/tysdk/Runtime/UnityBridgeFunc.cs | 8 - 5 files changed, 243 insertions(+), 9 deletions(-) create mode 100644 sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs create mode 100644 sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs.meta diff --git a/sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs b/sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs new file mode 100644 index 0000000..a148e49 --- /dev/null +++ b/sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs @@ -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 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 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 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 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 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(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 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); + } + } +} diff --git a/sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs.meta b/sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs.meta new file mode 100644 index 0000000..705f354 --- /dev/null +++ b/sdk-intergration/Packages/tysdk/Runtime/PaymentVerifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1b7f5b1ccc26bf047b00f9b79d7fa3da +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade_Pay.cs b/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade_Pay.cs index 7d4b2b1..a791136 100644 --- a/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade_Pay.cs +++ b/sdk-intergration/Packages/tysdk/Runtime/TYSdkFacade_Pay.cs @@ -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(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 GetSKUList() diff --git a/sdk-intergration/Packages/tysdk/Runtime/TYSdkModel.cs b/sdk-intergration/Packages/tysdk/Runtime/TYSdkModel.cs index 375df67..b5b5c6a 100644 --- a/sdk-intergration/Packages/tysdk/Runtime/TYSdkModel.cs +++ b/sdk-intergration/Packages/tysdk/Runtime/TYSdkModel.cs @@ -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; diff --git a/sdk-intergration/Packages/tysdk/Runtime/UnityBridgeFunc.cs b/sdk-intergration/Packages/tysdk/Runtime/UnityBridgeFunc.cs index 681b7ea..e665a43 100644 --- a/sdk-intergration/Packages/tysdk/Runtime/UnityBridgeFunc.cs +++ b/sdk-intergration/Packages/tysdk/Runtime/UnityBridgeFunc.cs @@ -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))