diff --git a/.github/workflows/dotnet-publish-prerelease.yml b/.github/workflows/dotnet-publish-prerelease.yml new file mode 100644 index 0000000..e5d24d2 --- /dev/null +++ b/.github/workflows/dotnet-publish-prerelease.yml @@ -0,0 +1,92 @@ +name: .NET + +on: + push: + tags: + - pre-v* + +jobs: + build: + name: Build And Publish + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./src + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 6.0.x + - name: Publish for Linux-x64 + run: dotnet publish -c Release -r linux-x64 --self-contained -p:PublishTrimmed=true -p:PublishSingleFile=true -p:PublishReadyToRun=true -o app/linux-x64 + - name: Publish for Windows-x64 + run: dotnet publish -c Release -r win-x64 --self-contained -p:PublishTrimmed=true -p:PublishSingleFile=true -p:PublishReadyToRun=true -o app/win-x64 + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: app + if-no-files-found: error + path: ${{ github.workspace }}/src/app + + create_release: + name: Create Release + needs: build + runs-on: ubuntu-latest + steps: + - name: Download artifact + uses: actions/download-artifact@v3 + with: + name: app + path: app + + - name: Zip artifact + run: | + cd app + mv linux-x64/appsettings.Example.json linux-x64/appsettings.json + zip -r linux-x64.zip linux-x64/* + mv win-x64/appsettings.Example.json win-x64/appsettings.json + zip -r win-x64.zip win-x64/* + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: true + + - name: Upload Release Asset for Linux-x64 + id: upload_linux + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: app/linux-x64.zip + asset_name: linux-x64.zip + asset_content_type: application/zip + + - name: Upload Release Asset for Windows-x64 + id: upload_win + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: app/win-x64.zip + asset_name: win-x64.zip + asset_content_type: application/zip + + cleanup: + name: Cleanup + needs: create_release + runs-on: ubuntu-latest + steps: + - name: Remove artifacts + uses: geekyeggo/delete-artifact@v2 + with: + name: "*" \ No newline at end of file diff --git a/.github/workflows/dotnet-publish-release.yml b/.github/workflows/dotnet-publish-release.yml new file mode 100644 index 0000000..1f59517 --- /dev/null +++ b/.github/workflows/dotnet-publish-release.yml @@ -0,0 +1,92 @@ +name: .NET + +on: + push: + tags: + - v* + +jobs: + build: + name: Build And Publish + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./src + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 6.0.x + - name: Publish for Linux-x64 + run: dotnet publish -c Release -r linux-x64 --self-contained -p:PublishTrimmed=true -p:PublishSingleFile=true -p:PublishReadyToRun=true -o app/linux-x64 + - name: Publish for Windows-x64 + run: dotnet publish -c Release -r win-x64 --self-contained -p:PublishTrimmed=true -p:PublishSingleFile=true -p:PublishReadyToRun=true -o app/win-x64 + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: app + if-no-files-found: error + path: ${{ github.workspace }}/src/app + + create_release: + name: Create Release + needs: build + runs-on: ubuntu-latest + steps: + - name: Download artifact + uses: actions/download-artifact@v3 + with: + name: app + path: app + + - name: Zip artifact + run: | + cd app + mv linux-x64/appsettings.Example.json linux-x64/appsettings.json + zip -r linux-x64.zip linux-x64/* + mv win-x64/appsettings.Example.json win-x64/appsettings.json + zip -r win-x64.zip win-x64/* + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + + - name: Upload Release Asset for Linux-x64 + id: upload_linux + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: app/linux-x64.zip + asset_name: linux-x64.zip + asset_content_type: application/zip + + - name: Upload Release Asset for Windows-x64 + id: upload_win + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: app/win-x64.zip + asset_name: win-x64.zip + asset_content_type: application/zip + + cleanup: + name: Cleanup + needs: create_release + runs-on: ubuntu-latest + steps: + - name: Remove artifacts + uses: geekyeggo/delete-artifact@v2 + with: + name: "*" \ No newline at end of file diff --git a/src/TokenPay.sln b/src/TokenPay.sln index f1efc1b..3e591a4 100644 --- a/src/TokenPay.sln +++ b/src/TokenPay.sln @@ -16,6 +16,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\README.md = ..\README.md EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github_workflows", "github_workflows", "{E016FFE1-38C6-4EB3-91AD-610E78DFBFB7}" + ProjectSection(SolutionItems) = preProject + ..\.github\workflows\dotnet-publish-prerelease.yml = ..\.github\workflows\dotnet-publish-prerelease.yml + ..\.github\workflows\dotnet-publish-release.yml = ..\.github\workflows\dotnet-publish-release.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,6 +36,9 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {E016FFE1-38C6-4EB3-91AD-610E78DFBFB7} = {D54245C4-8D70-442B-91C0-0053856152B0} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {55E2491A-F1DF-4F80-A121-D11734BCBFAF} EndGlobalSection diff --git a/src/TokenPay/BgServices/OrderCheckEVMBaseService.cs b/src/TokenPay/BgServices/OrderCheckEVMBaseService.cs index 9516c12..f78cef0 100644 --- a/src/TokenPay/BgServices/OrderCheckEVMBaseService.cs +++ b/src/TokenPay/BgServices/OrderCheckEVMBaseService.cs @@ -18,7 +18,8 @@ public class OrderCheckEVMBaseService : BaseScheduledService private readonly List _chains; private readonly IServiceProvider _serviceProvider; private readonly FlurlClient client; - + private bool UseDynamicAddress => _configuration.GetValue("UseDynamicAddress", true); + private bool UseDynamicAddressAmountMove => _configuration.GetValue("DynamicAddressConfig:AmountMove", false); public OrderCheckEVMBaseService(ILogger logger, IConfiguration configuration, IHostEnvironment env, @@ -115,16 +116,40 @@ protected override async Task ExecuteAsync() var order = orders.Where(x => x.Amount == RealAmount && x.ToAddress.ToLower() == item.To.ToLower() && x.CreateTime < item.DateTime) .OrderByDescending(x => x.CreateTime)//优先付最后一单 .FirstOrDefault(); + recheck: if (order != null) { order.FromAddress = item.From; order.BlockTransactionId = item.Hash; order.Status = OrderStatus.Paid; - order.PayTime = DateTime.Now; + order.PayTime = item.DateTime; + order.PayAmount = RealAmount; await _repository.UpdateAsync(order); orders.Remove(order); await SendAdminMessage(order); } + else + { + if (UseDynamicAddress && UseDynamicAddressAmountMove) + { + //允许非准确金额支付 + var Move = _configuration.GetSection($"DynamicAddressConfig:{chain.BaseCoin}").Get() ?? []; + if (Move.Length == 2) + { + var Down = Move[0]; //上浮金额 + var Up = Move[1]; //下浮金额 + order = orders.Where(x => x.Amount >= RealAmount - Down && x.Amount <= RealAmount + Up) + .Where(x => x.ToAddress == item.To && x.CreateTime < item.DateTime) + .OrderByDescending(x => x.CreateTime)//优先付最后一单 + .FirstOrDefault(); + if (order != null) + { + order.IsDynamicAmount = true; + goto recheck; + } + } + } + } } } } diff --git a/src/TokenPay/BgServices/OrderCheckEVMERC20Service.cs b/src/TokenPay/BgServices/OrderCheckEVMERC20Service.cs index ce169d7..ad491ef 100644 --- a/src/TokenPay/BgServices/OrderCheckEVMERC20Service.cs +++ b/src/TokenPay/BgServices/OrderCheckEVMERC20Service.cs @@ -19,7 +19,8 @@ public class OrderCheckEVMERC20Service : BaseScheduledService private readonly Channel _channel; private readonly IServiceProvider _serviceProvider; private readonly FlurlClient client; - + private bool UseDynamicAddress => _configuration.GetValue("UseDynamicAddress", true); + private bool UseDynamicAddressAmountMove => _configuration.GetValue("DynamicAddressConfig:AmountMove", false); public OrderCheckEVMERC20Service(ILogger logger, IConfiguration configuration, IHostEnvironment env, @@ -134,16 +135,40 @@ private async Task ERC20(IBaseRepository _repository, string Curren var order = orders.Where(x => x.Amount == item.RealAmount && x.ToAddress.ToLower() == item.To.ToLower() && x.CreateTime < item.DateTime) .OrderByDescending(x => x.CreateTime)//优先付最后一单 .FirstOrDefault(); + recheck: if (order != null) { order.FromAddress = item.From; order.BlockTransactionId = item.Hash; order.Status = OrderStatus.Paid; - order.PayTime = DateTime.Now; + order.PayTime = item.DateTime; + order.PayAmount = item.RealAmount; await _repository.UpdateAsync(order); orders.Remove(order); await SendAdminMessage(order); } + else + { + if (UseDynamicAddress && UseDynamicAddressAmountMove) + { + //允许非准确金额支付 + var Move = _configuration.GetSection($"DynamicAddressConfig:{erc20.Name}").Get() ?? []; + if (Move.Length == 2) + { + var Down = Move[0]; //上浮金额 + var Up = Move[1]; //下浮金额 + order = orders.Where(x => x.Amount >= item.RealAmount - Down && x.Amount <= item.RealAmount + Up) + .Where(x => x.ToAddress == item.To && x.CreateTime < item.DateTime) + .OrderByDescending(x => x.CreateTime)//优先付最后一单 + .FirstOrDefault(); + if (order != null) + { + order.IsDynamicAmount = true; + goto recheck; + } + } + } + } } } } diff --git a/src/TokenPay/BgServices/OrderCheckTRC20Service.cs b/src/TokenPay/BgServices/OrderCheckTRC20Service.cs index 4f27b50..509de40 100644 --- a/src/TokenPay/BgServices/OrderCheckTRC20Service.cs +++ b/src/TokenPay/BgServices/OrderCheckTRC20Service.cs @@ -16,7 +16,8 @@ public class OrderCheckTRC20Service : BaseScheduledService private readonly IHostEnvironment _env; private readonly Channel _channel; private readonly IServiceProvider _serviceProvider; - + private bool UseDynamicAddress => _configuration.GetValue("UseDynamicAddress", true); + private bool UseDynamicAddressAmountMove => _configuration.GetValue("DynamicAddressConfig:AmountMove", false); public OrderCheckTRC20Service(ILogger logger, IConfiguration configuration, IHostEnvironment env, @@ -109,16 +110,40 @@ protected override async Task ExecuteAsync() var order = orders.Where(x => x.Amount == item.Amount && x.ToAddress == item.To && x.CreateTime < item.BlockTimestamp.ToDateTime()) .OrderByDescending(x => x.CreateTime)//优先付最后一单 .FirstOrDefault(); + recheck: if (order != null) { order.FromAddress = item.From; order.BlockTransactionId = item.TransactionId; order.Status = OrderStatus.Paid; - order.PayTime = DateTime.Now; + order.PayTime = item.BlockTimestamp.ToDateTime(); + order.PayAmount = item.Amount; await _repository.UpdateAsync(order); orders.Remove(order); await SendAdminMessage(order); } + else + { + if (UseDynamicAddress && UseDynamicAddressAmountMove) + { + //允许非准确金额支付 + var Move = _configuration.GetSection("DynamicAddressConfig:USDT").Get() ?? []; + if (Move.Length == 2) + { + var Down = Move[0]; //上浮金额 + var Up = Move[1]; //下浮金额 + order = orders.Where(x => x.Amount >= item.Amount - Down && x.Amount <= item.Amount + Up) + .Where(x => x.ToAddress == item.To && x.CreateTime < item.BlockTimestamp.ToDateTime()) + .OrderByDescending(x => x.CreateTime)//优先付最后一单 + .FirstOrDefault(); + if (order != null) + { + order.IsDynamicAmount = true; + goto recheck; + } + } + } + } } } } diff --git a/src/TokenPay/BgServices/OrderCheckTRXService.cs b/src/TokenPay/BgServices/OrderCheckTRXService.cs index ce50ab6..284dd06 100644 --- a/src/TokenPay/BgServices/OrderCheckTRXService.cs +++ b/src/TokenPay/BgServices/OrderCheckTRXService.cs @@ -16,6 +16,8 @@ public class OrderCheckTRXService : BaseScheduledService private readonly IHostEnvironment _env; private readonly Channel _channel; private readonly IServiceProvider _serviceProvider; + private bool UseDynamicAddress => _configuration.GetValue("UseDynamicAddress", true); + private bool UseDynamicAddressAmountMove => _configuration.GetValue("DynamicAddressConfig:AmountMove", false); public OrderCheckTRXService(ILogger logger, IConfiguration configuration, @@ -106,16 +108,40 @@ protected override async Task ExecuteAsync() var order = orders.Where(x => x.Amount == raw.RealAmount && x.ToAddress == raw.ToAddressBase58 && x.CreateTime < item.BlockTimestamp.ToDateTime()) .OrderByDescending(x => x.CreateTime)//优先付最后一单 .FirstOrDefault(); + recheck: if (order != null) { order.FromAddress = raw.OwnerAddress.HexToeBase58(); order.BlockTransactionId = item.TxID; order.Status = OrderStatus.Paid; - order.PayTime = DateTime.Now; + order.PayTime = item.BlockTimestamp.ToDateTime(); + order.PayAmount = raw.Amount; await _repository.UpdateAsync(order); orders.Remove(order); await SendAdminMessage(order); } + else + { + if (UseDynamicAddress && UseDynamicAddressAmountMove) + { + //允许非准确金额支付 + var Move = _configuration.GetSection("DynamicAddressConfig:TRX").Get() ?? []; + if (Move.Length == 2) + { + var Down = Move[0]; //上浮金额 + var Up = Move[1]; //下浮金额 + var orderMove = orders.Where(x => x.Amount >= raw.RealAmount - Down && x.Amount <= raw.RealAmount + Up) + .Where(x => x.ToAddress == raw.ToAddressBase58 && x.CreateTime < item.BlockTimestamp.ToDateTime()) + .OrderByDescending(x => x.CreateTime)//优先付最后一单 + .FirstOrDefault(); + if (order != null) + { + order.IsDynamicAmount = true; + goto recheck; + } + } + } + } } } } diff --git a/src/TokenPay/BgServices/OrderPaySuccessService.cs b/src/TokenPay/BgServices/OrderPaySuccessService.cs index 7fca5f8..79d131a 100644 --- a/src/TokenPay/BgServices/OrderPaySuccessService.cs +++ b/src/TokenPay/BgServices/OrderPaySuccessService.cs @@ -57,11 +57,13 @@ private async Task SendAdminMessage(TokenOrders order) { order.Currency = order.Currency.Replace(item, ""); } + var curreny = order.Currency.Replace("TRC20", "").Split("_", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Last(); var message = @$"您有新订单!({order.ActualAmount} {BaseCurrency}) 订单编号:{order.OutOrderId} 原始金额:{order.ActualAmount} {BaseCurrency} -订单金额:{order.Amount} {order.Currency.Replace("TRC20", "").Split("_", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Last()} +订单金额:{order.Amount} {curreny} +实付金额:{order.PayAmount} {curreny}{(order.IsDynamicAmount ? "(动态金额订单)" : "")} 付款地址:{order.FromAddress} 收款地址:{order.ToAddress} 创建时间:{order.CreateTime:yyyy-MM-dd HH:mm:ss} @@ -85,7 +87,7 @@ private async Task SendAdminMessage(TokenOrders order) { if (order.Currency.StartsWith($"EVM_{chain.ChainNameEN}")) { - if(!string.IsNullOrEmpty(chain.ScanHost)) + if (!string.IsNullOrEmpty(chain.ScanHost)) message += @$" 查看交易"; break; } diff --git a/src/TokenPay/Domains/TokenOrders.cs b/src/TokenPay/Domains/TokenOrders.cs index f752524..2e33722 100644 --- a/src/TokenPay/Domains/TokenOrders.cs +++ b/src/TokenPay/Domains/TokenOrders.cs @@ -29,6 +29,15 @@ public class TokenOrders /// public DateTime? PayTime { get; set; } /// + /// 订单实际支付的金额,保留2位小数 + /// + [Column(Precision = 15, Scale = 2)] + public decimal? PayAmount { get; set; } + /// + /// 是否动态金额订单 + /// + public bool IsDynamicAmount { get; set; } + /// /// 来源地址 /// public string? FromAddress { get; set; } = null!; diff --git a/src/TokenPay/appsettings.Example.json b/src/TokenPay/appsettings.Example.json index d564c28..ffec56e 100644 --- a/src/TokenPay/appsettings.Example.json +++ b/src/TokenPay/appsettings.Example.json @@ -48,5 +48,11 @@ "RateMove": { //汇率微调,支持设置正负数,仅支持两位小数 "TRX_CNY": 0, "USDT_CNY": 0 + }, + "DynamicAddressConfig": { + "AmountMove": false, //使用动态地址收款时启用动态金额,支持非准确金额支付,用于优化中心化钱包或交易所提币扣除手续费后金额不匹配的情况,可自行决定是否开启,默认false表示不启用 + "TRX": [ 0, 2 ], //表示下浮0,上浮2,如果实际支付金额为100TRX,根据此配置,用户支付金额在100-102TRX订单都会成功 + "USDT": [ 1, 2 ], //表示下浮1,上浮2,如果实际支付金额为100USDT,根据此配置,用户支付金额在99-102USDT订单都会成功 + "ETH": [ 0.1, 0.15 ] //表示下浮0.1,上浮0.1,如果实际支付金额为0.5ETH,根据此配置,用户支付金额在0.4-0.65ETH订单都会成功 } }