优化取名逻辑

This commit is contained in:
cjd
2025-11-05 17:26:45 +08:00
parent 25de84e600
commit 68ae25fac2
8 changed files with 980 additions and 169 deletions

View File

@@ -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")
{

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,40 @@
using DouyinApi.Common.Option.Core;
namespace DouyinApi.Common.Option;
/// <summary>
/// DeepSeek 接口配置。
/// </summary>
public class DeepSeekOptions : IConfigurableOptions
{
/// <summary>
/// DeepSeek API 基础地址,例如 https://api.deepseek.com/v1 。
/// </summary>
public string BaseUrl { get; set; } = "https://api.deepseek.com/v1";
/// <summary>
/// DeepSeek API 密钥,需通过环境变量或安全配置注入。
/// </summary>
public string ApiKey { get; set; } = string.Empty;
/// <summary>
/// 使用的模型标识。
/// </summary>
public string Model { get; set; } = "deepseek-chat";
/// <summary>
/// 接口超时时间(秒)。
/// </summary>
public int TimeoutSeconds { get; set; } = 15;
/// <summary>
/// 生成温度,控制结果多样性。
/// </summary>
public double Temperature { get; set; } = 0.6;
/// <summary>
/// 最大生成 token 数。
/// </summary>
public int MaxTokens { get; set; } = 800;
}

View File

@@ -4,6 +4,8 @@ namespace DouyinApi.Model.Naming
{
public class NamingResponse
{
public NamingAnalysis Analysis { get; set; } = new NamingAnalysis();
public IEnumerable<NamingSuggestion> Results { get; set; } = new List<NamingSuggestion>();
}
@@ -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<string> Pillars { get; set; } = new List<string>();
public IReadOnlyList<FiveElementScore> ElementDistribution { get; set; } = new List<FiveElementScore>();
}
public class FiveElementScore
{
public string Element { get; set; } = string.Empty;
public int Count { get; set; }
}
}

View File

@@ -13,6 +13,7 @@
<ItemGroup>
<ProjectReference Include="..\DouyinApi.IServices\DouyinApi.IServices.csproj" />
<ProjectReference Include="..\DouyinApi.Repository\DouyinApi.Repository.csproj" />
<ProjectReference Include="..\DouyinApi.Common\DouyinApi.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -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<string, string> StemElements = new()
{
["甲"] = "木",
["乙"] = "木",
["丙"] = "火",
["丁"] = "火",
["戊"] = "土",
["己"] = "土",
["庚"] = "金",
["辛"] = "金",
["壬"] = "水",
["癸"] = "水"
};
private static readonly Dictionary<string, string> BranchElements = new()
{
["子"] = "水",
["丑"] = "土",
["寅"] = "木",
["卯"] = "木",
["辰"] = "土",
["巳"] = "火",
["午"] = "火",
["未"] = "土",
["申"] = "金",
["酉"] = "金",
["戌"] = "土",
["亥"] = "水"
};
private static readonly Dictionary<string, int> HourStemBaseIndex = new()
{
["甲"] = 0,
["己"] = 0,
["乙"] = 2,
["庚"] = 2,
["丙"] = 4,
["辛"] = 4,
["丁"] = 6,
["壬"] = 6,
["戊"] = 8,
["癸"] = 8
};
private static readonly Dictionary<string, int> 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<string, int>(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<string>()
: 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<string, int> 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<string, int> elementCounts,
IReadOnlyList<string> elementOrder,
IReadOnlyList<string> weakElements,
IReadOnlyList<string> 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<string, int> ElementCounts { get; }
public IReadOnlyList<string> ElementOrder { get; }
public IReadOnlyList<string> WeakElements { get; }
public IReadOnlyList<string> StrongElements { get; }
public bool IsBalanced { get; }
}

View File

@@ -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<NamingSuggestion> 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<NamingSuggestion>();
var usedCharacters = new HashSet<string>(StringComparer.Ordinal);
var usedNames = new HashSet<string>(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<string>(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<string> BuildElementSequence(BaziProfile profile)
{
var sequence = new List<string>();
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<string>();
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<string, List<CharacterProfile>> _profiles;
public CharacterLibrary()
{
_profiles = new Dictionary<string, List<CharacterProfile>>(StringComparer.Ordinal)
{
["木"] = new List<CharacterProfile>
{
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<CharacterProfile>
{
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<CharacterProfile>
{
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<CharacterProfile>
{
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<CharacterProfile>
{
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<string> 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
}

View File

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