1. 项目背景与核心价值
去年我决定彻底解决自己的财务糊涂账问题。试用了市面上十几款记账软件后,发现要么功能太复杂,要么数据隐私存疑,要么无法跨设备同步。作为一个全栈开发者,我意识到完全可以打造一个完全符合自己需求的记账系统——既能通过网页端详细管理数据,又能用手机小程序随时记录开销。
这个项目的核心价值在于:
- 完全掌控数据所有权(所有记录保存在自己的服务器)
- 深度定制分类体系和报表功能
- 实现"电脑记账+手机速记"的无缝体验
- 开发成本可控(主要技术栈都是开源方案)
经过三个月的迭代,目前系统已稳定运行半年,日均记账50+笔。下面就把从技术选型到部署上线的完整过程拆解给大家,特别会重点分享那些官方文档里不会写的实战经验。
2. 技术架构设计
2.1 整体技术栈选择
采用前后端分离架构,主要组件如下:
| 模块 | 技术方案 | 选型理由 |
|---|---|---|
| 前端 | Vue3 + Vant | 组件丰富,适合快速开发管理后台和移动端H5 |
| 小程序 | Uni-app | 一套代码同时生成微信/支付宝小程序,维护成本低 |
| 后端 | NestJS | 模块化架构适合复杂业务逻辑,TypeScript支持完善 |
| 数据库 | PostgreSQL | 财务数据需要严格的事务支持,JSONB类型便于存储动态字段 |
| 部署 | Docker + Nginx | 容器化部署方便迁移,Nginx处理静态资源和反向代理 |
| 数据分析 | ECharts | 强大的可视化库,支持自定义报表 |
| 同步方案 | WebSocket + 增量同步协议 | 解决多端数据一致性问题 |
避坑提示:初期尝试用MongoDB存储交易记录,但在复杂聚合查询时性能较差,特别是涉及多条件统计时。财务系统强烈建议使用关系型数据库。
2.2 关键业务流程设计
记账系统的核心流程需要特别关注数据一致性和操作便捷性:
记账流程:
- 小程序端:极简输入(金额+分类+备注)
- 网页端:完整表单(含票据图片上传、多账户转账等)
- 统一通过WebSocket实时同步到服务端
数据同步机制:
// 增量同步协议示例 interface SyncPacket { clientId: string; lastSyncTime: number; operations: Array<{ type: 'CREATE' | 'UPDATE' | 'DELETE'; table: 'transactions' | 'categories'; data: Record<string, any>; }>; }对账逻辑:
- 每日自动生成账户余额快照
- 提供余额差异检测功能
- 支持手动调整记录(保留审计日志)
3. 核心功能实现细节
3.1 多端统一的账目模型
设计良好的数据模型是系统的基石。我的账目模型包含以下关键字段:
interface Transaction { id: string; // 雪花ID amount: number; // 支持正负值 currency: string; // 默认CNY type: 'expense' | 'income' | 'transfer'; category: { // 分类体系 id: string; name: string; icon: string; }; account: string; // 关联账户ID targetAccount?: string; // 转账专用 occurTime: Date; // 实际发生时间 createTime: Date; // 记录创建时间 tags?: string[]; // 自定义标签 images?: string[]; // 票据图片URL remark?: string; // 备注文本 }分类体系设计技巧:
- 采用三级分类(大类-中类-小类)
- 支持用户自定义图标和颜色
- 内置常用分类模板(可导入)
- 通过material-design-icons库获得丰富的图标选择
3.2 实时同步的实现
多端数据同步是体验的关键,我的解决方案是:
客户端:
- 本地维护一个SQLite数据库
- 所有操作先在本地执行
- 通过队列异步同步到服务端
服务端:
// WebSocket网关实现 @WebSocketGateway() export class SyncGateway { @WebSocketServer() server: Server; @SubscribeMessage('sync') handleSync(client: Socket, payload: SyncPacket): SyncResult { // 处理冲突解决逻辑 return this.syncService.process(payload); } }冲突解决策略:
- 最后修改时间优先
- 保留冲突记录供人工处理
- 重要操作(如删除)需要二次确认
3.3 数据分析模块
报表功能采用按需计算方案:
预聚合设计:
CREATE MATERIALIZED VIEW daily_stats AS SELECT date_trunc('day', occur_time) AS day, type, category->>'id' AS category_id, SUM(amount) AS total FROM transactions GROUP BY day, type, category_id;动态查询接口:
@Get('report') async getReport( @Query('start') start: string, @Query('end') end: string, @Query('dimensions') dimensions: string[] ) { return this.reportService.build({ range: [new Date(start), new Date(end)], dimensions: dimensions.split(',') }); }可视化优化:
- 使用ECharts的dataset特性实现动态数据绑定
- 移动端采用简化版图表
- 支持报表模板保存
4. 部署与运维实战
4.1 服务器配置建议
我的生产环境配置(月均5000+条记录):
| 资源 | 规格 | 说明 |
|---|---|---|
| 云服务器 | 2核4G | 突发性能实例足够应对个人使用 |
| 数据库 | PostgreSQL 14 | 单独实例,1核2G配置 |
| 对象存储 | 50GB | 存储票据图片 |
| 带宽 | 5Mbps | 主要服务API和小程序资源 |
4.2 Docker部署方案
完整的docker-compose.yml配置:
version: '3' services: app: build: . ports: - "3000:3000" depends_on: - db environment: DATABASE_URL: postgres://user:pass@db:5432/ledger restart: unless-stopped db: image: postgres:14 volumes: - pg_data:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: pass restart: unless-stopped nginx: image: nginx:alpine ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./dist:/usr/share/nginx/html depends_on: - app volumes: pg_data:重要安全提示:务必配置数据库定期备份!我使用pg_dump每天凌晨3点全量备份到对象存储。
4.3 性能优化技巧
数据库层面:
- 为occur_time字段创建BRIN索引
- 对常用查询条件建立复合索引
- 定期执行VACUUM ANALYZE
API层面:
- 使用Redis缓存热点数据
- 实现ETag缓存策略
- 对大数据量查询实现分页
前端层面:
- 小程序采用分包加载
- 使用virtual-list优化长列表
- 实现骨架屏提升体验
5. 典型问题排查指南
5.1 同步冲突处理
现象:同一笔记录在不同设备显示不一致
排查步骤:
- 检查客户端和服务端的lastSyncTime
- 查看操作日志中的冲突记录
- 比对各端的操作序列
解决方案:
// 冲突解决算法示例 function resolveConflict(client, server) { if (client.version > server.version) { return { ...client, status: 'merged' }; } else if (server.version > client.version) { return { ...server, status: 'override' }; } else { // 保留双方记录并标记冲突 return [ { ...client, status: 'conflict' }, { ...server, status: 'conflict' } ]; } }5.2 报表数据不准
常见原因:
- 时区设置不一致
- 未刷新的物化视图
- 转账记录重复计算
修复方案:
- 统一使用UTC时间存储
- 设置物化视图自动刷新
- 转账统计时排除内部交易
5.3 小程序审核问题
经验总结:
- 避免使用"记账"等金融相关关键词
- 个人版不要提供多用户功能
- 数据导出格式需符合平台规范
- 隐私协议必须明确数据存储位置
6. 扩展与个性化方案
6.1 自动化记账
通过以下方式减少手动输入:
- 邮件账单解析(配置转发规则)
- 短信消费提醒抓取(Android可用Tasker)
- 支付宝/微信账单导入
# 账单解析示例(支付宝格式) def parse_alipay_bill(file): df = pd.read_csv(file, skiprows=3) return [{ 'amount': float(row['金额']), 'type': 'income' if row['收/支'] == '收入' else 'expense', 'occurTime': datetime.strptime(row['交易时间'], '%Y-%m-%d %H:%M:%S'), 'remark': row['商品说明'] } for _, row in df.iterrows()]6.2 多币种支持
实现方案:
- 存储交易时的实时汇率
- 提供汇率手动维护界面
- 报表支持按本币/原币展示
-- 汇率表设计 CREATE TABLE exchange_rates ( from_currency CHAR(3), to_currency CHAR(3), rate DECIMAL(10,6), date DATE, PRIMARY KEY (from_currency, to_currency, date) );6.3 预算功能
核心逻辑:
- 按周期(周/月/年)设置预算
- 支持分类预算和总预算
- 提供超额预警
// 预算检查中间件 @Injectable() export class BudgetAlertMiddleware implements NestMiddleware { async use(req: Request, res: Response, next: NextFunction) { const transaction = req.body; const budgets = await this.budgetService.getActiveBudgets(); budgets.forEach(budget => { if (budget.isOver(transaction)) { this.notificationService.sendAlert(budget); } }); next(); } }这个项目给我最大的收获是:好的个人工具必须平衡功能的完整性和使用的便捷性。现在我的记账系统已经能自动处理70%的日常记录,每月对账时间从原来的3小时缩短到20分钟。如果你也想打造自己的记账系统,建议先从最小可用版本开始,逐步迭代完善。