Bläddra i källkod

通知附件功能

ellisran 6 månader sedan
förälder
incheckning
da80747bfd

+ 98 - 1
app/controller/dashboard_controller.js

@@ -13,6 +13,9 @@ const officeList = require('../const/cld_office').list;
 const maintainConst = require('../const/maintain');
 const typeColMap = require('../const/advance').typeColMap;
 const moment = require('moment');
+const fs = require('fs');
+const path = require('path');
+const sendToWormhole = require('stream-wormhole');
 
 module.exports = app => {
 
@@ -206,12 +209,17 @@ module.exports = app => {
                 const rule = ctx.service.message.rule();
                 const jsValidator = await this.jsValidator.convert(rule).build();
                 const msgInfo = id === 0 ? {} : await ctx.service.message.getDataById(id);
+                const files = await ctx.service.messageAtt.getAtt(id);
                 const renderData = {
                     jsValidator,
                     msgInfo,
+                    files,
+                    whiteList: ctx.app.config.multipart.whitelist,
+                    moment,
                 };
                 await this.layout('dashboard/msg_add.ejs', renderData, 'dashboard/msg_modal.ejs');
             } catch (error) {
+                console.log(error);
                 // this.setMessage(error.toString(), this.messageType.ERROR);
                 ctx.redirect(ctx.request.header.referer);
             }
@@ -266,6 +274,7 @@ module.exports = app => {
                     ctx.redirect('/dashboard/msg');
                 }
             } catch (error) {
+                console.log(error);
                 ctx.redirect(ctx.request.header.referer);
             }
         }
@@ -287,11 +296,12 @@ module.exports = app => {
                 if (!msgInfo || msgInfo.create_uid !== ctx.session.sessionUser.accountId) {
                     throw '通知不存在或无权限操作';
                 }
-                const result = await ctx.service.message.deleteById(msgInfo.id);
+                const result = await ctx.service.message.deleteMsg(msgInfo.id);
                 if (result) {
                     ctx.redirect('/dashboard/msg');
                 }
             } catch (error) {
+                console.log(error);
                 ctx.redirect(ctx.request.header.referer);
             }
         }
@@ -310,6 +320,93 @@ module.exports = app => {
                 ctx.body = { err: 1, msg: err.toString(), data: null };
             }
         }
