2026-03-12 11:23:10 +08:00
const express = require ( 'express' );
const db = require ( '../db' );
2026-03-26 10:39:58 +08:00
const logger = require ( '../utils/logger' );
const { parseCSVLine , AppError } = require ( '../utils/helpers' );
2026-03-12 11:23:10 +08:00
const router = express . Router ();
// 导出数据
2026-03-26 10:39:58 +08:00
router . get ( '/export' , ( req , res , next ) => {
try {
const format = req . query . format ;
if ( ! format || ! [ 'json' , 'csv' ]. includes ( format )) {
throw new AppError ( '请指定格式: json 或 csv' , 400 );
}
const records = db . prepare (
'SELECT type, category, amount, quantity, unit_price, rebate, boss, partner, source, destination, note, created_at FROM records WHERE user_id = ? ORDER BY created_at DESC'
). all ( req . user . id );
const timestamp = new Date (). toISOString (). replace ( /[:.]/g , '-' ). slice ( 0 , 19 );
const filename = `gamer-export- ${ timestamp } ` ;
if ( format === 'json' ) {
const exportData = {
exportVersion : 1 ,
exportedAt : new Date (). toISOString (),
username : req . user . username ,
recordCount : records . length ,
records
};
logger . info ( `用户 ${ req . user . username } 导出 JSON 数据: ${ records . length } 条` );
res . setHeader ( 'Content-Type' , 'application/json; charset=utf-8' );
res . setHeader ( 'Content-Disposition' , `attachment; filename=" ${ filename } .json"` );
return res . send ( JSON . stringify ( exportData , null , 2 ));
}
// CSV with UTF-8 BOM
const BOM = '\uFEFF' ;
const cols = [ 'type' , 'category' , 'amount' , 'quantity' , 'unit_price' , 'rebate' , 'boss' , 'partner' , 'source' , 'destination' , 'note' , 'created_at' ];
const csvRows = records . map ( r => {
return cols . map ( col => {
const val = r [ col ];
if ( val === null || val === undefined ) return '' ;
const str = String ( val );
if ( str . includes ( ',' ) || str . includes ( '"' ) || str . includes ( '\n' )) {
return '"' + str . replace ( /"/g , '""' ) + '"' ;
}
return str ;
}). join ( ',' );
});
logger . info ( `用户 ${ req . user . username } 导出 CSV 数据: ${ records . length } 条` );
res . setHeader ( 'Content-Type' , 'text/csv; charset=utf-8' );
res . setHeader ( 'Content-Disposition' , `attachment; filename=" ${ filename } .csv"` );
res . send ( BOM + cols . join ( ',' ) + '\n' + csvRows . join ( '\n' ));
} catch ( err ) {
next ( err );
2026-03-12 11:23:10 +08:00
}
});
// 导入数据
2026-03-26 10:39:58 +08:00
router . post ( '/import' , express . text ({ type : '*/*' , limit : '10mb' }), ( req , res , next ) => {
2026-03-12 11:23:10 +08:00
try {
const raw = req . body ;
if ( ! raw || typeof raw !== 'string' ) {
2026-03-26 10:39:58 +08:00
throw new AppError ( '未收到文件内容' , 400 );
2026-03-12 11:23:10 +08:00
}
let records = [];
const requiredFields = [ 'type' , 'category' , 'amount' , 'created_at' ];
const allFields = [ 'type' , 'category' , 'amount' , 'quantity' , 'unit_price' , 'rebate' , 'boss' , 'partner' , 'source' , 'destination' , 'note' , 'created_at' ];
const trimmed = raw . replace ( /^\uFEFF/ , '' ). trim ();
if ( trimmed . startsWith ( '{' )) {
const data = JSON . parse ( trimmed );
if ( ! data . exportVersion || ! Array . isArray ( data . records )) {
2026-03-26 10:39:58 +08:00
throw new AppError ( '无效的JSON导出文件格式' , 400 );
2026-03-12 11:23:10 +08:00
}
records = data . records ;
} else {
const lines = trimmed . split ( '\n' ). map ( l => l . replace ( /\r$/ , '' ));
if ( lines . length < 2 ) {
2026-03-26 10:39:58 +08:00
throw new AppError ( 'CSV文件为空或格式不正确' , 400 );
2026-03-12 11:23:10 +08:00
}
if ( lines [ 0 ] !== allFields . join ( ',' )) {
2026-03-26 10:39:58 +08:00
throw new AppError ( 'CSV列头不匹配,请使用本系统导出的文件' , 400 );
2026-03-12 11:23:10 +08:00
}
const csvHeaders = lines [ 0 ]. split ( ',' );
for ( let i = 1 ; i < lines . length ; i ++ ) {
if ( ! lines [ i ]. trim ()) continue ;
const values = parseCSVLine ( lines [ i ]);
const record = {};
csvHeaders . forEach (( h , idx ) => { record [ h ] = values [ idx ] || null ; });
if ( record . amount ) record . amount = parseFloat ( record . amount );
if ( record . quantity ) record . quantity = parseInt ( record . quantity ) || null ;
if ( record . unit_price ) record . unit_price = parseFloat ( record . unit_price ) || null ;
if ( record . rebate ) record . rebate = parseFloat ( record . rebate ) || null ;
records . push ( record );
}
}
for ( const r of records ) {
for ( const f of requiredFields ) {
if ( ! r [ f ] && r [ f ] !== 0 ) {
2026-03-26 10:39:58 +08:00
throw new AppError ( `记录缺少必填字段: ${ f } ` , 400 );
2026-03-12 11:23:10 +08:00
}
}
if ( ! [ 'income' , 'expense' ]. includes ( r . type )) {
2026-03-26 10:39:58 +08:00
throw new AppError ( `无效的类型: ${ r . type } ` , 400 );
2026-03-12 11:23:10 +08:00
}
}
const existingCheck = db . prepare (
'SELECT id FROM records WHERE user_id = ? AND created_at = ? AND category = ? AND amount = ?'
);
const insertStmt = db . prepare ( `
INSERT INTO records (user_id, type, category, amount, quantity, unit_price, rebate, boss, partner, source, destination, note, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
` );
let imported = 0 , skipped = 0 ;
const insertMany = db . transaction (( recs ) => {
for ( const r of recs ) {
if ( existingCheck . get ( req . user . id , r . created_at , r . category , r . amount )) {
skipped ++ ; continue ;
}
insertStmt . run ( req . user . id , r . type , r . category , r . amount ,
r . quantity || null , r . unit_price || null , r . rebate || null ,
r . boss || null , r . partner || null ,
r . source || null , r . destination || null ,
r . note || null , r . created_at );
imported ++ ;
}
});
insertMany ( records );
2026-03-26 10:39:58 +08:00
logger . info ( `用户 ${ req . user . username } 导入数据: ${ imported } 条, 跳过 ${ skipped } 条` );
2026-03-12 11:23:10 +08:00
res . json ({ imported , skipped , total : records . length });
2026-03-26 10:39:58 +08:00
} catch ( err ) {
if ( err instanceof SyntaxError ) {
next ( new AppError ( '文件解析失败: ' + err . message , 400 ));
} else {
next ( err );
}
2026-03-12 11:23:10 +08:00
}
});
module . exports = router ;