using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using DouyinApi.Common.Option; using DouyinApi.IServices; using DouyinApi.Model.Naming; using DouyinApi.Services.Naming; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace DouyinApi.Services; public class NamingService : INamingService { private static readonly Regex SurnamePattern = new(@"^[\u4e00-\u9fa5]{1,2}$", RegexOptions.Compiled); private readonly IHttpClientFactory _httpClientFactory; private readonly DeepSeekOptions _deepSeekOptions; private readonly ILogger _logger; private readonly BaziCalculator _baziCalculator = new(); private readonly FallbackNameGenerator _fallbackGenerator = new(); public NamingService( IHttpClientFactory httpClientFactory, IOptions deepSeekOptions, ILogger logger) { _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); _deepSeekOptions = deepSeekOptions?.Value ?? new DeepSeekOptions(); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public Task ValidateSurnameAsync(string surname) { if (string.IsNullOrWhiteSpace(surname)) { return Task.FromResult(false); } return Task.FromResult(SurnamePattern.IsMatch(surname.Trim())); } public async Task GenerateNamesAsync(NamingRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (!await ValidateSurnameAsync(request.Surname).ConfigureAwait(false)) { throw new ArgumentException("invalid_surname"); } var birthDateTime = ComposeBirthDateTime(request); var baziProfile = _baziCalculator.Calculate(birthDateTime); var analysis = BuildAnalysis(request, baziProfile); var suggestions = await TryGenerateViaDeepSeekAsync(request, baziProfile).ConfigureAwait(false); if (!suggestions.Any()) { _logger.LogWarning("DeepSeek returned no suggestions, falling back to local generator. Request={@Request}", new { request.Surname, request.Gender, request.NameLength, request.BirthDate, request.BirthTime }); suggestions = _fallbackGenerator.Generate(request, baziProfile); if (!suggestions.Any()) { _logger.LogError("Fallback generator also returned empty results. Request={@Request}", new { request.Surname, request.Gender, request.NameLength, request.BirthDate, request.BirthTime }); } } else { _logger.LogInformation("DeepSeek generated {Count} suggestions for {Surname}", suggestions.Count, request.Surname); } return new NamingResponse { Analysis = analysis, Results = suggestions }; } private static DateTime ComposeBirthDateTime(NamingRequest request) { var dateFormats = new[] { "yyyy-MM-dd", "yyyy/M/d", "yyyy.M.d" }; if (!DateTime.TryParseExact(request.BirthDate, dateFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, out var datePart)) { datePart = DateTime.Today; } if (!string.IsNullOrWhiteSpace(request.BirthTime) && TimeSpan.TryParse(request.BirthTime, CultureInfo.InvariantCulture, out var timePart)) { return datePart.Date.Add(timePart); } return datePart.Date.AddHours(12); } private static NamingAnalysis BuildAnalysis(NamingRequest request, BaziProfile profile) { var pillars = new List { $"{profile.YearStem}{profile.YearBranch}年", $"{profile.MonthStem}{profile.MonthBranch}月", $"{profile.DayStem}{profile.DayBranch}日", $"{profile.HourStem}{profile.HourBranch}时" }; var distribution = profile.ElementOrder .Select(element => new FiveElementScore { Element = element, Count = profile.ElementCounts[element] }) .ToList(); var summaryBuilder = new StringBuilder(); summaryBuilder.Append("八字:"); summaryBuilder.Append(string.Join("|", pillars)); summaryBuilder.Append("。五行分布:"); summaryBuilder.Append(string.Join("、", distribution.Select(d => $"{d.Element}{d.Count}"))); if (profile.IsBalanced) { summaryBuilder.Append("。五行较为均衡,可结合个人志趣择名。"); } else { var weak = string.Join("、", profile.WeakElements); summaryBuilder.Append("。"); summaryBuilder.Append($"命局中{weak}之气偏弱,宜以对应意象入名调和。"); } return new NamingAnalysis { MatchSummary = summaryBuilder.ToString(), Pillars = pillars, ElementDistribution = distribution }; } private async Task> TryGenerateViaDeepSeekAsync(NamingRequest request, BaziProfile profile) { if (IsDeepSeekConfigInvalid()) { _logger.LogWarning("DeepSeek skipped: configuration missing."); return Array.Empty(); } try { var client = _httpClientFactory.CreateClient(); client.Timeout = TimeSpan.FromSeconds(_deepSeekOptions.TimeoutSeconds > 0 ? _deepSeekOptions.TimeoutSeconds : 15); var httpRequest = BuildDeepSeekRequest(request, profile); httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _deepSeekOptions.ApiKey); httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var response = await client.SendAsync(httpRequest).ConfigureAwait(false); var payload = await response.Content.ReadAsStringAsync().ConfigureAwait(false); if (!response.IsSuccessStatusCode) { _logger.LogWarning("DeepSeek 调用失败,状态码:{StatusCode},返回:{Payload}", (int)response.StatusCode, payload); return Array.Empty(); } _logger.LogInformation("DeepSeek 调用成功,状态码:{StatusCode},响应长度:{Length}", (int)response.StatusCode, payload?.Length ?? 0); return ParseDeepSeekResponse(payload, request); } catch (Exception ex) { _logger.LogWarning(ex, "DeepSeek 调用异常,启用兜底逻辑"); return Array.Empty(); } } private HttpRequestMessage BuildDeepSeekRequest(NamingRequest request, BaziProfile profile) { var endpoint = string.IsNullOrWhiteSpace(_deepSeekOptions.BaseUrl) ? "https://api.deepseek.com/v1/chat/completions" : $"{_deepSeekOptions.BaseUrl.TrimEnd('/')}/chat/completions"; var context = new { surname = request.Surname, gender = request.Gender, nameLength = request.NameLength, birthDate = request.BirthDate, birthTime = request.BirthTime, pillars = new { year = $"{profile.YearStem}{profile.YearBranch}", month = $"{profile.MonthStem}{profile.MonthBranch}", day = $"{profile.DayStem}{profile.DayBranch}", hour = $"{profile.HourStem}{profile.HourBranch}" }, elementDistribution = profile.ElementOrder.ToDictionary( element => element, element => profile.ElementCounts[element]), weakElements = profile.WeakElements, strongElements = profile.StrongElements, isBalanced = profile.IsBalanced }; var contextJson = JsonSerializer.Serialize(context, new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); var lengthDescriptor = string.Equals(request.NameLength, "single", StringComparison.OrdinalIgnoreCase) ? 1 : 2; var nameLength = request.Surname.Length + lengthDescriptor; var genderLabel = string.Equals(request.Gender, "female", StringComparison.OrdinalIgnoreCase) ? "女孩" : "男孩"; var userPrompt = new StringBuilder(); userPrompt.AppendLine("请扮演专业的中华姓名学顾问,根据八字五行提出姓名建议。"); userPrompt.AppendLine($"需基于以下 JSON 数据为姓氏“{request.Surname}”的{genderLabel}提供 5 个{nameLength}字中文名字:"); userPrompt.AppendLine(contextJson); userPrompt.AppendLine("要求:"); userPrompt.AppendLine("1. 每条建议输出对象包含 name、meaning、fiveElementReason 三个字段;"); userPrompt.AppendLine("2. meaning为姓名的含义和出处;"); userPrompt.AppendLine("3. 所有汉字需为生活常用的简体字,单字笔画尽量小于等于 12;"); userPrompt.AppendLine("4. name 必须包含姓氏且满足给定的字数;"); userPrompt.AppendLine("5. fiveElementReason 需指明所补足的五行依据。"); userPrompt.AppendLine("请仅返回 JSON 数据,格式为 {\"suggestions\":[{...}]},不要附加额外说明。"); var requestBody = new { model = _deepSeekOptions.Model, temperature = _deepSeekOptions.Temperature, //max_tokens = _deepSeekOptions.MaxTokens, response_format = new { type = _deepSeekOptions.ResponseFormat }, messages = new[] { new { role = "system", content = "你是一名资深的中文姓名学专家,擅长古诗词,诗经,易经中国传统文化根据八字喜用神给出实用建议。" }, new { role = "user", content = userPrompt.ToString() } } }; var message = new HttpRequestMessage(HttpMethod.Post, endpoint) { Content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json"), }; return message; } private IReadOnlyList ParseDeepSeekResponse(string payload, NamingRequest request) { try { if (string.IsNullOrWhiteSpace(payload)) { _logger.LogWarning("DeepSeek 响应为空字符串"); return Array.Empty(); } using var document = JsonDocument.Parse(payload); var choices = document.RootElement.GetProperty("choices"); if (choices.ValueKind != JsonValueKind.Array || choices.GetArrayLength() == 0) { _logger.LogWarning("DeepSeek 响应缺少 choices 数组,payload={Payload}", payload); return Array.Empty(); } var content = choices[0] .GetProperty("message") .GetProperty("content") .GetString(); if (string.IsNullOrWhiteSpace(content)) { _logger.LogWarning("DeepSeek 响应 message.content 为空"); return Array.Empty(); } using var suggestionsDoc = JsonDocument.Parse(content); if (!suggestionsDoc.RootElement.TryGetProperty("suggestions", out var suggestionsElement) || suggestionsElement.ValueKind != JsonValueKind.Array) { _logger.LogWarning("DeepSeek 返回内容缺少 suggestions 数组,content={Content}", content); return Array.Empty(); } var normalized = new List(); var uniqueNames = new HashSet(StringComparer.Ordinal); foreach (var item in suggestionsElement.EnumerateArray()) { var name = item.GetPropertyOrDefault("name"); var meaning = item.GetPropertyOrDefault("meaning"); var reason = item.GetPropertyOrDefault("fiveElementReason"); if (string.IsNullOrWhiteSpace(name)) { _logger.LogWarning("DeepSeek suggestion缺少 name 字段,item={@Item}", item.ToString()); continue; } name = name.Trim(); if (!name.StartsWith(request.Surname, StringComparison.Ordinal)) { name = $"{request.Surname}{name}"; } if (!ValidateNameLength(name, request)) { _logger.LogWarning( "DeepSeek 建议姓名长度与请求不符,已丢弃。Name={Name}, Expect={Expect}, Request={@Request}", name, request.NameLength, new { request.Surname, request.Gender, request.NameLength }); continue; } if (!uniqueNames.Add(name)) { _logger.LogInformation("DeepSeek 建议出现重复姓名,已跳过。Name={Name}", name); continue; } normalized.Add(new NamingSuggestion { Name = name, Meaning = string.IsNullOrWhiteSpace(meaning) ? "寓意待补充" : meaning.Trim(), ElementReason = string.IsNullOrWhiteSpace(reason) ? "结合八字五行进行补益。" : reason.Trim() }); if (normalized.Count >= 5) { break; } } if (normalized.Count == 0) { _logger.LogWarning("DeepSeek suggestions 解析后为空,原始 content={Content}", content); } return normalized; } catch (Exception ex) { _logger.LogWarning(ex, "解析 DeepSeek 响应失败"); return Array.Empty(); } } private static bool ValidateNameLength(string fullName, NamingRequest request) { if (string.IsNullOrWhiteSpace(fullName) || fullName.Length <= request.Surname.Length) { return false; } var expectedLength = string.Equals(request.NameLength, "single", StringComparison.OrdinalIgnoreCase) ? 1 : 2; var givenName = fullName.Substring(request.Surname.Length); return givenName.Length == expectedLength; } private bool IsDeepSeekConfigInvalid() { return string.IsNullOrWhiteSpace(_deepSeekOptions.ApiKey); } } internal static class JsonElementExtensions { public static string GetPropertyOrDefault(this JsonElement element, string propertyName) { if (element.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String) { return value.GetString() ?? string.Empty; } return string.Empty; } }