+
+        /**
+         * 上传附件
+         * @param {*} ctx 上下文
+         */
+        async msgUploadFile(ctx) {
+            let stream;
+            try {
+                const responseData = { err: 0, msg: '', data: {} };
+                const mid = ctx.params.id || 0;
+                if (!mid) throw '参数有误';
+                const parts = this.ctx.multipart({
+                    autoFields: true,
+                });
+                const files = [];
+                const create_time = Date.parse(new Date()) / 1000;
+                let idx = 0;
+                while ((stream = await parts()) !== undefined) {
+                    if (!stream.filename) {
+                        // 如果没有传入直接返回
+                        return;
+                    }
+                    const fileInfo = path.parse(stream.filename);
+                    const filepath = `app/public/upload/message/fujian_${create_time + idx.toString() + fileInfo.ext}`;
+                    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();
+                const payload = files.map(file => {
+                    let idx;
+                    if (Array.isArray(parts.field.name)) {
+                        idx = parts.field.name.findIndex(name => name === file.name);
+                    } else {
+                        idx = 'isString';
+                    }
+                    const newFile = {
+                        project_id: ctx.session.sessionProject.id,
+                        mid,
+                        uid: ctx.session.sessionUser.accountId,
+                        filename: file.name,
+                        fileext: file.ext,
+                        filesize: ctx.helper.bytesToSize(idx === 'isString' ? parts.field.size : parts.field.size[idx]),
+                        filepath: file.filepath,
+                        upload_time: in_time,
+                    };
+                    return newFile;
+                });
+                // 执行文件信息写入数据库
+                await ctx.service.messageAtt.saveFileMsgToDb(payload);
+                // 将最新的当前标段的所有文件信息返回
+                responseData.data = await ctx.service.messageAtt.getAtt(mid);
+                ctx.body = responseData;
+            } catch (err) {
+                stream && (await sendToWormhole(stream));
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
+        /**
+         * 删除附件
+         * @param {Ojbect} ctx 上下文
+         */
+        async msgDeleteFile(ctx) {
+            try {
+                const mid = ctx.params.id || 0;
+                const responseData = { err: 0, msg: '', data: {} };
+                const data = JSON.parse(ctx.request.body.data);
+                const fileInfo = await ctx.service.messageAtt.getDataById(data.id);
+                if (fileInfo) {
+                    // 先删除文件
+                    // 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.messageAtt.delete(data.id);
+                } else {
+                    throw '不存在该文件';
+                }
+                responseData.data = await ctx.service.messageAtt.getAtt(mid);
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
     }
 
     return DashboardController;

+ 42 - 0
app/controller/wap_controller.js

@@ -904,6 +904,46 @@ module.exports = app => {
          * @param {Object} ctx - egg全局变量
          * @return {void}
          */
+        async messageDownloadFile(ctx) {
+            const id = ctx.params.fid;
+            if (id) {
+                try {
+                    const fileInfo = await ctx.service.messageAtt.getDataById(id);
+                    if (fileInfo !== undefined && fileInfo !== '') {
+                        // const fileName = path.join(this.app.baseDir, fileInfo.filepath);
+                        // 解决中文无法下载问题
+                        const userAgent = (ctx.request.header['user-agent'] || '').toLowerCase();
+                        let disposition = '';
+                        if (userAgent.indexOf('msie') >= 0 || userAgent.indexOf('chrome') >= 0) {
+                            disposition = 'attachment; filename=' + encodeURIComponent(fileInfo.filename + fileInfo.fileext);
+                        } else if (userAgent.indexOf('firefox') >= 0) {
+                            disposition = 'attachment; filename*="utf8\'\'' + encodeURIComponent(fileInfo.filename + fileInfo.fileext) + '"';
+                        } else {
+                            /* safari等其他非主流浏览器只能自求多福了 */
+                            disposition = 'attachment; filename=' + new Buffer(fileInfo.filename + fileInfo.fileext).toString('binary');
+                        }
+                        ctx.response.set({
+                            'Content-Type': 'application/octet-stream',
+                            'Content-Disposition': disposition,
+                            'Content-Length': fileInfo.filesize,
+                        });
+                        // ctx.body = await fs.createReadStream(fileName);
+                        ctx.body = await ctx.helper.ossFileGet(fileInfo.filepath);
+                    } else {
+                        throw '不存在该文件';
+                    }
+                } catch (err) {
+                    this.log(err);
+                    this.setMessage(err.toString(), this.messageType.ERROR);
+                }
+            }
+        }
+
+        /**
+         * 下载附件
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
         async shoufangDownloadFile(ctx) {
             const id = ctx.params.fid;
             if (id) {
@@ -946,6 +986,7 @@ module.exports = app => {
                     throw '参数有误';
                 }
                 const msgInfo = await ctx.service.message.getDataById(msgId);
+                const files = await ctx.service.messageAtt.getAtt(msgId);
                 if (!msgInfo) {
                     throw '项目通知不存在';
                 }
@@ -955,6 +996,7 @@ module.exports = app => {
                 const renderData = {
                     msgInfo,
                     moment,
+                    files,
                 };
                 await ctx.render('wap/msg.ejs', renderData);
             } catch (error) {

+ 18 - 1
app/public/js/dashboard.js

@@ -43,6 +43,7 @@ $(document).ready(() => {
             $('#creator').html('');
             $('#view-msg .modal-title').text('系统通知');
         }
+        setFiles(msgInfo.files);
     });
 
     $('.system-msg').on('click', function () {
@@ -54,7 +55,23 @@ $(document).ready(() => {
         $('#content').html(msgInfo.content);
         $('#view-msg .modal-title').text('系统通知');
         $('#user_permission').html('');
-    })
+        setFiles(msgInfo.files);
+    });
+
+    function setFiles(files) {
+        $('#files-list').html('');
+        if (files && files.length > 0) {
+            let html = '<h5>附件</h5>';
+            for (const file of files) {
+                html += `<a href="${file.viewpath ? file.viewpath : '/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}<span class="pull-right text-secondary">${file.filesize}</span></div>
+                                        </div>
+                                    </a>`;
+            }
+            $('#files-list').html(html);
+        }
+    }
 
     // $('#notice').on('click', 'li a', function() {
     //     const id = $(this).data('id')

+ 3 - 0
app/router.js

@@ -90,6 +90,9 @@ module.exports = app => {
     app.get('/dashboard/msg/add/:id', sessionAuth, 'dashboardController.msgAdd');
     app.post('/dashboard/msg/set/:id', sessionAuth, datetimeFill, 'dashboardController.msgSet');
     app.get('/dashboard/msg/del/:id', sessionAuth, 'dashboardController.msgDelete');
+    app.post('/dashboard/msg/:id/file/upload', sessionAuth, 'dashboardController.msgUploadFile');
+    app.post('/dashboard/msg/:id/file/del', sessionAuth, 'dashboardController.msgDeleteFile');
+    app.get('/wap/message/download/file/:fid', 'wapController.messageDownloadFile');
 
     // 推送相关
     // app.post('/dashboard/push', sessionAuth, 'dashboardController.pushSet');

+ 65 - 20
app/service/message.js

@@ -99,25 +99,45 @@ module.exports = app => {
             if (data._csrf_j !== undefined) {
                 delete data._csrf_j;
             }
-            if (id > 0) {
-                // 修改操作时
-                data.id = id;
-                const msgInfo = await this.getDataById(id);
-                data.istop = parseFloat(msgInfo.istop) === 0 && parseInt(data.istop) === 1 ? data.create_time : data.istop === undefined ? 0 : msgInfo.istop;
-                delete data.create_time;
-            } else {
-                data.release_time = data.create_time;
-                data.project_id = projectId;
-                data.create_uid = user.accountId;
-                data.creator = user.name;
-                data.istop = parseInt(data.istop) === 1 ? data.create_time : 0;
+            const transaction = await this.db.beginTransaction();
+            try {
+                if (id > 0) {
+                    // 修改操作时
+                    data.id = id;
+                    const msgInfo = await this.getDataById(id);
+                    data.istop = parseFloat(msgInfo.istop) === 0 && parseInt(data.istop) === 1 ? data.create_time : data.istop === undefined ? 0 : msgInfo.istop;
+                    delete data.create_time;
+                } else {
+                    data.release_time = data.create_time;
+                    data.project_id = projectId;
+                    data.create_uid = user.accountId;
+                    data.creator = user.name;
+                    data.istop = parseInt(data.istop) === 1 ? data.create_time : 0;
+                }
+                const operate = id === 0 ? await transaction.insert(this.tableName, data) :
+                    await transaction.update(this.tableName, data);
+
+                const result = operate.affectedRows > 0 ? operate.insertId : false;
+                if (id === 0) {
+                    const attList = await this.ctx.service.messageAtt.getAllDataByCondition({ where: { mid: 0, uid: user.accountId } });
+                    if (attList.length > 0) {
+                        const fileUpdateDatas = [];
+                        for (const att of attList) {
+                            fileUpdateDatas.push({
+                                id: att.id,
+                                mid: result,
+                            });
+                        }
+                        await transaction.updateRows(this.ctx.service.messageAtt.tableName, fileUpdateDatas);
+                    }
+                }
+                await transaction.commit();
+                return result;
+            } catch (error) {
+                console.log(error);
+                await transaction.rollback();
+                return false;
             }
-            const operate = id === 0 ? await this.db.insert(this.tableName, data) :
-                await this.db.update(this.tableName, data);
-
-            const result = operate.affectedRows > 0 ? operate.insertId : false;
-
-            return result;
         }
 
         /**
@@ -130,12 +150,37 @@ module.exports = app => {
             if (type === 1) {
                 const sql = 'SELECT * FROM ?? WHERE `project_id` = ? AND `type` = ? ORDER BY CONCAT(`istop`,`release_time`) DESC LIMIT ?,?';
                 const sqlParam = [this.tableName, projectId, type, offset, limit];
-                return await this.db.query(sql, sqlParam);
+                const result = await this.db.query(sql, sqlParam);
+                for (const r of result) {
+                    r.files = await this.ctx.service.messageAtt.getAtt(r.id);
+                }
+                return result;
             }
 
             const sql = 'SELECT * FROM ?? WHERE `type` = ? AND `status` = ? ORDER BY `release_time` DESC LIMIT ?,?';
             const sqlParam = [this.tableName, type, 1, offset, limit];
-            return await this.db.query(sql, sqlParam);
+            const result = await this.db.query(sql, sqlParam);
+            for (const r of result) {
+                r.files = await this.ctx.service.messageAtt.getAtt(r.id);
+            }
+            return result;
+        }
+
+        async deleteMsg(id) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                await transaction.delete(this.tableName, { id });
+                // 删除附件
+                const attList = await this.ctx.service.messageAtt.getAllDataByCondition({ where: { mid: id } });
+                await this.ctx.helper.delFiles(attList);
+                await transaction.delete(this.ctx.service.messageAtt.tableName, { mid: id });
+                await transaction.commit();
+                return true;
+            } catch (error) {
+                console.log(error);
+                await transaction.rollback();
+                return false;
+            }
         }
     }
 

+ 64 - 0
app/service/message_att.js

@@ -0,0 +1,64 @@
+'use strict';
+
+/**
+ * Created by EllisRan on 2020/3/3.
+ */
+
+const BaseService = require('../base/base_service');
+
+module.exports = app => {
+
+    class MessageAtt extends BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'message_attachment';
+            this.dataId = 'id';
+        }
+
+        async getAtt(mid = 0) {
+            let userSql = '';
+            if (mid === 0) {
+                userSql += ' AND a.`uid` = ' + this.ctx.session.sessionUser.accountId;
+            }
+            const sql = 'SELECT a.*, b.name as username FROM ?? as a LEFT JOIN ?? as b ON a.`uid` = b.`id` WHERE a.`mid` = ?' + userSql + ' ORDER BY `upload_time` DESC';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, mid];
+            const result = await this.db.query(sql, sqlParam);
+            return result.map(item => {
+                item.orginpath = this.ctx.app.config.fujianOssPath + item.filepath;
+                if (!this.ctx.helper.canPreview(item.fileext)) {
+                    item.filepath = `/wap/message/download/file/file/${item.id}/download`;
+                } else {
+                    item.filepath = this.ctx.app.config.fujianOssPath + item.filepath;
+                    item.viewpath = item.filepath;
+                }
+                return item;
+            });
+        }
+
+        /**
+         * 存储上传的文件信息至数据库
+         * @param {Array} payload 载荷
+         * @return {Promise<void>} 数据库插入执行实例
+         */
+        async saveFileMsgToDb(payload) {
+            return await this.db.insert(this.tableName, payload);
+        }
+
+        /**
+         * 删除附件
+         * @param {Number} id - 附件id
+         * @return {void}
+         */
+        async delete(id) {
+            return await this.deleteById(id);
+        }
+    }
+    return MessageAtt;
+};

+ 1 - 1
app/view/dashboard/index.ejs

@@ -355,7 +355,7 @@
                         </div>
                         <div class="card-body p-0">
                             <div class="contant-height-three">
-                                <ul class="list-group list-group-flush">
+                                <ul class="list-group list-group-flush msg-height-list">
                                     <% if (msgList.length === 0) { %>
                                         <!--没有通知-->
                                         <li class="list-group-item text-muted text-center p-5">

+ 3 - 1
app/view/dashboard/modal.ejs

@@ -46,9 +46,11 @@
                                 </span>
                             </p>
                             <!--内容开始-->
-                            <div class="msg-content border-top-1 pt-3" id="content">
+                            <div class="msg-content border-top-1 pt-3 mb-2" id="content">
 
                             </div>
+                            <div class="mt-3" id="files-list">
+                            </div>
                         </div>
                     </div>
                 </div>

+ 12 - 0
app/view/dashboard/msg.ejs

@@ -42,6 +42,18 @@
                                 <div class="msg-content border-top-1 pt-3" id="content">
                                     <%- msgInfo.content %>
                                 </div>
+                                <div class="mt-3" id="files-list">
+                                    <% if (msgInfo.files && msgInfo.files.length > 0) { %>
+                                    <h5>附件</h5>
+                                    <% for (const file of msgInfo.files) { %>
+                                    <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 %><span class="pull-right text-secondary"><%- file.filesize %></span></div>
+                                        </div>
+                                    </a>
+                                    <% } %>
+                                    <% } %>
+                                </div>
                                 <% } %>
                             </div>
                         </div>

+ 109 - 1
app/view/dashboard/msg_add.ejs

@@ -21,6 +21,26 @@
                                         <textarea id="content" rows="15" name="content" class="form-control form-control-sm"><%- msgInfo.content %></textarea>
                                     </div>
                                 </div>
+                                <div class="form-group" id="msg-file">
+                                    <label>附件</label>
+                                    <input type="hidden" value="<%= msgInfo.id === undefined ? 0 : msgInfo.id %>" id="mid">
+                                    <div class="form-group upload-permission">
+                                        <label for="formGroupExampleInput">单个文件大小限制:30MB,支持<span data-toggle="tooltip" data-placement="bottom" title="doc,docx,xls,xlsx,ppt,pptx,pdf">office等文档格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="jpg,png,bmp">图片格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="rar,zip">压缩包格式</span></label>
+                                        <br>
+                                        <input type="file" class="" multiple>
+                                    </div>
+                                    <div class="sp-wrap" style="overflow: auto">
+                                        <table class="table table-sm table-bordered" id="file-table">
+                                            <thead>
+                                            <tr class="text-center">
+                                                <th width="5%">序号</th><th>名称</th><th width="15%">上传人</th><th width="15%">上传时间</th><th width="15%">操作</th>
+                                            </tr>
+                                            </thead>
+                                            <tbody>
+                                            </tbody>
+                                        </table>
+                                    </div>
+                                </div>
                             </div>
                         </div>
                     </div>
@@ -43,13 +63,101 @@
     </div>
 </div>
 <%- jsValidator %>
+<script src="/public/js/moment/moment.min.js"></script>
 <script type="text/javascript">
     new Vue({
         el: '#save-form',
     });
+    const user_id = <%- ctx.session.sessionUser.accountId %>;
+    const files = JSON.parse(unescape('<%- escape(JSON.stringify(files)) %>'));
+    const whiteList = JSON.parse('<%- JSON.stringify(whiteList) %>');
     $(function () {
         $('#btn_click').click(function () {
             $('#btn_type').val(2);
-        })
+        });
+
+        setFiles(files);
+
+        function setFiles(files, _this = '#file-table tbody') {
+            let filesHtml = '';
+            const newFiles = files.map(file => {
+                let showDel = false;
+                if (file.uid === user_id) {
+                    showDel = true
+                }
+                return {...file, showDel}
+            })
+            newFiles.forEach((file, idx) => {
+                filesHtml += `<tr class="text-center">
+                                        <td>${idx + 1}</td><td class="text-left"><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>
+                                            <div class="btn-group-table">
+                                                ${file.viewpath ? `<a href="${file.viewpath}" target="_blank" class="mr-1"><i class="fa fa-eye fa-fw"></i></a>` : ''}
+                                                <a href="/wap/message/download/file/${file.id}" class="mr-1"><i class="fa fa-download fa-fw"></i></a>
+                                                ${file.showDel ? `<a href="javascript: void(0);" class="text-danger file-del mr-1" data-id="${file.id}"><i class="fa fa-trash-o fa-fw text-danger"></i></a>` : ''}
+                                            </div>
+                                        </td>
+                                    </tr>`;
+            });
+            $(_this).html(filesHtml);
+        }
+
+        $('#msg-file input[type="file"]').change(function () {
+            const files = Array.from(this.files);
+            console.log(files);
+            const valiData = files.map(v => {
+                const ext = v.name.substring(v.name.lastIndexOf('.') + 1)
+                return {
+                    size: v.size,
+                    ext
+                }
+            })
+            const mid = parseInt($('#mid').val());
+            if (validateFiles(valiData)) {
+                if (files.length) {
+                    const formData = new FormData()
+                    files.forEach(file => {
+                        formData.append('name', file.name)
+                        formData.append('size', file.size)
+                        formData.append('file', file)
+                    })
+                    postDataWithFile(`/dashboard/msg/${mid}/file/upload`, formData, function (result) {
+                        setFiles(result);
+                    });
+                }
+            }
+            $('#msg-file input[type="file"]').val('');
+        });
+
+        $('body').on('click', '#msg-file .file-del', function () {
+            const fid = $(this).data('id');
+            const mid = parseInt($('#mid').val());
+            deleteAfterHint(function () {
+                postData(`/dashboard/msg/${mid}/file/del`, { id: fid }, function (result) {
+                    setFiles(result);
+                });
+            }, '确认删除该文件?');
+        });
     })
