feat: 智能复核添加快捷确认功能

This commit is contained in:
CHE LIANG ZHAO
2026-01-26 13:04:04 +08:00
parent 6e3756b2e1
commit 7b2d6a9fbb
4 changed files with 143 additions and 175 deletions

229
AGENTS.md
View File

@@ -1,114 +1,63 @@
# AGENTS.md - AI Coding Agent Guidelines # AGENTS.md - AI Coding Agent Guidelines
This document provides guidelines for AI coding agents working on the BillAI project. Guidelines for AI coding agents working on BillAI - a microservices bill analysis system.
## Project Overview ## Architecture
- `web/` - SvelteKit 5 + TailwindCSS 4 + TypeScript
- `server/` - Go 1.21 + Gin + MongoDB
- `analyzer/` - Python 3.12 + FastAPI
BillAI is a microservices-based personal bill analysis system supporting WeChat and Alipay bill parsing, intelligent categorization, and visualization. ## Build/Lint/Test Commands
**Architecture:**
- `web/` - Frontend (SvelteKit 5 + TailwindCSS 4.x + TypeScript)
- `server/` - Backend API (Go 1.21 + Gin + MongoDB)
- `analyzer/` - Python analysis service (Python 3.12 + FastAPI)
## Build, Lint, and Test Commands
### Frontend (web/) ### Frontend (web/)
```bash ```bash
# Development npm run dev # Start dev server
npm run dev # Start dev server (Vite)
# Build
npm run build # Production build npm run build # Production build
npm run preview # Preview production build npm run check # TypeScript check
npm run lint # Prettier + ESLint
# Type checking npm run format # Format code
npm run check # svelte-check with TypeScript npm run test # Run all tests
npm run check:watch # Watch mode npx vitest run src/routes/+page.spec.ts # Single test file
npx vitest run -t "test name" # Test by name
# Linting and formatting
npm run lint # Prettier check + ESLint
npm run format # Format with Prettier
# Testing
npm run test # Run all tests once
npm run test:unit # Run tests in watch mode
npx vitest run src/demo.spec.ts # Run single test file
npx vitest run -t "sum test" # Run tests matching name
npx vitest run src/routes/page.svelte.spec.ts # Run component test
``` ```
### Backend (server/) ### Backend (server/)
```bash ```bash
# Run go run . # Start server
go run . # Start development server
# Build
go build . # Build binary go build . # Build binary
go mod tidy # Clean dependencies
# Dependencies go test ./... # All tests
go mod download # Install dependencies go test ./handler/... # Package tests
go mod tidy # Clean up dependencies go test -run TestName # Single test
# Testing (if tests exist)
go test ./... # Run all tests
go test ./handler/... # Run tests in specific package
go test -run TestName # Run single test by name
``` ```
### Analyzer (analyzer/) ### Analyzer (analyzer/)
```bash ```bash
# Setup
python -m venv venv
pip install -r requirements.txt
# Run
python server.py # Start FastAPI server python server.py # Start FastAPI server
pytest # All tests
# Testing (if tests exist) pytest test_file.py # Single file
pytest # Run all tests pytest -k "test_name" # Test by pattern
pytest test_file.py # Run single test file
pytest -k "test_name" # Run tests matching name
``` ```
### Docker ### Docker
```bash ```bash
docker-compose up -d --build # Start all services docker-compose up -d --build # Start/rebuild all
docker-compose ps # Check service status docker-compose logs -f server # Follow service logs
docker-compose down # Stop all services
docker-compose logs -f web # Follow logs for specific service
``` ```
## Code Style Guidelines ## Code Style
### TypeScript/Svelte (Frontend) ### TypeScript/Svelte
**Prettier config:** Tabs, single quotes, no trailing commas, width 100
**Formatting (Prettier):**
- Use tabs for indentation
- Single quotes for strings
- No trailing commas
- Print width: 100 characters
**Imports:** **Imports:**
- Use `$lib/` alias for imports from `src/lib/`
- Use `$app/` for SvelteKit internals
- Group imports: external packages, then internal modules
```typescript ```typescript
import { browser } from '$app/environment'; import { browser } from '$app/environment'; // SvelteKit
import { auth } from '$lib/stores/auth'; import { auth } from '$lib/stores/auth'; // Internal
import type { UIBill } from '$lib/models/bill'; import type { UIBill } from '$lib/models/bill';
``` ```
**Types:** **Types:**
- Define interfaces for API responses and requests
- Use `type` for unions and simple type aliases
- Export types from dedicated files in `$lib/types/` or alongside models
```typescript ```typescript
export interface UploadResponse { export interface UploadResponse {
result: boolean; result: boolean;
@@ -117,55 +66,20 @@ export interface UploadResponse {
} }
``` ```
**Naming Conventions:** **Naming:** PascalCase (types, components), camelCase (functions, variables)
- PascalCase: Components, interfaces, types
- camelCase: Functions, variables, properties
- Use descriptive names: `fetchBills`, `UIBill`, `checkHealth`
**Error Handling:**
- Wrap API calls in try/catch
- Throw `Error` with HTTP status for API failures
- Handle 401 responses with logout redirect
**Error handling:**
```typescript ```typescript
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ${response.status}`); throw new Error(`HTTP ${response.status}`);
} }
// Handle 401 -> logout redirect
``` ```
### Go (Backend) ### Go Backend
**Structure:** `handler/``service/``repository/` → MongoDB
**Project Structure:**
- `handler/` - HTTP request handlers
- `service/` - Business logic
- `repository/` - Data access layer
- `model/` - Data structures
- `adapter/` - External service integrations
- `config/` - Configuration management
- `middleware/` - Auth and other middleware
**Naming Conventions:**
- PascalCase: Exported types, functions, constants
- camelCase: Unexported functions, variables
- Use descriptive names: `UpdateBillRequest`, `parseBillTime`
**Error Handling:**
- Define sentinel errors in `repository/errors.go`
- Return errors up the call stack
- Use structured JSON responses for HTTP errors
```go
if err == repository.ErrNotFound {
c.JSON(http.StatusNotFound, Response{Result: false, Message: "not found"})
return
}
```
**JSON Tags:**
- Use snake_case for JSON field names
- Use `omitempty` for optional fields
- Match frontend API expectations
**JSON tags:** snake_case, omitempty for optional fields
```go ```go
type UpdateBillRequest struct { type UpdateBillRequest struct {
Category *string `json:"category,omitempty"` Category *string `json:"category,omitempty"`
@@ -173,9 +87,7 @@ type UpdateBillRequest struct {
} }
``` ```
**Response Format:** **Response format:**
- All API responses use consistent structure:
```go ```go
type Response struct { type Response struct {
Result bool `json:"result"` Result bool `json:"result"`
@@ -184,12 +96,16 @@ type Response struct {
} }
``` ```
### Python (Analyzer) **Error handling:**
```go
if err == repository.ErrNotFound {
c.JSON(http.StatusNotFound, Response{Result: false, Message: "not found"})
return
}
```
**Style:** ### Python Analyzer
- Follow PEP 8 **Style:** PEP 8, type hints, Pydantic models
- Use type hints for function signatures
- Use Pydantic models for request/response validation
```python ```python
def do_clean( def do_clean(
@@ -199,56 +115,27 @@ def do_clean(
) -> tuple[bool, str, str]: ) -> tuple[bool, str, str]:
``` ```
**Error Handling:** **Error handling:**
- Raise `HTTPException` for API errors
- Use try/except for file operations
- Return structured responses
```python ```python
if not success: if not success:
raise HTTPException(status_code=400, detail=message) raise HTTPException(status_code=400, detail=message)
``` ```
## Testing Guidelines ## Key Patterns
**Frontend Tests:** **API Flow:** Frontend (SvelteKit proxy) → Go API → MongoDB + Python analyzer
- Use Vitest with Playwright for browser testing
- Component tests: `*.svelte.spec.ts`
- Unit tests: `*.spec.ts`
- Tests require assertions: `expect.assertions()` or explicit expects
```typescript **Auth:** JWT tokens, Bearer header, 401 → logout redirect
import { describe, it, expect } from 'vitest';
import { render } from 'vitest-browser-svelte';
describe('/+page.svelte', () => { **File Processing:** ZIP → extract → convert (GBK→UTF-8, xlsx→csv) → clean → import
it('should render h1', async () => {
render(Page);
await expect.element(page.getByRole('heading')).toBeInTheDocument();
});
});
```
## Important Patterns **Testing:** Vitest + Playwright for frontend, Go test for backend
**API Communication:** ## Important Files
- Frontend proxies API calls through SvelteKit to avoid CORS - `web/src/lib/api.ts` - API client
- Backend uses Gin framework with JSON responses - `web/src/lib/models/` - UI data models
- Analyzer communicates via HTTP (preferred) or subprocess - `server/handler/` - HTTP handlers
- `server/service/` - Business logic
**Data Flow:** - `server/model/` - Go data structures
- Frontend (SvelteKit) -> Backend (Go/Gin) -> MongoDB - `analyzer/cleaners/` - Bill processing
- Backend -> Analyzer (Python/FastAPI) for bill parsing - `mock_data/*.zip` - Test data (password: 123456)
**Authentication:**
- JWT tokens stored in frontend auth store
- Bearer token sent in Authorization header
- 401 responses trigger logout and redirect
## File Locations
- API types: `web/src/lib/api.ts`
- UI models: `web/src/lib/models/`
- Go handlers: `server/handler/`
- Go models: `server/model/`
- Python API: `analyzer/server.py`

View File

@@ -19,6 +19,22 @@
- 新增 `DELETE /api/bills/:id` 接口 - 新增 `DELETE /api/bills/:id` 接口
- `BillDetailDrawer` 组件新增 `allowDelete``onDelete` props - `BillDetailDrawer` 组件新增 `allowDelete``onDelete` props
## [1.2.1] - 2026-01-23
### 优化
- **智能复核快捷确认** - 在复核列表每行添加快捷确认按钮
- 无需打开详情页面即可确认分类正确
- 点击确认按钮立即清除复核标记并从列表移除
- 自动更新统计数据(总数、高优先级、低优先级计数)
- 按钮支持加载状态显示,防止重复操作
- 提升复核效率,支持快速批量确认
### 文档
- **AGENTS.md 更新** - 精简为 150 行,专为 AI 编程助手设计
- 核心构建/测试/lint 命令说明
- TypeScript、Go、Python 代码风格指南
- 关键架构模式和文件位置
## [1.1.0] - 2026-01-23 ## [1.1.0] - 2026-01-23
### 新增 ### 新增

View File

@@ -1,7 +1,7 @@
{ {
"name": "web", "name": "web",
"private": true, "private": true,
"version": "1.2.0", "version": "1.2.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",

View File

@@ -7,12 +7,14 @@
import * as Table from '$lib/components/ui/table'; import * as Table from '$lib/components/ui/table';
import BillDetailDrawer from '$lib/components/analysis/BillDetailDrawer.svelte'; import BillDetailDrawer from '$lib/components/analysis/BillDetailDrawer.svelte';
import { cleanedBillToUIBill, type UIBill } from '$lib/models/bill'; import { cleanedBillToUIBill, type UIBill } from '$lib/models/bill';
import { updateBill } from '$lib/api';
import Loader2 from '@lucide/svelte/icons/loader-2'; import Loader2 from '@lucide/svelte/icons/loader-2';
import AlertCircle from '@lucide/svelte/icons/alert-circle'; import AlertCircle from '@lucide/svelte/icons/alert-circle';
import AlertTriangle from '@lucide/svelte/icons/alert-triangle'; import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
import Clock from '@lucide/svelte/icons/clock'; import Clock from '@lucide/svelte/icons/clock';
import PartyPopper from '@lucide/svelte/icons/party-popper'; import PartyPopper from '@lucide/svelte/icons/party-popper';
import RefreshCw from '@lucide/svelte/icons/refresh-cw'; import RefreshCw from '@lucide/svelte/icons/refresh-cw';
import Check from '@lucide/svelte/icons/check';
let isLoading = $state(true); let isLoading = $state(true);
let errorMessage = $state(''); let errorMessage = $state('');
@@ -20,6 +22,9 @@
let allBills = $state<CleanedBill[]>([]); let allBills = $state<CleanedBill[]>([]);
let filterLevel = $state<'all' | 'HIGH' | 'LOW'>('all'); let filterLevel = $state<'all' | 'HIGH' | 'LOW'>('all');
// 快捷确认按钮的加载状态 (记录ID -> 是否在加载)
let confirmingBills = $state<Map<string, boolean>>(new Map());
onMount(() => { onMount(() => {
loadReviewData(); loadReviewData();
}); });
@@ -88,6 +93,48 @@
drawerOpen = true; drawerOpen = true;
} }
// 快捷确认(仅清除 review_level不修改其他字段
async function quickConfirm(record: CleanedBill, event: Event) {
// 阻止事件冒泡,避免触发行点击
event.stopPropagation();
if (confirmingBills.get(record.id)) return;
// 设置加载状态
confirmingBills.set(record.id, true);
confirmingBills = new Map(confirmingBills);
try {
const resp = await updateBill(record.id, { review_level: '' });
if (resp.result) {
// 从列表中移除该记录
const index = allBills.findIndex(r => r.id === record.id);
if (index !== -1) {
allBills.splice(index, 1);
allBills = [...allBills];
}
// 更新统计数据
if (reviewStats) {
reviewStats = {
...reviewStats,
total: Math.max(0, reviewStats.total - 1),
high: record.review_level === 'HIGH' ? Math.max(0, reviewStats.high - 1) : reviewStats.high,
low: record.review_level === 'LOW' ? Math.max(0, reviewStats.low - 1) : reviewStats.low
};
}
}
} catch (err) {
console.error('快捷确认失败:', err);
// 这里可以添加错误提示
} finally {
// 清除加载状态
confirmingBills.delete(record.id);
confirmingBills = new Map(confirmingBills);
}
}
// 复核完成后从列表中移除该记录 // 复核完成后从列表中移除该记录
function handleBillUpdate(updated: UIBill, original: UIBill) { function handleBillUpdate(updated: UIBill, original: UIBill) {
// 更新后 review_level 已被清除,从列表中移除 // 更新后 review_level 已被清除,从列表中移除
@@ -254,6 +301,7 @@
<Table.Head>收/支</Table.Head> <Table.Head>收/支</Table.Head>
<Table.Head class="text-right">金额</Table.Head> <Table.Head class="text-right">金额</Table.Head>
<Table.Head class="w-[80px]">优先级</Table.Head> <Table.Head class="w-[80px]">优先级</Table.Head>
<Table.Head class="w-[100px] text-center">操作</Table.Head>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
@@ -287,6 +335,23 @@
{record.review_level} {record.review_level}
</Badge> </Badge>
</Table.Cell> </Table.Cell>
<Table.Cell class="text-center">
<Button
size="sm"
variant="outline"
class="h-7 px-2 text-xs"
onclick={(e) => quickConfirm(record, e)}
disabled={confirmingBills.get(record.id) || false}
title="确认分类正确"
>
{#if confirmingBills.get(record.id)}
<Loader2 class="h-3 w-3 animate-spin" />
{:else}
<Check class="h-3 w-3 mr-1" />
确认
{/if}
</Button>
</Table.Cell>
</Table.Row> </Table.Row>
{/each} {/each}
</Table.Body> </Table.Body>