Browse Source

个人证书信息和标段从业人员功能

ellisran 1 year ago
parent
commit
76b00f00d5

+ 62 - 0
app/const/profile.js

@@ -0,0 +1,62 @@
+'use strict';
+
+/**
+ * 个人信息通用 相关常量
+ *
+ * @author ELlisran
+ * @date 2024/03/26
+ * @version
+ */
+// 执业注册
+const cert_reg = [
+    { name: '一级注册建筑师', value: 1, sort: 1 },
+    { name: '二级注册建筑师', value: 2, sort: 2 },
+    { name: '一级注册结构工程师', value: 3, sort: 3 },
+    { name: '二级注册结构工程师', value: 4, sort: 4 },
+    { name: '注册监理工程师', value: 5, sort: 5 },
+    { name: '一级注册造价工程师', value: 6, sort: 6 },
+    { name: '二级注册造价工程师', value: 7, sort: 7 },
+    { name: '一级注册建造师', value: 8, sort: 8 },
+    { name: '二级注册建造师', value: 9, sort: 9 },
+    { name: '注册土木工程师', value: 10, sort: 10 },
+    { name: '注册安全工程师', value: 11, sort: 11 },
+    { name: '注册咨询工程师', value: 12, sort: 12 },
+    { name: '注册结构工程师', value: 13, sort: 13 },
+];
+// 执业资格
+const cert_qual = [
+    { name: '监理工程师', value: 1, sort: 1 },
+    { name: '造价工程师', value: 2, sort: 2 },
+    { name: '建造师', value: 3, sort: 3 },
+];
+
+const post_cert_const = ['reg', 'qual', 'code', 'reg_unit', 'job_title', 'file_name', 'file_path', 'edu_json'];
+const cert_const = {
+    reg: 'registration',
+    qual: 'qualification',
+    code: 'code',
+    reg_unit: 'reg_unit',
+    job_title: 'job_title',
+    file_name: 'file_name',
+    file_path: 'file_path',
+    edu_json: 'edu_json',
+};
+const edu_json = {
+    id: null,
+    date: null,
+    unit: null,
+    file_path: null,
+    file_name: null,
+};
+
+const cert = {
+    certReg: cert_reg,
+    certQual: cert_qual,
+    postCertConst: post_cert_const,
+    certConst: cert_const,
+    eduJsonConst: edu_json,
+};
+
+module.exports = {
+    cert,
+};

+ 118 - 0
app/controller/profile_controller.js

@@ -16,6 +16,7 @@ const path = require('path');
 const sendToWormhole = require('stream-wormhole');
 const loginWay = require('../const/setting').loginWay;
 const wxWork = require('../lib/wx_work');
