const fs = require('fs'); const API_BASE = 'http://localhost:3000/api'; const USER = '猪2'; const CAT = { 餐饮: 5, 交通: 6, 购物: 7, 房租: 8, 水电燃气: 9, 话费: 10, 亚马逊物流费: 11, 亚马逊广告费: 12, 亚马逊货款: 13, 其他支出: 14, 工资: 1, 亚马逊转账: 2, 利息: 3, 其他收入: 4, }; function parseCSV(text) { const lines = text.split('\n'); // 找到表头行 let headerIdx = -1; for (let i = 0; i < lines.length; i++) { if (lines[i].startsWith('交易时间,')) { headerIdx = i; break; } } if (headerIdx === -1) throw new Error('找不到表头'); const records = []; for (let i = headerIdx + 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line || line.startsWith('---')) continue; // 支付宝CSV用逗号分隔,但商品说明中可能没有逗号问题(支付宝导出格式较规范) const parts = line.split(','); if (parts.length < 11) continue; records.push({ time: parts[0].trim(), txCategory: parts[1].trim(), counterparty: parts[2].trim(), account: parts[3].trim(), product: parts[4].trim(), direction: parts[5].trim(), // 收/支/不计收支 amount: parseFloat(parts[6].trim()), payMethod: parts[7].trim(), status: parts[8].trim(), orderId: parts[9].trim(), merchantId: parts[10].trim(), remark: (parts[11] || '').trim(), }); } return records; } function classify(r) { const { txCategory, counterparty, product, direction, status } = r; const combined = counterparty + product; // === 余额宝收益 → 利息(收入) === if (product.includes('收益发放') && combined.includes('天弘基金')) { return { type: 'income', id: CAT.利息, note: product }; } // === 淘宝签到提现 / 芭芭农场 → 利息(收入) === if (product.includes('淘宝签到提现') || product.includes('芭芭农场')) { return { type: 'income', id: CAT.利息, note: product }; } // === 跳过其他"不计收支"记录 === if (direction === '不计收支') { return null; // 退款、花呗还款、余额宝自动转入等 } // === 收入 === if (direction === '收入') { if (combined.includes('红包') || combined.includes('火把购')) { return { type: 'income', id: CAT.其他收入, note: `${counterparty}-${product}` }; } if (combined.includes('收钱码')) { return { type: 'income', id: CAT.其他收入, note: `收钱码-${counterparty}` }; } return { type: 'income', id: CAT.其他收入, note: `${counterparty}-${product}` }; } // === 支出 === // 亚马逊货款 if (product.includes('亚马逊') || (counterparty.includes('郦鸿飞') && product.includes('亚马逊'))) { return { type: 'expense', id: CAT.亚马逊货款, note: `亚马逊货款-${counterparty}` }; } // 餐饮 if (txCategory === '餐饮美食') { return { type: 'expense', id: CAT.餐饮, note: product.length > 30 ? counterparty : product }; } // 交通 if (txCategory === '交通出行') { return { type: 'expense', id: CAT.交通, note: `${counterparty}-${product}`.substring(0, 50) }; } if (product.includes('交通一码通')) { return { type: 'expense', id: CAT.交通, note: '杭州市民卡交通充值' }; } // 话费 if (txCategory === '充值缴费' && combined.includes('话费')) { return { type: 'expense', id: CAT.话费, note: product }; } // 水电燃气 if (txCategory === '充值缴费' && (combined.includes('国网') || combined.includes('电费') || combined.includes('燃气') || combined.includes('水费'))) { return { type: 'expense', id: CAT.水电燃气, note: product }; } // 保险 if (txCategory === '保险') { return { type: 'expense', id: CAT.其他支出, note: `保险-${product}`.substring(0, 50) }; } // 转账红包(支出) if (txCategory === '转账红包' && direction === '支出') { if (combined.includes('亚马逊')) { return { type: 'expense', id: CAT.亚马逊货款, note: `亚马逊货款-${counterparty}` }; } return { type: 'expense', id: CAT.其他支出, note: `转账-${counterparty}-${product}`.substring(0, 50) }; } // 购物类(日用百货/服饰/家居/数码/运动/美容/宠物/母婴/文化休闲/信用借还以外的充值缴费) const shoppingCategories = ['日用百货', '服饰装扮', '家居家装', '数码电器', '运动户外', '美容美发', '宠物', '母婴亲子', '文化休闲']; if (shoppingCategories.includes(txCategory)) { return { type: 'expense', id: CAT.购物, note: product.length > 40 ? counterparty : product.substring(0, 40) }; } // 兜底 return { type: 'expense', id: CAT.其他支出, note: `${txCategory}-${counterparty}-${product}`.substring(0, 50) }; } async function main() { const buf = fs.readFileSync('支付宝交易明细(20260101-20260321).csv'); const text = new TextDecoder('gbk').decode(buf); const rawRecords = parseCSV(text); console.log(`共解析 ${rawRecords.length} 条原始记录`); const toImport = []; let skipped = 0; for (const r of rawRecords) { // 跳过金额为0 if (r.amount === 0 || isNaN(r.amount)) { skipped++; continue; } // 跳过交易关闭 if (r.status === '交易关闭') { skipped++; continue; } const result = classify(r); if (!result) { skipped++; continue; } // 不计收支被跳过 const date = r.time.substring(0, 10); // YYYY-MM-DD toImport.push({ date, type: result.type, category_id: result.id, amount: r.amount, note: result.note, user: USER, }); } console.log(`需导入 ${toImport.length} 条,跳过 ${skipped} 条\n`); let success = 0, fail = 0; for (let i = 0; i < toImport.length; i++) { const r = toImport[i]; try { const res = await fetch(`${API_BASE}/records`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(r), }); if (res.ok) { success++; const label = r.type === 'income' ? '收入' : '支出'; console.log(`[${i + 1}/${toImport.length}] OK ${r.date} ${label} ¥${r.amount} ${r.note}`); } else { fail++; const err = await res.json(); console.log(`[${i + 1}/${toImport.length}] FAIL ${r.date} ¥${r.amount} - ${err.error}`); } } catch (e) { fail++; console.log(`[${i + 1}/${toImport.length}] FAIL ${r.date} ¥${r.amount} - ${e.message}`); } } console.log(`\n导入完成!成功: ${success}, 失败: ${fail}`); } main().catch(console.error);