Files
EmailBill/.doc/ai-service-refactoring-summary.md

331 lines
11 KiB
Markdown
Raw Permalink Normal View History

2026-02-10 17:49:19 +08:00
# 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` 中新增以下方法:
```csharp
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
```csharp
// 修改前
public abstract class EmailParseServicesBase(
ILogger<EmailParseServicesBase> logger,
IOpenAiService openAiService
) : IEmailParseServices
// 修改后
public abstract class EmailParseServicesBase(
ILogger<EmailParseServicesBase> logger,
ISmartHandleService smartHandleService
) : IEmailParseServices
```
调用改为:
```csharp
result = await smartHandleService.ParseEmailByAiAsync(emailContent) ?? [];
```
#### CategoryIconGenerationJob
```csharp
// 修改前
public class CategoryIconGenerationJob(
ITransactionCategoryRepository categoryRepository,
IOpenAiService openAiService,
ILogger<CategoryIconGenerationJob> logger) : IJob
// 修改后
public class CategoryIconGenerationJob(
ITransactionCategoryRepository categoryRepository,
ISmartHandleService smartHandleService,
ILogger<CategoryIconGenerationJob> logger) : IJob
```
调用简化为:
```csharp
var icons = await smartHandleService.GenerateCategoryIconsAsync(category.Name, category.Type, iconCount: 5);
```
#### TransactionCategoryApplication
```csharp
// 修改前
public class TransactionCategoryApplication(
...
IOpenAiService openAiService,
...) : ITransactionCategoryApplication
// 修改后
public class TransactionCategoryApplication(
...
ISmartHandleService smartHandleService,
...) : ITransactionCategoryApplication
```
调用简化为:
```csharp
var svg = await smartHandleService.GenerateSingleCategoryIconAsync(category.Name, category.Type);
```
#### BudgetService
```csharp
// 修改前
public class BudgetService(
...
IOpenAiService openAiService,
...) : IBudgetService
// 修改后
public class BudgetService(
...
ISmartHandleService smartHandleService,
...) : IBudgetService
```
调用简化为:
```csharp
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`
### 修改的业务服务
2. **Service/EmailServices/EmailParse/IEmailParseServices.cs**
- 构造函数参数改为 `ISmartHandleService`
- 移除 `ParseByAiAsync` 方法,改为调用 `smartHandleService.ParseEmailByAiAsync`
- 保留 `DetermineTransactionType` 为protected方法供子类使用
3. **Service/EmailServices/EmailParse/EmailParseFormCcsvc.cs**
- 构造函数参数改为 `ISmartHandleService`
4. **Service/EmailServices/EmailParse/EmailParseForm95555.cs**
- 构造函数参数改为 `ISmartHandleService`
5. **Service/Jobs/CategoryIconGenerationJob.cs**
- 构造函数参数改为 `ISmartHandleService`
- 简化 `GenerateIconsForCategoryAsync` 方法
6. **Application/TransactionCategoryApplication.cs**
- 构造函数参数改为 `ISmartHandleService`
- 简化 `GenerateIconAsync` 方法
7. **Service/Budget/BudgetService.cs**
- 构造函数参数改为 `ISmartHandleService`
- 简化报告生成调用
### 修改的测试文件
8. **WebApi.Test/Budget/BudgetStatsTest.cs**
- Mock对象改为 `ISmartHandleService`
9. **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`,实现了代码的集中管理和职责分离。重构后的代码更易维护、更易测试、更易扩展,符合单一职责原则和依赖倒置原则。所有测试用例全部通过,证明重构没有引入功能回归问题。