+const profileConst = require('../const/profile');
 
 module.exports = app => {
 
@@ -83,6 +84,123 @@ module.exports = app => {
 
             ctx.redirect(ctx.request.header.referer);
         }
+        /**
+         * 账号资料页面
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async cert(ctx) {
+            // 获取当前用户数据
+            const sessionUser = ctx.session.sessionUser;
+
+            // 获取账号数据
+            const accountData = await ctx.service.projectAccount.getDataByCondition({ id: sessionUser.accountId });
+            const certList = await ctx.service.accountCert.getAllDataByCondition({ where: { uid: sessionUser.accountId }, orders: [['create_time', 'desc']] });
+            // json转换
+            certList.forEach(item => {
+                item.edu_json = item.edu_json ? JSON.parse(item.edu_json) : [];
+            });
+            const renderData = {
+                accountData,
+                certList,
+                fujianOssPath: ctx.app.config.fujianOssPath,
+                certRegConst: profileConst.cert.certReg,
+                certQualConst: profileConst.cert.certQual,
+                jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.profile.cert),
+            };
+            await this.layout('profile/cert.ejs', renderData, 'profile/cert_modal.ejs');
+        }
+
+        async certSave(ctx) {
+            const response = {
+                err: 0,
+                msg: '',
+                data: {},
+            };
+            try {
+                const sessionUser = ctx.session.sessionUser;
+                const data = JSON.parse(ctx.request.body.data);
+                switch (data.type) {
+                    case 'add_cert':
+                        response.data = await ctx.service.accountCert.addCert(sessionUser.accountId);
+                        break;
+                    case 'update_cert':
+                        response.data = await ctx.service.accountCert.updateCert(data.update_data);
+                        break;
+                    case 'add_jx':
+                        response.data = await ctx.service.accountCert.addEduJson(data.id);
+                        break;
+                    case 'update_jx':
+                        response.data = await ctx.service.accountCert.updateEduJson(data.update_data);
+                        break;
+                    case 'del_cert': // 包括删除附件
+                        response.data = await ctx.service.accountCert.delCert(data.delete_data);
+                        break;
+                    default:throw '参数有误';
+                }
+            } catch (error) {
+                response.err = 1;
+                response.msg = error.toString();
+            }
+            ctx.body = response;
+        }
+        /**
+         * 上传证书(单选)
+         *
+         * @param {object} ctx - egg全局变量
+         * @return {void}
+         */
+        async certUpload(ctx) {
+            const responseData = {
+                err: 0, msg: '', data: null,
+            };
+            try {
+                const stream = await ctx.getFileStream();
+                const create_time = Date.parse(new Date()) / 1000;
+                const fileInfo = path.parse(stream.filename);
+                const id = stream.fields && stream.fields.id ? stream.fields.id : 0;
+                if (!id) throw '参数有误';
+                let jxid = '';
+                let type = '';
+                if (stream.fields && stream.fields.type === 'upload_jx') {
+                    jxid = stream.fields.jxid ? stream.fields.jxid : '';
+                    if (!jxid) throw '参数有误';
+                    type = 'jx';
+                } else if (stream.fields && stream.fields.type === 'upload_cert') {
+                    type = 'cert';
+                }
+                if (!type) throw '参数有误';
+                // 判断用户是否选择上传文件
+                if (!stream.filename) {
+                    throw '请选择上传的文件!';
+                }
+                const filename = stream.filename;
+                const filepath = `app/public/upload/profile/${ctx.session.sessionUser.accountId}/cert/zhengshu_${create_time + fileInfo.ext}`;
+                // await ctx.helper.saveStreamFile(stream, path.resolve(this.app.baseDir, filepath));
+                await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
+                await sendToWormhole(stream);
+                if (type === 'jx') {
+                    const info = await ctx.service.accountCert.getDataById(id);
+                    if (!info) throw '数据有误';
+                    const edu_json = info.edu_json ? JSON.parse(info.edu_json) : [];
+                    const jxInfo = edu_json.find(item => item.id === jxid);
+                    if (!jxInfo) throw '数据有误';
+                    jxInfo.file_path = filepath;
+                    jxInfo.file_name = filename;
+                    await ctx.service.accountCert.update({ edu_json: JSON.stringify(edu_json) }, { id });
+                } else {
+                    await ctx.service.accountCert.update({ file_path: filepath, file_name: filename }, { id });
+                }
+                responseData.data = { file_path: filepath, file_name: filename };
+            } catch (err) {
+                console.log(err);
+                responseData.err = 1;
+                responseData.msg = err;
+            }
+            ctx.body = responseData;
+        }
+
 
         /**
          * 修改密码操作

+ 55 - 0
app/controller/tender_controller.js

@@ -26,6 +26,7 @@ const tenderInfoModel = require('../lib/tender_info');
 const mapConst = require('../const/map');
 const advanceConst = require('../const/advance');
 const projectSetting = require('../const/project_setting');
+const profileConst = require('../const/profile');
 
 module.exports = app => {
 
@@ -1082,6 +1083,60 @@ module.exports = app => {
             }
         }
 
+        async certSet(ctx) {
+            try {
+                const allCertList = await ctx.service.accountCert.getAllDataByCondition({ where: { pid: ctx.session.sessionProject.id }, orders: [['create_time', 'desc']] });
+                const tenderCertList = await ctx.service.tenderCert.getListByTid(ctx.tender.id, allCertList);
+                const renderData = {
+                    allCertList,
+                    tenderCertList,
+                    certRegConst: profileConst.cert.certReg,
+                    fujianOssPath: ctx.app.config.fujianOssPath,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.tender.cert),
+                };
+                const accountList = await ctx.service.projectAccount.getAllDataByCondition({
+                    where: { project_id: ctx.session.sessionProject.id, enable: 1, id: ctx.helper._.uniq(ctx.helper._.map(allCertList, 'uid')) },
+                    columns: ['id', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group', 'mobile'],
+                });
+                renderData.accountList = accountList;
+                const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: ctx.session.sessionProject.id } });
+                renderData.accountGroup = unitList.map(item => {
+                    const groupList = accountList.filter(item1 => item1.company === item.name);
+                    return { groupName: item.name, groupList };
+                });
+                await this.layout('tender/cert.ejs', renderData, 'tender/cert_modal.ejs');
+            } catch (err) {
+                this.log(err);
+                ctx.redirect('/tender/' + ctx.tender.id);
+            }
+        }
+
+        async saveCert(ctx) {
+            const response = { err: 0, msg: '', data: {} };
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data) {
+                    throw '提交数据错误';
+                }
+                switch (data.type) {
+                    case 'save_user':
+                        response.data = await ctx.service.tenderCert.saveUserCert(ctx.tender.id, data.list);
+                        break;
+                    case 'update_cert':
+                        response.data = await ctx.service.tenderCert.updateOneCert(data.update_data);
+                        break;
+                    case 'paste_cert':
+                        response.data = await ctx.service.tenderCert.updateMoreCert(ctx.tender.id, data.update_data);
+                        break;
+                    default:break;
+                }
+                ctx.body = response;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: {} };
+            }
+        }
+
         /**
          * 游客账号设置
          * @param {object} ctx - 上下文

+ 4 - 0
app/extend/helper.js

@@ -1729,4 +1729,8 @@ module.exports = {
         //     endDateTime.getMinutes().toString().padStart(2, '0');
         return endDateTime;
     },
+    showCol4ObjArray(objArray, colName, key = 'value', showKey = 'name') {
+        const info = _.find(objArray, { [key]: colName });
+        return info ? info[showKey] : '';
+    },
 };

+ 378 - 0
app/public/js/profile_cert.js

@@ -0,0 +1,378 @@
+/**
+ * 账号相关js
+ *
+ * @author CaiAoLin
+ * @date 2018/1/26
+ * @version
+ */
+$(document).ready(function() {
+    autoFlashHeight();
+
+    $('#addCert').click(function() {
+        postData('/profile/cert/save', { type: 'add_cert' }, function(result) {
+            console.log(result);
+            const html = insertCertHtml(result.total, result.data);
+            $('#certs').prepend(html);
+        });
+    });
+
+    $('body').on('click', '#certs .add-jx-btn', function() {
+        const _self = $(this);
+        postData('/profile/cert/save', { type: 'add_jx', id: $(this).parents('.one-cert').data('cid') }, function(result) {
+            console.log(result);
+            const html = insertJxHtml(result.data, result.jxData);
+            _self.parents('.pull-right').siblings('.all-jx').prepend(html);
+            console.log(_self.parents('.pull-right').siblings('.all-jx').children('.one-jx').eq(0).find('.jx-date'));
+            _self.parents('.pull-right').siblings('.all-jx').children('.one-jx').eq(0).find('.jx-date').datepicker({
+                onShow: function (res) {
+                    res.$el.attr('readOnly', true);
+                },
+                onHide: function (res, animationCompleted) {
+                    if(animationCompleted) {
+                        res.$el.attr('readOnly', false);
+                        const val = res.$el.val();
+                        const oldVal = res.$el.attr('data-old-date') || '';
+                        // 日期格式判断
+                        if (val !== '' && !(isNaN(val) && !isNaN(Date.parse(val)))) {
+                            toastr.error('日期格式有误!');
+                            res.$el.val(oldVal);
+                            if (oldVal === '') {
+                                res.clear();
+                            } else {
+                                res.selectDate(new Date(oldVal));
+                            }
+                        } else if(val !== oldVal) {
+                            const data = {
+                                id: res.$el.parents('.one-cert').data('cid'),
+                                jxid: res.$el.parents('.one-jx').data('jxid'),
+                                key: 'date',
+                                value: val,
+                            };
+                            updateJxDataFun(data);
+                            res.$el.attr('data-old-date', val);
+                        }
+                    }
+                }
+            });
+        });
+    });
+
+    $('body').on('change', '#certs select', function() {
+        const value = parseInt($(this).val());
+        const key = $(this).data('type');
+        const update_data = {
+            id: $(this).parents('.one-cert').data('cid'),
+            key,
+            value,
+        };
+        postData('/profile/cert/save', { type: 'update_cert', update_data }, function(result) {
+            console.log(result);
+        });
+    });
+
+    $('body').on('change', '#certs .one-cert-msg input[type="text"]', function() {
+        const value = $(this).val();
+        const key = $(this).data('type');
+        const update_data = {
+            id: $(this).parents('.one-cert').data('cid'),
+            key,
+            value,
+        };
+        console.log(update_data);
+        postData('/profile/cert/save', { type: 'update_cert', update_data }, function(result) {
+            console.log(result);
+        });
+    });
+
+    $('body').on('change', '#certs .one-jx input[type="text"]', function() {
+        const value = $(this).val();
+        const key = $(this).data('type');
+        const update_data = {
+            id: $(this).parents('.one-cert').data('cid'),
+            jxid: $(this).parents('.one-jx').data('jxid'),
+            key,
+            value,
+        };
+        console.log(update_data);
+        updateJxDataFun(update_data);
+    });
+    // 设置默认值
+    $('.jx-date').each(function() {
+        const defaultValue = $(this).val(); // 获取当前元素的 value 属性作为默认值
+        if (defaultValue) {
+            // 初始化日期选择器,并设置默认值
+            $(this).datepicker().data('datepicker').selectDate(new Date(defaultValue));
+        }
+    });
+    $('body .jx-date').datepicker({
+        onShow: function (res) {
+            res.$el.attr('readOnly', true);
+        },
+        onHide: function (res, animationCompleted) {
+            if(animationCompleted) {
+                res.$el.attr('readOnly', false);
+                const val = res.$el.val();
+                const oldVal = res.$el.attr('data-old-date') || '';
+                // 日期格式判断
+                if (val !== '' && !(isNaN(val) && !isNaN(Date.parse(val)))) {
+                    toastr.error('日期格式有误!');
+                    res.$el.val(oldVal);
+                    if (oldVal === '') {
+                        res.clear();
+                    } else {
+                        res.selectDate(new Date(oldVal));
+                    }
+                } else if(val !== oldVal) {
+                    const data = {
+                        id: res.$el.parents('.one-cert').data('cid'),
+                        jxid: res.$el.parents('.one-jx').data('jxid'),
+                        key: 'date',
+                        value: val,
+                    };
+                    updateJxDataFun(data);
+                    res.$el.attr('data-old-date', val);
+                }
+            }
+        }
+    });
+
+    $('body').on('click', '#certs .del-cert-btn', function() {
+        $('#delete-cert-cid').val($(this).attr('data-cid'));
+        $('#delete-cert-jxid').val($(this).attr('data-jxid'));
+        if ($(this).attr('data-filename')) {
+            console.log($(this).attr('data-filename'), $(this).attr('data-jxid'));
+            const txt = $(this).attr('data-jxid') ? '培训证明:' : '证书附件:';
+            $('#delete-cert-title').text(txt + $(this).attr('data-filename'));
+            $('#delete-cert-type').val('file');
+        } else {
+            $('#delete-cert-title').text($(this).parents('.modal-header').find('b').text());
+            $('#delete-cert-type').val('cert');
+        }
+    });
+
+    $('#delete-cert-btn').click(function() {
+        const data = {
+            id: $('#delete-cert-cid').val(),
+            jxid: $('#delete-cert-jxid').val(),
+            type: $('#delete-cert-type').val(),
+        };
+        console.log(data);
+        const _self = $(this);
+        postData('/profile/cert/save', { type: 'del_cert', delete_data: data }, function(result) {
+            _self.parents('.modal').modal('hide');
+            if (data.type === 'file') {
+                if (data.jxid) {
+                    $(`.one-cert[data-cid="${data.id}"] .one-jx[data-jxid="${data.jxid}"]`).find('.file-show').html('<input type="file" class="jx-file-upload">');
+                } else {
+                    $(`.one-cert[data-cid="${data.id}"]`).find('.one-cert-msg').find('.file-show').html('<input type="file" class="cert-file-upload">');
+                }
+            } else {
+                if (data.jxid) {
+                    $(`.one-cert[data-cid="${data.id}"] .one-jx[data-jxid="${data.jxid}"]`).remove();
+                    // 教育信息重新排序
+                    for (let i = 0; i < $(`.one-cert[data-cid="${data.id}"] .one-jx`).length; i++) {
+                        $(`.one-cert[data-cid="${data.id}"] .one-jx`).eq(i).find('.jx-num').text($(`.one-cert[data-cid="${data.id}"] .one-jx`).length - i);
+                    }
+                } else {
+                    $(`.one-cert[data-cid="${data.id}"]`).remove();
+                    // 证书信息重新排序
+                    for (let i = 0; i < $(`#certs .one-cert`).length; i++) {
+                        $(`#certs .one-cert`).eq(i).find('.cert-num').text($(`#certs .one-cert`).length - i);
+                    }
+                }
+            }
+        });
+    });
+
+    // 上传证书附件
+    $('body').on('change', '#certs .cert-file-upload', function () {
+        const file = this.files[0];
+        const formData = new FormData();
+        if (file === undefined) {
+            toastr.error('未选择上传文件!');
+            $(this).val('');
+            return false;
+        }
+        const ext = file.name.toLowerCase().split('.').splice(-1)[0];
+        const imgStr = /(jpg|jpeg|png|bmp|BMP|JPG|PNG|JPEG|pdf|PDF)$/;
+        if (!imgStr.test(ext)) {
+            toastr.error('请上传正确的图片或pdf格式文件');
+            $(this).val('');
+            return
+        }
+        const filesize = file.size;
+        if (filesize > 30 * 1024 * 1024) {
+            toastr.error('上传的文件大小不能超过30MB!');
+            $(this).val('');
+            return false;
+        }
+        const id = $(this).parents('.one-cert').data('cid');
+        formData.append('type', 'upload_cert');
+        formData.append('id', id);
+        formData.append('file', file);
+        console.log(formData);
+        $(this).val('');
+        const _self = $(this).parents('.file-show');
+        postDataWithFile('/profile/cert/upload', formData, function (result) {
+            _self.html(`<div class="col-form-label">
+                                                                <a href="${fujianOssPath + result.file_path}" target="_blank">${result.file_name}</a> &nbsp;<a href="#del-cert" data-cid="${id}" data-jxid="" data-filename="${result.file_name}" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                            </div>`);
+        });
+    });
+
+    // 上传证书附件
+    $('body').on('change', '#certs .jx-file-upload', function () {
+        const file = this.files[0];
+        const formData = new FormData();
+        if (file === undefined) {
+            toastr.error('未选择上传文件!');
+            $(this).val('');
+            return false;
+        }
+        const ext = file.name.toLowerCase().split('.').splice(-1)[0];
+        const imgStr = /(jpg|jpeg|png|bmp|BMP|JPG|PNG|JPEG|pdf|PDF)$/;
+        if (!imgStr.test(ext)) {
+            toastr.error('请上传正确的图片或pdf格式文件');
+            $(this).val('');
+            return
+        }
+        const filesize = file.size;
+        if (filesize > 30 * 1024 * 1024) {
+            toastr.error('上传的文件大小不能超过30MB!');
+            $(this).val('');
+            return false;
+        }
+        const id = $(this).parents('.one-cert').data('cid');
+        const jxid = $(this).parents('.one-jx').data('jxid');
+        formData.append('type', 'upload_jx');
+        formData.append('id', id);
+        formData.append('jxid', jxid);
+        formData.append('file', file);
+        console.log(formData);
+        $(this).val('');
+        const _self = $(this).parents('.file-show');
+        postDataWithFile('/profile/cert/upload', formData, function (result) {
+            _self.html(`<div class="col-form-label">
+                                                                <a href="${fujianOssPath + result.file_path}" target="_blank">${result.file_name}</a> &nbsp;<a href="#del-cert" data-cid="${id}" data-jxid="${jxid}" data-filename="${result.file_name}" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                            </div>`);
+        });
+    })
+
+    function updateJxDataFun(data) {
+        console.log(data);
+        postData('/profile/cert/save', { type: 'update_jx', update_data: data }, function(result) {
+            // console.log(result);
+        });
+    }
+
+    function insertJxHtml(data, jxdata) {
+        const html = `
+            <div class="one-jx" data-jxid="${jxdata.id}">
+                                                        <div class="card mt-3">
+                                                            <div class="modal-header">
+                                                                <b>继续教育<span class="jx-num">${data.edu_json.length}</span></b>
+                                                                <div class="pull-right">
+                                                                    <a href="#del-cert" data-cid="${data.id}" data-jxid="${jxdata.id}" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                                </div>
+                                                            </div>
+                                                            <div class="card-body">
+                                                                <div class="form-group row">
+                                                                    <label for="uname" class="ml-3 col-form-label">培训时间:</label>
+                                                                    <div class="col-sm-10">
+                                                                        <input data-language="zh" data-old-date="${jxdata.date}" data-type="date" placeholder="请选择时间" type="text" data-date-format="yyyy-MM-dd" class="jx-date datepicker-here form-control form-control-sm" value="${jxdata.date ? jxdata.date : ''}">
+                                                                    </div>
+                                                                </div>
+                                                                <div class="form-group row">
+                                                                    <label for="uname" class="ml-3 col-form-label">培训单位:</label>
+                                                                    <div class="col-sm-10">
+                                                                        <input type="text" class="form-control form-control-sm" data-type="unit" value="${jxdata.unit ? jxdata.unit : ''}">
+                                                                    </div>
+                                                                </div>
+                                                                <div class="form-group row">
+                                                                    <label for="uname" class="ml-3 col-form-label">培训证明:</label>
+                                                                    <div class="col-sm-10 file-show">
+                                                                        <input type="file" class="jx-file-upload">
+                                                                    </div>
+                                                                </div>
+                                                            </div>
+                                                        </div>
+                                                    </div>`;
+        return html;
+    }
+
+    function insertCertHtml(i, data) {
+        let regHtml = ``;
+        for (const r of certRegConst) {
+            regHtml += `<option value="${r.value}">${r.name}</option>`;
+        }
+        let qualHtml = ``;
+        for (const q of certQualConst) {
+            qualHtml += `<option value="${q.value}">${q.name}</option>`;
+        }
+        const html = `<div class="col-6 mt-3 one-cert" data-cid="${data.id}">
+                                        <div class="card">
+                                            <div class="modal-header">
+                                                <b>证书信息<span class="cert-num">${i}</span></b>
+                                                <div class="pull-right">
+                                                    <a href="#del-cert" data-cid="${data.id}" data-jxid="" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                </div>
+                                            </div>
+                                            <div class="card-body">
+                                                <b>持证情况:</b>
+                                                <div class="m-3 one-cert-msg">
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">证件名称:</label>
+                                                        <div class="row col-sm-10 pr-0">
+                                                            <div class="col-6 pr-0">
+                                                                <select class="form-control form-control-sm select—cert-reg" data-type="reg">
+                                                                    <option value="0">请选择</option>
+                                                                    ${regHtml}
+                                                                </select>
+                                                            </div>
+                                                            <div class="col-6 pr-0">
+                                                                <select class="form-control form-control-sm select—cert-qual" data-type="qual">
+                                                                    <option value="0">请选择</option>
+                                                                    ${qualHtml}
+                                                                </select>
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">证件编号:</label>
+                                                        <div class="col-sm-10">
+                                                            <input type="text" class="form-control form-control-sm" data-type="code" value="${data.code ? data.code : ''}">
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">注册单位:</label>
+                                                        <div class="col-sm-10">
+                                                            <input type="text" class="form-control form-control-sm" data-type="reg_unit" value="${data.reg_unit ? data.reg_unit : ''}">
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">技术职称:</label>
+                                                        <div class="col-sm-10">
+                                                            <input type="text" class="form-control form-control-sm" data-type="job_title" value="${data.job_title ? data.job_title : ''}">
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">证书附件:</label>
+                                                        <div class="col-sm-10 file-show">
+                                                            <input type="file" class="cert-file-upload">
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                                <div>
+                                                    <b>继续教育情况:</b>
+                                                    <div class="pull-right">
+                                                        <a href="javascript:void(0);" class="add-jx-btn">+添加</a>
+                                                    </div>
+                                                    <div class="all-jx">
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </div>
+                </div>`;
+        return html;
+    }
+});

+ 14 - 1
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -1916,7 +1916,20 @@ const SpreadJsObj = {
                     }
                     // Drawing Text
                     spreadNS.CellTypes.Text.prototype.paint.apply(this, [canvas, value, x, y, w, h, style, options]);
-                } else {
+                } else if (style.hAlign === spreadNS.HorizontalAlign.center) {
+                    if (img) {
+                        if (style.backColor) {
+                            canvas.save();
+                            canvas.fillStyle = style.backColor;
+                            canvas.fillRect((x + x + w - indent - img.width) / 2, y, (indent + img.width + img.width) / 2, h);
+                            canvas.restore();
+                        }
+                        canvas.drawImage(img, (x + 10 + x + w - indent - img.width) / 2, y + (h - img.height) / 2);
+                        w = w - indent - img.width;
+                    }
+                    // Drawing Text
+                    spreadNS.CellTypes.Text.prototype.paint.apply(this, [canvas, value, x, y, w, h, style, options]);
+                } else  {
                     if (img) {
                         if (style.backColor) {
                             canvas.save();

+ 375 - 0
app/public/js/tender_cert.js

@@ -0,0 +1,375 @@
+$(function () {
+    autoFlashHeight();
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+            }
+            autoFlashHeight();
+        }
+    });
+
+    // 打开添加用户加载数据
+    $('#import').on('show.bs.modal', function (e) {
+        let html = '';
+        for (const tc of tenderCertList) {
+            let certHtml = '';
+            for (const c of tc.account_info.certs) {
+                certHtml += `<option value="${c.id}" ${c.id === tc.cert_id ? 'selected': ''}>${showCol4ObjArray(certRegConst, c.registration, 'value', 'name')}</option>`;
+            }
+            html += `<tr class="text-center" data-insert="0" data-id="${tc.id}" data-certid="${tc.cert_id}" data-remove="0">
+                        <td>${tc.account_info.name}</td>
+                        <td>${tc.account_info.role}</td>
+                        <td>
+                            <select class="form-control form-control-sm">
+                                ${certHtml}
+                            </select>
+                        </td>
+                        <td class="text-danger">移除</td>
+                    </tr>`;
+        }
+        $('#select-certs-table').html(html);
+    });
+
+    let timer = null
+    let oldSearchVal = null
+    $('#gr-search').bind('input propertychange', function (e) {
+        oldSearchVal = e.target.value
+        timer && clearTimeout(timer)
+        timer = setTimeout(() => {
+            const newVal = $('#gr-search').val()
+            let html = ''
+            if (newVal && newVal === oldSearchVal) {
+                accountList.filter(item => item && (item.name.indexOf(newVal) !== -1 || (item.mobile && item.mobile.indexOf(newVal) !== -1))).forEach(item => {
+                    html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                        <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                class="ml-auto">${item.mobile || ''}</span></p>
+                        <span class="text-muted">${item.role || ''}</span>
+                    </dd>`
+                })
+                $('.book-list').empty()
+                $('.book-list').append(html)
+            } else {
+                if (!$('.acc-btn').length) {
+                    accountGroup.forEach((group, idx) => {
+                        if (!group) return
+                        html += `<dt><a href="javascript: void(0);" class="acc-btn" data-groupid="${idx}" data-type="hide"><i class="fa fa-plus-square"></i>
+                        </a> ${group.groupName}</dt>
+                        <div class="dd-content" data-toggleid="${idx}">`
+                        group.groupList.forEach(item => {
+                            html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                                    <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                            class="ml-auto">${item.mobile || ''}</span></p>
+                                    <span class="text-muted">${item.role || ''}</span>
+                                </dd>`
+                        });
+                        html += '</div>'
+                    })
+                    $('.book-list').empty()
+                    $('.book-list').append(html)
+                }
+            }
+        }, 400);
+    });
+
+    // 添加到成员中
+    $('.book-list').on('click', 'dt', function () {
+        const idx = $(this).find('.acc-btn').attr('data-groupid')
+        const type = $(this).find('.acc-btn').attr('data-type')
+        if (type === 'hide') {
+            $(this).parent().find(`div[data-toggleid="${idx}"]`).show(() => {
+                $(this).children().find('i').removeClass('fa-plus-square').addClass('fa-minus-square-o')
+                $(this).find('.acc-btn').attr('data-type', 'show')
+
+            })
+        } else {
+            $(this).parent().find(`div[data-toggleid="${idx}"]`).hide(() => {
+                $(this).children().find('i').removeClass('fa-minus-square-o').addClass('fa-plus-square')
+                $(this).find('.acc-btn').attr('data-type', 'hide')
+            })
+        }
+        return false
+    });
+
+    // 添加到审批流程中
+    $('dl').on('click', 'dd', function () {
+        const auditorId = parseInt($(this).data('id'))
+        if (auditorId) {
+            console.log(auditorId);
+            const userInfo = _.find(accountList, { id: auditorId });
+            const certList = _.filter(allCertList, { uid: parseInt(auditorId) });
+            console.log(certList);
+            let certHtml = '';
+            for (const c of certList) {
+                certHtml += `<option value="${c.id}">${showCol4ObjArray(certRegConst, c.registration, 'value', 'name')}</option>`;
+            }
+            const html = `<tr class="text-center" data-insert="1" data-remove="0" data-uid="${userInfo.id}" data-certid="${certList.length > 0 ? certList[0].id : 0}">
+                        <td>${userInfo.name}</td>
+                        <td>${userInfo.role}</td>
+                        <td>
+                            <select class="form-control form-control-sm">
+                                ${certHtml}
+                            </select>
+                        </td>
+                        <td class="text-danger">移除</td>
+                    </tr>`;
+            $('#select-certs-table').append(html);
+        }
+    });
+
+    $('body').on('click', '#select-certs-table .text-danger', function () {
+        $(this).parent().addClass('bg-gray').attr('data-remove', 1);
+        $(this).siblings('td').find('select').prop('disabled', true);
+        $(this).removeClass('text-danger').text('已移除');
+    });
+
+    $('body').on('change', '#select-certs-table select', function () {
+        $(this).parents('tr').attr('data-certid', $(this).val());
+    });
+
+    $('#add_cert_btn').click(function () {
+        // 判断增删改
+        const insertList = [];
+        if ($('#select-certs-table tr[data-insert="1"][data-remove="0"]').length > 0) {
+            $('#select-certs-table tr[data-insert="1"][data-remove="0"]').each(function () {
+                insertList.push({
+                    uid: parseInt($(this).attr('data-uid')),
+                    cert_id: parseInt($(this).attr('data-certid'))
+                });
+            });
+        }
+        const removeList = [];
+        if ($('#select-certs-table tr[data-insert="0"][data-remove="1"]').length > 0) {
+            $('#select-certs-table tr[data-insert="0"][data-remove="1"]').each(function () {
+                removeList.push(parseInt($(this).attr('data-id')));
+            });
+        }
+        const updateList = [];
+        if ($('#select-certs-table tr[data-insert="0"][data-remove="0"]').length > 0) {
+            $('#select-certs-table tr[data-insert="0"][data-remove="0"]').each(function () {
+                const cert_id = parseInt($(this).attr('data-certid'))
+                const id = parseInt($(this).attr('data-id'));
+                const tcInfo = _.find(tenderCertList, { id });
+                if (tcInfo.cert_id !== cert_id) {
+                    updateList.push({
+                        id,
+                        cert_id
+                    });
+                }
+            });
+        }
+        console.log(insertList, removeList, updateList);
+        postData('/tender/' + tid + '/cert/save', { type: 'save_user', list: { insertList, removeList, updateList} }, function (result) {
+            tenderCertList = result;
+            SpreadJsObj.loadSheetData(certSpread.getActiveSheet(), SpreadJsObj.DataType.Data, tenderCertList);
+            $('#import').modal('hide');
+        });
+    });
+
+    // sjs展示
+    const certSpread = SpreadJsObj.createNewSpread($('#cert-spread')[0]);
+    const certSpreadSetting = {
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [25, 32],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+    };
+    const certSpreadSettingCols = [
+        {title: '姓名', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 80, formatter: '@', readOnly: true, getValue: 'getValue.name'},
+        {title: '技术职称', colSpan: '1', rowSpan: '2', field: 'job_title', hAlign: 0, width: 100, formatter: '@', readOnly: true, getValue: 'getValue.job_title'},
+        {title: '所在部门', colSpan: '1', rowSpan: '2', field: 'department', hAlign: 0, width: 100, formatter: '@'},
+        {title: '职务', colSpan: '1', rowSpan: '2', field: 'role', hAlign: 0, width: 80, formatter: '@', readOnly: true, getValue: 'getValue.role'},
+        {title: '在岗时间', colSpan: '1', rowSpan: '2', field: 'job_time', hAlign: 0, width: 150, formatter: '@'},
+        {title: '持证情况|证件名称', colSpan: '4|1', rowSpan: '1|1', field: 'cert_reg', hAlign: 0, width: 150, readOnly: true, getValue: 'getValue.cert_reg'},
+        {title: '|证书编号', colSpan: '|1', rowSpan: '|1', field: 'cert_code', hAlign: 0, width: 150, readOnly: true, getValue: 'getValue.cert_code'},
+        {title: '|注册单位', colSpan: '|1', rowSpan: '|1', field: 'reg_unit', hAlign: 0, width: 150, readOnly: true, getValue: 'getValue.reg_unit'},
+        {title: '|证书附件', colSpan: '|1', rowSpan: '|1', field: 'file_path', hAlign: 1, width: 55, readOnly: true, cellType: 'imageBtn',
+            normalImg: '#file_clip', hoverImg: '#file_clip_hover' , showImage: function (data) { return data && data.cert_info && data.cert_info.file_path; }},
+        {title: '继续教育情况|培训时间', colSpan: '3|1', rowSpan: '1|1', field: 'jx_date', hAlign: 0, width: 150, readOnly: true, getValue: 'getValue.jx_date'},
+        {title: '|培训单位', colSpan: '|1', rowSpan: '|1', field: 'jx_unit', hAlign: 0, width: 150, readOnly: true, getValue: 'getValue.jx_unit'},
+        {title: '|培训证明', colSpan: '|1', rowSpan: '|1', field: 'jx_path', hAlign: 1, width: 55, readOnly: true, cellType: 'imageBtn',
+            normalImg: '#file_clip', hoverImg: '#file_clip_hover', showImage: function (data) { return data && data.cert_info && data.cert_info.eduInfo && data.cert_info.eduInfo.file_path; }},
+        {title: '备注', colSpan: '1', rowSpan: '2', field: 'remark', hAlign: 0, width: 100},
+    ];
+    certSpreadSetting.cols = certSpreadSettingCols;
+
+    certSpreadSetting.imageClick = function (data, info) {
+        const col = info.sheet.zh_setting.cols[info.col];
+        if (col.field === 'file_path' && data && data.cert_info && data.cert_info.file_path) {
+            window.open(fujianOssPath + data.cert_info.file_path);
+        } else if (col.field === 'jx_path' && data && data.cert_info && data.cert_info.eduInfo && data.cert_info.eduInfo.file_path) {
+            window.open(fujianOssPath + data.cert_info.eduInfo.file_path);
+        }
+    };
+
+    const certCol = {
+        getValue: {
+            name: function (data) {
+                return data.account_info ? data.account_info.name : '';
+            },
+            job_title: function (data) {
+                return data.cert_info ? data.cert_info.job_title : '';
+            },
+            role: function (data) {
+                return data.account_info ? data.account_info.role : '';
+            },
+            cert_reg: function (data) {
+                return data.cert_info ? showCol4ObjArray(certRegConst, data.cert_info.registration, 'value', 'name') : '';
+            },
+            cert_code: function (data) {
+                return data.cert_info ? data.cert_info.code : '';
+            },
+            reg_unit: function (data) {
+                return data.cert_info ? data.cert_info.reg_unit : '';
+            },
+            file_path: function (data) {
+                // return data.cert_info ? fujianOssPath + data.cert_info.file_path : '';
+            },
+            jx_date: function (data) {
+                return data.cert_info && data.cert_info.eduInfo ? data.cert_info.eduInfo.date : '';
+            },
+            jx_unit: function (data) {
+                return data.cert_info && data.cert_info.eduInfo ? data.cert_info.eduInfo.unit : '';
+            },
+            jx_path: function (data) {
+                // return data.cert_info && data.cert_info.eduInfo ? fujianOssPath + data.cert_info.eduInfo.file_path : '';
+            }
+        },
+        readOnly: {
+        },
+    };
+
+    SpreadJsObj.initSpreadSettingEvents(certSpreadSetting, certCol);
+    SpreadJsObj.initSheet(certSpread.getActiveSheet(), certSpreadSetting);
+    SpreadJsObj.loadSheetData(certSpread.getActiveSheet(), SpreadJsObj.DataType.Data, tenderCertList);
+
+    const certSpreadObj = {
+        certSheetReset: function (redo = false) {
+
+            const newCertData = _.cloneDeep(tenderCertList);
+            if (redo) {
+                certSpread.getActiveSheet().reset();
+                SpreadJsObj.initSpreadSettingEvents(certSpreadSetting, certCol);
+                SpreadJsObj.initSheet(certSpread.getActiveSheet(), certSpreadSetting);
+            }
+            SpreadJsObj.loadSheetData(certSpread.getActiveSheet(), SpreadJsObj.DataType.Data, newCertData);
+        },
+        editEnded: function (e, info) {
+            if (info.sheet.zh_setting) {
+                const select = SpreadJsObj.getSelectObject(info.sheet);
+                const col = info.sheet.zh_setting.cols[info.col];
+                // 未改变值则不提交
+                let validText = col.type === 'Number' && is_numeric(info.editingText) ? parseFloat(info.editingText) : (info.editingText ? trimInvalidChar(info.editingText) : null);
+                const orgValue = select[col.field];
+                if (orgValue == validText || ((!orgValue || orgValue === '') && (validText === ''))) {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    return;
+                }
+                const update_data = {
+                    id: select.id,
+                }
+                update_data[col.field] = validText;
+                select[col.field] = validText;
+                // delete select.waitingLoading;
+
+                console.log(select);
+
+                // 更新至服务器
+                postData('/tender/' + tid + '/cert/save', { type: 'update_cert', update_data }, function (result) {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                }, function () {
+                    select[col.field] = orgValue;
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                });
+            }
+        },
+        deletePress: function (sheet) {
+            return;
+        },
+        clipboardPasted(e, info) {
+            const hint = {
+                cellError: {type: 'error', msg: '粘贴内容超出了表格范围'},
+            };
+            const range = info.cellRange;
+            const sortData = info.sheet.zh_data || [];
+            if (info.cellRange.row + info.cellRange.rowCount > sortData.length) {
+                toastMessageUniq(hint.cellError);
+                // SpreadJsObj.loadSheetData(materialSpread.getActiveSheet(), SpreadJsObj.DataType.Data, materialBillsData);
+                SpreadJsObj.reLoadSheetHeader(certSpread.getActiveSheet());
+                SpreadJsObj.reLoadSheetData(certSpread.getActiveSheet());
+                return;
+            }
+            if (sortData.length > 0 && range.col + range.colCount > 13) {
+                toastMessageUniq(hint.cellError);
+                SpreadJsObj.reLoadSheetHeader(certSpread.getActiveSheet());
+                SpreadJsObj.reLoadSheetData(certSpread.getActiveSheet());
+                return;
+            }
+            const data = [];
+            // const rowData = [];
+            for (let iRow = 0; iRow < range.rowCount; iRow++) {
+                let bPaste = true;
+                const curRow = range.row + iRow;
+                // const materialData = JSON.parse(JSON.stringify(sortData[curRow]));
+                const certData = { id: sortData[curRow].id };
+                const hintRow = range.rowCount > 1 ? curRow : '';
+                let sameCol = 0;
+                for (let iCol = 0; iCol < range.colCount; iCol++) {
+                    const curCol = range.col + iCol;
+                    const colSetting = info.sheet.zh_setting.cols[curCol];
+                    if (!colSetting) continue;
+
+                    let validText = info.sheet.getText(curRow, curCol);
+                    validText = colSetting.type === 'Number' && is_numeric(validText) ? parseFloat(validText) : (validText ? trimInvalidChar(validText) : null);
+                    const orgValue = sortData[curRow][colSetting.field];
+                    if (orgValue == validText || ((!orgValue || orgValue === '') && (validText === ''))) {
+                        sameCol++;
+                        if (range.colCount === sameCol)  {
+                            bPaste = false;
+                        }
+                        continue;
+                    }
+                    certData[colSetting.field] = validText;
+                    sortData[curRow][colSetting.field] = validText;
+                }
+                if (bPaste) {
+                    data.push(certData);
+                } else {
+                    SpreadJsObj.reLoadRowData(info.sheet, curRow);
+                }
+            }
+            if (data.length === 0) {
+                SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                return;
+            }
+            console.log(data);
+            // 更新至服务器
+            postData('/tender/' + tid + '/cert/save', { type: 'paste_cert', update_data: data }, function (result) {
+                tenderCertList = result;
+                certSpreadObj.certSheetReset();
+            }, function () {
+                SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                return;
+            });
+        },
+    }
+
+    certSpread.bind(spreadNS.Events.ClipboardPasted, certSpreadObj.clipboardPasted);
+    SpreadJsObj.addDeleteBind(certSpread, certSpreadObj.deletePress);
+    certSpread.bind(spreadNS.Events.EditEnded, certSpreadObj.editEnded);
+
+    function showCol4ObjArray(arr, col, key, showKey) {
+        const obj = _.find(arr, { [key]: col });
+        return obj ? obj[showKey] : '';
+    }
+});

