func-tbook-export.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. import { replaceDocx, createZip, uploadFileToOSS, docxToPdf, renderDocx } from "../../lib/docs";
  2. // const Parse = global.Parse;
  3. const path = require("path")
  4. const fs = require("fs")
  5. var TemplateDocxPath = path.join(__dirname,"template/模板-推荐申报表.docx")
  6. if(!fs.existsSync(TemplateDocxPath)){
  7. TemplateDocxPath = path.join(__dirname,"../../template/模板-推荐申报表.docx")
  8. }
  9. /**
  10. * 定义导出申报合集文档云函数
  11. * @example
  12. * Cloud Code test
  13. 导出流程
  14. 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
  15. 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
  16. 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
  17. 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
  18. 导出教材列表
  19. 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
  20. */
  21. export function defineTbookExportReport(app){
  22. app.post("/parse/api/tbook/export",async (request,res)=>{
  23. let processId = request.body.processId;
  24. let bookList = request.body.bookList;
  25. console.log(request.body)
  26. try{
  27. let result
  28. if(processId){
  29. result = await exportProcessReportDocs(processId)
  30. }
  31. if(bookList?.length){
  32. result = await exportProcessReportDocs(null,bookList)
  33. }
  34. if(result?.docsList?.length==0){
  35. // throw new Parse.Error(404,"合集内无申报教材")
  36. res.json({
  37. code:400,
  38. err:"合集内无申报教材"
  39. })
  40. }
  41. // return result
  42. res.json({
  43. code:200,
  44. result:result
  45. })
  46. }catch(err){
  47. console.error(err)
  48. res.json({
  49. code:400,
  50. err:err
  51. })
  52. // throw new Parse.Error(404,"导出申报合集失败")
  53. }
  54. // throw new Parse.Error(404,"未找到该流程合集")
  55. })
  56. Parse.Cloud.define("tbookExportReport", async (request) => {
  57. let processId = request.params.processId;
  58. let bookList = request.params.bookList;
  59. try{
  60. let result
  61. if(processId){
  62. result = await exportProcessReportDocs(processId)
  63. }
  64. if(bookList?.length){
  65. result = await exportProcessReportDocs(null,bookList)
  66. }
  67. if(result?.docsList?.length==0){
  68. throw new Parse.Error(404,"合集内无申报教材")
  69. }
  70. return result
  71. }catch(err){
  72. console.error(err)
  73. throw new Parse.Error(404,"导出申报合集失败")
  74. }
  75. throw new Parse.Error(404,"未找到该流程合集")
  76. },{
  77. fields : {
  78. processId:{
  79. required:false
  80. },
  81. }
  82. });
  83. }
  84. /**
  85. * 导出流程教材申报文件
  86. * @returns
  87. * docsList
  88. * zipUrl
  89. */
  90. export async function exportProcessReportDocs(processId,bookList) {
  91. if(!processId && !bookList?.length) return {}
  92. let textbookList
  93. if(processId){ // 流程读取教材列表
  94. let query = new Parse.Query("EduTextbook")
  95. query.equalTo("eduProcess",processId);
  96. textbookList = await query.find();
  97. }
  98. if(bookList?.length){ // 直接导出教材列表
  99. let query = new Parse.Query("EduTextbook")
  100. query.containedIn("objectId",bookList);
  101. textbookList = await query.find();
  102. }
  103. let docsList = []
  104. let plist = []
  105. for (let index = 0; index < textbookList.length; index++) {
  106. let textbook = textbookList[index];
  107. console.log("textbook",index)
  108. // 直接将异步调用的 Promise 添加到 plist
  109. plist.push(renderReportDocsByTextbook(textbook)); // 立即执行并返回 Promise
  110. }
  111. let presults = await Promise.all(plist);
  112. presults.forEach(result=>{
  113. if(result?.filePath){
  114. docsList.push(result)
  115. }
  116. })
  117. let zipPath,zipUrl
  118. if(docsList?.length){
  119. let now = new Date();
  120. let fileList = docsList?.map(item=>item?.pdfPath);
  121. let zipName = `申报书导出-${now.getFullYear()}${now.getMonth()+1}${now.getDate()}-${now.getHours()}${now.getMinutes()}${now.getSeconds()}.zip`
  122. zipPath = await createZip(fileList,zipName)
  123. if(zipPath){
  124. zipUrl = (await uploadFileToOSS(zipPath))?.url || null
  125. }
  126. docsList = docsList.map(item=>{return {code:item.code,title:item.title,url:item?.url}})
  127. }
  128. let result = {
  129. docsList,
  130. zipUrl
  131. }
  132. return result
  133. }
  134. module.exports.exportProcessReportDocs = exportProcessReportDocs
  135. function renderReportDocsByTextbook(textbook){
  136. console.log("renderReportDocsByTextbook")
  137. let json = textbook.toJSON();
  138. // console.log(json)
  139. // 圆圈选中未选 ○ 未选 ● 选中
  140. let circleCheck = ["○","●"];
  141. // 方块选中未选 ○ 未选 ● 选中
  142. let squareCheck = [`□`,String.fromCharCode(0xFE)];
  143. // 联系电话:默认为作者首个存在的电话;
  144. let mobile = json?.authorList?.find(item => item.mobile)?.mobile || ""
  145. // 填报时间:默认为创建时间
  146. let createdAt = new Date(textbook?.createdAt);
  147. let createdDate = `${createdAt?.getFullYear()}年${createdAt?.getMonth()+1}月${createdAt?.getDate()}日`;
  148. // 专业代码:前四位
  149. let majorCode = json?.majorId || json?.major?.code
  150. let majorCode6
  151. if(majorCode?.length>4){
  152. majorCode = majorCode.slice(0,4)
  153. majorCode6 = majorCode.slice(0,6)
  154. }
  155. let majorName = json?.majorName || json?.major?.name
  156. // 是否重点立项
  157. let importantProject = json?.importantProject?.join();
  158. let isJC = circleCheck[(json?.approval?.indexOf("基础")>-1)?1:0];
  159. let isZL = circleCheck[(json?.approval?.indexOf("战略")>-1)?1:0];
  160. let is101 = circleCheck[(json?.approval?.indexOf("101计划")>-1)?1:0]; // 2024新重点
  161. let isZY = circleCheck[(json?.approval?.indexOf("中央")>-1)?1:0];
  162. let isSX = circleCheck[(json?.approval?.indexOf("四新")>-1)?1:0];
  163. let isJS = circleCheck[(importantProject?.indexOf("建设")>-1)?1:0];
  164. let isNotImpt = (json?.approval?.indexOf("101计划")==-1) && (json?.approval?.indexOf("中央")==-1) && (json?.approval?.indexOf("四新")==-1) && (json?.approval?.indexOf("建设")==-1)
  165. isNotImpt = circleCheck[isNotImpt?1:0];
  166. // 初版时间
  167. let firstDate = new Date(textbook?.get("editionFirst"));
  168. let firstYear = firstDate?.getFullYear();
  169. let firstMonth = firstDate?.getMonth()+1;
  170. // 本版时间印次
  171. let currentDate = new Date(textbook?.get("editionDate"));
  172. let currentYear = currentDate?.getFullYear();
  173. let currentMonth = currentDate?.getMonth()+1;
  174. // 最新时间印次
  175. let latestDate = new Date(textbook?.get("printDate"));
  176. let latestYear = latestDate?.getFullYear();
  177. let latestMonth = latestDate?.getMonth()+1;
  178. // 初版至今重点项目
  179. let isBSQT = !((importantProject?.indexOf("建设")>-1) || (importantProject?.indexOf("本科国家")>-1) || (importantProject?.indexOf("省级优秀")>-1) || (importantProject?.indexOf("省级规划")>-1))// 是否其他省级奖项
  180. let bsqtName = (json?.importantProject || [])?.filter(item=>(item?.indexOf("建设")==-1&&item?.indexOf("本科国家")==-1&&item?.indexOf("省级优秀")==-1&&item?.indexOf("省级规划")==-1));
  181. // 教材适用
  182. let characteristic = (json?.characteristic?.filter(item=>item?.checked).map(item=>item.label) || [])?.join(",")
  183. // 作者列表 限6人
  184. let alist = []
  185. for (let index = 0; index < 9; index++) {
  186. 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||" "]
  187. }
  188. // 相关成果 限5项
  189. let aclist = []
  190. for (let index = 0; index < 5; index++) {
  191. aclist[index] = [json?.achievementOptions?.[index]?.name||" ",json?.achievementOptions?.[index]?.unit||" ",toYearMonth(json?.achievementOptions?.[index]?.date)||" "]
  192. }
  193. // 申报历程 限4项
  194. let clist = []
  195. for (let index = 0; index < 4; index++) {
  196. let num = json?.courses?.[index]?(index+1):" ";
  197. clist[index] = [num,toYearMonth(json?.courses?.[index]?.date)||" ",json?.courses?.[index]?.wordage||" ",json?.courses?.[index]?.num||" ",json?.courses?.[index]?.sumNum||" ",json?.courses?.[index]?.accolade||" "]
  198. }
  199. // 附件信息
  200. let mergeFiles = []
  201. // 1.教材电子版(必须提供)
  202. // (教材出版单位配合按要求上传各地推荐的本单位出版的纸质教材最新印次的完整PDF电子版;数字教材上传全部教材内容电子版或填写能够查看全部教材内容的链接地址、账号;纸质教材附带数字资源的,上传纸质教材最新印次的完整PDF电子版,以及全部数字资源电子版或能够查看全部数字资源内容的链接地址、账号。)
  203. // 2.所有作者政治审查意见(必须提供)
  204. // (对应作者姓名上传“作者政治审查表”。作者单位党委对作者进行审查,对政治思想表现情况进行评价,确保作者的正确政治方向、价值取向,无违法违纪等记录。教材编写成员涉及多个不同单位时需要各单位分别出具意见,并由所在单位党委盖章,格式要求从申报平台下载。)
  205. // 3.图书编校质量自查结果记录表(必须提供)
  206. // (教材出版单位对申报教材的编校质量自查后,按要求提供图书编校质量自查结果记录表,并加盖出版社公章。全册教材的不同分册以不同文件分别上传。格式要求从申报平台下载。)
  207. if(json?.selfResults?.url){mergeFiles.push(json?.selfResults?.url)}
  208. // 4.专家审查意见表(必须提供)
  209. // (由第一主编所在单位和出版机构邀请校内外相关学科专业领域专家,对教材进行思想性、学术性审查。专家不少于3名,其中半数以上为校外专家,专家分别实名评价并签字,并注明所在单位及专业身份。评价人不得是本教材的作者。)
  210. if(json?.expertOpinion?.url){mergeFiles.push(json?.expertOpinion?.url)}
  211. // 5.教材使用情况证明材料(必须提供)
  212. // (教材出版单位提供教材主要使用高校名单及使用情况证明材料,并加盖公章。)
  213. if(json?.evidence?.url){mergeFiles.push(json?.evidence?.url)}
  214. // 6.版权信息及CIP数据(必须提供)
  215. // (版权页截图,中国版本图书馆CIP查询截图,如CIP数据中无“教材”字样的,须再上传内容提要或前言或后记中可以证明本书为教材的相关内容截图。)
  216. if(json?.CIPImgUrl){mergeFiles.push(json?.CIPImgUrl)}
  217. // 7.其他材料(可选提供)
  218. if(json?.moreMaterial?.length){
  219. json?.moreMaterial.forEach(doc=>{
  220. if(doc?.url){
  221. mergeFiles.push(doc?.url)
  222. }
  223. })
  224. }
  225. // (其他佐证材料,限两份以内。)
  226. let fixData = {
  227. // 封面信息
  228. titlePad:padString(json?.title,21),
  229. ISBNPad:padString(json?.ISBN,21),
  230. ISBN:json?.ISBN,
  231. one:squareCheck[(json?.type=="单本"||json?.type=="单册")?1:0], // 单本/单册 方框
  232. full:squareCheck[json?.type=="全册"?1:0], // 全册
  233. oneCircle:circleCheck[(json?.type=="单本"||json?.type=="单册")?1:0], // 单本/单册 圆圈
  234. fullCircle:circleCheck[json?.type=="全册"?1:0], // 全册
  235. tn:json?.typeNumber,
  236. authorPad:padString(json?.author,21),
  237. mobile:padString(mobile,21),
  238. authorUnit:padString(json?.unit,21),
  239. publisherPad:padString(json?.editionUnit,21),
  240. recommandUnit:padString("",14), // 未找到
  241. majorCodePad:padString((majorCode),6),
  242. createdDate:padString(createdDate,21),
  243. // 基本信息
  244. title:json?.title,
  245. author:json?.author,
  246. unit:json?.unit,
  247. mc:majorCode,
  248. mc6:majorCode6,
  249. mn:majorName,
  250. lCN:circleCheck[(json?.lang=="中文")?1:0],
  251. lEN:circleCheck[(json?.lang=="英文")?1:0],
  252. lOT:circleCheck[(json?.lang?.indexOf("其他")>-1)?1:0],
  253. lSS:circleCheck[(json?.lang?.indexOf("少数")>-1)?1:0],
  254. authors:json?.authors, // 其他主编
  255. editor:json?.editor, // 其他编者
  256. isJC:isJC,
  257. isZL:isZL,
  258. isSX:isSX,
  259. is101:is101,
  260. isZY:isZY,
  261. isJS:isJS,
  262. isNotImpt:isNotImpt,
  263. publisher:json?.editionUnit,
  264. firstYear:firstYear,
  265. firstMonth:firstMonth,
  266. isZZ:circleCheck[(json?.carrierShape?.indexOf("纸质")>-1)?1:0],
  267. isDZ:circleCheck[(json?.carrierShape?.indexOf("电子")>-1)?1:0],
  268. isSZ:circleCheck[(json?.carrierShape?.indexOf("数字")>-1)?1:0],
  269. isQT:circleCheck[(json?.carrierShape?.indexOf("附带")>-1)?1:0],
  270. isFD:circleCheck[(json?.carrierShape?.indexOf("其他")>-1)?1:0],
  271. latestY:latestYear,
  272. latestM:latestMonth,
  273. latestNum:json?.printNumber || "",
  274. currentY:currentYear,
  275. currentM:currentMonth,
  276. currentNum:json?.editionNumber || "",
  277. printSum:json?.printSum || "",
  278. isBGJ:circleCheck[(importantProject?.indexOf("本科国家")>-1)?1:0],
  279. isBSYX:circleCheck[(importantProject?.indexOf("省级优秀")>-1)?1:0],
  280. isBSGH:circleCheck[(importantProject?.indexOf("省级规划")>-1)?1:0],
  281. isBSQT:circleCheck[isBSQT?1:0],
  282. bsqtName:bsqtName||"",
  283. isFirstNot:circleCheck[json?.importantProject?0:1],
  284. // 教材适用情况
  285. lessons:json?.lessons||[],
  286. period:json?.period||"", // 学时
  287. isBX:squareCheck[(characteristic?.indexOf("必修")>-1)?1:0],
  288. isXX:squareCheck[(characteristic?.indexOf("选修")>-1)?1:0],
  289. isTS:circleCheck[(characteristic?.indexOf("通识")>-1)?1:0],
  290. isGG:circleCheck[(characteristic?.indexOf("公共")>-1)?1:0],
  291. isZY:circleCheck[(characteristic?.indexOf("专业")>-1)?1:0],
  292. isSXZZ:squareCheck[(characteristic?.indexOf("思想")>-1)?1:0],
  293. isSY:squareCheck[(characteristic?.indexOf("实验")>-1)?1:0],
  294. // 作者列表
  295. 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],
  296. 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],
  297. 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],
  298. 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],
  299. 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],
  300. 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],
  301. // 成果列表
  302. ac11:aclist[0][0],ac12:aclist[0][1],ac13:aclist[0][2],
  303. ac21:aclist[1][0],ac22:aclist[1][1],ac23:aclist[1][2],
  304. ac31:aclist[2][0],ac32:aclist[2][1],ac33:aclist[2][2],
  305. ac41:aclist[3][0],ac42:aclist[3][1],ac43:aclist[3][2],
  306. ac51:aclist[4][0],ac52:aclist[4][1],ac53:aclist[4][2],
  307. // 历程列表
  308. c11:clist[0][0],c12:clist[0][1],c13:clist[0][2],c14:clist[0][3],c15:clist[0][4],c16:clist[0][5],
  309. c21:clist[1][0],c22:clist[1][1],c23:clist[1][2],c24:clist[1][3],c25:clist[1][4],c26:clist[1][5],
  310. c31:clist[2][0],c32:clist[2][1],c33:clist[2][2],c34:clist[2][3],c35:clist[2][4],c36:clist[2][5],
  311. c41:clist[3][0],c42:clist[3][1],c43:clist[3][2],c44:clist[3][3],c45:clist[3][4],c46:clist[3][5],
  312. }
  313. let bookData = json;
  314. Object.keys(fixData).forEach(key=>{
  315. bookData[key] = fixData[key]
  316. })
  317. // console.log(bookData)
  318. // console.log(json)
  319. let bookid = json.code || json?.objectId;
  320. let tempFileName = path.join(`${bookid}${json.title}.docx`)
  321. return new Promise(async (resolve)=>{
  322. let filePath,pdfPath,urlDocx,urlPdf
  323. try{
  324. // DOCX模板合成 速度2-3秒
  325. filePath = renderDocx(TemplateDocxPath,tempFileName,bookData)
  326. urlDocx = (await uploadFileToOSS(filePath))?.url || null
  327. console.log("DOCX CREATED:",filePath)
  328. // PDF文档拼接 速度10-30s 需要API支持
  329. pdfPath = filePath.replaceAll(".docx",".pdf")
  330. let options = {
  331. mergeFiles:mergeFiles
  332. }
  333. pdfPath = await docxToPdf(filePath,pdfPath,options) || filePath // 成功用pdf,失败继续用docx
  334. console.log("PDF CREATED:",filePath)
  335. if(pdfPath){
  336. urlPdf = (await uploadFileToOSS(pdfPath))?.url || null
  337. }
  338. }catch(err){
  339. console.error(err)
  340. }
  341. resolve({
  342. code:bookid,
  343. title:json?.title,
  344. filePath,
  345. pdfPath,
  346. urlDocx,
  347. urlPdf,
  348. })
  349. return
  350. replaceDocx(TemplateDocxPath,tempFileName,bookData,{onDocxComplete:async (filePath)=>{
  351. // 需要API支持
  352. let pdfPath = filePath.replaceAll(".docx",".pdf")
  353. filePath = await docxToPdf(filePath,pdfPath) || filePath // 成功用pdf,失败继续用docx
  354. let url = (await uploadFileToOSS(pdfPath))?.url || null
  355. resolve({
  356. code:bookid,
  357. title:json?.title,
  358. filePath,
  359. url
  360. })
  361. }})
  362. })
  363. }
  364. function toYearMonth(date){
  365. if(!date) return ""
  366. date = new Date(date?.iso||date);
  367. return `${date.getFullYear()}年${date.getMonth()+1}月`
  368. }
  369. function padString(str,width) {
  370. str = str || ""
  371. return str
  372. str = String(str)
  373. width = width || 21 // 目标宽度为21个单位
  374. let spaceChar = " " // 占位符 render可用空格,但document.xml用&#160;
  375. // 计算字符串的宽度
  376. const charWidth = {
  377. 'space': 1, // 空格占用1个单位
  378. 'zh': 3, // 汉字占用3个单位
  379. 'en': 1, // 英文字母占用1个单位
  380. 'other': 1 // 其他字符(如标点符号)占用1个单位
  381. };
  382. let strWidth = 0;
  383. // console.log(str)
  384. // 遍历文本中的每个字符
  385. for (let char of str) {
  386. if (/\s/.test(char)) {
  387. strWidth += charWidth.space; // 空格
  388. } else if (/[\u4e00-\u9fa5]/.test(char)) {
  389. strWidth += charWidth.zh; // 汉字
  390. } else if (/[a-zA-Z]/.test(char)) {
  391. strWidth += charWidth.en; // 英文字母
  392. } else {
  393. strWidth += charWidth.other; // 其他字符
  394. }
  395. }
  396. // for (let char of str) {
  397. // // 判断字符是否为中文
  398. // if (char.match(/[\u4e00-\u9fa5]/)) {
  399. // strWidth += 2; // 中文字符占4个单位
  400. // } else {
  401. // strWidth += 1; // 英文字符占1个单位
  402. // }
  403. // }
  404. const totalPadding = width - strWidth;
  405. // 如果已经达到或超过目标宽度,直接返回原字符串
  406. if (totalPadding <= 0) {
  407. return str;
  408. }
  409. // 计算左右两侧的空格数
  410. const leftPadding = Math.floor(totalPadding / 2) * 3;
  411. const rightPadding = Math.ceil(totalPadding / 2) * 3;
  412. // 生成填充空格的字符串
  413. const leftSpaces = spaceChar.repeat(leftPadding);
  414. const rightSpaces = spaceChar.repeat(rightPadding);
  415. // 返回补充后的字符串
  416. return leftSpaces + str + rightSpaces;
  417. }