diff --git a/DouyinApi.Api/Controllers/MiniProgram/NamingController.cs b/DouyinApi.Api/Controllers/MiniProgram/NamingController.cs index cc72070..7887031 100644 --- a/DouyinApi.Api/Controllers/MiniProgram/NamingController.cs +++ b/DouyinApi.Api/Controllers/MiniProgram/NamingController.cs @@ -54,7 +54,11 @@ namespace DouyinApi.Api.Controllers.MiniProgram try { var response = await _namingService.GenerateNamesAsync(normalizedRequest); - return Ok(new { results = response.Results }); + return Ok(new + { + analysis = response.Analysis, + results = response.Results + }); } catch (ArgumentException ex) when (ex.Message == "invalid_surname") { diff --git a/DouyinApi.Api/appsettings.json b/DouyinApi.Api/appsettings.json index fccfa43..e299b24 100644 --- a/DouyinApi.Api/appsettings.json +++ b/DouyinApi.Api/appsettings.json @@ -359,5 +359,13 @@ "Enabled": false, "Address": "http://localhost:5341/", "ApiKey": "" + }, + "DeepSeek": { + "BaseUrl": "https://api.deepseek.com/v1", + "ApiKey": "", + "Model": "deepseek-chat", + "TimeoutSeconds": 15, + "Temperature": 0.6, + "MaxTokens": 800 } -} \ No newline at end of file +} diff --git a/DouyinApi.Common/Option/DeepSeekOptions.cs b/DouyinApi.Common/Option/DeepSeekOptions.cs new file mode 100644 index 0000000..17849e6 --- /dev/null +++ b/DouyinApi.Common/Option/DeepSeekOptions.cs @@ -0,0 +1,40 @@ +using DouyinApi.Common.Option.Core; + +namespace DouyinApi.Common.Option; + +/// +/// DeepSeek 接口配置。 +/// +public class DeepSeekOptions : IConfigurableOptions +{ + /// + /// DeepSeek API 基础地址,例如 https://api.deepseek.com/v1 。 + /// + public string BaseUrl { get; set; } = "https://api.deepseek.com/v1"; + + /// + /// DeepSeek API 密钥,需通过环境变量或安全配置注入。 + /// + public string ApiKey { get; set; } = string.Empty; + + /// + /// 使用的模型标识。 + /// + public string Model { get; set; } = "deepseek-chat"; + + /// + /// 接口超时时间(秒)。 + /// + public int TimeoutSeconds { get; set; } = 15; + + /// + /// 生成温度,控制结果多样性。 + /// + public double Temperature { get; set; } = 0.6; + + /// + /// 最大生成 token 数。 + /// + public int MaxTokens { get; set; } = 800; +} + diff --git a/DouyinApi.Model/Naming/NamingResponse.cs b/DouyinApi.Model/Naming/NamingResponse.cs index 1fcf4b1..f8e0be6 100644 --- a/DouyinApi.Model/Naming/NamingResponse.cs +++ b/DouyinApi.Model/Naming/NamingResponse.cs @@ -4,6 +4,8 @@ namespace DouyinApi.Model.Naming { public class NamingResponse { + public NamingAnalysis Analysis { get; set; } = new NamingAnalysis(); + public IEnumerable Results { get; set; } = new List(); } @@ -12,5 +14,22 @@ namespace DouyinApi.Model.Naming public string Name { get; set; } = string.Empty; public string Meaning { get; set; } = string.Empty; + + public string ElementReason { get; set; } = string.Empty; + } + public class NamingAnalysis + { + public string MatchSummary { get; set; } = string.Empty; + + public IReadOnlyList Pillars { get; set; } = new List(); + + public IReadOnlyList ElementDistribution { get; set; } = new List(); + } + + public class FiveElementScore + { + public string Element { get; set; } = string.Empty; + + public int Count { get; set; } } } diff --git a/DouyinApi.Services/DouyinApi.Services.csproj b/DouyinApi.Services/DouyinApi.Services.csproj index 3eb509d..856d597 100644 --- a/DouyinApi.Services/DouyinApi.Services.csproj +++ b/DouyinApi.Services/DouyinApi.Services.csproj @@ -13,6 +13,7 @@ + diff --git a/DouyinApi.Services/Naming/BaziCalculator.cs b/DouyinApi.Services/Naming/BaziCalculator.cs new file mode 100644 index 0000000..24fb373 --- /dev/null +++ b/DouyinApi.Services/Naming/BaziCalculator.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DouyinApi.Services.Naming; + +internal sealed class BaziCalculator +{ + private static readonly string[] HeavenlyStems = + { + "甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸" + }; + + private static readonly string[] EarthlyBranches = + { + "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥" + }; + + private static readonly int[] MonthNumberToBranchIndex = + { + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1 + }; + + private static readonly string[] ElementOrder = { "木", "火", "土", "金", "水" }; + + private static readonly Dictionary StemElements = new() + { + ["甲"] = "木", + ["乙"] = "木", + ["丙"] = "火", + ["丁"] = "火", + ["戊"] = "土", + ["己"] = "土", + ["庚"] = "金", + ["辛"] = "金", + ["壬"] = "水", + ["癸"] = "水" + }; + + private static readonly Dictionary BranchElements = new() + { + ["子"] = "水", + ["丑"] = "土", + ["寅"] = "木", + ["卯"] = "木", + ["辰"] = "土", + ["巳"] = "火", + ["午"] = "火", + ["未"] = "土", + ["申"] = "金", + ["酉"] = "金", + ["戌"] = "土", + ["亥"] = "水" + }; + + private static readonly Dictionary HourStemBaseIndex = new() + { + ["甲"] = 0, + ["己"] = 0, + ["乙"] = 2, + ["庚"] = 2, + ["丙"] = 4, + ["辛"] = 4, + ["丁"] = 6, + ["壬"] = 6, + ["戊"] = 8, + ["癸"] = 8 + }; + + private static readonly Dictionary MonthStemBaseIndex = new() + { + ["甲"] = 2, + ["己"] = 2, + ["乙"] = 4, + ["庚"] = 4, + ["丙"] = 6, + ["辛"] = 6, + ["丁"] = 8, + ["壬"] = 8, + ["戊"] = 0, + ["癸"] = 0 + }; + + private static readonly DateTime DayBaseDate = new(1900, 1, 31); + private const int DayBaseIndex = 0; // 1900-01-31 对应甲子日 + + public BaziProfile Calculate(DateTime birthDateTime) + { + var solarYear = birthDateTime.Year; + var currentYearSpring = new DateTime(birthDateTime.Year, 2, 4); + if (birthDateTime < currentYearSpring) + { + solarYear -= 1; + } + + var yearStemIndex = Mod(solarYear - 4, HeavenlyStems.Length); + var yearBranchIndex = Mod(solarYear - 4, EarthlyBranches.Length); + + var monthNumber = ResolveMonthNumber(birthDateTime, solarYear); + var monthStemBase = MonthStemBaseIndex[HeavenlyStems[yearStemIndex]]; + var monthStemIndex = Mod(monthStemBase + monthNumber - 1, HeavenlyStems.Length); + var monthBranchIndex = MonthNumberToBranchIndex[monthNumber - 1]; + + var dayCycleIndex = Mod(DayBaseIndex + (int)(birthDateTime.Date - DayBaseDate).TotalDays, 60); + var dayStemIndex = Mod(dayCycleIndex, HeavenlyStems.Length); + var dayBranchIndex = Mod(dayCycleIndex, EarthlyBranches.Length); + + var hourBranchIndex = GetHourBranchIndex(birthDateTime.Hour); + var hourStemIndex = Mod(HourStemBaseIndex[HeavenlyStems[dayStemIndex]] + hourBranchIndex, HeavenlyStems.Length); + + var yearStem = HeavenlyStems[yearStemIndex]; + var yearBranch = EarthlyBranches[yearBranchIndex]; + var monthStem = HeavenlyStems[monthStemIndex]; + var monthBranch = EarthlyBranches[monthBranchIndex]; + var dayStem = HeavenlyStems[dayStemIndex]; + var dayBranch = EarthlyBranches[dayBranchIndex]; + var hourStem = HeavenlyStems[hourStemIndex]; + var hourBranch = EarthlyBranches[hourBranchIndex]; + + var elementCounts = ElementOrder.ToDictionary(element => element, _ => 0); + IncrementElement(elementCounts, StemElements[yearStem]); + IncrementElement(elementCounts, BranchElements[yearBranch]); + IncrementElement(elementCounts, StemElements[monthStem]); + IncrementElement(elementCounts, BranchElements[monthBranch]); + IncrementElement(elementCounts, StemElements[dayStem]); + IncrementElement(elementCounts, BranchElements[dayBranch]); + IncrementElement(elementCounts, StemElements[hourStem]); + IncrementElement(elementCounts, BranchElements[hourBranch]); + + var counts = ElementOrder + .Select(element => new KeyValuePair(element, elementCounts[element])) + .ToList(); + + var max = counts.Max(pair => pair.Value); + var min = counts.Min(pair => pair.Value); + + var isBalanced = max - min <= 1; + var weakElements = isBalanced + ? Array.Empty() + : counts.Where(pair => pair.Value == min).Select(pair => pair.Key).ToArray(); + var strongElements = counts.Where(pair => pair.Value == max).Select(pair => pair.Key).ToArray(); + + return new BaziProfile( + yearStem, + yearBranch, + monthStem, + monthBranch, + dayStem, + dayBranch, + hourStem, + hourBranch, + elementCounts, + ElementOrder, + weakElements, + strongElements, + isBalanced); + } + + private static int ResolveMonthNumber(DateTime dt, int solarYear) + { + var springStart = new DateTime(solarYear, 2, 4); + var jingZhe = new DateTime(solarYear, 3, 6); + var qingMing = new DateTime(solarYear, 4, 5); + var liXia = new DateTime(solarYear, 5, 5); + var mangZhong = new DateTime(solarYear, 6, 6); + var xiaoShu = new DateTime(solarYear, 7, 7); + var liQiu = new DateTime(solarYear, 8, 8); + var baiLu = new DateTime(solarYear, 9, 8); + var hanLu = new DateTime(solarYear, 10, 8); + var liDong = new DateTime(solarYear, 11, 7); + var daXue = new DateTime(solarYear, 12, 7); + var xiaoHan = new DateTime(solarYear + 1, 1, 6); + var nextSpring = new DateTime(solarYear + 1, 2, 4); + + if (dt >= springStart && dt < jingZhe) + { + return 1; + } + if (dt >= jingZhe && dt < qingMing) + { + return 2; + } + if (dt >= qingMing && dt < liXia) + { + return 3; + } + if (dt >= liXia && dt < mangZhong) + { + return 4; + } + if (dt >= mangZhong && dt < xiaoShu) + { + return 5; + } + if (dt >= xiaoShu && dt < liQiu) + { + return 6; + } + if (dt >= liQiu && dt < baiLu) + { + return 7; + } + if (dt >= baiLu && dt < hanLu) + { + return 8; + } + if (dt >= hanLu && dt < liDong) + { + return 9; + } + if (dt >= liDong && dt < daXue) + { + return 10; + } + if (dt >= daXue && dt < xiaoHan) + { + return 11; + } + if (dt >= xiaoHan && dt < nextSpring) + { + return 12; + } + return 11; + } + + private static int GetHourBranchIndex(int hour) + { + return ((hour + 1) / 2) % EarthlyBranches.Length; + } + + private static void IncrementElement(IDictionary container, string element) + { + if (container.TryGetValue(element, out var value)) + { + container[element] = value + 1; + } + } + + private static int Mod(int value, int modulus) + { + var result = value % modulus; + return result < 0 ? result + modulus : result; + } +} + +internal sealed class BaziProfile +{ + public BaziProfile( + string yearStem, + string yearBranch, + string monthStem, + string monthBranch, + string dayStem, + string dayBranch, + string hourStem, + string hourBranch, + IReadOnlyDictionary elementCounts, + IReadOnlyList elementOrder, + IReadOnlyList weakElements, + IReadOnlyList strongElements, + bool isBalanced) + { + YearStem = yearStem; + YearBranch = yearBranch; + MonthStem = monthStem; + MonthBranch = monthBranch; + DayStem = dayStem; + DayBranch = dayBranch; + HourStem = hourStem; + HourBranch = hourBranch; + ElementCounts = elementCounts; + ElementOrder = elementOrder; + WeakElements = weakElements; + StrongElements = strongElements; + IsBalanced = isBalanced; + } + + public string YearStem { get; } + + public string YearBranch { get; } + + public string MonthStem { get; } + + public string MonthBranch { get; } + + public string DayStem { get; } + + public string DayBranch { get; } + + public string HourStem { get; } + + public string HourBranch { get; } + + public IReadOnlyDictionary ElementCounts { get; } + + public IReadOnlyList ElementOrder { get; } + + public IReadOnlyList WeakElements { get; } + + public IReadOnlyList StrongElements { get; } + + public bool IsBalanced { get; } +} diff --git a/DouyinApi.Services/Naming/FallbackNameGenerator.cs b/DouyinApi.Services/Naming/FallbackNameGenerator.cs new file mode 100644 index 0000000..51b583b --- /dev/null +++ b/DouyinApi.Services/Naming/FallbackNameGenerator.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DouyinApi.Model.Naming; + +#nullable enable + +namespace DouyinApi.Services.Naming; + +internal sealed class FallbackNameGenerator +{ + private const int TargetCount = 5; + + private static readonly string[] ElementOrder = { "木", "火", "土", "金", "水" }; + + private readonly CharacterLibrary _library = new(); + + public IReadOnlyList Generate(NamingRequest request, BaziProfile profile) + { + var surname = request.Surname; + var isSingle = string.Equals(request.NameLength, "single", StringComparison.OrdinalIgnoreCase); + var gender = NormalizeGender(request.Gender); + + var results = new List(); + var usedCharacters = new HashSet(StringComparer.Ordinal); + var usedNames = new HashSet(StringComparer.Ordinal); + var sequence = BuildElementSequence(profile); + + var attempts = 0; + while (results.Count < TargetCount && attempts < 80) + { + var primaryElement = sequence[attempts % sequence.Count]; + var first = _library.Pick(primaryElement, gender, usedCharacters); + if (first == null) + { + attempts++; + continue; + } + + if (isSingle) + { + var fullName = surname + first.Character; + if (usedNames.Add(fullName)) + { + results.Add(new NamingSuggestion + { + Name = fullName, + Meaning = first.Meaning, + ElementReason = BuildSingleReason(profile, first) + }); + usedCharacters.Add(first.Character); + } + + attempts++; + continue; + } + + var tempUsed = new HashSet(usedCharacters, StringComparer.Ordinal) + { + first.Character + }; + + var secondaryElement = sequence[(attempts + 1) % sequence.Count]; + var second = _library.Pick(secondaryElement, gender, tempUsed); + if (second == null) + { + attempts++; + continue; + } + + var givenName = first.Character + second.Character; + var full = surname + givenName; + if (usedNames.Add(full)) + { + results.Add(new NamingSuggestion + { + Name = full, + Meaning = $"{first.Meaning},{second.Meaning}", + ElementReason = BuildDoubleReason(profile, first, second) + }); + usedCharacters.Add(first.Character); + usedCharacters.Add(second.Character); + } + + attempts++; + } + + return results; + } + + private static string NormalizeGender(string gender) + { + return string.Equals(gender, "female", StringComparison.OrdinalIgnoreCase) ? "female" : "male"; + } + + private static List BuildElementSequence(BaziProfile profile) + { + var sequence = new List(); + if (profile.WeakElements.Count > 0) + { + sequence.AddRange(profile.WeakElements); + } + + foreach (var element in ElementOrder) + { + if (!sequence.Contains(element)) + { + sequence.Add(element); + } + } + + return sequence; + } + + private static string BuildSingleReason(BaziProfile profile, CharacterProfile profileChar) + { + if (!profile.IsBalanced && profile.WeakElements.Contains(profileChar.Element)) + { + return $"命局中{profileChar.Element}势偏弱,选用同属性的“{profileChar.Character}”以补足生机。"; + } + + return $"“{profileChar.Character}”属{profileChar.Element},与命局相协,凸显个人气韵。"; + } + + private static string BuildDoubleReason(BaziProfile profile, CharacterProfile first, CharacterProfile second) + { + var clauses = new List(); + + if (!profile.IsBalanced && profile.WeakElements.Contains(first.Element)) + { + clauses.Add($"命局中{first.Element}势弱,首取“{first.Character}”以增添此行之力"); + } + else + { + clauses.Add($"“{first.Character}”属{first.Element},奠定名字的核心气场"); + } + + if (first.Element == second.Element) + { + clauses.Add($"再以同为{second.Element}属性的“{second.Character}”相辅,巩固能量"); + } + else if (!profile.IsBalanced && profile.WeakElements.Contains(second.Element)) + { + clauses.Add($"辅以{second.Element}属性“{second.Character}”同步补益"); + } + else + { + clauses.Add($"叠加{second.Element}属性“{second.Character}”,使五行流转更趋和谐"); + } + + return string.Join(",", clauses) + "。"; + } +} + +internal sealed class CharacterLibrary +{ + private readonly Dictionary> _profiles; + + public CharacterLibrary() + { + _profiles = new Dictionary>(StringComparer.Ordinal) + { + ["木"] = new List + { + new("林", "木", 8, "林木森郁,生机盎然", GenderAffinity.Neutral), + new("杉", "木", 7, "杉木挺拔,坚韧自持", GenderAffinity.Male), + new("柏", "木", 9, "柏树常青,守正不渝", GenderAffinity.Male), + new("桐", "木", 10, "梧桐清雅,自带高洁", GenderAffinity.Neutral), + new("柯", "木", 9, "柯木坚劲,胸怀正义", GenderAffinity.Male), + new("梓", "木", 11, "梓木葱郁,延续家风", GenderAffinity.Neutral), + new("芸", "木", 10, "芸草幽香,温婉灵动", GenderAffinity.Female), + new("茜", "木", 9, "茜草明艳,朝气蓬勃", GenderAffinity.Female) + }, + ["火"] = new List + { + new("炜", "火", 9, "炜火明亮,奋发向上", GenderAffinity.Male), + new("炎", "火", 8, "炎光熠熠,激情澎湃", GenderAffinity.Male), + new("晗", "火", 11, "晨晗初照,温暖柔和", GenderAffinity.Female), + new("晟", "火", 11, "晟意光盛,事业昌隆", GenderAffinity.Male), + new("旭", "火", 6, "旭日东升,胸怀朝阳", GenderAffinity.Neutral), + new("烁", "火", 10, "烁光璀璨,灵动敏捷", GenderAffinity.Neutral), + new("炫", "火", 9, "炫彩熠熠,才华外露", GenderAffinity.Neutral) + }, + ["土"] = new List + { + new("坤", "土", 8, "坤厚载物,包容从容", GenderAffinity.Neutral), + new("均", "土", 7, "均衡端稳,踏实可靠", GenderAffinity.Neutral), + new("垣", "土", 9, "垣墙坚实,守护安宁", GenderAffinity.Neutral), + new("城", "土", 9, "城池稳固,守正不移", GenderAffinity.Male), + new("岳", "土", 8, "山岳巍峨,志向高远", GenderAffinity.Male), + new("培", "土", 11, "培土厚植,涵养成长", GenderAffinity.Neutral), + new("堇", "土", 12, "堇色沉静,内敛温润", GenderAffinity.Female), + new("坦", "土", 8, "坦荡诚笃,胸怀光明", GenderAffinity.Male) + }, + ["金"] = new List + { + new("钧", "金", 9, "钧衡公正,持中守衡", GenderAffinity.Neutral), + new("铭", "金", 11, "铭记初心,彰显才华", GenderAffinity.Neutral), + new("锐", "金", 11, "锋芒锐利,果敢决断", GenderAffinity.Male), + new("钦", "金", 9, "钦敬谦和,端方稳重", GenderAffinity.Neutral), + new("钥", "金", 9, "钥启智慧,开拓视野", GenderAffinity.Neutral), + new("铠", "金", 12, "铠甲护身,坚毅勇敢", GenderAffinity.Male), + new("钰", "金", 10, "钰石珍贵,坚贞明亮", GenderAffinity.Female) + }, + ["水"] = new List + { + new("泽", "水", 9, "泽润万物,胸怀宽广", GenderAffinity.Neutral), + new("泓", "水", 8, "泓水清澈,心境澄明", GenderAffinity.Neutral), + new("润", "水", 10, "润泽柔润,涵养细腻", GenderAffinity.Female), + new("涵", "水", 11, "涵容自守,温婉内敛", GenderAffinity.Female), + new("沐", "水", 7, "沐浴春风,清新怡然", GenderAffinity.Neutral), + new("泊", "水", 8, "泊舟安然,沉稳淡泊", GenderAffinity.Male), + new("洵", "水", 9, "洵美诚笃,信义笃实", GenderAffinity.Male), + new("沁", "水", 8, "沁香流淌,气质清雅", GenderAffinity.Female) + } + }; + } + + public CharacterProfile? Pick(string element, string gender, HashSet usedCharacters) + { + if (!_profiles.TryGetValue(element, out var candidates)) + { + return null; + } + + // 优先匹配性别偏好 + foreach (var profile in candidates) + { + if (usedCharacters.Contains(profile.Character)) + { + continue; + } + + if (profile.Affinity == GenderAffinity.Neutral || + (profile.Affinity == GenderAffinity.Male && gender == "male") || + (profile.Affinity == GenderAffinity.Female && gender == "female")) + { + if (profile.StrokeCount <= 12) + { + return profile; + } + } + } + + // 兜底选择中性字 + foreach (var profile in candidates) + { + if (usedCharacters.Contains(profile.Character)) + { + continue; + } + + if (profile.Affinity == GenderAffinity.Neutral && profile.StrokeCount <= 12) + { + return profile; + } + } + + return null; + } +} + +internal sealed class CharacterProfile +{ + public CharacterProfile(string character, string element, int strokeCount, string meaning, GenderAffinity affinity) + { + Character = character; + Element = element; + StrokeCount = strokeCount; + Meaning = meaning; + Affinity = affinity; + } + + public string Character { get; } + + public string Element { get; } + + public int StrokeCount { get; } + + public string Meaning { get; } + + public GenderAffinity Affinity { get; } +} + +internal enum GenderAffinity +{ + Neutral, + Male, + Female +} diff --git a/DouyinApi.Services/NamingService.cs b/DouyinApi.Services/NamingService.cs index affa4b6..d389773 100644 --- a/DouyinApi.Services/NamingService.cs +++ b/DouyinApi.Services/NamingService.cs @@ -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 _logger; + private readonly BaziCalculator _baziCalculator = new(); + private readonly FallbackNameGenerator _fallbackGenerator = new(); + + public NamingService( + IHttpClientFactory httpClientFactory, + IOptions deepSeekOptions, + ILogger 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 MaleCharacters = new[] + public Task ValidateSurnameAsync(string surname) + { + if (string.IsNullOrWhiteSpace(surname)) { - '辰', '昇', '曜', '瀚', '宸', '沐', '景', '霖', '晟', '玥', '骁', '煜', '澜', '珩', '聿', '澄', '钧' - }; - - private static readonly IReadOnlyList FemaleCharacters = new[] - { - '瑶', '霏', '婉', '绮', '璇', '芷', '乂', '灵', '沁', '语', '晴', '若', '绫', '芸', '络', '梦', '澜' - }; - - private static readonly IReadOnlyList NeutralCharacters = new[] - { - '玄', '洛', '岚', '澈', '岑', '泓', '澜', '烨', '闻', '黎', '墨', '夙', '羲', '霆', '渊', '翎' - }; - - private static readonly Dictionary CharacterMeanings = new() - { - ['辰'] = "辰曜星光", - ['昇'] = "旭日东升", - ['曜'] = "光耀万里", - ['瀚'] = "瀚海无垠", - ['宸'] = "王者气度", - ['沐'] = "沐浴祥瑞", - ['景'] = "景行德高", - ['霖'] = "甘霖润泽", - ['晟'] = "光明昌盛", - ['玥'] = "王者之玉", - ['骁'] = "英勇不屈", - ['煜'] = "照耀四方", - ['澜'] = "碧波浩渺", - ['珩'] = "璞玉内敛", - ['聿'] = "持守正道", - ['澄'] = "心境澄明", - ['钧'] = "乾坤平衡", - ['瑶'] = "瑶光琼华", - ['霏'] = "霏霏瑞雪", - ['婉'] = "柔婉清扬", - ['绮'] = "绮丽灵动", - ['璇'] = "璇玑回转", - ['芷'] = "香草高洁", - ['乂'] = "安然有序", - ['灵'] = "灵秀夺目", - ['沁'] = "沁心甘露", - ['语'] = "言语有光", - ['晴'] = "晴空暖阳", - ['若'] = "若水明澈", - ['绫'] = "绫罗轻盈", - ['芸'] = "芸芸芳草", - ['络'] = "络绎华彩", - ['梦'] = "梦想成真", - ['玄'] = "玄妙莫测", - ['洛'] = "洛水灵韵", - ['岚'] = "山岚清气", - ['澈'] = "心境通透", - ['岑'] = "峻岭深沉", - ['泓'] = "泓泉清澈", - ['烨'] = "火光通明", - ['闻'] = "名闻四海", - ['黎'] = "黎明曙光", - ['墨'] = "墨香书韵", - ['夙'] = "夙愿成真", - ['羲'] = "伏羲灵息", - ['霆'] = "雷霆万钧", - ['渊'] = "渊源深厚", - ['翎'] = "翎羽轻灵" - }; - - private static readonly string[] Blessings = - { - "引瑞气入怀,扶摇直上", - "承先祖之德,守家风之和", - "与四时共鸣,步步生辉", - "纳天地灵气,行稳致远", - "凝万象之精魄,护佑昌隆", - "藏锋于怀,静待时来", - "兼济天下情怀,拥抱广阔未来" - }; - - public Task 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 GenerateNamesAsync(NamingRequest request) + return Task.FromResult(SurnamePattern.IsMatch(surname.Trim())); + } + + public async Task 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 + { + $"{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> TryGenerateViaDeepSeekAsync(NamingRequest request, BaziProfile profile) + { + if (IsDeepSeekConfigInvalid()) + { + 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(); } - if (!await ValidateSurnameAsync(request.Surname)) + 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 { - 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 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(); } - 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(); - 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(); + } + + using var suggestionsDoc = JsonDocument.Parse(content); + if (!suggestionsDoc.RootElement.TryGetProperty("suggestions", out var suggestionsElement) || + suggestionsElement.ValueKind != JsonValueKind.Array) + { + 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)) { 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 primaryPool, - IReadOnlyList 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(); - 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(); } } + + 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; + } }