+ 5 - 0
app/router.js

@@ -192,6 +192,8 @@ module.exports = app => {
     app.post('/tender/:id/map/upload', sessionAuth, tenderCheck, uncheckTenderCheck, 'tenderController.uploadMap');
     app.post('/tender/:id/load', sessionAuth, tenderCheck, 'tenderController.loadData');
     app.post('/tender/:id/saveRela', sessionAuth, tenderCheck, 'tenderController.saveRelaData');
+    app.get('/tender/:id/cert', sessionAuth, tenderCheck, 'tenderController.certSet');
+    app.post('/tender/:id/cert/save', sessionAuth, tenderCheck, 'tenderController.saveCert');
 
     app.get('/tender/:id/ctrl-price', sessionAuth, tenderCheck, 'ctrlPriceController.index');
     app.post('/tender/:id/ctrl-price/load', sessionAuth, tenderCheck, 'ctrlPriceController.load');
@@ -654,6 +656,9 @@ module.exports = app => {
     app.post('/tender/:id/measure/material/gcl/load', sessionAuth, tenderCheck, uncheckTenderCheck, 'materialController.loadGclData');
     // 个人账号相关
     app.get('/profile/info', sessionAuth, 'profileController.info');
+    app.get('/profile/cert', sessionAuth, 'profileController.cert');
+    app.post('/profile/cert/save', sessionAuth, 'profileController.certSave');
+    app.post('/profile/cert/upload', sessionAuth, 'profileController.certUpload');
     app.get('/profile/sms', sessionAuth, 'profileController.sms');
     app.post('/profile/sms/type', sessionAuth, 'profileController.smsType');
     app.get('/profile/sign', sessionAuth, 'profileController.sign');

+ 142 - 0
app/service/account_cert.js

@@ -0,0 +1,142 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+const profileConst = require('../const/profile');
+module.exports = app => {
+
+    class AccountCert extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'account_cert';
+        }
+
+        async addCert(uid) {
+            const result = await this.db.insert(this.tableName, {
+                pid: this.ctx.session.sessionProject.id,
+                uid,
+                create_time: new Date(),
+            });
+            return { total: await this.count({ uid }), data: await this.getDataById(result.insertId) };
+        }
+
+        async updateCert(data) {
+            const updateData = {
+                id: data.id,
+            };
+            // 判断key是否在常量里,并转换为对应的值
+            if (!this._.includes(profileConst.cert.postCertConst, data.key)) throw '参数有误';
+            updateData[profileConst.cert.certConst[data.key]] = data.value ? data.value : null;
+            return await this.db.update(this.tableName, updateData);
+        }
+
+        async addEduJson(id) {
+            const info = await this.getDataById(id);
+            if (!info) throw '数据有误';
+            const edu_json = info.edu_json ? JSON.parse(info.edu_json) : [];
+            const newEdu = this._.cloneDeep(profileConst.cert.eduJsonConst);
+            newEdu.id = this.ctx.app.uuid.v4();
+            edu_json.push(newEdu);
+            info.edu_json = edu_json;
+            const result = await this.db.update(this.tableName, {
+                id: info.id,
+                edu_json: JSON.stringify(edu_json),
+            });
+            return { data: info, jxData: newEdu };
+        }
+
+        async updateEduJson(data) {
+            const info = await this.getDataById(data.id);
+            if (!info) throw '数据有误';
+            const edu_json = info.edu_json ? JSON.parse(info.edu_json) : [];
+            if (edu_json.length === 0) throw '数据有误';
+            const updateEdu = this._.find(edu_json, { id: data.jxid });
+            if (!updateEdu) throw '数据有误';
+            updateEdu[data.key] = data.value ? data.value : null;
+            return await this.db.update(this.tableName, {
+                id: info.id,
+                edu_json: JSON.stringify(edu_json),
+            });
+        }
+
+        async delCert(data) {
+            if (!data.id) throw '参数有误';
+            const info = await this.getDataById(data.id);
+            if (!info) throw '数据有误1';
+            if (!data.type) throw '参数有误';
+            if (data.type === 'cert') {
+                const edu_json = info.edu_json ? JSON.parse(info.edu_json) : [];
+                if (data.jxid) {
+                    // 删除继续教育
+                    if (edu_json.length === 0) throw '数据有误';
+                    const delEdu = this._.findIndex(edu_json, { id: data.jxid });
+                    if (delEdu === -1) throw '数据有误2';
+                    if (edu_json[delEdu].file_path) {
+                        await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + edu_json[delEdu].file_path);
+                    }
+                    edu_json.splice(delEdu, 1);
+                    // 如果存在文件,需同步移除
+                    return await this.db.update(this.tableName, {
+                        id: info.id,
+                        edu_json: JSON.stringify(edu_json),
+                    });
+                }
+                // 删除整个证书
+                // 如果存在文件,需同步移除
+                // 判断是否已调用,已调用需要先删除再删除这里
+                const isUsed = await this.ctx.service.tenderCert.count({ cert_id: info.id });
+                if (isUsed > 0) throw '该证书已被添加到标段从业人员,无法删除';
+                if (info.file_path) {
+                    await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + info.file_path);
+                    if (edu_json.length > 0) {
+                        for (const item of edu_json) {
+                            if (item.file_path) {
+                                await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + item.file_path);
+                            }
+                        }
+                    }
+                }
+                return await this.db.delete(this.tableName, { id: data.id });
+            } else if (data.type === 'file') {
+                if (data.jxid) {
+                    const edu_json = info.edu_json ? JSON.parse(info.edu_json) : [];
+                    if (edu_json.length === 0) throw '数据有误';
+                    const eduInfo = this._.find(edu_json, {id: data.jxid});
+                    if (!eduInfo) throw '数据有误2';
+                    // 如果存在文件,需同步移除
+                    if (!eduInfo.file_path) throw '不存在培训证明';
+                    await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + eduInfo.file_path);
+                    eduInfo.file_path = null;
+                    eduInfo.file_name = null;
+                    return await this.db.update(this.tableName, {
+                        id: info.id,
+                        edu_json: JSON.stringify(edu_json),
+                    });
+                }
+                // 删除证书附件
+                if (!info.file_path) throw '不存在证书附件';
+                await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + info.file_path);
+                return await this.db.update(this.tableName, {
+                    id: info.id,
+                    file_path: null,
+                    file_name: null,
+                });
+            }
+            throw '参数有误';
+        }
+    }
+
+    return AccountCert;
+};

