yjbdcController.js 34 KB

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