Penny Lens 记账模块详细文档

2025年1月27日
7 分钟阅读
作者:Penny Lens Team

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 记账模块 - 让财务管理更科学、更高效 💰