yjbdcController.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. import moment from 'moment';
  2. import commonModel from '../../model/commonModel.js';
  3. import config from '../../config/index.js';
  4. import _ from 'lodash';
  5. import axios from 'axios';
  6. import { Encrypt, Decrypt } from '../../util/crypto/index.js';
  7. import { stringUtils } from '../../util/stringClass.js';
  8. import WXBizDataCrypt from '../../util/WXBizDataCrypt.js';
  9. import { globalCache } from '../../util/GlobalCache.js';
  10. import yjbdc from '../../model/yjbdc.js';
  11. import PDFDocument from 'pdfkit';
  12. import tencentcloud from 'tencentcloud-sdk-nodejs-ocr';
  13. const OcrClient = tencentcloud.ocr.v20181119.Client;
  14. const headers = {
  15. 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36',
  16. "Authorization": "Bearer "+config.huoshancloud.apikey,
  17. "Content-Type": "application/json"
  18. }
  19. export async function YJBDCLogin(ctx) {
  20. let param = ctx.request.body;
  21. if (param.param) {
  22. const paramStr = Decrypt(param.param, config.urlSecrets.aes_key, config.urlSecrets.aes_iv);
  23. //console.log("paramStr:"+paramStr);
  24. param = JSON.parse(paramStr);
  25. }
  26. const code = param.Code;
  27. //console.log("code:"+code);
  28. const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${config.wx.yjbdc_appid}&secret=${config.wx.yjbdc_appsecret}&js_code=${code}&grant_type=authorization_code`;
  29. let result = await axios.get(url)
  30. .then(res => {
  31. const json = res.data;
  32. //console.log("json:"+json);
  33. if (json && json.openid) {
  34. param.OpenID = json.openid;
  35. param.sessionKey = json.session_key;
  36. if (json.unionid)
  37. param.UnionID = json.unionid;
  38. return {errcode: 10000};
  39. }
  40. else {
  41. return json;
  42. }
  43. })
  44. .catch(err => {
  45. return {errcode: 101, errStr: err};
  46. });
  47. if (result.errcode == 10000) {
  48. delete param.Code;
  49. if (param.sessionKey && param.iv && param.encryptedData){
  50. //console.log("param.sessionKey:"+param.sessionKey);
  51. const pc = new WXBizDataCrypt(config.wx.yjbdc_appid, param.sessionKey);
  52. const dataUnionID = pc.decryptData(param.encryptedData , param.iv);
  53. //console.log(dataUnionID);
  54. param.UnionID = dataUnionID.unionId;
  55. }
  56. delete param.sessionKey;
  57. delete param.iv;
  58. delete param.encryptedData;
  59. //todo
  60. //param.OpenID="o4UHq4gaNlHfdTWxgl3fTgC1mFsI";
  61. let userList = await yjbdc.GetUsersInfo(param);
  62. if (userList.length > 0) {
  63. param.LastLoginTime = new Date();
  64. const time1 = moment(userList[0].ProductServiceTime).format('YYYY-MM-DD HH:mm:ss');
  65. const time3 = moment().format('YYYY-MM-DD HH:mm:ss');
  66. if (time1 < time3)
  67. param.IsMember = 0;
  68. delete param.Introducer;
  69. delete param.UserSource;
  70. delete param.SourceID;
  71. //console.log(param.NickName);
  72. if (param.NickName == "陌生用户") {
  73. delete param.NickName;
  74. delete param.AvatarUrl;
  75. delete param.Language;
  76. delete param.Gender;
  77. delete param.City;
  78. delete param.Province;
  79. delete param.Country;
  80. }
  81. await yjbdc.UpdateUsers(param);
  82. userList = await yjbdc.GetUsersInfo(param);
  83. }
  84. else {
  85. param.NickName = "陌生用户";
  86. param.AvatarUrl = "../images/userface_default.png";
  87. param.CreateTime = new Date();
  88. param.LastLoginTime = param.CreateTime;
  89. param.ProductServiceTime = param.CreateTime;
  90. const inseredID = await yjbdc.AddUsers(param);
  91. userList = await yjbdc.GetUsersInfo(param);
  92. }
  93. delete userList[0].OpenID;
  94. delete userList[0].UnionID;
  95. //产品支付是否显示
  96. if (param.ProgramVersion) {
  97. let param2 = {
  98. ProgramID: 186,
  99. Version: param.ProgramVersion,
  100. };
  101. let result3 = await commonModel.GetProductVersionList(param2);
  102. if (result3) {
  103. if ((param2.Version == result3[0].Version && result3[0].IsShowPay <= 0)
  104. || param2.Version > result3[0].Version) {
  105. userList[0].IsShow = result3[0].IsShowPay;
  106. }
  107. else {
  108. userList[0].IsShow = 1;
  109. }
  110. //针对iphone测试用户,永远是无支付状态
  111. if (userList[0].Brand == 'iPhone' && userList[0].WXLanguage == 'en-US'
  112. && userList[0].UserSource == '1001' && userList[0].IsPay == 0) {
  113. userList[0].IsShow = 0;
  114. }
  115. //针对微信测试用户,永远是无支付状态
  116. if ((userList[0].UserSource=='1001' && userList[0].System=="iOS 10.0.1")
  117. || (!userList[0].UserSource && (!userList[0].LastUserSource || userList[0].LastUserSource>10000))
  118. || userList[0].NickName.indexOf("dgztest")>=0){
  119. userList[0].IsShow=-1;
  120. }
  121. if (userList[0].IsMember===1)
  122. userList[0].IsShow=1;
  123. }
  124. }
  125. result = {errcode: 10000, result: userList[0]};
  126. }
  127. ctx.body = result;
  128. }
  129. export async function OCRImageData(ctx) {
  130. const params = ctx.request.body;
  131. const clientConfig = {
  132. credential: {
  133. secretId: config.tencentcloud.secretId,
  134. secretKey: config.tencentcloud.secretKey,
  135. },
  136. region: "ap-guangzhou",
  137. profile: {
  138. httpProfile: {
  139. endpoint: "ocr.tencentcloudapi.com",
  140. },
  141. },
  142. };
  143. // 实例化要请求产品的client对象,clientProfile是可选的
  144. const client = new OcrClient(clientConfig);
  145. const result = await client.GeneralBasicOCR(params);
  146. let param2={};
  147. param2.UserID=ctx.query.UserID;
  148. param2.CreateTime=moment().format('YYYY-MM-DD HH:mm:ss');
  149. param2.Params=JSON.stringify(params);
  150. param2.JSONString=JSON.stringify(result);
  151. await yjbdc.AddOCRInfo(param2);
  152. ctx.body = {"errcode": 10000, result};
  153. }
  154. export async function GenerateArticle(ctx) {
  155. const url='GenerateArticle?UserID='+ctx.query.UserID;
  156. let result = globalCache.get(url);
  157. if (result === 0) {
  158. const params = ctx.request.body;
  159. const words = JSON.parse(params.Words).join(",");
  160. const articleStyle = params.ArticleStyle;
  161. //文章类型
  162. const ARTICLE_STYLE={
  163. "成长":"指一个人从小慢慢长大,不仅身体变高变壮,还学会更多知识,懂得更多道理,变得越来越懂事、能干。",
  164. "童话":"充满神奇和想象的故事,里面可能有会说话的动物、美丽的公主、勇敢的王子,还有魔法和冒险,比如《白雪公主》、《小红帽》。",
  165. "家庭亲子":"指爸爸妈妈和孩子之间的相处,比如一起玩游戏、读书、聊天,让家庭充满爱和温暖。",
  166. "人生励志":"讲述一个人如何克服困难、努力奋斗,最终取得成功的故事,鼓励我们不要轻易放弃,比如《爱迪生发明电灯》。",
  167. "科幻":"科学幻想故事,里面有未来科技、外星人、太空旅行等,比如《海底两万里》、《流浪地球》、《时间的折皱》。",
  168. "奇幻":"充满魔法、神奇生物和不可思议事件的故事,比如《哈利·波特》。",
  169. "校园生活":"发生在学校里的故事,比如上课、考试、运动会、和同学一起玩耍,与友情、团结、如何学习、克服困难有关的事情,也可能有搞笑或感人的事情。",
  170. "节日文化":"不同节日的习俗和传统,比如春节贴春联、中秋节吃月饼、端午划龙舟、清明节扫墓、重阳节登高望远、元宵节猜灯谜,让我们了解不同文化。",
  171. "旅行":"去不同的地方游玩,看看美丽的风景,体验不一样的生活,比如爬山、逛动物园、参观博物馆。",
  172. "科普":"科学普及知识,用有趣的方式讲解自然、宇宙、动物、植物等科学现象,比如《十万个为什么》。",
  173. "动物":"生活在地球上的各种生物,有的会跑,有的会飞,有的会游泳,比如猫、狗、大象、企鹅,它们都有自己的特点和习性。",
  174. "环保":"指环境保护、保护大自然,让地球更干净、更健康。比如节约用水、减少垃圾、种树、不乱扔塑料袋,这样空气会更清新,动物也有更好的家园。每个人都可以从小事做起爱护地球。",
  175. };
  176. //等级难度
  177. const LEVEL=[
  178. {
  179. Key:"小学",
  180. Content:"难度上是中国的小学六年级毕业的孩子可以阅读,生成的文章中除了用户提供的单词,其他全部使用人教版小学英语词汇表的单词,以及Sight words中220个单词,不要超出范围。"
  181. },
  182. {
  183. Key:"初中",
  184. Content:"难度上是中国的初三毕业的孩子可以阅读,生成的文章中除了用户提供的单词,其他全部使用人教版初中英语词汇表的单词,以及Sight words中220个单词,不要超出范围。"
  185. },
  186. {
  187. Key:"高中",
  188. Content:"难度上是中国的高三毕业的孩子可以阅读,生成的文章使用人教版高中英语词汇表的单词."
  189. },
  190. {
  191. Key:"大学",
  192. Content:"难度上是中国的大学毕业生的孩子可以阅读,生成的文章使用大学生六级词汇表的单词."
  193. },
  194. ]
  195. let content = "将"+words+"这些单词生成一篇英文文章。要求"+
  196. "[难度];"+
  197. "单词若是脏话,像'f***'、's***'等,可以忽略;"+
  198. "文章类型是'"+articleStyle+"([类型])';"+
  199. "文章单词数在200个左右,最多不能超过300个;"+
  200. "文章按每句分成数组,且每句都有中文翻译;"+
  201. "提供5道针对文章阅读理解的单项选择题,各有四个选项,并提供答案;"+
  202. "单项选择题和选项也要有中文翻译;"+
  203. "文章生成可以用到单词的变形形式,比如用到单词的原型、复数形式、过去式、过去分词、比较级、最高级、第三人称单数等,请罗列出文章中单词所用到的形式;"+
  204. "内容格式为JSON:{ArticleEnglish:['<英文文章句子1>','<英文文章句子2>'...],ArticleChinese:['<中文翻译句子1>','<中文翻译句子2>'...],FormsOfWords:['<单词1形式1>','<单词1形式2>','<单词2形式1>',...],Question:[{QuestionEnglish:'<英语题目1>',QuestionChinese:'<题目1中文翻译>',OptionsEnglish:['A.<英语选项1>','B.<英语选项2>','C.<英语选项3>','D.<英语选项4>'],OptionsChinese:['A.<选项1中文翻译>','B.<选项2中文翻译>','C.<选项3中文翻译>','D.<选项4中文翻译>'],Answer:'<答案>'}...]}";
  205. content = content.replace("[难度]",LEVEL[Number(params.Level)].Content);
  206. content = content.replace("[类型]",ARTICLE_STYLE[articleStyle]);
  207. //console.log("content:"+content);
  208. const url = "https://ark.cn-beijing.volces.com/api/v3/chat/completions";
  209. const postJSON = {
  210. "model": "doubao-1.5-pro-32k-250115",
  211. "messages": [
  212. {
  213. "role": "user",
  214. "content": content,
  215. }
  216. ]};
  217. const response = await axios.post(encodeURI(url), postJSON, { headers });
  218. //console.log("parsedBody:" + JSON.stringify(response.data.choices[0].message.content));
  219. let result2=response.data.choices[0].message.content;
  220. let param2={};
  221. param2.UserID=ctx.query.UserID;
  222. param2.CreateTime=moment().format('YYYY-MM-DD HH:mm:ss');
  223. param2.Words=words;
  224. param2.Level=params.Level;
  225. param2.articleStyle=params.ArticleStyle;
  226. param2.BuildStr=content;
  227. param2.ArticleStart=JSON.parse(result2).ArticleEnglish[0];
  228. // 去除JSON字符串中的所有换行符(\r\n, \n, \r)
  229. param2.JSONString=JSON.stringify(result2.replace(/[\r\n]+/g, ''));
  230. param2.Flag=0;
  231. await yjbdc.AddArticleInfo(param2);
  232. result = { errcode: 10000, result: result2 };
  233. globalCache.set(url, result, config.BufferMemoryTime1);
  234. console.log("缓存60秒");
  235. }
  236. ctx.body = result;
  237. }
  238. export async function GetYJBDCArticleList(ctx) {
  239. const param = {
  240. UserID: ctx.query.UserID || 0,
  241. ID:ctx.query.ID || 0,
  242. };
  243. // 尝试从缓存获取
  244. const url='GetYJBDCArticleList?UserID='+param.UserID+"&ID="+param.ID;
  245. let result = globalCache.get(url);
  246. if (result === 0) {
  247. result = await yjbdc.GetYJBDCArticleList(param);
  248. if (param.ID==0){
  249. for(let i=0;i<result.length;i++){
  250. let item=result[i];
  251. item.CreateTime=moment(item.CreateTime).format("YYYY年MM月DD日 HH:mm");
  252. switch (item.Level){
  253. case 0:
  254. item.LevelStr="小学";
  255. break;
  256. case 1:
  257. item.LevelStr="初中";
  258. break;
  259. case 2:
  260. item.LevelStr="高中";
  261. break;
  262. case 3:
  263. item.LevelStr="大学";
  264. break;
  265. }
  266. }
  267. }
  268. globalCache.set(url, result, config.BufferMemoryTime);
  269. console.log("缓存60秒");
  270. }
  271. ctx.body = {"errcode": 10000, result:result};
  272. }
  273. export async function DeleteYJBDCArticleList(ctx) {
  274. const param = {
  275. UserID: ctx.query.UserID || 0,
  276. ID: ctx.query.ID || 0,
  277. };
  278. if (param.ID>0){
  279. const url='GetYJBDCArticleList?UserID='+param.UserID+"&ID=0";
  280. globalCache.delete(url);
  281. await yjbdc.UpdateYJBDCArticle(param);
  282. }
  283. ctx.body = {"errcode": 10000};
  284. }
  285. export async function GeneratePDF(ctx) {
  286. const params = ctx.request.body;
  287. if (!params || !params.Content) {
  288. ctx.status = 400;
  289. ctx.body = { error: 'Invalid request body: Content is required' };
  290. return;
  291. }
  292. const content = params.Content;
  293. console.log("Generating PDF with content:", JSON.stringify(content).substring(0, 200) + "...");
  294. try {
  295. // 创建新的 PDF 文档 - 使用A4尺寸
  296. const doc = new PDFDocument({
  297. size: 'A4', // 使用标准A4尺寸 (595.28 x 841.89 points)
  298. margins: {
  299. top: 0,
  300. bottom: 0,
  301. left: 0,
  302. right: 0
  303. },
  304. autoFirstPage: true
  305. });
  306. // 注册中文字体
  307. doc.registerFont('ChineseFont', '/usr/share/fonts/google-droid/DroidSansFallback.ttf');
  308. // 定义字体选择函数,根据内容是否包含中文选择合适的字体
  309. const selectFont = (text, defaultFont = 'Helvetica') => {
  310. // 检查文本是否包含中文字符
  311. if (/[\u4E00-\u9FFF]/.test(text)) {
  312. return 'ChineseFont';
  313. }
  314. return defaultFont;
  315. };
  316. // 收集生成的 PDF 数据
  317. const chunks = [];
  318. doc.on('data', (chunk) => chunks.push(chunk));
  319. // 像素到点(pt)的转换函数 - A4纸张像素尺寸2100×2970,PDFKit点尺寸595.28×841.89
  320. const pixelToPt = (pixel) => pixel * (595.28 / 2100);
  321. // 获取文章内容和单词列表
  322. let articleText = "";
  323. if (content.ArticleEnglish) {
  324. if (Array.isArray(content.ArticleEnglish)) {
  325. articleText = content.ArticleEnglish.join(" ");
  326. } else {
  327. articleText = content.ArticleEnglish;
  328. }
  329. } else {
  330. articleText = "No content available";
  331. }
  332. // 清理文章文本中的HTML标签
  333. articleText = articleText.replace(/<[^>]*>/g, '');
  334. // 获取单词列表(如果存在)
  335. let words = [];
  336. if (content.Words) {
  337. try {
  338. if (typeof content.Words === 'string') {
  339. // 尝试解析JSON字符串
  340. words = JSON.parse(content.Words);
  341. } else if (Array.isArray(content.Words)) {
  342. // 已经是数组
  343. words = content.Words;
  344. }
  345. } catch (e) {
  346. console.error("Error parsing words:", e);
  347. // 如果解析失败,尝试直接使用
  348. words = typeof content.Words === 'string' ? content.Words.split(',') : [];
  349. }
  350. }
  351. console.log("Words to display:", words);
  352. // 获取问题列表(如果存在)
  353. let questions = [];
  354. if (content.Question && Array.isArray(content.Question)) {
  355. questions = content.Question;
  356. }
  357. console.log("Questions to display:", questions.length);
  358. // 1. 在top:90,left:120像素处写"Story",60像素大小,Semibold粗细
  359. doc.font('Helvetica-Bold') // 使用Helvetica-Bold作为Semibold替代
  360. .fontSize(pixelToPt(60))
  361. .text("Story", pixelToPt(120), pixelToPt(90), {
  362. width: pixelToPt(200),
  363. align: 'left'
  364. });
  365. // 2. 在top:250,left:120像素处写英文文章,48像素大小,宽1240像素,高自适应,Regular粗细
  366. doc.font('Helvetica')
  367. .fontSize(pixelToPt(48))
  368. .text(articleText, pixelToPt(120), pixelToPt(250), {
  369. width: pixelToPt(1240),
  370. align: 'left',
  371. lineGap: pixelToPt(10) // 行间距
  372. });
  373. // 记录文章结束位置的y坐标
  374. const articleEndY = doc.y;
  375. console.log("Article end Y position:", articleEndY);
  376. // 3. 在top:90,left:1443像素处写"备注",48像素大小,Regular粗细
  377. // 使用中文字体显示中文
  378. doc.font('ChineseFont')
  379. .fontSize(pixelToPt(48))
  380. .text("备注", pixelToPt(1443), pixelToPt(90), {
  381. width: pixelToPt(537),
  382. align: 'left'
  383. });
  384. // 4. 在top:210,left:1443像素处画一条黑线,537像素宽,10像素高
  385. doc.rect(pixelToPt(1443), pixelToPt(210), pixelToPt(537), pixelToPt(10))
  386. .fill('black');
  387. // 5. 在top:250,left:1443像素,宽537像素,高900像素的空间内竖排显示1-10个单词,每行一个,右对齐
  388. doc.font('Helvetica') // 使用常规体(Regular)
  389. .fontSize(pixelToPt(48));
  390. // 计算单词显示区域
  391. const wordAreaTop = pixelToPt(250);
  392. const wordAreaLeft = pixelToPt(1443);
  393. const wordAreaWidth = pixelToPt(537);
  394. const wordAreaHeight = pixelToPt(900);
  395. // 确保words是数组
  396. const wordsArray = Array.isArray(words) ? words :
  397. (typeof words === 'string' ? words.split(',') : []);
  398. console.log("Words array for display:", wordsArray);
  399. // 显示单词(最多10个)
  400. const maxWords = Math.min(wordsArray.length, 10);
  401. // 根据单词数量动态计算每个单词的高度空间
  402. const wordHeight = pixelToPt(80); // 固定高度,确保足够的空间显示
  403. for (let i = 0; i < maxWords; i++) {
  404. const word = wordsArray[i];
  405. if (word) { // 确保单词存在
  406. const yPosition = wordAreaTop + (i * wordHeight);
  407. // 确保不超出指定区域
  408. if (yPosition + wordHeight <= wordAreaTop + wordAreaHeight) {
  409. // 添加调试信息
  410. console.log(`Drawing word: "${word}" at position: ${yPosition}`);
  411. doc.text(word.toString(), wordAreaLeft, yPosition, {
  412. width: wordAreaWidth,
  413. align: 'right', // 右对齐
  414. lineBreak: false // 防止自动换行
  415. });
  416. }
  417. }
  418. }
  419. // 6. 在文章显示全部完成的下方100像素,left是120位置,画一个黑线,1860像素宽,10像素高
  420. const lineY = Math.max(articleEndY, wordAreaTop + wordAreaHeight) + pixelToPt(100);
  421. doc.rect(pixelToPt(120), lineY, pixelToPt(1860), pixelToPt(10))
  422. .fill('black');
  423. // 7-12. 添加问题和答案部分
  424. if (questions.length > 0) {
  425. // 问题1和答案 - left:120像素位置
  426. if (questions.length >= 1) {
  427. // 问题标题 - 检查是否包含中文并使用适当的字体
  428. const q1Text = `1. ${questions[0].QuestionEnglish || "Question 1"}`;
  429. doc.font(/[\u4E00-\u9FFF]/.test(q1Text) ? 'ChineseFont' : 'Helvetica-Bold')
  430. .fontSize(pixelToPt(36))
  431. .text(q1Text, pixelToPt(120), lineY + pixelToPt(100), {
  432. width: pixelToPt(540),
  433. align: 'left'
  434. });
  435. // 问题选项
  436. doc.fontSize(pixelToPt(36));
  437. const options = questions[0].OptionsEnglish || [];
  438. let optionY = doc.y + pixelToPt(20);
  439. for (let i = 0; i < options.length; i++) {
  440. doc.text(options[i] || `Option ${i+1}`, pixelToPt(120), optionY, {
  441. width: pixelToPt(540),
  442. align: 'left'
  443. });
  444. optionY = doc.y + pixelToPt(10);
  445. }
  446. }
  447. // 问题2和答案 - left:120像素位置
  448. if (questions.length >= 2) {
  449. // 获取问题1结束的Y坐标
  450. const q1EndY = doc.y + pixelToPt(60);
  451. // 问题标题
  452. doc.font('Helvetica-Bold')
  453. .fontSize(pixelToPt(36))
  454. .text(`2. ${questions[1].QuestionEnglish || "Question 2"}`, pixelToPt(120), q1EndY, {
  455. width: pixelToPt(540),
  456. align: 'left'
  457. });
  458. // 问题选项
  459. doc.font('Helvetica')
  460. .fontSize(pixelToPt(36));
  461. const options = questions[1].OptionsEnglish || [];
  462. let optionY = doc.y + pixelToPt(20);
  463. for (let i = 0; i < options.length; i++) {
  464. doc.text(options[i] || `Option ${i+1}`, pixelToPt(120), optionY, {
  465. width: pixelToPt(540),
  466. align: 'left'
  467. });
  468. optionY = doc.y + pixelToPt(10);
  469. }
  470. }
  471. // 问题3和问题4 - left:740像素位置
  472. if (questions.length >= 3) {
  473. // 问题3标题
  474. doc.font('Helvetica-Bold')
  475. .fontSize(pixelToPt(36))
  476. .text(`3. ${questions[2].QuestionEnglish || "Question 3"}`, pixelToPt(740), lineY + pixelToPt(100), {
  477. width: pixelToPt(540),
  478. align: 'left'
  479. });
  480. // 问题3选项
  481. doc.font('Helvetica')
  482. .fontSize(pixelToPt(36));
  483. const options3 = questions[2].OptionsEnglish || [];
  484. let option3Y = doc.y + pixelToPt(20);
  485. for (let i = 0; i < options3.length; i++) {
  486. doc.text(options3[i] || `Option ${i+1}`, pixelToPt(740), option3Y, {
  487. width: pixelToPt(540),
  488. align: 'left'
  489. });
  490. option3Y = doc.y + pixelToPt(10);
  491. }
  492. // 问题4 - 如果存在
  493. if (questions.length >= 4) {
  494. // 获取问题3结束的Y坐标
  495. const q3EndY = doc.y + pixelToPt(60);
  496. // 问题4标题
  497. doc.font('Helvetica-Bold')
  498. .fontSize(pixelToPt(36))
  499. .text(`4. ${questions[3].QuestionEnglish || "Question 4"}`, pixelToPt(740), q3EndY, {
  500. width: pixelToPt(540),
  501. align: 'left'
  502. });
  503. // 问题4选项
  504. doc.font('Helvetica')
  505. .fontSize(pixelToPt(36));
  506. const options4 = questions[3].OptionsEnglish || [];
  507. let option4Y = doc.y + pixelToPt(20);
  508. for (let i = 0; i < options4.length; i++) {
  509. doc.text(options4[i] || `Option ${i+1}`, pixelToPt(740), option4Y, {
  510. width: pixelToPt(540),
  511. align: 'left'
  512. });
  513. option4Y = doc.y + pixelToPt(10);
  514. }
  515. }
  516. }
  517. // 问题5 - left:1360像素位置
  518. if (questions.length >= 5) {
  519. // 问题5标题
  520. doc.font('Helvetica-Bold')
  521. .fontSize(pixelToPt(36))
  522. .text(`5. ${questions[4].QuestionEnglish || "Question 5"}`, pixelToPt(1360), lineY + pixelToPt(100), {
  523. width: pixelToPt(540),
  524. align: 'left'
  525. });
  526. // 问题5选项
  527. doc.font('Helvetica')
  528. .fontSize(pixelToPt(36));
  529. const options5 = questions[4].OptionsEnglish || [];
  530. let option5Y = doc.y + pixelToPt(20);
  531. for (let i = 0; i < options5.length; i++) {
  532. doc.text(options5[i] || `Option ${i+1}`, pixelToPt(1360), option5Y, {
  533. width: pixelToPt(540),
  534. align: 'left'
  535. });
  536. option5Y = doc.y + pixelToPt(10);
  537. }
  538. }
  539. }
  540. // 13. 下方距离底边330像素,left:120像素处,画一条虚线,1860像素宽,10像素高,颜色为#D2D2D2
  541. const dashLineY = doc.page.height - pixelToPt(330);
  542. doc.strokeColor('#D2D2D2')
  543. .dash(pixelToPt(10), { space: pixelToPt(5) }) // 设置虚线样式
  544. .lineWidth(pixelToPt(10))
  545. .moveTo(pixelToPt(120), dashLineY)
  546. .lineTo(pixelToPt(120) + pixelToPt(1860), dashLineY)
  547. .stroke()
  548. .undash(); // 重置虚线样式
  549. // 14-18. 显示问题答案
  550. doc.font('Helvetica-Bold') // 使用Helvetica-Bold作为Semibold替代
  551. .fontSize(pixelToPt(45))
  552. .fillColor('black'); // 重置颜色为黑色
  553. const answersY = doc.page.height - pixelToPt(177);
  554. // 获取问题答案
  555. const answers = [];
  556. if (questions.length > 0) {
  557. for (let i = 0; i < Math.min(questions.length, 5); i++) {
  558. const answer = questions[i].Answer || "";
  559. answers.push(`${i+1}. ${answer}`);
  560. }
  561. }
  562. // 显示答案(如果存在)
  563. const answerPositions = [120, 277, 443, 611, 778];
  564. for (let i = 0; i < Math.min(answers.length, 5); i++) {
  565. doc.text(answers[i], pixelToPt(answerPositions[i]), answersY, {
  566. width: pixelToPt(100),
  567. align: 'left'
  568. });
  569. }
  570. // 19. 显示当前时间
  571. const currentTime = moment().format('YYYY年MM月DD日 HH:mm');
  572. const timeY = doc.page.height - pixelToPt(118);
  573. // 计算正确的位置:总宽度2100像素 - 右边距380像素 = 右边缘位置1720像素
  574. // 为了使用text()方法的右对齐,设置起始位置为1720-380=1340像素
  575. doc.font('ChineseFont') // 使用中文字体显示包含中文的日期
  576. .fontSize(pixelToPt(32))
  577. .text(currentTime, pixelToPt(1340), timeY, {
  578. width: pixelToPt(380),
  579. align: 'right'
  580. });
  581. console.log("PDF generation completed, finalizing document...");
  582. // 使用 Promise 等待 PDF 生成完成
  583. await new Promise((resolve, reject) => {
  584. // 监听 end 事件,表示 PDF 生成完成
  585. doc.on('end', () => {
  586. try {
  587. // 将所有数据块合并为一个 Buffer
  588. const pdfBuffer = Buffer.concat(chunks);
  589. console.log("PDF buffer size:", pdfBuffer.length);
  590. // 设置响应头
  591. ctx.set('Content-Type', 'application/pdf');
  592. ctx.set('Content-Disposition', 'attachment; filename=reading.pdf');
  593. // 设置响应体为 PDF buffer
  594. ctx.body = pdfBuffer;
  595. resolve();
  596. } catch (err) {
  597. console.error("Error in PDF end handler:", err);
  598. reject(err);
  599. }
  600. });
  601. // 监听错误事件
  602. doc.on('error', (err) => {
  603. console.error("PDF generation error:", err);
  604. reject(err);
  605. });
  606. // 结束文档生成
  607. doc.end();
  608. });
  609. } catch (error) {
  610. console.error('Error generating PDF:', error);
  611. ctx.status = 500;
  612. ctx.body = { error: error.message, stack: error.stack };
  613. }
  614. }