+ 112 - 0
app/service/tender_cert.js

@@ -0,0 +1,112 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+const profileConst = require('../const/profile');
+module.exports = app => {
+
+    class TenderCert extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'tender_cert';
+        }
+
+        async getListByTid(tid, allCertList) {
+            const list = await this.getAllDataByCondition({ where: { tid } });
+            if (list.length > 0) {
+                const accountList = await this.ctx.service.projectAccount.getAllDataByCondition({ columns: ['id', 'account', 'name', 'role'], where: { id: this._.uniq(this._.map(list, 'uid')) } });
+                for (const l of list) {
+                    const acInfo = this._.find(allCertList, { id: l.cert_id });
+                    if (acInfo) {
+                        acInfo.edu_json = acInfo.edu_json ? JSON.parse(acInfo.edu_json) : null;
+                        acInfo.eduInfo = acInfo.edu_json && acInfo.edu_json.length > 0 ? acInfo.edu_json[acInfo.edu_json.length - 1] : null;
+                        l.cert_info = acInfo;
+                    }
+                    const accountInfo = this._.find(accountList, { id: l.uid });
+                    if (accountInfo) {
+                        const certs = this._.filter(allCertList, { uid: l.uid });
+                        l.account_info = accountInfo;
+                        l.account_info.certs = certs;
+                    }
+                }
+            }
+            return list;
+        }
+
+        async saveUserCert(tid, list) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                if (list.insertList.length > 0) {
+                    const insertList = [];
+                    for (const i of list.insertList) {
+                        insertList.push({
+                            uid: i.uid,
+                            cert_id: i.cert_id,
+                            tid,
+                            create_time: new Date(),
+                        });
+                    }
+                    await transaction.insert(this.tableName, insertList);
+                }
+                if (list.removeList.length > 0) {
+                    await transaction.delete(this.tableName, { id: list.removeList });
+                }
+                if (list.updateList.length > 0) {
+                    await transaction.updateRows(this.tableName, list.updateList);
+                }
+                await transaction.commit();
+                const allCertList = await this.ctx.service.accountCert.getAllDataByCondition({ where: { pid: this.ctx.session.sessionProject.id }, orders: [['create_time', 'desc']] });
+                return await this.getListByTid(tid, allCertList);
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        async updateOneCert(data) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const info = await transaction.get(this.tableName, { id: data.id });
+                if (!info) throw '数据已不存在';
+                await transaction.update(this.tableName, data);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        async updateMoreCert(tid, data) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const updateData = [];
+                for (const d of data) {
+                    const info = await transaction.get(this.tableName, { id: d.id });
+                    if (!info) throw '数据已不存在';
+                    updateData.push(d);
+                }
+                if (updateData.length > 0) await transaction.updateRows(this.tableName, updateData);
+                await transaction.commit();
+                const allCertList = await this.ctx.service.accountCert.getAllDataByCondition({ where: { pid: this.ctx.session.sessionProject.id }, orders: [['create_time', 'desc']] });
+                return await this.getListByTid(tid, allCertList);
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+    }
+
+    return TenderCert;
+};

