添加功能
This commit is contained in:
@@ -29,5 +29,8 @@
|
||||
<!-- Job Scheduling -->
|
||||
<PackageVersion Include="Quartz" 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>
|
||||
</Project>
|
||||
@@ -16,6 +16,11 @@ public interface IBaseRepository<T> where T : BaseEntity
|
||||
/// </summary>
|
||||
Task<T?> GetByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取单条数据
|
||||
/// </summary>
|
||||
Task<T[]> GetByIdsAsync(long[] ids);
|
||||
|
||||
/// <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)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -149,6 +149,23 @@ public interface ITransactionRecordRepository : IBaseRepository<TransactionRecor
|
||||
/// <param name="completeSql">完整的SELECT SQL语句</param>
|
||||
/// <returns>动态查询结果列表</returns>
|
||||
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
|
||||
@@ -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)
|
||||
{
|
||||
// 先按照Reason分组,统计每个Reason的数量
|
||||
// 先按照Reason分组,统计每个Reason的数量和总金额
|
||||
var groups = await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => !string.IsNullOrEmpty(t.Reason))
|
||||
.Where(t => string.IsNullOrEmpty(t.Classify)) // 只统计未分类的
|
||||
@@ -352,11 +369,12 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
.ToListAsync(g => new
|
||||
{
|
||||
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;
|
||||
|
||||
// 分页
|
||||
@@ -365,22 +383,27 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
.Take(pageSize)
|
||||
.ToList();
|
||||
|
||||
// 为每个分组获取示例记录
|
||||
// 为每个分组获取详细信息
|
||||
var result = new List<ReasonGroupDto>();
|
||||
foreach (var group in pagedGroups)
|
||||
{
|
||||
var sample = await FreeSql.Select<TransactionRecord>()
|
||||
// 获取该分组的所有记录
|
||||
var records = await FreeSql.Select<TransactionRecord>()
|
||||
.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
|
||||
{
|
||||
Reason = group.Reason,
|
||||
Count = (int)group.Count,
|
||||
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;
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -568,6 +653,16 @@ public class ReasonGroupDto
|
||||
/// 示例分类(该分组中第一条记录的分类)
|
||||
/// </summary>
|
||||
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>
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
<PackageReference Include="HtmlAgilityPack" />
|
||||
<PackageReference Include="Quartz" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" />
|
||||
<PackageReference Include="JiebaNet.Analyser" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
</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() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
|
||||
@@ -17,15 +17,26 @@
|
||||
position="bottom"
|
||||
:style="{ height: '85%' }"
|
||||
round
|
||||
closeable
|
||||
>
|
||||
<div class="popup-container">
|
||||
<div class="popup-header-fixed">
|
||||
<h3>{{ selectedDateText }}</h3>
|
||||
<p v-if="dateTransactions.length">
|
||||
共 {{ dateTransactions.length }} 笔交易,
|
||||
<span v-html="getBalance(dateTransactions)" />
|
||||
</p>
|
||||
<van-icon
|
||||
name="cross"
|
||||
class="close-icon"
|
||||
@click="listVisible = false"
|
||||
/>
|
||||
<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 class="popup-scroll-content">
|
||||
@@ -56,6 +67,7 @@ import request from "@/api/request";
|
||||
import { getTransactionDetail, getTransactionsByDate } from "@/api/transactionRecord";
|
||||
import TransactionList from "@/components/TransactionList.vue";
|
||||
import TransactionDetail from "@/components/TransactionDetail.vue";
|
||||
import SmartClassifyButton from "@/components/SmartClassifyButton.vue";
|
||||
|
||||
const dailyStatistics = ref({});
|
||||
const listVisible = ref(false);
|
||||
@@ -107,6 +119,7 @@ const fetchDailyStatistics = async (year, month) => {
|
||||
}
|
||||
};
|
||||
|
||||
const smartClassifyButtonRef = ref(null);
|
||||
// 获取指定日期的交易列表
|
||||
const fetchDateTransactions = async (date) => {
|
||||
try {
|
||||
@@ -122,6 +135,8 @@ const fetchDateTransactions = async (date) => {
|
||||
dateTransactions.value = response
|
||||
.data
|
||||
.sort((a, b) => b.amount - a.amount);
|
||||
// 重置智能分类按钮
|
||||
smartClassifyButtonRef.value.reset()
|
||||
} else {
|
||||
dateTransactions.value = [];
|
||||
showToast(response.message || "获取交易列表失败");
|
||||
@@ -205,6 +220,17 @@ const onDetailSave = () => {
|
||||
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 dayCopy = { ...day };
|
||||
if (dayCopy.date.toDateString() === new Date().toDateString()) {
|
||||
@@ -264,4 +290,45 @@ fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
|
||||
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>
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
{{ 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>
|
||||
<template #right-icon>
|
||||
@@ -483,6 +486,12 @@ onMounted(() => {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.amount-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #ff976a;
|
||||
}
|
||||
|
||||
.unclassified-stat {
|
||||
padding-left: 16px;
|
||||
padding-top: 12px;
|
||||
|
||||
@@ -10,42 +10,70 @@
|
||||
<div class="scroll-content" style="padding-top: 5px;">
|
||||
<!-- 统计信息 -->
|
||||
<div class="stats-info">
|
||||
<span class="stats-label">未分类账单:</span>
|
||||
<span class="stats-value">{{ records.length }} / {{ unclassifiedCount }}</span>
|
||||
<span class="stats-label">未分类账单 </span>
|
||||
<span class="stats-value">{{ unclassifiedCount }} 条,本次分类 {{ reasonGroups.length }} 组</span>
|
||||
</div>
|
||||
|
||||
<!-- 账单列表 -->
|
||||
<TransactionList
|
||||
:transactions="records"
|
||||
:loading="false"
|
||||
:show-delete="false"
|
||||
:show-checkbox="true"
|
||||
:selected-ids="selectedIds"
|
||||
@click="viewDetail"
|
||||
@update:selected-ids="selectedIds = $event"
|
||||
/>
|
||||
<!-- 分组列表 -->
|
||||
<van-empty v-if="reasonGroups.length === 0 && !loading" description="暂无未分类账单" />
|
||||
|
||||
<van-cell-group v-else inset>
|
||||
<van-cell
|
||||
v-for="group in reasonGroups"
|
||||
:key="group.reason"
|
||||
clickable
|
||||
>
|
||||
<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>
|
||||
|
||||
<!-- 详情/编辑弹出层 -->
|
||||
<TransactionDetail
|
||||
v-model:show="detailVisible"
|
||||
:transaction="currentTransaction"
|
||||
@save="onDetailSave"
|
||||
/>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<div class="action-bar">
|
||||
<van-button
|
||||
type="primary"
|
||||
:loading="classifying"
|
||||
:disabled="selectedIds.size === 0"
|
||||
:disabled="selectedReasons.size === 0"
|
||||
@click="startClassify"
|
||||
class="action-btn"
|
||||
>
|
||||
{{ classifying ? '分类中...' : `开始分类 (${selectedIds.size}/${records.length})` }}
|
||||
{{ classifying ? '分类中...' : `开始分类 (${selectedReasons.size}组)` }}
|
||||
</van-button>
|
||||
|
||||
<van-button
|
||||
@@ -66,22 +94,18 @@ import { useRouter } from 'vue-router'
|
||||
import { showToast, showLoadingToast, closeToast, showConfirmDialog } from 'vant'
|
||||
import {
|
||||
getUnclassifiedCount,
|
||||
getUnclassified,
|
||||
getReasonGroups,
|
||||
smartClassify,
|
||||
batchUpdateClassify,
|
||||
getTransactionDetail
|
||||
batchUpdateClassify
|
||||
} from '@/api/transactionRecord'
|
||||
import TransactionList from '@/components/TransactionList.vue'
|
||||
import TransactionDetail from '@/components/TransactionDetail.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const unclassifiedCount = ref(0)
|
||||
const records = ref([])
|
||||
const selectedIds = ref(new Set()) // 选中的账单ID集合
|
||||
const reasonGroups = ref([]) // 改为分组数据
|
||||
const selectedReasons = ref(new Set()) // 选中的分组摘要集合
|
||||
const loading = ref(false)
|
||||
const classifying = ref(false)
|
||||
const hasChanges = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const currentTransaction = ref(null)
|
||||
const classifyBuffer = ref('') // SSE数据缓冲区
|
||||
|
||||
const onClickLeft = () => {
|
||||
@@ -109,37 +133,80 @@ const loadUnclassifiedCount = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载未分类账单列表
|
||||
const loadUnclassified = async () => {
|
||||
// 加载分组数据
|
||||
const loadReasonGroups = async () => {
|
||||
showLoadingToast({
|
||||
message: '加载中...',
|
||||
forbidClick: true,
|
||||
duration: 0
|
||||
})
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const res = await getUnclassified(10)
|
||||
// 获取所有未分类的分组,设置较大的pageSize以获取所有数据
|
||||
const res = await getReasonGroups(1, 20)
|
||||
if (res.success) {
|
||||
records.value = res.data
|
||||
// 默认全选所有账单
|
||||
selectedIds.value = new Set(res.data.map(r => r.id))
|
||||
// 后端已经按数量排序,我们需要计算每个分组的总金额并重新排序
|
||||
// 但是后端DTO没有返回总金额,我们先按数量排序即可
|
||||
reasonGroups.value = res.data || []
|
||||
// 默认全选所有分组
|
||||
selectedReasons.value = new Set(reasonGroups.value.map(g => g.reason))
|
||||
} else {
|
||||
showToast(res.message || '加载失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载账单失败', error)
|
||||
console.error('加载分组失败', error)
|
||||
showToast('加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
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 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) {
|
||||
showToast('请先选择要分类的账单')
|
||||
showToast('请先选择要分类的账单组')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -152,6 +219,9 @@ const startClassify = async () => {
|
||||
classifying.value = true
|
||||
classifyBuffer.value = '' // 重置缓冲区
|
||||
|
||||
// 用于存储分类结果的临时对象
|
||||
const classifyResults = new Map() // id -> {classify, type}
|
||||
|
||||
try {
|
||||
const response = await smartClassify(idsToClassify)
|
||||
|
||||
@@ -182,7 +252,7 @@ const startClassify = async () => {
|
||||
const eventType = eventMatch[1]
|
||||
const data = dataMatch[1]
|
||||
|
||||
handleSSEEvent(eventType, data)
|
||||
handleSSEEvent(eventType, data, classifyResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,19 +267,17 @@ const startClassify = async () => {
|
||||
}
|
||||
|
||||
// 处理SSE事件
|
||||
const handleSSEEvent = (eventType, data) => {
|
||||
const handleSSEEvent = (eventType, data, classifyResults) => {
|
||||
if (eventType === 'data') {
|
||||
try {
|
||||
// 累积AI输出的JSON片段
|
||||
classifyBuffer.value += data
|
||||
|
||||
// 尝试查找并提取完整的JSON对象
|
||||
// 使用更精确的方式:查找 { 和匹配的 }
|
||||
let startIndex = 0
|
||||
while (startIndex < classifyBuffer.value.length) {
|
||||
const openBrace = classifyBuffer.value.indexOf('{', startIndex)
|
||||
if (openBrace === -1) {
|
||||
// 没有找到开始的 {,清理前面的无用字符
|
||||
classifyBuffer.value = ''
|
||||
break
|
||||
}
|
||||
@@ -229,32 +297,37 @@ const handleSSEEvent = (eventType, data) => {
|
||||
}
|
||||
|
||||
if (closeBrace !== -1) {
|
||||
// 找到了完整的JSON
|
||||
const jsonStr = classifyBuffer.value.substring(openBrace, closeBrace + 1)
|
||||
|
||||
try {
|
||||
const result = JSON.parse(jsonStr)
|
||||
|
||||
if (result.id) {
|
||||
const record = records.value.find(r => r.id === result.id)
|
||||
if (record) {
|
||||
record.classify = result.classify || ''
|
||||
// 如果AI返回了type字段,也更新type
|
||||
if (result.type !== undefined && result.type !== null) {
|
||||
record.type = result.type
|
||||
// 存储分类结果
|
||||
classifyResults.set(result.id, {
|
||||
classify: result.classify || '',
|
||||
type: result.type !== undefined ? result.type : null
|
||||
})
|
||||
|
||||
// 更新对应分组的显示状态
|
||||
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) {
|
||||
console.error('JSON解析失败:', e)
|
||||
}
|
||||
|
||||
// 移除已处理的部分
|
||||
classifyBuffer.value = classifyBuffer.value.substring(closeBrace + 1)
|
||||
startIndex = 0 // 从头开始查找下一个JSON
|
||||
startIndex = 0
|
||||
} else {
|
||||
// 没有找到闭合括号,说明JSON还不完整,等待更多数据
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -274,13 +347,20 @@ const handleSSEEvent = (eventType, data) => {
|
||||
|
||||
// 保存分类
|
||||
const saveClassifications = async () => {
|
||||
const itemsToUpdate = records.value
|
||||
.filter(r => r.classify)
|
||||
.map(r => ({
|
||||
id: r.id,
|
||||
classify: r.classify,
|
||||
type: r.type
|
||||
}))
|
||||
// 收集所有已分类的账单
|
||||
const itemsToUpdate = []
|
||||
for (const group of reasonGroups.value) {
|
||||
if (group.sampleClassify) {
|
||||
// 为该分组的所有账单添加分类
|
||||
for (const id of group.transactionIds) {
|
||||
itemsToUpdate.push({
|
||||
id: id,
|
||||
classify: group.sampleClassify,
|
||||
type: group.sampleType
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (itemsToUpdate.length === 0) {
|
||||
showToast('没有需要保存的分类')
|
||||
@@ -300,7 +380,7 @@ const saveClassifications = async () => {
|
||||
hasChanges.value = false
|
||||
// 重新加载数据
|
||||
await loadUnclassifiedCount()
|
||||
await loadUnclassified()
|
||||
await loadReasonGroups()
|
||||
} else {
|
||||
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(() => {
|
||||
loadUnclassifiedCount()
|
||||
loadUnclassified()
|
||||
loadReasonGroups()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -353,6 +410,39 @@ onMounted(() => {
|
||||
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 {
|
||||
position: fixed;
|
||||
|
||||
@@ -154,7 +154,7 @@ public class LogController(ILogger<LogController> logger) : ControllerBase
|
||||
// 使用正则表达式解析
|
||||
var match = System.Text.RegularExpressions.Regex.Match(
|
||||
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)
|
||||
|
||||
@@ -8,6 +8,7 @@ public class TransactionRecordController(
|
||||
ITransactionRecordRepository transactionRepository,
|
||||
ITransactionCategoryRepository categoryRepository,
|
||||
IOpenAiService openAiService,
|
||||
ITextSegmentService textSegmentService,
|
||||
ILogger<TransactionRecordController> logger
|
||||
) : ControllerBase
|
||||
{
|
||||
@@ -31,18 +32,18 @@ public class TransactionRecordController(
|
||||
TransactionType? transactionType = type.HasValue ? (TransactionType)type.Value : null;
|
||||
var list = await transactionRepository.GetPagedListAsync(
|
||||
pageIndex,
|
||||
pageSize,
|
||||
searchKeyword,
|
||||
classify,
|
||||
transactionType,
|
||||
year,
|
||||
pageSize,
|
||||
searchKeyword,
|
||||
classify,
|
||||
transactionType,
|
||||
year,
|
||||
month,
|
||||
sortByAmount);
|
||||
var total = await transactionRepository.GetTotalCountAsync(
|
||||
searchKeyword,
|
||||
classify,
|
||||
transactionType,
|
||||
year,
|
||||
searchKeyword,
|
||||
classify,
|
||||
transactionType,
|
||||
year,
|
||||
month);
|
||||
|
||||
return new PagedResponse<TransactionRecord>
|
||||
@@ -392,7 +393,7 @@ public class TransactionRecordController(
|
||||
""";
|
||||
|
||||
var sqlText = await openAiService.ChatAsync(sqlPrompt);
|
||||
|
||||
|
||||
// 清理SQL文本
|
||||
sqlText = sqlText?.Trim() ?? "";
|
||||
sqlText = sqlText.TrimStart('`').TrimEnd('`');
|
||||
@@ -421,11 +422,11 @@ public class TransactionRecordController(
|
||||
|
||||
// 第三步:将查询结果序列化为JSON,直接传递给AI生成分析报告
|
||||
var dataJson = System.Text.Json.JsonSerializer.Serialize(queryResults, new JsonSerializerOptions
|
||||
{
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
});
|
||||
|
||||
|
||||
var dataPrompt = $"""
|
||||
当前日期:{DateTime.Now:yyyy年M月d日}
|
||||
用户问题:{request.UserInput}
|
||||
@@ -577,23 +578,64 @@ public class TransactionRecordController(
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取指定ID的账单
|
||||
var records = new List<TransactionRecord>();
|
||||
foreach (var id in request.TransactionIds)
|
||||
{
|
||||
var record = await transactionRepository.GetByIdAsync(id);
|
||||
if (record != null && record.Classify == string.Empty)
|
||||
{
|
||||
records.Add(record);
|
||||
}
|
||||
}
|
||||
// 获取指定ID的账单(作为样本)
|
||||
var sampleRecords = await transactionRepository.GetByIdsAsync(request.TransactionIds.ToArray());
|
||||
|
||||
if (records.Count == 0)
|
||||
if (sampleRecords.Length == 0)
|
||||
{
|
||||
await WriteEventAsync("error", "找不到指定的账单");
|
||||
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();
|
||||
|
||||
@@ -610,28 +652,43 @@ public class TransactionRecordController(
|
||||
}
|
||||
}
|
||||
|
||||
// 构建账单信息
|
||||
var billsInfo = string.Join("\n", records.Select((r, i) =>
|
||||
$"{i + 1}. ID={r.Id}, 摘要={r.Reason}, 金额={r.Amount}, 类型={GetTypeName(r.Type)}"));
|
||||
// 构建账单分组信息
|
||||
var billsInfo = new StringBuilder();
|
||||
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 = $$"""
|
||||
你是一个专业的账单分类助手。请根据提供的账单信息和分类列表,为每个账单选择最合适的分类。
|
||||
你是一个专业的账单分类助手。请根据提供的账单分组信息和分类列表,为每个分组选择最合适的分类。
|
||||
|
||||
可用的分类列表:
|
||||
{{categoryInfo}}
|
||||
|
||||
分类规则:
|
||||
1. 根据账单的摘要和金额,选择最匹配的分类
|
||||
2. 如果无法确定分类,可以选择""其他""
|
||||
1. 根据账单的摘要和涉及金额,选择最匹配的分类
|
||||
2. 如果提供了【参考】信息,优先参考相似账单的分类,这些是历史上已分类的相似账单
|
||||
3. 如果无法确定分类,可以选择"其他"
|
||||
4. 每个分组可能包含多条账单,你需要为整个分组选择一个分类
|
||||
|
||||
请对每个账单进行分类,每次输出一个账单的分类结果,格式如下:
|
||||
{"id": 账单ID, "type": 0:支出/1:收入/2:不计入收支(Type为Number枚举值) ,"classify": 分类名称}
|
||||
请对每个分组进行分类,每次输出一个分组的分类结果,格式如下:
|
||||
{"reason": "交易摘要", "type": 0:支出/1:收入/2:不计入收支(Type为Number枚举值) ,"classify": "分类名称"}
|
||||
|
||||
只输出JSON,不要有其他文字说明。
|
||||
""";
|
||||
|
||||
var userPrompt = $$"""
|
||||
请为以下账单进行分类:
|
||||
请为以下账单分组进行分类:
|
||||
|
||||
{{billsInfo}}
|
||||
|
||||
@@ -639,11 +696,58 @@ public class TransactionRecordController(
|
||||
""";
|
||||
|
||||
// 流式调用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 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", "分类完成");
|
||||
@@ -853,7 +957,7 @@ public class TransactionRecordController(
|
||||
// 根据关键词查询交易记录
|
||||
var allRecords = await transactionRepository.ExecuteRawSqlAsync(analysisInfo.Sql);
|
||||
logger.LogInformation("NLP分析查询到 {Count} 条记录,SQL: {Sql}", allRecords.Count, analysisInfo.Sql);
|
||||
|
||||
|
||||
// 为每条记录预设分类
|
||||
var recordsWithClassify = allRecords.Select(r => new TransactionRecordWithClassify
|
||||
{
|
||||
@@ -898,6 +1002,24 @@ public class TransactionRecordController(
|
||||
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)
|
||||
{
|
||||
return type switch
|
||||
@@ -949,6 +1071,21 @@ public record SmartClassifyRequest(
|
||||
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>
|
||||
/// 批量更新分类项DTO
|
||||
/// </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>
|
||||
</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>
|
||||
<Watch Remove="logs/**" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user