XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

EARSystem

【ASP】耳间,网页端的情绪智能推荐系统

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Globalization;
using System.Text.Json;
using EARSystem.Core;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Options;

namespace EARSystem.Infrastructure;

public class AMapOptions
{
    public string WebServiceKey { get; set; } = "";
    public string BrowserKey { get; set; } = "";
    public string JsSecurityCode { get; set; } = "";
    public bool AllowExternalReferences { get; set; }
}
public class MapServiceException(string message) : Exception(message);

public class AMapClient(HttpClient http, IOptions<AMapOptions> options)
{
    private AMapOptions Settings => options.Value;
    public bool Available => !string.IsNullOrWhiteSpace(Settings.WebServiceKey);
    private async Task<JsonDocument> GetAsync(string path, Dictionary<string, string?> query, CancellationToken ct)
    {
        if (!Available) throw new MapServiceException("尚未配置高德 Web 服务密钥,请选择演示模式。");
        query["key"] = Settings.WebServiceKey;
        try {
            using var response = await http.GetAsync(QueryHelpers.AddQueryString("https://restapi.amap.com/" + path, query), ct);
            if (!response.IsSuccessStatusCode) throw new MapServiceException("地图服务暂不可用,请稍后重试或切换演示模式。");
            var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync(ct));
            if (Text(document.RootElement, "status") != "1") {
                document.Dispose();
                throw new MapServiceException("地图接口调用失败,请检查密钥权限或配额;可以切换演示模式。");
            }
            return document;
        } catch (HttpRequestException) { throw new MapServiceException("无法连接地图服务,请稍后重试或切换演示模式。"); }
        catch (JsonException) { throw new MapServiceException("地图服务返回格式异常,请稍后重试。"); }
    }

    public async Task<Coordinate> ConvertAsync(Coordinate coordinate, CancellationToken ct)
    {
        if (coordinate.System == "GCJ02") return coordinate;
        using var doc = await GetAsync("v3/assistant/coordinate/convert", new() {
            ["locations"] = $"{coordinate.Longitude.ToString("F6", CultureInfo.InvariantCulture)},{coordinate.Latitude.ToString("F6", CultureInfo.InvariantCulture)}",
            ["coordsys"] = "gps" }, ct);
        var parts = Text(doc.RootElement, "locations").Split(',');
        if (parts.Length != 2 || !double.TryParse(parts[0], CultureInfo.InvariantCulture, out var lng)
            || !double.TryParse(parts[1], CultureInfo.InvariantCulture, out var lat)
            || !double.IsFinite(lng) || !double.IsFinite(lat) || Math.Abs(lng)>180 || Math.Abs(lat)>90)
            throw new MapServiceException("坐标转换失败,请手动选点。");
        return new(lng, lat);
    }

    public async Task<PlaceCandidate[]> SearchAsync(RecommendationQuery query, CancellationToken ct)
    {
        var categories = query.Categories.Length > 0 ? query.Categories : Enum.GetValues<PlaceCategory>();
        var results = await Task.WhenAll(categories.Distinct().Select(async category => {
            using var doc = await GetAsync("v5/place/around", new() {
                ["location"] = $"{query.Location.Longitude.ToString("F6", CultureInfo.InvariantCulture)},{query.Location.Latitude.ToString("F6", CultureInfo.InvariantCulture)}",
                ["radius"] = query.RadiusMeters.ToString(CultureInfo.InvariantCulture), ["keywords"] = Labels.Category(category),
                ["page_size"] = "20", ["page_num"] = "1", ["show_fields"] = "business" }, ct);
            if (!doc.RootElement.TryGetProperty("pois", out var pois) || pois.ValueKind != JsonValueKind.Array) return Array.Empty<PlaceCandidate>();
            return pois.EnumerateArray().Select(p => {
                var point = Text(p, "location").Split(',');
                if (point.Length != 2 || !double.TryParse(point[0], CultureInfo.InvariantCulture, out var lng)
                    || !double.TryParse(point[1], CultureInfo.InvariantCulture, out var lat)) return null;
                double? rating = null;
                decimal? cost = null;
                if (p.TryGetProperty("business", out var business) && business.ValueKind == JsonValueKind.Object) {
                    if (double.TryParse(Text(business, "rating"), CultureInfo.InvariantCulture, out var r) && r is >= 1 and <= 5) rating = r;
                    if (decimal.TryParse(Text(business, "cost"), CultureInfo.InvariantCulture, out var money) && money >= 0) cost = money;
                }
                var id = Text(p, "id");
                if (string.IsNullOrWhiteSpace(id) || !double.IsFinite(lng) || !double.IsFinite(lat) || Math.Abs(lng) > 180 || Math.Abs(lat) > 90) return null;
                return new PlaceCandidate("amap:" + id, null, Text(p, "name"), Text(p, "address"), Category(p, category),
                    lng, lat, cost, rating, null, null, 0, "Online", "类型推测");
            }).OfType<PlaceCandidate>().ToArray();
        }));
        return results.SelectMany(x => x).DistinctBy(x => x.Id)
            .OrderBy(x => RecommendationScorer.Distance(query.Location, x.Longitude, x.Latitude)).Take(100).ToArray();
    }

    public async Task<object[]> GeocodeAsync(string address, CancellationToken ct)
    {
        using var doc = await GetAsync("v3/geocode/geo", new() { ["address"] = address }, ct);
        if (!doc.RootElement.TryGetProperty("geocodes", out var values)) return [];
        return values.EnumerateArray().Take(5).Select(x => (object)new { name = Text(x, "formatted_address"),
            location = Text(x, "location"), system = "GCJ02" }).ToArray();
    }

    public static string Text(JsonElement value, string key) => value.ValueKind == JsonValueKind.Object
        && value.TryGetProperty(key, out var property) && property.ValueKind is JsonValueKind.String or JsonValueKind.Number
            ? property.ToString() : "";

    // Prefer the provider's declared POI type; only fall back to the retrieval category when it is missing.
    private static PlaceCategory Category(JsonElement poi, PlaceCategory fallback) {
        var type = Text(poi, "type");
        if (type.Contains("咖啡")) return PlaceCategory.Cafe;
        if (type.Contains("公园")) return PlaceCategory.Park;
        if (type.Contains("书店") || type.Contains("图书")) return PlaceCategory.Bookstore;
        if (new[] { "博物", "美术", "展览", "展馆" }.Any(type.Contains)) return PlaceCategory.Gallery;
        if (new[] { "娱乐", "休闲", "影院" }.Any(type.Contains)) return PlaceCategory.Entertainment;
        if (type.Contains("餐饮") || type.Contains("餐厅")) return PlaceCategory.Dining;
        return fallback;
    }
}