149 lines
5.1 KiB
JavaScript
Executable File
149 lines
5.1 KiB
JavaScript
Executable File
const XLSX = require('xlsx');
|
|
|
|
const API_BASE = 'http://localhost:3000/api';
|
|
const USER = '猪2';
|
|
|
|
// 分类ID映射
|
|
const CAT = {
|
|
// 支出
|
|
餐饮: 5, 交通: 6, 购物: 7, 房租: 8, 水电燃气: 9, 话费: 10,
|
|
亚马逊物流费: 11, 亚马逊广告费: 12, 亚马逊货款: 13, 其他支出: 14,
|
|
// 收入
|
|
工资: 1, 亚马逊转账: 2, 利息: 3, 其他收入: 4,
|
|
};
|
|
|
|
// Excel序列号转日期
|
|
function excelDateToStr(serial) {
|
|
const utcDays = Math.floor(serial - 25569);
|
|
const d = new Date(utcDays * 86400 * 1000);
|
|
const y = d.getUTCFullYear();
|
|
const m = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
const day = String(d.getUTCDate()).padStart(2, '0');
|
|
return `${y}-${m}-${day}`;
|
|
}
|
|
|
|
// 根据交易对方和商品智能分类
|
|
function classify(row) {
|
|
const counterparty = (row[2] || '').toString();
|
|
const product = (row[3] || '').toString();
|
|
const direction = (row[4] || '').toString(); // 收/支
|
|
const status = (row[7] || '').toString();
|
|
const txType = (row[1] || '').toString();
|
|
|
|
const isIncome = direction === '收入';
|
|
const combined = counterparty + product;
|
|
|
|
if (isIncome) {
|
|
// 收入分类
|
|
if (txType.includes('退款')) {
|
|
return { id: CAT.其他收入, note: `${counterparty}退款` };
|
|
}
|
|
if (txType.includes('红包') || combined.includes('红包')) {
|
|
return { id: CAT.其他收入, note: `微信红包-${counterparty}` };
|
|
}
|
|
return { id: CAT.其他收入, note: counterparty };
|
|
}
|
|
|
|
// 支出分类
|
|
// 交通
|
|
if (combined.includes('12306') || combined.includes('中铁网络')) {
|
|
return { id: CAT.交通, note: '12306火车票' };
|
|
}
|
|
if (combined.includes('货拉拉')) {
|
|
return { id: CAT.交通, note: `货拉拉-${product}` };
|
|
}
|
|
|
|
// 餐饮
|
|
if (combined.includes('肯德基') || combined.includes('KFC')) {
|
|
return { id: CAT.餐饮, note: '肯德基' };
|
|
}
|
|
if (combined.includes('美团') || combined.includes('大众点评')) {
|
|
return { id: CAT.餐饮, note: `美团-${counterparty}` };
|
|
}
|
|
if (combined.includes('菜馆') || combined.includes('米线') || combined.includes('羊肉')
|
|
|| combined.includes('餐') || combined.includes('饭') || combined.includes('食')
|
|
|| combined.includes('厨') || combined.includes('锅')) {
|
|
return { id: CAT.餐饮, note: counterparty };
|
|
}
|
|
if (counterparty.includes('农夫山泉')) {
|
|
return { id: CAT.餐饮, note: '农夫山泉饮用水' };
|
|
}
|
|
if (combined.includes('腐乳') || combined.includes('杨伯雪')) {
|
|
return { id: CAT.餐饮, note: `${counterparty}-转账购买` };
|
|
}
|
|
|
|
// 购物
|
|
if (combined.includes('拼多多')) {
|
|
return { id: CAT.购物, note: `拼多多-${product}` };
|
|
}
|
|
if (combined.includes('山姆') || combined.includes('Sams')) {
|
|
return { id: CAT.购物, note: '山姆会员商店' };
|
|
}
|
|
|
|
// 其他支出(带具体备注)
|
|
if (combined.includes('丰巢') || combined.includes('寄件')) {
|
|
return { id: CAT.其他支出, note: `快递寄件-${counterparty}` };
|
|
}
|
|
if (combined.includes('淘宝退货') || combined.includes('退货')) {
|
|
return { id: CAT.其他支出, note: '淘宝退货运费' };
|
|
}
|
|
if (combined.includes('文化') || combined.includes('地图') || combined.includes('门票')) {
|
|
return { id: CAT.其他支出, note: `旅游/门票-${counterparty}` };
|
|
}
|
|
if (counterparty.includes('倾城山')) {
|
|
return { id: CAT.其他支出, note: '景点门票-倾城山' };
|
|
}
|
|
|
|
// 兜底
|
|
return { id: CAT.其他支出, note: `${counterparty}-${product}` };
|
|
}
|
|
|
|
async function main() {
|
|
const wb = XLSX.readFile('微信支付账单流水文件(20260101-20260321).xlsx');
|
|
const ws = wb.Sheets[wb.SheetNames[0]];
|
|
const data = XLSX.utils.sheet_to_json(ws, { header: 1 });
|
|
|
|
const records = [];
|
|
for (let i = 18; i < data.length; i++) {
|
|
const row = data[i];
|
|
if (!row || row.length < 6) continue;
|
|
|
|
const dateStr = excelDateToStr(row[0]);
|
|
const direction = (row[4] || '').toString();
|
|
const amount = parseFloat(row[5]);
|
|
const type = direction === '收入' ? 'income' : 'expense';
|
|
const { id: category_id, note } = classify(row);
|
|
|
|
records.push({ date: dateStr, type, category_id, amount, note, user: USER });
|
|
}
|
|
|
|
console.log(`共解析 ${records.length} 条记录,开始导入...\n`);
|
|
|
|
let success = 0, fail = 0;
|
|
for (let i = 0; i < records.length; i++) {
|
|
const r = records[i];
|
|
try {
|
|
const res = await fetch(`${API_BASE}/records`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(r),
|
|
});
|
|
if (res.ok) {
|
|
success++;
|
|
console.log(`[${i + 1}/${records.length}] ✓ ${r.date} ${r.type === 'income' ? '收入' : '支出'} ¥${r.amount} ${r.note}`);
|
|
} else {
|
|
fail++;
|
|
const err = await res.json();
|
|
console.log(`[${i + 1}/${records.length}] ✗ ${r.date} ¥${r.amount} - ${err.error}`);
|
|
}
|
|
} catch (e) {
|
|
fail++;
|
|
console.log(`[${i + 1}/${records.length}] ✗ ${r.date} ¥${r.amount} - ${e.message}`);
|
|
}
|
|
}
|
|
|
|
console.log(`\n导入完成!成功: ${success}, 失败: ${fail}`);
|
|
}
|
|
|
|
main().catch(console.error);
|