Files
EmailBill/WebApi/Controllers/TransactionCategoryController.cs
SunCheng 63aaaf39c5
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 19s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
fix
2026-02-04 19:23:07 +08:00

413 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Service.AI;
namespace WebApi.Controllers;
[ApiController]
[Route("api/[controller]/[action]")]
public class TransactionCategoryController(
ITransactionCategoryRepository categoryRepository,
ITransactionRecordRepository transactionRecordRepository,
ILogger<TransactionCategoryController> logger,
IBudgetRepository budgetRepository,
IOpenAiService openAiService
) : ControllerBase
{
/// <summary>
/// 获取分类列表(支持按类型筛选)
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<TransactionCategory>>> GetListAsync([FromQuery] TransactionType? type = null)
{
try
{
List<TransactionCategory> categories;
if (type.HasValue)
{
categories = await categoryRepository.GetCategoriesByTypeAsync(type.Value);
}
else
{
categories = (await categoryRepository.GetAllAsync()).ToList();
}
return categories.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取分类列表失败");
return $"获取分类列表失败: {ex.Message}".Fail<List<TransactionCategory>>();
}
}
/// <summary>
/// 根据ID获取分类详情
/// </summary>
[HttpGet("{id}")]
public async Task<BaseResponse<TransactionCategory>> GetByIdAsync(long id)
{
try
{
var category = await categoryRepository.GetByIdAsync(id);
if (category == null)
{
return "分类不存在".Fail<TransactionCategory>();
}
return category.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取分类详情失败, Id: {Id}", id);
return $"获取分类详情失败: {ex.Message}".Fail<TransactionCategory>();
}
}
/// <summary>
/// 创建分类
/// </summary>
[HttpPost]
public async Task<BaseResponse<long>> CreateAsync([FromBody] CreateCategoryDto dto)
{
try
{
// 检查同名分类
var existing = await categoryRepository.GetByNameAndTypeAsync(dto.Name, dto.Type);
if (existing != null)
{
return "已存在相同名称的分类".Fail<long>();
}
var category = new TransactionCategory
{
Name = dto.Name,
Type = dto.Type
};
var result = await categoryRepository.AddAsync(category);
if (result)
{
return category.Id.Ok();
}
return "创建分类失败".Fail<long>();
}
catch (Exception ex)
{
logger.LogError(ex, "创建分类失败, Dto: {@Dto}", dto);
return $"创建分类失败: {ex.Message}".Fail<long>();
}
}
/// <summary>
/// 更新分类
/// </summary>
[HttpPost]
public async Task<BaseResponse> UpdateAsync([FromBody] UpdateCategoryDto dto)
{
try
{
var category = await categoryRepository.GetByIdAsync(dto.Id);
if (category == null)
{
return "分类不存在".Fail();
}
// 如果修改了名称,检查同名
if (category.Name != dto.Name)
{
var existing = await categoryRepository.GetByNameAndTypeAsync(dto.Name, category.Type);
if (existing != null && existing.Id != dto.Id)
{
return "已存在相同名称的分类".Fail();
}
// 同步更新交易记录中的分类名称
await transactionRecordRepository.UpdateCategoryNameAsync(category.Name, dto.Name, category.Type);
await budgetRepository.UpdateBudgetCategoryNameAsync(category.Name, dto.Name, category.Type);
}
category.Name = dto.Name;
category.UpdateTime = DateTime.Now;
var success = await categoryRepository.UpdateAsync(category);
if (success)
{
return "更新分类成功".Ok();
}
return "更新分类失败".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "更新分类失败, Dto: {@Dto}", dto);
return $"更新分类失败: {ex.Message}".Fail();
}
}
/// <summary>
/// 删除分类
/// </summary>
[HttpPost]
public async Task<BaseResponse> DeleteAsync([FromQuery] long id)
{
try
{
// 检查是否被使用
var inUse = await categoryRepository.IsCategoryInUseAsync(id);
if (inUse)
{
return "该分类已被使用,无法删除".Fail();
}
var success = await categoryRepository.DeleteAsync(id);
if (success)
{
return BaseResponse.Done();
}
return "删除分类失败,分类不存在".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "删除分类失败, Id: {Id}", id);
return $"删除分类失败: {ex.Message}".Fail();
}
}
/// <summary>
/// 批量创建分类(用于初始化)
/// </summary>
[HttpPost]
public async Task<BaseResponse<int>> BatchCreateAsync([FromBody] List<CreateCategoryDto> dtoList)
{
try
{
var categories = dtoList.Select(dto => new TransactionCategory
{
Name = dto.Name,
Type = dto.Type
}).ToList();
var result = await categoryRepository.AddRangeAsync(categories);
if (result)
{
return categories.Count.Ok();
}
return "批量创建分类失败".Fail<int>();
}
catch (Exception ex)
{
logger.LogError(ex, "批量创建分类失败, Count: {Count}", dtoList.Count);
return $"批量创建分类失败: {ex.Message}".Fail<int>();
}
}
/// <summary>
/// 为指定分类生成新的SVG图标
/// </summary>
[HttpPost]
public async Task<BaseResponse<string>> GenerateIconAsync([FromBody] GenerateIconDto dto)
{
try
{
var category = await categoryRepository.GetByIdAsync(dto.CategoryId);
if (category == null)
{
return "分类不存在".Fail<string>();
}
// 使用AI生成简洁、风格鲜明的SVG图标
var systemPrompt = @"你是一个专业的SVG图标设计师。你的任务是为预算分类生成极简风格、视觉识别度高的SVG图标。
## 核心设计原则
1. **语义相关性**:图标必须直观反映分类本质。例如:
- 「餐饮」→ 餐具、碗筷或热腾腾的食物
- 「交通」→ 汽车、地铁或公交车
- 「购物」→ 购物袋或购物车
- 「娱乐」→ 电影票、游戏手柄或麦克风
- 「医疗」→ 十字架或药丸
- 「工资」→ 钱袋或上升箭头
2. **极简风格**
- 线条简洁流畅,避免复杂细节
- 使用几何图形和圆润的边角
- 2-4个主要形状元素即可
- 笔画粗细统一stroke-width: 2
3. **视觉识别**
- 轮廓清晰,一眼能认出是什么
- 避免抽象符号,优先具象图形
- 留白合理,图标不要过于密集
## 技术规范
- viewBox=""0 0 24 24""
- 尺寸为 24×24
- 使用单色fill=""currentColor"" 或 stroke=""currentColor""
- 优先使用 stroke描边而非 fill填充更显轻盈
- stroke-width=""2"" stroke-linecap=""round"" stroke-linejoin=""round""
- 只返回 <svg> 标签及其内容,不要其他说明
## 回退方案
如果该分类实在无法用具象图形表达(如「其他」「杂项」等),则生成包含该分类**首字**的文字图标:
```xml
<svg viewBox=""0 0 24 24"" fill=""none"" xmlns=""http://www.w3.org/2000/svg"">
<circle cx=""12"" cy=""12"" r=""10"" stroke=""currentColor"" stroke-width=""2""/>
<text x=""12"" y=""16"" font-size=""12"" font-weight=""bold"" text-anchor=""middle"" fill=""currentColor"">{首字}</text>
</svg>
```
## 示例
**好的图标**
- 「咖啡」→ 咖啡杯+热气
- 「房租」→ 房子外轮廓
- 「健身」→ 哑铃
**差的图标**
- 过于复杂的写实风格
- 无法识别的抽象符号
- 图形过小或过密";
var transactionTypeDesc = category.Type switch
{
TransactionType.Expense => "支出",
TransactionType.Income => "收入",
_ => "不计收支"
};
var userPrompt = $@"请为「{category.Name}」分类生成图标({transactionTypeDesc}类别)。
要求:
1. 分析这个分类的核心含义
2. 选择最具代表性的视觉元素
3. 用极简线条勾勒出图标(优先使用 stroke 描边风格)
4. 如果实在无法用图形表达,则生成包含「{(category.Name.Length > 0 ? category.Name[0] : '?')}」的文字图标
直接返回SVG代码无需解释。";
var svgContent = await openAiService.ChatAsync(systemPrompt, userPrompt, timeoutSeconds: 30);
if (string.IsNullOrWhiteSpace(svgContent))
{
return "AI生成图标失败".Fail<string>();
}
// 提取SVG标签内容
var svgMatch = System.Text.RegularExpressions.Regex.Match(svgContent, @"<svg[^>]*>.*?</svg>",
System.Text.RegularExpressions.RegexOptions.Singleline);
if (!svgMatch.Success)
{
return "生成的内容不包含有效的SVG标签".Fail<string>();
}
var svg = svgMatch.Value;
// 解析现有图标数组
var icons = string.IsNullOrWhiteSpace(category.Icon)
? new List<string>()
: JsonSerializer.Deserialize<List<string>>(category.Icon) ?? new List<string>();
// 添加新图标
icons.Add(svg);
// 更新数据库
category.Icon = JsonSerializer.Serialize(icons);
category.UpdateTime = DateTime.Now;
var success = await categoryRepository.UpdateAsync(category);
if (!success)
{
return "更新分类图标失败".Fail<string>();
}
return svg.Ok<string>();
}
catch (Exception ex)
{
logger.LogError(ex, "生成图标失败, CategoryId: {CategoryId}", dto.CategoryId);
return $"生成图标失败: {ex.Message}".Fail<string>();
}
}
/// <summary>
/// 更新分类的选中图标索引
/// </summary>
[HttpPost]
public async Task<BaseResponse> UpdateSelectedIconAsync([FromBody] UpdateSelectedIconDto dto)
{
try
{
var category = await categoryRepository.GetByIdAsync(dto.CategoryId);
if (category == null)
{
return "分类不存在".Fail();
}
// 验证索引有效性
if (string.IsNullOrWhiteSpace(category.Icon))
{
return "该分类没有可用图标".Fail();
}
var icons = JsonSerializer.Deserialize<List<string>>(category.Icon);
if (icons == null || dto.SelectedIndex < 0 || dto.SelectedIndex >= icons.Count)
{
return "无效的图标索引".Fail();
}
// 这里可以添加一个SelectedIconIndex字段到实体中或者将选中的图标移到数组第一位
// 暂时采用移动到第一位的方式
var selectedIcon = icons[dto.SelectedIndex];
icons.RemoveAt(dto.SelectedIndex);
icons.Insert(0, selectedIcon);
category.Icon = JsonSerializer.Serialize(icons);
category.UpdateTime = DateTime.Now;
var success = await categoryRepository.UpdateAsync(category);
if (success)
{
return "更新图标成功".Ok();
}
return "更新图标失败".Fail();
}
catch (Exception ex)
{
logger.LogError(ex, "更新选中图标失败, Dto: {@Dto}", dto);
return $"更新选中图标失败: {ex.Message}".Fail();
}
}
}
/// <summary>
/// 创建分类DTO
/// </summary>
public record CreateCategoryDto(
string Name,
TransactionType Type
);
/// <summary>
/// 更新分类DTO
/// </summary>
public record UpdateCategoryDto(
long Id,
string Name
);
/// <summary>
/// 生成图标DTO
/// </summary>
public record GenerateIconDto(
long CategoryId
);
/// <summary>
/// 更新选中图标DTO
/// </summary>
public record UpdateSelectedIconDto(
long CategoryId,
int SelectedIndex
);