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;
+ }
}