31 changed files with 776 additions and 17 deletions
@ -0,0 +1,74 @@ |
|||||
|
using DotNetCore.CAP.Persistence; |
||||
|
using LINGYUN.Abp.EventBus.CAP; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.BackgroundWorkers; |
||||
|
using Volo.Abp.Threading; |
||||
|
|
||||
|
namespace DotNetCore.CAP.Processor |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 过期消息清理任务
|
||||
|
/// </summary>
|
||||
|
public class AbpCapExpiresMessageCleanupBackgroundWorker : AsyncPeriodicBackgroundWorkerBase |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 过期消息清理配置
|
||||
|
/// </summary>
|
||||
|
protected MessageCleanupOptions Options { get; } |
||||
|
/// <summary>
|
||||
|
/// Initializer
|
||||
|
/// </summary>
|
||||
|
protected IStorageInitializer Initializer { get; } |
||||
|
/// <summary>
|
||||
|
/// Storage
|
||||
|
/// </summary>
|
||||
|
protected IDataStorage Storage{ get; } |
||||
|
/// <summary>
|
||||
|
/// 创建过期消息清理任务
|
||||
|
/// </summary>
|
||||
|
/// <param name="timer"></param>
|
||||
|
/// <param name="storage"></param>
|
||||
|
/// <param name="initializer"></param>
|
||||
|
/// <param name="options"></param>
|
||||
|
/// <param name="serviceScopeFactory"></param>
|
||||
|
public AbpCapExpiresMessageCleanupBackgroundWorker( |
||||
|
AbpTimer timer, |
||||
|
IDataStorage storage, |
||||
|
IStorageInitializer initializer, |
||||
|
IOptions<MessageCleanupOptions> options, |
||||
|
IServiceScopeFactory serviceScopeFactory) |
||||
|
: base(timer, serviceScopeFactory) |
||||
|
{ |
||||
|
Storage = storage; |
||||
|
Options = options.Value; |
||||
|
Initializer = initializer; |
||||
|
|
||||
|
timer.Period = Options.Interval; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 异步执行任务
|
||||
|
/// </summary>
|
||||
|
/// <param name="workerContext"></param>
|
||||
|
/// <returns></returns>
|
||||
|
protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) |
||||
|
{ |
||||
|
var tables = new[] |
||||
|
{ |
||||
|
Initializer.GetPublishedTableName(), |
||||
|
Initializer.GetReceivedTableName() |
||||
|
}; |
||||
|
|
||||
|
foreach (var table in tables) |
||||
|
{ |
||||
|
Logger.LogDebug($"Collecting expired data from table: {table}"); |
||||
|
var time = DateTime.Now; |
||||
|
await Storage.DeleteExpiresAsync(table, time, Options.ItemBatch); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,115 @@ |
|||||
|
using DotNetCore.CAP.Internal; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace DotNetCore.CAP.Processor |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// CapProcessingServer
|
||||
|
/// </summary>
|
||||
|
public class AbpCapProcessingServer : IProcessingServer |
||||
|
{ |
||||
|
private readonly CancellationTokenSource _cts; |
||||
|
private readonly ILogger _logger; |
||||
|
private readonly ILoggerFactory _loggerFactory; |
||||
|
private readonly IServiceProvider _provider; |
||||
|
|
||||
|
private Task _compositeTask; |
||||
|
private ProcessingContext _context; |
||||
|
private bool _disposed; |
||||
|
/// <summary>
|
||||
|
/// CapProcessingServer
|
||||
|
/// </summary>
|
||||
|
/// <param name="logger"></param>
|
||||
|
/// <param name="loggerFactory"></param>
|
||||
|
/// <param name="provider"></param>
|
||||
|
public AbpCapProcessingServer( |
||||
|
ILogger<AbpCapProcessingServer> logger, |
||||
|
ILoggerFactory loggerFactory, |
||||
|
IServiceProvider provider) |
||||
|
{ |
||||
|
_logger = logger; |
||||
|
_loggerFactory = loggerFactory; |
||||
|
_provider = provider; |
||||
|
_cts = new CancellationTokenSource(); |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// Start
|
||||
|
/// </summary>
|
||||
|
public void Start() |
||||
|
{ |
||||
|
_logger.LogInformation("Starting the processing server."); |
||||
|
|
||||
|
_context = new ProcessingContext(_provider, _cts.Token); |
||||
|
|
||||
|
var processorTasks = GetProcessors() |
||||
|
.Select(InfiniteRetry) |
||||
|
.Select(p => p.ProcessAsync(_context)); |
||||
|
_compositeTask = Task.WhenAll(processorTasks); |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// Pulse
|
||||
|
/// </summary>
|
||||
|
public void Pulse() |
||||
|
{ |
||||
|
_logger.LogTrace("Pulsing the processor."); |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// Dispose
|
||||
|
/// </summary>
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
if (_disposed) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
_disposed = true; |
||||
|
|
||||
|
_logger.LogInformation("Shutting down the processing server..."); |
||||
|
_cts.Cancel(); |
||||
|
|
||||
|
_compositeTask?.Wait((int)TimeSpan.FromSeconds(10).TotalMilliseconds); |
||||
|
} |
||||
|
catch (AggregateException ex) |
||||
|
{ |
||||
|
var innerEx = ex.InnerExceptions[0]; |
||||
|
if (!(innerEx is OperationCanceledException)) |
||||
|
{ |
||||
|
_logger.LogWarning(innerEx, $"Expected an OperationCanceledException, but found '{innerEx.Message}'."); |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
_logger.LogWarning(ex, "An exception was occured when disposing."); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
_logger.LogInformation("### CAP shutdown!"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private IProcessor InfiniteRetry(IProcessor inner) |
||||
|
{ |
||||
|
return new InfiniteRetryProcessor(inner, _loggerFactory); |
||||
|
} |
||||
|
|
||||
|
private IProcessor[] GetProcessors() |
||||
|
{ |
||||
|
var returnedProcessors = new List<IProcessor> |
||||
|
{ |
||||
|
_provider.GetRequiredService<TransportCheckProcessor>(), |
||||
|
_provider.GetRequiredService<MessageNeedToRetryProcessor>(), |
||||
|
}; |
||||
|
|
||||
|
return returnedProcessors.ToArray(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
namespace LINGYUN.Abp.EventBus.CAP |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 过期消息清理配置项
|
||||
|
/// </summary>
|
||||
|
public class MessageCleanupOptions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 批量清理数量
|
||||
|
/// default: 1000
|
||||
|
/// </summary>
|
||||
|
public int ItemBatch { get; set; } = 1000; |
||||
|
/// <summary>
|
||||
|
/// 执行间隔(ms)
|
||||
|
/// default: 3600000 (1 hours)
|
||||
|
/// </summary>
|
||||
|
public int Interval { get; set; } = 3600000; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\LINGYUN.Abp.Location\LINGYUN.Abp.Location.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,15 @@ |
|||||
|
using Newtonsoft.Json; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Location.Baidu.Http |
||||
|
{ |
||||
|
public class BaiduInverseLocationResponse : BaiduLocationResponse |
||||
|
{ |
||||
|
[JsonProperty("formatted_address")] |
||||
|
public string Address { get; set; } |
||||
|
|
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Location.Baidu.Http |
||||
|
{ |
||||
|
public class BaiduLocationHttpClient |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
namespace LINGYUN.Abp.Location.Baidu.Http |
||||
|
{ |
||||
|
public abstract class BaiduLocationResponse |
||||
|
{ |
||||
|
public int Status { get; set; } |
||||
|
|
||||
|
public Location Location { get; set; } |
||||
|
|
||||
|
public bool IsSuccess() |
||||
|
{ |
||||
|
return Status == 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
namespace LINGYUN.Abp.Location.Baidu.Http |
||||
|
{ |
||||
|
public class BaiduPositiveLocationResponse : BaiduLocationResponse |
||||
|
{ |
||||
|
public int Precise { get; set; } |
||||
|
public int Confidence { get; set; } |
||||
|
public int Comprehension { get; set; } |
||||
|
public string Level { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
namespace LINGYUN.Abp.Location.Baidu.Http |
||||
|
{ |
||||
|
public class Location |
||||
|
{ |
||||
|
public double Lat { get; set; } |
||||
|
public double Lng { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
|
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<RootNamespace /> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Volo.Abp.Core" Version="2.8.0" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
</Project> |
||||
@ -0,0 +1,8 @@ |
|||||
|
using Volo.Abp.Modularity; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Location |
||||
|
{ |
||||
|
public class AbpLocationModule : AbpModule |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
namespace LINGYUN.Abp.Location |
||||
|
{ |
||||
|
public interface ILocationResolveProvider |
||||
|
{ |
||||
|
PositiveLocation GetPositiveLocation(string address); |
||||
|
|
||||
|
InverseLocation GetInverseLocation(double lat, double lng); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
namespace LINGYUN.Abp.Location |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 逆地址
|
||||
|
/// </summary>
|
||||
|
public class InverseLocation |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 详细地址
|
||||
|
/// </summary>
|
||||
|
public string Address { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 国家
|
||||
|
/// </summary>
|
||||
|
public string Country { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 省份
|
||||
|
/// </summary>
|
||||
|
public string Province { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 城市
|
||||
|
/// </summary>
|
||||
|
public string City { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 区县
|
||||
|
/// </summary>
|
||||
|
public string District { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 街道
|
||||
|
/// </summary>
|
||||
|
public string Street { get; set; } |
||||
|
/// <summary>
|
||||
|
/// adcode
|
||||
|
/// </summary>
|
||||
|
public string AdCode { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 乡镇
|
||||
|
/// </summary>
|
||||
|
public string Town { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 门牌号
|
||||
|
/// </summary>
|
||||
|
public string Number { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,97 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Location |
||||
|
{ |
||||
|
public abstract class Location |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 地球半径(米)
|
||||
|
/// </summary>
|
||||
|
public const double EARTH_RADIUS = 6378137; |
||||
|
/// <summary>
|
||||
|
/// 纬度
|
||||
|
/// </summary>
|
||||
|
public double Latitude { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 经度
|
||||
|
/// </summary>
|
||||
|
public double Longitude { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 计算两个位置的距离,返回两点的距离,单位 米
|
||||
|
/// 该公式为GOOGLE提供,误差小于0.2米
|
||||
|
/// </summary>
|
||||
|
/// <param name="location">参与计算的位置信息</param>
|
||||
|
/// <returns>返回两个位置的距离</returns>
|
||||
|
public double CalcDistance(Location location) |
||||
|
{ |
||||
|
return CalcDistance(this, location); |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// 计算两个位置的距离,返回两点的距离,单位 米
|
||||
|
/// 该公式为GOOGLE提供,误差小于0.2米
|
||||
|
/// </summary>
|
||||
|
/// <param name="location1">参与计算的位置信息</param>
|
||||
|
/// <param name="location2">参与计算的位置信息</param>
|
||||
|
/// <returns>返回两个位置的距离</returns>
|
||||
|
public static double CalcDistance(Location location1, Location location2) |
||||
|
{ |
||||
|
double radLat1 = Rad(location1.Latitude); |
||||
|
double radLng1 = Rad(location1.Longitude); |
||||
|
double radLat2 = Rad(location2.Latitude); |
||||
|
double radLng2 = Rad(location2.Longitude); |
||||
|
double a = radLat1 - radLat2; |
||||
|
double b = radLng1 - radLng2; |
||||
|
double result = 2 * Math.Asin(Math.Sqrt(Math.Pow(Math.Sin(a / 2), 2) + Math.Cos(radLat1) * Math.Cos(radLat2) * Math.Pow(Math.Sin(b / 2), 2))); |
||||
|
result *= EARTH_RADIUS; |
||||
|
return result; |
||||
|
} |
||||
|
/// <summary>
|
||||
|
/// 计算位置的偏移距离
|
||||
|
/// </summary>
|
||||
|
/// <param name="location">参与计算的位置</param>
|
||||
|
/// <param name="distance">位置偏移量,单位 米</param>
|
||||
|
/// <returns></returns>
|
||||
|
public static Position CalcOffsetDistance(Location location, double distance) |
||||
|
{ |
||||
|
double dlng = 2 * Math.Asin(Math.Sin(distance / (2 * EARTH_RADIUS)) / Math.Cos(Rad(location.Latitude))); |
||||
|
dlng = Deg(dlng); |
||||
|
double dlat = distance / EARTH_RADIUS; |
||||
|
dlat = Deg(dlat); |
||||
|
double leftTopLat = location.Latitude + dlat; |
||||
|
double leftTopLng = location.Longitude - dlng; |
||||
|
|
||||
|
double leftBottomLat = location.Latitude - dlat; |
||||
|
double leftBottomLng = location.Longitude - dlng; |
||||
|
|
||||
|
double rightTopLat = location.Latitude + dlat; |
||||
|
double rightTopLng = location.Longitude + dlng; |
||||
|
|
||||
|
double rightBottomLat = location.Latitude - dlat; |
||||
|
double rightBottomLng = location.Longitude + dlng; |
||||
|
|
||||
|
return new Position(leftTopLat, leftBottomLat, leftTopLng, leftBottomLng, |
||||
|
rightTopLat, rightBottomLat, rightTopLng, rightBottomLng); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 角度转换为弧度
|
||||
|
/// </summary>
|
||||
|
/// <param name="d"></param>
|
||||
|
/// <returns></returns>
|
||||
|
public static double Rad(double d) |
||||
|
{ |
||||
|
return d * Math.PI / 180d; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 弧度转换为角度
|
||||
|
/// </summary>
|
||||
|
/// <param name="d"></param>
|
||||
|
/// <returns></returns>
|
||||
|
public static double Deg(double d) |
||||
|
{ |
||||
|
return d * (180 / Math.PI); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
namespace LINGYUN.Abp.Location |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 位置量
|
||||
|
/// </summary>
|
||||
|
public class Position |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 左上纬度
|
||||
|
/// </summary>
|
||||
|
public double LeftTopLatitude { get; } |
||||
|
/// <summary>
|
||||
|
/// 左上经度
|
||||
|
/// </summary>
|
||||
|
public double LeftTopLongitude { get; } |
||||
|
/// <summary>
|
||||
|
/// 左下纬度
|
||||
|
/// </summary>
|
||||
|
public double LeftBottomLatitude { get; } |
||||
|
/// <summary>
|
||||
|
/// 左下经度
|
||||
|
/// </summary>
|
||||
|
public double LeftBottomLongitude { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 右上纬度
|
||||
|
/// </summary>
|
||||
|
public double RightTopLatitude { get; } |
||||
|
/// <summary>
|
||||
|
/// 右上经度
|
||||
|
/// </summary>
|
||||
|
public double RightTopLongitude { get; } |
||||
|
/// <summary>
|
||||
|
/// 右下纬度
|
||||
|
/// </summary>
|
||||
|
public double RightBottomLatitude { get; } |
||||
|
/// <summary>
|
||||
|
/// 右下经度
|
||||
|
/// </summary>
|
||||
|
public double RightBottomLongitude { get; } |
||||
|
|
||||
|
internal Position(double leftTopLat, double leftBottomLat, double leftTopLng, double leftBottomLng, |
||||
|
double rightTopLat, double rightBottomLat, double rightTopLng, double rightBottomLng) |
||||
|
{ |
||||
|
LeftTopLatitude = leftTopLat; |
||||
|
LeftBottomLatitude = leftBottomLat; |
||||
|
LeftTopLongitude = leftTopLng; |
||||
|
LeftBottomLongitude = leftBottomLng; |
||||
|
RightTopLatitude = rightTopLat; |
||||
|
RightTopLongitude = rightTopLng; |
||||
|
RightBottomLatitude = rightBottomLat; |
||||
|
RightBottomLongitude = rightBottomLng; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
namespace LINGYUN.Abp.Location |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 正地址
|
||||
|
/// </summary>
|
||||
|
public class PositiveLocation : Location |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 附加信息
|
||||
|
/// </summary>
|
||||
|
public int Precise { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 绝对精度
|
||||
|
/// </summary>
|
||||
|
public int Confidence { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 理解程度
|
||||
|
/// 分值范围0-100
|
||||
|
/// 分值越大,服务对地址理解程度越高
|
||||
|
/// </summary>
|
||||
|
public int Pomprehension { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 能精确理解的地址类型
|
||||
|
/// </summary>
|
||||
|
public string Level { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<el-row justify="center"> |
||||
|
<el-col :span="4"> |
||||
|
<label>{{ $t('AbpUiMultiTenancy.Tenant') }}</label> |
||||
|
</el-col> |
||||
|
<el-col :span="16"> |
||||
|
<label>{{ value }}</label> |
||||
|
</el-col> |
||||
|
<el-col :span="4"> |
||||
|
<el-link |
||||
|
type="info" |
||||
|
@click="handleSwitchTenant" |
||||
|
> |
||||
|
{{ $t('AbpUiMultiTenancy.SwitchTenant') }} |
||||
|
</el-link> |
||||
|
</el-col> |
||||
|
</el-row> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Prop, Vue } from 'vue-property-decorator' |
||||
|
import TenantService from '@/api/tenant' |
||||
|
import { setTenant, removeTenant } from '@/utils/sessions' |
||||
|
|
||||
|
@Component({ |
||||
|
name: 'TenantSelect' |
||||
|
}) |
||||
|
export default class extends Vue { |
||||
|
@Prop({ default: '' }) |
||||
|
private value?: string |
||||
|
|
||||
|
private handleSwitchTenant() { |
||||
|
this.$prompt(this.$t('AbpUiMultiTenancy.SwitchTenantHint').toString(), |
||||
|
this.$t('AbpUiMultiTenancy.SwitchTenant').toString(), { |
||||
|
showInput: true |
||||
|
}).then((val: any) => { |
||||
|
removeTenant() |
||||
|
if (val.value) { |
||||
|
TenantService.getTenantByName(val.value).then(tenant => { |
||||
|
if (tenant.success) { |
||||
|
setTenant(tenant.tenantId) |
||||
|
this.$emit('input', tenant.name) |
||||
|
} else { |
||||
|
this.$message.warning(this.$t('login.tenantIsNotAvailable', { name: val.value }).toString()) |
||||
|
} |
||||
|
}) |
||||
|
} else { |
||||
|
this.$emit('input', '') |
||||
|
} |
||||
|
}).catch(() => { |
||||
|
console.log() |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
Loading…
Reference in new issue