using Edge.Core.Processor; using Edge.Core.Processor.Dispatcher.Attributes; using Edge.Core.UniversalApi; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using VBaoProxyApp.Cloud; namespace VBaoProxyApp { [UniversalApi(Name = OnFuelTrxDoneEventName, EventDataType = typeof(object), Description = "使用短码授权加油的用户完成加油时,将触发此事件")] [UniversalApi(Name = "OnRemotePaymentFailed", EventDataType = typeof(object), Description = "当与云端进行支付流程进行中,且失败了,将触发此事件")] [MetaPartsDescriptor( "V宝车载定位", "用于与V宝车载定位站级系统的对接", new[] { "V宝" })] public class App : IAppProcessor { public const string OnFuelTrxDoneEventName = "OnFuelTrxDone"; private IServiceProvider services; private Applications.FDC.FdcServerHostApp fdcServerApp; private AppConfigV1 appConfig; private TimeSpan InRangeBeaconLifeTime = new TimeSpan(0, 1, 30); private ILogger logger = NullLogger.Instance; private object syncObject = new object(); public class AppConfigV1 { public string UserName { get; set; } = "3038"; public string Password { get; set; } = "111111"; /// /// Sample: http://url:8698 /// DO NOT end with slash /// public string ApiGatewayEntryUrlWithoutLastSlash { get; set; } = "http://47.97.120.160:8698"; public string DeviceSN { get; set; } = "66662e74-d51e-42ae-bc89-2d39312c9c30"; } public class InRangeBeacon { public string BeaconId { get; set; } public DateTime Timestamp { get; set; } public string AuthCode { get; set; } } private ConcurrentDictionary inRangeBeacons = new ConcurrentDictionary(); public string MetaConfigName { get; set; } public void Init(IEnumerable processors) { this.fdcServerApp = processors.WithHandlerOrApp().SelectHandlerOrAppThenCast().FirstOrDefault(); if (this.fdcServerApp == null) { this.logger.LogInformation("There's no FdcServerHostApp defined"); throw new ArgumentException("Must provide FdcServerHostApp"); } //this.fdcServerApp.OnFdcFuelSaleTransactinStateChange += async (s, a) => //{ // if (a.State == Edge.Core.Database.Models.FuelSaleTransactionState.Payable) // { // var trxes = await this.fdcServerApp.GetFuelSaleTrxDetailsAsync(new ApiData() // { // Parameters = new List() { // new ApiDataParameter() // { // Name="releasetoken", // Value = a.Transaction.ReleaseToken.ToString() // } // } // }); // var universalApiHub = this.services.GetRequiredService(); // await universalApiHub.FireEvent(this, OnFuelTrxDoneEventName, new // { // InRangeBeacon = new InRangeBeacon() // { // AuthCode = new Random().Next(0, 9999).ToString().PadLeft(4, '0'), // BeaconId = new Random().Next(99999, 999999999).ToString().PadLeft(10, '0'), // }, // FuelTrx = trxes.FirstOrDefault() // }); // } //}; } public App(AppConfigV1 appConfig, IServiceProvider services) { this.appConfig = appConfig; var loggerFactory = services.GetRequiredService(); this.logger = loggerFactory.CreateLogger("DynamicPrivate_VBaoProxyApp"); this.services = services; CloudHelper.Default.Credential = new CloudCredential() { ApiGatewayEntryUrl = appConfig.ApiGatewayEntryUrlWithoutLastSlash,// "http://wc.shaojun.xyz:8698", UserName = appConfig.UserName,// "507", Password = appConfig.Password,//"111111", DeviceSN = appConfig.DeviceSN,//"1234567890sss", }; } public async Task Start() { var extraInfos = this.fdcServerApp.GetNozzleExtraInfos(); var fuelItemBarcodes = extraInfos.GroupBy(i => i.ProductBarcode).Select(g => g.Key); foreach (var barcode in fuelItemBarcodes) { try { var cloudFuelItem = await CloudHelper.Default.GetPosItemAsync(barcode.ToString()); if (cloudFuelItem == null) throw new InvalidOperationException($"Local fuel item with barcode: {barcode} could not find mapping item in cloud side."); } catch (Exception exxx) { this.logger.LogError($"Failed to retrieve Cloud side fuel item based on local barcode: {barcode}, make sure local fuel items are all" + $" correctly set and mapped(by their item id) in cloud side. error detail: {exxx}"); throw; } } return true; } public Task Stop() { return Task.FromResult(true); } [UniversalApi(Description = "报告用户信标进入了感应范围,并获取验证码短码")] public async Task NotifyBeaconInRange(string userBeaconId) { this.logger.LogDebug($"NotifyBeaconInRange: {userBeaconId}"); try { if (!this.inRangeBeacons.TryGetValue(userBeaconId, out InRangeBeacon exist) || (DateTime.Now.Subtract(exist.Timestamp) > this.InRangeBeaconLifeTime)) { //remove the expired one if (exist != null) { this.logger.LogDebug($" removing an expired one..."); this.inRangeBeacons.TryRemove(userBeaconId, out _); } string authCode = null; while (true) { authCode = new Random().Next(0, 9999).ToString().PadLeft(4, '0'); if (this.inRangeBeacons.Values.Any(i => i.AuthCode == authCode)) continue; break; } var info = new InRangeBeacon() { BeaconId = userBeaconId, Timestamp = DateTime.Now, AuthCode = authCode, }; this.logger.LogDebug($" adding one with beaconId: {userBeaconId}, authCode: {authCode}"); this.inRangeBeacons.TryAdd(userBeaconId, info); return info; //var universalApiHub = this.services.GetRequiredService(); //await universalApiHub.FireEvent(this, OnUserAuthCodeGeneratedEventName, info); } else return exist; } finally { //if (lockTaken) spinLock_NotifyInRangeBeacon.Exit(false); } } [UniversalApi(Description = "验证短码是否有效")] public async Task ValidateAuthCode(string authCode) { bool lockTaken = false; try { var exist = this.inRangeBeacons.FirstOrDefault(i => i.Value.AuthCode == authCode); if (exist.Value != null && (DateTime.Now.Subtract(exist.Value.Timestamp) <= this.InRangeBeaconLifeTime)) { var accountResult = await CloudHelper.Default.GetAccountIdByVBaoBeaconIdAsync(exist.Value.BeaconId); var accountId = accountResult.GetProperty("userId").GetString(); var vehicleLicenseNo = accountResult.GetProperty("vehicleLicenseNo").GetString(); if (string.IsNullOrEmpty(accountId)) { return new { OverallResult = false, FailedReason = $"Could not find account id from remote by vbao beaconId: {exist.Value.BeaconId}, need pre-regist." }; } return new { OverallResult = true, VehicleLicenseNo = vehicleLicenseNo, }; } if (exist.Value != null) { this.logger.LogDebug($"Validating authCode: {authCode}, but it was expired."); this.inRangeBeacons.TryRemove(exist.Key, out _); } this.logger.LogDebug($"Validating authCode: {authCode} with failed."); return new { OverallResult = false, FailedReason = "无效的短码" }; } catch (Exception eeee) { return new { OverallResult = false, FailedReason = $"Unknown reason for retrieve the car plate No. from remote, detail: {eeee}", }; } finally { //if (lockTaken) spinLock_NotifyInRangeBeacon.Exit(false); } } [UniversalApi(Description = "使用掉authcode,并提供相关联的fuel trx release token")] public async Task NotifyAuthCodeConsumedWithFuelTrx(string authCode, int fuelTrxReleaseToken) { bool lockTaken = false; try { //spinLock_NotifyInRangeBeacon.Enter(ref lockTaken); var exist = this.inRangeBeacons.FirstOrDefault(i => i.Value.AuthCode == authCode); if (exist.Value == null) { this.logger.LogDebug($"Consuming authCode: {authCode} failed due to no such auth code."); return new { OverallResult = false, FailedReason = $"Consuming authCode: {authCode} failed due to no such auth code." }; } this.logger.LogDebug($"Consuming authCode: {authCode}, then GetFuelSaleTrxDetails for releaseToken: {fuelTrxReleaseToken}"); var trxes = await this.fdcServerApp.GetFuelSaleTrxDetailsAsync(new ApiData() { Parameters = new List() { new ApiDataParameter() { Name="releasetoken", Value = fuelTrxReleaseToken.ToString() } } }); if (trxes == null || !trxes.Any()) { //this.logger.LogDebug($" trxes details retrieved, count: {trxes?.Count() ?? -1}"); this.logger.LogDebug($"Consuming authCode: {authCode} failed due to no such fule trx in local."); return new { OverallResult = false, FailedReason = $"Consuming authCode: {authCode} partial failed(code consumed) due to no such fule trx." }; } var universalApiHub = this.services.GetRequiredService(); var pumpId = int.Parse(trxes.First().GetType().GetProperty("PumpId").GetValue(trxes.First()).ToString()); var logicalNozzleId = int.Parse(trxes.First().GetType().GetProperty("LogicalNozzleId").GetValue(trxes.First()).ToString()); var trxSeqNoFromPhysicalPump = int.Parse(trxes.First().GetType().GetProperty("TransactionSeqNumberFromPhysicalPump").GetValue(trxes.First()).ToString()); var fuelItemBarcode = trxes.First().GetType().GetProperty("ProductBarcode").GetValue(trxes.First()).ToString(); var amount = decimal.Parse(trxes.First().GetType().GetProperty("Amount").GetValue(trxes.First()).ToString()); var volume = decimal.Parse(trxes.First().GetType().GetProperty("Volume").GetValue(trxes.First()).ToString()); var price = decimal.Parse(trxes.First().GetType().GetProperty("Price").GetValue(trxes.First()).ToString()); var siteLevelNozzleId = int.Parse(trxes.First().GetType().GetProperty("SiteLevelNozzleId").GetValue(trxes.First()).ToString()); this.logger.LogDebug($" local fuel trx details-> pumpId: {pumpId}, logicalNozzleId: {logicalNozzleId}, amount: {amount}, volume: {volume}"); var fdcClientId = 987654; var lockedFuelTrx = await this.fdcServerApp.LockFuelSaleTrxAndNotifyAllFdcClientsAsync( fdcClientId, pumpId, trxSeqNoFromPhysicalPump, fuelTrxReleaseToken); if (lockedFuelTrx == null) { this.logger.LogError($"Local locking trx with releaseToken: {fuelTrxReleaseToken} failed."); await universalApiHub.FireEvent(this, "OnRemotePaymentFailed", new { FuelTrx = trxes.FirstOrDefault(), FailedReason = $"Local fuel trx with releaseToken: {fuelTrxReleaseToken} locking failed." }); return new { OverallResult = false, FailedReason = $"Local fuel trx with releaseToken: {fuelTrxReleaseToken} locking failed." }; } var cloudFuelItem = await CloudHelper.Default.GetPosItemAsync(fuelItemBarcode); if (cloudFuelItem == null) { this.logger.LogError($"Retreieve cloud pos item failed for fuel trx with releaseToken: {fuelTrxReleaseToken} with fuel item barcode: {fuelItemBarcode}"); await universalApiHub.FireEvent(this, "OnRemotePaymentFailed", new { FuelTrx = trxes.FirstOrDefault(), FailedReason = $"Local fuel item with barcode: {fuelItemBarcode} could not find mapping item in cloud side." }); return new { OverallResult = false, FailedReason = $"Local fuel item with barcode: {fuelItemBarcode} could not find mapping item in cloud side." }; } var createdTrxId = await CloudHelper.Default.CreateTransactionAsync(new ClientFuelTrxInfo() { PosItemId = cloudFuelItem.Id, PumpId = pumpId, NozzleId = logicalNozzleId, SiteNozzleNo = siteLevelNozzleId, Amount = amount, Volume = volume, Barcode = int.Parse(cloudFuelItem.BarCode), PayAmount = amount, Source = Dfs.WayneChina.PosModelMini.PosTrxSource.Outdoor, UnitPrice = price }, "vBao pay"); var accountResult = await CloudHelper.Default.GetAccountIdByVBaoBeaconIdAsync(exist.Value.BeaconId); var accountId = accountResult.GetProperty("userId").GetString(); var vehicleLicenseNo = accountResult.GetProperty("vehicleLicenseNo").GetString(); if (string.IsNullOrEmpty(accountId)) { await universalApiHub.FireEvent(this, "OnRemotePaymentFailed", new { FuelTrx = trxes.FirstOrDefault(), FailedReason = $"Could not find account id from remote by vbao beaconId: {exist.Value.BeaconId}, need pre-regist." }); return new { OverallResult = false, FailedReason = $"Could not find account id from remote by vbao beaconId: {exist.Value.BeaconId}, need pre-regist." }; } var commitResult = await CloudHelper.Default.CommitTransactionAsync(createdTrxId, new Dfs.WayneChina.PosModelMini.PosTrxMop() { Mop = new Dfs.WayneChina.PosModelMini.PosMop() { PaymentId = (int)PaymentID.MembershipPay, }, Paid = amount }, accountId); if (!commitResult) { this.logger.LogError($"Commit trx to cloud failed for fuel trx with releaseToken: {fuelTrxReleaseToken}"); await universalApiHub.FireEvent(this, "OnRemotePaymentFailed", new { FuelTrx = trxes.FirstOrDefault(), FailedReason = $"Commit trx to remote failed with MembershipPay, reason unknown." }); return new { OverallResult = false, FailedReason = $"Commit trx to remote failed with MembershipPay, reason unknown." }; } var clearedFuelTrx = await this.fdcServerApp.ClearFuelSaleTrxAndNotifyAllFdcClientsAsync( pumpId, trxSeqNoFromPhysicalPump.ToString(), fuelTrxReleaseToken, fdcClientId.ToString()); if (clearedFuelTrx == null) { this.logger.LogError($"Clear local fuel trx failed for fuel trx with releaseToken: {fuelTrxReleaseToken}"); await universalApiHub.FireEvent(this, "OnRemotePaymentFailed", new { FuelTrx = trxes.FirstOrDefault(), FailedReason = $"Local fuel trx with releaseToken: {fuelTrxReleaseToken} clear failed." }); return new { OverallResult = false, FailedReason = $"Local fuel trx with releaseToken: {fuelTrxReleaseToken} clear failed." }; } await universalApiHub.FireEvent(this, OnFuelTrxDoneEventName, new { InRangeBeacon = new { exist.Value.BeaconId, exist.Value.Timestamp, exist.Value.AuthCode }, FuelTrx = trxes.FirstOrDefault() }); this.inRangeBeacons.TryRemove(exist.Key, out _); this.logger.LogInformation($"Succeed finished a fuel trx payment with cloud and local with releaseToken: {fuelTrxReleaseToken}"); return new { OverallResult = true, VehicleLicenseNo = vehicleLicenseNo, //FailedReason = "", }; } catch (Exception eeee) { return new { OverallResult = false, FailedReason = $"Unknown reason to failed the payment from remote, detail: {eeee}", }; } finally { //if (lockTaken) spinLock_NotifyInRangeBeacon.Exit(false); } } } }