354 lines
11 KiB
C#
354 lines
11 KiB
C#
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. 只返回<svg>标签及其内容,不要其他说明文字
|
||
2. viewBox=""0 0 24 24""
|
||
3. 尺寸为24x24
|
||
4. 使用单色,fill=""currentColor""
|
||
5. 简洁的设计,适合作为应用图标";
|
||
|
||
var userPrompt = $"为分类"{category.Name}"({(category.Type == TransactionType.Expense ? "支出" : category.Type == TransactionType.Income ? "收入" : "不计收支")})生成一个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();
|
||
}
|
||
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
|
||
);
|