fix
This commit is contained in:
@@ -1,147 +1,52 @@
|
||||
using Service.AI;
|
||||
using Application;
|
||||
using Application.Dto.Category;
|
||||
|
||||
namespace WebApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]/[action]")]
|
||||
public class TransactionCategoryController(
|
||||
ITransactionCategoryRepository categoryRepository,
|
||||
ITransactionRecordRepository transactionRecordRepository,
|
||||
ILogger<TransactionCategoryController> logger,
|
||||
IBudgetRepository budgetRepository,
|
||||
IOpenAiService openAiService
|
||||
ITransactionCategoryApplication categoryApplication
|
||||
) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取分类列表(支持按类型筛选)
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<BaseResponse<List<TransactionCategory>>> GetListAsync([FromQuery] TransactionType? type = null)
|
||||
public async Task<BaseResponse<List<CategoryResponse>>> 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>>();
|
||||
}
|
||||
var categories = await categoryApplication.GetListAsync(type);
|
||||
return categories.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取分类详情
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<BaseResponse<TransactionCategory>> GetByIdAsync(long id)
|
||||
public async Task<BaseResponse<CategoryResponse>> 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>();
|
||||
}
|
||||
var category = await categoryApplication.GetByIdAsync(id);
|
||||
return category.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建分类
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<BaseResponse<long>> CreateAsync([FromBody] CreateCategoryDto dto)
|
||||
public async Task<BaseResponse<long>> CreateAsync([FromBody] CreateCategoryRequest request)
|
||||
{
|
||||
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>();
|
||||
}
|
||||
var categoryId = await categoryApplication.CreateAsync(request);
|
||||
return categoryId.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新分类
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<BaseResponse> UpdateAsync([FromBody] UpdateCategoryDto dto)
|
||||
public async Task<BaseResponse> UpdateAsync([FromBody] UpdateCategoryRequest request)
|
||||
{
|
||||
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();
|
||||
}
|
||||
await categoryApplication.UpdateAsync(request);
|
||||
return "更新分类成功".Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -150,263 +55,37 @@ public class TransactionCategoryController(
|
||||
[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();
|
||||
}
|
||||
await categoryApplication.DeleteAsync(id);
|
||||
return BaseResponse.Done();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量创建分类(用于初始化)
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<BaseResponse<int>> BatchCreateAsync([FromBody] List<CreateCategoryDto> dtoList)
|
||||
public async Task<BaseResponse<int>> BatchCreateAsync([FromBody] List<CreateCategoryRequest> requests)
|
||||
{
|
||||
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>();
|
||||
}
|
||||
var count = await categoryApplication.BatchCreateAsync(requests);
|
||||
return count.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为指定分类生成新的SVG图标
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<BaseResponse<string>> GenerateIconAsync([FromBody] GenerateIconDto dto)
|
||||
public async Task<BaseResponse<string>> GenerateIconAsync([FromBody] GenerateIconRequest request)
|
||||
{
|
||||
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>();
|
||||
}
|
||||
var svg = await categoryApplication.GenerateIconAsync(request);
|
||||
return svg.Ok<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新分类的选中图标索引
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<BaseResponse> UpdateSelectedIconAsync([FromBody] UpdateSelectedIconDto dto)
|
||||
public async Task<BaseResponse> UpdateSelectedIconAsync([FromBody] UpdateSelectedIconRequest request)
|
||||
{
|
||||
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();
|
||||
}
|
||||
await categoryApplication.UpdateSelectedIconAsync(request);
|
||||
return "更新图标成功".Ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user