import moment from 'moment'; import fs from 'fs'; import { promises as fsPromises } from 'fs'; 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 constantClass from '../../util/constant/index.js'; import yjbdc from '../../model/yjbdc.js'; import aiController from './aiController.js'; import PDFDocument from 'pdfkit'; import tencentcloud from 'tencentcloud-sdk-nodejs-ocr'; const ONE_DAY_MAX_BUILD_COUNT=12;//一天最大生成数 const OcrClient = tencentcloud.ocr.v20181119.Client; //小程序登录 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; } //OCR获取单词 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}; } //AI生成文章 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 = params.Words; let articleStyle = params.ArticleStyle; if (words){ const menuConfig=constantClass.GetYJBDCGenerateConfig(); //console.log("content:"+content); let level=""; if (!params.Level || params.Level=="" || params.Level==undefined || params.Level>="6"){ params.Level=0; } level=menuConfig.Level[Number(params.Level)].Name; let articleStyleContent=""; for(let i=0;i 0 ? jsonObj2.ArticleEnglish.join("\r\n") : "No content available"; // 去除JSON字符串中的所有换行符(\r\n, \n, \r) param2.JSONString=JSON.stringify(result2.replace(/[\r\n]+/g, '')); param2.Flag=0; let idInsert= await yjbdc.AddArticleInfo(param2); //result = { errcode: 10000, result: result2 }; let result3={}; result3.ID=idInsert.insertId; result3.Content=result2; result3.IsNew=true; result = { errcode: 10000, result: result3 }; globalCache.set(url, result, config.BufferMemoryTime); console.log("缓存60秒"); } catch(err){ console.error("AI生成错误:"+err); result = { errcode: 10000, result: "-1" }; } } else{ console.log("空单词串"); result = { errcode: 10000, result: "-1" }; } } ctx.body = result; } export async function GetYJBDCGenerateConfig(ctx) { const param = { UserID: ctx.query.UserID || 0, }; let result=constantClass.GetYJBDCGenerateConfig(); for (let i=0;i3){ result.AIVersion.splice(2,result.AIVersion.length-2); if (param.UserID==185){ const configArr=constantClass.GetYJBDCGenerateConfig(); result.AIVersion.push(configArr.AIVersion[5]); } // if (param.UserID<4){ // const configArr=constantClass.GetYJBDCGenerateConfig(); // result.AIVersion.push(configArr.AIVersion[6]); // } } ctx.body = {"errcode": 10000, result:result}; } //获得秒过当天任务完成后的英语单词(未完成) export async function getMiaoguoTodayAllWords(ctx) { const param = { UserID: ctx.query.UserID || 0, }; const url = `https://www.kylx365.com/api/GetMiaoguoCardList2?UserID=${param.UserID}&IsToday=2&CardType=0&OrderType=ac.LastTime%20desc`; let result = await axios.get(url) .then(res => { let list = res.data.result.List; if (list && list.length>0) { let arr=[]; for(let i=0;i { return {errcode: 101, errStr: err}; }); ctx.body = result; } //获得文章列表或具体文章 export async function GetYJBDCArticleList(ctx) { const param = { UserID: ctx.query.UserID || 0, ID:ctx.query.ID || 0, IsChoiceness:ctx.query.IsChoiceness || 0,//是否是精选文章 IsTodayCount: ctx.query.IsTodayCount || false, IsNew: ctx.query.IsNew || 0, PageID: ctx.query.PageID || 999999, PageCount: ctx.query.PageCount || 5, }; // 尝试从缓存获取 const url='GetYJBDCArticleList?IsChoiceness='+param.IsChoiceness+'&UserID='+param.UserID+'&ID='+param.ID; let result = globalCache.get(url); if (param.IsNew==1) result=0; if (result === 0) { result = await yjbdc.GetYJBDCArticleList(param); let menuConfig=constantClass.GetYJBDCGenerateConfig(); for(let i=0;i=today) count++; if (result[i].Flag==0 && result[i].ReadCount==0) unReadCount++; } let maxcount=ONE_DAY_MAX_BUILD_COUNT; if (param.UserID<4){ maxcount=100; } result={ TodayCount:count, MaxCount:maxcount, UnReadCount:unReadCount, }; } else if (!param.ID){ let arr=[],count=0,total=0; for(let i=0;iresult[i].ID && count0) result[0].RowsCount=total; } // console.log(result.length); // console.log(result.TodayCount); ctx.body = {"errcode": 10000, result:result}; } //删除生成的文章 export async function DeleteYJBDCArticleList(ctx) { const param = { UserID: ctx.query.UserID || 0, ID: ctx.query.ID || 0, Flag:-1, }; if (param.ID>0){ const url='GetYJBDCArticleList?UserID='+param.UserID+"&ID=0"; globalCache.delete(url); await yjbdc.UpdateYJBDCArticle(param); } ctx.body = {"errcode": 10000}; } export async function UpdateYJBDCArticle(ctx) { const param = ctx.request.body; if (param.ID>0 && param.UserID<12 && param.Source=="web"){ const url='GetYJBDCArticleList?IsChoiceness=0&UserID='+param.UserID+"&ID=0"; globalCache.delete(url); delete param.LevelStr; delete param.CreateTime; delete param.CreateTime; await yjbdc.UpdateYJBDCArticle(param); } ctx.body = {"errcode": 10000}; } //更新阅读数 export async function UpdateYJBDCArticleReadCount(ctx) { const param = { UserID: ctx.query.UserID || 0, ID: ctx.query.ID || 0, ReadCount:1, }; if (param.ID>0){ await yjbdc.UpdateYJBDCArticle(param); } ctx.body = {"errcode": 10000}; } //生成PDF 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; const menuConfig=constantClass.GetYJBDCGenerateConfig(); 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', './public/fonts/方正黑体简体.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)的转换函数 const pixelToPt = (pixel) => pixel * (595.28 / 2100); // 获取文章内容 let articleText = ""; if (content.ArticleEnglish) { articleText = Array.isArray(content.ArticleEnglish) ? content.ArticleEnglish.join(" ") : content.ArticleEnglish; } else { articleText = "No content available"; } articleText = articleText.replace(/<[^>]*>/g, ''); // 获取单词列表 let words = []; if (content.Words) { words = typeof content.Words === 'string' ? content.Words.split(",") : (Array.isArray(content.Words) ? content.Words : []); } // 获取问题列表 let questions = []; if (content.Question && Array.isArray(content.Question)) { questions = content.Question; } let articleStyleArr=menuConfig.ArticleStyle; let articleStyle="Story"; for(let i=0;i { doc.text(word, pixelToPt(122), wordY, { width: pixelToPt(535), align: 'left' }); wordY += pixelToPt(70); }); // 6. 文章内容 // 先计算文章内容的行数 doc.font('Helvetica'); // 使用48字号计算文本高度 const fontSize48 = pixelToPt(48); doc.fontSize(fontSize48); const textHeight48 = doc.heightOfString(articleText, { width: pixelToPt(1240), lineGap: pixelToPt(40.5) }); // 计算行数 (文本高度 / (字体大小 + 行间距)) const lineHeight = fontSize48 + pixelToPt(40.5); const lineCount = Math.ceil(textHeight48 / lineHeight); // 如果行数超过18行,则使用42字号 const fontSize = lineCount > 18 ? pixelToPt(42) : pixelToPt(48); // 渲染文章内容 doc.fontSize(fontSize) .text(articleText, pixelToPt(740), pixelToPt(105), { width: pixelToPt(1240), lineGap: pixelToPt(40.5) }); // 7. 黑线 const articleEndY = doc.y; doc.rect(pixelToPt(120), articleEndY + pixelToPt(41), pixelToPt(1860), pixelToPt(10)) .fill('black'); // 8-13. 问题和答案 if (questions.length > 0) { // 定义问题的X坐标位置 const questionXPositions = [ pixelToPt(120), // 问题1 pixelToPt(120), // 问题2 pixelToPt(740), // 问题3 pixelToPt(740), // 问题4 pixelToPt(1360) // 问题5 ]; // 存储每列最后一个选项的Y坐标位置 let lastOptionYPositions = { column1: 0, // 用于跟踪第一列(问题1)的最后位置 column2: 0, // 用于跟踪第二列(问题3)的最后位置 }; // 首先渲染问题1、3、5(第一行问题) const firstRowQuestions = [0, 2, 4]; // 问题1、3、5的索引 for (const i of firstRowQuestions) { if (i < questions.length && questions[i]) { const currentX = questionXPositions[i]; const currentY = articleEndY + pixelToPt(130); // 所有第一行问题的起始Y坐标相同 // 渲染问题文本 doc.font('Helvetica-Bold') .fontSize(pixelToPt(36)) .text(`${i+1}. ${questions[i].QuestionEnglish || `Question ${i+1}`}`, currentX, currentY, { width: pixelToPt(540), align: 'left' }); // 获取问题文本渲染后的Y坐标位置 const questionEndY = doc.y; // 选项起始位置 = 问题结束位置 + 20像素间距 let optionY = questionEndY + pixelToPt(28); // 渲染选项 const options = questions[i].OptionsEnglish || []; const optionLabels = ['A', 'B', 'C', 'D']; // 设置选项标签宽度和选项总宽度 const labelWidth = pixelToPt(40); // 标签(A./B./C./D.)的宽度 const totalWidth = pixelToPt(496); // 选项的总宽度 const contentWidth = totalWidth - labelWidth; // 选项内容的宽度 options.forEach((opt, index) => { if (index < optionLabels.length) { // 提取选项内容,移除可能存在的标签前缀 let optionContent = opt.replace(/^[A-D]\.\s*/, ''); // 渲染选项标签(A./B./C./D.) doc.font('Helvetica') .fontSize(pixelToPt(36)) .text(`${optionLabels[index]}.`, currentX, optionY, { width: labelWidth, align: 'left' }); // 渲染选项内容,确保折行时与内容第一行对齐 doc.font('Helvetica') .fontSize(pixelToPt(36)) .text(optionContent, currentX + labelWidth, optionY, { width: contentWidth, align: 'left' }); // 更新选项Y坐标,为下一个选项做准备 // 获取当前位置,确保下一个选项在当前选项完全渲染后的位置 optionY = doc.y + pixelToPt(8); } }); // 保存该列最后一个选项的Y坐标 if (i === 0) { lastOptionYPositions.column1 = doc.y; } else if (i === 2) { lastOptionYPositions.column2 = doc.y; } } } // 然后渲染问题2和4(第二行问题) if (questions.length > 1 && questions[1]) { // 问题2位于问题1的选项下方60像素处 const question2Y = lastOptionYPositions.column1 + pixelToPt(75); doc.font('Helvetica-Bold') .fontSize(pixelToPt(36)) .text(`2. ${questions[1].QuestionEnglish || "Question 2"}`, questionXPositions[1], question2Y, { width: pixelToPt(540), align: 'left' }); // 获取问题文本渲染后的Y坐标位置 const questionEndY = doc.y; // 选项起始位置 = 问题结束位置 + 20像素间距 let optionY = questionEndY + pixelToPt(28); // 渲染选项 const options = questions[1].OptionsEnglish || []; const optionLabels = ['A', 'B', 'C', 'D']; // 设置选项标签宽度和选项总宽度 const labelWidth = pixelToPt(40); // 标签(A./B./C./D.)的宽度 const totalWidth = pixelToPt(496); // 选项的总宽度 const contentWidth = totalWidth - labelWidth; // 选项内容的宽度 options.forEach((opt, index) => { if (index < optionLabels.length) { // 提取选项内容,移除可能存在的标签前缀 let optionContent = opt.replace(/^[A-D]\.\s*/, ''); // 渲染选项标签(A./B./C./D.) doc.font('Helvetica') .fontSize(pixelToPt(36)) .text(`${optionLabels[index]}.`, questionXPositions[1], optionY, { width: labelWidth, align: 'left' }); // 渲染选项内容,确保折行时与内容第一行对齐 doc.font('Helvetica') .fontSize(pixelToPt(36)) .text(optionContent, questionXPositions[1] + labelWidth, optionY, { width: contentWidth, align: 'left' }); // 更新选项Y坐标,为下一个选项做准备 optionY = doc.y + pixelToPt(8); } }); } if (questions.length > 3 && questions[3]) { // 问题4位于问题3的选项下方60像素处 const question4Y = lastOptionYPositions.column2 + pixelToPt(75); doc.font('Helvetica-Bold') .fontSize(pixelToPt(36)) .text(`4. ${questions[3].QuestionEnglish || "Question 4"}`, questionXPositions[3], question4Y, { width: pixelToPt(540), align: 'left' }); // 获取问题文本渲染后的Y坐标位置 const questionEndY = doc.y; // 选项起始位置 = 问题结束位置 + 20像素间距 let optionY = questionEndY + pixelToPt(28); // 渲染选项 const options = questions[3].OptionsEnglish || []; const optionLabels = ['A', 'B', 'C', 'D']; // 设置选项标签宽度和选项总宽度 const labelWidth = pixelToPt(40); // 标签(A./B./C./D.)的宽度 const totalWidth = pixelToPt(496); // 选项的总宽度 const contentWidth = totalWidth - labelWidth; // 选项内容的宽度 options.forEach((opt, index) => { if (index < optionLabels.length) { // 提取选项内容,移除可能存在的标签前缀 let optionContent = opt.replace(/^[A-D]\.\s*/, ''); // 渲染选项标签(A./B./C./D.) doc.font('Helvetica') .fontSize(pixelToPt(36)) .text(`${optionLabels[index]}.`, questionXPositions[3], optionY, { width: labelWidth, align: 'left' }); // 渲染选项内容,确保折行时与内容第一行对齐 doc.font('Helvetica') .fontSize(pixelToPt(36)) .text(optionContent, questionXPositions[3] + labelWidth, optionY, { width: contentWidth, align: 'left' }); // 更新选项Y坐标,为下一个选项做准备 optionY = doc.y + pixelToPt(8); } }); } // 问题5保持原位置不变 if (questions.length > 4 && questions[4]) { const currentX = questionXPositions[4]; const currentY = articleEndY + pixelToPt(130); doc.font('Helvetica-Bold') .fontSize(pixelToPt(36)) .text(`5. ${questions[4].QuestionEnglish || "Question 5"}`, currentX, currentY, { width: pixelToPt(540), align: 'left' }); const questionEndY = doc.y; let optionY = questionEndY + pixelToPt(28); // 渲染选项 const options = questions[4].OptionsEnglish || []; const optionLabels = ['A', 'B', 'C', 'D']; // 设置选项标签宽度和选项总宽度 const labelWidth = pixelToPt(40); // 标签(A./B./C./D.)的宽度 const totalWidth = pixelToPt(496); // 选项的总宽度 const contentWidth = totalWidth - labelWidth; // 选项内容的宽度 options.forEach((opt, index) => { if (index < optionLabels.length) { // 提取选项内容,移除可能存在的标签前缀 let optionContent = opt.replace(/^[A-D]\.\s*/, ''); // 渲染选项标签(A./B./C./D.) doc.font('Helvetica') .fontSize(pixelToPt(36)) .text(`${optionLabels[index]}.`, currentX, optionY, { width: labelWidth, align: 'left' }); // 渲染选项内容,确保折行时与内容第一行对齐 doc.font('Helvetica') .fontSize(pixelToPt(36)) .text(optionContent, currentX + labelWidth, optionY, { width: contentWidth, align: 'left' }); // 更新选项Y坐标,为下一个选项做准备 optionY = doc.y + pixelToPt(8); } }); } } // 14. 虚线 const lastContentY = doc.page.height - pixelToPt(190); // doc.rect(pixelToPt(120), lastContentY, // pixelToPt(1630), pixelToPt(10)) // .fill('#D2D2D2'); doc.strokeColor('#D2D2D2') .dash(pixelToPt(20), { space: pixelToPt(28) }) // 设置虚线样式:宽20像素,间隔20像素 .lineWidth(pixelToPt(10)) // 线高10像素 .moveTo(pixelToPt(120), lastContentY) .lineTo(pixelToPt(120) + pixelToPt(1630), lastContentY) .stroke() .undash(); // 重置虚线样式 // 15-19. 答案和底部信息 doc.font('Helvetica-Bold') // 使用Helvetica-Bold作为Semibold替代 .fontSize(pixelToPt(36)) .fillColor('black'); // 重置颜色为黑色 const answersY = doc.page.height - pixelToPt(144); // 获取问题答案 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, 262, 411, 561, 711]; for (let i = 0; i < Math.min(answers.length, 5); i++) { doc.text(answers[i], pixelToPt(answerPositions[i]), answersY+2, { width: pixelToPt(100), align: 'left' }); } // 20-21. 应用名称 doc.font('ChineseFont') .fontSize(pixelToPt(36)) .text("语境背单词(微信小程序)", pixelToPt(1338), answersY,{ width: pixelToPt(440), align: 'right' }); // 添加二维码图片 try { // 计算图片位置:距离右边120像素,距离底部100像素 // 由于PDFKit使用左上角坐标系,需要计算左上角坐标 const qrCodeWidth = pixelToPt(200); const qrCodeHeight = pixelToPt(200); const qrCodeX = doc.page.width - pixelToPt(120) - qrCodeWidth; // 右边距离转换为左边距离 const qrCodeY = doc.page.height - pixelToPt(100) - qrCodeHeight; // 底部距离转换为顶部距离 // 添加二维码图片 doc.image('./public/images/acode/YJBDC_QRCode.png', qrCodeX, qrCodeY, { width: qrCodeWidth, height: qrCodeHeight }); console.log("QR Code added successfully"); } catch (imgError) { console.error("Error adding QR Code image:", imgError); } // 结束PDF生成 doc.end(); // 等待PDF生成完成 const pdfBuffer = await new Promise((resolve) => { doc.on('end', () => { resolve(Buffer.concat(chunks)); }); }); let filename="语境背单词_"+moment().format("YYMMDD_HHmm"); // 设置响应头 ctx.set('Content-Type', 'application/pdf'); // 使用ASCII文件名作为主文件名,确保兼容性 const asciiFilename = 'yjbdc_' + moment().format("YYMMDD_HHmm") + '.pdf'; // 对中文文件名进行URL编码,用于filename*参数 const encodedFilename = encodeURIComponent(filename + '.pdf'); // 设置Content-Disposition头部,使用标准格式 // 首先提供ASCII文件名,然后提供UTF-8编码的文件名 ctx.set('Content-Disposition', `attachment; filename=${asciiFilename}; filename*=UTF-8''${encodedFilename}`); ctx.body = pdfBuffer; } catch (error) { console.error("Error generating PDF:", error); ctx.status = 500; ctx.body = { error: "Failed to generate PDF" }; } } //生成二维码 export async function BuildYJBDCQRCode(ctx) { try { // 获取微信访问令牌 const tokenUrl = `https://api.weixin.qq.com/cgi-bin/token?appid=${config.wx.yjbdc_appid}&secret=${config.wx.yjbdc_appsecret}&grant_type=client_credential`; const tokenResponse = await axios.get(tokenUrl); const tokenData = tokenResponse.data; if (!tokenData || !tokenData.access_token) { ctx.status = 400; ctx.body = { errcode: 101, errStr: '获取微信访问令牌失败' }; return; } const accessToken = tokenData.access_token; // 生成小程序码 const qrCodeUrl = `https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${accessToken}`; const path = './public/images/acode/'; const imageUrl = 'YJBDC_QRCode.png'; const fullPath = path + imageUrl; // 确保目录存在 if (!fs.existsSync(path)) { fs.mkdirSync(path, { recursive: true }); } const postData = { width: 280, scene: "SourceID=187" }; // 使用axios获取二维码并保存到文件 const qrCodeResponse = await axios({ method: 'POST', url: qrCodeUrl, data: postData, responseType: 'stream' }); // 创建写入流 const writer = fs.createWriteStream(fullPath); // 将响应数据写入文件 qrCodeResponse.data.pipe(writer); // 返回成功响应 ctx.body = { errcode: 10000, message: "二维码生成请求已发送" }; // 处理文件写入完成事件 writer.on('finish', () => { console.log("二维码生成成功:", fullPath); }); // 处理错误 writer.on('error', (err) => { console.error("二维码文件写入失败:", err); }); } catch (error) { console.error("生成二维码失败:", error); ctx.status = 500; ctx.body = { errcode: 500, errStr: '生成二维码失败', error: error.message }; } } export async function YJBDC_Articles_Admin(ctx) { console.log("yjbdc_articles"); const data = await fsPromises.readFile("./public/mg/yjbdc_articles.html"); ctx.body = data.toString(); };