LqTencentLocationService.cs 7.77 KB
using System;
using System.Diagnostics;
using System.Globalization;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using NCC.Dependency;
using NCC.DynamicApiController;
using NCC.Extend.Entitys.Dto.TencentLocation;
using NCC.FriendlyException;
using Newtonsoft.Json.Linq;

namespace NCC.Extend
{
    /// <summary>
    /// 腾讯位置服务(逆地址解析)
    /// </summary>
    [ApiDescriptionSettings(Tag = "Extend", Name = "LqTencentLocation", Order = 210)]
    [Route("api/Extend/[controller]")]
    [ApiController]
    public class LqTencentLocationService : IDynamicApiController, ITransient
    {
        private readonly IConfiguration _configuration;
        private readonly IHttpClientFactory _httpClientFactory;
        private readonly ILogger<LqTencentLocationService> _logger;

        /// <summary>
        /// 初始化腾讯位置服务
        /// </summary>
        /// <param name="configuration">配置</param>
        /// <param name="httpClientFactory">HttpClient 工厂</param>
        /// <param name="logger">日志</param>
        public LqTencentLocationService(
            IConfiguration configuration,
            IHttpClientFactory httpClientFactory,
            ILogger<LqTencentLocationService> logger)
        {
            _configuration = configuration;
            _httpClientFactory = httpClientFactory;
            _logger = logger;
        }

        /// <summary>
        /// 腾讯位置服务逆地址解析(经纬度 → 文字地址)
        /// </summary>
        /// <remarks>
        /// 说明:
        /// - 后端从配置读取腾讯位置服务 Key,不下发到前端
        /// - 调用腾讯接口:/ws/geocoder/v1/(逆地址解析)
        ///
        /// 示例请求:
        /// ```json
        /// {
        ///   "latitude": 30.572269,
        ///   "longitude": 104.066541
        /// }
        /// ```
        ///
        /// 参数说明:
        /// - latitude: 纬度(-90~90)
        /// - longitude: 经度(-180~180)
        /// </remarks>
        /// <param name="latitude">纬度</param>
        /// <param name="longitude">经度</param>
        /// <returns>原始坐标与解析后的地址</returns>
        /// <response code="200">解析成功</response>
        /// <response code="400">参数错误 / Key 缺失 / 腾讯接口业务错误</response>
        /// <response code="500">服务器内部错误</response>
        [HttpGet("ReverseGeocode")]
        public async Task<TencentReverseGeocodeOutput> ReverseGeocode(
            [FromQuery] decimal? latitude,
            [FromQuery] decimal? longitude)
        {
            if (!latitude.HasValue || !longitude.HasValue)
                throw NCCException.Oh("latitude/longitude 不能为空");

            if (latitude.Value < -90 || latitude.Value > 90)
                throw NCCException.Oh("latitude 超出范围(-90~90)");
            if (longitude.Value < -180 || longitude.Value > 180)
                throw NCCException.Oh("longitude 超出范围(-180~180)");

            var key = _configuration["NCC_App:TencentLbs:Key"]
                      ?? _configuration["NCC_APP:TencentLbs:Key"];
            if (string.IsNullOrWhiteSpace(key))
                throw NCCException.Oh("腾讯位置服务 Key 未配置,请设置配置项 NCC_App:TencentLbs:Key(或环境变量映射)");

            var timeoutSecondsRaw = _configuration["NCC_App:TencentLbs:TimeoutSeconds"]
                                    ?? _configuration["NCC_APP:TencentLbs:TimeoutSeconds"];
            var timeoutSeconds = 5;
            if (!string.IsNullOrWhiteSpace(timeoutSecondsRaw) && int.TryParse(timeoutSecondsRaw, out var ts) && ts > 0)
                timeoutSeconds = ts;

            var latStr = latitude.Value.ToString(CultureInfo.InvariantCulture);
            var lngStr = longitude.Value.ToString(CultureInfo.InvariantCulture);

            var url = $"https://apis.map.qq.com/ws/geocoder/v1/?location={latStr},{lngStr}&key={Uri.EscapeDataString(key)}&get_poi=0";

            var sw = Stopwatch.StartNew();
            try
            {
                using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds));
                var client = _httpClientFactory.CreateClient();

                using var req = new HttpRequestMessage(HttpMethod.Get, url);
                using var resp = await client.SendAsync(req, HttpCompletionOption.ResponseContentRead, cts.Token);
                var body = await resp.Content.ReadAsStringAsync(cts.Token);

                if (!resp.IsSuccessStatusCode)
                {
                    _logger.LogWarning(
                        "腾讯逆地址解析HTTP失败 status={Status} lat={Lat} lng={Lng} costMs={CostMs} body={Body}",
                        (int)resp.StatusCode, latStr, lngStr, sw.ElapsedMilliseconds, Truncate(body, 1200));
                    throw NCCException.Oh($"腾讯逆地址解析 HTTP 请求失败:{(int)resp.StatusCode}");
                }

                var json = JObject.Parse(body);
                var status = json["status"]?.Value<int?>() ?? -1;
                var message = json["message"]?.ToString() ?? "";

                if (status != 0)
                {
                    _logger.LogWarning(
                        "腾讯逆地址解析业务失败 status={Status} msg={Msg} lat={Lat} lng={Lng} costMs={CostMs} body={Body}",
                        status, message, latStr, lngStr, sw.ElapsedMilliseconds, Truncate(body, 1200));
                    throw NCCException.Oh($"腾讯逆地址解析失败:{(message.Length > 0 ? message : "未知错误")}");
                }

                var address = json["result"]?["address"]?.ToString();
                if (string.IsNullOrWhiteSpace(address))
                {
                    _logger.LogWarning(
                        "腾讯逆地址解析返回无address lat={Lat} lng={Lng} costMs={CostMs} body={Body}",
                        latStr, lngStr, sw.ElapsedMilliseconds, Truncate(body, 1200));
                    throw NCCException.Oh("腾讯逆地址解析返回地址为空");
                }

                _logger.LogInformation(
                    "腾讯逆地址解析成功 lat={Lat} lng={Lng} costMs={CostMs}",
                    latStr, lngStr, sw.ElapsedMilliseconds);

                return new TencentReverseGeocodeOutput
                {
                    location = new TencentLocationDto
                    {
                        latitude = latitude.Value,
                        longitude = longitude.Value
                    },
                    address = address
                };
            }
            catch (OperationCanceledException ex)
            {
                _logger.LogWarning(
                    ex,
                    "腾讯逆地址解析超时 lat={Lat} lng={Lng} costMs={CostMs} timeoutSeconds={TimeoutSeconds}",
                    latStr, lngStr, sw.ElapsedMilliseconds, timeoutSeconds);
                throw NCCException.Oh($"腾讯逆地址解析超时({timeoutSeconds}s)");
            }
            catch (AppFriendlyException)
            {
                throw;
            }
            catch (Exception ex)
            {
                _logger.LogError(
                    ex,
                    "腾讯逆地址解析异常 lat={Lat} lng={Lng} costMs={CostMs}",
                    latStr, lngStr, sw.ElapsedMilliseconds);
                throw NCCException.Oh($"腾讯逆地址解析异常:{ex.Message}");
            }
        }

        private static string Truncate(string text, int maxLen)
        {
            if (string.IsNullOrEmpty(text)) return text;
            return text.Length <= maxLen ? text : text.Substring(0, maxLen) + "...";
        }
    }
}