+ 153 - 0
app/view/profile/cert.ejs

@@ -0,0 +1,153 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content" id="app">
+    <div class="panel-title">
+        <div class="title-main d-flex justify-content-between">
+            <div class="d-inline-block">
+                <div class="btn-group group-tab">
+                    <a class="btn btn-sm btn-light" href="/profile/info">
+                        账号资料
+                    </a>
+                    <a class="btn btn-sm btn-light active" href="javascript:void(0);">
+                        证书信息
+                    </a>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <div class="row m-0 mt-3">
+                    <div class="col-12">
+                        <a href="javascript:void(0);" id="addCert">+添加证书</a>
+                        <div class="row mb-3" id="certs">
+                            <% if (certList.length > 0) { %>
+                                <% for (const [i,c] of certList.entries()) { %>
+                                    <div class="col-6 mt-3 one-cert" data-cid="<%- c.id %>">
+                                        <div class="card">
+                                            <div class="modal-header">
+                                                <b>证书信息<span class="cert-num"><%- certList.length-i %></span></b>
+                                                <div class="pull-right">
+                                                    <a href="#del-cert" data-cid="<%- c.id %>" data-jxid="" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                </div>
+                                            </div>
+                                            <div class="card-body">
+                                                <b>持证情况:</b>
+                                                <div class="m-3 one-cert-msg">
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">证件名称:</label>
+                                                        <div class="row col-sm-10 pr-0">
+                                                            <div class="col-6 pr-0">
+                                                                <select class="form-control form-control-sm select—cert-reg" data-type="reg">
+                                                                    <option value="0">请选择</option>
+                                                                    <% for (const r of certRegConst) { %>
+                                                                    <option value="<%- r.value %>" <% if (c.registration === r.value) { %>selected<% } %>><%- r.name %></option>
+                                                                    <% } %>
+                                                                </select>
+                                                            </div>
+                                                            <div class="col-6 pr-0">
+                                                                <select class="form-control form-control-sm select—cert-qual" data-type="qual">
+                                                                    <option value="0">请选择</option>
+                                                                    <% for (const q of certQualConst) { %>
+                                                                        <option value="<%- q.value %>" <% if (c.qualification === q.value) { %>selected<% } %>><%- q.name %></option>
+                                                                    <% } %>
+                                                                </select>
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">证件编号:</label>
+                                                        <div class="col-sm-10">
+                                                            <input type="text" class="form-control form-control-sm" data-type="code" value="<%- c.code %>">
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">注册单位:</label>
+                                                        <div class="col-sm-10">
+                                                            <input type="text" class="form-control form-control-sm" data-type="reg_unit" value="<%- c.reg_unit %>">
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">技术职称:</label>
+                                                        <div class="col-sm-10">
+                                                            <input type="text" class="form-control form-control-sm" data-type="job_title" value="<%- c.job_title %>">
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">证书附件:</label>
+                                                        <div class="col-sm-10 file-show">
+                                                            <% if (c.file_path) { %>
+                                                            <div class="col-form-label">
+                                                                <a href="<%- fujianOssPath + c.file_path %>" target="_blank"><%- c.file_name %></a> &nbsp;<a href="#del-cert" data-cid="<%- c.id %>" data-jxid="" data-filename="<%- c.file_name %>" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                            </div>
+                                                            <% } else { %>
+                                                                <input type="file" class="cert-file-upload">
+                                                            <% } %>
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                                <div>
+                                                    <b>继续教育情况:</b>
+                                                    <div class="pull-right">
+                                                        <a href="javascript:void(0);" class="add-jx-btn">+添加</a>
+                                                    </div>
+                                                    <div class="all-jx">
+                                                        <% if (c.edu_json.length > 0) { %>
+                                                            <% for (const [j, e] of c.edu_json.reverse().entries()) { %>
+                                                                <div class="one-jx" data-jxid="<%- e.id %>">
+                                                                    <div class="card mt-3">
+                                                                        <div class="modal-header">
+                                                                            <b>继续教育<span class="jx-num"><%- c.edu_json.length-j %></span></b>
+                                                                            <div class="pull-right">
+                                                                                <a href="#del-cert" data-cid="<%- c.id %>" data-jxid="<%- e.id %>" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                                            </div>
+                                                                        </div>
+                                                                        <div class="card-body">
+                                                                            <div class="form-group row">
+                                                                                <label for="uname" class="ml-3 col-form-label">培训时间:</label>
+                                                                                <div class="col-sm-10">
+                                                                                    <input data-language="zh" data-old-date="<%- e.date %>" data-type="date" placeholder="请选择时间" type="text" data-date-format="yyyy-mm-dd" class="jx-date datepicker-here form-control form-control-sm" value="<%- e.date %>">
+                                                                                </div>
+                                                                            </div>
+                                                                            <div class="form-group row">
+                                                                                <label for="uname" class="ml-3 col-form-label">培训单位:</label>
+                                                                                <div class="col-sm-10">
+                                                                                    <input type="text" class="form-control form-control-sm" data-type="unit" value="<%- e.unit %>">
+                                                                                </div>
+                                                                            </div>
+                                                                            <div class="form-group row">
+                                                                                <label for="uname" class="ml-3 col-form-label">培训证明:</label>
+                                                                                <div class="col-sm-10 file-show">
+                                                                                    <% if (e.file_path) { %>
+                                                                                        <div class="col-form-label">
+                                                                                            <a href="<%- fujianOssPath + e.file_path %>" target="_blank"><%- e.file_name %></a> &nbsp;<a href="#del-cert" data-cid="<%- c.id %>" data-jxid="<%- e.id %>" data-filename="<%- e.file_name %>" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                                                        </div>
+                                                                                    <% } else { %>
+                                                                                        <input type="file" class="jx-file-upload">
+                                                                                    <% } %>
+                                                                                </div>
+                                                                            </div>
+                                                                        </div>
+                                                                    </div>
+                                                                </div>
+                                                            <% } %>
+                                                        <% } %>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                <% } %>
+                            <% } %>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const certRegConst = JSON.parse(unescape('<%- escape(JSON.stringify(certRegConst)) %>'));
+    const certQualConst = JSON.parse(unescape('<%- escape(JSON.stringify(certQualConst)) %>'));
+    const fujianOssPath = JSON.parse(unescape('<%- escape(JSON.stringify(fujianOssPath)) %>'));
+</script>

