Files
accounting/data/import-alipay.js

188 lines
6.5 KiB
JavaScript
Raw Normal View History

2026-03-26 01:23:19 +08:00
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);