using System.Text.RegularExpressions; using MiaoJiZhang.Domain.Entities; using MiaoJiZhang.Domain.Enums; using MiaoJiZhang.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; namespace MiaoJiZhang.Api.Services; public record ParsedBill( TransactionType Type, long CategoryId, string CategoryName, string Note, decimal Amount, string? PaymentMethod, DateTime? OccurredAt = null, TransferDirection? TransferDirection = null, string? Counterparty = null); public record IntentResult(string Kind, ParsedBill? Bill); // bill | query | chat /// /// Cleans legacy AI notes for list display only. It never decides intent, type, or category. /// public static class TransactionNoteFormatter { private static readonly Regex AmountRegex = new( @"(? 20 ? note[..20] : note; } } /// /// AI 回复文案生成。当前为模板版(性格 × 口癖),后续接 LLM。 /// 性格模板与口癖均来自数据库(后台可配 —— 决策:Prompt 后台可调)。 /// public class ReplyService(AppDbContext db) { public async Task BillReplyAsync(long userId, ParsedBill bill) { var (persona, tic) = await GetPersonaAsync(userId); var action = bill.Type switch { TransactionType.Income => "收入", TransactionType.Transfer when bill.TransferDirection == TransferDirection.In => "转入", TransactionType.Transfer => "转出", _ => "支出", }; var body = persona switch { "gentle" => $"{action}记好啦~{bill.CategoryName} ¥{bill.Amount:F2}", "strict" => $"已记录{action}:{bill.CategoryName} ¥{bill.Amount:F2}。", "meme" => $"{action}记上了!{bill.CategoryName} ¥{bill.Amount:F2},家人们谁懂啊", _ => bill.Type.IsIncome(bill.TransferDirection) ? $"收入到账!{bill.CategoryName} ¥{bill.Amount:F2},钱包回血啦" : $"记好了!{bill.CategoryName} ¥{bill.Amount:F2},这笔支出我帮你盯着", }; return ApplyTic(body, persona, tic); } public async Task QueryReplyAsync(long userId, decimal topAmount, string topCategory, double percent) { var (persona, tic) = await GetPersonaAsync(userId); var body = persona switch { "gentle" => $"这个月「{topCategory}」花得最多,¥{topAmount:F0}(占 {percent:F0}%)~ 要不要我帮你定个小目标呀?", "strict" => $"本月最大支出分类:{topCategory} ¥{topAmount:F2},占总支出 {percent:F0}%。建议设置分类预算。", "meme" => $"必须是「{topCategory}」!¥{topAmount:F0},占了 {percent:F0}%,你这钱包是碎钞机吧 2333", _ => $"是「{topCategory}」¥{topAmount:F0}(占 {percent:F0}%)!继续这么花,月底吃土", }; return ApplyTic(body, persona, tic); } public async Task ChatReplyAsync(long userId) { var (persona, tic) = await GetPersonaAsync(userId); var body = persona == "gentle" ? "嗯嗯我在听~要记账直接说金额就行哦" : "在听。想记账就直接说,比如「午饭 26」"; return ApplyTic(body, persona, tic); } public Task MonthlyRoastAsync( long userId, decimal income, decimal expense, string topCategory, decimal topAmount) => PeriodRoastAsync(userId, "这个月", income, expense, topCategory, topAmount); public async Task PeriodRoastAsync( long userId, string periodLabel, decimal income, decimal expense, string topCategory, decimal topAmount) { var (persona, tic) = await GetPersonaAsync(userId); var ratio = income == 0 ? "N/A" : $"{expense / income * 100:F0}%"; var body = persona switch { "gentle" => $"{periodLabel}收入 ¥{income:F0}、支出 ¥{expense:F0},「{topCategory}」花了 ¥{topAmount:F0}。我们一起把结余慢慢提高一点吧。", "strict" => $"{periodLabel}收支比 {ratio}。最大支出分类「{topCategory}」¥{topAmount:F2},建议继续按预算复盘。", "meme" => $"{periodLabel}收入 ¥{income:F0},支出 ¥{expense:F0},「{topCategory}」就干掉 ¥{topAmount:F0},钱包这波压力不小。", _ => $"{periodLabel}收入 ¥{income:F0},「{topCategory}」花了 ¥{topAmount:F0}。下个周期这个分类我会继续帮你盯着。", }; return ApplyTic(body, persona, tic); } /// 表情包回复(按性格;用户发表情包时调用) public async Task<(string Text, string? StickerKey)> StickerReplyAsync(long userId, string stickerKey) { var (persona, tic) = await GetPersonaAsync(userId); if (stickerKey == "salary") { var t = persona == "strict" ? "检测到收入信号。请报具体金额,我来记录。" : ApplyTic("哦?发工资了?多少多少,快报数,我帮你记收入!", persona, tic); return (t, null); } var text = persona switch { "gentle" => "哈哈收到~心情好最重要啦", "strict" => "已收到。请继续保持记账习惯。", "meme" => "哈哈哈哈这表情包我先存了", _ => ApplyTic("卖萌也没用!该省还得省", persona, tic), }; // 回一个表情包(毒舌回生气,温柔回开心) var replySticker = persona == "gentle" ? "happy" : "angry"; return (text, replySticker); } private async Task<(string Persona, string Tic)> GetPersonaAsync(long userId) { var setting = await db.AiCompanionSettings.FirstOrDefaultAsync(s => s.UserId == userId); var personaKey = setting?.PersonaKey ?? "sassy_cat"; var avatarKey = setting?.AvatarKey ?? "cat"; var tic = await db.AiAvatars.Where(a => a.Key == avatarKey) .Select(a => a.SpeechTic).FirstOrDefaultAsync() ?? ""; return (personaKey, tic); } /// 口癖跟随形象(决策 20);严格管家不用口癖 private static string ApplyTic(string text, string persona, string tic) { if (string.IsNullOrEmpty(tic) || persona == "strict") return text; return text + tic; } }