+ 22 - 0
app/view/profile/cert_modal.ejs

@@ -0,0 +1,22 @@
+<div class="modal fade" id="del-cert" >
+    <div class="modal-dialog" role="document">
+        <form class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">删除</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <h6>确认删除<span id="delete-cert-title"></span>?</h6>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" id="delete-cert-cid">
+                <input type="hidden" id="delete-cert-jxid">
+                <input type="hidden" id="delete-cert-type">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-sm btn-danger" data-dismiss="modal" id="delete-cert-btn">确定删除</button>
+            </div>
+        </form>
+    </div>
+</div>

+ 15 - 2
app/view/profile/info.ejs

@@ -1,13 +1,26 @@
 <% include ./sub_menu.ejs %>
 <div class="panel-content" id="app">
     <div class="panel-title">
-        <div class="title-main">
-            <h2>账号资料</h2>
+        <div class="title-main d-flex justify-content-between">
+            <div class="d-inline-block">
+                <div class="btn-group group-tab">
+                    <a class="btn btn-sm btn-light active" href="javascript:void(0);">
+                        账号资料
+                    </a>
+                    <a class="btn btn-sm btn-light" href="/profile/cert">
+                        证书信息
+                    </a>
+                </div>
+            </div>
         </div>
     </div>
     <div class="content-wrap">
         <div class="c-body">
             <div class="sjs-height-0">
