commonController.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import moment from 'moment';
  2. import fs from 'fs';
  3. import { promises as fsPromises } from 'fs';
  4. import config from '../../config/index.js';
  5. import _ from 'lodash';
  6. import path from 'path';
  7. import gm from 'gm';
  8. import axios from 'axios';
  9. import COS from 'cos-nodejs-sdk-v5';
  10. import { uploadSingle } from '../../middleware/upload.js';
  11. import { globalState } from '../../util/globalState.js';
  12. const imageMagick = gm.subClass({ imageMagick: true });
  13. // 允许的文件类型
  14. const ALLOWED_IMAGE_TYPES = new Set([
  15. 'jpg', 'jpeg', 'png', 'bmp', 'heic', 'heif', 'webp', 'gif'
  16. ]);
  17. const ALLOWED_AUDIO_TYPES = new Set([
  18. 'aac', 'mp3', 'mp4', 'm4a', 'flac', 'ogg', 'ape', 'amr',
  19. 'wma', 'wav', 'aiff', 'caf'
  20. ]);
  21. const BLOCKED_TYPES = new Set(['php', 'js', 'txt']);
  22. // 文件上传配置
  23. const fileFilter = (req, file, cb) => {
  24. const extension = path.extname(file.originalname).toLowerCase().slice(1);
  25. if (BLOCKED_TYPES.has(extension)) {
  26. cb(new Error('不允许上传该类型的文件!'), false);
  27. } else if (ALLOWED_IMAGE_TYPES.has(extension) || ALLOWED_AUDIO_TYPES.has(extension)) {
  28. cb(null, true);
  29. } else {
  30. cb(new Error('文件格式不支持!'), false);
  31. }
  32. };
  33. // 配置上传中间件
  34. export const uploadMiddleware = uploadSingle('file', {
  35. dest: './public/uploads/',
  36. fileFilter,
  37. limits: {
  38. fileSize: 10 * 1024 * 1024 // 10MB
  39. }
  40. });
  41. // 小程序文件上传控制器
  42. export const UploadFile = async (ctx) => {
  43. try {
  44. const file = ctx.request.file;
  45. if (!file) {
  46. ctx.body = { errcode: 1002, errMsg: "文件上传错误!" };
  47. return;
  48. }
  49. const extension = path.extname(file.originalname).toLowerCase().slice(1);
  50. const filename = file.filename;
  51. const filepath = file.path;
  52. if (ALLOWED_IMAGE_TYPES.has(extension)) {
  53. await checkImage(filename, filepath);
  54. } else if (ALLOWED_AUDIO_TYPES.has(extension)) {
  55. await uploadFile(filename, filepath);
  56. }
  57. ctx.body = {
  58. errcode: 10000,
  59. result: {
  60. Source: file.originalname,
  61. Target: filename
  62. }
  63. };
  64. } catch (error) {
  65. console.error('Upload error:', error);
  66. ctx.body = { errcode: 1001, errMsg: error.message || "上传失败!" };
  67. // 清理临时文件
  68. if (ctx.request.file && ctx.request.file.path) {
  69. try {
  70. await fsPromises.unlink(ctx.request.file.path);
  71. } catch (e) {
  72. console.error('Failed to clean up temp file:', e);
  73. }
  74. }
  75. }
  76. // 安全清理定时器
  77. setTimeout(async () => {
  78. try {
  79. const filepath = './public/uploads/';
  80. const files = await fsPromises.readdir(filepath);
  81. for (const file of files) {
  82. const extension = path.extname(file).toLowerCase();
  83. if (BLOCKED_TYPES.has(extension.slice(1))) {
  84. await fsPromises.unlink(path.join(filepath, file));
  85. }
  86. }
  87. } catch (error) {
  88. console.error('Security cleanup error:', error);
  89. }
  90. }, 5000);
  91. };
  92. // 检查并处理图片
  93. async function checkImage(filename, filepath) {
  94. if (!await fsPromises.stat(filepath)) {
  95. throw new Error('文件不存在');
  96. }
  97. const stats = await fsPromises.stat(filepath);
  98. if (stats.size >= 126976) {
  99. const maxSize = 750;
  100. return new Promise((resolve, reject) => {
  101. imageMagick(filepath).size((err, size) => {
  102. if (err) {
  103. reject(err);
  104. return;
  105. }
  106. if (size.width > maxSize || size.height > maxSize) {
  107. imageMagick(filepath)
  108. .resize(maxSize, maxSize)
  109. .write(filepath, async () => {
  110. try {
  111. await uploadImage(filename, filepath);
  112. resolve();
  113. } catch (error) {
  114. reject(error);
  115. }
  116. });
  117. } else {
  118. uploadImage(filename, filepath).then(resolve).catch(reject);
  119. }
  120. });
  121. });
  122. } else {
  123. await uploadImage(filename, filepath);
  124. }
  125. }
  126. // 上传图片到腾讯云
  127. async function uploadImage(filename, filepath) {
  128. const cos = new COS(config.QCloud);
  129. const params = {
  130. Bucket: 'miaguo-1253256735',
  131. Region: 'ap-guangzhou',
  132. Key: filename,
  133. Body: fs.createReadStream(filepath),
  134. ContentLength: fs.statSync(filepath).size
  135. };
  136. return new Promise((resolve, reject) => {
  137. cos.putObject(params, (err, data) => {
  138. if (err) {
  139. reject(err);
  140. } else {
  141. fsPromises.unlink(filepath)
  142. .then(() => resolve(data))
  143. .catch(reject);
  144. }
  145. });
  146. });
  147. }
  148. // 上传文件到腾讯云
  149. async function uploadFile(filename, filepath) {
  150. const cos = new COS(config.QCloud);
  151. const params = {
  152. Bucket: 'miaguo-1253256735',
  153. Region: 'ap-guangzhou',
  154. Key: filename,
  155. Body: fs.createReadStream(filepath),
  156. ContentLength: fs.statSync(filepath).size
  157. };
  158. return new Promise((resolve, reject) => {
  159. cos.putObject(params, (err, data) => {
  160. if (err) {
  161. reject(err);
  162. } else {
  163. fsPromises.unlink(filepath)
  164. .then(() => resolve(data))
  165. .catch(reject);
  166. }
  167. });
  168. });
  169. }
  170. export async function GetBaiduToken (ctx) {
  171. const { Code, ProgramID } = ctx.query;
  172. let appid = '', secret = '';
  173. switch (ProgramID) {
  174. case '99':
  175. appid = config.wx.phonics_appid;
  176. secret = config.wx.phonics_appsecret;
  177. break;
  178. case '166':
  179. appid = config.wx.miaoguo_appid;
  180. secret = config.wx.miaoguo_appsecret;
  181. break;
  182. case '105':
  183. appid = config.wx.math_appid;
  184. secret = config.wx.math_appsecret;
  185. break;
  186. case '164':
  187. appid = config.wx.mathStar_appid;
  188. secret = config.wx.mathStar_appsecret;
  189. break;
  190. }
  191. const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appid}&secret=${secret}&js_code=${Code}&grant_type=authorization_code`;
  192. const resultLogin = await axios.get(url)
  193. .then(response => {
  194. const json = response.data;
  195. return json.openid ? { errcode: 10000 } : { errcode: 102 };
  196. })
  197. .catch(err => ({ errcode: 101, errStr: err }));
  198. let result = 0;
  199. if (resultLogin.errcode === 10000) {
  200. result = globalState.getBufferMemory('BaiduToken');
  201. if (result === 0) {
  202. const baiduUrl = 'https://openapi.baidu.com/oauth/2.0/token?grant_type=client_credentials&client_id=9r1Idx1mgMONvElxU3KGE5Gi&client_secret=f4f606f1a5c0b4eaf800e1e046802d81';
  203. result = await axios.get(baiduUrl)
  204. .then(response => {
  205. const json = response.data;
  206. if (json.access_token) {
  207. globalState.SetBufferMemory('BaiduToken', json.access_token, config.BufferMemoryTimeHigh);
  208. return json.access_token;
  209. }
  210. return 0;
  211. });
  212. }
  213. }
  214. ctx.body = { errcode: 10000, result };
  215. }