Browse Source

feat: 信息价ai填值

vian 8 months ago
parent
commit
cacc1b2b12

+ 21 - 0
modules/price_info_summary/controllers/index.js

@@ -15,6 +15,16 @@ class PriceInfoSummaryController extends BaseController {
         res.render("maintain/price_info_summary/html/main.html", renderData);
     }
 
+    // 获取所有数据
+    async getData(req, res) {
+        try {
+            const data = await facade.getData();
+            res.json({ error: 0, message: 'getData success', data });
+        } catch (err) {
+            console.log(err);
+        }
+    }
+
     // 获取分页数据
     async getPagingData(req, res) {
         try {
@@ -50,6 +60,17 @@ class PriceInfoSummaryController extends BaseController {
         }
     }
 
+    async aiMatch(req, res) {
+        try {
+            const { listA, listB } = JSON.parse(req.body.data);
+            const data = await facade.aiMatch(listA, listB);
+            res.json({ error: 0, data, message: 'aiMatch success' });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
     async exportSummaryData(request, response) {
         try {
             const excelData = await facade.exportExcelData();

+ 26 - 0
modules/price_info_summary/facade/index.js

@@ -2,6 +2,19 @@ const mongoose = require('mongoose');
 
 const priceInfoSummaryModel = mongoose.model('std_price_info_summary');
 
+const axios = require('axios');
+
+const axiosInstance = axios.create({
+    baseURL: 'http://112.74.42.187:26909/api/',
+    timeout: 60000 * 5,
+});
+
+// 获取所有数据
+const getData = async () => {
+    const items = await priceInfoSummaryModel.find({}).lean();
+    return items;
+}
+
 // 获取分页数据
 const getPagingData = async (page, pageSize, searchStr) => {
     let query = {};
@@ -57,6 +70,17 @@ async function saveInSummary(documents) {
     await priceInfoSummaryModel.insertMany(documents);
 }
 
+// ai匹配
+async function aiMatch(listA, listB) {
+    const url = '/material_sheet_match';
+    const res = await axiosInstance.post(url, { list_a: listA, list_b: listB }, { timeout: 1000 * 60 * 5 });
+    const resData = res.data;
+    if (resData.errno) {
+        throw new AppError('AI匹配失败');
+    }
+    return resData.data;
+}
+
 // 导出excel数据
 async function exportExcelData() {
     const items = await priceInfoSummaryModel.find({}).sort({ classCode: 1 }).lean();
@@ -73,8 +97,10 @@ async function exportExcelData() {
 }
 
 module.exports = {
+    getData,
     getPagingData,
     editSummaryData,
     saveInSummary,
     exportExcelData,
+    aiMatch,
 }

+ 2 - 0
modules/price_info_summary/routes/index.js

@@ -8,9 +8,11 @@ const { priceInfoSummaryController } = require('../controllers/index');
 
 module.exports = function (app) {
     router.get("/main", priceInfoSummaryController.auth, priceInfoSummaryController.init, priceInfoSummaryController.main);
+    router.post("/getData", priceInfoSummaryController.auth, priceInfoSummaryController.init, priceInfoSummaryController.getData);
     router.post("/getPagingData", priceInfoSummaryController.auth, priceInfoSummaryController.init, priceInfoSummaryController.getPagingData);
     router.post("/editSummaryData", priceInfoSummaryController.auth, priceInfoSummaryController.init, priceInfoSummaryController.editSummaryData);
     router.post("/saveInSummary", priceInfoSummaryController.auth, priceInfoSummaryController.init, priceInfoSummaryController.saveInSummary);
+    router.post("/aiMatch", priceInfoSummaryController.auth, priceInfoSummaryController.init, priceInfoSummaryController.aiMatch);
     router.get("/export", priceInfoSummaryController.auth, priceInfoSummaryController.init, priceInfoSummaryController.exportSummaryData);
 
     app.use("/priceInfoSummary", router);

+ 17 - 17
operation.js

@@ -23,7 +23,7 @@ app.locals.rootDir = _rootDir;
 dbm.connect(process.env.NODE_ENV);
 
 //引入报表模块
-fileUtils.getGlobbedFiles('./modules/all_models/*.js').forEach(function(modelPath) {
+fileUtils.getGlobbedFiles('./modules/all_models/*.js').forEach(function (modelPath) {
     require(path.resolve(modelPath));
 });
 
@@ -47,18 +47,18 @@ app.set('view options', {
 app.use(partials());
 
 let bodyParser = require('body-parser');
-app.use(bodyParser.urlencoded({limit: '3mb', extended: true}));
-app.use(bodyParser.json({limit: '3mb'}));
+app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));
+app.use(bodyParser.json({ limit: '10mb' }));
 
 app.use(session({
     name: 'usersSession',
     secret: 'session users secret',
-    cookie: {maxAge: 3600 * 6 * 1000},
+    cookie: { maxAge: 3600 * 6 * 1000 },
     resave: false,
     rolling: true,
     saveUninitialized: true
     //*
-    ,store: new DBStore({
+    , store: new DBStore({
         url: config.getSessionURL(process.env.NODE_ENV)
     })
     //*/
@@ -67,14 +67,14 @@ app.use(session({
 
 app.use(function (req, res, next) {
     let referer = '';
-    if(/\.map|\.ico$/.test(req.originalUrl) || /^\/cld\/(?!getCategoryStaff)/.test(req.originalUrl)) {
+    if (/\.map|\.ico$/.test(req.originalUrl) || /^\/cld\/(?!getCategoryStaff)/.test(req.originalUrl)) {
         next();
     } else {
         if (!/^\/login/.test(req.originalUrl) && !req.session.managerData) {
             if (req.headers["x-requested-with"] != null
                 && req.headers["x-requested-with"] == "XMLHttpRequest"
                 && req.url != "/login") {
-                return res.json({ret_code: 99, ret_msg: '登录信息失效,请您重新登录'});
+                return res.json({ ret_code: 99, ret_msg: '登录信息失效,请您重新登录' });
             } else {
                 return res.redirect('/login');
             }
@@ -84,16 +84,16 @@ app.use(function (req, res, next) {
 });
 
 //加载路由文件
-fileUtils.getGlobbedFiles('./modules/**/routes/*.js').forEach(function(modelPath) {
+fileUtils.getGlobbedFiles('./modules/**/routes/*.js').forEach(function (modelPath) {
     console.log(modelPath);
     require(path.resolve(modelPath))(app);
 });
 
 
-app.use(function(req, res, next) {
+app.use(function (req, res, next) {
     res.status(404).send('404 Error');
 });
-app.use(function(err, req, res, next) {
+app.use(function (err, req, res, next) {
     console.error(err.stack);
     res.status(500).send('500 Error');
 });
@@ -104,23 +104,23 @@ require('./public/stringUtil').setupDateFormat();
 
 //定时任务
 let sysSchedule = require('./modules/sys_tools/models/sys_model');
-schedule.scheduleJob({hour: 3, minute: 30, dayOfWeek: 7}, function(){
+schedule.scheduleJob({ hour: 3, minute: 30, dayOfWeek: 7 }, function () {
     sysSchedule.clearJunkData(function (err) {
-        if(err){
+        if (err) {
             console.log('清除失败');
         }
-        else{
+        else {
             console.log('清除成功');
         }
     })
 });
 
-schedule.scheduleJob({hour: 0, minute: 1}, function(){
+schedule.scheduleJob({ hour: 0, minute: 1 }, function () {
     sysSchedule.checkUserCompilationStatus(function (err) {
-        if(err){
+        if (err) {
             console.log('更新失败');
         }
-        else{
+        else {
             console.log('更新成功');
         }
     })
@@ -139,7 +139,7 @@ schedule.scheduleJob({hour: 0, minute: 1}, function(){
 
 
 let startPort = 6080;
-if(config[process.env.NODE_ENV].startPort) startPort = config[process.env.NODE_ENV].startPort;
+if (config[process.env.NODE_ENV].startPort) startPort = config[process.env.NODE_ENV].startPort;
 console.log(startPort);
 app.listen(startPort, function () {
     console.log("server started!");

+ 3 - 0
web/maintain/price_info_lib/html/edit.html

@@ -105,6 +105,9 @@
                 </div>
                 <div class="modal-body">
                     <button id="save-in-summary">保存至总表</button> 
+                    <button id="ai-match">AI填值</button> 
+                    <button id="save-data">保存AI填值</button> 
+                    <span>若要将AI填值后的内容保存至总表,请先保存AI填值</span>
                     <div class="empty-spread"></div>
                     <div class="recommend">
                         <span class="recommend-title">相似材料数据</span>

+ 2 - 2
web/maintain/price_info_lib/js/priceArea.js

@@ -11,8 +11,8 @@ const AREA_BOOK = (() => {
   const workBook = initSheet($('#area-spread')[0], setting);
   lockUtil.lockSpreads([workBook], locked);
   workBook.options.allowExtendPasteRange = false;
-  workBook.options.allowUserDragDrop = true;
-  workBook.options.allowUserDragFill = true;
+  workBook.options.allowUserDragDrop = false;
+  workBook.options.allowUserDragFill = false;
   const sheet = workBook.getSheet(0);
 
   // 排序显示

+ 180 - 0
web/maintain/price_info_lib/js/priceEmpty.js

@@ -285,6 +285,174 @@ const EMPTY_BOOK = (() => {
     }
   }
 
+  // 获取最大别名编码
+  const getMaxClassCode = (priceInfoSummary) => {
+    let maxClassCode = '';
+    let maxClassNumber = 0;
+    priceInfoSummary.forEach(item => {
+      if (!item.classCode) {
+        return;
+      }
+      const numMatch = item.classCode.match(/\d+/);
+      if (numMatch && +numMatch[0] > maxClassNumber) {
+        maxClassNumber = +numMatch[0];
+        maxClassCode = item.classCode;
+      }
+    });
+    // return { maxClassNumber, maxClassCode };
+    return maxClassCode;
+  }
+
+  // 在最大别名编码基础上+1;
+  const getNewMaxClassCode = (maxClassCode) => {
+    const numMatch = maxClassCode.match(/\d+/);
+    if (!numMatch) {
+      return 1;
+    }
+    const maxNumber = +numMatch[0];
+    let newMaxClassCode = String(maxNumber + 1);
+    // 补齐的位数
+    const pattern = numMatch[0].length - newMaxClassCode.length;
+    if (pattern > 0) {
+      for (let i = 0; i < pattern; i++) {
+        newMaxClassCode = `0${newMaxClassCode}`;
+      }
+    }
+    return newMaxClassCode;
+
+  }
+
+  // ai填值
+  const aiMatch = async () => {
+    try {
+      // 获取信息价总表
+      const priceInfoSummary = await ajaxPost('/priceInfoSummary/getData', {}, 1000 * 60 * 5);
+      const summaryGroupMap = _.groupBy(priceInfoSummary, 'code');
+      const noCodeSummary = priceInfoSummary.filter(item => !item.code);
+      const totalRows = workBookObj.sheet.getRowCount();
+      const changedCells = [];
+      const noMatchRows = []; // 没有匹配、ai没有命中的行,后续需要自动生成别名编码(最大的别名编码+1)
+      for (let i = 0; i < totalRows; i++) {
+        const rowData = getRowData(workBookObj.sheet, i, setting.header);
+        const code = rowData.code || '';
+        const toMatchSummary = code ? summaryGroupMap[code] || [] : noCodeSummary;
+        if (toMatchSummary.length) {
+          changedCells.push({ row: i });
+        } else {
+          noMatchRows.push(i);
+        }
+      }
+      const classCodeCol = setting.header.findIndex(h => h.dataCode === 'classCode');
+      const expStringCol = setting.header.findIndex(h => h.dataCode === 'expString');
+      const chunks = _.chunk(changedCells, 20);
+      let percent = 0;
+      $.bootstrapLoading.progressStart('AI填值', false);
+      $("#progress_modal_body").text('正在进行AI填值,请稍后...');
+      await setTimeoutSync(500);
+
+      // 分块进行ai匹配
+      const step = 100 / (chunks.length || 1);
+      for (const chunk of chunks) {
+        const listA = [];
+        const listB = [];
+        const summaryData = [];
+        chunk.forEach(item => {
+          const rowData = getRowData(workBookObj.sheet, item.row, setting.header);
+          listA.push(`${rowData.name || ''} ${rowData.specs}`);
+          const code = rowData.code || '';
+          const toMatchSummary = code ? summaryGroupMap[code] || [] : noCodeSummary;
+          summaryData.push(toMatchSummary);
+          const summaryKeys = toMatchSummary.map(summary => `${summary.name || ''} ${summary.specs || ''}`);
+          listB.push(summaryKeys)
+        });
+        const test = listB.map(item => item.length);
+        console.log(test);
+
+        const matchRes = await ajaxPost('/priceInfoSummary/aiMatch', { listA, listB }, 1000 * 60 * 5);
+        // 填匹配值到表格,不实时保存,因为需要人工核查
+        workBookObj.sheet.suspendEvent();
+        workBookObj.sheet.suspendPaint();
+        matchRes.forEach((item, index) => {
+          const firstMatch = item[0];
+          const chunkItem = chunk[index];
+          // 相似度过低的不命中
+          if (firstMatch.similarity < 50) {
+            noMatchRows.push(chunkItem.row);
+            return;
+          };
+          const summaryIndex = item[0].index;
+          const summaryItem = summaryData[index][summaryIndex];
+          if (chunkItem && summaryItem) {
+            workBookObj.sheet.setValue(chunkItem.row, classCodeCol, summaryItem.classCode);
+            // 如果实际行存在珠海地区的,才填计算式
+            const tableItems = getItemsFromTableItem(cache[chunkItem.row]);
+            const needExpString = tableItems.some(tItem => {
+              const area = AREA_BOOK.cache.find(areaItem => areaItem.ID === tItem.areaID)
+              return area && area.name && /珠海/.test(area.name);
+            });
+            if (needExpString) {
+              workBookObj.sheet.setValue(chunkItem.row, expStringCol, summaryItem.expString);
+            }
+          }
+        });
+        workBookObj.sheet.resumeEvent();
+        workBookObj.sheet.resumePaint();
+        percent += step;
+        $("#progress_modal_bar").css('width', `${percent}%`);
+        await setTimeoutSync(500);
+      }
+
+      // 没匹配到的行,自动生成别名编码
+      workBookObj.sheet.suspendEvent();
+      workBookObj.sheet.suspendPaint();
+      let curMaxClassCode = getMaxClassCode(priceInfoSummary);
+      for (const row of noMatchRows) {
+        const newClassCode = getNewMaxClassCode(curMaxClassCode);
+        workBookObj.sheet.setValue(row, classCodeCol, newClassCode);
+        curMaxClassCode = newClassCode;
+      }
+      workBookObj.sheet.resumeEvent();
+      workBookObj.sheet.resumePaint();
+
+
+    } catch (error) {
+      console.log(error);
+      alert(error);
+    }
+    await setTimeoutSync(500);
+    $.bootstrapLoading.progressEnd();
+  }
+
+  // 保存ai填值
+  const saveData = async () => {
+    try {
+      $.bootstrapLoading.progressStart('保存AI填值', false);
+      $("#progress_modal_body").text('正在保存AI填值,请稍后...');
+      await setTimeoutSync(500);
+      // 分批保存数据,以免数据库压力过大
+      const totalRows = workBookObj.sheet.getRowCount();
+      const changedCells = [];
+      for (let i = 0; i < totalRows; i++) {
+        changedCells.push({ row: i });
+      }
+      const chunks = _.chunk(changedCells, 100);
+      let percent = 0;
+      const step = 100 / (chunks.length || 1);
+      for (const chunk of chunks) {
+        await handleEdit(chunk);
+        percent += parseInt(`${step}`);
+        $("#progress_modal_bar").css('width', `${percent}%`);
+        await setTimeoutSync(200);
+      }
+    } catch (error) {
+      console.log(error);
+      alert(error);
+    }
+    setTimeout(() => {
+      $.bootstrapLoading.progressEnd();
+    }, 500);
+  }
+
 
   return {
     buildWorkBook,
@@ -294,6 +462,8 @@ const EMPTY_BOOK = (() => {
     workBookObj,
     updateRowCode,
     saveInSummary,
+    aiMatch,
+    saveData,
   }
 })();
 
@@ -314,4 +484,14 @@ $(document).ready(() => {
   $('#save-in-summary').click(() => {
     EMPTY_BOOK.saveInSummary();
   });
+
+  // AI填值
+  $('#ai-match').click(() => {
+    EMPTY_BOOK.aiMatch();
+  })
+
+  // 保存AI填值
+  $('#save-data').click(() => {
+    EMPTY_BOOK.saveData();
+  })
 });