import moment from 'moment'; import commonModel from '../../model/commonModel.js'; import config from '../../config/index.js'; import _ from 'lodash'; import axios from 'axios'; import { Encrypt, Decrypt } from '../../util/crypto/index.js'; import { stringUtils } from '../../util/stringClass.js'; import WXBizDataCrypt from '../../util/WXBizDataCrypt.js'; import { globalCache } from '../../util/GlobalCache.js'; import yjbdc from '../../model/yjbdc.js'; import PDFDocument from 'pdfkit'; import tencentcloud from 'tencentcloud-sdk-nodejs-ocr'; const OcrClient = tencentcloud.ocr.v20181119.Client; const headers = { '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', "Authorization": "Bearer "+config.huoshancloud.apikey, "Content-Type": "application/json" } export async function YJBDCLogin(ctx) { let param = ctx.request.body; if (param.param) { const paramStr = Decrypt(param.param, config.urlSecrets.aes_key, config.urlSecrets.aes_iv); //console.log("paramStr:"+paramStr); param = JSON.parse(paramStr); } const code = param.Code; //console.log("code:"+code); 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`; let result = await axios.get(url) .then(res => { const json = res.data; //console.log("json:"+json); if (json && json.openid) { param.OpenID = json.openid; param.sessionKey = json.session_key; if (json.unionid) param.UnionID = json.unionid; return {errcode: 10000}; } else { return json; } }) .catch(err => { return {errcode: 101, errStr: err}; }); if (result.errcode == 10000) { delete param.Code; if (param.sessionKey && param.iv && param.encryptedData){ //console.log("param.sessionKey:"+param.sessionKey); const pc = new WXBizDataCrypt(config.wx.yjbdc_appid, param.sessionKey); const dataUnionID = pc.decryptData(param.encryptedData , param.iv); //console.log(dataUnionID); param.UnionID = dataUnionID.unionId; } delete param.sessionKey; delete param.iv; delete param.encryptedData; //todo //param.OpenID="o4UHq4gaNlHfdTWxgl3fTgC1mFsI"; let userList = await yjbdc.GetUsersInfo(param); if (userList.length > 0) { param.LastLoginTime = new Date(); const time1 = moment(userList[0].ProductServiceTime).format('YYYY-MM-DD HH:mm:ss'); const time3 = moment().format('YYYY-MM-DD HH:mm:ss'); if (time1 < time3) param.IsMember = 0; delete param.Introducer; delete param.UserSource; delete param.SourceID; //console.log(param.NickName); if (param.NickName == "陌生用户") { delete param.NickName; delete param.AvatarUrl; delete param.Language; delete param.Gender; delete param.City; delete param.Province; delete param.Country; } await yjbdc.UpdateUsers(param); userList = await yjbdc.GetUsersInfo(param); } else { param.NickName = "陌生用户"; param.AvatarUrl = "../images/userface_default.png"; param.CreateTime = new Date(); param.LastLoginTime = param.CreateTime; param.ProductServiceTime = param.CreateTime; const inseredID = await yjbdc.AddUsers(param); userList = await yjbdc.GetUsersInfo(param); } delete userList[0].OpenID; delete userList[0].UnionID; //产品支付是否显示 if (param.ProgramVersion) { let param2 = { ProgramID: 186, Version: param.ProgramVersion, }; let result3 = await commonModel.GetProductVersionList(param2); if (result3) { if ((param2.Version == result3[0].Version && result3[0].IsShowPay <= 0) || param2.Version > result3[0].Version) { userList[0].IsShow = result3[0].IsShowPay; } else { userList[0].IsShow = 1; } //针对iphone测试用户,永远是无支付状态 if (userList[0].Brand == 'iPhone' && userList[0].WXLanguage == 'en-US' && userList[0].UserSource == '1001' && userList[0].IsPay == 0) { userList[0].IsShow = 0; } //针对微信测试用户,永远是无支付状态 if ((userList[0].UserSource=='1001' && userList[0].System=="iOS 10.0.1") || (!userList[0].UserSource && (!userList[0].LastUserSource || userList[0].LastUserSource>10000)) || userList[0].NickName.indexOf("dgztest")>=0){ userList[0].IsShow=-1; } if (userList[0].IsMember===1) userList[0].IsShow=1; } } result = {errcode: 10000, result: userList[0]}; } ctx.body = result; } export async function OCRImageData(ctx) { const params = ctx.request.body; const clientConfig = { credential: { secretId: config.tencentcloud.secretId, secretKey: config.tencentcloud.secretKey, }, region: "ap-guangzhou", profile: { httpProfile: { endpoint: "ocr.tencentcloudapi.com", }, }, }; // 实例化要请求产品的client对象,clientProfile是可选的 const client = new OcrClient(clientConfig); const result = await client.GeneralBasicOCR(params); let param2={}; param2.UserID=ctx.query.UserID; param2.CreateTime=moment().format('YYYY-MM-DD HH:mm:ss'); param2.Params=JSON.stringify(params); param2.JSONString=JSON.stringify(result); await yjbdc.AddOCRInfo(param2); ctx.body = {"errcode": 10000, result}; } export async function GenerateArticle(ctx) { const url='GenerateArticle?UserID='+ctx.query.UserID; let result = globalCache.get(url); if (result === 0) { const params = ctx.request.body; const words = JSON.parse(params.Words).join(","); const articleStyle = params.ArticleStyle; //文章类型 const ARTICLE_STYLE={ "成长":"指一个人从小慢慢长大,不仅身体变高变壮,还学会更多知识,懂得更多道理,变得越来越懂事、能干。", "童话":"充满神奇和想象的故事,里面可能有会说话的动物、美丽的公主、勇敢的王子,还有魔法和冒险,比如《白雪公主》、《小红帽》。", "家庭亲子":"指爸爸妈妈和孩子之间的相处,比如一起玩游戏、读书、聊天,让家庭充满爱和温暖。", "人生励志":"讲述一个人如何克服困难、努力奋斗,最终取得成功的故事,鼓励我们不要轻易放弃,比如《爱迪生发明电灯》。", "科幻":"科学幻想故事,里面有未来科技、外星人、太空旅行等,比如《海底两万里》、《流浪地球》、《时间的折皱》。", "奇幻":"充满魔法、神奇生物和不可思议事件的故事,比如《哈利·波特》。", "校园生活":"发生在学校里的故事,比如上课、考试、运动会、和同学一起玩耍,与友情、团结、如何学习、克服困难有关的事情,也可能有搞笑或感人的事情。", "节日文化":"不同节日的习俗和传统,比如春节贴春联、中秋节吃月饼、端午划龙舟、清明节扫墓、重阳节登高望远、元宵节猜灯谜,让我们了解不同文化。", "旅行":"去不同的地方游玩,看看美丽的风景,体验不一样的生活,比如爬山、逛动物园、参观博物馆。", "科普":"科学普及知识,用有趣的方式讲解自然、宇宙、动物、植物等科学现象,比如《十万个为什么》。", "动物":"生活在地球上的各种生物,有的会跑,有的会飞,有的会游泳,比如猫、狗、大象、企鹅,它们都有自己的特点和习性。", "环保":"指环境保护、保护大自然,让地球更干净、更健康。比如节约用水、减少垃圾、种树、不乱扔塑料袋,这样空气会更清新,动物也有更好的家园。每个人都可以从小事做起爱护地球。", }; //等级难度 const LEVEL=[ { Key:"小学", Content:"难度上是中国的小学六年级毕业的孩子可以阅读,生成的文章中除了用户提供的单词,其他全部使用人教版小学英语词汇表的单词,以及Sight words中220个单词,不要超出范围。" }, { Key:"初中", Content:"难度上是中国的初三毕业的孩子可以阅读,生成的文章中除了用户提供的单词,其他全部使用人教版初中英语词汇表的单词,以及Sight words中220个单词,不要超出范围。" }, { Key:"高中", Content:"难度上是中国的高三毕业的孩子可以阅读,生成的文章使用人教版高中英语词汇表的单词." }, { Key:"大学", Content:"难度上是中国的大学毕业生的孩子可以阅读,生成的文章使用大学生六级词汇表的单词." }, ] let content = "将"+words+"这些单词生成一篇英文文章。要求"+ "[难度];"+ "单词若是脏话,像'f***'、's***'等,可以忽略;"+ "文章类型是'"+articleStyle+"([类型])';"+ "文章单词数在200个左右,最多不能超过300个;"+ "文章按每句分成数组,且每句都有中文翻译;"+ "提供5道针对文章阅读理解的单项选择题,各有四个选项,并提供答案;"+ "单项选择题和选项也要有中文翻译;"+ "文章生成可以用到单词的变形形式,比如用到单词的原型、复数形式、过去式、过去分词、比较级、最高级、第三人称单数等,请罗列出文章中单词所用到的形式;"+ "内容格式为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:'<答案>'}...]}"; content = content.replace("[难度]",LEVEL[Number(params.Level)].Content); content = content.replace("[类型]",ARTICLE_STYLE[articleStyle]); //console.log("content:"+content); const url = "https://ark.cn-beijing.volces.com/api/v3/chat/completions"; const postJSON = { "model": "doubao-1.5-pro-32k-250115", "messages": [ { "role": "user", "content": content, } ]}; const response = await axios.post(encodeURI(url), postJSON, { headers }); //console.log("parsedBody:" + JSON.stringify(response.data.choices[0].message.content)); let result2=response.data.choices[0].message.content; let param2={}; param2.UserID=ctx.query.UserID; param2.CreateTime=moment().format('YYYY-MM-DD HH:mm:ss'); param2.Words=words; param2.Level=params.Level; param2.articleStyle=params.ArticleStyle; param2.BuildStr=content; param2.ArticleStart=JSON.parse(result2).ArticleEnglish[0]; // 去除JSON字符串中的所有换行符(\r\n, \n, \r) param2.JSONString=JSON.stringify(result2.replace(/[\r\n]+/g, '')); param2.Flag=0; await yjbdc.AddArticleInfo(param2); result = { errcode: 10000, result: result2 }; globalCache.set(url, result, config.BufferMemoryTime1); console.log("缓存60秒"); } ctx.body = result; } export async function GetYJBDCArticleList(ctx) { const param = { UserID: ctx.query.UserID || 0, ID:ctx.query.ID || 0, }; // 尝试从缓存获取 const url='GetYJBDCArticleList?UserID='+param.UserID+"&ID="+param.ID; let result = globalCache.get(url); if (result === 0) { result = await yjbdc.GetYJBDCArticleList(param); if (param.ID==0){ for(let i=0;i0){ const url='GetYJBDCArticleList?UserID='+param.UserID+"&ID=0"; globalCache.delete(url); await yjbdc.UpdateYJBDCArticle(param); } ctx.body = {"errcode": 10000}; } export async function GeneratePDF(ctx) { const params = ctx.request.body; if (!params || !params.Content) { ctx.status = 400; ctx.body = { error: 'Invalid request body: Content is required' }; return; } const content = params.Content; console.log("Generating PDF with content:", JSON.stringify(content).substring(0, 200) + "..."); try { // 创建新的 PDF 文档 - 使用A4尺寸 const doc = new PDFDocument({ size: 'A4', // 使用标准A4尺寸 (595.28 x 841.89 points) margins: { top: 0, bottom: 0, left: 0, right: 0 }, autoFirstPage: true }); // 注册中文字体 doc.registerFont('ChineseFont', '/usr/share/fonts/google-droid/DroidSansFallback.ttf'); // 定义字体选择函数,根据内容是否包含中文选择合适的字体 const selectFont = (text, defaultFont = 'Helvetica') => { // 检查文本是否包含中文字符 if (/[\u4E00-\u9FFF]/.test(text)) { return 'ChineseFont'; } return defaultFont; }; // 收集生成的 PDF 数据 const chunks = []; doc.on('data', (chunk) => chunks.push(chunk)); // 像素到点(pt)的转换函数 - A4纸张像素尺寸2100×2970,PDFKit点尺寸595.28×841.89 const pixelToPt = (pixel) => pixel * (595.28 / 2100); // 获取文章内容和单词列表 let articleText = ""; if (content.ArticleEnglish) { if (Array.isArray(content.ArticleEnglish)) { articleText = content.ArticleEnglish.join(" "); } else { articleText = content.ArticleEnglish; } } else { articleText = "No content available"; } // 清理文章文本中的HTML标签 articleText = articleText.replace(/<[^>]*>/g, ''); // 获取单词列表(如果存在) let words = []; if (content.Words) { try { if (typeof content.Words === 'string') { // 尝试解析JSON字符串 words = JSON.parse(content.Words); } else if (Array.isArray(content.Words)) { // 已经是数组 words = content.Words; } } catch (e) { console.error("Error parsing words:", e); // 如果解析失败,尝试直接使用 words = typeof content.Words === 'string' ? content.Words.split(',') : []; } } console.log("Words to display:", words); // 获取问题列表(如果存在) let questions = []; if (content.Question && Array.isArray(content.Question)) { questions = content.Question; } console.log("Questions to display:", questions.length); // 1. 在top:90,left:120像素处写"Story",60像素大小,Semibold粗细 doc.font('Helvetica-Bold') // 使用Helvetica-Bold作为Semibold替代 .fontSize(pixelToPt(60)) .text("Story", pixelToPt(120), pixelToPt(90), { width: pixelToPt(200), align: 'left' }); // 2. 在top:250,left:120像素处写英文文章,48像素大小,宽1240像素,高自适应,Regular粗细 doc.font('Helvetica') .fontSize(pixelToPt(48)) .text(articleText, pixelToPt(120), pixelToPt(250), { width: pixelToPt(1240), align: 'left', lineGap: pixelToPt(10) // 行间距 }); // 记录文章结束位置的y坐标 const articleEndY = doc.y; console.log("Article end Y position:", articleEndY); // 3. 在top:90,left:1443像素处写"备注",48像素大小,Regular粗细 // 使用中文字体显示中文 doc.font('ChineseFont') .fontSize(pixelToPt(48)) .text("备注", pixelToPt(1443), pixelToPt(90), { width: pixelToPt(537), align: 'left' }); // 4. 在top:210,left:1443像素处画一条黑线,537像素宽,10像素高 doc.rect(pixelToPt(1443), pixelToPt(210), pixelToPt(537), pixelToPt(10)) .fill('black'); // 5. 在top:250,left:1443像素,宽537像素,高900像素的空间内竖排显示1-10个单词,每行一个,右对齐 doc.font('Helvetica') // 使用常规体(Regular) .fontSize(pixelToPt(48)); // 计算单词显示区域 const wordAreaTop = pixelToPt(250); const wordAreaLeft = pixelToPt(1443); const wordAreaWidth = pixelToPt(537); const wordAreaHeight = pixelToPt(900); // 确保words是数组 const wordsArray = Array.isArray(words) ? words : (typeof words === 'string' ? words.split(',') : []); console.log("Words array for display:", wordsArray); // 显示单词(最多10个) const maxWords = Math.min(wordsArray.length, 10); // 根据单词数量动态计算每个单词的高度空间 const wordHeight = pixelToPt(80); // 固定高度,确保足够的空间显示 for (let i = 0; i < maxWords; i++) { const word = wordsArray[i]; if (word) { // 确保单词存在 const yPosition = wordAreaTop + (i * wordHeight); // 确保不超出指定区域 if (yPosition + wordHeight <= wordAreaTop + wordAreaHeight) { // 添加调试信息 console.log(`Drawing word: "${word}" at position: ${yPosition}`); doc.text(word.toString(), wordAreaLeft, yPosition, { width: wordAreaWidth, align: 'right', // 右对齐 lineBreak: false // 防止自动换行 }); } } } // 6. 在文章显示全部完成的下方100像素,left是120位置,画一个黑线,1860像素宽,10像素高 const lineY = Math.max(articleEndY, wordAreaTop + wordAreaHeight) + pixelToPt(100); doc.rect(pixelToPt(120), lineY, pixelToPt(1860), pixelToPt(10)) .fill('black'); // 7-12. 添加问题和答案部分 if (questions.length > 0) { // 问题1和答案 - left:120像素位置 if (questions.length >= 1) { // 问题标题 - 检查是否包含中文并使用适当的字体 const q1Text = `1. ${questions[0].QuestionEnglish || "Question 1"}`; doc.font(/[\u4E00-\u9FFF]/.test(q1Text) ? 'ChineseFont' : 'Helvetica-Bold') .fontSize(pixelToPt(36)) .text(q1Text, pixelToPt(120), lineY + pixelToPt(100), { width: pixelToPt(540), align: 'left' }); // 问题选项 doc.fontSize(pixelToPt(36)); const options = questions[0].OptionsEnglish || []; let optionY = doc.y + pixelToPt(20); for (let i = 0; i < options.length; i++) { doc.text(options[i] || `Option ${i+1}`, pixelToPt(120), optionY, { width: pixelToPt(540), align: 'left' }); optionY = doc.y + pixelToPt(10); } } // 问题2和答案 - left:120像素位置 if (questions.length >= 2) { // 获取问题1结束的Y坐标 const q1EndY = doc.y + pixelToPt(60); // 问题标题 doc.font('Helvetica-Bold') .fontSize(pixelToPt(36)) .text(`2. ${questions[1].QuestionEnglish || "Question 2"}`, pixelToPt(120), q1EndY, { width: pixelToPt(540), align: 'left' }); // 问题选项 doc.font('Helvetica') .fontSize(pixelToPt(36)); const options = questions[1].OptionsEnglish || []; let optionY = doc.y + pixelToPt(20); for (let i = 0; i < options.length; i++) { doc.text(options[i] || `Option ${i+1}`, pixelToPt(120), optionY, { width: pixelToPt(540), align: 'left' }); optionY = doc.y + pixelToPt(10); } } // 问题3和问题4 - left:740像素位置 if (questions.length >= 3) { // 问题3标题 doc.font('Helvetica-Bold') .fontSize(pixelToPt(36)) .text(`3. ${questions[2].QuestionEnglish || "Question 3"}`, pixelToPt(740), lineY + pixelToPt(100), { width: pixelToPt(540), align: 'left' }); // 问题3选项 doc.font('Helvetica') .fontSize(pixelToPt(36)); const options3 = questions[2].OptionsEnglish || []; let option3Y = doc.y + pixelToPt(20); for (let i = 0; i < options3.length; i++) { doc.text(options3[i] || `Option ${i+1}`, pixelToPt(740), option3Y, { width: pixelToPt(540), align: 'left' }); option3Y = doc.y + pixelToPt(10); } // 问题4 - 如果存在 if (questions.length >= 4) { // 获取问题3结束的Y坐标 const q3EndY = doc.y + pixelToPt(60); // 问题4标题 doc.font('Helvetica-Bold') .fontSize(pixelToPt(36)) .text(`4. ${questions[3].QuestionEnglish || "Question 4"}`, pixelToPt(740), q3EndY, { width: pixelToPt(540), align: 'left' }); // 问题4选项 doc.font('Helvetica') .fontSize(pixelToPt(36)); const options4 = questions[3].OptionsEnglish || []; let option4Y = doc.y + pixelToPt(20); for (let i = 0; i < options4.length; i++) { doc.text(options4[i] || `Option ${i+1}`, pixelToPt(740), option4Y, { width: pixelToPt(540), align: 'left' }); option4Y = doc.y + pixelToPt(10); } } } // 问题5 - left:1360像素位置 if (questions.length >= 5) { // 问题5标题 doc.font('Helvetica-Bold') .fontSize(pixelToPt(36)) .text(`5. ${questions[4].QuestionEnglish || "Question 5"}`, pixelToPt(1360), lineY + pixelToPt(100), { width: pixelToPt(540), align: 'left' }); // 问题5选项 doc.font('Helvetica') .fontSize(pixelToPt(36)); const options5 = questions[4].OptionsEnglish || []; let option5Y = doc.y + pixelToPt(20); for (let i = 0; i < options5.length; i++) { doc.text(options5[i] || `Option ${i+1}`, pixelToPt(1360), option5Y, { width: pixelToPt(540), align: 'left' }); option5Y = doc.y + pixelToPt(10); } } } // 13. 下方距离底边330像素,left:120像素处,画一条虚线,1860像素宽,10像素高,颜色为#D2D2D2 const dashLineY = doc.page.height - pixelToPt(330); doc.strokeColor('#D2D2D2') .dash(pixelToPt(10), { space: pixelToPt(5) }) // 设置虚线样式 .lineWidth(pixelToPt(10)) .moveTo(pixelToPt(120), dashLineY) .lineTo(pixelToPt(120) + pixelToPt(1860), dashLineY) .stroke() .undash(); // 重置虚线样式 // 14-18. 显示问题答案 doc.font('Helvetica-Bold') // 使用Helvetica-Bold作为Semibold替代 .fontSize(pixelToPt(45)) .fillColor('black'); // 重置颜色为黑色 const answersY = doc.page.height - pixelToPt(177); // 获取问题答案 const answers = []; if (questions.length > 0) { for (let i = 0; i < Math.min(questions.length, 5); i++) { const answer = questions[i].Answer || ""; answers.push(`${i+1}. ${answer}`); } } // 显示答案(如果存在) const answerPositions = [120, 277, 443, 611, 778]; for (let i = 0; i < Math.min(answers.length, 5); i++) { doc.text(answers[i], pixelToPt(answerPositions[i]), answersY, { width: pixelToPt(100), align: 'left' }); } // 19. 显示当前时间 const currentTime = moment().format('YYYY年MM月DD日 HH:mm'); const timeY = doc.page.height - pixelToPt(118); // 计算正确的位置:总宽度2100像素 - 右边距380像素 = 右边缘位置1720像素 // 为了使用text()方法的右对齐,设置起始位置为1720-380=1340像素 doc.font('ChineseFont') // 使用中文字体显示包含中文的日期 .fontSize(pixelToPt(32)) .text(currentTime, pixelToPt(1340), timeY, { width: pixelToPt(380), align: 'right' }); console.log("PDF generation completed, finalizing document..."); // 使用 Promise 等待 PDF 生成完成 await new Promise((resolve, reject) => { // 监听 end 事件,表示 PDF 生成完成 doc.on('end', () => { try { // 将所有数据块合并为一个 Buffer const pdfBuffer = Buffer.concat(chunks); console.log("PDF buffer size:", pdfBuffer.length); // 设置响应头 ctx.set('Content-Type', 'application/pdf'); ctx.set('Content-Disposition', 'attachment; filename=reading.pdf'); // 设置响应体为 PDF buffer ctx.body = pdfBuffer; resolve(); } catch (err) { console.error("Error in PDF end handler:", err); reject(err); } }); // 监听错误事件 doc.on('error', (err) => { console.error("PDF generation error:", err); reject(err); }); // 结束文档生成 doc.end(); }); } catch (error) { console.error('Error generating PDF:', error); ctx.status = 500; ctx.body = { error: error.message, stack: error.stack }; } }