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