seller 端商品字段批量维护工具
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. const express = require('express');
  2. const multer = require('multer');
  3. const XLSX = require('xlsx');
  4. const path = require('path');
  5. const fs = require('fs');
  6. const uploadRouter = require('./routes/upload');
  7. const dotenv = require('dotenv');
  8. // 确保在其他代码之前加载环境变量
  9. const result = dotenv.config({
  10. path: path.resolve(__dirname, '.env')
  11. });
  12. if (result.error) {
  13. console.error('Error loading .env file:', result.error);
  14. }
  15. const app = express();
  16. const upload = multer({ dest: 'uploads/' });
  17. // 用于存储原始工作簿格式
  18. let originalWorkbook = null;
  19. // 配置中间件
  20. app.use(express.json({limit: '50mb'}));
  21. app.use(express.urlencoded({ extended: true, limit: '50mb' }));
  22. app.use(express.static(path.join(__dirname, 'public')));
  23. // 注册图片上传路由
  24. app.use('/', uploadRouter);
  25. // Excel文件上传处理
  26. app.post('/upload', upload.single('excelFile'), (req, res) => {
  27. if (!req.file) {
  28. return res.status(400).send('No file uploaded.');
  29. }
  30. // 读取并保存原始工作簿
  31. originalWorkbook = XLSX.readFile(req.file.path, {
  32. cellStyles: true,
  33. cellNF: true,
  34. cellFormula: true
  35. });
  36. const firstSheet = originalWorkbook.Sheets[originalWorkbook.SheetNames[0]];
  37. // 读取标题 (A1 到 AR1)
  38. const headers = [];
  39. for (let i = 0; i < 44; i++) {
  40. const cellRef = XLSX.utils.encode_cell({c: i, r: 0});
  41. const cell = firstSheet[cellRef];
  42. headers.push(cell ? cell.v : '');
  43. }
  44. // 读取数据并保留单元格格式信息
  45. const range = XLSX.utils.decode_range(firstSheet['!ref']);
  46. const data = [];
  47. for(let R = 1; R <= range.e.r; ++R) {
  48. const row = {};
  49. for(let C = 0; C <= range.e.c; ++C) {
  50. const cellRef = XLSX.utils.encode_cell({c: C, r: R});
  51. const cell = firstSheet[cellRef];
  52. if(cell) {
  53. row[headers[C]] = cell.v;
  54. }
  55. }
  56. data.push(row);
  57. }
  58. // 保存原始文件路径
  59. originalWorkbook.filePath = req.file.path;
  60. res.json({headers, data});
  61. });
  62. // 保存编辑后的数据
  63. app.post('/save', (req, res) => {
  64. try {
  65. const { headers, data } = req.body;
  66. // 检查超长内容
  67. const exceedingCells = [];
  68. data.forEach((row, rowIndex) => {
  69. headers.forEach((header, colIndex) => {
  70. const content = row[header];
  71. if (content && content.length > 32767) {
  72. exceedingCells.push({
  73. row: rowIndex + 2,
  74. column: header,
  75. length: content.length
  76. });
  77. }
  78. });
  79. });
  80. if (exceedingCells.length > 0) {
  81. const errorMessage = exceedingCells.map(cell =>
  82. `第 ${cell.row} 行的 "${cell.column}" 列内容长度为 ${cell.length},超出了Excel单元格32767个字符的限制`
  83. ).join('\n');
  84. return res.status(400).json({
  85. error: '存在超出Excel字符限制的单元格',
  86. details: exceedingCells,
  87. message: errorMessage
  88. });
  89. }
  90. if (!originalWorkbook) {
  91. return res.status(400).json({ error: 'No original workbook found. Please upload file first.' });
  92. }
  93. const originalSheet = originalWorkbook.Sheets[originalWorkbook.SheetNames[0]];
  94. data.forEach((row, rowIndex) => {
  95. headers.forEach((header, colIndex) => {
  96. const cellRef = XLSX.utils.encode_cell({ r: rowIndex + 1, c: colIndex });
  97. const value = row[header];
  98. if (value === undefined || value === null) {
  99. // 如果单元格值为空,则删除该单元格
  100. delete originalSheet[cellRef];
  101. } else {
  102. // 创建或更新单元格
  103. originalSheet[cellRef] = {
  104. t: 's', // 设置类型为字符串
  105. v: value.toString(), // 值
  106. w: value.toString(), // 格式化显示值
  107. h: value.toString(), // HTML显示值
  108. };
  109. }
  110. });
  111. });
  112. // 创建downloads目录(如果不存在)
  113. const downloadsDir = path.join(__dirname, 'downloads');
  114. if (!fs.existsSync(downloadsDir)) {
  115. fs.mkdirSync(downloadsDir);
  116. }
  117. // 保存文件
  118. const fileName = `edited_${Date.now()}.xlsx`;
  119. const filePath = path.join(downloadsDir, fileName);
  120. XLSX.writeFile(originalWorkbook, filePath, {
  121. bookType: 'xlsx',
  122. bookSST: false,
  123. type: 'file',
  124. cellStyles: true,
  125. cellNF: true,
  126. cellFormula: true
  127. });
  128. res.json({ downloadUrl: `/download/${fileName}` });
  129. } catch (error) {
  130. console.error('Save error:', error);
  131. res.status(500).json({ error: error.message });
  132. }
  133. });
  134. // 下载文件
  135. app.get('/download/:filename', (req, res) => {
  136. const filePath = path.join(__dirname, 'downloads', req.params.filename);
  137. res.download(filePath, req.params.filename, (err) => {
  138. if (!err) {
  139. // 下载完成后删除文件
  140. fs.unlinkSync(filePath);
  141. }
  142. });
  143. });
  144. // 错误处理中间件
  145. app.use((err, req, res, next) => {
  146. console.error(err.stack);
  147. res.status(500).json({ error: err.message || '服务器内部错误' });
  148. });
  149. // 清理函数:当服务器关闭时删除临时文件
  150. function cleanup() {
  151. if (originalWorkbook && originalWorkbook.filePath) {
  152. try {
  153. fs.unlinkSync(originalWorkbook.filePath);
  154. } catch (err) {
  155. console.error('Error cleaning up:', err);
  156. }
  157. }
  158. }
  159. // 监听进程退出事件
  160. process.on('SIGINT', () => {
  161. cleanup();
  162. process.exit();
  163. });
  164. process.on('SIGTERM', () => {
  165. cleanup();
  166. process.exit();
  167. });
  168. const port = process.env.PORT || 3100;
  169. app.listen(port, () => {
  170. console.log(`Server is running on port ${port}`);
  171. });