Procházet zdrojové kódy

操作日志记录

laiguoran před 4 roky
rodič
revize
30df216a29

+ 40 - 0
app/const/project_log.js

@@ -0,0 +1,40 @@
+'use strict';
+
+/**
+ * 项目操作日志
+ *
+ * @author ELlisran
+ * @date 2021/01/12
+ * @version
+ */
+// 操作模块
+const type = {
+    all: 0,
+    tender: 1,
+    stage: 2,
+    change: 3,
+    material: 4,
+};
+
+const type_list = [
+    { code: 'all', type: type.all, name: '全部模块' },
+    { code: 'tender', type: type.tender, name: '标段' },
+    { code: 'stage', type: type.stage, name: '计量期' },
+    { code: 'change', type: type.change, name: '变更令' },
+    { code: 'material', type: type.material, name: '材料调差' },
+];
+// 操作状态
+const status = {
+    delete: 1, // 删除
+};
+const status_list = [
+    '',
+    '删除',
+];
+
+module.exports = {
+    type,
+    type_list,
+    status,
+    status_list,
+};

+ 25 - 0
app/controller/setting_controller.js

@@ -13,6 +13,7 @@ const settingConst = require('../const/setting.js');
 const settingMenu = require('../../config/menu').settingMenu;
 const accountGroup = require('../const/account_group').group;
 const permission = require('../const/account_permission').permission;
+const projectLog = require('../const/project_log');
 
 module.exports = app => {
 
@@ -648,6 +649,30 @@ module.exports = app => {
                 ctx.body = { err: 1, msg: error.toString(), data: null };
             }
         }
+
+        async logs(ctx) {
+            try {
+                // 获取项目数据
+                const projectId = ctx.session.sessionProject.id;
+                const projectData = await ctx.service.project.getDataById(projectId);
+                if (projectData === null) {
+                    throw '没有对应的项目数据';
+                }
+                const settingType = ctx.params.type ? parseInt(ctx.params.type) : 0;
+                const logs = await ctx.service.projectLog.getLogs(projectId, settingType);
+                const renderData = {
+                    projectData,
+                    officeList,
+                    projectLog,
+                    settingType,
+                    logs,
+                };
+                await this.layout('setting/logs.ejs', renderData);
+            } catch (error) {
+                console.log(error);
+                ctx.redirect('/dashboard');
+            }
+        }
     }
 
     return SettingController;

+ 73 - 0
app/extend/helper.js

@@ -19,6 +19,7 @@ Decimal.set({ precision: 50, defaults: true });
 const SMS = require('../lib/sms');
 const WX = require('../lib/wechat');
 const timesLen = 100;
+const UAParser = require('ua-parser-js');
 
 module.exports = {
     _,
@@ -1276,4 +1277,76 @@ module.exports = {
         }
         return result;
     },
+
+    /**
+     * 创建登录日志
+     * @return {Boolean} 日志是否创建成功
+     * @param {Number} type - 登录类型
+     * @param {Number} status - 是否显示记录
+     */
+    async getUserIPMsg() {
+        const { ctx } = this;
+        const ip = ctx.request.ip ? ctx.request.ip : '';
+        const ipInfo = await this.getIpInfoFromApi(ip);
+        const parser = new UAParser(ctx.header['user-agent']);
+        const osInfo = parser.getOS();
+        const cpuInfo = parser.getCPU();
+        const browserInfo = parser.getBrowser();
+        const ipMsg = {
+            os: `${osInfo.name} ${osInfo.version} ${cpuInfo.architecture}`,
+            browser: `${browserInfo.name} ${browserInfo.version}`,
+            ip,
+            address: ipInfo,
+        };
+        return ipMsg;
+    },
+
+    /**
+     * 根据ip请求获取详细地址
+     * @param {String} a_ip - ip地址
+     * @return {String} 详细地址
+     */
+    async getIpInfoFromApi(a_ip = '') {
+        if (!a_ip) return '';
+        if (a_ip === '127.0.0.1' || a_ip === '::1' || a_ip.indexOf('192.168') !== -1) return '服务器本机访问';
+        const { ip = '', region = '', city = '', isp = '' } = await this.sendIpRequest(a_ip);
+        let address = '';
+        region && (address += region + '省');
+        city && (address += city + '市 ');
+        isp && (address += isp + ' ');
+        ip && (address += `(${ip})`);
+        return address;
+    },
+
+    /**
+     * 发送请求获取详细地址
+     * @param {String} ip - ip地址
+     * @return {Object} the result of request
+     * @private
+     */
+    async sendIpRequest(ip) {
+        return new Promise(resolve => {
+            this.ctx.curl(`https://api01.aliyun.venuscn.com/ip?ip=${ip}`, {
+                dateType: 'json',
+                encoding: 'utf8',
+                timeout: 2000,
+                headers: {
+                    Authorization: 'APPCODE 85c64bffe70445c4af9df7ae31c7bfcc',
+                },
+            }).then(({ status, data }) => {
+                if (status === 200) {
+                    const result = JSON.parse(data.toString()).data;
+                    if (!result.ip) {
+                        resolve({});
+                    } else {
+                        resolve(result);
+                    }
+                } else {
+                    resolve({});
+                }
+            }).catch(() => {
+                resolve({});
+            });
+        });
+    },
 };

