Files
2026-05-26 16:15:54 +08:00

448 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using asap.core;
using Newtonsoft.Json.Linq;
using tysdk;
using UnityEngine;
using Newtonsoft.Json;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using UnityEngine.Networking;
using System.Threading;
namespace game
{
public class AggIAPService : IIAPService
{
[Inject] public IUserService userService { get; set; }
[Inject] public IConfig config { get; set; }
[Inject] public ICustomServerMgr svr { get; set; }
public void Init() { }
public void Release() { }
#if UNITY_ANDROID
private const string payment = "google";
#else
private const string payment = "apple";
#endif
class IAPConfig
{
public bool Active { get; set; }
public string OrderSvrUrl { get; set; }
public string FuncKey { get; set; }
}
private async Task<IAPConfig> BeforePurch()
{
var configRemoteUrl = config.Get<string>(GConstant.K_Static_Res_URL, GConstant.V_Static_Res_URL);
string iapConfigUrl = null;
//var encrypted = false;
if (configRemoteUrl == GConstant.V_Static_Res_URL)
{
iapConfigUrl = String.Format(configRemoteUrl, $"{Application.platform}iapcfg.json");
//encrypted = true;
}
else
{
iapConfigUrl = configRemoteUrl.Replace(VersionTool.PackVer, $"{Application.platform}iapcfg.json");
}
var timeoutValues = new float[]{1,2,4};
var maxRetryTimes = timeoutValues.Length;
for (int retryTime = 0; retryTime < maxRetryTimes; retryTime++)
{
var iapConfig = await DoGetRawConfig(iapConfigUrl,timeoutValues[retryTime], retryTime, maxRetryTimes);
if (iapConfig != null )
{
Debug.Log($"<color=orange>Load iapConfig success {retryTime + 1}/{maxRetryTimes}</color>");
return iapConfig;
}
}
return null;
}
private async Task<IAPConfig> DoGetRawConfig(string url, float timeoutInSec, int retryTimes, int maxRetryTimes)
{
using(var req = UnityWebRequest.Get(url))
{
var stopwatch = new System.Diagnostics.Stopwatch();
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(timeoutInSec));
var ct = cts.Token;
stopwatch.Start();
req.timeout = (int)timeoutInSec;
req.SetRequestHeader("Accept", "application/json");
var op = req.SendWebRequest();
try
{
while (!op.isDone && !ct.IsCancellationRequested)
{
await Awaiters.NextFrame;
}
ct.ThrowIfCancellationRequested();
stopwatch.Stop();
if (req.result == UnityWebRequest.Result.Success)
{
var decryptKey = "tbambooz";
DESCryptoServiceProvider des = new DESCryptoServiceProvider();
des.Key = Encoding.ASCII.GetBytes(decryptKey);
des.IV = Encoding.ASCII.GetBytes(decryptKey);
var rawCfg = req.downloadHandler.text;
ICryptoTransform decryptor = des.CreateDecryptor(des.Key, des.IV);
byte[] inputBytes = Convert.FromBase64String(rawCfg);
byte[] outputBytes = decryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length);
rawCfg = Encoding.UTF8.GetString(outputBytes);
var iapConfig = JsonConvert.DeserializeObject<IAPConfig>(rawCfg);
Debug.Log($"[AggIAPService] DoGetRawConfig Success {url}");
return iapConfig;
}
#if AGG
using (var e = GEvent.TackEvent("retry"))
{
e.AddContent("type", "iap_config");
e.AddContent("duration_ms", stopwatch.ElapsedMilliseconds);
e.AddContent("reason", req.result.ToString());
e.AddContent("skip", retryTimes >= maxRetryTimes - 1);
e.AddContent("error", req.error);
}
#endif // AGG
Debug.LogWarning($"Load iap config failed {req.result.ToString()} {req.error} {retryTimes + 1}/{maxRetryTimes}");
}
catch (OperationCanceledException)
{
req.Abort();
stopwatch.Stop();
#if AGG
using (var e = GEvent.TackEvent("retry"))
{
e.AddContent("type", "iap_config");
e.AddContent("duration_ms", stopwatch.ElapsedMilliseconds);
e.AddContent("reason", "cancel");
e.AddContent("skip", retryTimes >= maxRetryTimes - 1);
e.AddContent("error", req.error ?? "cancel");
}
#endif // AGG
Debug.LogWarning($"Load remote config cancel {req.result.ToString()} {req.error} {retryTimes + 1}/{maxRetryTimes}");
}
finally
{
cts.Dispose();
}
return null;
}
}
public async Task<IList<SKUDetail>> GetSKUs()
{
Debug.Log("[AggIAPService] GetSKUList fetch");
var prodList = await TYSdkFacade.Instance.GetSKUList();
if (prodList.code != 0)
{
Debug.LogWarning("[AggIAPService] GetSKUList Failed");
return null;
}
Debug.Log($"[AggIAPService] GetSKUList Success prod Num {prodList.products.Count}");
return prodList.products;
}
public async Task<PaymentInfo> Pay(Action<string> OnSuccessFromSdk, string replenishmentDataStr, SKUDetail sku, int count, float usdPrice, JObject extraPurcheInfo)
{
await Awaiters.NextFrame;
GameCore.UIManager.ShowWaitingBlock("AggIAPService::Pay");
await Awaiters.NextFrame;
//======= check iap config
var iapCfg = await BeforePurch();
if (iapCfg == null)
{
Debug.LogWarning("Fetch iap cfg fail");
GameCore.UIManager.HideWaitingBlock("AggIAPService::Pay");
SendRechargeEvent(sku, usdPrice, "iapcfg_fail", extraPurcheInfo, count);
return new PaymentInfo() { code = "10002" };
}
else if (!iapCfg.Active)
{
GameCore.UIManager.HideWaitingBlock("AggIAPService::Pay");
return new PaymentInfo() { code = "10001" };
}
//===== check iap config end======
RTServiceBgFgManager.keepAlive = true;
SendRechargeEvent(sku, usdPrice, "start", extraPurcheInfo, count);
var purchaseInfo = new JObject();
purchaseInfo["ver"] = 3;
purchaseInfo["pfid"] = userService.UserId;
purchaseInfo["sum"] = usdPrice * count;
purchaseInfo["customData"] = replenishmentDataStr;
PaymentInfo paymentInfo = await TYSdkFacade.Instance.Pay(sku, count, usdPrice, purchaseInfo);
if (!paymentInfo.isSuccessFromSdk)
{
SendRechargeEvent(sku, usdPrice, "fail", extraPurcheInfo, count);
}
else
{
OnSuccessFromSdk(paymentInfo.orderId);
await WaitForSvrOrder(paymentInfo, sku, usdPrice, count, extraPurcheInfo, iapCfg);
}
if (paymentInfo.isSuccess)
{
await ConfirmOrder(paymentInfo, iapCfg);
}
GameCore.UIManager.HideWaitingBlock("AggIAPService::Pay");
RTServiceBgFgManager.keepAlive = false;
return paymentInfo;
}
private async Task WaitForSvrOrder(PaymentInfo paymentInfo, SKUDetail sku, float usdPrice, int count, JObject extraPurcheInfo, IAPConfig iapCfg)
{
Debug.Log("[AggIAPService] WaitForSvrOrder");
await Awaiters.Seconds(2);
var startTime = ZZTimeHelper.UtcNow();
var confirmEvt = new CS_OrderConfirmEvt()
{
orderId = paymentInfo.orderId,
sdkUserId = userService.CustomId
}.ToJson();
while (true)
{
var result = await IAPPost("csconfirm", iapCfg, confirmEvt);
Debug.Log("[AggIAPService] WaitForSvrOrder request done");
if (!string.IsNullOrEmpty(result))
{
Debug.Log($"[AggIAPService] WaitForSvrOrder respon {result}");
var sconfirmEvt = JsonConvert.DeserializeObject<SC_OrderConfirmEvt>(result);
if (sconfirmEvt.isSuccess)
{
if (paymentInfo.Check(sconfirmEvt.orderId, sconfirmEvt.productId))
{
SendRevenueEvent(sku, usdPrice, paymentInfo.orderId, extraPurcheInfo, count);
}
else
{
SendRechargeEvent(sku, usdPrice, "checkfail", extraPurcheInfo, paymentInfo.count);
}
return;
}
}
else
{
Debug.Log("[AggIAPService] WaitForSvrOrder request error");
}
await Awaiters.Seconds(2);
if (ZZTimeHelper.UtcNow() - startTime > TimeSpan.FromSeconds(50))
{
SendRechargeEvent(sku, usdPrice, "timeout", extraPurcheInfo, paymentInfo.count);
Debug.Log("[AggIAPService] WaitForSvrOrder timeout");
break;
}
Debug.Log("[AggIAPService] WaitForSvrOrder retry");
}
}
private async Task ConfirmOrder(PaymentInfo paymentInfo, IAPConfig iapCfg)
{
var confirmEvt = new CS_OrderConfirmEvt()
{
orderId = paymentInfo.orderId,
sdkUserId = userService.CustomId
}.ToJson();
int count = 3;
string result;
bool re = false;
while (count > 0)
{
count--;
result = await IAPPost("receive", iapCfg, confirmEvt);
if (bool.TryParse(result, out re) && re)
{
return;
}
}
if (!re)
{
paymentInfo.code = "10000";
}
}
/// <summary>
/// 获取未领奖的订单号列表 时间戳
/// </summary>
/// <param name="day">时间戳 最早订单时间 -1m</param>
/// <returns></returns>
public async Task<Dictionary<string, string>> GetOrderUnreceived()
{
var iapCfg = await BeforePurch();
if (iapCfg == null)
{
Debug.LogError("Fetch iap cfg fail, on get unreceive order ");
return new Dictionary<string, string>();
}
var orderUnreceived = new CS_OrderUnreceived()
{
sdkUserId = userService.CustomId,
customData = true
}.ToJson();
string result = await IAPPost("unreceive", iapCfg, orderUnreceived);
if (string.IsNullOrEmpty(result) || result == "[]")
{
return new Dictionary<string, string>();
}
else
{
SC_OrderUnreceived response = JsonConvert.DeserializeObject<SC_OrderUnreceived>(result);
return response?.orders;
}
}
public async Task<bool> ConfirmOrders(List<string> orderIDs)
{
if (orderIDs == null || orderIDs.Count == 0) return false;
var confirmEvt = new CS_OrdersConfirmEvt()
{
orderIds = orderIDs,
sdkUserId = userService.CustomId
}.ToJson();
var iapCfg = await BeforePurch();
if (iapCfg == null)
{
return false;
}
string result = await IAPPost("receiveorders", iapCfg, confirmEvt);
if (string.IsNullOrEmpty(result) || result == "[]" || !bool.TryParse(result, out bool re))
{
return false;
}
return true;
}
private void SendRechargeEvent(SKUDetail sku, float usdPrice, string state, JObject extraPurcheInfo, int count = 1)
{
using (var e = GEvent.TackEvent("recharge_state"))
{
e.AddContent(AFInAppEvents.CONTENT_ID, sku.ProdID)
.AddContent(AFInAppEvents.PRICE, usdPrice)
.AddContent(AFInAppEvents.CURRENCY, sku.price_currency_code)
.AddContent("payment", payment)
.AddContent("payment_state", state)
.AddContent(AFInAppEvents.QUANTITY, count);
if (null != extraPurcheInfo)
{
foreach (var item in extraPurcheInfo)
{
e.AddContent(item.Key, item.Value);
}
}
}
}
private void SendRevenueEvent(SKUDetail sku, float usdPrice, string orderId, JObject extraPurcheInfo, int count = 1)
{
using (var e = GEvent.RevenueEvent())
{
e.AddContent(AFInAppEvents.CONTENT_ID, sku.ProdID)
.AddContent(AFInAppEvents.PRICE, usdPrice)
.AddContent(AFInAppEvents.CURRENCY, sku.price_currency_code)
.AddContent("payment", payment)
.AddContent(AFInAppEvents.QUANTITY, 1)
.AddContent("order_id", orderId)
.AddContent("restore", false)
.AddContent("payment_state", "finish")
.AddContent("af_revenue", sku.ProdPrice * count);
if (null != extraPurcheInfo)
{
foreach (var item in extraPurcheInfo)
{
e.AddContent(item.Key, item.Value);
}
}
}
}
async Task<string> IAPPost(string route, IAPConfig iapCfg, string bodyString)
{
var apiurl = config.Get<string>(GConstant.K_Event_API_URL, iapCfg.OrderSvrUrl);
if (apiurl == iapCfg.OrderSvrUrl)
{
return await svr.CustomServerPost($"order/{route}", bodyString);
}
else
{
var httpClient = new HttpClient();
httpClient.BaseAddress = new System.Uri(iapCfg.OrderSvrUrl);
httpClient.Timeout = TimeSpan.FromSeconds(10);
httpClient.DefaultRequestHeaders.Add("x-functions-key", iapCfg.FuncKey);
try
{
var content = new StringContent(bodyString, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(route, content);
string result = await response.Content.ReadAsStringAsync();
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
return result;
}
else
{
Debug.LogError($"iap post fail {response.StatusCode}");
return null;
}
}
catch (Exception e)
{
Debug.LogError($"iap post fail {e.Message}");
return null;
}
}
}
}
}