优化取名逻辑
This commit is contained in:
@@ -1,204 +1,350 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
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
|
||||
namespace DouyinApi.Services;
|
||||
|
||||
public class NamingService : INamingService
|
||||
{
|
||||
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)
|
||||
{
|
||||
private static readonly Regex SurnameRegex = new(@"^[\u4e00-\u9fa5]{1,2}$", RegexOptions.Compiled);
|
||||
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
|
||||
_deepSeekOptions = deepSeekOptions?.Value ?? new DeepSeekOptions();
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
private static readonly IReadOnlyList<char> MaleCharacters = new[]
|
||||
public Task<bool> ValidateSurnameAsync(string surname)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(surname))
|
||||
{
|
||||
'辰', '昇', '曜', '瀚', '宸', '沐', '景', '霖', '晟', '玥', '骁', '煜', '澜', '珩', '聿', '澄', '钧'
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyList<char> FemaleCharacters = new[]
|
||||
{
|
||||
'瑶', '霏', '婉', '绮', '璇', '芷', '乂', '灵', '沁', '语', '晴', '若', '绫', '芸', '络', '梦', '澜'
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyList<char> NeutralCharacters = new[]
|
||||
{
|
||||
'玄', '洛', '岚', '澈', '岑', '泓', '澜', '烨', '闻', '黎', '墨', '夙', '羲', '霆', '渊', '翎'
|
||||
};
|
||||
|
||||
private static readonly Dictionary<char, string> CharacterMeanings = new()
|
||||
{
|
||||
['辰'] = "辰曜星光",
|
||||
['昇'] = "旭日东升",
|
||||
['曜'] = "光耀万里",
|
||||
['瀚'] = "瀚海无垠",
|
||||
['宸'] = "王者气度",
|
||||
['沐'] = "沐浴祥瑞",
|
||||
['景'] = "景行德高",
|
||||
['霖'] = "甘霖润泽",
|
||||
['晟'] = "光明昌盛",
|
||||
['玥'] = "王者之玉",
|
||||
['骁'] = "英勇不屈",
|
||||
['煜'] = "照耀四方",
|
||||
['澜'] = "碧波浩渺",
|
||||
['珩'] = "璞玉内敛",
|
||||
['聿'] = "持守正道",
|
||||
['澄'] = "心境澄明",
|
||||
['钧'] = "乾坤平衡",
|
||||
['瑶'] = "瑶光琼华",
|
||||
['霏'] = "霏霏瑞雪",
|
||||
['婉'] = "柔婉清扬",
|
||||
['绮'] = "绮丽灵动",
|
||||
['璇'] = "璇玑回转",
|
||||
['芷'] = "香草高洁",
|
||||
['乂'] = "安然有序",
|
||||
['灵'] = "灵秀夺目",
|
||||
['沁'] = "沁心甘露",
|
||||
['语'] = "言语有光",
|
||||
['晴'] = "晴空暖阳",
|
||||
['若'] = "若水明澈",
|
||||
['绫'] = "绫罗轻盈",
|
||||
['芸'] = "芸芸芳草",
|
||||
['络'] = "络绎华彩",
|
||||
['梦'] = "梦想成真",
|
||||
['玄'] = "玄妙莫测",
|
||||
['洛'] = "洛水灵韵",
|
||||
['岚'] = "山岚清气",
|
||||
['澈'] = "心境通透",
|
||||
['岑'] = "峻岭深沉",
|
||||
['泓'] = "泓泉清澈",
|
||||
['烨'] = "火光通明",
|
||||
['闻'] = "名闻四海",
|
||||
['黎'] = "黎明曙光",
|
||||
['墨'] = "墨香书韵",
|
||||
['夙'] = "夙愿成真",
|
||||
['羲'] = "伏羲灵息",
|
||||
['霆'] = "雷霆万钧",
|
||||
['渊'] = "渊源深厚",
|
||||
['翎'] = "翎羽轻灵"
|
||||
};
|
||||
|
||||
private static readonly string[] Blessings =
|
||||
{
|
||||
"引瑞气入怀,扶摇直上",
|
||||
"承先祖之德,守家风之和",
|
||||
"与四时共鸣,步步生辉",
|
||||
"纳天地灵气,行稳致远",
|
||||
"凝万象之精魄,护佑昌隆",
|
||||
"藏锋于怀,静待时来",
|
||||
"兼济天下情怀,拥抱广阔未来"
|
||||
};
|
||||
|
||||
public Task<bool> ValidateSurnameAsync(string surname)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(surname))
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
return Task.FromResult(SurnameRegex.IsMatch(surname.Trim()));
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public async Task<NamingResponse> GenerateNamesAsync(NamingRequest request)
|
||||
return Task.FromResult(SurnamePattern.IsMatch(surname.Trim()));
|
||||
}
|
||||
|
||||
public async Task<NamingResponse> GenerateNamesAsync(NamingRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
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())
|
||||
{
|
||||
suggestions = _fallbackGenerator.Generate(request, baziProfile);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
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())
|
||||
{
|
||||
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>();
|
||||
}
|
||||
|
||||
if (!await ValidateSurnameAsync(request.Surname))
|
||||
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
|
||||
{
|
||||
throw new ArgumentException("invalid_surname");
|
||||
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) ? "单字" : "双字";
|
||||
var genderLabel = string.Equals(request.Gender, "female", StringComparison.OrdinalIgnoreCase) ? "女孩" : "男孩";
|
||||
|
||||
var userPrompt = new StringBuilder();
|
||||
userPrompt.AppendLine("请扮演专业的中华姓名学顾问,根据八字五行提出姓名建议。");
|
||||
userPrompt.AppendLine($"需基于以下 JSON 数据为姓氏“{request.Surname}”的{genderLabel}提供 5 个{lengthDescriptor}中文名字:");
|
||||
userPrompt.AppendLine(contextJson);
|
||||
userPrompt.AppendLine("要求:");
|
||||
userPrompt.AppendLine("1. 每条建议输出对象包含 name、meaning、fiveElementReason 三个字段;");
|
||||
userPrompt.AppendLine("2. 所有汉字需为生活常用的简体字,单字笔画尽量小于等于 12;");
|
||||
userPrompt.AppendLine("3. name 必须包含姓氏且满足给定的字数;");
|
||||
userPrompt.AppendLine("4. fiveElementReason 需指明所补足的五行依据。");
|
||||
userPrompt.AppendLine("请仅返回 JSON 数据,格式为 {\"suggestions\":[{...}]},不要附加额外说明。");
|
||||
|
||||
var requestBody = new
|
||||
{
|
||||
model = _deepSeekOptions.Model,
|
||||
temperature = _deepSeekOptions.Temperature,
|
||||
max_tokens = _deepSeekOptions.MaxTokens,
|
||||
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
|
||||
{
|
||||
using var document = JsonDocument.Parse(payload);
|
||||
var choices = document.RootElement.GetProperty("choices");
|
||||
if (choices.ValueKind != JsonValueKind.Array || choices.GetArrayLength() == 0)
|
||||
{
|
||||
return Array.Empty<NamingSuggestion>();
|
||||
}
|
||||
|
||||
var seed = CreateSeed(request);
|
||||
var random = new Random(seed);
|
||||
var pool = request.Gender == "female" ? FemaleCharacters : MaleCharacters;
|
||||
var fallbackPool = NeutralCharacters;
|
||||
var content = choices[0]
|
||||
.GetProperty("message")
|
||||
.GetProperty("content")
|
||||
.GetString();
|
||||
|
||||
var suggestions = new List<NamingSuggestion>();
|
||||
var attempts = 0;
|
||||
|
||||
while (suggestions.Count < 5 && attempts < 100)
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
attempts++;
|
||||
var given = GenerateGivenName(request.NameLength, pool, fallbackPool, random);
|
||||
var fullName = $"{request.Surname}{given}";
|
||||
if (suggestions.Any(s => s.Name == fullName))
|
||||
return Array.Empty<NamingSuggestion>();
|
||||
}
|
||||
|
||||
using var suggestionsDoc = JsonDocument.Parse(content);
|
||||
if (!suggestionsDoc.RootElement.TryGetProperty("suggestions", out var suggestionsElement) ||
|
||||
suggestionsElement.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
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))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var meaning = ComposeMeaning(request.Surname, given, request.BirthDate, request.BirthTime, random);
|
||||
suggestions.Add(new NamingSuggestion
|
||||
name = name.Trim();
|
||||
if (!name.StartsWith(request.Surname, StringComparison.Ordinal))
|
||||
{
|
||||
Name = fullName,
|
||||
Meaning = meaning
|
||||
});
|
||||
}
|
||||
|
||||
if (!suggestions.Any())
|
||||
{
|
||||
suggestions.Add(new NamingSuggestion
|
||||
{
|
||||
Name = $"{request.Surname}玄珏",
|
||||
Meaning = "玄珠映月,珏石生辉,与生辰相应,寓意行稳致远"
|
||||
});
|
||||
}
|
||||
|
||||
return new NamingResponse
|
||||
{
|
||||
Results = suggestions
|
||||
};
|
||||
}
|
||||
|
||||
private static int CreateSeed(NamingRequest request)
|
||||
{
|
||||
var raw = $"{request.Surname}-{request.Gender}-{request.BirthDate}-{request.BirthTime}-{request.NameLength}";
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(raw));
|
||||
return Math.Abs(BitConverter.ToInt32(bytes, 0));
|
||||
}
|
||||
|
||||
private static string GenerateGivenName(
|
||||
string nameLength,
|
||||
IReadOnlyList<char> primaryPool,
|
||||
IReadOnlyList<char> fallbackPool,
|
||||
Random random)
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
var length = nameLength == "single" ? 1 : 2;
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
var pool = (i == 0) ? primaryPool : fallbackPool;
|
||||
buffer.Append(pool[random.Next(pool.Count)]);
|
||||
}
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
private static string ComposeMeaning(string surname, string given, string birthDate, string birthTime, Random random)
|
||||
{
|
||||
var fragments = new List<string>();
|
||||
foreach (var ch in given)
|
||||
{
|
||||
if (CharacterMeanings.TryGetValue(ch, out var desc))
|
||||
{
|
||||
fragments.Add(desc);
|
||||
name = $"{request.Surname}{name}";
|
||||
}
|
||||
else
|
||||
|
||||
if (!ValidateNameLength(name, request))
|
||||
{
|
||||
fragments.Add($"{ch}寓意祥瑞");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!uniqueNames.Add(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;
|
||||
}
|
||||
}
|
||||
|
||||
var blessing = Blessings[random.Next(Blessings.Length)];
|
||||
var birthFragment = string.IsNullOrWhiteSpace(birthTime)
|
||||
? $"{birthDate}之辰"
|
||||
: $"{birthDate} {birthTime} 时刻";
|
||||
|
||||
return $"{string.Join(",", fragments)},与{surname}氏气脉相连,于{birthFragment}呼应,{blessing}。";
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user