index.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. const mongoose = require('mongoose');
  2. const uuidV1 = require('uuid/v1');
  3. const _ = require('lodash');
  4. const scMathUtil = require('../../../public/scMathUtil').getUtil();
  5. const { CRAWL_LOG_KEY, ProcessStatus } = require('../../../public/constants/price_info_constant');
  6. const priceInfoLibModel = mongoose.model('std_price_info_lib');
  7. const priceInfoClassModel = mongoose.model('std_price_info_class');
  8. const priceInfoItemModel = mongoose.model('std_price_info_items');
  9. const priceInfoAreaModel = mongoose.model('std_price_info_areas');
  10. const compilationModel = mongoose.model('compilation');
  11. const importLogsModel = mongoose.model('import_logs');
  12. const priceInfoIndexModel = mongoose.model('std_price_info_index');
  13. async function getLibs(query) {
  14. return await priceInfoLibModel.find(query).lean();
  15. }
  16. async function createLib(name, period, compilationID) {
  17. // 将2020-01变成2020年01月
  18. const reg = /(\d{4})-(\d{2})/;
  19. const formattedPeriod = period.replace(reg, '$1年-$2月');
  20. const lib = {
  21. ID: uuidV1(),
  22. name,
  23. period: formattedPeriod,
  24. compilationID,
  25. createDate: Date.now(),
  26. };
  27. await priceInfoLibModel.create(lib);
  28. return lib;
  29. }
  30. async function updateLib(query, updateData) {
  31. await priceInfoLibModel.update(query, updateData);
  32. }
  33. async function deleteLib(libID) {
  34. await priceInfoClassModel.remove({ libID });
  35. await priceInfoItemModel.remove({ libID });
  36. await priceInfoLibModel.remove({ ID: libID });
  37. }
  38. async function processChecking(key) {
  39. const logData = key
  40. ? await importLogsModel.findOne({ key })
  41. : await importLogsModel.findOne({ key: CRAWL_LOG_KEY });
  42. if (!logData) {
  43. return { status: ProcessStatus.FINISH };
  44. }
  45. if (logData.status === ProcessStatus.FINISH || logData.status === ProcessStatus.ERROR) {
  46. await importLogsModel.remove({ key: logData.key });
  47. }
  48. return { status: logData.status, errorMsg: logData.errorMsg || '', key: logData.key };
  49. }
  50. // 爬取数据
  51. async function crawlDataByCompilation(compilationID, from, to) {
  52. if (!compilationID) {
  53. throw '无有效费用定额。';
  54. }
  55. const compilationData = await compilationModel.findOne({ _id: mongoose.Types.ObjectId(compilationID) }, 'overWriteUrl').lean();
  56. if (!compilationData || !compilationData.overWriteUrl) {
  57. throw '无有效费用定额。';
  58. }
  59. // 从overWriteUrl提取并组装爬虫文件
  60. const reg = /\/([^/]+)\.js/;
  61. const matched = compilationData.overWriteUrl.match(reg);
  62. const crawlURL = `${matched[1]}_price_crawler.js`;
  63. let crawlData;
  64. try {
  65. const crawler = require(`../../../web/over_write/crawler/${crawlURL}`);
  66. crawlData = crawler.crawlData;
  67. } catch (e) {
  68. console.log(e);
  69. throw '该费用定额无可用爬虫方法。'
  70. }
  71. //await crawlData(from, to);
  72. // 异步不等结果,结果由checking来获取
  73. crawlDataByMiddleware(crawlData, from, to, compilationID);
  74. }
  75. // 爬取数据中间件,主要处理checking初始化
  76. async function crawlDataByMiddleware(crawlFunc, from, to, compilationID) {
  77. const logUpdateData = { status: ProcessStatus.FINISH };
  78. try {
  79. const logData = {
  80. key: CRAWL_LOG_KEY,
  81. content: '正在爬取数据,请稍候……',
  82. status: ProcessStatus.START,
  83. create_time: Date.now()
  84. };
  85. await importLogsModel.create(logData);
  86. await crawlFunc(from, to, compilationID);
  87. } catch (err) {
  88. console.log(err);
  89. logUpdateData.errorMsg = String(err);
  90. logUpdateData.status = ProcessStatus.ERROR;
  91. } finally {
  92. await importLogsModel.update({ key: CRAWL_LOG_KEY }, logUpdateData);
  93. }
  94. }
  95. // 导入excel数据,格式如下
  96. // 格式1:
  97. //地区 分类 编码 名称 规格型号 单位 不含税价 含税价
  98. //江北区 黑色及有色金属 热轧光圆钢筋 φ6(6.5) 3566.37 4030
  99. //江北区 木、竹材料及其制品 柏木门套线 60×10 8.76 9.9
  100. // 格式2:
  101. //地区 分类 编码 名称 规格型号 不含税价 含税价
  102. //江北区 黑色及有色金属 热轧光圆钢筋 φ6(6.5) 3566.37 4030
  103. // 柏木门套线 60×10 8.76 9.9
  104. // 沥青混凝土 AC-13 982.3 1110
  105. //
  106. //北碚区 木、竹材料及其制品 热轧光圆钢筋 φ6(6.5) 3566.37 4030
  107. async function importExcelData(libID, sheetData) {
  108. const libs = await getLibs({ ID: libID });
  109. const compilationID = libs[0].compilationID;
  110. // 建立区映射表:名称-ID映射、ID-名称映射
  111. const areaList = await getAreas(compilationID);
  112. const areaMap = {};
  113. areaList.forEach(({ ID, name }) => {
  114. areaMap[name] = ID;
  115. areaMap[ID] = name;
  116. });
  117. // 建立分类映射表:地区名称@分类名称:ID映射
  118. /* const classMap = {};
  119. const classList = await getClassData(libID);
  120. classList.forEach(({ ID, areaID, name }) => {
  121. const areaName = areaMap[areaID] || '';
  122. classMap[`${areaName}@${name}`] = ID;
  123. }); */
  124. // 第一行获取行映射
  125. const colMap = {};
  126. for (let col = 0; col < sheetData[0].length; col++) {
  127. const cellText = sheetData[0][col];
  128. switch (cellText) {
  129. case '地区':
  130. colMap.area = col;
  131. break;
  132. case '分类':
  133. colMap.class = col;
  134. break;
  135. case '编码':
  136. colMap.code = col;
  137. break;
  138. case '名称':
  139. colMap.name = col;
  140. break;
  141. case '规格型号':
  142. colMap.specs = col;
  143. break;
  144. case '单位':
  145. colMap.unit = col;
  146. break;
  147. case '不含税价':
  148. colMap.noTaxPrice = col;
  149. break;
  150. case '含税价':
  151. colMap.taxPrice = col;
  152. break;
  153. }
  154. }
  155. // 提取数据
  156. const data = [];
  157. const classData = [];
  158. const areaClassDataMap = {};
  159. let curAreaName;
  160. let curClassName;
  161. let curClassID;
  162. for (let row = 1; row < sheetData.length; row++) {
  163. const areaName = sheetData[row][colMap.area] || '';
  164. const className = sheetData[row][colMap.class] || '';
  165. const code = sheetData[row][colMap.code] || '';
  166. const name = sheetData[row][colMap.name] || '';
  167. const specs = sheetData[row][colMap.specs] || '';
  168. const unit = sheetData[row][colMap.unit] || '';
  169. const noTaxPrice = sheetData[row][colMap.noTaxPrice] || '';
  170. const taxPrice = sheetData[row][colMap.taxPrice] || '';
  171. if (!className && !code && !name && !specs && !noTaxPrice && !taxPrice) { // 认为是空数据
  172. continue;
  173. }
  174. if (areaName && areaName !== curAreaName) {
  175. curAreaName = areaName;
  176. }
  177. const areaID = areaMap[curAreaName];
  178. if (!areaID) {
  179. continue;
  180. }
  181. if (className && className !== curClassName) {
  182. curClassName = className;
  183. const classItem = {
  184. libID,
  185. areaID,
  186. ID: uuidV1(),
  187. ParentID: '-1',
  188. NextSiblingID: '-1',
  189. name: curClassName
  190. };
  191. curClassID = classItem.ID;
  192. classData.push(classItem);
  193. (areaClassDataMap[areaID] || (areaClassDataMap[areaID] = [])).push(classItem);
  194. const preClassItem = areaClassDataMap[areaID][areaClassDataMap[areaID].length - 2];
  195. if (preClassItem) {
  196. preClassItem.NextSiblingID = classItem.ID;
  197. }
  198. }
  199. if (!curClassID) {
  200. continue;
  201. }
  202. data.push({
  203. ID: uuidV1(),
  204. compilationID,
  205. libID,
  206. areaID,
  207. classID: curClassID,
  208. period: libs[0].period,
  209. code,
  210. name,
  211. specs,
  212. unit,
  213. noTaxPrice,
  214. taxPrice
  215. });
  216. }
  217. if (classData.length) {
  218. await priceInfoClassModel.remove({ libID });
  219. await priceInfoClassModel.insertMany(classData);
  220. }
  221. if (data.length) {
  222. await priceInfoItemModel.remove({ libID });
  223. await priceInfoItemModel.insertMany(data);
  224. } else {
  225. throw 'excel没有有效数据。'
  226. }
  227. }
  228. // 导入excel关键字数据(主表+副表),目前只针对珠海,根据列号导入
  229. /*
  230. 主表:主从对应码 别名编码 材料名称 规格 单位 含税价(元) 除税价(元) 月份备注 计算式
  231. 副表:主从对应码 关键字 单位 关键字效果 组别 选项号
  232. */
  233. async function importKeyData(libID, mainData, subData) {
  234. const lib = await priceInfoLibModel.findOne({ ID: libID }).lean();
  235. if (!lib) {
  236. throw new Error('库不存在');
  237. }
  238. const zh = await priceInfoAreaModel.findOne({ name: { $regex: '珠海' } }).lean();
  239. if (!zh) {
  240. throw new Error('该库不存在珠海地区');
  241. }
  242. // 删除珠海地区所有材料
  243. await priceInfoItemModel.deleteMany({ libID, areaID: zh.ID });
  244. const classItems = await priceInfoClassModel.find({ libID, areaID: zh.ID }).lean();
  245. // 分类树前四位编码 - 分类节点ID映射表
  246. let otherClassID = '';
  247. const classMap = {};
  248. classItems.forEach(item => {
  249. if (item.name) {
  250. if (!otherClassID && /其他/.test(item.name)) {
  251. otherClassID = item.ID;
  252. }
  253. const code = item.name.substr(0, 4);
  254. if (/\d{4}/.test(code)) {
  255. classMap[code] = item.ID;
  256. }
  257. }
  258. });
  259. // 主从对应码 - 关键字数组映射
  260. const keywordMap = {};
  261. for (let row = 1; row < subData.length; row++) {
  262. const rowData = subData[row];
  263. const keywordItem = {
  264. code: rowData[0] ? String(rowData[0]) : '',
  265. keyword: rowData[1] || '',
  266. unit: rowData[2] || '',
  267. coe: rowData[3] || '',
  268. group: rowData[4] || '',
  269. optionCode: rowData[5] || '',
  270. };
  271. if (!keywordItem.code) {
  272. continue;
  273. }
  274. (keywordMap[keywordItem.code] || (keywordMap[keywordItem.code] = [])).push(keywordItem);
  275. }
  276. const priceItems = [];
  277. for (let row = 1; row < mainData.length; row++) {
  278. const rowData = mainData[row];
  279. const code = rowData[0] ? String(rowData[0]) : '';
  280. if (!code) {
  281. continue;
  282. }
  283. const matchCode = code.substring(0, 4);
  284. const classID = classMap[matchCode] || otherClassID;
  285. const priceItem = {
  286. code,
  287. libID,
  288. classID,
  289. ID: uuidV1(),
  290. compilationID: lib.compilationID,
  291. areaID: zh.ID,
  292. period: lib.period,
  293. classCode: rowData[1] || '',
  294. name: rowData[2] || '',
  295. specs: rowData[3] || '',
  296. unit: rowData[4] || '',
  297. taxPrice: rowData[5] || '',
  298. noTaxPrice: rowData[6] || '',
  299. dateRemark: rowData[7] || '',
  300. expString: rowData[8] || '',
  301. keywordList: keywordMap[code] || [],
  302. }
  303. priceItems.push(priceItem);
  304. }
  305. if (priceItems.length) {
  306. await priceInfoItemModel.insertMany(priceItems);
  307. }
  308. }
  309. /* async function importExcelData(libID, sheetData) {
  310. const libs = await getLibs({ ID: libID });
  311. const compilationID = libs[0].compilationID;
  312. // 建立区映射表:名称-ID映射、ID-名称映射
  313. const areaList = await getAreas(compilationID);
  314. const areaMap = {};
  315. areaList.forEach(({ ID, name }) => {
  316. areaMap[name] = ID;
  317. areaMap[ID] = name;
  318. });
  319. // 建立分类映射表:地区名称@分类名称:ID映射
  320. const classMap = {};
  321. const classList = await getClassData(libID);
  322. classList.forEach(({ ID, areaID, name }) => {
  323. const areaName = areaMap[areaID] || '';
  324. classMap[`${areaName}@${name}`] = ID;
  325. });
  326. // 第一行获取行映射
  327. const colMap = {};
  328. for (let col = 0; col < sheetData[0].length; col++) {
  329. const cellText = sheetData[0][col];
  330. switch (cellText) {
  331. case '地区':
  332. colMap.area = col;
  333. break;
  334. case '分类':
  335. colMap.class = col;
  336. break;
  337. case '编码':
  338. colMap.code = col;
  339. break;
  340. case '名称':
  341. colMap.name = col;
  342. break;
  343. case '规格型号':
  344. colMap.specs = col;
  345. break;
  346. case '单位':
  347. colMap.unit = col;
  348. break;
  349. case '不含税价':
  350. colMap.noTaxPrice = col;
  351. break;
  352. case '含税价':
  353. colMap.taxPrice = col;
  354. break;
  355. }
  356. }
  357. // 提取数据
  358. const data = [];
  359. let curAreaName;
  360. let curClassName;
  361. for (let row = 1; row < sheetData.length; row++) {
  362. const areaName = sheetData[row][colMap.area] || '';
  363. const className = sheetData[row][colMap.class] || '';
  364. const code = sheetData[row][colMap.code] || '';
  365. const name = sheetData[row][colMap.name] || '';
  366. const specs = sheetData[row][colMap.specs] || '';
  367. const unit = sheetData[row][colMap.unit] || '';
  368. const noTaxPrice = sheetData[row][colMap.noTaxPrice] || '';
  369. const taxPrice = sheetData[row][colMap.taxPrice] || '';
  370. if (!code && !name && !specs && !noTaxPrice && !taxPrice) { // 认为是空数据
  371. continue;
  372. }
  373. if (areaName && areaName !== curAreaName) {
  374. curAreaName = areaName;
  375. }
  376. if (className && className !== curClassName) {
  377. curClassName = className;
  378. }
  379. const areaID = areaMap[curAreaName];
  380. if (!areaID) {
  381. continue;
  382. }
  383. const classID = classMap[`${curAreaName}@${curClassName}`];
  384. if (!classID) {
  385. continue;
  386. }
  387. data.push({
  388. ID: uuidV1(),
  389. compilationID,
  390. libID,
  391. areaID,
  392. classID,
  393. period: libs[0].period,
  394. code,
  395. name,
  396. specs,
  397. unit,
  398. noTaxPrice,
  399. taxPrice
  400. });
  401. }
  402. if (data.length) {
  403. await priceInfoItemModel.remove({ libID });
  404. await priceInfoItemModel.insertMany(data);
  405. } else {
  406. throw 'excel没有有效数据。'
  407. }
  408. } */
  409. // 获取费用定额的地区数据
  410. async function getAreas(compilationID) {
  411. return await priceInfoAreaModel.find({ compilationID }, '-_id ID name serialNo').lean();
  412. }
  413. async function updateAres(updateData) {
  414. const bulks = [];
  415. updateData.forEach(({ ID, name }) => bulks.push({
  416. updateOne: {
  417. filter: { ID },
  418. update: { name }
  419. }
  420. }));
  421. if (bulks.length) {
  422. await priceInfoAreaModel.bulkWrite(bulks);
  423. }
  424. }
  425. async function insertAreas(insertData) {
  426. await priceInfoAreaModel.insertMany(insertData);
  427. }
  428. async function deleteAreas(deleteData) {
  429. await priceInfoClassModel.remove({ areaID: { $in: deleteData } });
  430. await priceInfoItemModel.remove({ areaID: { $in: deleteData } });
  431. await priceInfoAreaModel.remove({ ID: { $in: deleteData } });
  432. }
  433. async function getClassData(libID, areaID) {
  434. if (libID && areaID) {
  435. return await priceInfoClassModel.find({ libID, areaID }, '-_id').lean();
  436. }
  437. if (libID) {
  438. return await priceInfoClassModel.find({ libID }, '-_id').lean();
  439. }
  440. if (areaID) {
  441. return await priceInfoClassModel.find({ areaID }, '-_id').lean();
  442. }
  443. }
  444. async function getPriceData(classIDList) {
  445. return await priceInfoItemModel.find({ classID: { $in: classIDList } }, '-_id').lean();
  446. }
  447. const UpdateType = {
  448. UPDATE: 'update',
  449. DELETE: 'delete',
  450. CREATE: 'create',
  451. };
  452. async function editPriceData(postData) {
  453. const bulks = [];
  454. postData.forEach(data => {
  455. if (data.type === UpdateType.UPDATE) {
  456. bulks.push({
  457. updateOne: {
  458. filter: { ID: data.ID },
  459. update: { ...data.data }
  460. }
  461. });
  462. } else if (data.type === UpdateType.DELETE) {
  463. bulks.push({
  464. deleteOne: {
  465. filter: { ID: data.ID }
  466. }
  467. });
  468. } else {
  469. bulks.push({
  470. insertOne: {
  471. document: data.data
  472. }
  473. });
  474. }
  475. });
  476. if (bulks.length) {
  477. await priceInfoItemModel.bulkWrite(bulks);
  478. }
  479. }
  480. async function editClassData(updateData) {
  481. const bulks = [];
  482. const deleteIDList = [];
  483. updateData.forEach(({ type, filter, update, document }) => {
  484. if (type === UpdateType.UPDATE) {
  485. bulks.push({
  486. updateOne: {
  487. filter,
  488. update
  489. }
  490. });
  491. } else if (type === UpdateType.DELETE) {
  492. deleteIDList.push(filter.ID);
  493. bulks.push({
  494. deleteOne: {
  495. filter
  496. }
  497. });
  498. } else {
  499. bulks.push({
  500. insertOne: {
  501. document
  502. }
  503. });
  504. }
  505. });
  506. if (deleteIDList.length) {
  507. await priceInfoItemModel.remove({ classID: { $in: deleteIDList } });
  508. }
  509. if (bulks.length) {
  510. await priceInfoClassModel.bulkWrite(bulks);
  511. }
  512. }
  513. //计算指标平均值
  514. function calcIndexAvg(period, areaID, compilationID, preCodeMap) {
  515. const newData = [];
  516. for (const code in preCodeMap) {
  517. const indexArr = preCodeMap[code];
  518. let total = 0;
  519. for (const index of indexArr) {
  520. total = scMathUtil.roundForObj(total + index, 2);
  521. }
  522. const avg = scMathUtil.roundForObj(total / indexArr.length, 2);
  523. newData.push({ ID: uuidV1(), code, period, areaID, compilationID, index: avg })
  524. }
  525. return newData
  526. }
  527. //一个月里有classCode相同,但是价格不同的情况,取平均值
  528. function getClassCodePriceAvgMap(items) {
  529. const classCodeMap = {};
  530. for (const b of items) {
  531. classCodeMap[b.classCode] ? classCodeMap[b.classCode].push(b) : classCodeMap[b.classCode] = [b];
  532. }
  533. for (const classCode in classCodeMap) {
  534. const baseItems = classCodeMap[classCode];
  535. const item = baseItems[0];
  536. if (baseItems.length > 1) {
  537. let sum = 0;
  538. for (const b of baseItems) {
  539. sum += parseFloat(b.noTaxPrice);
  540. }
  541. classCodeMap[classCode] = { code: item.code, name: item.name, price: scMathUtil.roundForObj(sum / baseItems.length, 2) };
  542. } else {
  543. classCodeMap[classCode] = { code: item.code, name: item.name, price: parseFloat(item.noTaxPrice) }
  544. }
  545. }
  546. return classCodeMap
  547. }
  548. async function calcPriceIndex(libID, period, areaID, compilationID) {
  549. const baseItems = await priceInfoItemModel.find({ areaID, period: '2022年-01月' }).lean();//以珠海 22年1月的数据为基准
  550. const currentItems = await priceInfoItemModel.find({ areaID, period }).lean();
  551. const preCodeMap = {};//编码前4位-指数映射
  552. const baseAvgMap = getClassCodePriceAvgMap(baseItems);
  553. const currentAvgMap = getClassCodePriceAvgMap(currentItems);
  554. let message = '';
  555. for (const classCode in currentAvgMap) {
  556. const c = currentAvgMap[classCode];
  557. const preCode = c.code.substr(0, 4);
  558. let index = 1;
  559. const baseItem = baseAvgMap[classCode];
  560. const tem = { index, classCode, name: c.name, code: c.code };
  561. if (baseItem && baseItem.price) {//一个月份里有多个值时,先取平均再计算
  562. index = scMathUtil.roundForObj(c.price / baseItem.price, 2);
  563. tem.baseName = baseItem.name;
  564. }
  565. tem.index = index;
  566. if (Math.abs(index - 1) > 0.2) {
  567. const string = `classCode:${tem.classCode},编号:${tem.code},基础名称:${tem.baseName},当前库中名称:${tem.name},指数:${tem.index};\n`;
  568. message += string;
  569. console.log(string)
  570. }
  571. preCodeMap[preCode] ? preCodeMap[preCode].push(index) : preCodeMap[preCode] = [index];
  572. }
  573. const newIndexData = calcIndexAvg(period, areaID, compilationID, preCodeMap)
  574. //删除旧数据
  575. await priceInfoIndexModel.deleteMany({ areaID, period });
  576. //插入新数据
  577. await priceInfoIndexModel.insertMany(newIndexData);
  578. return message;
  579. }
  580. async function exportExcelData(libID) {
  581. const priceItems = await priceInfoItemModel.find({ libID }).lean();
  582. // 整理数据
  583. let priceData = [];
  584. for (const tmp of priceItems) {
  585. const item = [tmp.code || '', tmp.classCode || '', tmp.name || '', tmp.specs || '', tmp.unit || '', tmp.taxPrice || '', tmp.noTaxPrice || '', tmp.remark || '', tmp.expString || ''];
  586. priceData.push(item);
  587. }
  588. const excelData = [['主从对应码', '别名编码', '材料名称', '规格型号', '单位', '含税价(元)', '除税价(元)', '多价备注', '计算式']];
  589. excelData.push.apply(excelData, priceData);
  590. return excelData;
  591. }
  592. module.exports = {
  593. getLibs,
  594. createLib,
  595. updateLib,
  596. deleteLib,
  597. processChecking,
  598. crawlDataByCompilation,
  599. importExcelData,
  600. importKeyData,
  601. getAreas,
  602. updateAres,
  603. insertAreas,
  604. deleteAreas,
  605. getClassData,
  606. calcPriceIndex,
  607. getPriceData,
  608. editPriceData,
  609. editClassData,
  610. exportExcelData
  611. }