func-tbook-export.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817
  1. import { replaceDocx, createZip, uploadFileToOSS, docxToPdf, renderDocx, toBarCode, downloadUrl } from "../../lib/docs";
  2. // const Parse = global.Parse;
  3. const path = require("path")
  4. const fs = require("fs")
  5. var TemplateDocxDir = path.join(__dirname,"template")
  6. if(!fs.existsSync(TemplateDocxDir)){
  7. TemplateDocxDir = path.join(__dirname,"../../template")
  8. }
  9. const tempDir = path.join(__dirname , "temp");
  10. const crypto = require('crypto');
  11. /**
  12. * 常用Word字符
  13. */
  14. // 圆圈选中未选 ○ 未选 ● 选中
  15. const circleCheck = ["○","●"];
  16. // 方块选中未选 ○ 未选 ● 选中
  17. const squareCheck = [`□`,String.fromCharCode(0xFE)];
  18. /**
  19. * 定义导出申报合集文档云函数
  20. * @example
  21. * Cloud Code test
  22. 导出流程
  23. curl -X POST -H "Content-Type: application/json" -H 'X-Parse-Application-Id: edu-textbook' -d '{ "processId": "FR7KZtefyR" }' http://127.0.0.1:61337/parse/functions/tbookExportReport
  24. curl -X POST -H "Content-Type: application/json" -H 'X-Parse-Application-Id: edu-textbook' -d '{ "processId": "FR7KZtefyR" }' http://8.140.98.43/parse/functions/tbookExportReport
  25. curl -X POST -H "Content-Type: application/json" -H 'X-Parse-Application-Id: edu-textbook' -d '{ "processId": "FR7KZtefyR" }' http://8.140.98.43/parse/api/tbook/export
  26. curl -X POST -H "Content-Type: application/json" -H 'X-Parse-Application-Id: edu-textbook' -d '{ "processId": "FR7KZtefyR" }' https://145.tbook.com.cn/parse/api/tbook/export
  27. 导出教材列表
  28. curl -X POST -H "Content-Type: application/json" -H 'X-Parse-Application-Id: edu-textbook' -d '{ "bookList": ["9V575dapEM","2YBKitpCJL","xLdiEaHGrX"] }' http://8.140.98.43/parse/functions/tbookExportReport
  29. */
  30. export function defineTbookExportReport(app){
  31. // 全新导出,每次都重新导出
  32. app.post("/parse/api/tbook/export",async (request,res)=>{
  33. request.setTimeout(500*1000);
  34. let processId = request.body.processId;
  35. let bookList = request.body.bookList;
  36. console.log(request.body)
  37. try{
  38. let result
  39. if(processId){
  40. result = await exportProcessReportDocs(processId)
  41. }
  42. if(bookList?.length){
  43. result = await exportProcessReportDocs(null,bookList)
  44. }
  45. if(result?.docsList?.length==0){
  46. // throw new Parse.Error(404,"合集内无申报教材")
  47. res.json({
  48. code:400,
  49. err:"合集内无申报教材"
  50. })
  51. return
  52. }
  53. // return result
  54. res.json({
  55. code:200,
  56. result:result
  57. })
  58. return
  59. }catch(err){
  60. console.error(err)
  61. res.json({
  62. code:400,
  63. err:err
  64. })
  65. return
  66. // throw new Parse.Error(404,"导出申报合集失败")
  67. }
  68. // throw new Parse.Error(404,"未找到该流程合集")
  69. })
  70. // 预览导出:已导出直接返回
  71. // curl -X POST -H "Content-Type: application/json" -H 'X-Parse-Application-Id: edu-textbook' -d '{ "bookid": "j4pt2MMTXM" }' https://145.tbook.com.cn/parse/api/tbook/preview
  72. app.post("/parse/api/tbook/preview",async (request,res)=>{
  73. let bookid = request.body.bookid;
  74. let isNew = request.body.isNew || false;
  75. try{
  76. let result
  77. if(bookid){
  78. let book
  79. if(!isNew){
  80. let query = new Parse.Query("EduTextbook")
  81. query.include("childrens","department")
  82. book = await query.get(bookid);
  83. }
  84. if(book?.get("export")){
  85. res.json({
  86. code:200,
  87. data:{
  88. urlPdf:book?.get("export")
  89. }
  90. })
  91. return
  92. }
  93. result = await exportProcessReportDocs(null,[bookid])
  94. let urlPdf = result?.docsList?.[0]?.urlPdf;
  95. if(urlPdf){
  96. book.set('export',urlPdf);
  97. book.save();
  98. res.json({
  99. code:200,
  100. data:{
  101. urlPdf:urlPdf
  102. }
  103. })
  104. return
  105. }
  106. }
  107. console.log(result)
  108. if(result?.docsList?.length==0){
  109. // throw new Parse.Error(404,"合集内无申报教材")
  110. res.json({
  111. code:400,
  112. err:"无该教材"
  113. })
  114. return
  115. }
  116. // return result
  117. res.json({
  118. code:200,
  119. result:result
  120. })
  121. return
  122. }catch(err){
  123. console.error(err)
  124. res.json({
  125. code:400,
  126. err:err
  127. })
  128. return
  129. // throw new Parse.Error(404,"导出申报合集失败")
  130. }
  131. // throw new Parse.Error(404,"未找到该流程合集")
  132. })
  133. Parse.Cloud.define("tbookExportReport", async (request) => {
  134. let processId = request.params.processId;
  135. let bookList = request.params.bookList;
  136. try{
  137. let result
  138. if(processId){
  139. result = await exportProcessReportDocs(processId)
  140. }
  141. if(bookList?.length){
  142. result = await exportProcessReportDocs(null,bookList)
  143. }
  144. if(result?.docsList?.length==0){
  145. throw new Parse.Error(404,"合集内无申报教材")
  146. }
  147. return result
  148. }catch(err){
  149. console.error(err)
  150. throw new Parse.Error(404,"导出申报合集失败")
  151. }
  152. throw new Parse.Error(404,"未找到该流程合集")
  153. },{
  154. fields : {
  155. processId:{
  156. required:false
  157. },
  158. }
  159. });
  160. }
  161. /**
  162. * 导出流程教材申报文件
  163. * @returns
  164. * docsList
  165. * zipUrl
  166. */
  167. export async function exportProcessReportDocs(processId,bookList,isNew) {
  168. isNew = isNew || false;
  169. if(!processId && !bookList?.length) return {}
  170. let textbookList
  171. if(processId){ // 流程读取教材列表
  172. let query = new Parse.Query("EduTextbook")
  173. query.include("childrens","department")
  174. query.equalTo("recommend",true);
  175. query.equalTo("eduProcess",processId);
  176. textbookList = await query.find();
  177. }
  178. console.log(bookList)
  179. if(bookList?.length){ // 直接导出教材列表
  180. let query = new Parse.Query("EduTextbook")
  181. query.include("childrens","department")
  182. query.containedIn("objectId",bookList);
  183. textbookList = await query.find();
  184. }
  185. let docsList = []
  186. let plist = []
  187. for (let index = 0; index < textbookList.length; index++) {
  188. let textbook = textbookList[index];
  189. console.log("textbook",index)
  190. // 直接将异步调用的 Promise 添加到 plist
  191. plist.push(renderReportDocsByTextbook(textbook,isNew)); // 立即执行并返回 Promise
  192. }
  193. let presults = await Promise.all(plist);
  194. console.log(presults)
  195. presults.forEach(result=>{
  196. if(result?.filePath || result?.pdfPath){
  197. docsList.push(result)
  198. }
  199. })
  200. let zipPath,zipUrl
  201. if(docsList?.length){
  202. let now = new Date();
  203. let fileList = docsList?.map(item=>item?.pdfPath);
  204. // zip包md5生成
  205. let md5 = crypto.createHash('md5');
  206. let fileListMd5 = md5.update(fileList?.join("")).digest('hex')
  207. let zipName = `申报书导出-${now.getFullYear()}${now.getMonth()+1}${now.getDate()}${now.getHours()}${now.getMinutes()}${now.getSeconds()}${now.getMilliseconds()}-${fileListMd5}.zip`
  208. zipPath = await createZip(fileList,zipName,{tempDir:null})
  209. if(zipPath){
  210. zipUrl = (await uploadFileToOSS(zipPath))?.url || null
  211. fs.rmSync(zipPath)
  212. fileList.forEach(tempFile=>{
  213. console.log("rm",tempFile)
  214. fs.rmSync(path.dirname(tempFile),{recursive:true,force:true});
  215. })
  216. }
  217. docsList = docsList.map(item=>{return {code:item.code,objectId:item?.objectId,title:item.title,url:item?.url,urlPdf:item?.urlPdf}})
  218. }
  219. let result = {
  220. docsList,
  221. zipUrl
  222. }
  223. return result
  224. }
  225. module.exports.exportProcessReportDocs = exportProcessReportDocs
  226. function fixVolumeData(json,volumeData,index,totalJson,isChildren){
  227. let volumeIndex = index;
  228. json.index = index + 1;
  229. // 教材基本信息
  230. // 教材所属学科专业类:前四位 discipline
  231. let disciplineCode4 = volumeData?.discipline?.code?.slice(0,4) || json?.discipline?.code?.slice(0,4) || ""
  232. let disciplineName4 = volumeData?.discipline?.name || json?.discipline?.name || ""
  233. // 教材应用对象及所属学科专业类:前四位 majorPoniter
  234. let majorCode4 = volumeData?.majorPoniter?.code?.slice(0,4) || json?.majorPoniter?.code?.slice(0,4) || ""
  235. let majorName4 = volumeData?.majorPoniter?.name || json?.majorPoniter?.name || ""
  236. // 专业代码:前六位 major
  237. let majorCode6 = volumeData?.major?.code?.slice(0) || json?.major?.code?.slice(0) || ""
  238. let majorName6 = volumeData?.major?.name || json?.major?.name || ""
  239. // 初版时间
  240. let firstDate
  241. if(volumeData?.editionFirst?.iso){
  242. firstDate = new Date(volumeData?.editionFirst.iso);
  243. }
  244. let firstYear = firstDate&&firstDate?.getFullYear();
  245. let firstMonth = firstDate&&(firstDate?.getMonth()+1);
  246. // 本版时间印次
  247. let currentDate
  248. if(volumeData?.editionDate?.iso){
  249. currentDate = new Date(volumeData?.editionDate.iso);
  250. }
  251. let currentYear = currentDate&&currentDate?.getFullYear();
  252. let currentMonth = currentDate&&(currentDate?.getMonth()+1);
  253. // 最新时间印次
  254. let latestDate
  255. if(volumeData?.printDate?.iso){
  256. latestDate = new Date(volumeData?.printDate.iso);
  257. }
  258. let latestYear = latestDate&&latestDate?.getFullYear();
  259. let latestMonth = latestDate&&(latestDate?.getMonth()+1);
  260. let latestNum = volumeData?.printNumber || "";
  261. let currentNum = volumeData?.editionNumber || "";
  262. let printSum = volumeData?.printSum?volumeData?.printSum+"万":"" || "";
  263. // 是否重点立项
  264. let importantProject = volumeData?.importantProject?.join();
  265. // 初版至今重点项目
  266. let isBSQT = !((importantProject?.indexOf("建设")>-1) || (importantProject?.indexOf("本科国家")>-1) || (importantProject?.indexOf("省级优秀")>-1) || (importantProject?.indexOf("省级规划")>-1))// 是否其他省级奖项
  267. let bsqtName = (volumeData?.importantProject || [])?.filter(item=>(item?.indexOf("建设")==-1&&item?.indexOf("本科国家")==-1&&item?.indexOf("省级优秀")==-1&&item?.indexOf("省级规划")==-1));
  268. json.importantProject = importantProject
  269. json.firstDate = firstDate
  270. json.firstYear = firstYear || ""
  271. json.firstMonth = firstMonth || ""
  272. json.currentDate = currentDate
  273. json.currentYear = currentYear || ""
  274. json.currentMonth = currentMonth || ""
  275. json.latestDate = latestDate
  276. json.latestYear = latestYear || ""
  277. json.latestMonth = latestMonth || ""
  278. json.currentNum = currentNum
  279. json.latestMonth = latestMonth
  280. json.latestNum = latestNum
  281. json.printSum = printSum
  282. json.isBSQT = isBSQT
  283. json.bsqtName = (importantProject?.indexOf("其他省部级")>-1) && volumeData?.importantProjectOther ? volumeData?.importantProjectOther : bsqtName
  284. json.uBS = circleCheck[(volumeData?.unitType?.indexOf("部属")>-1)?1:0];
  285. json.uHJ = circleCheck[(volumeData?.unitType?.indexOf("合建")>-1)?1:0];
  286. json.uGJ = circleCheck[(volumeData?.unitType?.indexOf("共建")>-1)?1:0];
  287. json.uQT = circleCheck[(volumeData?.unitType?.indexOf("其他")>-1)?1:0];
  288. json.dc4 = disciplineCode4;
  289. json.dn4 = disciplineName4;
  290. json.mc4 = majorCode4;
  291. json.mn4 = majorName4;
  292. json.majorCodePad = disciplineCode4;
  293. json.mc6 = majorCode6;
  294. json.mn6 = majorName6;
  295. json.lCN = circleCheck[(volumeData?.lang=="中文")?1:0];
  296. json.lEN = circleCheck[(volumeData?.lang=="英文")?1:0];
  297. json.lMW = circleCheck[(volumeData?.lang=="盲文")?1:0];
  298. json.lOT = circleCheck[(volumeData?.lang?.indexOf("其他")>-1)?1:0];
  299. json.lSS = circleCheck[(volumeData?.lang?.indexOf("少数")>-1)?1:0];
  300. json.publisher = volumeData?.editionUnit;
  301. json.isDZ = circleCheck[(volumeData?.carrierShape?.indexOf("电子教材")>-1)?1:0];
  302. json.isSZ = circleCheck[(volumeData?.carrierShape?.indexOf("数字教材")>-1 || volumeData?.carrierShape?.indexOf("电子教材")>-1)?1:0];
  303. json.isQT = circleCheck[(volumeData?.carrierShape?.indexOf("其他")>-1)?1:0];
  304. json.isFD = circleCheck[(volumeData?.carrierShape?.indexOf("附带")>-1)?1:0];
  305. json.isZZ = circleCheck[(volumeData?.carrierShape?.indexOf("纸质教材")>-1 && volumeData?.carrierShape?.indexOf("附带")==-1)?1:0];
  306. json.latestY = latestYear;
  307. json.latestM = latestMonth;
  308. json.currentY = currentYear;
  309. json.currentM = currentMonth;
  310. console.log(importantProject)
  311. json.isGJS = circleCheck[(importantProject?.indexOf("建设")>-1)?1:0];
  312. json.isBGJ = circleCheck[(importantProject?.indexOf("本科国家")>-1)?1:0];
  313. json.isBSYX = circleCheck[(importantProject?.indexOf("省级优秀")>-1)?1:0];
  314. json.isBSGH = circleCheck[(importantProject?.indexOf("省级规划")>-1)?1:0];
  315. // json.isBSQT = circleCheck[isBSQT&&bsqtName?1:0];
  316. json.isBSQT = circleCheck[(importantProject?.indexOf("其他省部级")>-1)?1:0];
  317. json.isFirstNot = circleCheck[volumeData?.importantProject?.length > 0?0:1];
  318. // 作者列表 + 政治审查表
  319. totalJson.examineList = totalJson.examineList || []
  320. totalJson.zhengshenList = totalJson.zhengshenList || []
  321. // console.log(volumeData?.authorList)
  322. for (let index = 0; index < 6; index++) { // 补充空值
  323. if(!volumeData?.authorList?.[index]){
  324. if(!volumeData?.authorList){
  325. // console.log(volumeData)
  326. volumeData.authorList = []
  327. }
  328. volumeData.authorList.push({name:"",unit:"",birth:"",nationality:"",job:"",title:"",mobile:"",email:"",work:""})
  329. }
  330. }
  331. volumeData.authorList = volumeData?.authorList?.map((item,index)=>{ // 转换格式
  332. item.index = index + 1
  333. item.birth = toYearMonth(item?.birth)
  334. if(item?.examine){
  335. if(!(isChildren&&volumeIndex==0)){ // 子册第一册不重复导出
  336. // totalJson.examineList = pushDistinctItem(totalJson.examineList,item) // 名称去重
  337. // console.log(item)
  338. totalJson.zhengshenList.push(item?.examine);
  339. totalJson.examineList.push(item); // 全量导出
  340. }
  341. }
  342. return item
  343. })
  344. // console.log(totalJson.examineList)
  345. json.authorList = volumeData?.authorList;
  346. // 其他编者政治审查表
  347. totalJson.otherList = totalJson.otherList || []
  348. if(volumeData?.otherEditor){
  349. if(!(isChildren&&volumeIndex==0)){ // 子册第一册不重复导出
  350. // totalJson.otherList = pushDistinctItem(totalJson.otherList,volumeData?.otherEditor) // 名称去重
  351. totalJson.zhengshenList.push(volumeData?.otherEditor)
  352. totalJson.otherList.push(volumeData?.otherEditor); //全量导出
  353. }
  354. }
  355. // 成果列表
  356. // aclist[index] = [json?.achievementOptions?.[index]?.name||" ",json?.achievementOptions?.[index]?.unit||" ",toYearMonth(json?.achievementOptions?.[index]?.date)||" "]
  357. for (let index = 0; index < 5; index++) { // 补充空值
  358. if(!volumeData?.achievementOptions){
  359. volumeData.achievementOptions = []
  360. }
  361. if(!volumeData?.achievementOptions?.[index]){
  362. volumeData.achievementOptions.push({name:"",unit:"",date:""})
  363. }
  364. volumeData.achievementOptions = volumeData?.achievementOptions?.map((item,index)=>{ // 转换格式
  365. item.index = index + 1
  366. item.date = toYearMonth(item?.date)
  367. return item
  368. })
  369. }
  370. json.achievementOptions = volumeData?.achievementOptions;
  371. // 历程列表
  372. // clist[index] = [num,toYearMonth(json?.courses?.[index]?.date)||" ",json?.courses?.[index]?.wordage||" ",json?.courses?.[index]?.num||" ",json?.courses?.[index]?.sumNum||" ",json?.courses?.[index]?.accolade||" "]
  373. for (let index = 0; index < 5; index++) { // 补充空值
  374. if(!volumeData?.courses){
  375. volumeData.courses = []
  376. }
  377. if(!volumeData?.courses?.[index]){
  378. volumeData?.courses.push({date:"",wordage:"",num:"",sumNum:"",accolade:""})
  379. }
  380. }
  381. volumeData.courses = volumeData?.courses?.map((item,index)=>{ // 转换格式
  382. item.index = index + 1
  383. item.date = toYearMonth(item?.date)
  384. return item
  385. })
  386. json.courses = volumeData?.courses;
  387. return json
  388. }
  389. function pushDistinctItem(list,item){
  390. let isDistinct = true
  391. if(list?.indexOf(item)>-1){
  392. isDistinct = false;
  393. }
  394. if(list?.find(old=>old?.name&&old?.name==item?.name)){
  395. isDistinct = false;
  396. }
  397. if(isDistinct){
  398. list.push(item);
  399. }
  400. return list
  401. }
  402. function renderReportDocsByTextbook(textbook,isNew){
  403. let TID = textbook?.id;
  404. console.log("renderReportDocsByTextbook",textbook?.id)
  405. return new Promise(async (resolve)=>{
  406. let json = textbook.toJSON();
  407. json.TID = TID;
  408. json.hasNoSign = false; // 未上传作者签名
  409. json.hasNoUnit = false; // 未上传单位资料
  410. json.hasNoNine = false; // 无需添加滞后九
  411. let bookid = json.code || json?.objectId;
  412. let exportPDF = textbook.get("exportPDF");
  413. if(isNew) exportPDF = null;
  414. if(exportPDF){
  415. // console.log(exportPDF)
  416. let exportPath = await downloadUrl(exportPDF,{tempDir:path.join(tempDir,textbook.id),keepFileName:true});
  417. // console.log(exportPath)
  418. resolve({
  419. code:bookid,
  420. objectId:textbook.id,
  421. title:json?.title,
  422. pdfPath:exportPath,
  423. urlPdf:exportPDF,
  424. })
  425. return
  426. }
  427. // 默认单册为第一分册
  428. let book1Data = json?.childrens?.[0]
  429. // console.log(json,book1Data)
  430. if(book1Data){
  431. Object.keys(book1Data).forEach(key=>{
  432. json[key] = book1Data[key] || json[key] || ""
  433. })
  434. }
  435. // 联系电话:默认为作者首个存在的电话;
  436. let mobile = json?.authorList?.find(item => item.mobile)?.mobile || ""
  437. // 填报时间:默认为创建时间
  438. let createdAt = new Date(textbook?.createdAt);
  439. let createdDate = `${createdAt?.getFullYear()}年${createdAt?.getMonth()+1}月${createdAt?.getDate()}日`;
  440. // 教材适用
  441. let characteristic = (json?.characteristic?.filter(item=>item?.checked).map(item=>item.label) || [])?.join(",")
  442. json = fixVolumeData(json,book1Data,0,json)
  443. let volumeList = []
  444. for (let index = 0; index < 12; index++) {
  445. if(json?.childrens?.[index]){
  446. let volumeData = json?.childrens?.[index]
  447. volumeData = fixVolumeData(volumeData,volumeData,index,json,true)
  448. volumeList.push(volumeData)
  449. }
  450. }
  451. // 作者列表 限6人
  452. let alist = []
  453. for (let index = 0; index < 9; index++) {
  454. alist[index] = [json?.authorList?.[index]?.name||" ",json?.authorList?.[index]?.unit||" ",toYearMonth(json?.authorList?.[index]?.birth)||" ",json?.authorList?.[index]?.nationality||" ",json?.authorList?.[index]?.job||" ",json?.authorList?.[index]?.title||" ",json?.authorList?.[index]?.mobile||" ",json?.authorList?.[index]?.email||" ",json?.authorList?.[index]?.work||" "]
  455. }
  456. // 相关成果 限5项
  457. let aclist = []
  458. for (let index = 0; index < 5; index++) {
  459. aclist[index] = [json?.achievementOptions?.[index]?.name||" ",json?.achievementOptions?.[index]?.unit||" ",toYearMonth(json?.achievementOptions?.[index]?.date)||" "]
  460. }
  461. // 申报历程 限4项
  462. let clist = []
  463. for (let index = 0; index < 4; index++) {
  464. let num = json?.courses?.[index]?(index+1):" ";
  465. clist[index] = [num,toYearMonth(json?.courses?.[index]?.date)||" ",json?.courses?.[index]?.wordage||" ",json?.courses?.[index]?.num||" ",json?.courses?.[index]?.sumNum||" ",json?.courses?.[index]?.accolade||" "]
  466. }
  467. // 附件信息
  468. let mergeFiles = []
  469. // 教材电子版
  470. // (教材出版单位配合按要求上传各地推荐的本单位出版的纸质教材最新印次的完整PDF电子版;数字教材上传全部教材内容电子版或填写能够查看全部教材内容的链接地址、账号;纸质教材附带数字资源的,上传纸质教材最新印次的完整PDF电子版,以及全部数字资源电子版或能够查看全部数字资源内容的链接地址、账号。)
  471. let isNotImpt = (json?.approval?.indexOf("101计划")==-1) && (json?.approval?.indexOf("中央有关部门")==-1) && (json?.approval?.indexOf("四新")==-1) && (json?.approval?.indexOf("首届全国")==-1);
  472. // 1.获批截图
  473. if(json?.approvedImgUrl && !isNotImpt){
  474. mergeFiles.push(json?.approvedImgUrl);
  475. }
  476. // 2.所有作者政治审查意见(必须提供)
  477. // (对应作者姓名上传“作者政治审查表”。作者单位党委对作者进行审查,对政治思想表现情况进行评价,确保作者的正确政治方向、价值取向,无违法违纪等记录。教材编写成员涉及多个不同单位时需要各单位分别出具意见,并由所在单位党委盖章,格式要求从申报平台下载。)
  478. // console.log("json.examineList");
  479. // console.log("json.examineList");
  480. // console.log("json.examineList");
  481. // console.log("json.examineList");
  482. // console.log(json.examineList);
  483. // json.examineList.forEach(author=>{
  484. // if(author?.examine) mergeFiles.push(author?.examine)
  485. // })
  486. // console.log(mergeFiles)
  487. // let otherMap = {}
  488. json?.zhengshenList?.forEach(item=>{ // 合并不重复其他编者政治审查表
  489. // if(otherMap[item]) return
  490. // otherMap[item] = true
  491. mergeFiles.push(item)
  492. })
  493. // json?.otherList?.forEach(item=>{ // 合并不重复其他编者政治审查表
  494. // if(otherMap[item]) return
  495. // otherMap[item] = true
  496. // mergeFiles.push(item)
  497. // })
  498. // 3.图书编校质量自查结果记录表(必须提供)
  499. // (教材出版单位对申报教材的编校质量自查后,按要求提供图书编校质量自查结果记录表,并加盖出版社公章。全册教材的不同分册以不同文件分别上传。格式要求从申报平台下载。)
  500. if(json?.selfResults?.url){mergeFiles.push(json?.selfResults?.url)}
  501. // 4.专家审查意见表(必须提供)
  502. // (由第一主编所在单位或出版机构邀请校内外相关学科专业领域专家,对教材进行思想性、学术性审查。专家不少于3名,其中半数以上为校外专家,专家分别实名评价并签字,并注明所在单位及专业身份。评价人不得是本教材的作者。)
  503. if(json?.expertOpinion?.url){mergeFiles.push(json?.expertOpinion?.url)}
  504. // 5.教材使用情况证明材料(必须提供)
  505. // (教材出版单位提供教材主要使用高校名单及使用情况证明材料,并加盖公章。)
  506. if(json?.evidence?.url){mergeFiles.push(json?.evidence?.url)}
  507. // 6.版权信息及CIP数据(必须提供)
  508. // (版权页截图,中国版本图书馆CIP查询截图,如CIP数据中无“教材”字样的,须再上传内容提要或前言或后记中可以证明本书为教材的相关内容截图。)
  509. //6.1 版权页截图
  510. if(json?.copyrightImgUrl){mergeFiles.push(json?.copyrightImgUrl)}
  511. //6.2 中国版本图书馆 CIP 查询截图
  512. if(json?.CIPImgUrl){mergeFiles.push(json?.CIPImgUrl)}
  513. //6.3 CIP 相关证明截图
  514. if(json?.cipProveFile?.length){
  515. json?.cipProveFile.forEach(item=>{
  516. item?.url && mergeFiles.push(item?.url)
  517. })
  518. }
  519. // 7.其他材料(可选提供)
  520. if(json?.moreMaterial?.length){
  521. json?.moreMaterial.forEach(doc=>{
  522. if(doc?.url){
  523. mergeFiles.push(doc?.url)
  524. }
  525. })
  526. }
  527. let bookTempDir = path.join(tempDir,json?.TID)
  528. if(!fs.existsSync(bookTempDir)) fs.mkdirSync(bookTempDir)
  529. let codePngBuffer = await toBarCode(json?.code);
  530. let codePngPath = path.join(bookTempDir,bookid+"code.png")
  531. fs.writeFileSync(codePngPath,codePngBuffer)
  532. // let codeBarImg =
  533. // console.log(codeBarImg)
  534. // (其他佐证材料,限两份以内。)
  535. console.log(json?.approval)
  536. // let isNotImpt = (json?.approval?.indexOf("101计划")==-1) && (json?.approval?.indexOf("中央有关部门")==-1) && (json?.approval?.indexOf("四新")==-1) && (json?.approval?.indexOf("首届全国")==-1);
  537. let fixData = {
  538. volumeList:volumeList,
  539. // 图片信息
  540. codeBarImg:codePngPath,
  541. // 封面信息
  542. titlePad:padString(json?.title,21),
  543. ISBNPad:fromatFiled(json?.childrens, 'ISBN') || padString(json?.ISBN,21),
  544. ISBN:json?.ISBN,
  545. inviteUnit:json?.inviteUnit||json?.department?.name||"",
  546. one:squareCheck[(json?.type=="单本"||json?.type=="单册")?1:0], // 单本/单册 方框
  547. full:squareCheck[json?.type=="全册"?1:0], // 全册
  548. oneCircle:circleCheck[(json?.type=="单本"||json?.type=="单册")?1:0], // 单本/单册 圆圈
  549. fullCircle:circleCheck[json?.type=="全册"?1:0], // 全册
  550. tn:json?.typeNumber,
  551. authorPad:fromatFiled(json?.childrens, 'author') || padString(json?.author,21),
  552. mobile:padString(mobile,21),
  553. authorUnit:fromatFiled(json?.childrens, 'unit') || padString(json?.unit,21),
  554. publisherPad:fromatFiled(json?.childrens, 'editionUnit') || padString(json?.editionUnit,21),
  555. createdDate:padString(createdDate,21),
  556. // 基本信息
  557. title:json?.title,
  558. isJC: circleCheck[(json?.approval?.indexOf("基础")>-1)?1:0],
  559. isZL: circleCheck[(json?.approval?.indexOf("战略")>-1)?1:0],
  560. is101: circleCheck[(json?.approval?.indexOf("101计划")>-1)?1:0], // 2024新重点
  561. isZY: circleCheck[(json?.approval?.indexOf("中央有关部门")>-1)?1:0],
  562. isSX: circleCheck[(json?.approval?.indexOf("四新")>-1)?1:0],
  563. isJS: circleCheck[(json?.approval?.indexOf("首届全国")>-1)?1:0],
  564. isNotImpt: circleCheck[isNotImpt?1:0],
  565. // 教材适用情况
  566. lessons:json?.lessons||[],
  567. period:json?.period||"", // 学时
  568. isBX:squareCheck[(characteristic?.indexOf("必修")>-1)?1:0],
  569. isXX:squareCheck[(characteristic?.indexOf("选修")>-1)?1:0],
  570. isTS:circleCheck[(characteristic?.indexOf("通识")>-1)?1:0],
  571. isGG:circleCheck[(characteristic?.indexOf("公共")>-1)?1:0],
  572. isZYK:circleCheck[(characteristic?.indexOf("专业")>-1)?1:0],
  573. isSXZZ:squareCheck[(characteristic?.indexOf("思想")>-1)?1:0],
  574. isSY:squareCheck[(characteristic?.indexOf("实验")>-1)?1:0],
  575. // 作者列表
  576. a11:alist[0][0],a12:alist[0][1],a13:alist[0][2],a14:alist[0][3],a15:alist[0][4],a16:alist[0][5],a17:alist[0][6],a18:alist[0][7],a19:alist[0][8],
  577. a21:alist[1][0],a22:alist[1][1],a23:alist[1][2],a24:alist[1][3],a25:alist[1][4],a26:alist[1][5],a27:alist[1][6],a28:alist[1][7],a29:alist[1][8],
  578. a31:alist[2][0],a32:alist[2][1],a33:alist[2][2],a34:alist[2][3],a35:alist[2][4],a36:alist[2][5],a37:alist[2][6],a38:alist[2][7],a39:alist[2][8],
  579. a41:alist[3][0],a42:alist[3][1],a43:alist[3][2],a44:alist[3][3],a45:alist[3][4],a46:alist[3][5],a47:alist[3][6],a48:alist[3][7],a49:alist[3][8],
  580. a51:alist[4][0],a52:alist[4][1],a53:alist[4][2],a54:alist[4][3],a55:alist[4][4],a56:alist[4][5],a57:alist[4][6],a58:alist[4][7],a59:alist[4][8],
  581. a61:alist[5][0],a62:alist[5][1],a63:alist[5][2],a64:alist[5][3],a65:alist[5][4],a66:alist[5][5],a67:alist[5][6],a68:alist[5][7],a69:alist[5][8],
  582. // 成果列表
  583. ac11:aclist[0][0],ac12:aclist[0][1],ac13:aclist[0][2],
  584. ac21:aclist[1][0],ac22:aclist[1][1],ac23:aclist[1][2],
  585. ac31:aclist[2][0],ac32:aclist[2][1],ac33:aclist[2][2],
  586. ac41:aclist[3][0],ac42:aclist[3][1],ac43:aclist[3][2],
  587. ac51:aclist[4][0],ac52:aclist[4][1],ac53:aclist[4][2],
  588. // 历程列表
  589. c11:clist[0][0],c12:clist[0][1],c13:clist[0][2],c14:clist[0][3],c15:clist[0][4],c16:clist[0][5],
  590. c21:clist[1][0],c22:clist[1][1],c23:clist[1][2],c24:clist[1][3],c25:clist[1][4],c26:clist[1][5],
  591. c31:clist[2][0],c32:clist[2][1],c33:clist[2][2],c34:clist[2][3],c35:clist[2][4],c36:clist[2][5],
  592. c41:clist[3][0],c42:clist[3][1],c43:clist[3][2],c44:clist[3][3],c45:clist[3][4],c46:clist[3][5],
  593. }
  594. let bookData = json;
  595. Object.keys(fixData).forEach(key=>{
  596. bookData[key] = fixData[key] || bookData[key] || ""
  597. if(typeof bookData[key]=="string"){
  598. bookData[key] = bookData[key].replaceAll("\x02"," ")
  599. bookData[key] = bookData[key].replaceAll("\u0002"," ") // 字段填写字符中,存在无法转码的乱码字符
  600. if(key=="ac31"){
  601. console.log(bookData[key])
  602. }
  603. }
  604. })
  605. // console.log(bookData)
  606. // console.log(json)
  607. let title = bookData.title?.replaceAll("/","")
  608. let tempFileName = path.join(`${bookid}_${title}_申报书及附件.docx`)
  609. // mergeFiles 处理七八九是否签名或后置问题
  610. let lastPageList = []
  611. let docx7 = await getPageDocx(json?.TID,bookid,"七",bookData);
  612. let docx8 = await getPageDocx(json?.TID,bookid,"八",bookData);
  613. let docx9 = await getPageDocx(json?.TID,bookid,"九",bookData);
  614. lastPageList.push(json?.authorSignPDF?.url||docx7);
  615. lastPageList.push(json?.unitMaterial?.url||docx8);
  616. lastPageList.push(docx9);
  617. let lastPageFileName = path.join(bookTempDir,`${bookid}_${title}_申报书及附件_789.pdf`)
  618. console.log(lastPageList)
  619. let lastPagePdf = await docxToPdf(null,lastPageFileName,{mergeFiles:lastPageList,tempDir:bookTempDir,mergeAll:true}) // 成功用pdf,失败继续用docx
  620. docx7&&fs.rmSync(docx7)
  621. docx8&&fs.rmSync(docx8)
  622. docx9&&fs.rmSync(docx9)
  623. // mergeFiles = [...lastPageList,...mergeFiles]
  624. mergeFiles = [lastPagePdf,...mergeFiles]
  625. // 开始文件合并导出
  626. let filePath,pdfPath,urlDocx,urlPdf
  627. let TemplateDocxPath
  628. if(json?.childrens?.length>1){
  629. TemplateDocxPath = path.join(TemplateDocxDir,"模板-本科教材申报书-全册.docx")
  630. }else{
  631. TemplateDocxPath = path.join(TemplateDocxDir,"模板-本科教材申报书-单册.docx")
  632. }
  633. try{
  634. // DOCX模板合成 速度2-3秒
  635. filePath = renderDocx(TemplateDocxPath,tempFileName,bookData,{tempDir:bookTempDir})
  636. urlDocx = (await uploadFileToOSS(filePath))?.url || null
  637. console.log("DOCX CREATED:",filePath)
  638. // PDF文档拼接 速度10-30s 需要API支持
  639. pdfPath = filePath.replaceAll(".docx",".pdf")
  640. let options = {
  641. mergeFiles:mergeFiles,
  642. tempDir:bookTempDir
  643. }
  644. pdfPath = await docxToPdf(filePath,pdfPath,options) || filePath // 成功用pdf,失败继续用docx
  645. console.log("PDF CREATED:",filePath)
  646. if(pdfPath){
  647. urlPdf = (await uploadFileToOSS(pdfPath,json?.objectId))?.url || null
  648. if(urlPdf){
  649. textbook.set("exportPDF",urlPdf);
  650. textbook.save();
  651. }
  652. }
  653. }catch(err){
  654. console.error(err)
  655. }
  656. resolve({
  657. code:bookid,
  658. objectId:json?.TID,
  659. title:json?.title,
  660. filePath,
  661. pdfPath,
  662. urlDocx,
  663. urlPdf,
  664. })
  665. return
  666. })
  667. }
  668. async function getPageDocx(objectId,bookid,pageName,bookData){
  669. let tplPath = path.join(TemplateDocxDir,`模板-本科教材申报书-${pageName}.docx`);
  670. let title = bookData?.title?.replaceAll("/","")
  671. let tempFileName = path.join(objectId,`${bookid}_${title}_申报书及附件_${pageName}.docx`);
  672. let filePath = renderDocx(tplPath,tempFileName,bookData);
  673. return filePath;
  674. // 转pdf
  675. // let pdfPath = filePath.replaceAll(".docx",".pdf")
  676. // let options = {}
  677. // pdfPath = await docxToPdf(null,pdfPath,options) || filePath // 成功用pdf,失败继续用docx
  678. // return pdfPath
  679. }
  680. function toYearMonth(date){
  681. // console.log("toYearMonth",date)
  682. if(typeof date == "string" && (date?.indexOf("年")>-1 || date?.indexOf("-")>-1)) return date
  683. if(!date) return ""
  684. let datestr = date?.iso||date
  685. if(!datestr) return ""
  686. date = new Date(datestr);
  687. // return `${date.getFullYear()}年${date.getMonth()+1}月`
  688. return `${date.getFullYear()}-${get2bitint(date.getMonth()+1)}`
  689. }
  690. function get2bitint(value){
  691. if(value>=10) return value;
  692. if(value>=0&&value<10) return "0" + value;
  693. return value
  694. }
  695. function padString(str,width) {
  696. str = str || ""
  697. return str
  698. str = String(str)
  699. width = width || 21 // 目标宽度为21个单位
  700. let spaceChar = " " // 占位符 render可用空格,但document.xml用&#160;
  701. // 计算字符串的宽度
  702. const charWidth = {
  703. 'space': 1, // 空格占用1个单位
  704. 'zh': 3, // 汉字占用3个单位
  705. 'en': 1, // 英文字母占用1个单位
  706. 'other': 1 // 其他字符(如标点符号)占用1个单位
  707. };
  708. let strWidth = 0;
  709. // console.log(str)
  710. // 遍历文本中的每个字符
  711. for (let char of str) {
  712. if (/\s/.test(char)) {
  713. strWidth += charWidth.space; // 空格
  714. } else if (/[\u4e00-\u9fa5]/.test(char)) {
  715. strWidth += charWidth.zh; // 汉字
  716. } else if (/[a-zA-Z]/.test(char)) {
  717. strWidth += charWidth.en; // 英文字母
  718. } else {
  719. strWidth += charWidth.other; // 其他字符
  720. }
  721. }
  722. // for (let char of str) {
  723. // // 判断字符是否为中文
  724. // if (char.match(/[\u4e00-\u9fa5]/)) {
  725. // strWidth += 2; // 中文字符占4个单位
  726. // } else {
  727. // strWidth += 1; // 英文字符占1个单位
  728. // }
  729. // }
  730. const totalPadding = width - strWidth;
  731. // 如果已经达到或超过目标宽度,直接返回原字符串
  732. if (totalPadding <= 0) {
  733. return str;
  734. }
  735. // 计算左右两侧的空格数
  736. const leftPadding = Math.floor(totalPadding / 2) * 3;
  737. const rightPadding = Math.ceil(totalPadding / 2) * 3;
  738. // 生成填充空格的字符串
  739. const leftSpaces = spaceChar.repeat(leftPadding);
  740. const rightSpaces = spaceChar.repeat(rightPadding);
  741. // 返回补充后的字符串
  742. return leftSpaces + str + rightSpaces;
  743. }
  744. /* 格式化重复字段(首页多册情况展示:书号、第一主编、主编单位、出 版 社) */
  745. function fromatFiled(list, filed){
  746. let arr = [];
  747. list.forEach((item) => {
  748. arr.push(item[filed]);
  749. });
  750. let j = Array.from(new Set(arr)).join('、');
  751. return j || '-';
  752. }