!1754 feat(#I4720Q): add location services

* chore: bump version to 5.7.4
* doc: 增加自定义扩展文档
* doc: 增加 Locator 菜单
* refactor: 调整 IPLocatorOption 内部参数
* refactor:  移除 IPLocator 服务注入更改到 BootstrapBlazor
* refactor: 日志增加 IP 地址
* chore: 移动文件
* doc: 增加编码注意事项
* feat: 增加本地连接解析
* feat: 根据最新百度接口更新类结构
* chore: 增加编码支持
* doc: 增加 Locator 示例
* feat: 增加 Locator 配置回调委托
* refactor: 重构代码消除提示信息
* chore: 增加 Http 包依赖
* feat: 增加接口实现类
* feat: 增加 IPLocator 服务注入
* feat: 增加 AddIPLocator 服务注入代码
* feat: 增加 StringExtensions 扩展方法
* feat: 增加 IPLocator 相关接口定义
* refactor: 更改日志 Key 值
This commit is contained in:
Argo 2021-08-24 10:12:14 +00:00
parent 2cfd040561
commit 2b7112653c
19 changed files with 495 additions and 18 deletions

View File

@ -3,18 +3,10 @@
// Website: https://www.blazor.zone or https://argozhang.github.io/
using BootstrapBlazor.Components;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
namespace BootstrapBlazor.Server
{
@ -52,7 +44,8 @@ namespace BootstrapBlazor.Server
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(builder => builder.AddConsole());
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
services.AddCors();
services.AddResponseCompression();

View File

@ -379,6 +379,7 @@
"GroupBox": "GroupBox",
"HandwrittenPage": "HandwrittenPage",
"ListView": "ListView",
"Locator": "Locator",
"Popover": "Popover",
"QRCode": "QRCode",
"Search": "Search",

View File

@ -379,6 +379,7 @@
"GroupBox": "集合 GroupBox",
"HandwrittenPage": "手写组件 HandwrittenPage",
"ListView": "列表组件 ListView",
"Locator": "位置定位 Locator",
"Popover": "弹出窗 Popover",
"QRCode": "二维码 QRCode",
"Search": "搜索框 Search",

View File

@ -14,9 +14,6 @@ namespace BootstrapBlazor.Shared.Pages
/// </summary>
public partial class Client
{
/// <summary>
///
/// </summary>
[Inject]
[NotNull]
private WebClientService? ClientService { get; set; }

View File

@ -0,0 +1,58 @@
@page "/locator"
<h3>获取 IP 地理位置</h3>
<h4>多用系统日志跟踪</h4>
<Block Title="普通用法" Introduction="注入服务显示客户端地理位置信息">
<p><b>用法介绍</b></p>
<div class="mb-3">
<p>组件中使用注入服务 <code>IIPLocatorProvider</code> 调用 <code>Locate</code> 方法</p>
<Pre>[Inject]
[NotNull]
private IIPLocatorProvider? IPLocator { get; set; }
</Pre>
</div>
<Tips>
<div>由于地理位置查询接口返回字符集可能是其他字符集如 <code>gbk</code>,程序会报错;</div>
<div><b>解决办法:</b></div>
<div><code>Startup</code> 文件中 <code>ConfigureServices</code> 方法内增加下面这句话即可解决</div>
</Tips>
<Pre>Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)</Pre>
<div class="mb-2">IP 测试数据</div>
<div><code>112.224.74.239</code> 山东省济南市 联通</div>
<div class="mb-3"><code>183.160.236.53</code> 安徽省合肥市 电信</div>
<div class="row g-3 form-inline">
<div class="col-12 col-sm-6">
<BootstrapInput @bind-Value="Ip" DisplayText="Ip 地址" ShowLabel="true" />
</div>
<div class="col-12 col-sm-6">
<Display Value="Location" DisplayText="地理位置" ShowLabel="true" />
</div>
<div class="col-12">
<Button Icon="fa fa-location" Text="定位" OnClick="OnClick" />
</div>
</div>
<p class="mt-3"><b>扩展自定义地理位置查询接口</b></p>
<div>
<p><b>1. 实现自定义定位器</b></p>
<Pre>private class CustomerLocator : IIPLocator
{
public Task&lt;string&gt; Locate(IPLocatorOption option)
{
throw new NotImplementedException();
}
}</Pre>
<p><b>2. 配置自定义定位器</b></p>
<Pre>public void ConfigureServices(IServiceCollection services)
{
services.AddBootstrapBlazor(locatorAction: option =>
{
option.LocatorName = "CustomerLocator";
option.LocatorFactory = name => new CustomerLocator();
option.Url = "http://apis.juhe.cn/ip/ipNew?key=f57102d1b9fadd3f4a1c29072d0c0206&ip=";
});
}</Pre>
<div>通过 <code>AddBootstrapBlazor</code> 方法的回调委托参数进行自定义定位器 <code>CustomerLocator</code> 配置</div>
</div>
</Block>

View File

@ -0,0 +1,54 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using BootstrapBlazor.Components;
using Microsoft.AspNetCore.Components;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
namespace BootstrapBlazor.Shared.Pages
{
/// <summary>
///
/// </summary>
public partial class Locator
{
[Inject]
[NotNull]
private WebClientService? ClientService { get; set; }
[Inject]
[NotNull]
private IIPLocatorProvider? IPLocator { get; set; }
private string? Ip { get; set; }
private string? Location { get; set; }
/// <summary>
///
/// </summary>
/// <param name="firstRender"></param>
/// <returns></returns>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await ClientService.RetrieveRemoteInfo();
Ip = ClientService.Ip;
StateHasChanged();
}
}
private async Task OnClick()
{
if (!string.IsNullOrEmpty(Ip))
{
Location = await IPLocator.Locate(Ip);
}
}
}
}

