Penny Lens 记账模块详细文档
🎯 模块概述
记账模块是 Penny Lens 系统的核心功能模块,基于复式记账法实现科学的财务管理。该模块支持收入、支出、转账等多种记账类型,提供智能分类、批量操作、数据验证等高级功能,确保财务数据的准确性和完整性。
🏗️ 技术架构
核心组件
src/├── pages/accounting/ # 记账页面│ ├── index.vue # 记账主页│ ├── add-record.vue # 添加记录│ ├── edit-record.vue # 编辑记录│ ├── batch-import.vue # 批量导入│ └── record-detail.vue # 记录详情├── components/Accounting/ # 记账组件│ ├── RecordForm.vue # 记录表单│ ├── CategorySelector.vue # 分类选择器│ ├── AssetSelector.vue # 资产选择器│ ├── BatchEditor.vue # 批量编辑器│ └── RecordList.vue # 记录列表├── store/accounting.ts # 记账状态管理├── services/accounting.ts # 记账服务├── utils/accounting.ts # 记账工具├── types/accounting.ts # 记账类型定义└── validators/accounting.ts # 记账验证器💰 记账类型
1. 收入记账
功能描述: 记录各种收入来源
数据结构:
interface IncomeRecord {
id: string;
type: 'income';
amount: number; // 金额(分)
category: string; // 收入分类
asset: string; // 收入资产
description?: string; // 描述
date: number; // 日期
tags?: string[]; // 标签
attachments?: string[]; // 附件
createdAt: number;
updatedAt: number;
}实现示例:
// 创建收入记录
async function createIncomeRecord(data: CreateIncomeRequest): Promise<IncomeRecord> {
// 验证数据
const validation = validateIncomeRecord(data);
if (!validation.isValid) {
throw new Error(validation.errors.join(', '));
}
// 创建记录
const record = await accountingService.createRecord({
...data,
type: 'income',
amount: data.amount * 100 // 转换为分
});
// 更新资产余额
await assetService.updateBalance(data.asset, data.amount, 'income');
return record;
}2. 支出记账
功能描述: 记录各种支出项目
数据结构:
interface ExpenseRecord {
id: string;
type: 'expense';
amount: number; // 金额(分)
category: string; // 支出分类
asset: string; // 支出资产
description?: string; // 描述
date: number; // 日期
tags?: string[]; // 标签
attachments?: string[]; // 附件
budget?: string; // 关联预算
createdAt: number;
updatedAt: number;
}实现示例:
// 创建支出记录
async function createExpenseRecord(data: CreateExpenseRequest): Promise<ExpenseRecord> {
// 验证数据
const validation = validateExpenseRecord(data);
if (!validation.isValid) {
throw new Error(validation.errors.join(', '));
}
// 检查资产余额
const asset = await assetService.getAsset(data.asset);
if (asset.balance < data.amount) {
throw new Error('资产余额不足');
}
// 创建记录
const record = await accountingService.createRecord({
...data,
type: 'expense',
amount: data.amount * 100 // 转换为分
});
// 更新资产余额
await assetService.updateBalance(data.asset, -data.amount, 'expense');
// 更新预算执行情况
if (data.budget) {
await budgetService.updateExecution(data.budget, data.amount);
}
return record;
}3. 转账记账
功能描述: 记录资产间的资金转移
数据结构:
interface TransferRecord {
id: string;
type: 'transfer';
amount: number; // 金额(分)
fromAsset: string; // 转出资产
toAsset: string; // 转入资产
description?: string; // 描述
date: number; // 日期
fee?: number; // 手续费
createdAt: number;
updatedAt: number;
}实现示例:
// 创建转账记录
async function createTransferRecord(data: CreateTransferRequest): Promise<TransferRecord> {
// 验证数据
const validation = validateTransferRecord(data);
if (!validation.isValid) {
throw new Error(validation.errors.join(', '));
}
// 检查转出资产余额
const fromAsset = await assetService.getAsset(data.fromAsset);
if (fromAsset.balance < data.amount + (data.fee || 0)) {
throw new Error('转出资产余额不足');
}
// 创建转账记录
const record = await accountingService.createRecord({
...data,
type: 'transfer',
amount: data.amount * 100,
fee: data.fee ? data.fee * 100 : 0
});
// 更新资产余额
await Promise.all([
assetService.updateBalance(data.fromAsset, -(data.amount + (data.fee || 0)), 'transfer'),
assetService.updateBalance(data.toAsset, data.amount, 'transfer')
]);
return record;
}🧠 智能分类系统
分类管理
功能描述: 智能分类推荐和管理
数据结构:
interface Category {
id: string;
name: string;
type: 'income' | 'expense';
icon: string;
color: string;
parentId?: string;
isDefault: boolean;
keywords: string[]; // 关键词
frequency: number; // 使用频率
createdAt: number;
updatedAt: number;
}智能推荐实现:
// 智能分类推荐
async function getCategoryRecommendations(description: string, type: 'income' | 'expense'): Promise<Category[]> {
// 关键词匹配
const keywords = extractKeywords(description);
// 查找匹配的分类
const categories = await categoryService.getCategoriesByType(type);
// 计算匹配度
const scoredCategories = categories.map(category => {
const score = calculateMatchScore(keywords, category.keywords);
return { ...category, score };
});
// 按匹配度和使用频率排序
return scoredCategories
.sort((a, b) => (b.score * 0.7 + b.frequency * 0.3) - (a.score * 0.7 + a.frequency * 0.3))
.slice(0, 5);
}
// 关键词提取
function extractKeywords(text: string): string[] {
// 使用正则表达式提取关键词
const keywords = text.match(/[\u4e00-\u9fa5a-zA-Z]+/g) || [];
return keywords.map(keyword => keyword.toLowerCase());
}
// 匹配度计算
function calculateMatchScore(inputKeywords: string[], categoryKeywords: string[]): number {
let score = 0;
inputKeywords.forEach(keyword => {
if (categoryKeywords.includes(keyword)) {
score += 1;
}
});
return score / Math.max(inputKeywords.length, 1);
}📊 批量操作功能
批量导入
功能描述: 支持从Excel、CSV等格式批量导入记账记录
实现示例:
// 批量导入记录
async function batchImportRecords(file: File): Promise<BatchImportResult> {
// 解析文件
const records = await parseImportFile(file);
// 验证数据
const validation = await validateBatchRecords(records);
if (!validation.isValid) {
return {
success: false,
errors: validation.errors,
imported: 0,
total: records.length
};
}
// 批量创建记录
const results = await Promise.allSettled(
records.map(record => createRecord(record))
);
// 统计结果
const successCount = results.filter(result => result.status === 'fulfilled').length;
const errors = results
.filter(result => result.status === 'rejected')
.map(result => (result as PromiseRejectedResult).reason);
return {
success: errors.length === 0,
errors,
imported: successCount,
total: records.length
};
}
// 解析导入文件
async function parseImportFile(file: File): Promise<ImportRecord[]> {
const text = await file.text();
const lines = text.split('\n');
const headers = lines[0].split(',');
return lines.slice(1).map((line, index) => {
const values = line.split(',');
return {
row: index + 2,
type: values[0],
amount: parseFloat(values[1]),
category: values[2],
asset: values[3],
description: values[4],
date: new Date(values[5]).getTime()
};
});
}批量编辑
功能描述: 支持批量修改记账记录
实现示例:
// 批量编辑记录
async function batchEditRecords(recordIds: string[], updates: Partial<AccountingRecord>): Promise<void> {
// 验证更新数据
const validation = validateBatchUpdates(updates);
if (!validation.isValid) {
throw new Error(validation.errors.join(', '));
}
// 获取原始记录
const originalRecords = await accountingService.getRecordsByIds(recordIds);
// 批量更新
await Promise.all(
recordIds.map(async (id) => {
const originalRecord = originalRecords.find(r => r.id === id);
if (!originalRecord) return;
// 更新记录
await accountingService.updateRecord(id, updates);
// 更新资产余额(如果需要)
if (updates.amount !== undefined || updates.asset !== undefined) {
await updateAssetBalanceForRecord(originalRecord, updates);
}
})
);
}🔍 数据验证
记录验证
功能描述: 确保记账数据的准确性和完整性
实现示例:
// 记录验证器
export class AccountingValidator {
// 验证收入记录
static validateIncomeRecord(data: CreateIncomeRequest): ValidationResult {
const errors: string[] = [];
if (!data.amount || data.amount <= 0) {
errors.push('收入金额必须大于0');
}
if (!data.category) {
errors.push('收入分类不能为空');
}
if (!data.asset) {
errors.push('收入资产不能为空');
}
if (!data.date) {
errors.push('收入日期不能为空');
}
if (data.description && data.description.length > 200) {
errors.push('描述不能超过200个字符');
}
return {
isValid: errors.length === 0,
errors
};
}
// 验证支出记录
static validateExpenseRecord(data: CreateExpenseRequest): ValidationResult {
const errors: string[] = [];
if (!data.amount || data.amount <= 0) {
errors.push('支出金额必须大于0');
}
if (!data.category) {
errors.push('支出分类不能为空');
}
if (!data.asset) {
errors.push('支出资产不能为空');
}
if (!data.date) {
errors.push('支出日期不能为空');
}
return {
isValid: errors.length === 0,
errors
};
}
// 验证转账记录
static validateTransferRecord(data: CreateTransferRequest): ValidationResult {
const errors: string[] = [];
if (!data.amount || data.amount <= 0) {
errors.push('转账金额必须大于0');
}
if (!data.fromAsset) {
errors.push('转出资产不能为空');
}
if (!data.toAsset) {
errors.push('转入资产不能为空');
}
if (data.fromAsset === data.toAsset) {
errors.push('转出和转入资产不能相同');
}
if (!data.date) {
errors.push('转账日期不能为空');
}
return {
isValid: errors.length === 0,
errors
};
}
}📈 统计分析
收支统计
功能描述: 提供详细的收支统计分析
实现示例:
// 收支统计
async function getIncomeExpenseStats(params: StatsParams): Promise<IncomeExpenseStats> {
const { startDate, endDate, groupBy = 'day' } = params;
// 获取收入统计
const incomeStats = await accountingService.getStats({
type: 'income',
startDate,
endDate,
groupBy
});
// 获取支出统计
const expenseStats = await accountingService.getStats({
type: 'expense',
startDate,
endDate,
groupBy
});
// 计算净收入
const netIncome = incomeStats.total - expenseStats.total;
return {
income: incomeStats,
expense: expenseStats,
netIncome,
period: { startDate, endDate },
groupBy
};
}
// 分类统计
async function getCategoryStats(params: CategoryStatsParams): Promise<CategoryStats[]> {
const { type, startDate, endDate, limit = 10 } = params;
const stats = await accountingService.getCategoryStats({
type,
startDate,
endDate,
limit
});
return stats.map(stat => ({
category: stat.category,
amount: stat.amount,
count: stat.count,
percentage: stat.percentage,
trend: stat.trend
}));
}🔄 状态管理
Pinia Store
功能描述: 使用Pinia管理记账状态
实现示例:
// 记账状态管理
export const useAccountingStore = defineStore('accounting', () => {
// 状态
const records = ref<AccountingRecord[]>([]);
const categories = ref<Category[]>([]);
const isLoading = ref(false);
const currentFilter = ref<RecordFilter>({});
// 计算属性
const filteredRecords = computed(() => {
return filterRecords(records.value, currentFilter.value);
});
const totalIncome = computed(() => {
return records.value
.filter(r => r.type === 'income')
.reduce((sum, r) => sum + r.amount, 0);
});
const totalExpense = computed(() => {
return records.value
.filter(r => r.type === 'expense')
.reduce((sum, r) => sum + r.amount, 0);
});
const netIncome = computed(() => {
return totalIncome.value - totalExpense.value;
});
// 操作
const fetchRecords = async (params?: RecordQueryParams): Promise<void> => {
isLoading.value = true;
try {
const result = await accountingService.getRecords(params);
records.value = result.list;
} catch (error) {
console.error('获取记录失败:', error);
throw error;
} finally {
isLoading.value = false;
}
};
const addRecord = async (data: CreateRecordRequest): Promise<AccountingRecord> => {
try {
const record = await accountingService.createRecord(data);
records.value.unshift(record);
return record;
} catch (error) {
console.error('添加记录失败:', error);
throw error;
}
};
const updateRecord = async (id: string, data: UpdateRecordRequest): Promise<AccountingRecord> => {
try {
const record = await accountingService.updateRecord(id, data);
const index = records.value.findIndex(r => r.id === id);
if (index !== -1) {
records.value[index] = record;
}
return record;
} catch (error) {
console.error('更新记录失败:', error);
throw error;
}
};
const deleteRecord = async (id: string): Promise<void> => {
try {
await accountingService.deleteRecord(id);
records.value = records.value.filter(r => r.id !== id);
} catch (error) {
console.error('删除记录失败:', error);
throw error;
}
};
const setFilter = (filter: RecordFilter): void => {
currentFilter.value = filter;
};
return {
records,
categories,
isLoading,
currentFilter,
filteredRecords,
totalIncome,
totalExpense,
netIncome,
fetchRecords,
addRecord,
updateRecord,
deleteRecord,
setFilter
};
});🧪 测试
单元测试
功能描述: 记账模块的单元测试
实现示例:
// 记账验证器测试
describe('AccountingValidator', () => {
describe('validateIncomeRecord', () => {
it('should validate valid income record', () => {
const data = {
amount: 1000,
category: 'salary',
asset: 'bank',
date: Date.now(),
description: '工资收入'
};
const result = AccountingValidator.validateIncomeRecord(data);
expect(result.isValid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should reject invalid income record', () => {
const data = {
amount: -100,
category: '',
asset: '',
date: 0,
description: 'a'.repeat(201)
};
const result = AccountingValidator.validateIncomeRecord(data);
expect(result.isValid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
});
});
});
// 记账服务测试
describe('AccountingService', () => {
it('should create income record successfully', async () => {
const data = {
amount: 1000,
category: 'salary',
asset: 'bank',
date: Date.now(),
description: '工资收入'
};
const record = await accountingService.createRecord(data);
expect(record.id).toBeDefined();
expect(record.type).toBe('income');
expect(record.amount).toBe(1000);
});
});📚 最佳实践
1. 数据一致性
- 确保记账记录与资产余额同步
- 使用事务处理复杂操作
- 实现数据验证和错误处理
- 定期数据校验和修复
2. 用户体验
- 提供快速记账入口
- 智能分类推荐
- 批量操作支持
- 数据导入导出功能
3. 性能优化
- 分页加载记录列表
- 缓存常用数据
- 异步处理批量操作
- 优化数据库查询
4. 安全考虑
- 验证用户权限
- 防止数据篡改
- 敏感信息加密
- 操作日志记录
Penny Lens 记账模块 - 让财务管理更科学、更高效 💰
