Files
EmailBill/.doc/ai-service-refactoring-summary.md
SunCheng d052ae5197 fix
2026-02-10 17:49:19 +08:00

11 KiB
Raw Blame History

AI 服务重构总结


title: AI调用统一封装到SmartHandleService author: AI Assistant
date: 2026-02-10 status: final category: 重构

概述

本次重构将项目中所有分散的 AI 调用统一封装到 SmartHandleService实现了AI功能的集中管理和统一维护。

重构前的问题

在重构前,项目中有多个地方直接依赖 IOpenAiService 进行 AI 调用:

  1. EmailParseServicesBase (Service/EmailServices/EmailParse/IEmailParseServices.cs)

    • 邮件解析AI兜底逻辑
    • 包含大量重复的类型判断代码
  2. CategoryIconGenerationJob (Service/Jobs/CategoryIconGenerationJob.cs)

    • 定时任务批量生成分类图标
    • 包含复杂的Prompt构建逻辑
  3. TransactionCategoryApplication (Application/TransactionCategoryApplication.cs)

    • 手动触发单个图标生成
    • 包含SVG提取和验证逻辑
  4. BudgetService (Service/Budget/BudgetService.cs)

    • 生成预算执行报告
    • 包含复杂的数据格式化逻辑

存在的问题

  • 代码重复多处存在相似的AI调用代码
  • 职责分散AI相关逻辑散落在不同层级
  • 难以维护修改AI调用逻辑需要改动多个文件
  • 测试困难需要在多个测试类中Mock IOpenAiService

重构方案

1. 扩展 SmartHandleService 接口

Service/AI/SmartHandleService.cs 中新增以下方法:

public interface ISmartHandleService
{
    // 原有方法
    Task SmartClassifyAsync(long[] transactionIds, Action<(string type, string data)> chunkAction);
    Task AnalyzeBillAsync(string userInput, Action<string> chunkAction);
    Task<TransactionParseResult?> ParseOneLineBillAsync(string text);

    // 新增方法
    
    /// <summary>
    /// 从邮件正文中使用AI提取交易记录AI兜底方案
    /// </summary>
    Task<(string card, string reason, decimal amount, decimal balance, TransactionType type, DateTime? occurredAt)[]?> 
        ParseEmailByAiAsync(string emailBody);

    /// <summary>
    /// 为分类生成多个SVG图标定时任务使用
    /// </summary>
    Task<List<string>?> GenerateCategoryIconsAsync(string categoryName, TransactionType categoryType, int iconCount = 5);

    /// <summary>
    /// 为分类生成单个SVG图标手动触发使用
    /// </summary>
    Task<string?> GenerateSingleCategoryIconAsync(string categoryName, TransactionType categoryType);

    /// <summary>
    /// 生成预算执行报告HTML格式
    /// </summary>
    Task<string?> GenerateBudgetReportAsync(string promptWithData, int year, int month);
}

2. 实现新增方法

ParseEmailByAiAsync - 邮件解析

EmailParseServicesBase.ParseByAiAsync 方法迁移到 SmartHandleService

  • 保留完整的JSON解析逻辑
  • 保留markdown代码块清理逻辑
  • 保留单个对象兼容处理
  • 将类型判断逻辑保留在基类中供子类使用

GenerateCategoryIconsAsync - 批量图标生成

CategoryIconGenerationJob.GenerateIconsForCategoryAsync 的核心逻辑迁移:

  • 封装5种不同风格的图标生成Prompt
  • 处理JSON数组解析和验证
  • 统一的错误处理和日志记录

GenerateSingleCategoryIconAsync - 单个图标生成

TransactionCategoryApplication.GenerateIconAsync 的核心逻辑迁移:

  • 简化的极简风格Prompt
  • SVG标签提取逻辑
  • 统一的返回格式

GenerateBudgetReportAsync - 预算报告生成

BudgetService 中的报告生成逻辑迁移:

  • 接收完整的Prompt包含数据和格式要求
  • 统一的HTML格式要求
  • 统一的错误处理

3. 更新调用方

EmailParseServicesBase

// 修改前
public abstract class EmailParseServicesBase(
    ILogger<EmailParseServicesBase> logger,
    IOpenAiService openAiService
) : IEmailParseServices

// 修改后
public abstract class EmailParseServicesBase(
    ILogger<EmailParseServicesBase> logger,
    ISmartHandleService smartHandleService
) : IEmailParseServices

调用改为:

result = await smartHandleService.ParseEmailByAiAsync(emailContent) ?? [];

CategoryIconGenerationJob

// 修改前
public class CategoryIconGenerationJob(
    ITransactionCategoryRepository categoryRepository,
    IOpenAiService openAiService,
    ILogger<CategoryIconGenerationJob> logger) : IJob

// 修改后
public class CategoryIconGenerationJob(
    ITransactionCategoryRepository categoryRepository,
    ISmartHandleService smartHandleService,
    ILogger<CategoryIconGenerationJob> logger) : IJob

调用简化为:

var icons = await smartHandleService.GenerateCategoryIconsAsync(category.Name, category.Type, iconCount: 5);

TransactionCategoryApplication

