Просмотр исходного кода

每晚12点自动清理file文件并且写入日志

shaogen1995 1 месяц назад
Родитель
Сommit
ffee17f56e

+ 10 - 0
package-lock.json

@@ -19,6 +19,7 @@
         "mongoose": "^9.0.2",
         "morgan": "^1.10.1",
         "multer": "^2.0.2",
+        "node-cron": "^4.2.1",
         "sharp": "^0.34.5",
         "svg-captcha": "^1.4.0",
         "util": "^0.12.5"
@@ -2745,6 +2746,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/node-cron": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmmirror.com/node-cron/-/node-cron-4.2.1.tgz",
+      "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
     "node_modules/npm-run-path": {
       "version": "2.0.2",
       "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-2.0.2.tgz",

+ 1 - 0
package.json

@@ -30,6 +30,7 @@
     "mongoose": "^9.0.2",
     "morgan": "^1.10.1",
     "multer": "^2.0.2",
+    "node-cron": "^4.2.1",
     "sharp": "^0.34.5",
     "svg-captcha": "^1.4.0",
     "util": "^0.12.5"

+ 5 - 0
src/app.ts

@@ -9,6 +9,8 @@ import { getLocalIP } from './util/index.js';
 import { errorHandler } from './middleware/index.js';
 import proofZhong from './middleware/proof.js';
 
+import { clearAllFu } from './util/clearAll.js';
+
 const app = express();
 
 // 解析客户端请求
@@ -44,6 +46,9 @@ app.use('/api/v1', router);
 // 最后,注册错误处理中间件
 app.use(errorHandler);
 
+// 每晚12点定时清理
+clearAllFu();
+
 app.listen(8500, () => {
   console.log('Run http://localhost:8500');
   console.log(`Run http://${getLocalIP()}:8500`);

+ 4 - 1
src/config/config.default.ts

@@ -22,7 +22,10 @@ const buildUrl =
   'mongodb://root:4Dkk2021testproject%25@127.0.0.1:27017/anHui_guoJiaTongBuFuShe?authSource=admin';
 
 // 上传静态文件的目录
-export const upStaticFileUrl = '/data/data/museum_anhui_hefei_guojia_tongbufusheshiyanshi_data';
+export const upStaticFileUrl = '/data/data/museum_anhui_hefei_guojia_tongbufusheshiyanshi_data/';
+
+// 写入日志路径
+export const writeLogUrl = '/data/data/node_porject/museum_anhui_hefei_guojia_tongbufusheshiyanshi';
 
 // 服务器链接地址
 export const mongodbUrl = isEnv ? envUrl : buildUrl;

+ 2 - 2
src/controller/userController.ts

@@ -9,9 +9,9 @@ import { generateCaptcha, ipLocResFu } from '../util/index.js';
 // 登录模块 需要做定时器处理,防止短时间多次发送
 let loginFlag: any = {};
 
-setTimeout(() => {
+export const clearLoginCode = () => {
   loginFlag = {};
-}, 60 * 1000 * 60 * 2);
+};
 
 const loginTimeFu = (key: string) => {
   if (loginFlag[key].loginFlagTime === 0) loginFlag[key].loginFlagTime = Date.now();

+ 1 - 1
src/middleware/fileUpload.ts

@@ -16,7 +16,7 @@ const storage = multer.diskStorage({
 
     // 针对 本地和 生产 生成不同路径的静态文件
     if (isEnv) baseDir = 'uploads';
-    else baseDir = upStaticFileUrl + '/uploads';
+    else baseDir = upStaticFileUrl + 'uploads';
 
     const targetDir = path.join(baseDir, upPath || 'default');
 

+ 3 - 2
src/middleware/index.ts

@@ -1,4 +1,4 @@
-import { isEnv } from '../config/config.default.js';
+import { isEnv, writeLogUrl } from '../config/config.default.js';
 import { AddTxtFileFu } from '../util/index.js';
 import resSend from '../util/resSend.js';
 
@@ -21,7 +21,8 @@ export const isAdminZhong = (req: any, res: any, next: any) => {
 export const errorHandler = (err: any, req: any, res: any, next: any) => {
   // 记录错误日志,便于后端排查
   // console.error('错误处理中间件捕获到异常:', err);
-  if (!isEnv) AddTxtFileFu(import.meta.dirname, '../config/access.log', err);
+
+  if (!isEnv) AddTxtFileFu(writeLogUrl, 'config/access.log', err);
 
   // 设置默认错误状态码和消息
   const statusCode = err.statusCode || 500;

+ 1 - 1
src/model/goodsModel.ts

@@ -39,7 +39,7 @@ const GoodsSchema = new mongoose.Schema({
   },
   // 多张图片id集合
   fileIds: {
-    type: [Number],
+    type: [String],
     default: [],
   },
   // 模型链接地址

+ 150 - 0
src/util/clearAll.ts

@@ -0,0 +1,150 @@
+import cron from 'node-cron';
+import fs from 'fs/promises'; // 使用 Promise 版本的 fs API
+import path from 'path';
+
+import { Files, Goods } from '../model/index.js';
+import { clearLoginCode } from '../controller/userController.js';
+import { isEnv, upStaticFileUrl, writeLogUrl } from '../config/config.default.js';
+import { AddTxtFileFu } from './index.js';
+
+/**
+ * 增强版清理函数:删除孤立Files记录及对应的静态文件
+ */
+export const clearFileFu = async (Model: any) => {
+  try {
+    console.log('开始清理孤立的files数据和静态文件...');
+
+    // 1. 聚合管道:找出所有videos文档中正在使用的fileId
+    const usedFileIds = await Model.aggregate([
+      { $unwind: '$fileIds' },
+      { $group: { _id: null, ids: { $addToSet: '$fileIds' } } },
+    ]);
+    const usedIdsArray = usedFileIds.length > 0 ? usedFileIds[0].ids : [];
+
+    // 2. 查找需要删除的孤立文件记录
+    const filesToDelete = await Files.find({
+      _id: { $nin: usedIdsArray },
+    });
+
+    if (filesToDelete.length === 0) {
+      console.log('没有需要清理的孤立文件记录。');
+      return;
+    }
+
+    console.log(`找到 ${filesToDelete.length} 条待删除的文件记录,开始清理静态文件...`);
+
+    // 3. 并行删除静态文件,并处理结果
+    const fileDeletionResults = await Promise.allSettled(
+      filesToDelete.map(async (fileDoc) => {
+        const deletionPromises = [];
+
+        // 处理 originalUrl
+        if (fileDoc.originalUrl) {
+          // 关键:将URL路径转换为服务器上的绝对路径
+          const fullOriginalPath = path.join(
+            isEnv ? process.cwd() : upStaticFileUrl,
+            '',
+            fileDoc.originalUrl
+          );
+          deletionPromises.push(deleteFileSafely(fullOriginalPath));
+        }
+
+        // 处理 compressedUrl
+        if (fileDoc.compressedUrl) {
+          const fullCompressedPath = path.join(
+            isEnv ? process.cwd() : upStaticFileUrl,
+            '',
+            fileDoc.compressedUrl
+          );
+          deletionPromises.push(deleteFileSafely(fullCompressedPath));
+        }
+
+        // 等待当前文件的所有路径删除完成
+        await Promise.allSettled(deletionPromises);
+        return fileDoc._id; // 返回成功处理了文件的文档ID
+      })
+    );
+
+    // 4. 分析文件删除结果
+    const successfulFileDeletions: any[] = [];
+    const failedFileDeletions: any[] = [];
+
+    fileDeletionResults.forEach((result, index) => {
+      if (result.status === 'fulfilled') {
+        successfulFileDeletions.push(result.value);
+      } else {
+        console.error(`删除第${index}条记录的静态文件时失败:`, result.reason);
+        failedFileDeletions.push(filesToDelete[index]._id);
+      }
+    });
+
+    // 5. 删除数据库记录(仅删除静态文件已清理或本来就不存在的记录)
+    // 注意:即使静态文件删除失败,也可能选择不删除数据库记录,以便手动处理
+    const recordsToDeleteFromDB = successfulFileDeletions; // 这里可以根据策略调整
+    const deleteResult = await Files.deleteMany({
+      _id: { $in: recordsToDeleteFromDB },
+    });
+
+    console.log(`清理完成!`);
+    if (!isEnv)
+      AddTxtFileFu(
+        writeLogUrl,
+        'config/access.log',
+        `- 成功删除静态文件并移除数据库记录:${deleteResult.deletedCount} 条`
+      );
+    if (!isEnv)
+      AddTxtFileFu(
+        writeLogUrl,
+        'config/access.log',
+        `- 静态文件删除失败(数据库记录保留): ${failedFileDeletions.length} 条`
+      );
+
+    if (failedFileDeletions.length > 0) {
+      if (!isEnv)
+        AddTxtFileFu(
+          writeLogUrl,
+          'config/access.log',
+          `以下记录的静态文件可能需要手动处理:${failedFileDeletions}`
+        );
+    }
+  } catch (error) {
+    if (!isEnv) AddTxtFileFu(writeLogUrl, 'config/access.log', `清理过程中发生未知错误${error}`);
+  }
+};
+
+/**
+ * 安全删除文件的辅助函数
+ * @param {string} filePath 文件的绝对路径
+ */
+async function deleteFileSafely(filePath: string) {
+  try {
+    await fs.access(filePath); // 检查文件是否存在
+    await fs.unlink(filePath); // 删除文件
+    console.log(`成功删除静态文件: ${path.basename(filePath)}`);
+  } catch (err: any) {
+    if (err.code === 'ENOENT') {
+      // 文件不存在,可记录日志或忽略
+      console.warn(`文件不存在,无需删除: ${filePath}`);
+    } else {
+      // 其他错误(如权限不足),重新抛出供上层处理
+      throw new Error(`删除文件 ${filePath} 时出错: ${err.message}`);
+    }
+  }
+}
+
+export const clearAllFu = () => {
+  // 每晚12点清理文件
+  cron.schedule(
+    '0 0 * * *',
+    () => {
+      console.log('执行定时任务...');
+      // 清理 展品展示中 没有使用的file文件
+      clearFileFu(Goods);
+      // 清理用户登录的验证码对象
+      clearLoginCode();
+    },
+    {
+      timezone: 'Asia/Shanghai', // 根据你的服务器时区设置
+    }
+  );
+};

+ 2 - 1
src/util/index.ts

@@ -23,6 +23,7 @@ export const getLocalIP = () => {
 };
 
 import svgCaptcha from 'svg-captcha';
+import dayjs from 'dayjs';
 
 // 2:------------ 生成验证码
 export const generateCaptcha = () => {
@@ -54,7 +55,7 @@ export const AddTxtFileFu = (nowPath: any, fileName: string, content: string) =>
   const filePath = path.join(nowPath, fileName);
 
   // 在要追加的内容末尾加上换行符 `\n`
-  const dataToAppend = content + '\n';
+  const dataToAppend = dayjs().format('YYYY-MM-DD HH:mm:ss') + content + '\n';
   fs.appendFile(filePath, dataToAppend, 'utf8', (err) => {
     if (err) {
       return;