Files
Api/DouyinApi.Services/NamingService.cs
2025-11-06 19:23:42 +08:00

406 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<NamingService> _logger;
private readonly BaziCalculator _baziCalculator = new();
private readonly FallbackNameGenerator _fallbackGenerator = new();
public NamingService(
IHttpClientFactory httpClientFactory,
IOptions<DeepSeekOptions> deepSeekOptions,
ILogger<NamingService> logger)
{
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
_deepSeekOptions = deepSeekOptions?.Value ?? new DeepSeekOptions();
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public Task<bool> ValidateSurnameAsync(string surname)
{
if (string.IsNullOrWhiteSpace(surname))
{
return Task.FromResult(false);
}
return Task.FromResult(SurnamePattern.IsMatch(surname.Trim()));
}
public async Task<NamingResponse> 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<string>
{
$"{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<IReadOnlyList<NamingSuggestion>> TryGenerateViaDeepSeekAsync(NamingRequest request, BaziProfile profile)
{
if (IsDeepSeekConfigInvalid())
{
_logger.LogWarning("DeepSeek skipped: configuration missing.");
return Array.Empty<NamingSuggestion>();
}
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<NamingSuggestion>();
}
_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<NamingSuggestion>();
}
}
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<NamingSuggestion> ParseDeepSeekResponse(string payload, NamingRequest request)
{
try
{
if (string.IsNullOrWhiteSpace(payload))
{
_logger.LogWarning("DeepSeek 响应为空字符串");
return Array.Empty<NamingSuggestion>();
}
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<NamingSuggestion>();
}
var content = choices[0]
.GetProperty("message")
.GetProperty("content")
.GetString();
if (string.IsNullOrWhiteSpace(content))
{
_logger.LogWarning("DeepSeek 响应 message.content 为空");
return Array.Empty<NamingSuggestion>();
}
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<NamingSuggestion>();
}
var normalized = new List<NamingSuggestion>();
var uniqueNames = new HashSet<string>(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<NamingSuggestion>();
}
}
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;
}
}