Files
Api/DouyinApi.Services/NamingService.cs

406 lines
16 KiB
C#
Raw Normal View History

2025-11-05 00:22:21 +08:00
using System;
using System.Collections.Generic;
2025-11-05 17:26:45 +08:00
using System.Globalization;
2025-11-05 00:22:21 +08:00
using System.Linq;
2025-11-05 17:26:45 +08:00
using System.Net.Http;
using System.Net.Http.Headers;
2025-11-05 00:22:21 +08:00
using System.Text;
2025-11-05 17:26:45 +08:00
using System.Text.Encodings.Web;
using System.Text.Json;
2025-11-05 00:22:21 +08:00
using System.Text.RegularExpressions;
using System.Threading.Tasks;
2025-11-05 17:26:45 +08:00
using DouyinApi.Common.Option;
2025-11-05 00:22:21 +08:00
using DouyinApi.IServices;
using DouyinApi.Model.Naming;
2025-11-05 17:26:45 +08:00
using DouyinApi.Services.Naming;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
2025-11-05 00:22:21 +08:00
2025-11-05 17:26:45 +08:00
namespace DouyinApi.Services;
public class NamingService : INamingService
2025-11-05 00:22:21 +08:00
{
2025-11-05 17:26:45 +08:00
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)
2025-11-05 00:22:21 +08:00
{
2025-11-05 17:26:45 +08:00
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
_deepSeekOptions = deepSeekOptions?.Value ?? new DeepSeekOptions();
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
2025-11-05 00:22:21 +08:00
2025-11-05 17:26:45 +08:00
public Task<bool> ValidateSurnameAsync(string surname)
{
if (string.IsNullOrWhiteSpace(surname))
2025-11-05 00:22:21 +08:00
{
2025-11-05 17:26:45 +08:00
return Task.FromResult(false);
2025-11-05 00:22:21 +08:00
}
2025-11-05 17:26:45 +08:00
return Task.FromResult(SurnamePattern.IsMatch(surname.Trim()));
}
public async Task<NamingResponse> GenerateNamesAsync(NamingRequest request)
{
if (request == null)
2025-11-05 00:22:21 +08:00
{
2025-11-05 17:26:45 +08:00
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())
{
2025-11-06 19:23:42 +08:00
_logger.LogWarning("DeepSeek returned no suggestions, falling back to local generator. Request={@Request}", new
{
request.Surname,
request.Gender,
request.NameLength,
request.BirthDate,
request.BirthTime
});
2025-11-05 17:26:45 +08:00
suggestions = _fallbackGenerator.Generate(request, baziProfile);
2025-11-06 19:23:42 +08:00
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);
2025-11-05 17:26:45 +08:00
}
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
2025-11-05 00:22:21 +08:00
{
2025-11-05 17:26:45 +08:00
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())
{
2025-11-06 19:23:42 +08:00
_logger.LogWarning("DeepSeek skipped: configuration missing.");
2025-11-05 17:26:45 +08:00
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>();
2025-11-05 00:22:21 +08:00
}
2025-11-06 19:23:42 +08:00
_logger.LogInformation("DeepSeek 调用成功,状态码:{StatusCode},响应长度:{Length}", (int)response.StatusCode, payload?.Length ?? 0);
2025-11-05 17:26:45 +08:00
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
2025-11-05 00:22:21 +08:00
{
2025-11-05 17:26:45 +08:00
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
});
2025-11-06 19:23:42 +08:00
var lengthDescriptor = string.Equals(request.NameLength, "single", StringComparison.OrdinalIgnoreCase) ? 1 : 2;
var nameLength = request.Surname.Length + lengthDescriptor;
2025-11-05 17:26:45 +08:00
var genderLabel = string.Equals(request.Gender, "female", StringComparison.OrdinalIgnoreCase) ? "女孩" : "男孩";
var userPrompt = new StringBuilder();
userPrompt.AppendLine("请扮演专业的中华姓名学顾问,根据八字五行提出姓名建议。");
2025-11-06 19:23:42 +08:00
userPrompt.AppendLine($"需基于以下 JSON 数据为姓氏“{request.Surname}”的{genderLabel}提供 5 个{nameLength}字中文名字:");
2025-11-05 17:26:45 +08:00
userPrompt.AppendLine(contextJson);
userPrompt.AppendLine("要求:");
userPrompt.AppendLine("1. 每条建议输出对象包含 name、meaning、fiveElementReason 三个字段;");
2025-11-06 19:23:42 +08:00
userPrompt.AppendLine("2. meaning为姓名的含义和出处");
userPrompt.AppendLine("3. 所有汉字需为生活常用的简体字,单字笔画尽量小于等于 12");
userPrompt.AppendLine("4. name 必须包含姓氏且满足给定的字数;");
userPrompt.AppendLine("5. fiveElementReason 需指明所补足的五行依据。");
2025-11-05 17:26:45 +08:00
userPrompt.AppendLine("请仅返回 JSON 数据,格式为 {\"suggestions\":[{...}]},不要附加额外说明。");
var requestBody = new
{
model = _deepSeekOptions.Model,
temperature = _deepSeekOptions.Temperature,
2025-11-06 19:23:42 +08:00
//max_tokens = _deepSeekOptions.MaxTokens,
response_format = new { type = _deepSeekOptions.ResponseFormat },
2025-11-05 17:26:45 +08:00
messages = new[]
{
2025-11-06 19:23:42 +08:00
new { role = "system", content = "你是一名资深的中文姓名学专家,擅长古诗词,诗经,易经中国传统文化根据八字喜用神给出实用建议。" },
2025-11-05 17:26:45 +08:00
new { role = "user", content = userPrompt.ToString() }
}
};
var message = new HttpRequestMessage(HttpMethod.Post, endpoint)
{
2025-11-06 19:23:42 +08:00
Content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json"),
2025-11-05 17:26:45 +08:00
};
return message;
}
private IReadOnlyList<NamingSuggestion> ParseDeepSeekResponse(string payload, NamingRequest request)
{
try
{
2025-11-06 19:23:42 +08:00
if (string.IsNullOrWhiteSpace(payload))
{
_logger.LogWarning("DeepSeek 响应为空字符串");
return Array.Empty<NamingSuggestion>();
}
2025-11-05 17:26:45 +08:00
using var document = JsonDocument.Parse(payload);
var choices = document.RootElement.GetProperty("choices");
if (choices.ValueKind != JsonValueKind.Array || choices.GetArrayLength() == 0)
{
2025-11-06 19:23:42 +08:00
_logger.LogWarning("DeepSeek 响应缺少 choices 数组payload={Payload}", payload);
2025-11-05 17:26:45 +08:00
return Array.Empty<NamingSuggestion>();
2025-11-05 00:22:21 +08:00
}
2025-11-05 17:26:45 +08:00
var content = choices[0]
.GetProperty("message")
.GetProperty("content")
.GetString();
2025-11-05 00:22:21 +08:00
2025-11-05 17:26:45 +08:00
if (string.IsNullOrWhiteSpace(content))
2025-11-05 00:22:21 +08:00
{
2025-11-06 19:23:42 +08:00
_logger.LogWarning("DeepSeek 响应 message.content 为空");
2025-11-05 17:26:45 +08:00
return Array.Empty<NamingSuggestion>();
}
using var suggestionsDoc = JsonDocument.Parse(content);
if (!suggestionsDoc.RootElement.TryGetProperty("suggestions", out var suggestionsElement) ||
suggestionsElement.ValueKind != JsonValueKind.Array)
{
2025-11-06 19:23:42 +08:00
_logger.LogWarning("DeepSeek 返回内容缺少 suggestions 数组content={Content}", content);
2025-11-05 17:26:45 +08:00
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))
2025-11-05 00:22:21 +08:00
{
2025-11-06 19:23:42 +08:00
_logger.LogWarning("DeepSeek suggestion缺少 name 字段item={@Item}", item.ToString());
2025-11-05 00:22:21 +08:00
continue;
}
2025-11-05 17:26:45 +08:00
name = name.Trim();
if (!name.StartsWith(request.Surname, StringComparison.Ordinal))
2025-11-05 00:22:21 +08:00
{
2025-11-05 17:26:45 +08:00
name = $"{request.Surname}{name}";
2025-11-05 00:22:21 +08:00
}
2025-11-05 17:26:45 +08:00
if (!ValidateNameLength(name, request))
2025-11-05 00:22:21 +08:00
{
2025-11-06 19:23:42 +08:00
_logger.LogWarning(
"DeepSeek 建议姓名长度与请求不符已丢弃。Name={Name}, Expect={Expect}, Request={@Request}",
name,
request.NameLength,
new
{
request.Surname,
request.Gender,
request.NameLength
});
2025-11-05 17:26:45 +08:00
continue;
}
if (!uniqueNames.Add(name))
{
2025-11-06 19:23:42 +08:00
_logger.LogInformation("DeepSeek 建议出现重复姓名已跳过。Name={Name}", name);
2025-11-05 17:26:45 +08:00
continue;
}
normalized.Add(new NamingSuggestion
{
Name = name,
Meaning = string.IsNullOrWhiteSpace(meaning) ? "寓意待补充" : meaning.Trim(),
ElementReason = string.IsNullOrWhiteSpace(reason) ? "结合八字五行进行补益。" : reason.Trim()
});
if (normalized.Count >= 5)
{
break;
2025-11-05 00:22:21 +08:00
}
}
2025-11-06 19:23:42 +08:00
if (normalized.Count == 0)
{
_logger.LogWarning("DeepSeek suggestions 解析后为空,原始 content={Content}", content);
}
2025-11-05 17:26:45 +08:00
return normalized;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "解析 DeepSeek 响应失败");
return Array.Empty<NamingSuggestion>();
2025-11-05 00:22:21 +08:00
}
}
2025-11-05 17:26:45 +08:00
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;
}
2025-11-05 00:22:21 +08:00
}