shaogen1995 недель назад: 2
Родитель
Сommit
20a2b47f65

+ 245 - 3
src/controller/auditController.ts

@@ -1,8 +1,10 @@
 import dayjs from 'dayjs';
-import { Audit } from '../model/index.js';
+import { Audit, AuditFile } from '../model/index.js';
 import { generateCaptcha, ipLocResFu } from '../util/index.js';
 import resSend from '../util/resSend.js';
-import { isEnv } from '../config/config.default.js';
+import { isEnv, upStaticFileUrl } from '../config/config.default.js';
+import path from 'path';
+import { deleteFileSafely } from '../util/clearAll.js';
 
 //  需要做定时器处理,防止短时间多次发送
 let loginFlag: any = {};
@@ -12,6 +14,7 @@ export const clearAuditCode = () => {
 };
 
 const audit = {
+  // 展示端 获取code
   getCode: async (req: any, res: any) => {
     req.apiDescription = '展示端-获取验证码';
     const clientIp = ipLocResFu(req);
@@ -28,7 +31,8 @@ const audit = {
     res.type('svg');
     res.send(captcha.data);
   },
-  saveAudit: async (req: any, res: any) => {
+  // 展示端 素材上传-提交
+  saveWebAudit: async (req: any, res: any) => {
     const clientIp = ipLocResFu(req);
     if (!loginFlag[clientIp]) loginFlag[clientIp] = { loginFlagCode: '' };
 
@@ -51,6 +55,244 @@ const audit = {
       return resSend(res, 0, '素材上传成功', findObj);
     } else return resSend(res, 400, '验证码错误');
   },
+  // 后台管理-获取素材提交审核的列表
+  getAuditList: async (req: any, res: any) => {
+    req.apiDescription = '素材审核-获取素材审核列表';
+    const { pageNum = 1, pageSize = 10, searchKey = '' } = req.body;
+
+    // 构建查询条件
+    const query: any = {};
+
+    if (searchKey) {
+      // 使用正则表达式实现模糊查询,'i'表示不区分大小写
+      query.title = { $regex: searchKey, $options: 'i' };
+    }
+
+    // 计算跳过的文档数量
+    const skip = (pageNum - 1) * pageSize;
+
+    // createTime降序
+    const sortCondition: any = { createTime: -1 };
+
+    // 并行执行:获取总条数和查询当前页数据
+    const [total, data] = await Promise.all([
+      // 获取满足条件的总记录数
+      Audit.countDocuments(query),
+      // 查询当前页数据
+      Audit.find(query).skip(skip).limit(parseInt(pageSize)).sort(sortCondition), // 按sort字段降序,数字越大越靠前;相同则按updateTime降序
+    ]);
+
+    // 计算总页数
+    const totalPages = Math.ceil(total / pageSize);
+    return resSend(res, 0, '获取素材审核列表成功', {
+      list: data,
+      pageNum: parseInt(pageNum),
+      pageSize: parseInt(pageSize),
+      total,
+      totalPages,
+    });
+  },
+  delAudit: async (req: any, res: any) => {
+    const { _id } = req.params; // 从URL参数中获取ID
+    // 1. 根据ID查找数据
+    const info = await Audit.findById(_id);
+    if (!info) return resSend(res, 404, '_id错误或数据已被删除');
+
+    const deletedInfo: any = await Audit.findByIdAndDelete(_id);
+    req.apiDescription = `素材审核-删除素材审核数据-${deletedInfo.title}`;
+    return resSend(res, 0, '删除素材审核数据成功');
+  },
+  // 后台管理-通过id获取素材审核详情
+  getAuditInfo: async (req: any, res: any) => {
+    const { _id } = req.params;
+
+    if (!_id) return resSend(res, 400, '_id不能为空');
+
+    // 1. 根据ID查询商品基本信息
+    const findInfo = await Audit.findById(_id);
+    if (!findInfo) return resSend(res, 404, '_id错误或数据已被删除');
+
+    // 2. 如果 fileIds 存在且是非空数组,则查询关联的文件
+    let fileDetails: any[] = [];
+    if (findInfo.fileIds && findInfo.fileIds.length > 0) {
+      // 使用 $in 操作符批量查询 Files 集合
+      fileDetails = await AuditFile.find({
+        _id: { $in: findInfo.fileIds },
+      });
+      // 3. 按照 fileIds 数组的顺序对 fileDetails 进行排序
+      if (fileDetails.length > 0) {
+        // 创建一个映射,将文件的 _id 映射到文件对象
+        const fileMap = new Map();
+        fileDetails.forEach((file) => {
+          // 统一转为字符串进行比较
+          fileMap.set(file._id.toString(), file);
+        });
+
+        // 按照 fileIds 的顺序重新构建 fileDetails 数组
+        const sortedFileDetails: any[] = [];
+        findInfo.fileIds.forEach((fileId) => {
+          const file = fileMap.get(fileId.toString());
+          if (file) {
+            sortedFileDetails.push(file);
+          }
+        });
+
+        // 用排序后的数组替换原始查询结果
+        fileDetails = sortedFileDetails;
+      }
+    }
+
+    // 3. 将文件详情合并到返回结果中
+    const info = {
+      ...findInfo.toObject(), // 将Mongoose文档转为普通JS对象
+      fileDetails: fileDetails,
+    };
+
+    req.apiDescription = `素材审核-获取素材审核详情-${info.title}`;
+    return resSend(res, 0, '获取素材审核详情成功', info);
+  },
+  // 后台管理-素材审核-审核
+  saveAudit: async (req: any, res: any) => {
+    const { _id, auditArr = [] } = req.body;
+
+    if (!_id) return resSend(res, 400, '_id不能为空');
+
+    if (typeof auditArr !== 'object' || auditArr.length < 1)
+      return resSend(res, 404, '文件_id不能为空');
+
+    // 需求1:更新Audit表的state字段为true
+    const auditRecord = await Audit.findById(_id);
+    if (!auditRecord) return resSend(res, 404, '_id错误或数据已被删除');
+
+    if (auditRecord.state) return resSend(res, 404, '这条数据已经被审核了');
+
+    // 2. 并行执行两个独立的更新操作(替代事务)
+    const [auditUpdateResult, fileUpdateResult] = await Promise.all([
+      // 更新Audit表的状态
+      Audit.updateOne(
+        { _id: _id },
+        {
+          $set: {
+            state: true,
+            updateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+          },
+        }
+      ),
+      // 使用bulkWrite批量更新附件表(核心优化)
+      AuditFile.bulkWrite(
+        auditArr.map((item: any) => ({
+          updateOne: {
+            filter: { _id: item._id },
+            update: {
+              $set: {
+                state: true,
+                remark: item.remark,
+                title: auditRecord.title,
+                phone: auditRecord.phone,
+                operator: req.user.userName,
+                updateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+                auditId: _id,
+              },
+            },
+            // 可选:如果附件不存在则创建
+            // upsert: true
+          },
+        })),
+        { ordered: false } // 无序执行,即使某个操作失败也继续执行其他操作
+      ),
+    ]);
+
+    // 3. 检查附件更新结果
+    let succeedCount = 0;
+    let failedCount = 0;
+
+    // bulkWrite结果分析
+    if (fileUpdateResult.modifiedCount !== undefined) {
+      succeedCount = fileUpdateResult.modifiedCount;
+      // 如果有upsert操作,还需要加上upsertedCount
+      failedCount = auditArr.length - succeedCount;
+    }
+
+    req.apiDescription = `素材审核-审核-${auditRecord.title}`;
+    return resSend(res, 0, '审核成功', {
+      succeed: succeedCount,
+      failed: failedCount,
+      total: auditArr.length,
+      auditUpdated: auditUpdateResult.modifiedCount,
+    });
+  },
+  // --------------------------------------
+  // 后台管理-获取素材库列表(过滤只返回state 为 true的数据)
+  getLibraryList: async (req: any, res: any) => {
+    req.apiDescription = '素材审核-获取素材库列表';
+    const { pageNum = 1, pageSize = 10, searchKey = '', type = '' } = req.body;
+
+    // 构建查询条件
+    const query: any = {
+      state: true,
+    };
+
+    if (type) query.type = type;
+
+    if (searchKey) {
+      // 使用正则表达式实现模糊查询,'i'表示不区分大小写
+      query.title = { $regex: searchKey, $options: 'i' };
+    }
+
+    // 计算跳过的文档数量
+    const skip = (pageNum - 1) * pageSize;
+
+    // 修改排序逻辑:先按sort字段降序,再按updateTime字段降序
+    const sortCondition: any = { sort: -1, updateTime: -1 };
+
+    // 并行执行:获取总条数和查询当前页数据
+    const [total, data] = await Promise.all([
+      // 获取满足条件的总记录数
+      AuditFile.countDocuments(query),
+      // 查询当前页数据
+      AuditFile.find(query).skip(skip).limit(parseInt(pageSize)).sort(sortCondition), // 按sort字段降序,数字越大越靠前;相同则按updateTime降序
+    ]);
+
+    // 计算总页数
+    const totalPages = Math.ceil(total / pageSize);
+    return resSend(res, 0, '获取素材库列表成功', {
+      list: data,
+      pageNum: parseInt(pageNum),
+      pageSize: parseInt(pageSize),
+      total,
+      totalPages,
+    });
+  },
+
+  // 素材库-删除
+  delLibrary: async (req: any, res: any) => {
+    const { _id } = req.params; // 从URL参数中获取ID
+    // 1. 根据ID查找数据
+    const info = await AuditFile.findById(_id);
+    if (!info) return resSend(res, 404, '_id错误或数据已被删除');
+
+    const deletedInfo: any = await AuditFile.findByIdAndDelete(_id);
+
+    // 同时删除静态文件
+    if (deletedInfo.originalUrl) {
+      const fullOriginalPath = path.join(
+        isEnv ? process.cwd() : upStaticFileUrl,
+        '',
+        deletedInfo.originalUrl
+      );
+      deleteFileSafely(fullOriginalPath);
+    }
+    if (deletedInfo.compressedUrl) {
+      const fullOriginalPath = path.join(
+        isEnv ? process.cwd() : upStaticFileUrl,
+        '',
+        deletedInfo.compressedUrl
+      );
+      deleteFileSafely(fullOriginalPath);
+    }
+    req.apiDescription = `素材审核-删除素材库数据-${deletedInfo.title}`;
+    return resSend(res, 0, '删除素材库数据成功');
+  },
 };
 
 export default audit;

+ 31 - 2
src/middleware/fileUpload.ts

@@ -3,11 +3,40 @@ import path from 'path';
 import fs from 'fs';
 import { isEnv, upStaticFileUrl } from '../config/config.default.js';
 
+// 智能编码检测函数
+function detectAndFixEncoding(str: string) {
+  // 如果已经是正常的中文,直接返回
+  if (/[\u4e00-\u9fa5]/.test(str)) {
+    return str; // 包含中文字符,说明已经是正确的UTF-8
+  }
+
+  // 如果不包含中文字符,尝试多种编码转换
+  const encodings: any = ['latin1', 'utf8', 'gbk', 'gb2312'];
+
+  for (const encoding of encodings) {
+    try {
+      // 尝试从指定编码转换为UTF-8
+      const decoded = Buffer.from(str, 'binary').toString(encoding);
+
+      // 检查转换后是否包含中文字符
+      if (/[\u4e00-\u9fa5]/.test(decoded)) {
+        return decoded; // 转换成功
+      }
+    } catch (error) {
+      // 编码转换失败,尝试下一个
+      continue;
+    }
+  }
+
+  // 如果都不成功,返回原始字符串
+  return str;
+}
+
 // 配置 Multer 存储策略
 const storage = multer.diskStorage({
   destination: (req: any, file, cb) => {
     // 在文件过滤阶段就修复编码问题
-    file.originalname = Buffer.from(file.originalname, 'latin1').toString('utf8');
+    file.originalname = detectAndFixEncoding(file.originalname);
 
     const upPath = req.query.upPath || 'default';
 
@@ -42,7 +71,7 @@ const fileFilter = (req: any, file: Express.Multer.File, cb: multer.FileFilterCa
 export const uploadZhong = multer({
   storage: storage,
   limits: {
-    fileSize: 500 * 1024 * 1024, // 500MB限制
+    fileSize: 1024 * 1024 * 1024 * 2, // 2GB限制
   },
   fileFilter: fileFilter,
 });

+ 33 - 4
src/model/auditFileModel.ts

@@ -19,12 +19,10 @@ const auditFileSchema = new mongoose.Schema({
     type: Number,
     required: true,
   },
-  isSelect: {
-    type: Boolean,
-    required: false,
-  },
+
   compressedSize: {
     type: Number,
+    default: 0,
   },
   originalUrl: {
     type: String,
@@ -35,6 +33,37 @@ const auditFileSchema = new mongoose.Schema({
     default: '',
   },
 
+  // 是否要显示到素材库
+  state: {
+    type: Boolean,
+    default: false,
+  },
+  // 备注
+  remark: {
+    type: String,
+    default: '',
+  },
+  // 标题
+  title: {
+    type: String,
+    default: '',
+  },
+  // 联系方式
+  phone: {
+    type: String,
+    default: '',
+  },
+  // 素材审核表绑定的id
+  auditId: {
+    type: String,
+    default: '',
+  },
+  // 审核人
+  operator: {
+    type: String,
+    default: '',
+  },
+
   updateTime: {
     type: String,
     default: dayjs().format('YYYY-MM-DD HH:mm:ss'),

+ 5 - 1
src/model/auditModel.ts

@@ -22,7 +22,11 @@ const auditSchema = new mongoose.Schema({
     type: [String],
     default: [],
   },
-
+  // 状态
+  state: {
+    type: Boolean,
+    default: false,
+  },
   createTime: {
     type: String,
     default: dayjs().format('YYYY-MM-DD HH:mm:ss'),

+ 25 - 0
src/router/audit.ts

@@ -0,0 +1,25 @@
+import express from 'express';
+import audit from '../controller/auditController.js';
+// 检测token的中间件
+import { verifyToken } from '../middleware/jwt.js';
+// 记录日志的中间件
+import requestLogger from '../middleware/requestLogger.js';
+
+const auditRouter = express.Router();
+
+auditRouter
+  // 素材审核-列表
+  .post('/getAuditList', verifyToken, audit.getAuditList)
+  // 素材审核-删除
+  .get('/delAudit/:_id', verifyToken, requestLogger, audit.delAudit)
+  // 素材审核-详情
+  .get('/getAuditInfo/:_id', verifyToken, audit.getAuditInfo)
+  // 素材审核-审核
+  .post('/saveAudit', verifyToken, requestLogger, audit.saveAudit)
+  // -------------------------------
+  // 素材库-列表
+  .post('/getLibraryList', verifyToken, audit.getLibraryList)
+  // 素材库-删除
+  .get('/delLibrary/:_id', verifyToken, requestLogger, audit.delLibrary);
+
+export default auditRouter;

+ 2 - 0
src/router/index.ts

@@ -3,11 +3,13 @@ import userRouter from './user.js';
 import issueRouter from './issue.js';
 import fileRouter from './file.js';
 import showRouter from './show.js';
+import auditRouter from './audit.js';
 
 const router = express.Router();
 
 router.use('/user', userRouter);
 router.use('/issue', issueRouter);
+router.use('/audit', auditRouter);
 router.use('/file', fileRouter);
 
 // 展示端

+ 1 - 1
src/router/show.ts

@@ -33,7 +33,7 @@ showRouter
   //获取验证码
   .get('/getCode', audit.getCode)
   // 素材上传-保存
-  .post('/saveAudit', auditUserVali, audit.saveAudit)
+  .post('/saveAudit', auditUserVali, audit.saveWebAudit)
   // 上传文件
   .post('/upload', uploadZhong.single('file'), (req, res) => {
     return file.upload(req, res, AuditFile);

+ 45 - 36
src/util/clearAll.ts

@@ -2,7 +2,7 @@ import cron from 'node-cron';
 import fs from 'fs/promises'; // 使用 Promise 版本的 fs API
 import path from 'path';
 
-import { Files, Goods, Share } from '../model/index.js';
+import { Audit, AuditFile, Files, Goods, Share } from '../model/index.js';
 import { clearLoginCode } from '../controller/userController.js';
 import { isEnv, upStaticFileUrl, writeLogUrl } from '../config/config.default.js';
 import { AddTxtFileFu } from './index.js';
@@ -11,20 +11,34 @@ import { clearAuditCode } from '../controller/auditController.js';
 /**
  * 增强版清理函数:删除孤立Files记录及对应的静态文件
  */
-export const clearFileFu = async (Model: any, FileModel: any) => {
+export const clearFileFu = async (Models: any[], FileModel: 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 : [];
+    // 1. 并行获取所有模型中正在使用的fileId
+    const usedFileIdsPromises = Models.map((Model) =>
+      Model.aggregate([
+        { $unwind: '$fileIds' },
+        { $group: { _id: null, ids: { $addToSet: '$fileIds' } } },
+      ])
+    );
+
+    const usedFileIdsResults = await Promise.all(usedFileIdsPromises);
+
+    // 2. 合并所有模型的fileIds并去重[6,7](@ref)
+    let allUsedIds: any[] = [];
+    usedFileIdsResults.forEach((result) => {
+      if (result.length > 0 && result[0].ids) {
+        allUsedIds = [...allUsedIds, ...result[0].ids];
+      }
+    });
+
+    // 使用Set去重[6,7](@ref)
+    const uniqueUsedIds = [...new Set(allUsedIds)];
 
-    // 2. 查找需要删除的孤立文件记录
+    // 3. 查找需要删除的孤立文件记录
     const filesToDelete = await FileModel.find({
-      _id: { $nin: usedIdsArray },
+      _id: { $nin: uniqueUsedIds },
     });
 
     if (filesToDelete.length === 0) {
@@ -34,14 +48,12 @@ export const clearFileFu = async (Model: any, FileModel: any) => {
 
     console.log(`找到 ${filesToDelete.length} 条待删除的文件记录,开始清理静态文件...`);
 
-    // 3. 并行删除静态文件,并处理结果
+    // 4. 并行删除静态文件,并处理结果(此部分代码保持不变)
     const fileDeletionResults = await Promise.allSettled(
       filesToDelete.map(async (fileDoc: any) => {
         const deletionPromises = [];
 
-        // 处理 originalUrl
         if (fileDoc.originalUrl) {
-          // 关键:将URL路径转换为服务器上的绝对路径
           const fullOriginalPath = path.join(
             isEnv ? process.cwd() : upStaticFileUrl,
             '',
@@ -50,7 +62,6 @@ export const clearFileFu = async (Model: any, FileModel: any) => {
           deletionPromises.push(deleteFileSafely(fullOriginalPath));
         }
 
-        // 处理 compressedUrl
         if (fileDoc.compressedUrl) {
           const fullCompressedPath = path.join(
             isEnv ? process.cwd() : upStaticFileUrl,
@@ -60,13 +71,12 @@ export const clearFileFu = async (Model: any, FileModel: any) => {
           deletionPromises.push(deleteFileSafely(fullCompressedPath));
         }
 
-        // 等待当前文件的所有路径删除完成
         await Promise.allSettled(deletionPromises);
-        return fileDoc._id; // 返回成功处理了文件的文档ID
+        return fileDoc._id;
       })
     );
 
-    // 4. 分析文件删除结果
+    // 5. 分析文件删除结果(此部分代码保持不变)
     const successfulFileDeletions: any[] = [];
     const failedFileDeletions: any[] = [];
 
@@ -79,38 +89,37 @@ export const clearFileFu = async (Model: any, FileModel: any) => {
       }
     });
 
-    // 5. 删除数据库记录(仅删除静态文件已清理或本来就不存在的记录)
-    // 注意:即使静态文件删除失败,也可能选择不删除数据库记录,以便手动处理
-    const recordsToDeleteFromDB = successfulFileDeletions; // 这里可以根据策略调整
-    const deleteResult = await Files.deleteMany({
-      _id: { $in: recordsToDeleteFromDB },
+    // 6. 删除数据库记录
+    const deleteResult = await FileModel.deleteMany({
+      _id: { $in: successfulFileDeletions },
     });
 
     console.log(`清理完成!`);
-    if (!isEnv)
+    if (!isEnv) {
       AddTxtFileFu(
         writeLogUrl,
         '/config/clearFile.log',
         `- 成功删除静态文件并移除数据库记录:${deleteResult.deletedCount} 条`
       );
-    if (!isEnv)
       AddTxtFileFu(
         writeLogUrl,
         '/config/clearFile.log',
         `- 静态文件删除失败(数据库记录保留): ${failedFileDeletions.length} 条`
       );
+    }
 
-    if (failedFileDeletions.length > 0) {
-      if (!isEnv)
-        AddTxtFileFu(
-          writeLogUrl,
-          '/config/clearFile.log',
-          `以下记录的静态文件可能需要手动处理:${failedFileDeletions}`
-        );
+    if (failedFileDeletions.length > 0 && !isEnv) {
+      AddTxtFileFu(
+        writeLogUrl,
+        '/config/clearFile.log',
+        `以下记录的静态文件可能需要手动处理:${failedFileDeletions}`
+      );
     }
   } catch (error) {
-    if (!isEnv)
-      AddTxtFileFu(writeLogUrl, '/config/clearFile.log', `清理过程中发生未知错误${error}`);
+    console.error('清理过程中发生未知错误:', error);
+    if (!isEnv) {
+      AddTxtFileFu(writeLogUrl, '/config/clearFile.log', `清理过程中发生未知错误:${error}`);
+    }
   }
 };
 
@@ -118,7 +127,7 @@ export const clearFileFu = async (Model: any, FileModel: any) => {
  * 安全删除文件的辅助函数
  * @param {string} filePath 文件的绝对路径
  */
-async function deleteFileSafely(filePath: string) {
+export async function deleteFileSafely(filePath: string) {
   try {
     await fs.access(filePath); // 检查文件是否存在
     await fs.unlink(filePath); // 删除文件
@@ -141,8 +150,8 @@ export const clearAllFu = () => {
     () => {
       console.log('执行定时任务...');
       // 清理 多个集合中 有fileIds 没有使用的file文件
-      clearFileFu(Goods, Files);
-      clearFileFu(Share, Files);
+      clearFileFu([Goods, Share], Files);
+      clearFileFu([Audit], AuditFile);
       // 清理用户登录的验证码对象
       clearLoginCode();
       clearAuditCode();