移除对账功能 后期从长计议
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 1m57s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 1m57s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
This commit is contained in:
202
AGENTS.md
Normal file
202
AGENTS.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# AGENTS.md - EmailBill Project Guidelines
|
||||
|
||||
Full-stack budget tracking app with .NET 10 backend and Vue 3 frontend.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
EmailBill/
|
||||
├── Common/ # Shared utilities and abstractions
|
||||
├── Entity/ # Database entities (FreeSql ORM)
|
||||
├── Repository/ # Data access layer
|
||||
├── Service/ # Business logic layer
|
||||
├── WebApi/ # ASP.NET Core Web API
|
||||
├── WebApi.Test/ # Backend tests (xUnit)
|
||||
└── Web/ # Vue 3 frontend (Vite + Vant UI)
|
||||
```
|
||||
|
||||
## Build & Test Commands
|
||||
|
||||
### Backend (.NET 10)
|
||||
```bash
|
||||
# Build and run
|
||||
dotnet build EmailBill.sln
|
||||
dotnet run --project WebApi/WebApi.csproj
|
||||
|
||||
# Run all tests
|
||||
dotnet test WebApi.Test/WebApi.Test.csproj
|
||||
|
||||
# Run single test class
|
||||
dotnet test --filter "FullyQualifiedName~BudgetStatsTest"
|
||||
|
||||
# Run single test method
|
||||
dotnet test --filter "FullyQualifiedName~BudgetStatsTest.GetCategoryStats_月度_Test"
|
||||
|
||||
# Clean
|
||||
dotnet clean EmailBill.sln
|
||||
```
|
||||
|
||||
### Frontend (Vue 3)
|
||||
```bash
|
||||
cd Web
|
||||
|
||||
# Setup and dev
|
||||
pnpm install
|
||||
pnpm dev
|
||||
|
||||
# Build and preview
|
||||
pnpm build
|
||||
pnpm preview
|
||||
|
||||
# Lint and format
|
||||
pnpm lint # ESLint with auto-fix
|
||||
pnpm format # Prettier formatting
|
||||
```
|
||||
|
||||
## C# Code Style
|
||||
|
||||
**Namespaces & Imports:**
|
||||
- File-scoped namespaces: `namespace Entity;`
|
||||
- Global usings in `Common/GlobalUsings.cs`
|
||||
- Sort using statements alphabetically
|
||||
|
||||
**Naming:**
|
||||
- Classes/Methods: `PascalCase`
|
||||
- Interfaces: `IPascalCase`
|
||||
- Private fields: `_camelCase`
|
||||
- Parameters/locals: `camelCase`
|
||||
|
||||
**Entities:**
|
||||
- Inherit from `BaseEntity`
|
||||
- Use `[Column]` attributes for FreeSql ORM
|
||||
- IDs via Snowflake: `YitIdHelper.NextId()`
|
||||
- Use XML docs (`///`) for public APIs
|
||||
- **Chinese comments for business logic** (per `.github/csharpe.prompt.md`)
|
||||
|
||||
**Best Practices:**
|
||||
- Use modern C# syntax (records, pattern matching, nullable types)
|
||||
- Use `IDateTimeProvider` instead of `DateTime.Now` for testability
|
||||
- Avoid deep nesting, keep code flat and readable
|
||||
- Reuse utilities from `Common` project
|
||||
|
||||
**Example:**
|
||||
```csharp
|
||||
namespace Entity;
|
||||
|
||||
/// <summary>
|
||||
/// 实体基类
|
||||
/// </summary>
|
||||
public abstract class BaseEntity
|
||||
{
|
||||
[Column(IsPrimary = true)]
|
||||
public long Id { get; set; } = YitIdHelper.NextId();
|
||||
|
||||
public DateTime CreateTime { get; set; } = DateTime.Now;
|
||||
}
|
||||
```
|
||||
|
||||
## Vue/TypeScript Style
|
||||
|
||||
**Component Structure:**
|
||||
```vue
|
||||
<template>
|
||||
<van-config-provider :theme="theme">
|
||||
<div class="component-name">
|
||||
<!-- Content -->
|
||||
</div>
|
||||
</van-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useMessageStore } from '@/stores/message'
|
||||
|
||||
const messageStore = useMessageStore()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.component-name {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Composition API with `<script setup lang="ts">`
|
||||
- Import order: Vue APIs → external libs → internal modules
|
||||
- Use `@/` alias for absolute imports, avoid `../../../`
|
||||
- Vant UI components: `<van-*>`
|
||||
- Pinia for state, Vue Router for navigation
|
||||
- SCSS with BEM naming, mobile-first design
|
||||
|
||||
**ESLint Rules (see `Web/eslint.config.js`):**
|
||||
- 2-space indentation
|
||||
- Single quotes, no semicolons
|
||||
- `const` over `let`, no `var`
|
||||
- Always use `===` (strict equality)
|
||||
- `space-before-function-paren: 'always'`
|
||||
- Max 1 empty line between blocks
|
||||
- Vue: multi-word component names disabled
|
||||
|
||||
**Prettier Rules (see `Web/.prettierrc.json`):**
|
||||
- Single quotes, no semicolons
|
||||
- Trailing commas: none
|
||||
- Print width: 100 chars
|
||||
|
||||
## Testing
|
||||
|
||||
**Backend (xUnit + NSubstitute + FluentAssertions):**
|
||||
```csharp
|
||||
public class BudgetStatsTest : BaseTest
|
||||
{
|
||||
private readonly IBudgetRepository _repo = Substitute.For<IBudgetRepository>();
|
||||
|
||||
[Fact]
|
||||
public async Task GetCategoryStats_月度_Test()
|
||||
{
|
||||
// Arrange
|
||||
_repo.GetAllAsync().Returns(testData);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetCategoryStatsAsync(category, date);
|
||||
|
||||
// Assert
|
||||
result.Month.Limit.Should().Be(2500);
|
||||
}
|
||||
}
|
||||
```
|
||||
- Arrange-Act-Assert pattern
|
||||
- Constructor injection for dependencies
|
||||
- Use Chinese test method names for domain clarity
|
||||
|
||||
**Frontend:**
|
||||
- Vue Test Utils for components
|
||||
- axios-mock-adapter for API mocking
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Before committing backend:** `dotnet test`
|
||||
2. **Before committing frontend:** `pnpm lint && pnpm build`
|
||||
3. **Database migrations:** Use FreeSql (check `Repository/`)
|
||||
4. **API docs:** Scalar OpenAPI viewer
|
||||
|
||||
## Environment
|
||||
|
||||
**Required:**
|
||||
- .NET 10 SDK
|
||||
- Node.js 20.19+ or 22.12+
|
||||
- pnpm
|
||||
|
||||
**Database:** SQLite (embedded)
|
||||
|
||||
**Config:**
|
||||
- Backend: `appsettings.json`
|
||||
- Frontend: `.env.development` / `.env.production`
|
||||
|
||||
## Critical Guidelines (from `.github/csharpe.prompt.md`)
|
||||
|
||||
- 优先使用新C#语法 (Use modern C# syntax)
|
||||
- 优先使用中文注释 (Prefer Chinese comments for business logic)
|
||||
- 优先复用已有方法 (Reuse existing methods)
|
||||
- 不要深嵌套代码 (Avoid deep nesting)
|
||||
- 保持代码简洁易读 (Keep code clean and readable)
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Entity;
|
||||
namespace Entity;
|
||||
|
||||
/// <summary>
|
||||
/// 银行交易记录(由邮件解析生成)
|
||||
@@ -20,11 +20,6 @@ public class TransactionRecord : BaseEntity
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 退款金额
|
||||
/// </summary>
|
||||
public decimal RefundAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 交易后余额
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Repository;
|
||||
namespace Repository;
|
||||
|
||||
public interface ITransactionRecordRepository : IBaseRepository<TransactionRecord>
|
||||
{
|
||||
@@ -178,15 +178,6 @@ public interface ITransactionRecordRepository : IBaseRepository<TransactionRecor
|
||||
/// <returns>带相关度分数的已分类账单列表</returns>
|
||||
Task<List<(TransactionRecord record, double relevanceScore)>> GetClassifiedByKeywordsWithScoreAsync(List<string> keywords, double minMatchRate = 0.3, int limit = 10);
|
||||
|
||||
/// <summary>
|
||||
/// 获取抵账候选列表
|
||||
/// </summary>
|
||||
/// <param name="currentId">当前交易ID</param>
|
||||
/// <param name="amount">当前交易金额</param>
|
||||
/// <param name="currentType">当前交易类型</param>
|
||||
/// <returns>候选交易列表</returns>
|
||||
Task<List<TransactionRecord>> GetCandidatesForOffsetAsync(long currentId, decimal amount, TransactionType currentType);
|
||||
|
||||
/// <summary>
|
||||
/// 获取待确认分类的账单列表
|
||||
/// </summary>
|
||||
@@ -676,33 +667,6 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
return scoredResults;
|
||||
}
|
||||
|
||||
public async Task<List<TransactionRecord>> GetCandidatesForOffsetAsync(long currentId, decimal amount, TransactionType currentType)
|
||||
{
|
||||
var absAmount = Math.Abs(amount);
|
||||
var minAmount = absAmount - 5;
|
||||
var maxAmount = absAmount + 5;
|
||||
|
||||
var currentRecord = await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.Id == currentId)
|
||||
.FirstAsync();
|
||||
|
||||
if (currentRecord == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var list = await FreeSql.Select<TransactionRecord>()
|
||||
.Where(t => t.Id != currentId)
|
||||
.Where(t => t.Type != currentType)
|
||||
.Where(t => Math.Abs(t.Amount) >= minAmount && Math.Abs(t.Amount) <= maxAmount)
|
||||
.Take(50)
|
||||
.ToListAsync();
|
||||
|
||||
return list.OrderBy(t => Math.Abs(Math.Abs(t.Amount) - absAmount))
|
||||
.ThenBy(x => Math.Abs((x.OccurredAt - currentRecord.OccurredAt).TotalSeconds))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<int> UpdateCategoryNameAsync(string oldName, string newName, TransactionType type)
|
||||
{
|
||||
return await FreeSql.Update<TransactionRecord>()
|
||||
@@ -769,7 +733,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
|
||||
{
|
||||
g.Key.Classify,
|
||||
g.Key.Type,
|
||||
TotalAmount = g.Sum(g.Value.Amount - g.Value.RefundAmount)
|
||||
TotalAmount = g.Sum(g.Value.Amount)
|
||||
});
|
||||
|
||||
return result.ToDictionary(x => (x.Classify, x.Type), x => x.TotalAmount);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Service;
|
||||
namespace Service;
|
||||
|
||||
public interface ISmartHandleService
|
||||
{
|
||||
|
||||
@@ -87,8 +87,6 @@ export const getDailyStatistics = (params) => {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取累积余额统计数据(用于余额卡片)
|
||||
* @param {Object} params - 查询参数
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import request from './request'
|
||||
import request from './request'
|
||||
|
||||
/**
|
||||
* 交易记录相关 API
|
||||
@@ -223,32 +223,6 @@ export const nlpAnalysis = (userInput) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取抵账候选列表
|
||||
* @param {number} id - 当前交易ID
|
||||
* @returns {Promise<{success: boolean, data: Array}>}
|
||||
*/
|
||||
export const getCandidatesForOffset = (id) => {
|
||||
return request({
|
||||
url: `/TransactionRecord/GetCandidatesForOffset/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 抵账(删除两笔交易)
|
||||
* @param {number} id1 - 交易ID 1
|
||||
* @param {number} id2 - 交易ID 2
|
||||
* @returns {Promise<{success: boolean}>}
|
||||
*/
|
||||
export const offsetTransactions = (id1, id2) => {
|
||||
return request({
|
||||
url: '/TransactionRecord/OffsetTransactions',
|
||||
method: 'post',
|
||||
data: { id1, id2 }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 一句话录账解析
|
||||
* @param {string} text - 用户输入的自然语言文本
|
||||
|
||||
@@ -38,30 +38,58 @@
|
||||
/>
|
||||
|
||||
<!-- 交易类型 -->
|
||||
<van-field name="type" label="类型">
|
||||
<van-field
|
||||
name="type"
|
||||
label="类型"
|
||||
>
|
||||
<template #input>
|
||||
<van-radio-group v-model="form.type" direction="horizontal" @change="handleTypeChange">
|
||||
<van-radio :name="0"> 支出 </van-radio>
|
||||
<van-radio :name="1"> 收入 </van-radio>
|
||||
<van-radio :name="2"> 不计 </van-radio>
|
||||
<van-radio-group
|
||||
v-model="form.type"
|
||||
direction="horizontal"
|
||||
@change="handleTypeChange"
|
||||
>
|
||||
<van-radio :name="0">
|
||||
支出
|
||||
</van-radio>
|
||||
<van-radio :name="1">
|
||||
收入
|
||||
</van-radio>
|
||||
<van-radio :name="2">
|
||||
不计
|
||||
</van-radio>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<!-- 分类 -->
|
||||
<van-field name="category" label="分类">
|
||||
<van-field
|
||||
name="category"
|
||||
label="分类"
|
||||
>
|
||||
<template #input>
|
||||
<span v-if="!categoryName" style="color: var(--van-text-color-3)">请选择分类</span>
|
||||
<span
|
||||
v-if="!categoryName"
|
||||
style="color: var(--van-text-color-3)"
|
||||
>请选择分类</span>
|
||||
<span v-else>{{ categoryName }}</span>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<!-- 分类选择组件 -->
|
||||
<ClassifySelector v-model="categoryName" :type="form.type" />
|
||||
<ClassifySelector
|
||||
v-model="categoryName"
|
||||
:type="form.type"
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="actions">
|
||||
<van-button round block type="primary" native-type="submit" :loading="loading">
|
||||
<van-button
|
||||
round
|
||||
block
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
:loading="loading"
|
||||
>
|
||||
{{ submitText }}
|
||||
</van-button>
|
||||
<slot name="actions" />
|
||||
@@ -69,7 +97,12 @@
|
||||
</van-form>
|
||||
|
||||
<!-- 日期选择弹窗 -->
|
||||
<van-popup v-model:show="showDatePicker" position="bottom" round teleport="body">
|
||||
<van-popup
|
||||
v-model:show="showDatePicker"
|
||||
position="bottom"
|
||||
round
|
||||
teleport="body"
|
||||
>
|
||||
<van-date-picker
|
||||
v-model="currentDate"
|
||||
title="选择日期"
|
||||
@@ -79,7 +112,12 @@
|
||||
</van-popup>
|
||||
|
||||
<!-- 时间选择弹窗 -->
|
||||
<van-popup v-model:show="showTimePicker" position="bottom" round teleport="body">
|
||||
<van-popup
|
||||
v-model:show="showTimePicker"
|
||||
position="bottom"
|
||||
round
|
||||
teleport="body"
|
||||
>
|
||||
<van-time-picker
|
||||
v-model="currentTime"
|
||||
title="选择时间"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<div class="manual-bill-add">
|
||||
<BillForm ref="billFormRef" :loading="saving" @submit="handleSave" />
|
||||
<BillForm
|
||||
ref="billFormRef"
|
||||
:loading="saving"
|
||||
@submit="handleSave"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!parseResult" class="input-section" style="margin: 12px 12px 0 16px">
|
||||
<div
|
||||
v-if="!parseResult"
|
||||
class="input-section"
|
||||
style="margin: 12px 12px 0 16px"
|
||||
>
|
||||
<van-field
|
||||
v-model="text"
|
||||
type="textarea"
|
||||
@@ -23,7 +27,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="parseResult" class="result-section">
|
||||
<div
|
||||
v-if="parseResult"
|
||||
class="result-section"
|
||||
>
|
||||
<BillForm
|
||||
:initial-data="parseResult"
|
||||
:loading="saving"
|
||||
@@ -31,7 +38,13 @@
|
||||
@submit="handleSave"
|
||||
>
|
||||
<template #actions>
|
||||
<van-button plain round block class="mt-2" @click="parseResult = null">
|
||||
<van-button
|
||||
plain
|
||||
round
|
||||
block
|
||||
class="mt-2"
|
||||
@click="parseResult = null"
|
||||
>
|
||||
重新输入
|
||||
</van-button>
|
||||
</template>
|
||||
|
||||
@@ -1,21 +1,35 @@
|
||||
<template>
|
||||
<div class="summary-container">
|
||||
<transition :name="transitionName" mode="out-in">
|
||||
<transition
|
||||
:name="transitionName"
|
||||
mode="out-in"
|
||||
>
|
||||
<div
|
||||
v-if="stats && (stats.month || stats.year)"
|
||||
:key="dateKey"
|
||||
class="summary-card common-card"
|
||||
>
|
||||
<!-- 左切换按钮 -->
|
||||
<div class="nav-arrow left" @click.stop="changeMonth(-1)">
|
||||
<div
|
||||
class="nav-arrow left"
|
||||
@click.stop="changeMonth(-1)"
|
||||
>
|
||||
<van-icon name="arrow-left" />
|
||||
</div>
|
||||
|
||||
<div class="summary-content">
|
||||
<template v-for="(config, key) in periodConfigs" :key="key">
|
||||
<template
|
||||
v-for="(config, key) in periodConfigs"
|
||||
:key="key"
|
||||
>
|
||||
<div class="summary-item">
|
||||
<div class="label">{{ config.label }}{{ title }}率</div>
|
||||
<div class="value" :class="getValueClass(stats[key]?.rate || '0.0')">
|
||||
<div class="label">
|
||||
{{ config.label }}{{ title }}率
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
:class="getValueClass(stats[key]?.rate || '0.0')"
|
||||
>
|
||||
{{ stats[key]?.rate || '0.0' }}<span class="unit">%</span>
|
||||
</div>
|
||||
<div class="sub-info">
|
||||
@@ -24,7 +38,10 @@
|
||||
<span class="amount">¥{{ formatMoney(stats[key]?.limit || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="config.showDivider" class="divider" />
|
||||
<div
|
||||
v-if="config.showDivider"
|
||||
class="divider"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +55,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 非本月时显示的日期标识 -->
|
||||
<div v-if="!isCurrentMonth" class="date-tag">
|
||||
<div
|
||||
v-if="!isCurrentMonth"
|
||||
class="date-tag"
|
||||
>
|
||||
{{ props.date.getFullYear() }}年{{ props.date.getMonth() + 1 }}月
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<template>
|
||||
<PopupContainer v-model="visible" title="设置存款分类" height="60%">
|
||||
<PopupContainer
|
||||
v-model="visible"
|
||||
title="设置存款分类"
|
||||
height="60%"
|
||||
>
|
||||
<div class="savings-config-content">
|
||||
<div class="config-header">
|
||||
<p class="subtitle">这些分类的统计值将计入“存款”中</p>
|
||||
<p class="subtitle">
|
||||
这些分类的统计值将计入“存款”中
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="category-section">
|
||||
<div class="section-title">可多选分类</div>
|
||||
<div class="section-title">
|
||||
可多选分类
|
||||
</div>
|
||||
<ClassifySelector
|
||||
v-model="selectedCategories"
|
||||
:type="2"
|
||||
@@ -18,7 +26,14 @@
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<van-button block round type="primary" @click="onSubmit"> 保存配置 </van-button>
|
||||
<van-button
|
||||
block
|
||||
round
|
||||
type="primary"
|
||||
@click="onSubmit"
|
||||
>
|
||||
保存配置
|
||||
</van-button>
|
||||
</template>
|
||||
</PopupContainer>
|
||||
</template>
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
<template>
|
||||
<div class="global-add-bill">
|
||||
<!-- Floating Add Bill Button -->
|
||||
<div class="floating-add" @click="openAddBill">
|
||||
<div
|
||||
class="floating-add"
|
||||
@click="openAddBill"
|
||||
>
|
||||
<van-icon name="plus" />
|
||||
</div>
|
||||
|
||||
<!-- Add Bill Modal -->
|
||||
<PopupContainer v-model="showAddBill" title="记一笔" height="75%">
|
||||
<van-tabs v-model:active="activeTab" shrink>
|
||||
<van-tab title="一句话录账" name="one">
|
||||
<OneLineBillAdd :key="componentKey" @success="handleSuccess" />
|
||||
<PopupContainer
|
||||
v-model="showAddBill"
|
||||
title="记一笔"
|
||||
height="75%"
|
||||
>
|
||||
<van-tabs
|
||||
v-model:active="activeTab"
|
||||
shrink
|
||||
>
|
||||
<van-tab
|
||||
title="一句话录账"
|
||||
name="one"
|
||||
>
|
||||
<OneLineBillAdd
|
||||
:key="componentKey"
|
||||
@success="handleSuccess"
|
||||
/>
|
||||
</van-tab>
|
||||
<van-tab title="手动录账" name="manual">
|
||||
<ManualBillAdd :key="componentKey" @success="handleSuccess" />
|
||||
<van-tab
|
||||
title="手动录账"
|
||||
name="manual"
|
||||
>
|
||||
<ManualBillAdd
|
||||
:key="componentKey"
|
||||
@success="handleSuccess"
|
||||
/>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</PopupContainer>
|
||||
|
||||
@@ -12,21 +12,36 @@
|
||||
<!-- 头部区域 -->
|
||||
<div class="popup-header-fixed">
|
||||
<!-- 标题行 (无子标题且有操作时使用 Grid 布局) -->
|
||||
<div class="header-title-row" :class="{ 'has-actions': !subtitle && hasActions }">
|
||||
<div
|
||||
class="header-title-row"
|
||||
:class="{ 'has-actions': !subtitle && hasActions }"
|
||||
>
|
||||
<h3 class="popup-title">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<!-- 无子标题时,操作按钮与标题同行 -->
|
||||
<div v-if="!subtitle && hasActions" class="header-actions-inline">
|
||||
<div
|
||||
v-if="!subtitle && hasActions"
|
||||
class="header-actions-inline"
|
||||
>
|
||||
<slot name="header-actions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 子标题/统计信息 -->
|
||||
<div v-if="subtitle" class="header-stats">
|
||||
<span class="stats-text" v-html="subtitle" />
|
||||
<div
|
||||
v-if="subtitle"
|
||||
class="header-stats"
|
||||
>
|
||||
<span
|
||||
class="stats-text"
|
||||
v-html="subtitle"
|
||||
/>
|
||||
<!-- 额外操作插槽 -->
|
||||
<slot v-if="hasActions" name="header-actions" />
|
||||
<slot
|
||||
v-if="hasActions"
|
||||
name="header-actions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +51,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 底部页脚,固定不可滚动 -->
|
||||
<div v-if="slots.footer" class="popup-footer-fixed">
|
||||
<div
|
||||
v-if="slots.footer"
|
||||
class="popup-footer-fixed"
|
||||
>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
<template>
|
||||
<PopupContainer v-model="visible" title="交易详情" height="75%" :closeable="false">
|
||||
<template #header-actions>
|
||||
<van-button size="small" type="primary" plain @click="handleOffsetClick"> 抵账 </van-button>
|
||||
</template>
|
||||
|
||||
<template>
|
||||
<PopupContainer
|
||||
v-model="visible"
|
||||
title="交易详情"
|
||||
height="75%"
|
||||
:closeable="false"
|
||||
>
|
||||
<van-form style="margin-top: 12px">
|
||||
<van-cell-group inset>
|
||||
<van-cell title="记录时间" :value="formatDate(transaction.createTime)" />
|
||||
<van-cell
|
||||
title="记录时间"
|
||||
:value="formatDate(transaction.createTime)"
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group inset title="交易明细">
|
||||
<van-cell-group
|
||||
inset
|
||||
title="交易明细"
|
||||
>
|
||||
<van-field
|
||||
v-model="occurredAtLabel"
|
||||
name="occurredAt"
|
||||
@@ -48,21 +55,33 @@
|
||||
:rules="[{ required: true, message: '请输入交易后余额' }]"
|
||||
/>
|
||||
|
||||
<van-field name="type" label="交易类型">
|
||||
<van-field
|
||||
name="type"
|
||||
label="交易类型"
|
||||
>
|
||||
<template #input>
|
||||
<van-radio-group
|
||||
v-model="editForm.type"
|
||||
direction="horizontal"
|
||||
@change="handleTypeChange"
|
||||
>
|
||||
<van-radio :name="0"> 支出 </van-radio>
|
||||
<van-radio :name="1"> 收入 </van-radio>
|
||||
<van-radio :name="2"> 不计 </van-radio>
|
||||
<van-radio :name="0">
|
||||
支出
|
||||
</van-radio>
|
||||
<van-radio :name="1">
|
||||
收入
|
||||
</van-radio>
|
||||
<van-radio :name="2">
|
||||
不计
|
||||
</van-radio>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-field name="classify" label="交易分类">
|
||||
<van-field
|
||||
name="classify"
|
||||
label="交易分类"
|
||||
>
|
||||
<template #input>
|
||||
<div style="flex: 1">
|
||||
<div
|
||||
@@ -74,7 +93,10 @@
|
||||
class="suggestion-tip"
|
||||
@click="applySuggestion"
|
||||
>
|
||||
<van-icon name="bulb-o" class="suggestion-icon" />
|
||||
<van-icon
|
||||
name="bulb-o"
|
||||
class="suggestion-icon"
|
||||
/>
|
||||
<span class="suggestion-text">
|
||||
建议: {{ transaction.unconfirmedClassify }}
|
||||
<span
|
||||
@@ -87,11 +109,14 @@
|
||||
({{ getTypeName(transaction.unconfirmedType) }})
|
||||
</span>
|
||||
</span>
|
||||
<div class="suggestion-apply">应用</div>
|
||||
<div class="suggestion-apply">
|
||||
应用
|
||||
</div>
|
||||
<span v-else-if="!editForm.classify" style="color: var(--van-gray-5)"
|
||||
>请选择交易分类</span
|
||||
>
|
||||
</div>
|
||||
<span
|
||||
v-else-if="!editForm.classify"
|
||||
style="color: var(--van-gray-5)"
|
||||
>请选择交易分类</span>
|
||||
<span v-else>{{ editForm.classify }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -106,30 +131,25 @@
|
||||
</van-form>
|
||||
|
||||
<template #footer>
|
||||
<van-button round block type="primary" :loading="submitting" @click="onSubmit">
|
||||
<van-button
|
||||
round
|
||||
block
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="onSubmit"
|
||||
>
|
||||
保存修改
|
||||
</van-button>
|
||||
</template>
|
||||
</PopupContainer>
|
||||
|
||||
<!-- 抵账候选列表弹窗 -->
|
||||
<PopupContainer v-model="showOffsetPopup" title="选择抵账交易" height="75%">
|
||||
<van-list>
|
||||
<van-cell
|
||||
v-for="item in offsetCandidates"
|
||||
:key="item.id"
|
||||
:title="item.reason"
|
||||
:label="formatDate(item.occurredAt)"
|
||||
:value="item.amount"
|
||||
is-link
|
||||
@click="handleCandidateSelect(item)"
|
||||
/>
|
||||
<van-empty v-if="offsetCandidates.length === 0" description="暂无匹配的抵账交易" />
|
||||
</van-list>
|
||||
</PopupContainer>
|
||||
|
||||
<!-- 日期选择弹窗 -->
|
||||
<van-popup v-model:show="showDatePicker" position="bottom" round teleport="body">
|
||||
<van-popup
|
||||
v-model:show="showDatePicker"
|
||||
position="bottom"
|
||||
round
|
||||
teleport="body"
|
||||
>
|
||||
<van-date-picker
|
||||
v-model="currentDate"
|
||||
title="选择日期"
|
||||
@@ -139,7 +159,12 @@
|
||||
</van-popup>
|
||||
|
||||
<!-- 时间选择弹窗 -->
|
||||
<van-popup v-model:show="showTimePicker" position="bottom" round teleport="body">
|
||||
<van-popup
|
||||
v-model:show="showTimePicker"
|
||||
position="bottom"
|
||||
round
|
||||
teleport="body"
|
||||
>
|
||||
<van-time-picker
|
||||
v-model="currentTime"
|
||||
title="选择时间"
|
||||
@@ -151,14 +176,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch, defineProps, defineEmits, computed, nextTick } from 'vue'
|
||||
import { showToast, showConfirmDialog } from 'vant'
|
||||
import { showToast } from 'vant'
|
||||
import dayjs from 'dayjs'
|
||||
import PopupContainer from '@/components/PopupContainer.vue'
|
||||
import ClassifySelector from '@/components/ClassifySelector.vue'
|
||||
import {
|
||||
updateTransaction,
|
||||
getCandidatesForOffset,
|
||||
offsetTransactions
|
||||
updateTransaction
|
||||
} from '@/api/transactionRecord'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -341,50 +364,6 @@ const formatDate = (dateString) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 抵账相关
|
||||
const showOffsetPopup = ref(false)
|
||||
const offsetCandidates = ref([])
|
||||
|
||||
const handleOffsetClick = async () => {
|
||||
try {
|
||||
const res = await getCandidatesForOffset(editForm.id)
|
||||
if (res.success) {
|
||||
offsetCandidates.value = res.data || []
|
||||
showOffsetPopup.value = true
|
||||
} else {
|
||||
showToast(res.message || '获取抵账列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取抵账列表出错:', error)
|
||||
showToast('获取抵账列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCandidateSelect = (candidate) => {
|
||||
showConfirmDialog({
|
||||
title: '确认抵账',
|
||||
message: `确认将当前交易与 "${candidate.reason}" (${candidate.amount}) 互相抵消吗?\n抵消后两笔交易将被删除。`
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
const res = await offsetTransactions(editForm.id, candidate.id)
|
||||
if (res.success) {
|
||||
showToast('抵账成功')
|
||||
showOffsetPopup.value = false
|
||||
visible.value = false
|
||||
emit('save') // 触发列表刷新
|
||||
} else {
|
||||
showToast(res.message || '抵账失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('抵账出错:', error)
|
||||
showToast('抵账失败')
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// on cancel
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="transaction-list-container transaction-list">
|
||||
<van-list :loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
|
||||
<van-cell-group v-if="transactions && transactions.length" inset style="margin-top: 10px">
|
||||
<van-list
|
||||
:loading="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-cell-group
|
||||
v-if="transactions && transactions.length"
|
||||
inset
|
||||
style="margin-top: 10px"
|
||||
>
|
||||
<van-swipe-cell
|
||||
v-for="transaction in transactions"
|
||||
:key="transaction.id"
|
||||
@@ -14,7 +23,10 @@
|
||||
class="checkbox-col"
|
||||
@update:model-value="toggleSelection(transaction)"
|
||||
/>
|
||||
<div class="transaction-card" @click="handleClick(transaction)">
|
||||
<div
|
||||
class="transaction-card"
|
||||
@click="handleClick(transaction)"
|
||||
>
|
||||
<div class="card-left">
|
||||
<div class="transaction-title">
|
||||
<span class="reason">{{ transaction.reason || '(无摘要)' }}</span>
|
||||
@@ -33,11 +45,16 @@
|
||||
→ {{ transaction.upsetedClassify }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="transaction.importFrom">来源: {{ transaction.importFrom }}</div>
|
||||
<div v-if="transaction.importFrom">
|
||||
来源: {{ transaction.importFrom }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-middle">
|
||||
<van-tag :type="getTypeTagType(transaction.type)" size="medium">
|
||||
<van-tag
|
||||
:type="getTypeTagType(transaction.type)"
|
||||
size="medium"
|
||||
>
|
||||
{{ getTypeName(transaction.type) }}
|
||||
</van-tag>
|
||||
<template
|
||||
@@ -47,7 +64,10 @@
|
||||
"
|
||||
>
|
||||
→
|
||||
<van-tag :type="getTypeTagType(transaction.upsetedType)" size="medium">
|
||||
<van-tag
|
||||
:type="getTypeTagType(transaction.upsetedType)"
|
||||
size="medium"
|
||||
>
|
||||
{{ getTypeName(transaction.upsetedType) }}
|
||||
</van-tag>
|
||||
</template>
|
||||
@@ -57,21 +77,25 @@
|
||||
<div :class="['amount', getAmountClass(transaction.type)]">
|
||||
{{ formatAmount(transaction.amount, transaction.type) }}
|
||||
</div>
|
||||
<div v-if="transaction.balance && transaction.balance > 0" class="balance">
|
||||
余额: {{ formatMoney(transaction.balance) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="transaction.refundAmount && transaction.refundAmount > 0"
|
||||
v-if="transaction.balance && transaction.balance > 0"
|
||||
class="balance"
|
||||
>
|
||||
退款: {{ formatMoney(transaction.refundAmount) }}
|
||||
余额: {{ formatMoney(transaction.balance) }}
|
||||
</div>
|
||||
</div>
|
||||
<van-icon name="arrow" size="16" color="var(--van-gray-5)" />
|
||||
<van-icon
|
||||
name="arrow"
|
||||
size="16"
|
||||
color="var(--van-gray-5)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="showDelete" #right>
|
||||
<template
|
||||
v-if="showDelete"
|
||||
#right
|
||||
>
|
||||
<van-button
|
||||
square
|
||||
type="danger"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div class="page-container-flex">
|
||||
<!-- 顶部导航栏 -->
|
||||
<van-nav-bar title="账单" placeholder>
|
||||
<van-nav-bar
|
||||
title="账单"
|
||||
placeholder
|
||||
>
|
||||
<template #right>
|
||||
<van-button
|
||||
v-if="tabActive === 'email'"
|
||||
@@ -20,15 +23,38 @@
|
||||
/>
|
||||
</template>
|
||||
</van-nav-bar>
|
||||
<van-tabs v-model:active="tabActive" type="card" style="margin: 12px 0 2px 0">
|
||||
<van-tab title="账单" name="balance" />
|
||||
<van-tab title="邮件" name="email" />
|
||||
<van-tab title="消息" name="message" />
|
||||
<van-tabs
|
||||
v-model:active="tabActive"
|
||||
type="card"
|
||||
style="margin: 12px 0 2px 0"
|
||||
>
|
||||
<van-tab
|
||||
title="账单"
|
||||
name="balance"
|
||||
/>
|
||||
<van-tab
|
||||
title="邮件"
|
||||
name="email"
|
||||
/>
|
||||
<van-tab
|
||||
title="消息"
|
||||
name="message"
|
||||
/>
|
||||
</van-tabs>
|
||||
|
||||
<TransactionsRecord v-if="tabActive === 'balance'" ref="transactionsRecordRef" />
|
||||
<EmailRecord v-else-if="tabActive === 'email'" ref="emailRecordRef" />
|
||||
<MessageView v-else-if="tabActive === 'message'" ref="messageViewRef" :is-component="true" />
|
||||
<TransactionsRecord
|
||||
v-if="tabActive === 'balance'"
|
||||
ref="transactionsRecordRef"
|
||||
/>
|
||||
<EmailRecord
|
||||
v-else-if="tabActive === 'email'"
|
||||
ref="emailRecordRef"
|
||||
/>
|
||||
<MessageView
|
||||
v-else-if="tabActive === 'message'"
|
||||
ref="messageViewRef"
|
||||
:is-component="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 分组列表 -->
|
||||
<van-empty v-if="!hasData && finished" description="暂无数据" />
|
||||
<van-empty
|
||||
v-if="!hasData && finished"
|
||||
description="暂无数据"
|
||||
/>
|
||||
|
||||
<van-list
|
||||
v-model:loading="listLoading"
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
|
||||
<div class="scroll-content">
|
||||
<!-- 第一层:选择交易类型 -->
|
||||
<div v-if="currentLevel === 0" class="level-container">
|
||||
<div
|
||||
v-if="currentLevel === 0"
|
||||
class="level-container"
|
||||
>
|
||||
<van-cell-group inset>
|
||||
<van-cell
|
||||
v-for="type in typeOptions"
|
||||
@@ -23,22 +26,48 @@
|
||||
</div>
|
||||
|
||||
<!-- 第二层:分类列表 -->
|
||||
<div v-else class="level-container">
|
||||
<div
|
||||
v-else
|
||||
class="level-container"
|
||||
>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="breadcrumb">
|
||||
<van-tag type="primary" closeable style="margin-left: 16px" @close="handleBackToRoot">
|
||||
<van-tag
|
||||
type="primary"
|
||||
closeable
|
||||
style="margin-left: 16px"
|
||||
@close="handleBackToRoot"
|
||||
>
|
||||
{{ currentTypeName }}
|
||||
</van-tag>
|
||||
</div>
|
||||
|
||||
<!-- 分类列表 -->
|
||||
<van-empty v-if="categories.length === 0" description="暂无分类" />
|
||||
<van-empty
|
||||
v-if="categories.length === 0"
|
||||
description="暂无分类"
|
||||
/>
|
||||
|
||||
<van-cell-group v-else inset>
|
||||
<van-swipe-cell v-for="category in categories" :key="category.id">
|
||||
<van-cell :title="category.name" is-link @click="handleEdit(category)" />
|
||||
<van-cell-group
|
||||
v-else
|
||||
inset
|
||||
>
|
||||
<van-swipe-cell
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
>
|
||||
<van-cell
|
||||
:title="category.name"
|
||||
is-link
|
||||
@click="handleEdit(category)"
|
||||
/>
|
||||
<template #right>
|
||||
<van-button square type="danger" text="删除" @click="handleDelete(category)" />
|
||||
<van-button
|
||||
square
|
||||
type="danger"
|
||||
text="删除"
|
||||
@click="handleDelete(category)"
|
||||
/>
|
||||
</template>
|
||||
</van-swipe-cell>
|
||||
</van-cell-group>
|
||||
@@ -49,7 +78,12 @@
|
||||
|
||||
<div class="bottom-button">
|
||||
<!-- 新增分类按钮 -->
|
||||
<van-button type="primary" size="large" icon="plus" @click="handleAddCategory">
|
||||
<van-button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon="plus"
|
||||
@click="handleAddCategory"
|
||||
>
|
||||
新增分类
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<div class="page-container-flex classification-nlp">
|
||||
<van-nav-bar title="自然语言分类" left-text="返回" left-arrow @click-left="onClickLeft" />
|
||||
<van-nav-bar
|
||||
title="自然语言分类"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
@click-left="onClickLeft"
|
||||
/>
|
||||
|
||||
<div class="scroll-content">
|
||||
<!-- 输入区域 -->
|
||||
@@ -18,18 +23,36 @@
|
||||
</van-cell-group>
|
||||
|
||||
<div class="action-buttons">
|
||||
<van-button type="primary" block round :loading="analyzing" @click="handleAnalyze">
|
||||
<van-button
|
||||
type="primary"
|
||||
block
|
||||
round
|
||||
:loading="analyzing"
|
||||
@click="handleAnalyze"
|
||||
>
|
||||
分析查询
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分析结果展示 -->
|
||||
<div v-if="analysisResult" class="result-section">
|
||||
<div
|
||||
v-if="analysisResult"
|
||||
class="result-section"
|
||||
>
|
||||
<van-cell-group inset>
|
||||
<van-cell title="查询关键词" :value="analysisResult.searchKeyword" />
|
||||
<van-cell title="AI建议类型" :value="getTypeName(analysisResult.targetType)" />
|
||||
<van-cell title="AI建议分类" :value="analysisResult.targetClassify" />
|
||||
<van-cell
|
||||
title="查询关键词"
|
||||
:value="analysisResult.searchKeyword"
|
||||
/>
|
||||
<van-cell
|
||||
title="AI建议类型"
|
||||
:value="getTypeName(analysisResult.targetType)"
|
||||
/>
|
||||
<van-cell
|
||||
title="AI建议分类"
|
||||
:value="analysisResult.targetClassify"
|
||||
/>
|
||||
<van-cell
|
||||
title="找到记录"
|
||||
:value="`${analysisResult.records.length} 条`"
|
||||
@@ -48,12 +71,30 @@
|
||||
/>
|
||||
|
||||
<!-- 记录列表弹窗 -->
|
||||
<PopupContainer v-model="showRecordsList" title="交易记录列表" height="75%">
|
||||
<PopupContainer
|
||||
v-model="showRecordsList"
|
||||
title="交易记录列表"
|
||||
height="75%"
|
||||
>
|
||||
<div style="background: var(--van-background)">
|
||||
<!-- 批量操作按钮 -->
|
||||
<div class="batch-actions">
|
||||
<van-button plain type="primary" size="small" @click="selectAll"> 全选 </van-button>
|
||||
<van-button plain type="default" size="small" @click="selectNone"> 全不选 </van-button>
|
||||
<van-button
|
||||
plain
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="selectAll"
|
||||
>
|
||||
全选
|
||||
</van-button>
|
||||
<van-button
|
||||
plain
|
||||
type="default"
|
||||
size="small"
|
||||
@click="selectNone"
|
||||
>
|
||||
全不选
|
||||
</van-button>
|
||||
<van-button
|
||||
type="success"
|
||||
size="small"
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<template>
|
||||
<div class="page-container-flex smart-classification">
|
||||
<van-nav-bar title="智能分类" left-text="返回" left-arrow @click-left="onClickLeft" />
|
||||
<van-nav-bar
|
||||
title="智能分类"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
@click-left="onClickLeft"
|
||||
/>
|
||||
|
||||
<div class="scroll-content" style="padding-top: 5px">
|
||||
<div
|
||||
class="scroll-content"
|
||||
style="padding-top: 5px"
|
||||
>
|
||||
<!-- 统计信息 -->
|
||||
<div class="stats-info">
|
||||
<span class="stats-label">未分类账单 </span>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<div class="page-container login-container">
|
||||
<div class="login-box">
|
||||
<h1 class="login-title">账单</h1>
|
||||
<h1 class="login-title">
|
||||
账单
|
||||
</h1>
|
||||
<div class="login-form">
|
||||
<van-field
|
||||
v-model="password"
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 下拉刷新区域 -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-pull-refresh
|
||||
v-model="refreshing"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<!-- 加载提示 -->
|
||||
<van-loading
|
||||
v-if="loading && !(transactionList && transactionList.length)"
|
||||
|
||||
58
WebApi.Test/Basic/DatabaseTest.cs
Normal file
58
WebApi.Test/Basic/DatabaseTest.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using FreeSql;
|
||||
using Repository;
|
||||
|
||||
namespace WebApi.Test.Basic;
|
||||
|
||||
public class DatabaseTest : BaseTest, IDisposable
|
||||
{
|
||||
protected IFreeSql FreeSql { get; }
|
||||
protected ITransactionRecordRepository Repository { get; }
|
||||
|
||||
public DatabaseTest()
|
||||
{
|
||||
FreeSql = new FreeSqlBuilder()
|
||||
.UseConnectionString(DataType.Sqlite, "Data Source=:memory:")
|
||||
.UseAutoSyncStructure(true)
|
||||
.UseNoneCommandParameter(true)
|
||||
.Build();
|
||||
|
||||
Repository = new TransactionRecordRepository(FreeSql);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
FreeSql.Dispose();
|
||||
}
|
||||
|
||||
protected TransactionRecord CreateTestRecord(
|
||||
decimal amount,
|
||||
TransactionType type = TransactionType.Expense,
|
||||
DateTime? occurredAt = null,
|
||||
string reason = "测试摘要",
|
||||
string classify = "测试分类")
|
||||
{
|
||||
return new TransactionRecord
|
||||
{
|
||||
Amount = amount,
|
||||
Type = type,
|
||||
OccurredAt = occurredAt ?? DateTime.Now,
|
||||
Reason = reason,
|
||||
Classify = classify,
|
||||
Card = "1234",
|
||||
Balance = 1000,
|
||||
EmailMessageId = 1,
|
||||
ImportNo = Guid.NewGuid().ToString(),
|
||||
ImportFrom = "测试"
|
||||
};
|
||||
}
|
||||
|
||||
protected TransactionRecord CreateExpense(decimal amount, DateTime? occurredAt = null, string reason = "支出", string classify = "餐饮")
|
||||
{
|
||||
return CreateTestRecord(-amount, TransactionType.Expense, occurredAt, reason, classify);
|
||||
}
|
||||
|
||||
protected TransactionRecord CreateIncome(decimal amount, DateTime? occurredAt = null, string reason = "收入", string classify = "工资")
|
||||
{
|
||||
return CreateTestRecord(amount, TransactionType.Income, occurredAt, reason, classify);
|
||||
}
|
||||
}
|
||||
@@ -689,51 +689,6 @@ public class TransactionRecordController(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取抵账候选列表
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<BaseResponse<TransactionRecord[]>> GetCandidatesForOffset(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var current = await transactionRepository.GetByIdAsync(id);
|
||||
if (current == null)
|
||||
{
|
||||
return ((TransactionRecord[])[]).Ok();
|
||||
}
|
||||
|
||||
var list = await transactionRepository.GetCandidatesForOffsetAsync(id, current.Amount, current.Type);
|
||||
|
||||
return list.ToArray().Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "获取抵账候选列表失败,交易ID: {TransactionId}", id);
|
||||
return $"获取抵账候选列表失败: {ex.Message}".Fail<TransactionRecord[]>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 抵账(删除两笔交易)
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> OffsetTransactions([FromBody] OffsetTransactionDto dto)
|
||||
{
|
||||
var t1 = await transactionRepository.GetByIdAsync(dto.Id1);
|
||||
var t2 = await transactionRepository.GetByIdAsync(dto.Id2);
|
||||
|
||||
if (t1 == null || t2 == null)
|
||||
{
|
||||
return NotFound("交易记录不存在");
|
||||
}
|
||||
|
||||
await transactionRepository.DeleteAsync(dto.Id1);
|
||||
await transactionRepository.DeleteAsync(dto.Id2);
|
||||
|
||||
return Ok(new { success = true, message = "抵账成功" });
|
||||
}
|
||||
|
||||
private async Task WriteEventAsync(string eventType, string data)
|
||||
{
|
||||
var message = $"event: {eventType}\ndata: {data}\n\n";
|
||||
@@ -823,14 +778,6 @@ public record BillAnalysisRequest(
|
||||
string UserInput
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 抵账请求DTO
|
||||
/// </summary>
|
||||
public record OffsetTransactionDto(
|
||||
long Id1,
|
||||
long Id2
|
||||
);
|
||||
|
||||
public record ParseOneLineRequestDto(
|
||||
string Text
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user