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

refactor(inspection): 提取巡检附件操作为统一类

- 新增 InspectionAttachment 类,统一管理安全与质量巡检附件的
  上传、删除及权限校验逻辑
- 附件上传文件名从时间戳改为 UUID,避免并发写入冲突
- 为附件服务方法添加事务支持,确保流程上下文校验与数据写入一致性
- 上传异常时自动清理已写入 OSS 但未入库的孤立文件
- 前端附件列表新增"阶段"列,集中权限判断并增加 XSS 转义防护
- 删除附件时传入 flowContext 参数以校验流程状态是否已变更
caipin 1 неделя назад
Родитель
Сommit
62b783288e

+ 26 - 30
app/controller/quality_controller.js

@@ -19,6 +19,7 @@ const sendToWormhole = require('stream-wormhole');
 const fs = require('fs');
 const path = require('path');
 const PermissionCheck = require('../const/account_permission').PermissionCheck;
+const InspectionAttachment = require('../lib/inspection_attachment');
 
 module.exports = app => {
     class QualityController extends app.BaseController {
@@ -609,7 +610,7 @@ module.exports = app => {
                 filter.count[filter.status.checkStop] = await ctx.service.qualityInspection.getCountByStatus(ctx.tender.id, filter.status.checkStop);// await ctx.service.change.pendingDatas(tender.id, ctx.session.sessionUser.accountId);
                 const inspectionList = await ctx.service.qualityInspection.getListByStatus(ctx.tender.id, status, 1, sorts, orders);
                 const total = await ctx.service.qualityInspection.getCountByStatus(ctx.tender.id, status);
-                const allAttList = inspectionList.length > 0 ? await ctx.service.qualityInspectionAtt.getAllAtt(ctx.tender.id, ctx.helper._.map(inspectionList, 'id')) : [];
+                const allAttList = inspectionList.length > 0 ? await new InspectionAttachment(ctx, 'quality').getFiles(ctx.helper._.map(inspectionList, 'id')) : [];
                 for (const c of inspectionList) {
                     c.attList = ctx.helper._.filter(allAttList, { qiid: c.id });
                     c.curAuditors = await ctx.service.qualityInspectionAudit.getAuditorsByStatus(c.id, c.status, c.times);
@@ -735,7 +736,7 @@ module.exports = app => {
                 const tender = await ctx.service.tender.getDataById(ctx.tender.id);
                 await this._getInspectionAuditViewData(ctx);
                 // 获取附件列表
-                const fileList = await ctx.service.qualityInspectionAtt.getAllAtt(ctx.tender.id, ctx.inspection.id);
+                const fileList = await new InspectionAttachment(ctx, 'quality').getFiles(ctx.inspection.id);
                 // 获取用户人验证手机号
                 const renderData = {
                     moment,
@@ -898,26 +899,23 @@ module.exports = app => {
          */
         async uploadInspectionFile(ctx) {
             let stream;
+            const files = [];
+            let saved = false;
             try {
-                if (ctx.inspection.flow_version === 1 && !ctx.inspection.canUpload) throw '当前不可上传附件';
+                const attachment = new InspectionAttachment(ctx, 'quality');
+                if (!attachment.canUpload(ctx.inspection)) throw '当前不可上传附件';
                 const parts = this.ctx.multipart({
                     autoFields: true,
                 });
-                const files = [];
-                const create_time = Date.parse(new Date()) / 1000;
-                let idx = 0;
-                const extra_upload = ctx.inspection.status === auditConst.inspection.status.checked;
                 while ((stream = await parts()) !== undefined) {
                     if (!stream.filename) {
-                        // 如果没有传入直接返回
-                        return;
+                        throw '请选择附件';
                     }
                     const fileInfo = path.parse(stream.filename);
-                    const filepath = `app/public/upload/${this.ctx.tender.id.toString()}/quality_inspection/fujian_${create_time + idx.toString() + fileInfo.ext}`;
+                    const filepath = `app/public/upload/${this.ctx.tender.id.toString()}/quality_inspection/fujian_${ctx.app.uuid.v4()}${fileInfo.ext}`;
                     // await ctx.helper.saveStreamFile(stream, path.resolve(this.app.baseDir, 'app', filepath));
                     await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
                     files.push({ filepath, name: stream.filename, ext: fileInfo.ext });
-                    ++idx;
                     stream && (await sendToWormhole(stream));
                 }
                 const in_time = new Date();
@@ -937,17 +935,27 @@ module.exports = app => {
                         filesize: ctx.helper.bytesToSize(idx === 'isString' ? parts.field.size : parts.field.size[idx]),
                         filepath: file.filepath,
                         upload_time: in_time,
-                        extra_upload,
                     };
                     return newFile;
                 });
                 // 执行文件信息写入数据库
-                await ctx.service.qualityInspectionAtt.saveFileMsgToDb(payload);
+                await attachment.saveFiles(payload, JSON.parse(parts.field.flowContext || 'null'));
+                saved = true;
                 // 将最新的当前标段的所有文件信息返回
-                const data = await ctx.service.qualityInspectionAtt.getAllAtt(ctx.tender.id, ctx.inspection.id);
+                const data = await attachment.getFiles(ctx.inspection.id);
                 ctx.body = { err: 0, msg: '', data };
             } catch (err) {
                 stream && (await sendToWormhole(stream));
+                // 流程已变更或保存失败时,仅清理本次未入库的上传文件。
+                if (!saved) {
+                    for (const file of files) {
+                        try {
+                            await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + file.filepath);
+                        } catch (cleanupError) {
+                            this.log(cleanupError);
+                        }
+                    }
+                }
                 this.log(err);
                 ctx.body = { err: 1, msg: err.toString(), data: null };
             }
@@ -959,22 +967,10 @@ module.exports = app => {
          */
         async deleteInspectionFile(ctx) {
             try {
-                const { id } = JSON.parse(ctx.request.body.data);
-                const fileInfo = await ctx.service.qualityInspectionAtt.getDataById(id);
-                if (ctx.inspection.flow_version === 1) {
-                    if (!fileInfo || fileInfo.qiid !== ctx.inspection.id || !ctx.inspection.canUpload || fileInfo.uid !== ctx.session.sessionUser.accountId) throw '您无权删除该附件';
-                    if (ctx.inspection.status === qualityInspectionConst.status.checked && !fileInfo.extra_upload && !PermissionCheck.delFile(ctx.session.sessionUser.permission)) throw '该附件不可删除';
-                }
-                if (fileInfo && Object.keys(fileInfo).length) {
-                    // 先删除文件
-                    // await fs.unlinkSync(path.resolve(this.app.baseDir, './app', fileInfo.filepath));
-                    await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + fileInfo.filepath);
-                    // 再删除数据库
-                    await ctx.service.qualityInspectionAtt.delete(id);
-                } else {
-                    throw '不存在该文件';
-                }
-                const data = await ctx.service.qualityInspectionAtt.getAllAtt(ctx.tender.id, ctx.inspection.id);
+                const { id, flowContext } = JSON.parse(ctx.request.body.data);
+                const attachment = new InspectionAttachment(ctx, 'quality');
+                await attachment.deleteFile(id, flowContext);
+                const data = await attachment.getFiles(ctx.inspection.id);
                 ctx.body = { err: 0, msg: '请求成功', data };
             } catch (err) {
                 this.log(err);

+ 26 - 30
app/controller/safe_controller.js

@@ -19,6 +19,7 @@ const sendToWormhole = require('stream-wormhole');
 const fs = require('fs');
 const path = require('path');
 const PermissionCheck = require('../const/account_permission').PermissionCheck;
+const InspectionAttachment = require('../lib/inspection_attachment');
 
 module.exports = app => {
     class SafeController extends app.BaseController {
@@ -669,7 +670,7 @@ module.exports = app => {
                 filter.count[filter.status.checkStop] = await ctx.service.safeInspection.getCountByStatus(ctx.tender.id, filter.status.checkStop);// await ctx.service.change.pendingDatas(tender.id, ctx.session.sessionUser.accountId);
                 const inspectionList = await ctx.service.safeInspection.getListByStatus(ctx.tender.id, status, 1, sorts, orders);
                 const total = await ctx.service.safeInspection.getCountByStatus(ctx.tender.id, status);
-                const allAttList = inspectionList.length > 0 ? await ctx.service.safeInspectionAtt.getAllAtt(ctx.tender.id, ctx.helper._.map(inspectionList, 'id')) : [];
+                const allAttList = inspectionList.length > 0 ? await new InspectionAttachment(ctx, 'safe').getFiles(ctx.helper._.map(inspectionList, 'id')) : [];
                 for (const c of inspectionList) {
                     c.attList = ctx.helper._.filter(allAttList, { qiid: c.id });
                     c.curAuditors = await ctx.service.safeInspectionAudit.getAuditorsByStatus(c.id, c.status, c.times);
@@ -804,7 +805,7 @@ module.exports = app => {
                 const tender = await ctx.service.tender.getDataById(ctx.tender.id);
                 await this._getInspectionAuditViewData(ctx);
                 // 获取附件列表
-                const fileList = await ctx.service.safeInspectionAtt.getAllAtt(ctx.tender.id, ctx.inspection.id);
+                const fileList = await new InspectionAttachment(ctx, 'safe').getFiles(ctx.inspection.id);
                 // 获取用户人验证手机号
                 const renderData = {
                     moment,
@@ -972,26 +973,23 @@ module.exports = app => {
          */
         async uploadInspectionFile(ctx) {
             let stream;
+            const files = [];
+            let saved = false;
             try {
-                if (ctx.inspection.flow_version === 1 && !ctx.inspection.canUpload) throw '当前不可上传附件';
+                const attachment = new InspectionAttachment(ctx, 'safe');
+                if (!attachment.canUpload(ctx.inspection)) throw '当前不可上传附件';
                 const parts = this.ctx.multipart({
                     autoFields: true,
                 });
-                const files = [];
-                const create_time = Date.parse(new Date()) / 1000;
-                let idx = 0;
-                const extra_upload = ctx.inspection.status === auditConst.inspection.status.checked;
                 while ((stream = await parts()) !== undefined) {
                     if (!stream.filename) {
-                        // 如果没有传入直接返回
-                        return;
+                        throw '请选择附件';
                     }
                     const fileInfo = path.parse(stream.filename);
-                    const filepath = `app/public/upload/${this.ctx.tender.id.toString()}/safe_inspection/fujian_${create_time + idx.toString() + fileInfo.ext}`;
+                    const filepath = `app/public/upload/${this.ctx.tender.id.toString()}/safe_inspection/fujian_${ctx.app.uuid.v4()}${fileInfo.ext}`;
                     // await ctx.helper.saveStreamFile(stream, path.resolve(this.app.baseDir, 'app', filepath));
                     await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
                     files.push({ filepath, name: stream.filename, ext: fileInfo.ext });
-                    ++idx;
                     stream && (await sendToWormhole(stream));
                 }
                 const in_time = new Date();
@@ -1011,17 +1009,27 @@ module.exports = app => {
                         filesize: ctx.helper.bytesToSize(idx === 'isString' ? parts.field.size : parts.field.size[idx]),
                         filepath: file.filepath,
                         upload_time: in_time,
-                        extra_upload,
                     };
                     return newFile;
                 });
                 // 执行文件信息写入数据库
-                await ctx.service.safeInspectionAtt.saveFileMsgToDb(payload);
+                await attachment.saveFiles(payload, JSON.parse(parts.field.flowContext || 'null'));
+                saved = true;
                 // 将最新的当前标段的所有文件信息返回
-                const data = await ctx.service.safeInspectionAtt.getAllAtt(ctx.tender.id, ctx.inspection.id);
+                const data = await attachment.getFiles(ctx.inspection.id);
                 ctx.body = { err: 0, msg: '', data };
             } catch (err) {
                 stream && (await sendToWormhole(stream));
+                // 流程已变更或保存失败时,仅清理本次未入库的上传文件。
+                if (!saved) {
+                    for (const file of files) {
+                        try {
+                            await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + file.filepath);
+                        } catch (cleanupError) {
+                            this.log(cleanupError);
+                        }
+                    }
+                }
                 this.log(err);
                 ctx.body = { err: 1, msg: err.toString(), data: null };
             }
@@ -1033,22 +1041,10 @@ module.exports = app => {
          */
         async deleteInspectionFile(ctx) {
             try {
-                const { id } = JSON.parse(ctx.request.body.data);
-                const fileInfo = await ctx.service.safeInspectionAtt.getDataById(id);
-                if (ctx.inspection.flow_version === 1) {
-                    if (!fileInfo || fileInfo.qiid !== ctx.inspection.id || !ctx.inspection.canUpload || fileInfo.uid !== ctx.session.sessionUser.accountId) throw '您无权删除该附件';
-                    if (ctx.inspection.status === safeInspectionConst.status.checked && !fileInfo.extra_upload && !PermissionCheck.delFile(ctx.session.sessionUser.permission)) throw '该附件不可删除';
-                }
-                if (fileInfo && Object.keys(fileInfo).length) {
-                    // 先删除文件
-                    // await fs.unlinkSync(path.resolve(this.app.baseDir, './app', fileInfo.filepath));
-                    await ctx.app.fujianOss.delete(ctx.app.config.fujianOssFolder + fileInfo.filepath);
-                    // 再删除数据库
-                    await ctx.service.safeInspectionAtt.delete(id);
-                } else {
-                    throw '不存在该文件';
-                }
-                const data = await ctx.service.safeInspectionAtt.getAllAtt(ctx.tender.id, ctx.inspection.id);
+                const { id, flowContext } = JSON.parse(ctx.request.body.data);
+                const attachment = new InspectionAttachment(ctx, 'safe');
+                await attachment.deleteFile(id, flowContext);
+                const data = await attachment.getFiles(ctx.inspection.id);
                 ctx.body = { err: 0, msg: '请求成功', data };
             } catch (err) {
                 this.log(err);

+ 2 - 1
app/controller/wap_controller.js

@@ -26,6 +26,7 @@ const uuid = require('node-uuid');
 const nlsToken = require('../lib/nls_token');
 const streamToArray = require('stream-to-array');
 const PermissionCheck = require('../const/account_permission').PermissionCheck;
+const InspectionAttachment = require('../lib/inspection_attachment');
 
 module.exports = app => {
 
@@ -1315,7 +1316,7 @@ module.exports = app => {
                 const inspection = ctx.inspection;
                 await ctx.service[type + 'Inspection'].loadAuditViewData(ctx.inspection);
                 // 获取附件列表
-                const fileList = await ctx.service[type + 'InspectionAtt'].getAllAtt(ctx.tender.id, ctx.inspection.id);
+                const fileList = await new InspectionAttachment(ctx, type).getFiles(ctx.inspection.id);
                 const renderData = {
                     moment,
                     tender,

+ 112 - 0
app/lib/inspection_attachment.js

@@ -0,0 +1,112 @@
+'use strict';
+
+const PermissionCheck = require('../const/account_permission').PermissionCheck;
+
+// 安全、质量巡检共用附件阶段及操作权限规则。
+class InspectionAttachment {
+    constructor(ctx, type) {
+        if (!['safe', 'quality'].includes(type)) throw '无效的巡检类型';
+        this.ctx = ctx;
+        this.inspectionService = ctx.service[type + 'Inspection'];
+        this.auditService = ctx.service[type + 'InspectionAudit'];
+        this.attachmentService = ctx.service[type + 'InspectionAtt'];
+    }
+
+    getFileStage(file) {
+        // 尚未回填的历史数据与数据库迁移后的规则保持一致。
+        return file.flow_stage === null || file.flow_stage === undefined || Number(file.flow_stage) === 0 ? 1 : Number(file.flow_stage);
+    }
+
+    getUploadStage(inspection) {
+        if (inspection.flow_version === 1) return [1, 2, 3].includes(inspection.flow_stage) ? inspection.flow_stage : 0;
+        // 旧流程只有上报、整改,完成后的补传归入整改阶段。
+        if ([3, 12].includes(inspection.status)) return 2;
+        return [1, 2, 5, 6].includes(inspection.status) ? 1 : 0;
+    }
+
+    canUpload(inspection, records) {
+        if (!inspection || inspection.status === 4 || !this.getUploadStage(inspection)) return false;
+        if (inspection.flow_version !== 1) return Boolean(inspection.filePermission);
+        // 页面和上传入口沿用中间件已计算的权限;事务内传入最新审批记录后重新校验。
+        if (!records && typeof inspection.canUpload === 'boolean') return inspection.canUpload;
+        const uid = this.ctx.session.sessionUser.accountId;
+        if ([1, 3, 5].includes(inspection.status)) return inspection.uid === uid;
+        const current = (records || inspection.auditors || []).filter(record => !record.is_old && record.times === inspection.times
+            && record.flow_stage === inspection.flow_stage && record.stage_round === inspection.stage_round);
+        if (inspection.flow_stage === 2) {
+            current.sort((a, b) => a.audit_order - b.audit_order || a.order - b.order || a.id - b.id);
+            const active = current.find(record => [1, 12].includes(record.status));
+            return inspection.status === 12 && Boolean(active && active.aid === uid && active.status === 12);
+        }
+        return [2, 6].includes(inspection.status) && current.some(record => record.aid === uid && record.status === 2);
+    }
+
+    canDeleteFile(file, inspection, records) {
+        if (!file || !inspection || file.tid !== inspection.tid || file.qiid !== inspection.id) return false;
+        if (file.uid !== this.ctx.session.sessionUser.accountId || !this.canUpload(inspection, records)) return false;
+        if (this.getFileStage(file) !== this.getUploadStage(inspection)) return false;
+        return inspection.status !== 3 || Boolean(file.extra_upload) || PermissionCheck.delFile(this.ctx.session.sessionUser.permission);
+    }
+
+    async getFiles(qiid) {
+        if (this.ctx.inspection) this.ctx.inspection.canUploadAttachments = this.canUpload(this.ctx.inspection);
+        const files = await this.attachmentService.getAllAtt(this.ctx.tender.id, qiid);
+        return files.map(file => Object.assign(file, {
+            flow_stage: this.getFileStage(file),
+            stageName: ['', '上报', '整改', '批复'][this.getFileStage(file)] || '未知阶段',
+            canDelete: this.canDeleteFile(file, this.ctx.inspection),
+        }));
+    }
+
+    async saveFiles(files, flowContext) {
+        const transaction = await this.attachmentService.beginTransaction();
+        try {
+            const { current } = await this.getCurrentContext(transaction, flowContext);
+            if (!files.length) throw '请选择附件';
+            const payload = files.map(file => Object.assign({}, file, {
+                tid: current.tid, qiid: current.id, uid: this.ctx.session.sessionUser.accountId,
+                flow_stage: this.getUploadStage(current), extra_upload: current.status === 3,
+            }));
+            const result = await this.attachmentService.saveFileMsgToDb(payload, transaction);
+            await transaction.commit();
+            return result;
+        } catch (err) {
+            await transaction.rollback();
+            throw err;
+        }
+    }
+
+    async deleteFile(id, flowContext) {
+        id = Number(id);
+        if (!Number.isSafeInteger(id) || id <= 0) throw '无效的附件';
+        const transaction = await this.attachmentService.beginTransaction();
+        try {
+            const { current, records } = await this.getCurrentContext(transaction, flowContext);
+            const file = await this.attachmentService.getMaterialFileById(id, transaction);
+            if (!this.canDeleteFile(file, current, records)) throw '只能删除本人在当前阶段上传的附件';
+            await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + file.filepath);
+            await this.attachmentService.delete(id, transaction);
+            await transaction.commit();
+        } catch (err) {
+            await transaction.rollback();
+            throw err;
+        }
+    }
+
+    async getCurrentContext(transaction, flowContext) {
+        const { ctx } = this;
+        const rows = await this.inspectionService.getAllDataForUpdate({ id: ctx.inspection.id, tid: ctx.tender.id }, transaction);
+        const current = rows[0];
+        if (!current) throw '巡检不存在';
+        const fields = ['flow_version', 'flow_stage', 'stage_round', 'times', 'status'];
+        if (!flowContext || fields.some(key => Number(flowContext[key]) !== Number(current[key]) || Number(ctx.inspection[key]) !== Number(current[key]))) {
+            throw '流程已更新,请刷新后操作附件';
+        }
+        current.filePermission = ctx.inspection.filePermission;
+        const records = await this.auditService.getAllDataByCondition({ where: { qiid: current.id, times: current.times, is_old: 0 } }, transaction);
+        if (!this.canUpload(current, records)) throw '当前不可操作附件';
+        return { current, records };
+    }
+}
+
+module.exports = InspectionAttachment;

+ 49 - 6
app/public/css/main.css

@@ -3750,23 +3750,23 @@ button.gy-stat:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
 
 .quality-inspection-attachments .modal-dialog,
 .safe-inspection-attachments .modal-dialog {
-    width: 900px;
+    width: max-content;
     max-width: calc(100% - 32px);
     margin-right: auto;
     margin-left: auto;
 }
 .quality-inspection-attachments table,
 .safe-inspection-attachments table {
-    width: 100%;
-    max-width: 100%;
-    table-layout: fixed;
+    width: auto;
+    min-width: 100%;
+    max-width: none;
+    table-layout: auto;
 }
 .quality-inspection-attachments th,
 .quality-inspection-attachments td,
 .safe-inspection-attachments th,
 .safe-inspection-attachments td {
-    white-space: normal;
-    overflow-wrap: break-word;
+    white-space: nowrap;
 }
 
 .quality-inspection-menu .nav-list a,
@@ -3821,3 +3821,46 @@ button.gy-stat:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
     top: 0;
     height: 100%;
 }
+
+.inspection-detail-attachments {
+    max-width: 100%;
+    overflow-x: auto;
+}
+.inspection-detail-attachments-table {
+    width: 100%;
+    min-width: 600px;
+    margin-bottom: 0;
+    table-layout: fixed;
+}
+.inspection-detail-attachments-table .attachment-col-number {
+    width: 5%;
+}
+.inspection-detail-attachments-table .attachment-col-name {
+    width: auto;
+}
+.inspection-detail-attachments-table .attachment-col-stage,
+.inspection-detail-attachments-table .attachment-col-user,
+.inspection-detail-attachments-table .attachment-col-actions {
+    width: 10%;
+}
+.inspection-detail-attachments-table .attachment-col-time {
+    width: 15%;
+}
+.inspection-detail-attachments-table th,
+.inspection-detail-attachments-table td {
+    overflow-wrap: break-word;
+}
+.inspection-detail-attachments-table thead th:nth-child(n+3),
+.inspection-detail-attachments-table .inspection-attachment-row td:nth-child(n+3) {
+    text-align: center;
+}
+.inspection-detail-attachments-table .inspection-attachment-name {
+    min-width: 300px;
+    text-align: left;
+}
+.inspection-detail-attachments-table .inspection-attachment-name a {
+    display: block;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}

+ 1 - 1
app/public/js/quality_inspection.js

@@ -321,7 +321,7 @@ $(document).ready(() => {
         $('#file-content').empty();
         let html = '';
         files.forEach((file, idx) => {
-            html += `<tr><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td class="text-center">${file.username}</td><td class="text-center">${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td class="text-center"><a href="/sp/${spid}/quality/tender/${file.tid}/inspection/${file.qiid}/information/file/${file.id}/download"><i class="fa fa-download"></i></a></td></tr>`
+            html += `<tr><td class="text-center">${file.stageName || '上报'}</td><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td class="text-center">${file.username}</td><td class="text-center">${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td class="text-center"><a href="/sp/${spid}/quality/tender/${file.tid}/inspection/${file.qiid}/information/file/${file.id}/download"><i class="fa fa-download"></i></a></td></tr>`
         })
         $('#file-content').append(html);
     }

+ 19 - 21
app/public/js/quality_inspection_information.js

@@ -616,6 +616,7 @@ $(document).ready(function () {
         if (validateFiles(valiData)) {
             if (files.length) {
                 const formData = new FormData();
+                formData.append('flowContext', JSON.stringify(getQualityInspectionAttachmentContext()));
                 files.forEach(file => {
                     formData.append('name', file.name);
                     formData.append('size', file.size);
@@ -631,30 +632,22 @@ $(document).ready(function () {
     })
     function handleFileList(files = []) {
         $('#file-content').empty();
-        const newFiles = files.map(file => {
-            let showDel = false;
-            if (file.uid === cur_uid) {
-                if (inspection.status === auditConst.status.checked) {
-                    showDel = Boolean(file.extra_upload ) || deleteFilePermission
-                } else {
-                    showDel = true
-                }
-            }
-            if (inspection.flow_version === 1 && !inspection.canUpload) showDel = false;
-            return {...file, showDel}
-        })
+        const canUploadAttachments = typeof inspection.canUploadAttachments === 'boolean'
+            ? inspection.canUploadAttachments
+            : Boolean(inspection.flow_version === 1 ? inspection.canUpload : inspection.filePermission);
+        const newFiles = files.map(file => ({...file, showDel: file.canDelete === true}));
         const is_wap = window.location.pathname.indexOf('/wap/') !== -1;
         if (is_wap) {
             let html = `<div style="display: flex;">
             <b>附件</b>`;
-                if (inspection.flow_version === 1 ? inspection.canUpload : inspection.filePermission) {
+                if (canUploadAttachments) {
                     html += `<span style="margin-left: auto;"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></span>`;
                 }
             html += `</div><div>`;
             newFiles.forEach((file, idx) => {
                 html += `<a href="${file.filepath}" target="_blank">
                     <div class="card my-1" style="cursor: pointer">
-                        <div class="card-body"><i class="fa fa-file"></i> ${file.filename}<small class="pull-right">${file.showDel ? `<span class="badge">
+                        <div class="card-body"><span class="badge badge-light mr-1">${file.stageName || '上报'}</span><i class="fa fa-file"></i> ${file.filename}<small class="pull-right">${file.showDel ? `<span class="badge">
                                     <span class="dropdown">
                                 <a href="javascript:void(0)" class="btn-sm text-danger px-1" title="移除" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-remove"></i></a>
                                 <div class="dropdown-menu">
@@ -674,13 +667,11 @@ $(document).ready(function () {
             html += '</div>';
             $('#file-content').append(html);
         } else {
-            let html = (inspection.flow_version === 1 ? inspection.canUpload : inspection.filePermission) ? `<tr><td colspan="5"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></td></tr>` : '';
+            let html = canUploadAttachments ? `<tr><td colspan="6"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></td></tr>` : '';
             newFiles.forEach((file, idx) => {
-                if (file.showDel) {
-                    html += `<tr><td>${idx + 1}</td><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td><a href="${preUrl}/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a><a href="javascript: void(0);" class="text-danger file-del" data-id="${file.id}"><i class="fa fa-remove"></i></a></td></tr>`
-                } else {
-                    html += `<tr><td width="70">${idx + 1}</td><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td><a href="${preUrl}/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a></td></tr>`
-                }
+                const filename = _.escape(file.filename || '');
+                const deleteLink = file.showDel ? `<a href="javascript: void(0);" class="text-danger file-del" data-id="${file.id}"><i class="fa fa-remove"></i></a>` : '';
+                html += `<tr class="inspection-attachment-row"><td>${idx + 1}</td><td class="inspection-attachment-name"><a href="${file.filepath}" target="_blank" title="${filename}">${filename}</a></td><td>${file.stageName || '上报'}</td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td><a href="${preUrl}/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a>${deleteLink}</td></tr>`;
             });
             $('#file-content').append(html);
         }
@@ -689,7 +680,7 @@ $(document).ready(function () {
     $('#file-content').on('click', 'a', function () {
         if ($(this).hasClass('file-del')) {
             const id = $(this).data('id');
-            postData(`${preUrl}/file/delete`, {id}, (result) => {
+            postData(`${preUrl}/file/delete`, {id, flowContext: getQualityInspectionAttachmentContext()}, (result) => {
                 handleFileList(result);
             })
         }
@@ -1101,3 +1092,10 @@ function bindQualityInspectionRectificationAutosave(readData) {
         });
     }
 }
+
+function getQualityInspectionAttachmentContext() {
+    return {
+        flow_version: inspection.flow_version, flow_stage: inspection.flow_stage,
+        stage_round: inspection.stage_round, times: inspection.times, status: inspection.status
+    };
+}

+ 1 - 1
app/public/js/safe_inspection.js

@@ -321,7 +321,7 @@ $(document).ready(() => {
         $('#file-content').empty();
         let html = '';
         files.forEach((file, idx) => {
-            html += `<tr><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td class="text-center">${file.username}</td><td class="text-center">${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td class="text-center"><a href="/sp/${spid}/safe/tender/${file.tid}/inspection/${file.qiid}/information/file/${file.id}/download"><i class="fa fa-download"></i></a></td></tr>`
+            html += `<tr><td class="text-center">${file.stageName || '上报'}</td><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td class="text-center">${file.username}</td><td class="text-center">${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td class="text-center"><a href="/sp/${spid}/safe/tender/${file.tid}/inspection/${file.qiid}/information/file/${file.id}/download"><i class="fa fa-download"></i></a></td></tr>`
         })
         $('#file-content').append(html);
     }

+ 19 - 21
app/public/js/safe_inspection_information.js

@@ -616,6 +616,7 @@ $(document).ready(function () {
         if (validateFiles(valiData)) {
             if (files.length) {
                 const formData = new FormData();
+                formData.append('flowContext', JSON.stringify(getSafeInspectionAttachmentContext()));
                 files.forEach(file => {
                     formData.append('name', file.name);
                     formData.append('size', file.size);
@@ -631,30 +632,22 @@ $(document).ready(function () {
     })
     function handleFileList(files = []) {
         $('#file-content').empty();
-        const newFiles = files.map(file => {
-            let showDel = false;
-            if (file.uid === cur_uid) {
-                if (inspection.status === auditConst.status.checked) {
-                    showDel = Boolean(file.extra_upload ) || deleteFilePermission
-                } else {
-                    showDel = true
-                }
-            }
-            if (inspection.flow_version === 1 && !inspection.canUpload) showDel = false;
-            return {...file, showDel}
-        })
+        const canUploadAttachments = typeof inspection.canUploadAttachments === 'boolean'
+            ? inspection.canUploadAttachments
+            : Boolean(inspection.flow_version === 1 ? inspection.canUpload : inspection.filePermission);
+        const newFiles = files.map(file => ({...file, showDel: file.canDelete === true}));
         const is_wap = window.location.pathname.indexOf('/wap/') !== -1;
         if (is_wap) {
             let html = `<div style="display: flex;">
             <b>附件</b>`;
-                if (inspection.flow_version === 1 ? inspection.canUpload : inspection.filePermission) {
+                if (canUploadAttachments) {
                     html += `<span style="margin-left: auto;"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></span>`;
                 }
             html += `</div><div>`;
             newFiles.forEach((file, idx) => {
                 html += `<a href="${file.filepath}" target="_blank">
                     <div class="card my-1" style="cursor: pointer">
-                        <div class="card-body"><i class="fa fa-file"></i> ${file.filename}<small class="pull-right">${file.showDel ? `<span class="badge">
+                        <div class="card-body"><span class="badge badge-light mr-1">${file.stageName || '上报'}</span><i class="fa fa-file"></i> ${file.filename}<small class="pull-right">${file.showDel ? `<span class="badge">
                                     <span class="dropdown">
                                 <a href="javascript:void(0)" class="btn-sm text-danger px-1" title="移除" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fa fa-remove"></i></a>
                                 <div class="dropdown-menu">
@@ -674,13 +667,11 @@ $(document).ready(function () {
             html += '</div>';
             $('#file-content').append(html);
         } else {
-            let html = (inspection.flow_version === 1 ? inspection.canUpload : inspection.filePermission) ? `<tr><td colspan="5"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></td></tr>` : '';
+            let html = canUploadAttachments ? `<tr><td colspan="6"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></td></tr>` : '';
             newFiles.forEach((file, idx) => {
-                if (file.showDel) {
-                    html += `<tr><td>${idx + 1}</td><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td><a href="${preUrl}/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a><a href="javascript: void(0);" class="text-danger file-del" data-id="${file.id}"><i class="fa fa-remove"></i></a></td></tr>`
-                } else {
-                    html += `<tr><td width="70">${idx + 1}</td><td><a href="${file.filepath}" target="_blank">${file.filename}</a></td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td><a href="${preUrl}/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a></td></tr>`
-                }
+                const filename = _.escape(file.filename || '');
+                const deleteLink = file.showDel ? `<a href="javascript: void(0);" class="text-danger file-del" data-id="${file.id}"><i class="fa fa-remove"></i></a>` : '';
+                html += `<tr class="inspection-attachment-row"><td>${idx + 1}</td><td class="inspection-attachment-name"><a href="${file.filepath}" target="_blank" title="${filename}">${filename}</a></td><td>${file.stageName || '上报'}</td><td>${file.username}</td><td>${moment(file.upload_time).format('YYYY-MM-DD HH:mm:ss')}</td><td><a href="${preUrl}/file/${file.id}/download" class="mr-2"><i class="fa fa-download"></i></a>${deleteLink}</td></tr>`;
             });
             $('#file-content').append(html);
         }
@@ -689,7 +680,7 @@ $(document).ready(function () {
     $('#file-content').on('click', 'a', function () {
         if ($(this).hasClass('file-del')) {
             const id = $(this).data('id');
-            postData(`${preUrl}/file/delete`, {id}, (result) => {
+            postData(`${preUrl}/file/delete`, {id, flowContext: getSafeInspectionAttachmentContext()}, (result) => {
                 handleFileList(result);
             })
         }
@@ -1101,3 +1092,10 @@ function bindSafeInspectionRectificationAutosave(readData) {
         });
     }
 }
+
+function getSafeInspectionAttachmentContext() {
+    return {
+        flow_version: inspection.flow_version, flow_stage: inspection.flow_stage,
+        stage_round: inspection.stage_round, times: inspection.times, status: inspection.status
+    };
+}

+ 10 - 6
app/service/quality_inspection_att.js

@@ -55,8 +55,9 @@ module.exports = app => {
          * @param {Array} payload 载荷
          * @return {Promise<void>} 数据库插入执行实例
          */
-        async saveFileMsgToDb(payload) {
-            return await this.db.insert(this.tableName, payload);
+        async saveFileMsgToDb(payload, transaction = false) {
+            const conn = transaction || this.db;
+            return await conn.insert(this.tableName, payload);
         }
 
         /**
@@ -64,8 +65,9 @@ module.exports = app => {
          * @param {Number} id 文件id
          * @return {Promise<void>} 数据库查询实例
          */
-        async getMaterialFileById(id) {
-            return await this.getDataByCondition({ id });
+        async getMaterialFileById(id, transaction = false) {
+            const conn = transaction || this.db;
+            return await conn.get(this.tableName, { id });
         }
 
         /**
@@ -73,8 +75,10 @@ module.exports = app => {
          * @param {Number} id - 附件id
          * @return {void}
          */
-        async delete(id) {
-            return await this.deleteById(id);
+        async delete(id, transaction = false) {
+            const conn = transaction || this.db;
+            const result = await conn.delete(this.tableName, { id });
+            return result.affectedRows > 0;
         }
     }
     return QualityInspectionAtt;

+ 10 - 6
app/service/safe_inspection_att.js

@@ -55,8 +55,9 @@ module.exports = app => {
          * @param {Array} payload 载荷
          * @return {Promise<void>} 数据库插入执行实例
          */
-        async saveFileMsgToDb(payload) {
-            return await this.db.insert(this.tableName, payload);
+        async saveFileMsgToDb(payload, transaction = false) {
+            const conn = transaction || this.db;
+            return await conn.insert(this.tableName, payload);
         }
 
         /**
@@ -64,8 +65,9 @@ module.exports = app => {
          * @param {Number} id 文件id
          * @return {Promise<void>} 数据库查询实例
          */
-        async getMaterialFileById(id) {
-            return await this.getDataByCondition({ id });
+        async getMaterialFileById(id, transaction = false) {
+            const conn = transaction || this.db;
+            return await conn.get(this.tableName, { id });
         }
 
         /**
@@ -73,8 +75,10 @@ module.exports = app => {
          * @param {Number} id - 附件id
          * @return {void}
          */
-        async delete(id) {
-            return await this.deleteById(id);
+        async delete(id, transaction = false) {
+            const conn = transaction || this.db;
+            const result = await conn.delete(this.tableName, { id });
+            return result.affectedRows > 0;
         }
     }
     return SafeInspectionAtt;

+ 24 - 13
app/view/quality/inspection_information.ejs

@@ -198,19 +198,30 @@
                     </div>
                     <% } %>
                     <% } %>
-                    <table class="table table-bordered mt-3">
-                        <thead>
-                        <tr>
-                            <th></th>
-                            <th class="text-center">附件</th>
-                            <th class="text-center">上传者</th>
-                            <th class="text-center" width="200">上传时间</th>
-                            <th class="text-center">操作</th>
-                        </tr>
-                        </thead>
-                        <tbody id="file-content">
-                        </tbody>
-                    </table>
+                    <div class="inspection-detail-attachments mt-3 mb-3">
+                        <table class="table table-bordered inspection-detail-attachments-table">
+                            <colgroup>
+                                <col class="attachment-col-number">
+                                <col class="attachment-col-name">
+                                <col class="attachment-col-stage">
+                                <col class="attachment-col-user">
+                                <col class="attachment-col-time">
+                                <col class="attachment-col-actions">
+                            </colgroup>
+                            <thead>
+                            <tr>
+                                <th>序号</th>
+                                <th class="text-center">附件</th>
+                                <th>阶段</th>
+                                <th>上传者</th>
+                                <th>上传时间</th>
+                                <th>操作</th>
+                            </tr>
+                            </thead>
+                            <tbody id="file-content">
+                            </tbody>
+                        </table>
+                    </div>
                     <% if (inspection.flow_version === 1) { %>
                         <% include ./inspection_flow.ejs %>
                     <% } else { %>

+ 1 - 1
app/view/quality/inspection_modal.ejs

@@ -34,7 +34,7 @@
                 <div class="modal-height-500" style="overflow: auto;">
                     <table class="table table-sm table-bordered">
                         <thead class="text-center">
-                        <tr><th width="45%">文件名</th><th width="15%">上传人</th><th width="25%">上传时间</th><th width="15%">操作</th></tr>
+                        <tr><th>阶段</th><th>文件名</th><th>上传人</th><th>上传时间</th><th>操作</th></tr>
                         </thead>
                         <tbody id="file-content"></tbody>
                     </table>

+ 24 - 13
app/view/safe/inspection_information.ejs

@@ -198,19 +198,30 @@
                     </div>
                     <% } %>
                     <% } %>
-                    <table class="table table-bordered mt-3">
-                        <thead>
-                        <tr>
-                            <th></th>
-                            <th class="text-center">附件</th>
-                            <th class="text-center">上传者</th>
-                            <th class="text-center" width="200">上传时间</th>
-                            <th class="text-center">操作</th>
-                        </tr>
-                        </thead>
-                        <tbody id="file-content">
-                        </tbody>
-                    </table>
+                    <div class="inspection-detail-attachments mt-3 mb-3">
+                        <table class="table table-bordered inspection-detail-attachments-table">
+                            <colgroup>
+                                <col class="attachment-col-number">
+                                <col class="attachment-col-name">
+                                <col class="attachment-col-stage">
+                                <col class="attachment-col-user">
+                                <col class="attachment-col-time">
+                                <col class="attachment-col-actions">
+                            </colgroup>
+                            <thead>
+                            <tr>
+                                <th>序号</th>
+                                <th class="text-center">附件</th>
+                                <th>阶段</th>
+                                <th>上传者</th>
+                                <th>上传时间</th>
+                                <th>操作</th>
+                            </tr>
+                            </thead>
+                            <tbody id="file-content">
+                            </tbody>
+                        </table>
+                    </div>
                     <% if (inspection.flow_version === 1) { %>
                         <% include ./inspection_flow.ejs %>
                     <% } else { %>

+ 1 - 1
app/view/safe/inspection_modal.ejs

@@ -34,7 +34,7 @@
                 <div class="modal-height-500" style="overflow: auto;">
                     <table class="table table-sm table-bordered">
                         <thead class="text-center">
-                        <tr><th width="45%">文件名</th><th width="15%">上传人</th><th width="25%">上传时间</th><th width="15%">操作</th></tr>
+                        <tr><th>阶段</th><th>文件名</th><th>上传人</th><th>上传时间</th><th>操作</th></tr>
                         </thead>
                         <tbody id="file-content"></tbody>
                     </table>

+ 2 - 2
app/view/wap/shenpi_inspection.ejs

@@ -117,13 +117,13 @@
                 <div class="mt-3" id="file-content">
                     <div style="display: flex;">
                         <b>附件</b>
-                        <% if (inspection.filePermission) { %><span style="margin-left: auto;"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></span><% } %>
+                        <% if (typeof inspection.canUploadAttachments === 'boolean' ? inspection.canUploadAttachments : (inspection.flow_version === 1 ? inspection.canUpload : inspection.filePermission)) { %><span style="margin-left: auto;"><a href="#addfujian" data-toggle="modal" class="btn btn-sm btn-light text-primary" data-placement="bottom" title="" data-original-title="上传附件"><i class="fa fa-cloud-upload" aria-hidden="true"></i> 上传附件</a></span><% } %>
                     </div>
                     <div>
                         <% for (const file of fileList) { %>
                             <a href="<% if (file.viewpath) { %><%- file.viewpath %><% } else { %>/wap/message/download/file/<%- file.id %><% } %>" target="_blank">
                                 <div class="card my-1" style="cursor: pointer">
-                                    <div class="card-body"><i class="fa fa-file"></i> <%- file.filename %><small class="pull-right text-secondary"><%- file.filesize %></small></div>
+                                    <div class="card-body"><span class="badge badge-light mr-1"><%= file.stageName || '上报' %></span><i class="fa fa-file"></i> <%- file.filename %><small class="pull-right text-secondary"><%- file.filesize %></small></div>
                                 </div>
                             </a>
                         <% } %>

+ 11 - 0
sql/update.sql

@@ -196,3 +196,14 @@ ALTER TABLE `zh_quality_inspection_audit`
     (`qiid`, `times`, `flow_stage`, `stage_round`, `is_old`, `status`),
   ADD INDEX `idx_quality_audit_todo`
     (`aid`, `status`, `qiid`, `times`);
+
+-- 安全、质量巡检附件阶段:flow_stage 字段已添加,本段不重复新增字段。
+-- 历史未标记附件默认归入上报,保留已标记的整改、批复附件;本段可重复执行。
+
+ALTER TABLE `zh_safe_inspection_attachment`
+  MODIFY COLUMN `flow_stage` tinyint unsigned NOT NULL DEFAULT 1
+    COMMENT '附件上传阶段:1上报(含历史附件),2整改,3批复';
+
+ALTER TABLE `zh_quality_inspection_attachment`
+  MODIFY COLUMN `flow_stage` tinyint unsigned NOT NULL DEFAULT 1
+    COMMENT '附件上传阶段:1上报(含历史附件),2整改,3批复';