+    /**
+     * 校验文件大小、格式
+     * @param {Array} files 文件数组
+     */
+    function validateFiles(files) {
+        if (files.length > 10) {
+            toastr.error('至多同时上传10个文件');
+            return false
+        }
+        return files.every(file => {
+            if (file.size > 1024 * 1024 * 30) {
+                toastr.error('文件大小限制为30MB');
+                return false
+            }
+            if (whiteList.indexOf('.' + file.ext.toLowerCase()) === -1) {
+                toastr.error('请上传正确的格式文件');
+                return false
+            }
+            return true
+        })
+    }
 </script>

+ 1 - 0
app/view/dashboard/msg_modal.ejs

@@ -1,3 +1,4 @@
+<% include ../shares/delete_hint_modal.ejs %>
 <div class="modal fade" tabindex="-1" role="dialog" id="del-modal">
     <div class="modal-dialog" role="document">
         <div class="modal-content">

+ 11 - 1
app/view/wap/msg.ejs

@@ -43,9 +43,19 @@
                 <span id="creator"><%- msgInfo.type === 1 ? msgInfo.creator : '' %></span> 发布于 <span id="release_time"><%= moment(msgInfo.release_time*1000).format('YYYY-MM-DD') %></span>
             </p>
             <!--内容开始-->
