yjbdcController.js 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357
  1. import moment from 'moment';
  2. import fs from 'fs';
  3. import { promises as fsPromises } from 'fs';
  4. import commonModel from '../../model/commonModel.js';
  5. import config from '../../config/index.js';
  6. import _ from 'lodash';
  7. import axios from 'axios';
  8. import { Encrypt, Decrypt } from '../../util/crypto/index.js';
  9. import { stringUtils } from '../../util/stringClass.js';
  10. import WXBizDataCrypt from '../../util/WXBizDataCrypt.js';
  11. import { globalCache } from '../../util/GlobalCache.js';
  12. import constantClass from '../../util/constant/index.js';
  13. import yjbdc from '../../model/yjbdc.js';
  14. import aiController from './aiController.js';
  15. import PDFDocument from 'pdfkit';
  16. import tencentcloud from 'tencentcloud-sdk-nodejs-ocr';
  17. const ONE_DAY_MAX_BUILD_COUNT=12;//一天最大生成数
  18. const OcrClient = tencentcloud.ocr.v20181119.Client;
  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. //OCR获取单词
  131. export async function OCRImageData(ctx) {
  132. const params = ctx.request.body;
  133. const clientConfig = {
  134. credential: {
  135. secretId: config.tencentcloud.secretId,
  136. secretKey: config.tencentcloud.secretKey,
  137. },
  138. region: "ap-guangzhou",
  139. profile: {
  140. httpProfile: {
  141. endpoint: "ocr.tencentcloudapi.com",
  142. },
  143. },
  144. };
  145. // 实例化要请求产品的client对象,clientProfile是可选的
  146. const client = new OcrClient(clientConfig);
  147. const result = await client.GeneralBasicOCR(params);
  148. let param2={};
  149. param2.UserID=ctx.query.UserID;
  150. param2.CreateTime=moment().format('YYYY-MM-DD HH:mm:ss');
  151. param2.Params=JSON.stringify(params);
  152. param2.JSONString=JSON.stringify(result);
  153. await yjbdc.AddOCRInfo(param2);
  154. ctx.body = {"errcode": 10000, result};
  155. }
  156. //AI生成文章
  157. export async function GenerateArticle(ctx) {
  158. const url='GenerateArticle?UserID='+ctx.query.UserID;
  159. let result = globalCache.get(url);
  160. if (result === 0) {
  161. const params = ctx.request.body;
  162. const words = params.Words;
  163. let articleStyle = params.ArticleStyle;
  164. if (words){
  165. //生成中
  166. result = { errcode: 10000, result: "-2" };
  167. globalCache.set(url, result, config.BufferMemoryTime);
  168. console.log("生成中,暂停生成60秒");
  169. const menuConfig=constantClass.GetYJBDCGenerateConfig();
  170. //console.log("content:"+content);
  171. let level="";
  172. if (!params.Level || params.Level=="" || params.Level==undefined || params.Level>="6"){
  173. params.Level=0;
  174. }
  175. level=menuConfig.Level[Number(params.Level)].Name;
  176. let articleStyleContent="";
  177. for(let i=0;i<menuConfig.ArticleStyle.length;i++){
  178. if (params.ArticleStyle==menuConfig.ArticleStyle[i].Name){
  179. articleStyleContent=menuConfig.ArticleStyle[i].Content;
  180. }
  181. }
  182. //console.log("Level:"+level);
  183. let content={
  184. "instruction": "用单词("+words+")生成"+level+"难度的文章。",
  185. "requirements": [
  186. "200-300词,要求:"+articleStyle+"("+articleStyleContent+")",
  187. "每句分数组提供中英双语,ArticleEnglish必须纯英文不含中文,ArticleChinese必须纯中文不含未翻译的英文单词",
  188. "所有单词至少出现一次(允许变形)",
  189. "生成5道四选一阅读题(含答案)",
  190. "提供用户单词在文中中文翻译",
  191. "所有字段必须完整出现,任何缺失需立即补全并维持原顺序,特别是Question中的所有字段(QuestionEnglish、QuestionChinese、OptionsEnglish、OptionsChinese、Answer)都必须存在"
  192. ],
  193. "output_format": {
  194. "ArticleEnglish": ["句子1", "句子2..."],
  195. "ArticleChinese": ["翻译1", "翻译2..."],
  196. "WordChinese": ["单词1:翻译1", "单词2:翻译2..."],
  197. "Question": [{
  198. "QuestionEnglish": "题干",
  199. "QuestionChinese": "题干翻译",
  200. "OptionsEnglish": ["A.选项", "B.选项", "C.选项", "D.选项"],
  201. "OptionsChinese": ["A.译", "B.译", "C.译", "D.译"],
  202. "Answer": "正确选项字母"
  203. }]
  204. }
  205. };
  206. content=JSON.stringify(content);
  207. // 从请求参数中获取AI提供者,如果没有指定则使用默认值
  208. let aiProvider = '';
  209. for(let i=0;i<menuConfig.AIVersion.length;i++){
  210. if (menuConfig.AIVersion[i].Version==params.AIVersion){
  211. aiProvider=menuConfig.AIVersion[i].Model;
  212. break;
  213. }
  214. }
  215. //给用户一些比较好的体验
  216. if (params.AIVersion=="1.0"){
  217. aiProvider = 'ali-Moonshot-Kimi-K2-Instruct';
  218. }
  219. else if (params.AIVersion=="1.5"){
  220. aiProvider = 'doubao-kimi-k2-250711';
  221. }
  222. try {
  223. //开始时间
  224. let timeStart=new Date().getTime();
  225. // 使用aiController生成文章
  226. let result2 = await aiController.generateArticle(content, aiProvider);
  227. //console.log(result2);
  228. //debugger;
  229. // 校验和修复JSON结构
  230. result2 = aiController.validateAndFixJSON(result2);
  231. //console.log("JSON结构已校验和修复");
  232. result2 =aiController.normalizeArticleFields(result2);
  233. // 解析JSON以增强FormsOfWords
  234. const jsonObj = JSON.parse(result2);
  235. // 增强FormsOfWords,检测文章中单词的变形形式和拼写错误
  236. const enhancedJsonObj = aiController.enhanceFormsOfWords(jsonObj, words);
  237. // 再次确保中文句子被正确分类
  238. if (enhancedJsonObj.ArticleEnglish && Array.isArray(enhancedJsonObj.ArticleEnglish)) {
  239. const englishSentences = [];
  240. const chineseSentences = [];
  241. // 遍历ArticleEnglish数组,分离英文和中文句子
  242. enhancedJsonObj.ArticleEnglish.forEach(sentence => {
  243. // 检查句子是否包含中文字符
  244. if (/[\u4e00-\u9fa5]/.test(sentence)) {
  245. chineseSentences.push(sentence);
  246. } else {
  247. englishSentences.push(sentence);
  248. }
  249. });
  250. // 更新ArticleEnglish数组,只保留英文句子
  251. enhancedJsonObj.ArticleEnglish = englishSentences;
  252. // 如果ArticleChinese不存在或不是数组,则创建它
  253. if (!enhancedJsonObj.ArticleChinese || !Array.isArray(enhancedJsonObj.ArticleChinese)) {
  254. enhancedJsonObj.ArticleChinese = [];
  255. }
  256. // 将中文句子添加到ArticleChinese数组中
  257. chineseSentences.forEach(sentence => {
  258. if (!enhancedJsonObj.ArticleChinese.includes(sentence)) {
  259. enhancedJsonObj.ArticleChinese.push(sentence);
  260. }
  261. });
  262. }
  263. // 将增强后的对象转回JSON字符串
  264. result2 = JSON.stringify(enhancedJsonObj);
  265. //console.log("FormsOfWords已增强,添加了单词变形和拼写错误检测");
  266. // 记录增强后的单词数量
  267. if (enhancedJsonObj.FormsOfWords && Array.isArray(enhancedJsonObj.FormsOfWords)) {
  268. console.log(`增强了${enhancedJsonObj.FormsOfWords.length}个单词的变形形式`);
  269. }
  270. let timeEnd=new Date().getTime();
  271. let param2={};
  272. param2.UserID=ctx.query.UserID;
  273. param2.CreateTime=moment().format('YYYY-MM-DD HH:mm:ss');
  274. param2.Words=words;
  275. param2.Level=params.Level;
  276. param2.articleStyle=articleStyle;
  277. param2.AIProvider=aiProvider;
  278. param2.BuildStr=content;
  279. param2.GenerateTime=Math.round((timeEnd-timeStart)/1000);
  280. console.log("生成时间:"+param2.GenerateTime+"秒");
  281. // 尝试解析JSON以获取ArticleStart
  282. const jsonObj2 = JSON.parse(result2);
  283. param2.ArticleStart = jsonObj2.ArticleEnglish && jsonObj2.ArticleEnglish.length > 0 ? jsonObj2.ArticleEnglish.join("\r\n") : "No content available";
  284. // 去除JSON字符串中的所有换行符(\r\n, \n, \r)
  285. param2.JSONString=JSON.stringify(result2.replace(/[\r\n]+/g, ''));
  286. param2.Flag=0;
  287. let idInsert= await yjbdc.AddArticleInfo(param2);
  288. //result = { errcode: 10000, result: result2 };
  289. let result3={};
  290. result3.ID=idInsert.insertId;
  291. result3.Content=result2;
  292. result3.IsNew=true;
  293. result = { errcode: 10000, result: result3 };
  294. globalCache.delete(url);
  295. console.log("删除缓存,恢复生成");
  296. }
  297. catch(err){
  298. console.error("AI生成错误:"+err);
  299. result = { errcode: 10000, result: "-1" };
  300. }
  301. }
  302. else{
  303. console.log("空单词串");
  304. result = { errcode: 10000, result: "-1" };
  305. }
  306. }
  307. ctx.body = result;
  308. }
  309. export async function GetYJBDCGenerateConfig(ctx) {
  310. const param = {
  311. UserID: ctx.query.UserID || 0,
  312. };
  313. let result=constantClass.GetYJBDCGenerateConfig();
  314. for (let i=0;i<result.Level.length;i++){
  315. delete result.Level[i].English;
  316. }
  317. for(let i=0;i<result.ArticleStyle.length;i++){
  318. delete result.ArticleStyle[i].English;
  319. delete result.ArticleStyle[i].Content;
  320. }
  321. if (param.UserID>3){
  322. result.AIVersion.splice(2,result.AIVersion.length-2);
  323. if (param.UserID==185){
  324. const configArr=constantClass.GetYJBDCGenerateConfig();
  325. result.AIVersion.push(configArr.AIVersion[5]);
  326. }
  327. // if (param.UserID<4){
  328. // const configArr=constantClass.GetYJBDCGenerateConfig();
  329. // result.AIVersion.push(configArr.AIVersion[6]);
  330. // }
  331. }
  332. ctx.body = {"errcode": 10000, result:result};
  333. }
  334. //获得秒过当天任务完成后的英语单词
  335. export async function GetMiaoguoTodayAllWords(ctx) {
  336. const param = {
  337. UserID: ctx.query.UserID || 0,
  338. };
  339. const url = `https://www.kylx365.com/api/GetMiaoguoCardList2?UserID=${param.UserID}&IsToday=2&CardType=0&OrderType=ac.LastTime%20desc`;
  340. let result = await axios.get(url)
  341. .then(res => {
  342. let list = res.data.result.List;
  343. if (list && list.length>0) {
  344. let arr=[],arrNew=[];
  345. const today=moment().format("YYYY-MM-DD 00:00:00");
  346. for(let i=0;i<list.length;i++){
  347. // console.log("问题:"+list[i].Content[1].Content);
  348. // console.log("标签:"+list[i].Content[0].Content);
  349. // console.log("答案:"+list[i].Content[2].Content);
  350. let str1=fun1(list[i].Content[1].Content);
  351. if (str1){
  352. if (list[i].FirstTime>today){
  353. arrNew.push(str1)
  354. }
  355. else{
  356. arr.push(str1);
  357. }
  358. }
  359. else{
  360. let str2=fun1(list[i].Content[2].Content);
  361. if (str2){
  362. if (list[i].FirstTime>today){
  363. arrNew.push(str2)
  364. }
  365. else{
  366. arr.push(str2);
  367. }
  368. }
  369. }
  370. }
  371. let arr2=stringUtils.extractEnglishWords(arr);
  372. let arr3=stringUtils.extractEnglishWords(arrNew);
  373. let arr4=arr3.concat(arr2);
  374. return {"errcode": 10000, result:arr4.join(",")}
  375. }
  376. else{
  377. return {errcode: 101};
  378. }
  379. })
  380. .catch(err => {
  381. debugger;
  382. return {errcode: 101, errStr: err};
  383. });
  384. ctx.body = result;
  385. function fun1(str){
  386. let result="";
  387. if (str.length>100)
  388. result="";
  389. else if (str.indexOf("[特")>=0){
  390. let str3=str.substring(str.indexOf("[特")+2);
  391. str3=str3.substring(str3.indexOf("]")+1,str3.indexOf("[/特]"));
  392. result=str3;
  393. }
  394. else if (str.indexOf("[线]")>=0){
  395. let str3=str.substring(str.indexOf("[线]")+3,str.indexOf("[/线]"));
  396. result=str3;
  397. }
  398. else if (str.indexOf("[光]")>=0){
  399. let str3=str.substring(str.indexOf("[光]")+3,str.indexOf("[/光]"));
  400. result=str3;
  401. }
  402. else {
  403. if (isValidString(str))
  404. result=str;
  405. }
  406. return result;
  407. }
  408. function isValidString(str) {
  409. // 正则表达式:允许大小写字母(a-zA-Z)、单引号(')、减号(-)和空格(\s)
  410. return /^[a-zA-Z'\-\s]+$/.test(str);
  411. }
  412. }
  413. //获得文章列表或具体文章
  414. export async function GetYJBDCArticleList(ctx) {
  415. const param = {
  416. UserID: ctx.query.UserID || 0,
  417. ID:ctx.query.ID || 0,
  418. IsFine:ctx.query.IsFine || 0,//是否是精选文章
  419. IsTodayCount: ctx.query.IsTodayCount || false,
  420. IsNew: ctx.query.IsNew || 0,
  421. PageID: ctx.query.PageID || 999999,
  422. PageCount: ctx.query.PageCount || 10,
  423. };
  424. if (param.IsFine)
  425. param.PageCount=9999;
  426. // 尝试从缓存获取
  427. const url='GetYJBDCArticleList?IsFine='+param.IsFine+'&UserID='+param.UserID+'&ID='+param.ID;
  428. let result = globalCache.get(url);
  429. if (param.IsNew==1)
  430. result=0;
  431. if (result === 0) {
  432. result = await yjbdc.GetYJBDCArticleList(param);
  433. let menuConfig=constantClass.GetYJBDCGenerateConfig();
  434. // 随机选择三个不重复的索引用于今日推荐
  435. let recommendIndices = [];
  436. if(result.length > 3) {
  437. while(recommendIndices.length < 3) {
  438. const randomIndex = Math.floor(Math.random() * result.length);
  439. if(!recommendIndices.includes(randomIndex)) {
  440. recommendIndices.push(randomIndex);
  441. }
  442. }
  443. } else {
  444. // 如果结果少于3个,全部标记为推荐
  445. for(let i = 0; i < result.length; i++) {
  446. recommendIndices.push(i);
  447. }
  448. }
  449. for(let i=0;i<result.length;i++){
  450. let item=result[i];
  451. item.CreateTime=moment(item.CreateTime).format("YYYY年MM月DD日 HH:mm");
  452. //debugger;
  453. item.LevelStr=menuConfig.Level[item.Level].Name;
  454. // 添加今日推荐标记
  455. item.IsRecommend = recommendIndices.includes(i);
  456. }
  457. globalCache.set(url, result, config.BufferMemoryTime);
  458. console.log("缓存"+config.BufferMemoryTime+"秒");
  459. }
  460. //console.log(result.length);
  461. if (param.IsTodayCount && !param.ID){
  462. let count=0,unReadCount=0;
  463. const today=moment().format("YYYY年MM月DD日 00:00");
  464. for(let i=0;i<result.length;i++){
  465. if (result[i].CreateTime>=today)
  466. count++;
  467. if (result[i].Flag==0 && result[i].ReadCount==0)
  468. unReadCount++;
  469. }
  470. let maxcount=ONE_DAY_MAX_BUILD_COUNT;
  471. if (param.UserID<4){
  472. maxcount=100;
  473. }
  474. result={
  475. TodayCount:count,
  476. MaxCount:maxcount,
  477. UnReadCount:unReadCount,
  478. };
  479. }
  480. else if (!param.ID){
  481. let arr=[],count=0,total=0;
  482. for(let i=0;i<result.length;i++){
  483. if (result[i].Flag==0){
  484. if (param.PageID>result[i].ID && count<param.PageCount){
  485. arr.push(result[i]);
  486. count++;
  487. }
  488. total++;
  489. }
  490. }
  491. result=arr;
  492. if (result.length>0)
  493. result[0].RowsCount=total;
  494. }
  495. // console.log(result.length);
  496. // console.log(result.TodayCount);
  497. ctx.body = {"errcode": 10000, result:result};
  498. }
  499. //删除生成的文章
  500. export async function DeleteYJBDCArticleList(ctx) {
  501. const param = {
  502. UserID: ctx.query.UserID || 0,
  503. ID: ctx.query.ID || 0,
  504. Flag:-1,
  505. };
  506. if (param.ID>0){
  507. const url='GetYJBDCArticleList?UserID='+param.UserID+"&ID=0";
  508. globalCache.delete(url);
  509. await yjbdc.UpdateYJBDCArticle(param);
  510. }
  511. ctx.body = {"errcode": 10000};
  512. }
  513. // 更新文章数据(目前仅用于网站)
  514. export async function UpdateYJBDCArticle(ctx) {
  515. const param = ctx.request.body;
  516. if (param.ID>0 && param.UserID<12 && param.Source=="web"){
  517. const url='GetYJBDCArticleList?IsFine=0&UserID='+param.UserID+"&ID=0";
  518. globalCache.delete(url);
  519. const url2='GetYJBDCArticleList?IsFine=0&UserID='+param.UserID+"&ID="+param.ID;
  520. globalCache.delete(url2);
  521. delete param.LevelStr;
  522. delete param.CreateTime;
  523. delete param.IsRecommend;
  524. await yjbdc.UpdateYJBDCArticle(param);
  525. }
  526. ctx.body = {"errcode": 10000};
  527. }
  528. //更新阅读数
  529. export async function UpdateYJBDCArticleReadCount(ctx) {
  530. const param = {
  531. UserID: ctx.query.UserID || 0,
  532. ID: ctx.query.ID || 0,
  533. ReadCount:1,
  534. };
  535. if (param.ID>0){
  536. await yjbdc.UpdateYJBDCArticle(param);
  537. }
  538. ctx.body = {"errcode": 10000};
  539. }
  540. //生成PDF
  541. export async function GeneratePDF(ctx) {
  542. const params = ctx.request.body;
  543. if (!params || !params.Content) {
  544. ctx.status = 400;
  545. ctx.body = { error: 'Invalid request body: Content is required' };
  546. return;
  547. }
  548. const content = params.Content;
  549. const menuConfig=constantClass.GetYJBDCGenerateConfig();
  550. try {
  551. // 创建新的 PDF 文档 - 使用A4尺寸
  552. const doc = new PDFDocument({
  553. size: 'A4', // 使用标准A4尺寸 (595.28 x 841.89 points)
  554. margins: {
  555. top: 0,
  556. bottom: 0,
  557. left: 0,
  558. right: 0
  559. },
  560. autoFirstPage: true
  561. });
  562. // 注册中文字体
  563. doc.registerFont('ChineseFont', './public/fonts/方正黑体简体.TTF');
  564. // 定义字体选择函数
  565. const selectFont = (text, defaultFont = 'Helvetica') => {
  566. if (/[\u4E00-\u9FFF]/.test(text)) {
  567. return 'ChineseFont';
  568. }
  569. return defaultFont;
  570. };
  571. // 收集生成的 PDF 数据
  572. const chunks = [];
  573. doc.on('data', (chunk) => chunks.push(chunk));
  574. // 像素到点(pt)的转换函数
  575. const pixelToPt = (pixel) => pixel * (595.28 / 2100);
  576. // 获取文章内容
  577. let articleText = "";
  578. if (content.ArticleEnglish) {
  579. articleText = Array.isArray(content.ArticleEnglish) ?
  580. content.ArticleEnglish.join(" ") : content.ArticleEnglish;
  581. } else {
  582. articleText = "No content available";
  583. }
  584. articleText = articleText.replace(/<[^>]*>/g, '');
  585. articleText = articleText.replace(/(^|[,.]\s+)'([^']+)'/g, '$1"$2"');
  586. // 获取单词列表
  587. let words = [];
  588. if (content.Words) {
  589. words = typeof content.Words === 'string' ?
  590. content.Words.split(",") :
  591. (Array.isArray(content.Words) ? content.Words : []);
  592. }
  593. // 获取问题列表
  594. let questions = [];
  595. if (content.Question && Array.isArray(content.Question)) {
  596. questions = content.Question;
  597. }
  598. let articleStyleArr=menuConfig.ArticleStyle;
  599. let articleStyle="Story";
  600. for(let i=0;i<articleStyleArr.length;i++){
  601. if (articleStyleArr[i].Name==content.ArticleStyle){
  602. articleStyle=articleStyleArr[i].English;
  603. break;
  604. }
  605. }
  606. // 1. 标题 - 文章类型
  607. doc.font(selectFont(articleStyle))
  608. .fontSize(pixelToPt(48))
  609. .text(articleStyle,
  610. pixelToPt(120), pixelToPt(110));
  611. // 2. 副标题 - 难度级别
  612. doc.font('Helvetica')
  613. .fontSize(pixelToPt(30))
  614. .text(menuConfig.Level[Number(content.Level)].English || "",
  615. pixelToPt(120), pixelToPt(170));
  616. // 3. 时间
  617. const currentDate = moment().format("YYYY年MM月DD日 HH:mm");
  618. if (params.CreateTime)
  619. currentDate=moment(params.CreateTime).format("YYYY年MM月DD日 HH:mm");
  620. doc.font('ChineseFont')
  621. .fontSize(pixelToPt(30))
  622. .text(currentDate,
  623. pixelToPt(120), pixelToPt(212));
  624. // 4. 黑线
  625. doc.rect(pixelToPt(120), pixelToPt(289), pixelToPt(537), pixelToPt(10))
  626. .fill('black');
  627. // 5. 单词列表
  628. doc.font('Helvetica')
  629. .fontSize(pixelToPt(36));
  630. let wordY = pixelToPt(364);
  631. words.slice(0, 10).forEach(word => {
  632. doc.text(word, pixelToPt(122), wordY, {
  633. width: pixelToPt(535),
  634. align: 'left'
  635. });
  636. wordY += pixelToPt(70);
  637. });
  638. // 6. 文章内容
  639. // 先计算文章内容的行数
  640. doc.font('Helvetica');
  641. // 使用48字号计算文本高度
  642. const fontSize48 = pixelToPt(48);
  643. doc.fontSize(fontSize48);
  644. const textHeight48 = doc.heightOfString(articleText, {
  645. width: pixelToPt(1240),
  646. lineGap: pixelToPt(40.5)
  647. });
  648. // 计算行数 (文本高度 / (字体大小 + 行间距))
  649. const lineHeight = fontSize48 + pixelToPt(40.5);
  650. const lineCount = Math.ceil(textHeight48 / lineHeight);
  651. // 如果行数超过18行,则使用42字号
  652. const fontSize = lineCount > 18 ? pixelToPt(42) : pixelToPt(48);
  653. // 渲染文章内容
  654. doc.fontSize(fontSize)
  655. .text(articleText, pixelToPt(740), pixelToPt(105), {
  656. width: pixelToPt(1240),
  657. lineGap: pixelToPt(40.5)
  658. });
  659. // 7. 黑线
  660. const articleEndY = doc.y;
  661. const wordListEndY = doc.y; // 假设单词列表的Y坐标
  662. const maxY = Math.max(articleEndY, wordListEndY);
  663. // 检查内容是否过长,需要分页
  664. const pageHeight = doc.page.height;
  665. const availableHeight = pageHeight - pixelToPt(400); // 预留底部空间
  666. const needsNewPage = articleEndY > availableHeight;
  667. if (doc && typeof doc.line === 'function') {
  668. doc.line(20, maxY, 200, maxY);
  669. }
  670. // 如果内容过长,则创建新页面
  671. if (needsNewPage) {
  672. // 在第一页底部添加提示
  673. doc.font('Helvetica')
  674. .fontSize(pixelToPt(24))
  675. .text("(Continued on next page...)",
  676. pixelToPt(740), pageHeight - pixelToPt(100), {
  677. align: 'center'
  678. });
  679. // 添加新页面
  680. doc.addPage();
  681. // 在新页面顶部添加标题
  682. doc.font(selectFont(articleStyle))
  683. .fontSize(pixelToPt(36))
  684. .text(articleStyle + " (Continued)",
  685. pixelToPt(120), pixelToPt(50));
  686. // 在新页面添加黑线
  687. doc.rect(pixelToPt(120), pixelToPt(100),
  688. pixelToPt(1860), pixelToPt(10))
  689. .fill('black');
  690. } else {
  691. // 如果不需要分页,正常添加黑线
  692. doc.rect(pixelToPt(120), articleEndY + pixelToPt(41),
  693. pixelToPt(1860), pixelToPt(10))
  694. .fill('black');
  695. }
  696. // 8-13. 问题和答案
  697. if (questions.length > 0) {
  698. // 定义问题的X坐标位置
  699. const questionXPositions = [
  700. pixelToPt(120), // 问题1
  701. pixelToPt(120), // 问题2
  702. pixelToPt(740), // 问题3
  703. pixelToPt(740), // 问题4
  704. pixelToPt(1360) // 问题5
  705. ];
  706. // 存储每列最后一个选项的Y坐标位置
  707. let lastOptionYPositions = {
  708. column1: 0, // 用于跟踪第一列(问题1)的最后位置
  709. column2: 0, // 用于跟踪第二列(问题3)的最后位置
  710. };
  711. // 确定问题的起始Y坐标
  712. // 如果内容过长需要分页,则在新页面上从固定位置开始
  713. // 否则在文章内容下方开始
  714. const questionsStartY = needsNewPage ?
  715. pixelToPt(150) : // 新页面上的起始位置
  716. articleEndY + pixelToPt(130); // 原页面上的起始位置
  717. // 首先渲染问题1、3、5(第一行问题)
  718. const firstRowQuestions = [0, 2, 4]; // 问题1、3、5的索引
  719. for (const i of firstRowQuestions) {
  720. if (i < questions.length && questions[i]) {
  721. const currentX = questionXPositions[i];
  722. const currentY = questionsStartY; // 使用计算出的起始Y坐标
  723. questions[i].QuestionEnglish = questions[i].QuestionEnglish.replace(/(^|[,.]\s+)'([^']+)'/g, '$1"$2"');
  724. // 渲染问题文本
  725. doc.font('Helvetica-Bold')
  726. .fontSize(pixelToPt(36))
  727. .text(`${i+1}. ${questions[i].QuestionEnglish || `Question ${i+1}`}`,
  728. currentX, currentY, {
  729. width: pixelToPt(540),
  730. align: 'left'
  731. });
  732. // 获取问题文本渲染后的Y坐标位置
  733. const questionEndY = doc.y;
  734. // 选项起始位置 = 问题结束位置 + 20像素间距
  735. let optionY = questionEndY + pixelToPt(28);
  736. // 渲染选项
  737. const options = questions[i].OptionsEnglish || [];
  738. const optionLabels = ['A', 'B', 'C', 'D'];
  739. // 设置选项标签宽度和选项总宽度
  740. const labelWidth = pixelToPt(40); // 标签(A./B./C./D.)的宽度
  741. const totalWidth = pixelToPt(496); // 选项的总宽度
  742. const contentWidth = totalWidth - labelWidth; // 选项内容的宽度
  743. options.forEach((opt, index) => {
  744. if (index < optionLabels.length) {
  745. // 提取选项内容,移除可能存在的标签前缀
  746. let optionContent = opt.replace(/^[A-D]\.\s*/, '');
  747. // 渲染选项标签(A./B./C./D.)
  748. doc.font('Helvetica')
  749. .fontSize(pixelToPt(36))
  750. .text(`${optionLabels[index]}.`, currentX, optionY, {
  751. width: labelWidth,
  752. align: 'left'
  753. });
  754. // 渲染选项内容,确保折行时与内容第一行对齐
  755. doc.font('Helvetica')
  756. .fontSize(pixelToPt(36))
  757. .text(optionContent, currentX + labelWidth, optionY, {
  758. width: contentWidth,
  759. align: 'left'
  760. });
  761. // 更新选项Y坐标,为下一个选项做准备
  762. // 获取当前位置,确保下一个选项在当前选项完全渲染后的位置
  763. optionY = doc.y + pixelToPt(8);
  764. }
  765. });
  766. // 保存该列最后一个选项的Y坐标
  767. if (i === 0) {
  768. lastOptionYPositions.column1 = doc.y;
  769. } else if (i === 2) {
  770. lastOptionYPositions.column2 = doc.y;
  771. }
  772. }
  773. }
  774. // 然后渲染问题2和4(第二行问题)
  775. if (questions.length > 1 && questions[1]) {
  776. // 问题2位于问题1的选项下方60像素处
  777. const question2Y = lastOptionYPositions.column1 + pixelToPt(75);
  778. doc.font('Helvetica-Bold')
  779. .fontSize(pixelToPt(36))
  780. .text(`2. ${questions[1].QuestionEnglish || "Question 2"}`,
  781. questionXPositions[1], question2Y, {
  782. width: pixelToPt(540),
  783. align: 'left'
  784. });
  785. // 获取问题文本渲染后的Y坐标位置
  786. const questionEndY = doc.y;
  787. // 选项起始位置 = 问题结束位置 + 20像素间距
  788. let optionY = questionEndY + pixelToPt(28);
  789. // 渲染选项
  790. const options = questions[1].OptionsEnglish || [];
  791. const optionLabels = ['A', 'B', 'C', 'D'];
  792. // 设置选项标签宽度和选项总宽度
  793. const labelWidth = pixelToPt(40); // 标签(A./B./C./D.)的宽度
  794. const totalWidth = pixelToPt(496); // 选项的总宽度
  795. const contentWidth = totalWidth - labelWidth; // 选项内容的宽度
  796. options.forEach((opt, index) => {
  797. if (index < optionLabels.length) {
  798. // 提取选项内容,移除可能存在的标签前缀
  799. let optionContent = opt.replace(/^[A-D]\.\s*/, '');
  800. // 渲染选项标签(A./B./C./D.)
  801. doc.font('Helvetica')
  802. .fontSize(pixelToPt(36))
  803. .text(`${optionLabels[index]}.`, questionXPositions[1], optionY, {
  804. width: labelWidth,
  805. align: 'left'
  806. });
  807. // 渲染选项内容,确保折行时与内容第一行对齐
  808. doc.font('Helvetica')
  809. .fontSize(pixelToPt(36))
  810. .text(optionContent, questionXPositions[1] + labelWidth, optionY, {
  811. width: contentWidth,
  812. align: 'left'
  813. });
  814. // 更新选项Y坐标,为下一个选项做准备
  815. optionY = doc.y + pixelToPt(8);
  816. }
  817. });
  818. }
  819. if (questions.length > 3 && questions[3]) {
  820. // 问题4位于问题3的选项下方60像素处
  821. const question4Y = lastOptionYPositions.column2 + pixelToPt(75);
  822. doc.font('Helvetica-Bold')
  823. .fontSize(pixelToPt(36))
  824. .text(`4. ${questions[3].QuestionEnglish || "Question 4"}`,
  825. questionXPositions[3], question4Y, {
  826. width: pixelToPt(540),
  827. align: 'left'
  828. });
  829. // 获取问题文本渲染后的Y坐标位置
  830. const questionEndY = doc.y;
  831. // 选项起始位置 = 问题结束位置 + 20像素间距
  832. let optionY = questionEndY + pixelToPt(28);
  833. // 渲染选项
  834. const options = questions[3].OptionsEnglish || [];
  835. const optionLabels = ['A', 'B', 'C', 'D'];
  836. // 设置选项标签宽度和选项总宽度
  837. const labelWidth = pixelToPt(40); // 标签(A./B./C./D.)的宽度
  838. const totalWidth = pixelToPt(496); // 选项的总宽度
  839. const contentWidth = totalWidth - labelWidth; // 选项内容的宽度
  840. options.forEach((opt, index) => {
  841. if (index < optionLabels.length) {
  842. // 提取选项内容,移除可能存在的标签前缀
  843. let optionContent = opt.replace(/^[A-D]\.\s*/, '');
  844. // 渲染选项标签(A./B./C./D.)
  845. doc.font('Helvetica')
  846. .fontSize(pixelToPt(36))
  847. .text(`${optionLabels[index]}.`, questionXPositions[3], optionY, {
  848. width: labelWidth,
  849. align: 'left'
  850. });
  851. // 渲染选项内容,确保折行时与内容第一行对齐
  852. doc.font('Helvetica')
  853. .fontSize(pixelToPt(36))
  854. .text(optionContent, questionXPositions[3] + labelWidth, optionY, {
  855. width: contentWidth,
  856. align: 'left'
  857. });
  858. // 更新选项Y坐标,为下一个选项做准备
  859. optionY = doc.y + pixelToPt(8);
  860. }
  861. });
  862. }
  863. // 问题5
  864. if (questions.length > 4 && questions[4]) {
  865. const currentX = questionXPositions[4];
  866. // 如果分页,问题5的Y坐标与问题1和3相同
  867. const currentY = questionsStartY;
  868. doc.font('Helvetica-Bold')
  869. .fontSize(pixelToPt(36))
  870. .text(`5. ${questions[4].QuestionEnglish || "Question 5"}`,
  871. currentX, currentY, {
  872. width: pixelToPt(540),
  873. align: 'left'
  874. });
  875. const questionEndY = doc.y;
  876. let optionY = questionEndY + pixelToPt(28);
  877. // 渲染选项
  878. const options = questions[4].OptionsEnglish || [];
  879. const optionLabels = ['A', 'B', 'C', 'D'];
  880. // 设置选项标签宽度和选项总宽度
  881. const labelWidth = pixelToPt(40); // 标签(A./B./C./D.)的宽度
  882. const totalWidth = pixelToPt(496); // 选项的总宽度
  883. const contentWidth = totalWidth - labelWidth; // 选项内容的宽度
  884. options.forEach((opt, index) => {
  885. if (index < optionLabels.length) {
  886. // 提取选项内容,移除可能存在的标签前缀
  887. let optionContent = opt.replace(/^[A-D]\.\s*/, '');
  888. // 渲染选项标签(A./B./C./D.)
  889. doc.font('Helvetica')
  890. .fontSize(pixelToPt(36))
  891. .text(`${optionLabels[index]}.`, currentX, optionY, {
  892. width: labelWidth,
  893. align: 'left'
  894. });
  895. // 渲染选项内容,确保折行时与内容第一行对齐
  896. doc.font('Helvetica')
  897. .fontSize(pixelToPt(36))
  898. .text(optionContent, currentX + labelWidth, optionY, {
  899. width: contentWidth,
  900. align: 'left'
  901. });
  902. // 更新选项Y坐标,为下一个选项做准备
  903. optionY = doc.y + pixelToPt(8);
  904. }
  905. });
  906. }
  907. }
  908. // 14. 虚线
  909. // 计算虚线的位置 - 如果是分页,则在新页面上绘制
  910. const lastContentY = needsNewPage ?
  911. doc.page.height - pixelToPt(190) : // 新页面上的位置
  912. doc.page.height - pixelToPt(190); // 原页面上的位置
  913. doc.strokeColor('#D2D2D2')
  914. .dash(pixelToPt(20), { space: pixelToPt(28) }) // 设置虚线样式:宽20像素,间隔20像素
  915. .lineWidth(pixelToPt(10)) // 线高10像素
  916. .moveTo(pixelToPt(120), lastContentY)
  917. .lineTo(pixelToPt(120) + pixelToPt(1630), lastContentY)
  918. .stroke()
  919. .undash(); // 重置虚线样式
  920. // 15-19. 答案和底部信息
  921. doc.font('Helvetica-Bold') // 使用Helvetica-Bold作为Semibold替代
  922. .fontSize(pixelToPt(36))
  923. .fillColor('black'); // 重置颜色为黑色
  924. const answersY = doc.page.height - pixelToPt(144);
  925. // 获取问题答案
  926. const answers = [];
  927. if (questions.length > 0) {
  928. for (let i = 0; i < Math.min(questions.length, 5); i++) {
  929. const answer = questions[i].Answer || "";
  930. answers.push(`${i+1}. ${answer}`);
  931. }
  932. }
  933. // 显示答案(如果存在)
  934. const answerPositions = [120, 262, 411, 561, 711];
  935. for (let i = 0; i < Math.min(answers.length, 5); i++) {
  936. doc.text(answers[i], pixelToPt(answerPositions[i]), answersY+2, {
  937. width: pixelToPt(100),
  938. align: 'left'
  939. });
  940. }
  941. // 20-21. 应用名称
  942. doc.font('ChineseFont')
  943. .fontSize(pixelToPt(36))
  944. .text("语境背单词(微信小程序)", pixelToPt(1338), answersY,{
  945. width: pixelToPt(440),
  946. align: 'right'
  947. });
  948. // 添加二维码图片
  949. try {
  950. // 计算图片位置:距离右边120像素,距离底部100像素
  951. // 由于PDFKit使用左上角坐标系,需要计算左上角坐标
  952. const qrCodeWidth = pixelToPt(200);
  953. const qrCodeHeight = pixelToPt(200);
  954. const qrCodeX = doc.page.width - pixelToPt(120) - qrCodeWidth; // 右边距离转换为左边距离
  955. const qrCodeY = doc.page.height - pixelToPt(100) - qrCodeHeight; // 底部距离转换为顶部距离
  956. // 添加二维码图片
  957. doc.image('./public/images/acode/YJBDC_QRCode.png', qrCodeX, qrCodeY, {
  958. width: qrCodeWidth,
  959. height: qrCodeHeight
  960. });
  961. console.log("QR Code added successfully");
  962. } catch (imgError) {
  963. console.error("Error adding QR Code image:", imgError);
  964. }
  965. // 结束PDF生成
  966. doc.end();
  967. // 等待PDF生成完成
  968. const pdfBuffer = await new Promise((resolve) => {
  969. doc.on('end', () => {
  970. resolve(Buffer.concat(chunks));
  971. });
  972. });
  973. let filename="语境背单词_"+moment().format("YYMMDD_HHmm");
  974. // 设置响应头
  975. ctx.set('Content-Type', 'application/pdf');
  976. // 使用ASCII文件名作为主文件名,确保兼容性
  977. const asciiFilename = 'yjbdc_' + moment().format("YYMMDD_HHmm") + '.pdf';
  978. // 对中文文件名进行URL编码,用于filename*参数
  979. const encodedFilename = encodeURIComponent(filename + '.pdf');
  980. // 设置Content-Disposition头部,使用标准格式
  981. // 首先提供ASCII文件名,然后提供UTF-8编码的文件名
  982. ctx.set('Content-Disposition', `attachment; filename=${asciiFilename}; filename*=UTF-8''${encodedFilename}`);
  983. ctx.body = pdfBuffer;
  984. } catch (error) {
  985. console.error("Error generating PDF:", error);
  986. ctx.status = 500;
  987. ctx.body = { error: "Failed to generate PDF" };
  988. }
  989. }
  990. //生成二维码
  991. export async function BuildYJBDCQRCode(ctx) {
  992. try {
  993. // 获取微信访问令牌
  994. const tokenUrl = `https://api.weixin.qq.com/cgi-bin/token?appid=${config.wx.yjbdc_appid}&secret=${config.wx.yjbdc_appsecret}&grant_type=client_credential`;
  995. const tokenResponse = await axios.get(tokenUrl);
  996. const tokenData = tokenResponse.data;
  997. if (!tokenData || !tokenData.access_token) {
  998. ctx.status = 400;
  999. ctx.body = { errcode: 101, errStr: '获取微信访问令牌失败' };
  1000. return;
  1001. }
  1002. const accessToken = tokenData.access_token;
  1003. // 生成小程序码
  1004. const qrCodeUrl = `https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${accessToken}`;
  1005. const path = './public/images/acode/';
  1006. const imageUrl = 'YJBDC_QRCode.png';
  1007. const fullPath = path + imageUrl;
  1008. // 确保目录存在
  1009. if (!fs.existsSync(path)) {
  1010. fs.mkdirSync(path, { recursive: true });
  1011. }
  1012. const postData = {
  1013. width: 280,
  1014. scene: "SourceID=187"
  1015. };
  1016. // 使用axios获取二维码并保存到文件
  1017. const qrCodeResponse = await axios({
  1018. method: 'POST',
  1019. url: qrCodeUrl,
  1020. data: postData,
  1021. responseType: 'stream'
  1022. });
  1023. // 创建写入流
  1024. const writer = fs.createWriteStream(fullPath);
  1025. // 将响应数据写入文件
  1026. qrCodeResponse.data.pipe(writer);
  1027. // 返回成功响应
  1028. ctx.body = { errcode: 10000, message: "二维码生成请求已发送" };
  1029. // 处理文件写入完成事件
  1030. writer.on('finish', () => {
  1031. console.log("二维码生成成功:", fullPath);
  1032. });
  1033. // 处理错误
  1034. writer.on('error', (err) => {
  1035. console.error("二维码文件写入失败:", err);
  1036. });
  1037. } catch (error) {
  1038. console.error("生成二维码失败:", error);
  1039. ctx.status = 500;
  1040. ctx.body = {
  1041. errcode: 500,
  1042. errStr: '生成二维码失败',
  1043. error: error.message
  1044. };
  1045. }
  1046. }
  1047. export async function GetWordChinese(ctx) {
  1048. const param = {
  1049. UserID: ctx.query.UserID || 0,
  1050. Word: ctx.query.Word || '',
  1051. Level: ctx.query.Level || '0'
  1052. };
  1053. if (!param.Word) {
  1054. ctx.body = {"errcode": 10001, "errStr": "单词不能为空"};
  1055. return;
  1056. }
  1057. // 使用单词作为缓存键,为每个单词单独缓存结果
  1058. const cacheKey = `GetWordChinese_${param.Word}_${param.Level}`;
  1059. let result = globalCache.get(cacheKey);
  1060. if (!result) {
  1061. // 缓存未命中,从数据库获取
  1062. result = await yjbdc.GetWordChinese(param);
  1063. // 如果没有找到结果,尝试查找单词的原形
  1064. if (!result || result.length === 0) {
  1065. // 使用stringUtils.getWordBaseForm获取可能的原形
  1066. const possibleBaseWords = stringUtils.getWordBaseForm(param.Word);
  1067. // 尝试每个可能的原形
  1068. for (const baseWord of possibleBaseWords) {
  1069. //console.log(`尝试查找单词 ${param.Word} 的可能原形: ${baseWord}`);
  1070. const baseParam = {...param, Word: baseWord};
  1071. const baseResult = await yjbdc.GetWordChinese(baseParam);
  1072. if (baseResult && baseResult.length > 0) {
  1073. //console.log(`找到单词 ${param.Word} 的原形 ${baseWord}`);
  1074. result = baseResult;
  1075. break;
  1076. }
  1077. }
  1078. }
  1079. // 缓存结果,使用较长的过期时间,因为单词释义很少变化
  1080. if (result && result.length > 0) {
  1081. globalCache.set(cacheKey, result, config.BufferMemoryTimeHighBest);
  1082. //console.log(`缓存单词 ${param.Word} 的释义,7天有效期`);
  1083. }
  1084. }
  1085. // 根据Level筛选合适的单词释义
  1086. let selectedResult = null;
  1087. if (result && result.length > 0) {
  1088. // 将Level转换为数字
  1089. const level = parseInt(param.Level);
  1090. // 根据不同Level筛选结果
  1091. if (level >= 0 && level <= 2) {
  1092. // Level 0-2,返回数组单词第一条
  1093. const filtered = result.filter(item => item.BookID >= 151);
  1094. if (filtered.length > 0) {
  1095. selectedResult = filtered[0];
  1096. } else {
  1097. // 如果没有符合条件的,返回BookID较小的第一条
  1098. selectedResult = result[0];
  1099. }
  1100. } else if (level === 3) {
  1101. // Level 3,优选返回BookID >= 169的
  1102. const filtered = result.filter(item => item.BookID >= 169);
  1103. if (filtered.length > 0) {
  1104. selectedResult = filtered[0];
  1105. } else {
  1106. // 如果没有符合条件的,返回BookID较大的第一条
  1107. result.sort((a, b) => b.BookID - a.BookID);
  1108. selectedResult = result[0];
  1109. }
  1110. } else if (level === 4) {
  1111. // Level 4,优选返回BookID >= 173的
  1112. const filtered = result.filter(item => item.BookID >= 173);
  1113. if (filtered.length > 0) {
  1114. selectedResult = filtered[0];
  1115. } else {
  1116. // 如果没有符合条件的,返回BookID较大的第一条
  1117. result.sort((a, b) => b.BookID - a.BookID);
  1118. selectedResult = result[0];
  1119. }
  1120. } else if (level === 5) {
  1121. // Level 5,优选返回BookID >= 178的
  1122. const filtered = result.filter(item => item.BookID >= 178);
  1123. if (filtered.length > 0) {
  1124. selectedResult = filtered[0];
  1125. } else {
  1126. // 如果没有符合条件的,返回BookID较大的第一条
  1127. result.sort((a, b) => b.BookID - a.BookID);
  1128. selectedResult = result[0];
  1129. }
  1130. }
  1131. // 移除 BookID 字段
  1132. if (selectedResult) {
  1133. delete selectedResult.BookID;
  1134. }
  1135. }
  1136. ctx.body = {"errcode": 10000, result: selectedResult};
  1137. }
  1138. export async function YJBDC_Articles_Admin(ctx) {
  1139. //console.log("yjbdc_articles");
  1140. const data = await fsPromises.readFile("./public/mg/yjbdc_articles.html");
  1141. ctx.body = data.toString();
  1142. };