+ 3 - 0
app/router.js

@@ -80,6 +80,9 @@ module.exports = app => {
     app.post('/setting/category/update', sessionAuth, 'settingController.updateCategory');
     app.post('/setting/category/value', sessionAuth, 'settingController.setCategoryValue');
     app.post('/setting/category/level', sessionAuth, 'settingController.resetCategoryLevel');
+    // 操作日志
+    app.get('/setting/logs', sessionAuth, 'settingController.logs');
+    app.get('/setting/logs/type/:type', sessionAuth, 'settingController.logs');
 
     // 项目相关
     app.get('/project/info', sessionAuth, 'projectController.info');

+ 4 - 0
app/service/change.js

@@ -17,6 +17,7 @@ const SMS = require('../lib/sms');
 const SmsAliConst = require('../const/sms_alitemplate');
 const wxConst = require('../const/wechat_template');
 const pushType = require('../const/audit').pushType;
+const projectLogConst = require('../const/project_log');
 
 module.exports = app => {
     class Change extends app.BaseService {
@@ -1157,6 +1158,7 @@ module.exports = app => {
             this.transaction = await this.db.beginTransaction();
             let result = false;
             try {
+                const changeInfo = await this.getDataByCondition({ cid });
                 // 先删除清单,审批人列表
                 await this.transaction.delete(this.ctx.service.changeAuditList.tableName, { cid });
                 await this.transaction.delete(this.ctx.service.changeAudit.tableName, { cid });
@@ -1170,6 +1172,8 @@ module.exports = app => {
                 }
                 // 最后删除变更令
                 await this.transaction.delete(this.tableName, { cid });
+                // 记录删除日志
+                await this.ctx.service.projectLog.addProjectLog(this.transaction, projectLogConst.type.change, projectLogConst.status.delete, changeInfo.code);
                 await this.transaction.commit();
                 result = true;
             } catch (e) {

+ 3 - 62
app/service/login_logging.js

@@ -7,7 +7,7 @@
  * @date 2020/8/31
  * @version
  */
-const UAParser = require('ua-parser-js');
+// const UAParser = require('ua-parser-js');
 
 module.exports = app => {
     class LoginLogging extends app.BaseService {
@@ -39,75 +39,16 @@ module.exports = app => {
          */
         async addLoginLog(type, status) {
             const { ctx } = this;
-            const ip = ctx.request.ip ? ctx.request.ip : '';
-            const ipInfo = await this.getIpInfoFromApi(ip);
-            const parser = new UAParser(ctx.header['user-agent']);
-            const osInfo = parser.getOS();
-            const cpuInfo = parser.getCPU();
-            const browserInfo = parser.getBrowser();
+            const ipMsg = await ctx.helper.getUserIPMsg();
             const payload = {
-                os: `${osInfo.name} ${osInfo.version} ${cpuInfo.architecture}`,
-                browser: `${browserInfo.name} ${browserInfo.version}`,
-                ip,
-                address: ipInfo,
                 uid: ctx.session.sessionUser.accountId,
                 pid: ctx.session.sessionProject.id,
                 type,
                 show: status,
             };
+            this._.assign(payload, ipMsg);
             return await this.createLog(payload);
         }
-
-        /**
-         * 根据ip请求获取详细地址
-         * @param {String} a_ip - ip地址
-         * @return {String} 详细地址
-         */
-        async getIpInfoFromApi(a_ip = '') {
-            if (!a_ip) return '';
-            if (a_ip === '127.0.0.1' || a_ip === '::1' || a_ip.indexOf('192.168') !== -1) return '服务器本机访问';
-            const { ip = '', region = '', city = '', isp = '' } = await this.sendRequest(a_ip);
-            let address = '';
-            region && (address += region + '省');
-            city && (address += city + '市 ');
-            isp && (address += isp + ' ');
-            ip && (address += `(${ip})`);
-            return address;
-
-        }
-
-        /**
-         * 发送请求获取详细地址
-         * @param {String} ip - ip地址
-         * @return {Object} the result of request
-         * @private
-         */
-        async sendRequest(ip) {
-            return new Promise(resolve => {
-                this.ctx.curl(`https://api01.aliyun.venuscn.com/ip?ip=${ip}`, {
-                    dateType: 'json',
-                    encoding: 'utf8',
-                    timeout: 2000,
-                    headers: {
-                        Authorization: 'APPCODE 85c64bffe70445c4af9df7ae31c7bfcc',
-                    },
-                }).then(({ status, data }) => {
-                    if (status === 200) {
-                        const result = JSON.parse(data.toString()).data;
-                        if (!result.ip) {
-                            resolve({});
-                        } else {
-                            resolve(result);
-                        }
-                    } else {
-                        resolve({});
-                    }
-                }).catch(() => {
-                    resolve({});
-                });
-            });
-        }
-
         /**
          * 获取登录日志
          * @param {Number} pid - 项目id

+ 4 - 0
app/service/material.js

@@ -9,6 +9,7 @@
  */
 
 const auditConst = require('../const/audit').material;
+const projectLogConst = require('../const/project_log');
 module.exports = app => {
     class Material extends app.BaseService {
         /**
@@ -213,6 +214,9 @@ module.exports = app => {
                     await transaction.query(sql2, sqlParam2);
                 }
                 await transaction.delete(this.tableName, { id });
+
+                // 记录删除日志
+                await this.ctx.service.projectLog.addProjectLog(transaction, projectLogConst.type.material, projectLogConst.status.delete, '第' + materialInfo.order + '期');
                 await transaction.commit();
                 return true;
             } catch (err) {

+ 53 - 0
app/service/project_log.js

@@ -0,0 +1,53 @@
+'use strict';
+
+/**
+ * 项目操作日志-数据模型
+ *
+ * @author ellisran
+ * @date 2021/01/12
+ * @version
+ */
+
+module.exports = app => {
+    class projectLog extends app.BaseService {
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'project_log';
+        }
+
+        /**
+         * 创建登录日志
+         * @return {Boolean} 日志是否创建成功
+         * @param {Number} type - 登录类型
+         * @param {Number} status - 是否显示记录
+         */
+        async addProjectLog(transaction, type, status, msg, tid = 0) {
+            const { ctx } = this;
+            const ipMsg = await ctx.helper.getUserIPMsg();
+            const payload = {
+                uid: ctx.session.sessionUser.accountId,
+                pid: ctx.session.sessionProject.id,
+                tid: ctx.tender.id ? ctx.tender.id : tid,
+                type,
+                status,
+                msg,
+            };
+            this._.assign(payload, ipMsg);
+            return await transaction.insert(this.tableName, payload);
+        }
+
+        /**
+         * 获取操作日志
+         * @param {Number} pid - 项目id
+         * @param {Number} type - 类型
+         * @return {Promise<Array>} 日志数组
+         */
+        async getLogs(pid, type = 0) {
+            const typeSql = parseInt(type) !== 0 ? ' AND A.`type` = ' + type : '';
+            const sql = 'SELECT A.*, B.`name` as `username`, B.`mobile` FROM ?? as A LEFT JOIN ?? as B ON A.`uid` = B.`id` WHERE A.`pid` = ?' + typeSql + ' AND TO_DAYS(NOW()) - TO_DAYS(A.`create_time`) <= 30  ORDER BY A.`id` DESC';
+            const sqlParam = [this.tableName, this.ctx.service.projectAccount.tableName, pid];
+            return await this.db.query(sql, sqlParam);
+        }
+    }
+    return projectLog;
+};

+ 3 - 0
app/service/stage.js

@@ -14,6 +14,7 @@ const roleRelSvr = require('./role_rpt_rel');
 const fs = require('fs');
 const path = require('path');
 const _ = require('lodash');
+const projectLogConst = require('../const/project_log');
 
 module.exports = app => {
     class Stage extends app.BaseService {
@@ -546,6 +547,8 @@ module.exports = app => {
                 }
                 await transaction.delete(this.ctx.service.stageBonus.tableName, { sid: id });
                 await transaction.delete(this.ctx.service.stageOther.tableName, { sid: id });
+                // 记录删除日志
+                await this.ctx.service.projectLog.addProjectLog(transaction, projectLogConst.type.stage, projectLogConst.status.delete, '第' + stageInfo.order + '期');
                 await transaction.commit();
                 return true;
             } catch (err) {

+ 5 - 0
app/service/tender.js

@@ -10,6 +10,7 @@
 
 const tenderConst = require('../const/tender');
 const auditConst = require('../const/audit');
+const projectLogConst = require('../const/project_log');
 const fs = require('fs');
 const path = require('path');
 const commonQueryColumns = ['id', 'project_id', 'name', 'status', 'category', 'ledger_times', 'ledger_status', 'measure_type', 'user_id', 'valuation', 'total_price', 'deal_tp', 'copy_id'];
@@ -270,6 +271,7 @@ module.exports = app => {
         async deleteTenderNoBackup(id) {
             const transaction = await this.db.beginTransaction();
             try {
+                const tenderMsg = await this.getDataById(id);
                 // 先删除附件文件
                 const attList = await this.ctx.service.changeAtt.getAllDataByCondition({ where: { tid: id } });
                 const newAttList = await this.ctx.service.materialFile.getAllMaterialFiles(id);
@@ -325,6 +327,9 @@ module.exports = app => {
                 await transaction.delete(this.ctx.service.materialFile.tableName, { tid: id });
 
                 await transaction.delete(this.ctx.service.advanceFile.tableName, { tid: id });
+
+                // 记录删除日志
+                await this.ctx.service.projectLog.addProjectLog(transaction, projectLogConst.type.tender, projectLogConst.status.delete, tenderMsg.name, id);
                 await transaction.commit();
                 return true;
             } catch (err) {

+ 64 - 0
app/view/setting/logs.ejs

@@ -0,0 +1,64 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main">
+            <div>
+                <div class="d-inline-block">
+                    <div class="dropdown">
+                        <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            <i class="fa fa-puzzle-piece"></i> <%- projectLog.type_list[settingType].name %>
+                        </button>
+                        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton" x-placement="bottom-start" style="position: absolute; transform: translate3d(0px, 26px, 0px); top: 0px; left: 0px; will-change: transform;">
+                            <% for (const type in projectLog.type_list) { %>
+                            <% if (parseInt(type) !== settingType) { %>
+                            <a class="dropdown-item" href="/setting/logs<% if (parseInt(type) !== 0) { %>/type/<%- type %><% } %>"><%- projectLog.type_list[type].name %></a>
+                            <% } %>
+                            <% } %>
+                        </div>
+                    </div>
+                </div>
+                <div class="d-inline-block">
+                    目前仅记录30天内日志
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <div class="m-3">
+                    <!--删除日志-->
+                    <table class="table table-hover table-bordered">
+                        <thead>
+                        <th>模块</th>
+                        <th>操作</th>
+                        <th>名称</th>
+                        <th>操作账号</th>
+                        <th>操作时间</th>
+                        <th>系统</th>
+                        <th>浏览器</th>
+                        <th>登录地址</th>
+                        </thead>
+                        <% for (const log of logs) { %>
+                        <tr>
+                            <td><%- projectLog.type_list[log.type].name %></td>
+                            <td><%- projectLog.status_list[log.status] %></td>
+                            <td><%- log.msg %><span class="badge badge-pill badge-secondary float-right" title="id"><%- log.tid %></span></td>
+                            <td><%- log.username %><% if (log.mobile) { %>(<%- log.mobile %>)<% } %></td>
+                            <td><%- moment(log.create_time).format('YYYY-MM-DD HH:mm:ss') %></td>
+                            <td><%- log.os %></td>
+                            <td><%- log.browser %></td>
+                            <td><%- log.address %></td>
+                        </tr>
+                        <% } %>
+                    </table>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    $(function () {
+        autoFlashHeight();
+    })
+</script>

+ 6 - 0
config/menu.js

@@ -273,6 +273,12 @@ const settingMenu = {
         url: '/setting/category',
         caption: '标段自定义类别',
     },
+    log: {
+        name: '操作日志',
+        display: true,
+        url: '/setting/logs',
+        caption: '操作日志',
+    },
 };
 
 const profileMenu = {