-            <div class="msg-content border-top-1 pt-3" id="content">
+            <div class="msg-content border-top-1 pt-3 mb-2" id="content">
                 <%- msgInfo.content %>
             </div>
+            <div class="mt-3">
+                <b>附件</b>
+                <% for (const file of files) { %>
+                <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>
+                </a>
+                <% } %>
+            </div>
         </div>
     </div>
     <!--底栏菜单-->

+ 14 - 0
sql/update.sql

@@ -85,6 +85,20 @@ CREATE TABLE `zh_sub_project_file`  (
   PRIMARY KEY (`id`) USING BTREE
 ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
 
+CREATE TABLE `zh_message_attachment` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `project_id` int(11) DEFAULT NULL COMMENT '项目id',
+  `mid` int(11) NOT NULL DEFAULT '0' COMMENT '通知id',
+  `uid` int(11) NOT NULL COMMENT '上传者id',
+  `filename` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT '文件名称',
+  `fileext` varchar(5) COLLATE utf8_unicode_ci NOT NULL COMMENT '文件后缀',
+  `filesize` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT '文件大小',
+  `filepath` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT '文件存储路径',
+  `upload_time` datetime NOT NULL COMMENT '上传时间',
+  PRIMARY KEY (`id`) USING BTREE,
+  KEY `idx_mid` (`mid`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='通知附件表';
+
 ALTER TABLE `zh_tender_info`
 ADD COLUMN `over_range_check` varchar(255) NOT NULL DEFAULT '' AFTER `s_type`;