添加功能
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 29s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s

This commit is contained in:
孙诚
2025-12-29 20:30:15 +08:00
parent a13e1fe9e8
commit 0d94276a0d
22 changed files with 560706 additions and 138 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View 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

File diff suppressed because one or more lines are too long

View 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>

View File

@@ -1,4 +1,4 @@
/* eslint-disable no-console */
export function register() {
if ('serviceWorker' in navigator) {

View File

@@ -17,15 +17,26 @@
position="bottom"
:style="{ height: '85%' }"
round
closeable
>
<div class="popup-container">
<div class="popup-header-fixed">
<h3>{{ selectedDateText }}</h3>
<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>

View File

@@ -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;

View File

@@ -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
// 存储分类结果
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) {
record.type = result.type
group.sampleType = result.type
}
hasChanges.value = true
break
}
}
}
} 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;

View File

@@ -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)

View File

@@ -8,6 +8,7 @@ public class TransactionRecordController(
ITransactionRecordRepository transactionRepository,
ITransactionCategoryRepository categoryRepository,
IOpenAiService openAiService,
ITextSegmentService textSegmentService,
ILogger<TransactionRecordController> logger
) : ControllerBase
{
@@ -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", "分类完成");
@@ -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>

File diff suppressed because it is too large Load Diff

349046
WebApi/Resources.bak/dict.txt Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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
}
}

View File

@@ -0,0 +1,330 @@
一个
没有
自己
可以
因为
所以
如果
已经
什么
怎么
为什么
多少
这个
那个
这些
那些
哪些
什么样
怎样
如何
多么
何等
若干
左右
前后
上下
东西
南北
里外
内外
大小
高低
长短
多少
好坏
新旧
早晚
前面
后面
上面
下面
左边
右边
里面
外面
中间
旁边
附近
周围
全部
所有
一切
任何
每个
各自
彼此
相互
互相
共同
一起
同时
当时
平时
随时
及时
准时
按时
到时
届时
临时
暂时
长期
短期
永远
从来
向来
素来
一向
历来
总是
常常
往往
通常
一般
大概
大约
左右
上下
几乎
差不多
可能
也许
大概
或许
恐怕
难道
究竟
到底
果然
居然
竟然
偏偏
简直
实在
的确
确实
真正
真是
果真
当真
委实
着实
十分
非常
稍微
一点
有点
一些
若干
许多
很多
好多
大量
少量
大批
成批
整个
全部
部分
多半
大半
少半
绝大部分
绝大多数
极少数
等等
之类
以及
及其
乃至
甚至
甚而
进而
从而
因而
所以
因此
于是
然后
接着
随后
继而
终于
最后
最终
结果
总之
综上所述
由此可见
显而易见
众所周知
不言而喭
毫无疑问
毋庸置疑
无可置疑
不容置疑
无庸讳言
何必
何况
何妨
反正
不过
只是
但是
然而
可是
只不过
不外
无非
虽然
尽管
即使
就算
哪怕
纵使
纵然
既然
假如
如果
倘若
要是
万一
除非
不管
无论
不论
任凭
只要
只有
除了
除去
除开
撇开
此外
另外
以外
之外
不仅
不但
不只
不光
而且
并且
况且
何况
甚至
以至
乃至
与其
宁可
宁愿
还是
或者
抑或
要么
不是
就是
是否
难道
莫非

View File

@@ -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>