// 修改前
public class TransactionCategoryApplication(
    ...
    IOpenAiService openAiService,
    ...) : ITransactionCategoryApplication

// 修改后
public class TransactionCategoryApplication(
    ...
    ISmartHandleService smartHandleService,
    ...) : ITransactionCategoryApplication

调用简化为:

var svg = await smartHandleService.GenerateSingleCategoryIconAsync(category.Name, category.Type);

BudgetService

// 修改前
public class BudgetService(
    ...
    IOpenAiService openAiService,
    ...) : IBudgetService

// 修改后
public class BudgetService(
    ...
    ISmartHandleService smartHandleService,
    ...) : IBudgetService

调用简化为:

var htmlReport = await smartHandleService.GenerateBudgetReportAsync(dataPrompt, year, month);

4. 更新测试代码

更新所有测试类,将 IOpenAiService 的Mock改为 ISmartHandleService

  • BudgetStatsTest.cs
  • TransactionCategoryApplicationTest.cs

重构收益

1. 代码集中管理

  • 所有AI调用逻辑集中在 SmartHandleService
  • 便于统一修改Prompt策略
  • 便于添加新的AI功能

2. 职责清晰

  • OpenAiService底层AI API调用同步/流式)
  • SmartHandleService业务级AI功能封装
  • 业务服务层专注业务逻辑无需关心AI调用细节

3. 易于测试

  • 只需Mock ISmartHandleService
  • 测试更简洁Mock对象更少
  • 可以为 SmartHandleService 编写独立的单元测试

4. 易于扩展

  • 新增AI功能只需在 SmartHandleService 中添加方法
  • 不影响现有业务代码
  • 符合开闭原则(对扩展开放,对修改封闭)

5. 代码复用

  • 消除了多处重复的AI调用代码
  • 统一的错误处理和日志记录
  • 统一的返回格式和验证逻辑

测试结果

重构后运行全部211个单元测试全部通过

已通过! - 失败: 0通过: 211已跳过: 0总计: 211持续时间: 22 s

受影响的文件清单

新增/修改的文件

  1. Service/AI/SmartHandleService.cs
    • 新增4个方法接口定义
    • 新增4个方法实现
    • 新增辅助方法:ParseEmailSingleRecord, DetermineTransactionType

修改的业务服务

  1. Service/EmailServices/EmailParse/IEmailParseServices.cs

    • 构造函数参数改为 ISmartHandleService
    • 移除 ParseByAiAsync 方法,改为调用 smartHandleService.ParseEmailByAiAsync
    • 保留 DetermineTransactionType 为protected方法供子类使用
  2. Service/EmailServices/EmailParse/EmailParseFormCcsvc.cs

    • 构造函数参数改为 ISmartHandleService
  3. Service/EmailServices/EmailParse/EmailParseForm95555.cs

    • 构造函数参数改为 ISmartHandleService
  4. Service/Jobs/CategoryIconGenerationJob.cs

    • 构造函数参数改为 ISmartHandleService
    • 简化 GenerateIconsForCategoryAsync 方法
  5. Application/TransactionCategoryApplication.cs

    • 构造函数参数改为 ISmartHandleService
    • 简化 GenerateIconAsync 方法
  6. Service/Budget/BudgetService.cs

    • 构造函数参数改为 ISmartHandleService
    • 简化报告生成调用

修改的测试文件

  1. WebApi.Test/Budget/BudgetStatsTest.cs

    • Mock对象改为 ISmartHandleService
  2. WebApi.Test/Application/TransactionCategoryApplicationTest.cs

    • Mock对象改为 ISmartHandleService

架构图

┌─────────────────────────────────────────────────────────────┐
│                     业务层 (Business Layer)                  │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ BudgetService│  │CategoryApp   │  │EmailParse    │      │
│  │              │  │              │  │Services      │      │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘      │
│         │                 │                 │              │
│         └─────────────────┼─────────────────┘              │
│                           │                                │
└───────────────────────────┼────────────────────────────────┘
                            │ 依赖
┌───────────────────────────┼────────────────────────────────┐
│              AI 服务层 (AI Service Layer)                   │
│                           │                                │
│                   ┌───────▼────────┐                        │
│                   │SmartHandleService│ ◄── 统一封装AI调用    │
│                   │  (业务级AI功能)   │                      │
│                   └───────┬────────┘                        │
│                           │                                │
│                   ┌───────▼────────┐                        │
│                   │ OpenAiService  │ ◄── 底层API调用        │
│                   │ (HTTP Client)  │                        │
│                   └────────────────┘                        │
└─────────────────────────────────────────────────────────────┘

后续建议

  1. 添加单元测试:为 SmartHandleService 的新方法添加独立的单元测试
  2. 监控日志关注AI调用的性能和错误日志
  3. Prompt优化基于实际使用反馈持续优化Prompt
  4. 缓存机制:考虑为图标生成等场景添加缓存
  5. 重试机制考虑为AI调用添加重试逻辑

总结

本次重构成功将项目中所有AI调用统一封装到 SmartHandleService,实现了代码的集中管理和职责分离。重构后的代码更易维护、更易测试、更易扩展,符合单一职责原则和依赖倒置原则。所有测试用例全部通过,证明重构没有引入功能回归问题。