添加功能
This commit is contained in:
@@ -29,5 +29,8 @@
|
|||||||
<!-- Job Scheduling -->
|
<!-- Job Scheduling -->
|
||||||
<PackageVersion Include="Quartz" Version="3.13.1" />
|
<PackageVersion Include="Quartz" Version="3.13.1" />
|
||||||
<PackageVersion Include="Quartz.Extensions.Hosting" Version="3.13.1" />
|
<PackageVersion Include="Quartz.Extensions.Hosting" Version="3.13.1" />
|
||||||
|
<!-- Text Processing -->
|
||||||
|
<PackageVersion Include="JiebaNet.Analyser" Version="1.0.6" />
|
||||||
|
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -16,6 +16,11 @@ public interface IBaseRepository<T> where T : BaseEntity
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task<T?> GetByIdAsync(long id);
|
Task<T?> GetByIdAsync(long id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据ID获取单条数据
|
||||||
|
/// </summary>
|
||||||
|
Task<T[]> GetByIdsAsync(long[] ids);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 添加数据
|
/// 添加数据
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -76,6 +81,19 @@ public abstract class BaseRepository<T>(IFreeSql freeSql) : IBaseRepository<T> w
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual async Task<T[]> GetByIdsAsync(long[] ids)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await FreeSql.Select<T>().Where(x => ids.Contains(x.Id)).ToListAsync();
|
||||||
|
return result.ToArray();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public virtual async Task<bool> AddAsync(T entity)
|
public virtual async Task<bool> AddAsync(T entity)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -149,6 +149,23 @@ public interface ITransactionRecordRepository : IBaseRepository<TransactionRecor
|
|||||||
/// <param name="completeSql">完整的SELECT SQL语句</param>
|
/// <param name="completeSql">完整的SELECT SQL语句</param>
|
||||||
/// <returns>动态查询结果列表</returns>
|
/// <returns>动态查询结果列表</returns>
|
||||||
Task<List<dynamic>> ExecuteDynamicSqlAsync(string completeSql);
|
Task<List<dynamic>> ExecuteDynamicSqlAsync(string completeSql);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据关键词查询已分类的账单(用于智能分类参考)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keywords">关键词列表</param>
|
||||||
|
/// <param name="limit">返回结果数量限制</param>
|
||||||
|
/// <returns>已分类的账单列表</returns>
|
||||||
|
Task<List<TransactionRecord>> GetClassifiedByKeywordsAsync(List<string> keywords, int limit = 10);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据关键词查询已分类的账单,并计算相关度分数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keywords">关键词列表</param>
|
||||||
|
/// <param name="minMatchRate">最小匹配率(0.0-1.0),默认0.3表示至少匹配30%的关键词</param>
|
||||||
|
/// <param name="limit">返回结果数量限制</param>
|
||||||
|
/// <returns>带相关度分数的已分类账单列表</returns>
|
||||||
|
Task<List<(TransactionRecord record, double relevanceScore)>> GetClassifiedByKeywordsWithScoreAsync(List<string> keywords, double minMatchRate = 0.3, int limit = 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<TransactionRecord>(freeSql), ITransactionRecordRepository
|
public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<TransactionRecord>(freeSql), ITransactionRecordRepository
|
||||||
@@ -344,7 +361,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
|||||||
|
|
||||||
public async Task<(List<ReasonGroupDto> list, int total)> GetReasonGroupsAsync(int pageIndex = 1, int pageSize = 20)
|
public async Task<(List<ReasonGroupDto> list, int total)> GetReasonGroupsAsync(int pageIndex = 1, int pageSize = 20)
|
||||||
{
|
{
|
||||||
// 先按照Reason分组,统计每个Reason的数量
|
// 先按照Reason分组,统计每个Reason的数量和总金额
|
||||||
var groups = await FreeSql.Select<TransactionRecord>()
|
var groups = await FreeSql.Select<TransactionRecord>()
|
||||||
.Where(t => !string.IsNullOrEmpty(t.Reason))
|
.Where(t => !string.IsNullOrEmpty(t.Reason))
|
||||||
.Where(t => string.IsNullOrEmpty(t.Classify)) // 只统计未分类的
|
.Where(t => string.IsNullOrEmpty(t.Classify)) // 只统计未分类的
|
||||||
@@ -352,11 +369,12 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
|||||||
.ToListAsync(g => new
|
.ToListAsync(g => new
|
||||||
{
|
{
|
||||||
Reason = g.Key,
|
Reason = g.Key,
|
||||||
Count = g.Count()
|
Count = g.Count(),
|
||||||
|
TotalAmount = g.Sum(g.Value.Amount)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 按数量降序排序
|
// 按总金额绝对值降序排序
|
||||||
var sortedGroups = groups.OrderByDescending(g => g.Count).ToList();
|
var sortedGroups = groups.OrderByDescending(g => Math.Abs(g.TotalAmount)).ToList();
|
||||||
var total = sortedGroups.Count;
|
var total = sortedGroups.Count;
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
@@ -365,22 +383,27 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
|||||||
.Take(pageSize)
|
.Take(pageSize)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// 为每个分组获取示例记录
|
// 为每个分组获取详细信息
|
||||||
var result = new List<ReasonGroupDto>();
|
var result = new List<ReasonGroupDto>();
|
||||||
foreach (var group in pagedGroups)
|
foreach (var group in pagedGroups)
|
||||||
{
|
{
|
||||||
var sample = await FreeSql.Select<TransactionRecord>()
|
// 获取该分组的所有记录
|
||||||
|
var records = await FreeSql.Select<TransactionRecord>()
|
||||||
.Where(t => t.Reason == group.Reason)
|
.Where(t => t.Reason == group.Reason)
|
||||||
.FirstAsync();
|
.Where(t => string.IsNullOrEmpty(t.Classify))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
if (sample != null)
|
if (records.Count > 0)
|
||||||
{
|
{
|
||||||
|
var sample = records.First();
|
||||||
result.Add(new ReasonGroupDto
|
result.Add(new ReasonGroupDto
|
||||||
{
|
{
|
||||||
Reason = group.Reason,
|
Reason = group.Reason,
|
||||||
Count = (int)group.Count,
|
Count = (int)group.Count,
|
||||||
SampleType = sample.Type,
|
SampleType = sample.Type,
|
||||||
SampleClassify = sample.Classify ?? string.Empty
|
SampleClassify = sample.Classify ?? string.Empty,
|
||||||
|
TransactionIds = records.Select(r => r.Id).ToList(),
|
||||||
|
TotalAmount = Math.Abs(group.TotalAmount)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -542,6 +565,68 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
|||||||
|
|
||||||
return trends;
|
return trends;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<TransactionRecord>> GetClassifiedByKeywordsAsync(List<string> keywords, int limit = 10)
|
||||||
|
{
|
||||||
|
if (keywords == null || keywords.Count == 0)
|
||||||
|
{
|
||||||
|
return new List<TransactionRecord>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = FreeSql.Select<TransactionRecord>()
|
||||||
|
.Where(t => t.Classify != ""); // 只查询已分类的账单
|
||||||
|
|
||||||
|
// 构建OR条件:Reason包含任意一个关键词
|
||||||
|
if (keywords.Count > 0)
|
||||||
|
{
|
||||||
|
query = query.Where(t => keywords.Any(keyword => t.Reason.Contains(keyword)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query
|
||||||
|
.OrderByDescending(t => t.OccurredAt)
|
||||||
|
.Limit(limit)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<(TransactionRecord record, double relevanceScore)>> GetClassifiedByKeywordsWithScoreAsync(List<string> keywords, double minMatchRate = 0.3, int limit = 10)
|
||||||
|
{
|
||||||
|
if (keywords == null || keywords.Count == 0)
|
||||||
|
{
|
||||||
|
return new List<(TransactionRecord, double)>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询所有已分类且包含任意关键词的账单
|
||||||
|
var candidates = await FreeSql.Select<TransactionRecord>()
|
||||||
|
.Where(t => t.Classify != "")
|
||||||
|
.Where(t => keywords.Any(keyword => t.Reason.Contains(keyword)))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// 计算每个候选账单的相关度分数
|
||||||
|
var scoredResults = candidates
|
||||||
|
.Select(record =>
|
||||||
|
{
|
||||||
|
var matchedCount = keywords.Count(keyword => record.Reason.Contains(keyword, StringComparison.OrdinalIgnoreCase));
|
||||||
|
var matchRate = (double)matchedCount / keywords.Count;
|
||||||
|
|
||||||
|
// 额外加分:完全匹配整个摘要(相似度更高)
|
||||||
|
var exactMatchBonus = keywords.Any(k => record.Reason.Equals(k, StringComparison.OrdinalIgnoreCase)) ? 0.2 : 0.0;
|
||||||
|
|
||||||
|
// 长度相似度加分:长度越接近,相关度越高
|
||||||
|
var avgKeywordLength = keywords.Average(k => k.Length);
|
||||||
|
var lengthSimilarity = 1.0 - Math.Min(1.0, Math.Abs(record.Reason.Length - avgKeywordLength) / Math.Max(record.Reason.Length, avgKeywordLength));
|
||||||
|
var lengthBonus = lengthSimilarity * 0.1;
|
||||||
|
|
||||||
|
var score = matchRate + exactMatchBonus + lengthBonus;
|
||||||
|
return (record, score);
|
||||||
|
})
|
||||||
|
.Where(x => x.score >= minMatchRate) // 过滤低相关度结果
|
||||||
|
.OrderByDescending(x => x.score) // 按相关度降序
|
||||||
|
.ThenByDescending(x => x.record.OccurredAt) // 相同分数时,按时间降序
|
||||||
|
.Take(limit)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return scoredResults;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -568,6 +653,16 @@ public class ReasonGroupDto
|
|||||||
/// 示例分类(该分组中第一条记录的分类)
|
/// 示例分类(该分组中第一条记录的分类)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SampleClassify { get; set; } = string.Empty;
|
public string SampleClassify { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 该分组的所有账单ID列表
|
||||||
|
/// </summary>
|
||||||
|
public List<long> TransactionIds { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 该分组的总金额(绝对值)
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalAmount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
<PackageReference Include="HtmlAgilityPack" />
|
<PackageReference Include="HtmlAgilityPack" />
|
||||||
<PackageReference Include="Quartz" />
|
<PackageReference Include="Quartz" />
|
||||||
<PackageReference Include="Quartz.Extensions.Hosting" />
|
<PackageReference Include="Quartz.Extensions.Hosting" />
|
||||||
|
<PackageReference Include="JiebaNet.Analyser" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
152
Service/TextSegmentService.cs
Normal file
152
Service/TextSegmentService.cs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
namespace Service;
|
||||||
|
|
||||||
|
using JiebaNet.Segmenter;
|
||||||
|
using JiebaNet.Analyser;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文本分词服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface ITextSegmentService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 从文本中提取关键词
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">待分析的文本</param>
|
||||||
|
/// <param name="topN">返回前N个关键词,默认5个</param>
|
||||||
|
/// <returns>关键词列表</returns>
|
||||||
|
List<string> ExtractKeywords(string text, int topN = 5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对文本进行分词
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">待分词的文本</param>
|
||||||
|
/// <returns>分词结果列表</returns>
|
||||||
|
List<string> Segment(string text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基于 JiebaNet 的文本分词服务实现
|
||||||
|
/// </summary>
|
||||||
|
public class TextSegmentService : ITextSegmentService
|
||||||
|
{
|
||||||
|
private readonly JiebaSegmenter _segmenter;
|
||||||
|
private readonly TfidfExtractor _extractor;
|
||||||
|
private readonly ILogger<TextSegmentService> _logger;
|
||||||
|
|
||||||
|
public TextSegmentService(ILogger<TextSegmentService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_segmenter = new JiebaSegmenter();
|
||||||
|
_extractor = new TfidfExtractor();
|
||||||
|
|
||||||
|
// 仅添加JiebaNet词典中可能缺失的特定业务词汇
|
||||||
|
AddCustomWords();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加自定义词典 - 仅添加JiebaNet词典中可能缺失的特定词汇
|
||||||
|
/// </summary>
|
||||||
|
private void AddCustomWords()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 只添加可能缺失的特定业务词汇
|
||||||
|
// 大部分常用词(如"美团"、"支付宝"等)JiebaNet已内置
|
||||||
|
var customWords = new[]
|
||||||
|
{
|
||||||
|
"水电费", "物业费", "燃气费" // 复合词,确保作为整体识别 // TODO 做成配置文件 让 AI定期提取复合词汇填入到这边
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var word in customWords)
|
||||||
|
{
|
||||||
|
_segmenter.AddWord(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customWords.Length > 0)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("已加载 {Count} 个自定义词汇", customWords.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "添加自定义词典失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> ExtractKeywords(string text, int topN = 5)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 使用 TF-IDF 算法提取关键词(已内置停用词过滤)
|
||||||
|
var keywords = _extractor.ExtractTags(text, topN, new List<string>());
|
||||||
|
|
||||||
|
// 过滤单字,保留有意义的词
|
||||||
|
var filteredKeywords = keywords
|
||||||
|
.Where(k => k.Length >= 2)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// 如果过滤后没有关键词,使用基础分词并选择最长的词
|
||||||
|
if (filteredKeywords.Count == 0)
|
||||||
|
{
|
||||||
|
var segments = Segment(text);
|
||||||
|
filteredKeywords = segments
|
||||||
|
.Where(s => s.Length >= 2)
|
||||||
|
.OrderByDescending(s => s.Length)
|
||||||
|
.Take(topN)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果还是没有,返回原文的前10个字符
|
||||||
|
if (filteredKeywords.Count == 0 && text.Length > 0)
|
||||||
|
{
|
||||||
|
filteredKeywords.Add(text.Length > 10 ? text.Substring(0, 10) : text);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("从文本 '{Text}' 中提取关键词: {Keywords}",
|
||||||
|
text, string.Join(", ", filteredKeywords));
|
||||||
|
|
||||||
|
return filteredKeywords;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "提取关键词失败,文本: {Text}", text);
|
||||||
|
// 降级处理:返回原文
|
||||||
|
return new List<string> { text.Length > 10 ? text.Substring(0, 10) : text };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> Segment(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 执行分词
|
||||||
|
var segments = _segmenter.Cut(text).ToList();
|
||||||
|
|
||||||
|
// 过滤空白和停用词
|
||||||
|
var filteredSegments = segments
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s) && s.Trim().Length > 0)
|
||||||
|
.Select(s => s.Trim())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return filteredSegments;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "分词失败,文本: {Text}", text);
|
||||||
|
return new List<string> { text };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Web/.eslintcache
Normal file
1
Web/.eslintcache
Normal file
File diff suppressed because one or more lines are too long
287
Web/src/components/SmartClassifyButton.vue
Normal file
287
Web/src/components/SmartClassifyButton.vue
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
<template>
|
||||||
|
<van-button
|
||||||
|
v-if="hasTransactions"
|
||||||
|
:type="hasClassifiedResults ? 'success' : 'primary'"
|
||||||
|
size="small"
|
||||||
|
:loading="loading || saving"
|
||||||
|
:disabled="loading || saving"
|
||||||
|
@click="handleClick"
|
||||||
|
class="smart-classify-btn"
|
||||||
|
>
|
||||||
|
<template v-if="!loading && !saving">
|
||||||
|
<van-icon :name="hasClassifiedResults ? 'success' : 'fire'" />
|
||||||
|
<span style="margin-left: 4px;">{{ hasClassifiedResults ? '保存分类' : '智能分类' }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ saving ? '保存中...' : '分类中...' }}
|
||||||
|
</template>
|
||||||
|
</van-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { showToast, closeToast } from 'vant'
|
||||||
|
import { smartClassify, batchUpdateClassify } from '@/api/transactionRecord'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
transactions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update', 'save'])
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const classifiedResults = ref([])
|
||||||
|
let toastInstance = null
|
||||||
|
|
||||||
|
const hasTransactions = computed(() => {
|
||||||
|
return props.transactions && props.transactions.length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasClassifiedResults = computed(() => {
|
||||||
|
return classifiedResults.value.length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击按钮处理
|
||||||
|
*/
|
||||||
|
const handleClick = () => {
|
||||||
|
if (hasClassifiedResults.value) {
|
||||||
|
handleSaveClassify()
|
||||||
|
} else {
|
||||||
|
handleSmartClassify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存分类结果
|
||||||
|
*/
|
||||||
|
const handleSaveClassify = async () => {
|
||||||
|
try {
|
||||||
|
saving.value = true
|
||||||
|
showToast({
|
||||||
|
message: '正在保存...',
|
||||||
|
duration: 0,
|
||||||
|
forbidClick: true,
|
||||||
|
loadingType: 'spinner'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 准备批量更新数据
|
||||||
|
const items = classifiedResults.value.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
classify: item.classify,
|
||||||
|
type: item.type
|
||||||
|
}))
|
||||||
|
|
||||||
|
const response = await batchUpdateClassify(items)
|
||||||
|
|
||||||
|
closeToast()
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
showToast({
|
||||||
|
type: 'success',
|
||||||
|
message: `保存成功,已更新 ${items.length} 条记录`,
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清空已分类结果
|
||||||
|
classifiedResults.value = []
|
||||||
|
|
||||||
|
// 通知父组件刷新数据
|
||||||
|
emit('save')
|
||||||
|
} else {
|
||||||
|
showToast({
|
||||||
|
type: 'fail',
|
||||||
|
message: response.message || '保存失败',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存分类失败:', error)
|
||||||
|
closeToast()
|
||||||
|
showToast({
|
||||||
|
type: 'fail',
|
||||||
|
message: '保存失败,请重试',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理智能分类
|
||||||
|
*/
|
||||||
|
const handleSmartClassify = async () => {
|
||||||
|
if (!props.transactions || props.transactions.length === 0) {
|
||||||
|
showToast('没有可分类的交易记录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空之前的分类结果
|
||||||
|
classifiedResults.value = []
|
||||||
|
|
||||||
|
const transactionIds = props.transactions.map(t => t.id)
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
// 清除之前的Toast
|
||||||
|
if (toastInstance) {
|
||||||
|
closeToast()
|
||||||
|
}
|
||||||
|
|
||||||
|
toastInstance = showToast({
|
||||||
|
message: '正在智能分类...',
|
||||||
|
duration: 0,
|
||||||
|
forbidClick: true,
|
||||||
|
loadingType: 'spinner'
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await smartClassify(transactionIds)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('智能分类请求失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取流式响应
|
||||||
|
const reader = response.body.getReader()
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
let buffer = ''
|
||||||
|
let processedCount = 0
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read()
|
||||||
|
|
||||||
|
if (done) break
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
|
||||||
|
// 处理完整的事件(SSE格式:event: type\ndata: data\n\n)
|
||||||
|
const events = buffer.split('\n\n')
|
||||||
|
buffer = events.pop() || '' // 保留最后一个不完整的部分
|
||||||
|
|
||||||
|
for (const eventBlock of events) {
|
||||||
|
if (!eventBlock.trim()) continue
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lines = eventBlock.split('\n')
|
||||||
|
let eventType = ''
|
||||||
|
let eventData = ''
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('event: ')) {
|
||||||
|
eventType = line.slice(7).trim()
|
||||||
|
} else if (line.startsWith('data: ')) {
|
||||||
|
eventData = line.slice(6).trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'start') {
|
||||||
|
// 开始分类
|
||||||
|
closeToast()
|
||||||
|
toastInstance = showToast({
|
||||||
|
message: eventData,
|
||||||
|
duration: 0,
|
||||||
|
forbidClick: true,
|
||||||
|
loadingType: 'spinner'
|
||||||
|
})
|
||||||
|
} else if (eventType === 'data') {
|
||||||
|
// 收到分类结果
|
||||||
|
const data = JSON.parse(eventData)
|
||||||
|
processedCount++
|
||||||
|
|
||||||
|
// 记录分类结果
|
||||||
|
classifiedResults.value.push({
|
||||||
|
id: data.id,
|
||||||
|
classify: data.Classify,
|
||||||
|
type: data.Type
|
||||||
|
})
|
||||||
|
|
||||||
|
// 实时更新交易记录的分类信息
|
||||||
|
const index = props.transactions.findIndex(t => t.id === data.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
const transaction = props.transactions[index]
|
||||||
|
transaction.upsetedClassify = data.Classify
|
||||||
|
transaction.upsetedType = data.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度
|
||||||
|
closeToast()
|
||||||
|
toastInstance = showToast({
|
||||||
|
message: `已分类 ${processedCount} 条`,
|
||||||
|
duration: 0,
|
||||||
|
forbidClick: true,
|
||||||
|
loadingType: 'spinner'
|
||||||
|
})
|
||||||
|
} else if (eventType === 'end') {
|
||||||
|
// 分类完成
|
||||||
|
closeToast()
|
||||||
|
toastInstance = null
|
||||||
|
showToast({
|
||||||
|
type: 'success',
|
||||||
|
message: `分类完成,请点击"保存分类"按钮保存结果`,
|
||||||
|
duration: 3000
|
||||||
|
})
|
||||||
|
} else if (eventType === 'error') {
|
||||||
|
// 处理错误
|
||||||
|
closeToast()
|
||||||
|
toastInstance = null
|
||||||
|
showToast({
|
||||||
|
type: 'fail',
|
||||||
|
message: eventData || '分类失败',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析SSE事件失败:', e, eventBlock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('智能分类失败:', error)
|
||||||
|
closeToast()
|
||||||
|
toastInstance = null
|
||||||
|
showToast({
|
||||||
|
type: 'fail',
|
||||||
|
message: '智能分类失败,请重试',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
// 确保Toast被清除
|
||||||
|
if (toastInstance) {
|
||||||
|
setTimeout(() => {
|
||||||
|
closeToast()
|
||||||
|
toastInstance = null
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置组件状态
|
||||||
|
*/
|
||||||
|
const reset = () => {
|
||||||
|
classifiedResults.value = []
|
||||||
|
loading.value = false
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
reset
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.smart-classify-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
|
|
||||||
export function register() {
|
export function register() {
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
|
|||||||
@@ -17,15 +17,26 @@
|
|||||||
position="bottom"
|
position="bottom"
|
||||||
:style="{ height: '85%' }"
|
:style="{ height: '85%' }"
|
||||||
round
|
round
|
||||||
closeable
|
|
||||||
>
|
>
|
||||||
<div class="popup-container">
|
<div class="popup-container">
|
||||||
<div class="popup-header-fixed">
|
<div class="popup-header-fixed">
|
||||||
<h3>{{ selectedDateText }}</h3>
|
<van-icon
|
||||||
<p v-if="dateTransactions.length">
|
name="cross"
|
||||||
共 {{ dateTransactions.length }} 笔交易,
|
class="close-icon"
|
||||||
<span v-html="getBalance(dateTransactions)" />
|
@click="listVisible = false"
|
||||||
</p>
|
/>
|
||||||
|
<h3 class="date-title">{{ selectedDateText }}</h3>
|
||||||
|
<div class="header-stats">
|
||||||
|
<p v-if="dateTransactions.length">
|
||||||
|
共 {{ dateTransactions.length }} 笔交易,
|
||||||
|
<span v-html="getBalance(dateTransactions)" />
|
||||||
|
</p>
|
||||||
|
<SmartClassifyButton
|
||||||
|
ref="smartClassifyButtonRef"
|
||||||
|
:transactions="dateTransactions"
|
||||||
|
@save="onSmartClassifySave"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="popup-scroll-content">
|
<div class="popup-scroll-content">
|
||||||
@@ -56,6 +67,7 @@ import request from "@/api/request";
|
|||||||
import { getTransactionDetail, getTransactionsByDate } from "@/api/transactionRecord";
|
import { getTransactionDetail, getTransactionsByDate } from "@/api/transactionRecord";
|
||||||
import TransactionList from "@/components/TransactionList.vue";
|
import TransactionList from "@/components/TransactionList.vue";
|
||||||
import TransactionDetail from "@/components/TransactionDetail.vue";
|
import TransactionDetail from "@/components/TransactionDetail.vue";
|
||||||
|
import SmartClassifyButton from "@/components/SmartClassifyButton.vue";
|
||||||
|
|
||||||
const dailyStatistics = ref({});
|
const dailyStatistics = ref({});
|
||||||
const listVisible = ref(false);
|
const listVisible = ref(false);
|
||||||
@@ -107,6 +119,7 @@ const fetchDailyStatistics = async (year, month) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const smartClassifyButtonRef = ref(null);
|
||||||
// 获取指定日期的交易列表
|
// 获取指定日期的交易列表
|
||||||
const fetchDateTransactions = async (date) => {
|
const fetchDateTransactions = async (date) => {
|
||||||
try {
|
try {
|
||||||
@@ -122,6 +135,8 @@ const fetchDateTransactions = async (date) => {
|
|||||||
dateTransactions.value = response
|
dateTransactions.value = response
|
||||||
.data
|
.data
|
||||||
.sort((a, b) => b.amount - a.amount);
|
.sort((a, b) => b.amount - a.amount);
|
||||||
|
// 重置智能分类按钮
|
||||||
|
smartClassifyButtonRef.value.reset()
|
||||||
} else {
|
} else {
|
||||||
dateTransactions.value = [];
|
dateTransactions.value = [];
|
||||||
showToast(response.message || "获取交易列表失败");
|
showToast(response.message || "获取交易列表失败");
|
||||||
@@ -205,6 +220,17 @@ const onDetailSave = () => {
|
|||||||
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
|
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 智能分类保存回调
|
||||||
|
const onSmartClassifySave = async () => {
|
||||||
|
// 保存完成后重新加载数据
|
||||||
|
if (selectedDate.value) {
|
||||||
|
await fetchDateTransactions(selectedDate.value);
|
||||||
|
}
|
||||||
|
// 重新加载统计数据
|
||||||
|
const now = selectedDate.value || new Date();
|
||||||
|
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
|
||||||
|
};
|
||||||
|
|
||||||
const formatterCalendar = (day) => {
|
const formatterCalendar = (day) => {
|
||||||
const dayCopy = { ...day };
|
const dayCopy = { ...day };
|
||||||
if (dayCopy.date.toDateString() === new Date().toDateString()) {
|
if (dayCopy.date.toDateString() === new Date().toDateString()) {
|
||||||
@@ -264,4 +290,45 @@ fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
|
|||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 弹窗头部样式 */
|
||||||
|
.popup-header-fixed {
|
||||||
|
padding: 16px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #969799;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-title {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-stats p {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #646566;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-scroll-content {
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -56,6 +56,9 @@
|
|||||||
{{ group.sampleClassify }}
|
{{ group.sampleClassify }}
|
||||||
</van-tag>
|
</van-tag>
|
||||||
<span class="count-text">{{ group.count }} 条记录</span>
|
<span class="count-text">{{ group.count }} 条记录</span>
|
||||||
|
<span class="amount-text" v-if="group.totalAmount">
|
||||||
|
¥{{ Math.abs(group.totalAmount).toFixed(2) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #right-icon>
|
<template #right-icon>
|
||||||
@@ -483,6 +486,12 @@ onMounted(() => {
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.amount-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #ff976a;
|
||||||
|
}
|
||||||
|
|
||||||
.unclassified-stat {
|
.unclassified-stat {
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
|
|||||||
@@ -10,42 +10,70 @@
|
|||||||
<div class="scroll-content" style="padding-top: 5px;">
|
<div class="scroll-content" style="padding-top: 5px;">
|
||||||
<!-- 统计信息 -->
|
<!-- 统计信息 -->
|
||||||
<div class="stats-info">
|
<div class="stats-info">
|
||||||
<span class="stats-label">未分类账单:</span>
|
<span class="stats-label">未分类账单 </span>
|
||||||
<span class="stats-value">{{ records.length }} / {{ unclassifiedCount }}</span>
|
<span class="stats-value">{{ unclassifiedCount }} 条,本次分类 {{ reasonGroups.length }} 组</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 账单列表 -->
|
<!-- 分组列表 -->
|
||||||
<TransactionList
|
<van-empty v-if="reasonGroups.length === 0 && !loading" description="暂无未分类账单" />
|
||||||
:transactions="records"
|
|
||||||
:loading="false"
|
<van-cell-group v-else inset>
|
||||||
:show-delete="false"
|
<van-cell
|
||||||
:show-checkbox="true"
|
v-for="group in reasonGroups"
|
||||||
:selected-ids="selectedIds"
|
:key="group.reason"
|
||||||
@click="viewDetail"
|
clickable
|
||||||
@update:selected-ids="selectedIds = $event"
|
>
|
||||||
/>
|
<template #title>
|
||||||
|
<div class="group-header">
|
||||||
|
<van-checkbox
|
||||||
|
:model-value="selectedReasons.has(group.reason)"
|
||||||
|
@click.stop="toggleGroupSelection(group.reason)"
|
||||||
|
/>
|
||||||
|
<div class="group-title">
|
||||||
|
{{ group.reason }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #label>
|
||||||
|
<div class="group-info">
|
||||||
|
<van-tag
|
||||||
|
:type="getTypeColor(group.sampleType)"
|
||||||
|
size="medium"
|
||||||
|
style="margin-right: 8px;"
|
||||||
|
>
|
||||||
|
{{ getTypeName(group.sampleType) }}
|
||||||
|
</van-tag>
|
||||||
|
<van-tag
|
||||||
|
v-if="group.sampleClassify"
|
||||||
|
type="primary"
|
||||||
|
size="medium"
|
||||||
|
style="margin-right: 8px;"
|
||||||
|
>
|
||||||
|
{{ group.sampleClassify }}
|
||||||
|
</van-tag>
|
||||||
|
<span class="count-text">{{ group.count }} 条</span>
|
||||||
|
<span class="amount-text" v-if="group.totalAmount">
|
||||||
|
¥{{ Math.abs(group.totalAmount).toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</van-cell>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
<!-- 底部安全距离 -->
|
<!-- 底部安全距离 -->
|
||||||
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 详情/编辑弹出层 -->
|
|
||||||
<TransactionDetail
|
|
||||||
v-model:show="detailVisible"
|
|
||||||
:transaction="currentTransaction"
|
|
||||||
@save="onDetailSave"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 底部操作按钮 -->
|
<!-- 底部操作按钮 -->
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<van-button
|
<van-button
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="classifying"
|
:loading="classifying"
|
||||||
:disabled="selectedIds.size === 0"
|
:disabled="selectedReasons.size === 0"
|
||||||
@click="startClassify"
|
@click="startClassify"
|
||||||
class="action-btn"
|
class="action-btn"
|
||||||
>
|
>
|
||||||
{{ classifying ? '分类中...' : `开始分类 (${selectedIds.size}/${records.length})` }}
|
{{ classifying ? '分类中...' : `开始分类 (${selectedReasons.size}组)` }}
|
||||||
</van-button>
|
</van-button>
|
||||||
|
|
||||||
<van-button
|
<van-button
|
||||||
@@ -66,22 +94,18 @@ import { useRouter } from 'vue-router'
|
|||||||
import { showToast, showLoadingToast, closeToast, showConfirmDialog } from 'vant'
|
import { showToast, showLoadingToast, closeToast, showConfirmDialog } from 'vant'
|
||||||
import {
|
import {
|
||||||
getUnclassifiedCount,
|
getUnclassifiedCount,
|
||||||
getUnclassified,
|
getReasonGroups,
|
||||||
smartClassify,
|
smartClassify,
|
||||||
batchUpdateClassify,
|
batchUpdateClassify
|
||||||
getTransactionDetail
|
|
||||||
} from '@/api/transactionRecord'
|
} from '@/api/transactionRecord'
|
||||||
import TransactionList from '@/components/TransactionList.vue'
|
|
||||||
import TransactionDetail from '@/components/TransactionDetail.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const unclassifiedCount = ref(0)
|
const unclassifiedCount = ref(0)
|
||||||
const records = ref([])
|
const reasonGroups = ref([]) // 改为分组数据
|
||||||
const selectedIds = ref(new Set()) // 选中的账单ID集合
|
const selectedReasons = ref(new Set()) // 选中的分组摘要集合
|
||||||
|
const loading = ref(false)
|
||||||
const classifying = ref(false)
|
const classifying = ref(false)
|
||||||
const hasChanges = ref(false)
|
const hasChanges = ref(false)
|
||||||
const detailVisible = ref(false)
|
|
||||||
const currentTransaction = ref(null)
|
|
||||||
const classifyBuffer = ref('') // SSE数据缓冲区
|
const classifyBuffer = ref('') // SSE数据缓冲区
|
||||||
|
|
||||||
const onClickLeft = () => {
|
const onClickLeft = () => {
|
||||||
@@ -109,37 +133,80 @@ const loadUnclassifiedCount = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载未分类账单列表
|
// 加载分组数据
|
||||||
const loadUnclassified = async () => {
|
const loadReasonGroups = async () => {
|
||||||
showLoadingToast({
|
showLoadingToast({
|
||||||
message: '加载中...',
|
message: '加载中...',
|
||||||
forbidClick: true,
|
forbidClick: true,
|
||||||
duration: 0
|
duration: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getUnclassified(10)
|
// 获取所有未分类的分组,设置较大的pageSize以获取所有数据
|
||||||
|
const res = await getReasonGroups(1, 20)
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
records.value = res.data
|
// 后端已经按数量排序,我们需要计算每个分组的总金额并重新排序
|
||||||
// 默认全选所有账单
|
// 但是后端DTO没有返回总金额,我们先按数量排序即可
|
||||||
selectedIds.value = new Set(res.data.map(r => r.id))
|
reasonGroups.value = res.data || []
|
||||||
|
// 默认全选所有分组
|
||||||
|
selectedReasons.value = new Set(reasonGroups.value.map(g => g.reason))
|
||||||
} else {
|
} else {
|
||||||
showToast(res.message || '加载失败')
|
showToast(res.message || '加载失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载账单失败', error)
|
console.error('加载分组失败', error)
|
||||||
showToast('加载失败')
|
showToast('加载失败')
|
||||||
} finally {
|
} finally {
|
||||||
|
loading.value = false
|
||||||
closeToast()
|
closeToast()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换分组选择状态
|
||||||
|
const toggleGroupSelection = (reason) => {
|
||||||
|
if (selectedReasons.value.has(reason)) {
|
||||||
|
selectedReasons.value.delete(reason)
|
||||||
|
} else {
|
||||||
|
selectedReasons.value.add(reason)
|
||||||
|
}
|
||||||
|
// 触发响应式更新
|
||||||
|
selectedReasons.value = new Set(selectedReasons.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取类型名称
|
||||||
|
const getTypeName = (type) => {
|
||||||
|
const typeMap = {
|
||||||
|
0: '支出',
|
||||||
|
1: '收入',
|
||||||
|
2: '不计收支'
|
||||||
|
}
|
||||||
|
return typeMap[type] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取类型对应的标签颜色
|
||||||
|
const getTypeColor = (type) => {
|
||||||
|
const colorMap = {
|
||||||
|
0: 'danger', // 支出 - 红色
|
||||||
|
1: 'success', // 收入 - 绿色
|
||||||
|
2: 'default' // 不计收支 - 灰色
|
||||||
|
}
|
||||||
|
return colorMap[type] || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
// 开始智能分类
|
// 开始智能分类
|
||||||
const startClassify = async () => {
|
const startClassify = async () => {
|
||||||
const idsToClassify = Array.from(selectedIds.value)
|
// 获取所有选中分组的账单ID
|
||||||
|
const idsToClassify = []
|
||||||
|
for (const group of reasonGroups.value) {
|
||||||
|
if (selectedReasons.value.has(group.reason)) {
|
||||||
|
idsToClassify.push(...group.transactionIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (idsToClassify.length === 0) {
|
if (idsToClassify.length === 0) {
|
||||||
showToast('请先选择要分类的账单')
|
showToast('请先选择要分类的账单组')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +219,9 @@ const startClassify = async () => {
|
|||||||
classifying.value = true
|
classifying.value = true
|
||||||
classifyBuffer.value = '' // 重置缓冲区
|
classifyBuffer.value = '' // 重置缓冲区
|
||||||
|
|
||||||
|
// 用于存储分类结果的临时对象
|
||||||
|
const classifyResults = new Map() // id -> {classify, type}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await smartClassify(idsToClassify)
|
const response = await smartClassify(idsToClassify)
|
||||||
|
|
||||||
@@ -182,7 +252,7 @@ const startClassify = async () => {
|
|||||||
const eventType = eventMatch[1]
|
const eventType = eventMatch[1]
|
||||||
const data = dataMatch[1]
|
const data = dataMatch[1]
|
||||||
|
|
||||||
handleSSEEvent(eventType, data)
|
handleSSEEvent(eventType, data, classifyResults)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,19 +267,17 @@ const startClassify = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理SSE事件
|
// 处理SSE事件
|
||||||
const handleSSEEvent = (eventType, data) => {
|
const handleSSEEvent = (eventType, data, classifyResults) => {
|
||||||
if (eventType === 'data') {
|
if (eventType === 'data') {
|
||||||
try {
|
try {
|
||||||
// 累积AI输出的JSON片段
|
// 累积AI输出的JSON片段
|
||||||
classifyBuffer.value += data
|
classifyBuffer.value += data
|
||||||
|
|
||||||
// 尝试查找并提取完整的JSON对象
|
// 尝试查找并提取完整的JSON对象
|
||||||
// 使用更精确的方式:查找 { 和匹配的 }
|
|
||||||
let startIndex = 0
|
let startIndex = 0
|
||||||
while (startIndex < classifyBuffer.value.length) {
|
while (startIndex < classifyBuffer.value.length) {
|
||||||
const openBrace = classifyBuffer.value.indexOf('{', startIndex)
|
const openBrace = classifyBuffer.value.indexOf('{', startIndex)
|
||||||
if (openBrace === -1) {
|
if (openBrace === -1) {
|
||||||
// 没有找到开始的 {,清理前面的无用字符
|
|
||||||
classifyBuffer.value = ''
|
classifyBuffer.value = ''
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -229,32 +297,37 @@ const handleSSEEvent = (eventType, data) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (closeBrace !== -1) {
|
if (closeBrace !== -1) {
|
||||||
// 找到了完整的JSON
|
|
||||||
const jsonStr = classifyBuffer.value.substring(openBrace, closeBrace + 1)
|
const jsonStr = classifyBuffer.value.substring(openBrace, closeBrace + 1)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = JSON.parse(jsonStr)
|
const result = JSON.parse(jsonStr)
|
||||||
|
|
||||||
if (result.id) {
|
if (result.id) {
|
||||||
const record = records.value.find(r => r.id === result.id)
|
// 存储分类结果
|
||||||
if (record) {
|
classifyResults.set(result.id, {
|
||||||
record.classify = result.classify || ''
|
classify: result.classify || '',
|
||||||
// 如果AI返回了type字段,也更新type
|
type: result.type !== undefined ? result.type : null
|
||||||
if (result.type !== undefined && result.type !== null) {
|
})
|
||||||
record.type = result.type
|
|
||||||
|
// 更新对应分组的显示状态
|
||||||
|
for (const group of reasonGroups.value) {
|
||||||
|
if (group.transactionIds.includes(result.id)) {
|
||||||
|
group.sampleClassify = result.classify || ''
|
||||||
|
if (result.type !== undefined && result.type !== null) {
|
||||||
|
group.sampleType = result.type
|
||||||
|
}
|
||||||
|
hasChanges.value = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
hasChanges.value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('JSON解析失败:', e)
|
console.error('JSON解析失败:', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除已处理的部分
|
|
||||||
classifyBuffer.value = classifyBuffer.value.substring(closeBrace + 1)
|
classifyBuffer.value = classifyBuffer.value.substring(closeBrace + 1)
|
||||||
startIndex = 0 // 从头开始查找下一个JSON
|
startIndex = 0
|
||||||
} else {
|
} else {
|
||||||
// 没有找到闭合括号,说明JSON还不完整,等待更多数据
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,13 +347,20 @@ const handleSSEEvent = (eventType, data) => {
|
|||||||
|
|
||||||
// 保存分类
|
// 保存分类
|
||||||
const saveClassifications = async () => {
|
const saveClassifications = async () => {
|
||||||
const itemsToUpdate = records.value
|
// 收集所有已分类的账单
|
||||||
.filter(r => r.classify)
|
const itemsToUpdate = []
|
||||||
.map(r => ({
|
for (const group of reasonGroups.value) {
|
||||||
id: r.id,
|
if (group.sampleClassify) {
|
||||||
classify: r.classify,
|
// 为该分组的所有账单添加分类
|
||||||
type: r.type
|
for (const id of group.transactionIds) {
|
||||||
}))
|
itemsToUpdate.push({
|
||||||
|
id: id,
|
||||||
|
classify: group.sampleClassify,
|
||||||
|
type: group.sampleType
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (itemsToUpdate.length === 0) {
|
if (itemsToUpdate.length === 0) {
|
||||||
showToast('没有需要保存的分类')
|
showToast('没有需要保存的分类')
|
||||||
@@ -300,7 +380,7 @@ const saveClassifications = async () => {
|
|||||||
hasChanges.value = false
|
hasChanges.value = false
|
||||||
// 重新加载数据
|
// 重新加载数据
|
||||||
await loadUnclassifiedCount()
|
await loadUnclassifiedCount()
|
||||||
await loadUnclassified()
|
await loadReasonGroups()
|
||||||
} else {
|
} else {
|
||||||
showToast(res.message || '保存失败')
|
showToast(res.message || '保存失败')
|
||||||
}
|
}
|
||||||
@@ -312,32 +392,9 @@ const saveClassifications = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看详情
|
|
||||||
const viewDetail = async (transaction) => {
|
|
||||||
try {
|
|
||||||
const response = await getTransactionDetail(transaction.id)
|
|
||||||
if (response.success) {
|
|
||||||
currentTransaction.value = response.data
|
|
||||||
detailVisible.value = true
|
|
||||||
} else {
|
|
||||||
showToast(response.message || '获取详情失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取详情出错:', error)
|
|
||||||
showToast('获取详情失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 详情保存后的回调
|
|
||||||
const onDetailSave = async () => {
|
|
||||||
// 重新加载数据
|
|
||||||
await loadUnclassifiedCount()
|
|
||||||
await loadUnclassified()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadUnclassifiedCount()
|
loadUnclassifiedCount()
|
||||||
loadUnclassified()
|
loadReasonGroups()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -353,6 +410,39 @@ onMounted(() => {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 分组头部 */
|
||||||
|
.group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #969799;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #ff976a;
|
||||||
|
}
|
||||||
|
|
||||||
/* 底部操作栏 */
|
/* 底部操作栏 */
|
||||||
.action-bar {
|
.action-bar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ public class LogController(ILogger<LogController> logger) : ControllerBase
|
|||||||
// 使用正则表达式解析
|
// 使用正则表达式解析
|
||||||
var match = System.Text.RegularExpressions.Regex.Match(
|
var match = System.Text.RegularExpressions.Regex.Match(
|
||||||
line,
|
line,
|
||||||
@"^\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3}\s+[+-]\d{2}:\d{2})\]\s+\[(\w+)\]\s+(.*)$"
|
@"^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} [+-]\d{2}:\d{2})\] \[([A-Z]{3})\] (.*)$"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (match.Success)
|
if (match.Success)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ public class TransactionRecordController(
|
|||||||
ITransactionRecordRepository transactionRepository,
|
ITransactionRecordRepository transactionRepository,
|
||||||
ITransactionCategoryRepository categoryRepository,
|
ITransactionCategoryRepository categoryRepository,
|
||||||
IOpenAiService openAiService,
|
IOpenAiService openAiService,
|
||||||
|
ITextSegmentService textSegmentService,
|
||||||
ILogger<TransactionRecordController> logger
|
ILogger<TransactionRecordController> logger
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
@@ -577,23 +578,64 @@ public class TransactionRecordController(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取指定ID的账单
|
// 获取指定ID的账单(作为样本)
|
||||||
var records = new List<TransactionRecord>();
|
var sampleRecords = await transactionRepository.GetByIdsAsync(request.TransactionIds.ToArray());
|
||||||
foreach (var id in request.TransactionIds)
|
|
||||||
{
|
|
||||||
var record = await transactionRepository.GetByIdAsync(id);
|
|
||||||
if (record != null && record.Classify == string.Empty)
|
|
||||||
{
|
|
||||||
records.Add(record);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (records.Count == 0)
|
if (sampleRecords.Length == 0)
|
||||||
{
|
{
|
||||||
await WriteEventAsync("error", "找不到指定的账单");
|
await WriteEventAsync("error", "找不到指定的账单");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重新按Reason分组所有待分类账单
|
||||||
|
var groupedRecords = sampleRecords
|
||||||
|
.GroupBy(r => r.Reason)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
Reason = g.Key,
|
||||||
|
Ids = g.Select(r => r.Id).ToList(),
|
||||||
|
Count = g.Count(),
|
||||||
|
TotalAmount = g.Sum(r => r.Amount),
|
||||||
|
SampleType = g.First().Type
|
||||||
|
})
|
||||||
|
.OrderByDescending(g => Math.Abs(g.TotalAmount))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// 【增强功能】对每个分组的摘要进行分词,查询已分类的相似账单
|
||||||
|
var referenceRecords = new Dictionary<string, List<TransactionRecord>>();
|
||||||
|
foreach (var group in groupedRecords)
|
||||||
|
{
|
||||||
|
// 使用专业分词库提取关键词
|
||||||
|
var keywords = textSegmentService.ExtractKeywords(group.Reason);
|
||||||
|
|
||||||
|
if (keywords.Count > 0)
|
||||||
|
{
|
||||||
|
// 查询包含这些关键词且已分类的账单(带相关度评分)
|
||||||
|
// minMatchRate=0.4 表示至少匹配40%的关键词才被认为是相似的
|
||||||
|
var similarClassifiedWithScore = await transactionRepository.GetClassifiedByKeywordsWithScoreAsync(keywords, minMatchRate: 0.4, limit: 10);
|
||||||
|
|
||||||
|
if (similarClassifiedWithScore.Count > 0)
|
||||||
|
{
|
||||||
|
// 只取前5个最相关的
|
||||||
|
var topSimilar = similarClassifiedWithScore.Take(5).Select(x => x.record).ToList();
|
||||||
|
referenceRecords[group.Reason] = topSimilar;
|
||||||
|
|
||||||
|
// 记录调试信息
|
||||||
|
logger.LogDebug("摘要 '{Reason}' 提取关键词: {Keywords}, 找到 {Count} 个相似账单,相关度分数: {Scores}",
|
||||||
|
group.Reason,
|
||||||
|
string.Join(", ", keywords),
|
||||||
|
similarClassifiedWithScore.Count,
|
||||||
|
string.Join(", ", similarClassifiedWithScore.Select(x => $"{x.record.Reason}({x.relevanceScore:F2})")));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogDebug("摘要 '{Reason}' 提取关键词: {Keywords}, 未找到高相关度的相似账单",
|
||||||
|
group.Reason,
|
||||||
|
string.Join(", ", keywords));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取所有分类
|
// 获取所有分类
|
||||||
var categories = await categoryRepository.GetAllAsync();
|
var categories = await categoryRepository.GetAllAsync();
|
||||||
|
|
||||||
@@ -610,28 +652,43 @@ public class TransactionRecordController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建账单信息
|
// 构建账单分组信息
|
||||||
var billsInfo = string.Join("\n", records.Select((r, i) =>
|
var billsInfo = new StringBuilder();
|
||||||
$"{i + 1}. ID={r.Id}, 摘要={r.Reason}, 金额={r.Amount}, 类型={GetTypeName(r.Type)}"));
|
foreach (var (group, index) in groupedRecords.Select((g, i) => (g, i)))
|
||||||
|
{
|
||||||
|
billsInfo.AppendLine($"{index + 1}. 摘要={group.Reason}, 当前类型={GetTypeName(group.SampleType)}, 当前分类={(string.IsNullOrEmpty(group.SampleType.ToString()) ? "未分类" : group.SampleType.ToString())}, 涉及金额={group.TotalAmount}");
|
||||||
|
|
||||||
|
// 如果有相似的已分类账单,添加参考信息
|
||||||
|
if (referenceRecords.TryGetValue(group.Reason, out var references))
|
||||||
|
{
|
||||||
|
billsInfo.AppendLine(" 【参考】相似且已分类的账单:");
|
||||||
|
foreach (var refer in references.Take(3)) // 最多显示3个参考
|
||||||
|
{
|
||||||
|
billsInfo.AppendLine($" - 摘要={refer.Reason}, 分类={refer.Classify}, 类型={GetTypeName(refer.Type)}, 金额={refer.Amount}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var systemPrompt = $$"""
|
var systemPrompt = $$"""
|
||||||
你是一个专业的账单分类助手。请根据提供的账单信息和分类列表,为每个账单选择最合适的分类。
|
你是一个专业的账单分类助手。请根据提供的账单分组信息和分类列表,为每个分组选择最合适的分类。
|
||||||
|
|
||||||
可用的分类列表:
|
可用的分类列表:
|
||||||
{{categoryInfo}}
|
{{categoryInfo}}
|
||||||
|
|
||||||
分类规则:
|
分类规则:
|
||||||
1. 根据账单的摘要和金额,选择最匹配的分类
|
1. 根据账单的摘要和涉及金额,选择最匹配的分类
|
||||||
2. 如果无法确定分类,可以选择""其他""
|
2. 如果提供了【参考】信息,优先参考相似账单的分类,这些是历史上已分类的相似账单
|
||||||
|
3. 如果无法确定分类,可以选择"其他"
|
||||||
|
4. 每个分组可能包含多条账单,你需要为整个分组选择一个分类
|
||||||
|
|
||||||
请对每个账单进行分类,每次输出一个账单的分类结果,格式如下:
|
请对每个分组进行分类,每次输出一个分组的分类结果,格式如下:
|
||||||
{"id": 账单ID, "type": 0:支出/1:收入/2:不计入收支(Type为Number枚举值) ,"classify": 分类名称}
|
{"reason": "交易摘要", "type": 0:支出/1:收入/2:不计入收支(Type为Number枚举值) ,"classify": "分类名称"}
|
||||||
|
|
||||||
只输出JSON,不要有其他文字说明。
|
只输出JSON,不要有其他文字说明。
|
||||||
""";
|
""";
|
||||||
|
|
||||||
var userPrompt = $$"""
|
var userPrompt = $$"""
|
||||||
请为以下账单进行分类:
|
请为以下账单分组进行分类:
|
||||||
|
|
||||||
{{billsInfo}}
|
{{billsInfo}}
|
||||||
|
|
||||||
@@ -639,11 +696,58 @@ public class TransactionRecordController(
|
|||||||
""";
|
""";
|
||||||
|
|
||||||
// 流式调用AI
|
// 流式调用AI
|
||||||
await WriteEventAsync("start", $"开始分类 {records.Count} 条账单");
|
await WriteEventAsync("start", $"开始分类,共 {sampleRecords.Length} 条账单");
|
||||||
|
|
||||||
|
// 用于存储AI返回的分组分类结果
|
||||||
|
var classifyResults = new List<(string Reason, string Classify, TransactionType Type)>();
|
||||||
|
var buffer = new StringBuilder();
|
||||||
|
var sendedIds = new HashSet<long>();
|
||||||
await foreach (var chunk in openAiService.ChatStreamAsync(systemPrompt, userPrompt))
|
await foreach (var chunk in openAiService.ChatStreamAsync(systemPrompt, userPrompt))
|
||||||
{
|
{
|
||||||
await WriteEventAsync("data", chunk);
|
buffer.Append(chunk);
|
||||||
|
|
||||||
|
// 尝试解析完整的JSON对象
|
||||||
|
var bufferStr = buffer.ToString();
|
||||||
|
var startIdx = 0;
|
||||||
|
while (startIdx < bufferStr.Length)
|
||||||
|
{
|
||||||
|
var openBrace = bufferStr.IndexOf('{', startIdx);
|
||||||
|
if (openBrace == -1) break;
|
||||||
|
|
||||||
|
var closeBrace = FindMatchingBrace(bufferStr, openBrace);
|
||||||
|
if (closeBrace == -1) break;
|
||||||
|
|
||||||
|
var jsonStr = bufferStr.Substring(openBrace, closeBrace - openBrace + 1);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = JsonSerializer.Deserialize<GroupClassifyResult>(jsonStr);
|
||||||
|
if (result != null && !string.IsNullOrEmpty(result.Reason))
|
||||||
|
{
|
||||||
|
classifyResults.Add((result.Reason, result.Classify ?? "", result.Type));
|
||||||
|
// 每一条结果单独通知
|
||||||
|
var group = groupedRecords.FirstOrDefault(g => g.Reason == result.Reason);
|
||||||
|
if (group != null)
|
||||||
|
{
|
||||||
|
// 为该分组的所有账单ID返回分类结果
|
||||||
|
foreach (var id in group.Ids)
|
||||||
|
{
|
||||||
|
if (!sendedIds.Contains(id))
|
||||||
|
{
|
||||||
|
sendedIds.Add(id);
|
||||||
|
var resultJson = JsonSerializer.Serialize(new { id, result.Classify, result.Type });
|
||||||
|
await WriteEventAsync("data", resultJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "解析AI分类结果失败: {JsonStr}", jsonStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
startIdx = closeBrace + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await WriteEventAsync("end", "分类完成");
|
await WriteEventAsync("end", "分类完成");
|
||||||
@@ -898,6 +1002,24 @@ public class TransactionRecordController(
|
|||||||
await Response.Body.FlushAsync();
|
await Response.Body.FlushAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找匹配的右括号
|
||||||
|
/// </summary>
|
||||||
|
private static int FindMatchingBrace(string str, int startPos)
|
||||||
|
{
|
||||||
|
int braceCount = 0;
|
||||||
|
for (int i = startPos; i < str.Length; i++)
|
||||||
|
{
|
||||||
|
if (str[i] == '{') braceCount++;
|
||||||
|
else if (str[i] == '}')
|
||||||
|
{
|
||||||
|
braceCount--;
|
||||||
|
if (braceCount == 0) return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetTypeName(TransactionType type)
|
private static string GetTypeName(TransactionType type)
|
||||||
{
|
{
|
||||||
return type switch
|
return type switch
|
||||||
@@ -949,6 +1071,21 @@ public record SmartClassifyRequest(
|
|||||||
List<long>? TransactionIds = null
|
List<long>? TransactionIds = null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分组分类结果DTO(用于AI返回结果解析)
|
||||||
|
/// </summary>
|
||||||
|
public record GroupClassifyResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("reason")]
|
||||||
|
public string Reason { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("classify")]
|
||||||
|
public string? Classify { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public TransactionType Type { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 批量更新分类项DTO
|
/// 批量更新分类项DTO
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
79460
WebApi/Resources.bak/char_state_tab.json
Normal file
79460
WebApi/Resources.bak/char_state_tab.json
Normal file
File diff suppressed because it is too large
Load Diff
349046
WebApi/Resources.bak/dict.txt
Normal file
349046
WebApi/Resources.bak/dict.txt
Normal file
File diff suppressed because it is too large
Load Diff
89711
WebApi/Resources.bak/pos_prob_emit.json
Normal file
89711
WebApi/Resources.bak/pos_prob_emit.json
Normal file
File diff suppressed because it is too large
Load Diff
258
WebApi/Resources.bak/pos_prob_start.json
Normal file
258
WebApi/Resources.bak/pos_prob_start.json
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
{
|
||||||
|
"E-e": -3.14e+100,
|
||||||
|
"E-d": -3.14e+100,
|
||||||
|
"E-g": -3.14e+100,
|
||||||
|
"E-f": -3.14e+100,
|
||||||
|
"E-a": -3.14e+100,
|
||||||
|
"E-c": -3.14e+100,
|
||||||
|
"E-b": -3.14e+100,
|
||||||
|
"E-m": -3.14e+100,
|
||||||
|
"S-rg": -10.275268591948773,
|
||||||
|
"E-o": -3.14e+100,
|
||||||
|
"E-n": -3.14e+100,
|
||||||
|
"E-i": -3.14e+100,
|
||||||
|
"E-h": -3.14e+100,
|
||||||
|
"E-k": -3.14e+100,
|
||||||
|
"E-j": -3.14e+100,
|
||||||
|
"E-u": -3.14e+100,
|
||||||
|
"E-t": -3.14e+100,
|
||||||
|
"E-w": -3.14e+100,
|
||||||
|
"E-v": -3.14e+100,
|
||||||
|
"E-q": -3.14e+100,
|
||||||
|
"E-p": -3.14e+100,
|
||||||
|
"E-s": -3.14e+100,
|
||||||
|
"M-bg": -3.14e+100,
|
||||||
|
"M-uj": -3.14e+100,
|
||||||
|
"E-y": -3.14e+100,
|
||||||
|
"E-x": -3.14e+100,
|
||||||
|
"E-z": -3.14e+100,
|
||||||
|
"B-uz": -3.14e+100,
|
||||||
|
"S-d": -3.903919764181873,
|
||||||
|
"M-rg": -3.14e+100,
|
||||||
|
"E-nt": -3.14e+100,
|
||||||
|
"B-d": -3.9750475297585357,
|
||||||
|
"B-uv": -3.14e+100,
|
||||||
|
"E-vi": -3.14e+100,
|
||||||
|
"B-mq": -6.78695300139688,
|
||||||
|
"M-rr": -3.14e+100,
|
||||||
|
"S-ag": -6.954113917960154,
|
||||||
|
"M-jn": -3.14e+100,
|
||||||
|
"E-l": -3.14e+100,
|
||||||
|
"M-rz": -3.14e+100,
|
||||||
|
"B-ud": -3.14e+100,
|
||||||
|
"S-an": -12.84021794941031,
|
||||||
|
"B-qg": -3.14e+100,
|
||||||
|
"B-ug": -3.14e+100,
|
||||||
|
"M-y": -3.14e+100,
|
||||||
|
"S-qg": -3.14e+100,
|
||||||
|
"S-z": -3.14e+100,
|
||||||
|
"S-y": -6.1970794699489575,
|
||||||
|
"S-x": -8.427419656069674,
|
||||||
|
"S-w": -3.14e+100,
|
||||||
|
"S-v": -3.053292303412302,
|
||||||
|
"S-u": -6.940320595827818,
|
||||||
|
"S-t": -3.14e+100,
|
||||||
|
"B-nrt": -4.985642733519195,
|
||||||
|
"S-r": -2.7635336784127853,
|
||||||
|
"S-q": -4.888658618255058,
|
||||||
|
"M-zg": -3.14e+100,
|
||||||
|
"S-o": -8.464460927750023,
|
||||||
|
"S-n": -3.8551483897645107,
|
||||||
|
"B-zg": -3.14e+100,
|
||||||
|
"S-l": -3.14e+100,
|
||||||
|
"S-k": -6.940320595827818,
|
||||||
|
"S-in": -3.14e+100,
|
||||||
|
"S-i": -3.14e+100,
|
||||||
|
"S-h": -8.650563207383884,
|
||||||
|
"S-g": -6.507826815331734,
|
||||||
|
"B-f": -5.491630418482717,
|
||||||
|
"S-e": -5.942513006281674,
|
||||||
|
"M-en": -3.14e+100,
|
||||||
|
"S-c": -4.786966795861212,
|
||||||
|
"S-b": -6.472888763970454,
|
||||||
|
"S-a": -3.9025396831295227,
|
||||||
|
"B-g": -3.14e+100,
|
||||||
|
"B-b": -5.018374362109218,
|
||||||
|
"B-c": -3.423880184954888,
|
||||||
|
"M-ug": -3.14e+100,
|
||||||
|
"B-a": -4.762305214596967,
|
||||||
|
"E-qe": -3.14e+100,
|
||||||
|
"M-x": -3.14e+100,
|
||||||
|
"E-nz": -3.14e+100,
|
||||||
|
"M-z": -3.14e+100,
|
||||||
|
"M-u": -3.14e+100,
|
||||||
|
"B-k": -3.14e+100,
|
||||||
|
"M-w": -3.14e+100,
|
||||||
|
"B-jn": -3.14e+100,
|
||||||
|
"S-yg": -13.533365129970255,
|
||||||
|
"B-o": -8.433498702146057,
|
||||||
|
"B-l": -4.905883584659895,
|
||||||
|
"B-m": -3.6524299819046386,
|
||||||
|
"M-m": -3.14e+100,
|
||||||
|
"M-l": -3.14e+100,
|
||||||
|
"M-o": -3.14e+100,
|
||||||
|
"M-n": -3.14e+100,
|
||||||
|
"M-i": -3.14e+100,
|
||||||
|
"M-h": -3.14e+100,
|
||||||
|
"B-t": -3.3647479094528574,
|
||||||
|
"M-ul": -3.14e+100,
|
||||||
|
"B-z": -7.045681111485645,
|
||||||
|
"M-d": -3.14e+100,
|
||||||
|
"M-mg": -3.14e+100,
|
||||||
|
"B-y": -9.844485675856319,
|
||||||
|
"M-a": -3.14e+100,
|
||||||
|
"S-nrt": -3.14e+100,
|
||||||
|
"M-c": -3.14e+100,
|
||||||
|
"M-uz": -3.14e+100,
|
||||||
|
"E-mg": -3.14e+100,
|
||||||
|
"B-i": -6.1157847275557105,
|
||||||
|
"M-b": -3.14e+100,
|
||||||
|
"E-uz": -3.14e+100,
|
||||||
|
"B-n": -1.6966257797548328,
|
||||||
|
"E-uv": -3.14e+100,
|
||||||
|
"M-ud": -3.14e+100,
|
||||||
|
"M-p": -3.14e+100,
|
||||||
|
"E-ul": -3.14e+100,
|
||||||
|
"E-mq": -3.14e+100,
|
||||||
|
"M-s": -3.14e+100,
|
||||||
|
"M-yg": -3.14e+100,
|
||||||
|
"E-uj": -3.14e+100,
|
||||||
|
"E-ud": -3.14e+100,
|
||||||
|
"S-ln": -3.14e+100,
|
||||||
|
"M-r": -3.14e+100,
|
||||||
|
"E-ng": -3.14e+100,
|
||||||
|
"B-r": -3.4098187790818413,
|
||||||
|
"E-en": -3.14e+100,
|
||||||
|
"M-qg": -3.14e+100,
|
||||||
|
"B-s": -5.522673590839954,
|
||||||
|
"S-rr": -3.14e+100,
|
||||||
|
"B-p": -4.200984132085048,
|
||||||
|
"B-dg": -3.14e+100,
|
||||||
|
"M-uv": -3.14e+100,
|
||||||
|
"S-zg": -3.14e+100,
|
||||||
|
"B-v": -2.6740584874265685,
|
||||||
|
"S-tg": -6.272842531880403,
|
||||||
|
"B-w": -3.14e+100,
|
||||||
|
"B-e": -8.563551830394255,
|
||||||
|
"M-k": -3.14e+100,
|
||||||
|
"M-j": -3.14e+100,
|
||||||
|
"B-df": -8.888974230828882,
|
||||||
|
"M-e": -3.14e+100,
|
||||||
|
"E-tg": -3.14e+100,
|
||||||
|
"M-t": -3.14e+100,
|
||||||
|
"E-nr": -3.14e+100,
|
||||||
|
"M-nrfg": -3.14e+100,
|
||||||
|
"B-nr": -2.2310495913769506,
|
||||||
|
"E-df": -3.14e+100,
|
||||||
|
"E-dg": -3.14e+100,
|
||||||
|
"S-jn": -3.14e+100,
|
||||||
|
"M-q": -3.14e+100,
|
||||||
|
"B-mg": -3.14e+100,
|
||||||
|
"B-ln": -3.14e+100,
|
||||||
|
"M-f": -3.14e+100,
|
||||||
|
"E-ln": -3.14e+100,
|
||||||
|
"E-yg": -3.14e+100,
|
||||||
|
"S-bg": -3.14e+100,
|
||||||
|
"E-ns": -3.14e+100,
|
||||||
|
"B-tg": -3.14e+100,
|
||||||
|
"E-qg": -3.14e+100,
|
||||||
|
"S-nr": -4.483663103956885,
|
||||||
|
"S-ns": -3.14e+100,
|
||||||
|
"M-vn": -3.14e+100,
|
||||||
|
"S-nt": -12.147070768850364,
|
||||||
|
"S-nz": -3.14e+100,
|
||||||
|
"S-ad": -11.048458480182255,
|
||||||
|
"B-yg": -3.14e+100,
|
||||||
|
"M-v": -3.14e+100,
|
||||||
|
"E-vn": -3.14e+100,
|
||||||
|
"S-ng": -4.913434861102905,
|
||||||
|
"M-g": -3.14e+100,
|
||||||
|
"M-nt": -3.14e+100,
|
||||||
|
"S-en": -3.14e+100,
|
||||||
|
"M-nr": -3.14e+100,
|
||||||
|
"M-ns": -3.14e+100,
|
||||||
|
"S-vq": -3.14e+100,
|
||||||
|
"B-uj": -3.14e+100,
|
||||||
|
"M-nz": -3.14e+100,
|
||||||
|
"B-qe": -3.14e+100,
|
||||||
|
"M-in": -3.14e+100,
|
||||||
|
"M-ng": -3.14e+100,
|
||||||
|
"S-vn": -11.453923588290419,
|
||||||
|
"E-zg": -3.14e+100,
|
||||||
|
"S-vi": -3.14e+100,
|
||||||
|
"S-vg": -5.9430181843676895,
|
||||||
|
"S-vd": -3.14e+100,
|
||||||
|
"B-ad": -6.680066036784177,
|
||||||
|
"E-rz": -3.14e+100,
|
||||||
|
"B-ag": -3.14e+100,
|
||||||
|
"B-vd": -9.044728760238115,
|
||||||
|
"S-mq": -3.14e+100,
|
||||||
|
"B-vi": -12.434752841302146,
|
||||||
|
"E-rr": -3.14e+100,
|
||||||
|
"B-rr": -12.434752841302146,
|
||||||
|
"M-vq": -3.14e+100,
|
||||||
|
"E-jn": -3.14e+100,
|
||||||
|
"B-vn": -4.3315610890163585,
|
||||||
|
"S-mg": -10.825314928868044,
|
||||||
|
"B-in": -3.14e+100,
|
||||||
|
"M-vi": -3.14e+100,
|
||||||
|
"M-an": -3.14e+100,
|
||||||
|
"M-vd": -3.14e+100,
|
||||||
|
"B-rg": -3.14e+100,
|
||||||
|
"M-vg": -3.14e+100,
|
||||||
|
"M-ad": -3.14e+100,
|
||||||
|
"M-ag": -3.14e+100,
|
||||||
|
"E-rg": -3.14e+100,
|
||||||
|
"S-uz": -9.299258625372996,
|
||||||
|
"B-en": -3.14e+100,
|
||||||
|
"S-uv": -8.15808672228609,
|
||||||
|
"S-df": -3.14e+100,
|
||||||
|
"S-dg": -8.948397651299683,
|
||||||
|
"M-qe": -3.14e+100,
|
||||||
|
"B-ng": -3.14e+100,
|
||||||
|
"E-bg": -3.14e+100,
|
||||||
|
"S-ul": -8.4153713175535,
|
||||||
|
"S-uj": -6.85251045118004,
|
||||||
|
"S-ug": -7.5394037026636855,
|
||||||
|
"B-ns": -2.8228438314969213,
|
||||||
|
"S-ud": -7.728230161053767,
|
||||||
|
"B-nt": -4.846091668182416,
|
||||||
|
"B-ul": -3.14e+100,
|
||||||
|
"E-in": -3.14e+100,
|
||||||
|
"B-bg": -3.14e+100,
|
||||||
|
"M-df": -3.14e+100,
|
||||||
|
"M-dg": -3.14e+100,
|
||||||
|
"M-nrt": -3.14e+100,
|
||||||
|
"B-j": -5.0576191284681915,
|
||||||
|
"E-ug": -3.14e+100,
|
||||||
|
"E-vq": -3.14e+100,
|
||||||
|
"B-vg": -3.14e+100,
|
||||||
|
"B-nz": -3.94698846057672,
|
||||||
|
"S-qe": -3.14e+100,
|
||||||
|
"B-rz": -7.946116471570005,
|
||||||
|
"B-nrfg": -5.873722175405573,
|
||||||
|
"E-ad": -3.14e+100,
|
||||||
|
"E-ag": -3.14e+100,
|
||||||
|
"B-u": -9.163917277503234,
|
||||||
|
"M-ln": -3.14e+100,
|
||||||
|
"B-an": -8.697083223018778,
|
||||||
|
"M-mq": -3.14e+100,
|
||||||
|
"E-an": -3.14e+100,
|
||||||
|
"S-s": -3.14e+100,
|
||||||
|
"B-q": -6.998123858956596,
|
||||||
|
"E-nrt": -3.14e+100,
|
||||||
|
"B-h": -13.533365129970255,
|
||||||
|
"E-r": -3.14e+100,
|
||||||
|
"S-p": -2.9868401813596317,
|
||||||
|
"M-tg": -3.14e+100,
|
||||||
|
"S-rz": -3.14e+100,
|
||||||
|
"S-nrfg": -3.14e+100,
|
||||||
|
"B-vq": -12.147070768850364,
|
||||||
|
"B-x": -3.14e+100,
|
||||||
|
"E-vd": -3.14e+100,
|
||||||
|
"E-nrfg": -3.14e+100,
|
||||||
|
"S-m": -3.269200652116097,
|
||||||
|
"E-vg": -3.14e+100,
|
||||||
|
"S-f": -5.194820249981676,
|
||||||
|
"S-j": -4.911992119644354
|
||||||
|
}
|
||||||
5639
WebApi/Resources.bak/pos_prob_trans.json
Normal file
5639
WebApi/Resources.bak/pos_prob_trans.json
Normal file
File diff suppressed because it is too large
Load Diff
35234
WebApi/Resources.bak/prob_emit.json
Normal file
35234
WebApi/Resources.bak/prob_emit.json
Normal file
File diff suppressed because it is too large
Load Diff
18
WebApi/Resources.bak/prob_trans.json
Normal file
18
WebApi/Resources.bak/prob_trans.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"M": {
|
||||||
|
"M": -1.2603623820268226,
|
||||||
|
"E": -0.33344856811948514
|
||||||
|
},
|
||||||
|
"S": {
|
||||||
|
"S": -0.6658631448798212,
|
||||||
|
"B": -0.7211965654669841
|
||||||
|
},
|
||||||
|
"B": {
|
||||||
|
"M": -0.916290731874155,
|
||||||
|
"E": -0.51082562376599
|
||||||
|
},
|
||||||
|
"E": {
|
||||||
|
"S": -0.8085250474669937,
|
||||||
|
"B": -0.5897149736854513
|
||||||
|
}
|
||||||
|
}
|
||||||
330
WebApi/Resources.bak/stopwords.txt
Normal file
330
WebApi/Resources.bak/stopwords.txt
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
的
|
||||||
|
了
|
||||||
|
在
|
||||||
|
是
|
||||||
|
我
|
||||||
|
有
|
||||||
|
和
|
||||||
|
就
|
||||||
|
不
|
||||||
|
人
|
||||||
|
都
|
||||||
|
一
|
||||||
|
一个
|
||||||
|
上
|
||||||
|
也
|
||||||
|
很
|
||||||
|
到
|
||||||
|
说
|
||||||
|
要
|
||||||
|
去
|
||||||
|
你
|
||||||
|
会
|
||||||
|
着
|
||||||
|
没有
|
||||||
|
看
|
||||||
|
好
|
||||||
|
自己
|
||||||
|
这
|
||||||
|
那
|
||||||
|
与
|
||||||
|
或
|
||||||
|
等
|
||||||
|
及
|
||||||
|
而
|
||||||
|
为
|
||||||
|
之
|
||||||
|
于
|
||||||
|
对
|
||||||
|
从
|
||||||
|
中
|
||||||
|
可以
|
||||||
|
但
|
||||||
|
因为
|
||||||
|
所以
|
||||||
|
如果
|
||||||
|
已经
|
||||||
|
还
|
||||||
|
又
|
||||||
|
再
|
||||||
|
更
|
||||||
|
最
|
||||||
|
元
|
||||||
|
块
|
||||||
|
角
|
||||||
|
分
|
||||||
|
笔
|
||||||
|
条
|
||||||
|
张
|
||||||
|
个
|
||||||
|
次
|
||||||
|
回
|
||||||
|
天
|
||||||
|
月
|
||||||
|
年
|
||||||
|
来
|
||||||
|
给
|
||||||
|
把
|
||||||
|
被
|
||||||
|
让
|
||||||
|
向
|
||||||
|
往
|
||||||
|
跟
|
||||||
|
比
|
||||||
|
将
|
||||||
|
以
|
||||||
|
用
|
||||||
|
由
|
||||||
|
她
|
||||||
|
他
|
||||||
|
它
|
||||||
|
们
|
||||||
|
您
|
||||||
|
咱
|
||||||
|
啥
|
||||||
|
哪
|
||||||
|
什么
|
||||||
|
怎么
|
||||||
|
为什么
|
||||||
|
多少
|
||||||
|
几
|
||||||
|
每
|
||||||
|
各
|
||||||
|
某
|
||||||
|
此
|
||||||
|
彼
|
||||||
|
其
|
||||||
|
该
|
||||||
|
这个
|
||||||
|
那个
|
||||||
|
这些
|
||||||
|
那些
|
||||||
|
哪些
|
||||||
|
什么样
|
||||||
|
怎样
|
||||||
|
如何
|
||||||
|
多么
|
||||||
|
何等
|
||||||
|
若干
|
||||||
|
左右
|
||||||
|
前后
|
||||||
|
上下
|
||||||
|
东西
|
||||||
|
南北
|
||||||
|
里外
|
||||||
|
内外
|
||||||
|
大小
|
||||||
|
高低
|
||||||
|
长短
|
||||||
|
多少
|
||||||
|
好坏
|
||||||
|
新旧
|
||||||
|
早晚
|
||||||
|
前面
|
||||||
|
后面
|
||||||
|
上面
|
||||||
|
下面
|
||||||
|
左边
|
||||||
|
右边
|
||||||
|
里面
|
||||||
|
外面
|
||||||
|
中间
|
||||||
|
旁边
|
||||||
|
附近
|
||||||
|
周围
|
||||||
|
全部
|
||||||
|
所有
|
||||||
|
一切
|
||||||
|
任何
|
||||||
|
每个
|
||||||
|
各自
|
||||||
|
彼此
|
||||||
|
相互
|
||||||
|
互相
|
||||||
|
共同
|
||||||
|
一起
|
||||||
|
同时
|
||||||
|
当时
|
||||||
|
平时
|
||||||
|
随时
|
||||||
|
及时
|
||||||
|
准时
|
||||||
|
按时
|
||||||
|
到时
|
||||||
|
届时
|
||||||
|
临时
|
||||||
|
暂时
|
||||||
|
长期
|
||||||
|
短期
|
||||||
|
永远
|
||||||
|
从来
|
||||||
|
向来
|
||||||
|
素来
|
||||||
|
一向
|
||||||
|
历来
|
||||||
|
总是
|
||||||
|
常常
|
||||||
|
往往
|
||||||
|
通常
|
||||||
|
一般
|
||||||
|
大概
|
||||||
|
大约
|
||||||
|
左右
|
||||||
|
上下
|
||||||
|
几乎
|
||||||
|
差不多
|
||||||
|
可能
|
||||||
|
也许
|
||||||
|
大概
|
||||||
|
或许
|
||||||
|
恐怕
|
||||||
|
难道
|
||||||
|
岂
|
||||||
|
究竟
|
||||||
|
到底
|
||||||
|
果然
|
||||||
|
居然
|
||||||
|
竟然
|
||||||
|
偏偏
|
||||||
|
简直
|
||||||
|
实在
|
||||||
|
的确
|
||||||
|
确实
|
||||||
|
真正
|
||||||
|
真是
|
||||||
|
果真
|
||||||
|
当真
|
||||||
|
委实
|
||||||
|
着实
|
||||||
|
十分
|
||||||
|
非常
|
||||||
|
很
|
||||||
|
挺
|
||||||
|
最
|
||||||
|
更
|
||||||
|
极
|
||||||
|
太
|
||||||
|
甚
|
||||||
|
颇
|
||||||
|
稍
|
||||||
|
略
|
||||||
|
稍微
|
||||||
|
些
|
||||||
|
点
|
||||||
|
一点
|
||||||
|
有点
|
||||||
|
一些
|
||||||
|
若干
|
||||||
|
许多
|
||||||
|
很多
|
||||||
|
好多
|
||||||
|
大量
|
||||||
|
少量
|
||||||
|
大批
|
||||||
|
成批
|
||||||
|
整个
|
||||||
|
全部
|
||||||
|
部分
|
||||||
|
半
|
||||||
|
多半
|
||||||
|
大半
|
||||||
|
少半
|
||||||
|
绝大部分
|
||||||
|
绝大多数
|
||||||
|
极少数
|
||||||
|
等等
|
||||||
|
之类
|
||||||
|
以及
|
||||||
|
及其
|
||||||
|
乃至
|
||||||
|
甚至
|
||||||
|
甚而
|
||||||
|
进而
|
||||||
|
从而
|
||||||
|
因而
|
||||||
|
所以
|
||||||
|
因此
|
||||||
|
于是
|
||||||
|
然后
|
||||||
|
接着
|
||||||
|
随后
|
||||||
|
继而
|
||||||
|
终于
|
||||||
|
最后
|
||||||
|
最终
|
||||||
|
结果
|
||||||
|
总之
|
||||||
|
综上所述
|
||||||
|
由此可见
|
||||||
|
显而易见
|
||||||
|
众所周知
|
||||||
|
不言而喭
|
||||||
|
毫无疑问
|
||||||
|
毋庸置疑
|
||||||
|
无可置疑
|
||||||
|
不容置疑
|
||||||
|
无庸讳言
|
||||||
|
何必
|
||||||
|
何况
|
||||||
|
何妨
|
||||||
|
反正
|
||||||
|
不过
|
||||||
|
只是
|
||||||
|
但是
|
||||||
|
然而
|
||||||
|
可是
|
||||||
|
只不过
|
||||||
|
不外
|
||||||
|
无非
|
||||||
|
虽然
|
||||||
|
尽管
|
||||||
|
即使
|
||||||
|
就算
|
||||||
|
哪怕
|
||||||
|
纵使
|
||||||
|
纵然
|
||||||
|
既然
|
||||||
|
假如
|
||||||
|
如果
|
||||||
|
倘若
|
||||||
|
要是
|
||||||
|
万一
|
||||||
|
除非
|
||||||
|
不管
|
||||||
|
无论
|
||||||
|
不论
|
||||||
|
任凭
|
||||||
|
只要
|
||||||
|
只有
|
||||||
|
除了
|
||||||
|
除去
|
||||||
|
除开
|
||||||
|
撇开
|
||||||
|
此外
|
||||||
|
另外
|
||||||
|
以外
|
||||||
|
之外
|
||||||
|
不仅
|
||||||
|
不但
|
||||||
|
不只
|
||||||
|
不光
|
||||||
|
而且
|
||||||
|
并且
|
||||||
|
况且
|
||||||
|
何况
|
||||||
|
甚至
|
||||||
|
以至
|
||||||
|
乃至
|
||||||
|
与其
|
||||||
|
宁可
|
||||||
|
宁愿
|
||||||
|
还是
|
||||||
|
或者
|
||||||
|
抑或
|
||||||
|
要么
|
||||||
|
不是
|
||||||
|
就是
|
||||||
|
是否
|
||||||
|
难道
|
||||||
|
莫非
|
||||||
|
岂
|
||||||
@@ -20,6 +20,17 @@
|
|||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- 移除默认包含的 Resources JSON 文件,然后显式添加并设置复制规则 -->
|
||||||
|
<Content Remove="Resources\*.json" />
|
||||||
|
<Content Include="Resources\*.txt">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="Resources\*.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Watch Remove="logs/**" />
|
<Watch Remove="logs/**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user