+<!--                <nav class="nav nav-tabs m-3" role="tablist">-->
+<!--                    <a class="nav-item nav-link active" href="javascript:void(0);">账号资料</a>-->
+<!--                    <a class="nav-item nav-link" href="/profile/cert">证书信息</a>-->
+<!--                </nav>-->
                 <div class="row m-0">
                     <div class="col-5 my-3">
                         <!--账号资料-->

+ 36 - 0
app/view/tender/cert.ejs

@@ -0,0 +1,36 @@
+<% include ./tender_sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <!--工具栏-->
+        <div class="title-main d-flex justify-content-between">
+            <% include ./tender_sub_mini_menu.ejs %>
+            <!--工具栏-->
+            <div>
+                <div class="d-inline-block">
+                    <a class="btn btn-sm btn-primary" href="#addusers" data-toggle="modal" data-target="#import" >添加用户</a>
+                </div>
+            </div>
+            <div class="ml-auto">
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body col">
+            <div class="sjs-height-1" id="cert-spread">
+            </div>
+        </div>
+    </div>
+</div>
+<div style="display: none">
+    <img src="/public/images/file_clip.png" id="file_clip" />
+    <img src="/public/images/file_clip_hover.png" id="file_clip_hover" />
+</div>
+<script>
+    const tid = parseInt('<%- ctx.tender.id %>');
+    const accountGroup = JSON.parse(unescape('<%- escape(JSON.stringify(accountGroup)) %>'));
+    const accountList = JSON.parse(unescape('<%- escape(JSON.stringify(accountList)) %>'));
+    const fujianOssPath = JSON.parse(unescape('<%- escape(JSON.stringify(fujianOssPath)) %>'));
+    let tenderCertList = JSON.parse(unescape('<%- escape(JSON.stringify(tenderCertList)) %>'));
+    const allCertList = JSON.parse(unescape('<%- escape(JSON.stringify(allCertList)) %>'));
+    const certRegConst = JSON.parse(unescape('<%- escape(JSON.stringify(certRegConst)) %>'));
+</script>