View File

@ -435,6 +435,12 @@ namespace BootstrapBlazor.Shared.Shared
Url = "listviews"
},
new()
{
IsNew = true,
Text = Localizer["Locator"],
Url = "locator"
},
new()
{
Text = Localizer["QRCode"],
Url = "qrcodes"

View File

@ -1,13 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<Version>5.7.3</Version>
<Version>5.7.4</Version>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="5.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="5.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="5.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="5.0" />
</ItemGroup>
@ -16,6 +17,7 @@
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="6.0.0-preview.7.*" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.0-preview.7.*" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0-preview.7.*" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-preview.7.*" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="6.0.0-preview.7.*" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0-preview.7.*" />
</ItemGroup>

View File

@ -0,0 +1,53 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BootstrapBlazor.Components
{
/// <summary>
///
/// </summary>
internal class BaiDuIPLocator : DefaultIPLocator
{
/// <summary>
/// 详细地址信息
/// </summary>
public IEnumerable<LocationInfo>? Data { get; set; }
/// <summary>
/// 结果状态返回码
/// </summary>
public string? Status { get; set; }
/// <summary>
///
/// </summary>
/// <param name="option"></param>
/// <returns></returns>
public override Task<string> Locate(IPLocatorOption option) => Locate<BaiDuIPLocator>(option);
/// <summary>
///
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Status == "0" ? (Data?.FirstOrDefault()?.Location ?? "XX XX") : "Error";
}
}
/// <summary>
///
/// </summary>
public class LocationInfo
{
/// <summary>
///
/// </summary>
public string? Location { get; set; }
}
}

View File

@ -0,0 +1,50 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using Microsoft.Extensions.Logging;
using System;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
namespace BootstrapBlazor.Components
{
/// <summary>
///
/// </summary>
internal class DefaultIPLocator : IIPLocator
{
/// <summary>
///
/// </summary>
/// <param name="option"></param>
/// <returns></returns>
public virtual Task<string> Locate(IPLocatorOption option) => Task.FromResult(string.Empty);
/// <summary>
///
/// </summary>
/// <param name="option"></param>
/// <returns></returns>
protected static async Task<string> Locate<T>(IPLocatorOption option) where T : class
{
string? ret = null;
try
{
if (!string.IsNullOrEmpty(option.Url) && !string.IsNullOrEmpty(option.IP) && option.HttpClient != null)
{
var url = string.Format(option.Url, option.IP);
using var token = new CancellationTokenSource(option.RequestTimeout);
var result = await option.HttpClient.GetFromJsonAsync<T>(url, token.Token);
ret = result?.ToString();
}
}
catch (Exception ex)
{
option.Logger?.LogError(ex, option.Url, option.IP);
}
return ret ?? string.Empty;
}
}
}