+ 68 - 0
app/view/tender/cert_modal.ejs

@@ -0,0 +1,68 @@
+<!--导入-->
+<div class="modal fade" id="import" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">添加用户</h5>
+            </div>
+            <div class="modal-body pt-1">
+                <div class="d-flex flex-row bg-graye">
+                    <div class="p-2 dropdown">
+                        <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            添加用户
+                        </button>
+                        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton" style="width:220px">
+                            <div class="mb-2 p-2"><input class="form-control form-control-sm" placeholder="姓名/手机 检索" id="gr-search" autocomplete="off"></div>
+                            <dl class="list-unstyled book-list">
+                                <% accountGroup.forEach((group, idx) => { %>
+                                    <dt><a href="javascript: void(0);" class="acc-btn" data-groupid="<%- idx %>" data-type="hide"><i class="fa fa-plus-square"></i></a> <%- group.groupName %></dt>
+                                    <div class="dd-content" data-toggleid="<%- idx %>">
+                                        <% group.groupList.forEach(item => { %>
+                                            <dd class="border-bottom p-2 mb-0 " data-id="<%- item.id %>" >
+                                                <p class="mb-0 d-flex"><span class="text-primary"><%- item.name %></span><span
+                                                            class="ml-auto"><%- item.mobile %></span></p>
+                                                <span class="text-muted"><%- item.role %></span>
+                                            </dd>
+                                        <% });%>
+                                    </div>
+                                <% }) %>
+                            </dl>
+                        </div>
+                    </div>
+                </div>
+                <table class="table table-bordered">
+                    <thead>
+                    <tr>
+                        <th>用户名</th>
+                        <th>角色/职位</th>
+                        <th>选择证书</th>
+                        <th>操作</th>
+                    </tr>
+                    </thead>
+                    <tbody id="select-certs-table">
+                    <% for (const tc of tenderCertList) { %>
+                    <tr class="text-center">
+                        <td><%- tc.account_info.name %></td>
+                        <td><%- tc.account_info.role %></td>
+                        <td>
+                            <select class="form-control form-control-sm">
+                                <% for (const c of tc.account_info.certs) { %>
+                                <option value="<%- c.id %>" <% if (tc.cert_id === c.id) { %>selected<% } %>><%- ctx.helper.showCol4ObjArray(certRegConst, c.registration, 'value', 'name') %></option>
+                                <% } %>
+                            </select>
+                        </td>
+                        <td class="text-danger">移除</td>
+                    </tr>
+                    <% } %>
+                    </tbody>
+                </table>
+            </div>
+            <div class="modal-footer d-flex justify-content-between">
+                <div class="ml-auto">
+                    <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                    <button type="button" class="btn btn-sm btn-primary" id="add_cert_btn">添加</button>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

+ 1 - 0
app/view/tender/detail.ejs

@@ -55,6 +55,7 @@
                         <a href="#bd-set-6" data-toggle="modal" data-target="#bd-set-6" class="dropdown-item" >章节设置</a>
                         <a href="#bd-set-7" data-toggle="modal" data-target="#bd-set-7" class="dropdown-item" >付款账号</a>
                         <a class="dropdown-item" href="javascript: void(0);" id="copyBtn">拷贝设置</a>
+                        <a class="dropdown-item" href="/tender/<%- tender.id %>/cert">从业人员</a>
                     </div>
                 </div>
                 <% if (ctx.session.sessionUser.is_admin) { %>

+ 27 - 0
config/web.js

@@ -138,6 +138,21 @@ const JsFiles = {
                 ],
                 mergeFile: 'tender_shenpi',
             },
+            cert: {
+                files: ['/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js', '/public/js/decimal.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/shares/cs_tools.js',
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                    '/public/js/tender_cert.js',
+                ],
+                mergeFile: 'tender_cert',
+            },
             ctrlPrice: {
                 files: [
                     '/public/js/js-xlsx/xlsx.full.min.js',
@@ -1403,6 +1418,18 @@ const JsFiles = {
                 mergeFile: 'setting_user',
             },
         },
+        profile: {
+            cert: {
+                files: [
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                ],
+                mergeFiles: [
+                    '/public/js/profile_cert.js',
+                ],
+                mergeFile: 'profile_cert',
+            },
+        },
         settle: {
             list: {
                 files: [],

+ 27 - 0
sql/update.sql

@@ -48,3 +48,30 @@ INSERT INTO `zh_filing_template` (`id`, `temp_id`, `tree_pid`, `tree_order`, `tr
 
 UPDATE zh_sub_project SET filing_template_id = "698e87d8-e947-4049-98e4-15aae7c5c7fc", filing_template_name = '建设项目档案管理规范DA_T 28-2018' WHERE management <> "";
 
+CREATE TABLE `zh_account_cert`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `pid` int NULL COMMENT '项目id',
+  `uid` int NULL COMMENT '用户id',
+  `registration` tinyint(5) NULL DEFAULT NULL COMMENT '执业注册',
+  `qualification` tinyint(5) NULL DEFAULT NULL COMMENT '执业资格',
+  `code` varchar(255) NULL DEFAULT NULL COMMENT '证件编号',
+  `reg_unit` varchar(255) NULL DEFAULT NULL COMMENT '注册单位',
+  `job_title` varchar(255) NULL DEFAULT NULL COMMENT '技术职称',
+  `file_name` varchar(255) NULL COMMENT '文件名称',
+  `file_path` varchar(255) NULL COMMENT '文件下载地址',
+  `edu_json` json NULL COMMENT '继续教育json',
+  `create_time` datetime NULL COMMENT '创建时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT = '用户证书表';
+
+CREATE TABLE `zh_tender_cert`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `tid` int NULL COMMENT '标段id',
+  `uid` int NULL COMMENT '用户id',
+  `cert_id` int NULL COMMENT '个人证书id',
+  `department` varchar(255) NULL DEFAULT NULL COMMENT '所在部门',
+  `job_time` varchar(255) NULL DEFAULT NULL COMMENT '在岗时间',
+  `remark` varchar(1000) NULL DEFAULT NULL COMMENT '备注',
+  `create_time` datetime NULL COMMENT '入库时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT = '标段从业人员表';