View File

@ -0,0 +1,58 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace BootstrapBlazor.Components
{
/// <summary>
/// 默认 IP 地理位置定位器
/// </summary>
internal class DefaultIPLocatorProvider : IIPLocatorProvider
{
private readonly IPLocatorOption _option = new();
/// <summary>
/// 构造函数
/// </summary>
/// <param name="factory"></param>
/// <param name="logger"></param>
public DefaultIPLocatorProvider(IHttpClientFactory factory, ILogger<DefaultIPLocatorProvider> logger)
{
_option.HttpClient = factory.CreateClient();
_option.Logger = logger;
}
/// <summary>
/// 定位方法
/// </summary>
/// <param name="ip"></param>
/// <returns></returns>
public async Task<string> Locate(string ip)
{
string? ret = null;
// 解析本机地址
if (string.IsNullOrEmpty(ip) || _option.Localhosts.Any(p => p == ip))
{
ret = "本地连接";
}
else
{
// IP定向器地址未设置
_option.IP = ip;
if (string.IsNullOrEmpty(_option.Url))
{
ret = string.Empty;
}
var locator = _option.LocatorFactory(_option.LocatorName);
ret = await locator.Locate(_option);
}
return ret ?? string.Empty;
}
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using System.Threading.Tasks;
namespace BootstrapBlazor.Components
{
/// <summary>
///
/// </summary>
public interface IIPLocator
{
/// <summary>
///
/// </summary>
/// <param name="option">定位器配置信息</param>
/// <returns>定位器定位结果</returns>
Task<string> Locate(IPLocatorOption option);
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using System.Threading.Tasks;
namespace BootstrapBlazor.Components
{
/// <summary>
/// IP 地址定位服务
/// </summary>
public interface IIPLocatorProvider
{
/// <summary>
///
/// </summary>
/// <param name="ip"></param>
/// <returns></returns>
Task<string> Locate(string ip);
}
}

View File

@ -0,0 +1,54 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using Microsoft.Extensions.Logging;
namespace BootstrapBlazor.Components
{
/// <summary>
/// IP定位器配置项
/// </summary>
public class IPLocatorOption
{
/// <summary>
/// 获得/设置 定位器名称
/// </summary>
public string LocatorName { get; set; } = nameof(BaiDuIPLocator);
/// <summary>
/// 获得/设置 定位器创建方法未设置使用内部定位器
/// </summary>
public Func<string, IIPLocator> LocatorFactory { get; set; } = _ => new BaiDuIPLocator();
/// <summary>
/// 获得/设置 IP定位器请求地址
/// </summary>
public string Url { get; set; } = "https://sp0.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?resource_id=6006&query={0}";
/// <summary>
/// 获得/设置 IP地址请求超时时间 默认为 3000 毫秒
/// </summary>
public int RequestTimeout { get; set; } = 3000;
/// <summary>
/// 获得 本机地址列表
/// </summary>
public List<string> Localhosts { get; } = new List<string>(new string[] { "::1", "127.0.0.1" });
/// <summary>
/// 获得/设置 IP地址
/// </summary>
internal string? IP { get; set; }
/// <summary>
/// 获得/设置 HttpClient 实体类
/// </summary>
internal HttpClient? HttpClient { get; set; }
/// <summary>
/// 获得/设置 ILogger 实体类
/// </summary>
internal ILogger<IIPLocatorProvider>? Logger { get; set; }
}
}

View File

@ -88,7 +88,7 @@ namespace BootstrapBlazor.Components
logger.AppendLine();
}
logger.AppendFormat("{0}: {1}", "Exception:", exception.Message);
logger.AppendFormat("{0}: {1}", "Exception", exception.Message);
logger.AppendLine();
logger.Append(new string('*', 45));

View File

@ -24,12 +24,13 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary>
/// <param name="services"></param>
/// <param name="configureOptions"></param>
/// <param name="setupAction"></param>
/// <param name="localizationAction"></param>
/// <param name="locatorAction"></param>
/// <returns></returns>
public static IServiceCollection AddBootstrapBlazor(this IServiceCollection services, Action<BootstrapBlazorOptions>? configureOptions = null, Action<JsonLocalizationOptions>? setupAction = null)
public static IServiceCollection AddBootstrapBlazor(this IServiceCollection services, Action<BootstrapBlazorOptions>? configureOptions = null, Action<JsonLocalizationOptions>? localizationAction = null, Action<IPLocatorOption>? locatorAction = null)
{
services.AddAuthorizationCore();
services.AddJsonLocalization(setupAction);
services.AddJsonLocalization(localizationAction);
services.TryAddScoped<IComponentIdGenerator, DefaultIdGenerator>();
services.TryAddScoped<ITableExcelExport, DefaultExcelExport>();
services.TryAddScoped(typeof(IDataService<>), typeof(NullDataService<>));
@ -47,6 +48,13 @@ namespace Microsoft.Extensions.DependencyInjection
{
configureOptions?.Invoke(options);
});
services.AddHttpClient();
services.TryAddSingleton<IIPLocatorProvider, DefaultIPLocatorProvider>();
services.Configure<IPLocatorOption>(options =>
{
locatorAction?.Invoke(options);
});
return services;
}

View File

@ -0,0 +1,100 @@
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Website: https://www.blazor.zone or https://argozhang.github.io/
using System;
using System.Collections.Generic;
namespace BootstrapBlazor.Components
{
/// <summary>
///
/// </summary>
internal static class StringExtensions
{
/// <summary>
/// SpanSplit 扩展方法
/// </summary>
/// <param name="source">源数组</param>
/// <param name="splitStr">分隔符数组 分割规则作为整体</param>
/// <param name="stringSplitOptions">StringSplitOptions 选项</param>
/// <returns>分割后的字符串数组</returns>
public static List<string> SpanSplit(this string source, string? splitStr = null, StringSplitOptions stringSplitOptions = StringSplitOptions.None)
{
var ret = new List<string>();
if (string.IsNullOrEmpty(source))
{
return ret;
}
if (string.IsNullOrEmpty(splitStr))
{
splitStr = Environment.NewLine;
}
var sourceSpan = source.AsSpan();
var splitSpan = splitStr.AsSpan();
do
{
var n = sourceSpan.IndexOf(splitSpan);
if (n == -1)
{
n = sourceSpan.Length;
}
ret.Add(stringSplitOptions == StringSplitOptions.None
? sourceSpan.Slice(0, n).ToString()
: sourceSpan.Slice(0, n).Trim().ToString());
sourceSpan = sourceSpan[Math.Min(sourceSpan.Length, n + splitSpan.Length)..];
}
while (sourceSpan.Length > 0);
return ret;
}
/// <summary>
/// SpanSplit 扩展方法
/// </summary>
/// <param name="source">源数组</param>
/// <param name="splitStr">分隔符数组 分割规则是任意一个</param>
/// <param name="stringSplitOptions">StringSplitOptions 选项</param>
/// <returns>分割后的字符串数组</returns>
public static List<string> SpanSplitAny(this string source, string splitStr, StringSplitOptions stringSplitOptions = StringSplitOptions.None)
{
var ret = new List<string>();
if (string.IsNullOrEmpty(source))
{
return ret;
}
if (string.IsNullOrEmpty(splitStr))
{
ret.Add(source);
return ret;
}
var sourceSpan = source.AsSpan();
var splitSpan = splitStr.AsSpan();
do
{
var n = sourceSpan.IndexOfAny(splitSpan);
if (n == -1)
{
n = sourceSpan.Length;
}
if (n > 0)
{
ret.Add(stringSplitOptions == StringSplitOptions.None
? sourceSpan.Slice(0, n).ToString()
: sourceSpan.Slice(0, n).Trim().ToString());
}
sourceSpan = sourceSpan[Math.Min(sourceSpan.Length, n + 1)..];
}
while (sourceSpan.Length > 0);
return ret;
}
}
}