articles.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  1. const express = require('express');
  2. const router = express.Router();
  3. const {Article, Category} = require('../../models')
  4. const {Op} = require('sequelize')
  5. /**
  6. * @api {get} /admin/articles 查询文章列表
  7. * @apiName GetArticles
  8. * @apiGroup Articles
  9. * @apiVersion 1.0.0
  10. *
  11. * @apiDescription 获取文章列表,支持分页和多条件筛选
  12. *
  13. * @apiParam {Number} [currentPage=1] 当前页码,默认为1
  14. * @apiParam {Number} [pageSize=10] 每页显示数量,默认为10
  15. * @apiParam {String} [title] 标题搜索关键词(模糊匹配)
  16. * @apiParam {String|Array} [cropIds] 作物筛选ID,支持逗号分隔的多个ID或数组形式
  17. * @apiParam {Number} [categoryId] 用户分类ID,根据用户传递的category参数值进行精确匹配
  18. * @apiParam {Number} [isRecommended] 推荐筛选,0-非推荐文章,1-推荐文章
  19. * @apiParam {String|Array} [newsTypeIds] 文章类型筛选ID,支持逗号分隔的多个ID或数组形式,根据type字段进行筛选
  20. *
  21. * @apiSuccess {Boolean} status 请求状态
  22. * @apiSuccess {String} message 响应消息
  23. * @apiSuccess {Object} data 响应数据
  24. * @apiSuccess {Array} data.articles 文章列表
  25. * @apiSuccess {Number} data.articles.id 文章ID
  26. * @apiSuccess {String} data.articles.title 文章标题
  27. * @apiSuccess {String} [data.articles.subtitle] 文章副标题
  28. * @apiSuccess {String} data.articles.content 文章内容
  29. * @apiSuccess {Number} data.articles.type 文章类型
  30. * @apiSuccess {String} [data.articles.img] 文章图片
  31. * @apiSuccess {Date} [data.articles.date] 文章日期
  32. * @apiSuccess {String} [data.articles.author] 作者
  33. * @apiSuccess {Number} data.articles.category 用户分类ID
  34. * @apiSuccess {Number} data.articles.crop 作物分类ID
  35. * @apiSuccess {Number} data.articles.isRecommended 是否推荐(0-不推荐,1-推荐)
  36. * @apiSuccess {String} [data.articles.seoKeyword] SEO关键词
  37. * @apiSuccess {String} [data.articles.seoDescription] SEO描述
  38. * @apiSuccess {Date} data.articles.createdAt 创建时间
  39. * @apiSuccess {Date} data.articles.updatedAt 更新时间
  40. * @apiSuccess {Object} [data.articles.cropInfo] 作物信息
  41. * @apiSuccess {Number} data.articles.cropInfo.id 作物ID
  42. * @apiSuccess {String} data.articles.cropInfo.name 作物名称
  43. * @apiSuccess {Number} data.articles.cropInfo.level 作物层级
  44. * @apiSuccess {Number} data.articles.cropInfo.parentId 父级作物ID
  45. * @apiSuccess {Object} data.pagination 分页信息
  46. * @apiSuccess {Number} data.pagination.total 总数量
  47. * @apiSuccess {Number} data.pagination.currentPage 当前页码
  48. * @apiSuccess {Number} data.pagination.pageSize 每页数量
  49. *
  50. * @apiSuccessExample {json} Success-Response:
  51. * HTTP/1.1 200 OK
  52. * {
  53. * "status": true,
  54. * "message": "成功",
  55. * "data": {
  56. * "articles": [
  57. * {
  58. * "id": 96,
  59. * "title": "测试文章",
  60. * "subtitle": "副标题",
  61. * "content": "<p>文章内容</p>",
  62. * "type": 1,
  63. * "img": null,
  64. * "date": null,
  65. * "author": "作者",
  66. * "category": 1,
  67. * "crop": 43,
  68. * "isRecommended": 1,
  69. * "seoKeyword": null,
  70. * "seoDescription": null,
  71. * "createdAt": "2025-09-14T09:21:24.000Z",
  72. * "updatedAt": "2025-09-14T09:21:24.000Z",
  73. * "cropInfo": {
  74. * "id": 43,
  75. * "name": "荔枝",
  76. * "level": 2,
  77. * "parentId": 40
  78. * }
  79. * }
  80. * ],
  81. * "pagination": {
  82. * "total": 19,
  83. * "currentPage": 1,
  84. * "pageSize": 10
  85. * }
  86. * }
  87. * }
  88. *
  89. * @apiErrorExample {json} Error-Response:
  90. * HTTP/1.1 500 Internal Server Error
  91. * {
  92. * "status": false,
  93. * "message": "失败",
  94. * "errors": ["错误信息"]
  95. * }
  96. */
  97. router.get('/', async function(req, res, next) {
  98. try {
  99. const query = req.query
  100. //当前是第几页,如果不传,那就是第一页
  101. const currentPage = Math.abs(Number(query.currentPage)) || 1
  102. //每页显示多少条数据,如果不传,那就显示10条
  103. const pageSize = Math.abs(Number(query.pageSize)) || 10
  104. //计算 offset
  105. const offset = (currentPage - 1) * pageSize
  106. const condition = {
  107. order:[['updatedAt','DESC']],
  108. limit:pageSize,
  109. offset,
  110. attributes: [
  111. 'id', 'title', 'subtitle', 'content', 'type', 'img', 'date',
  112. 'author', 'category', 'crop', 'isRecommended', 'seoKeyword',
  113. 'seoDescription', 'createdAt', 'updatedAt'
  114. ],
  115. include: [{
  116. model: Category,
  117. as: 'cropInfo',
  118. attributes: ['id', 'name', 'level', 'parentId'],
  119. required: false // LEFT JOIN,即使没有作物也能返回文章
  120. }]
  121. }
  122. // 构建查询条件
  123. const whereConditions = {};
  124. // 标题搜索
  125. if(query.title){
  126. whereConditions.title = {
  127. [Op.like]:`%${query.title}%`
  128. }
  129. }
  130. // 作物筛选 - 支持多选和包含子分类
  131. if(query.cropIds){
  132. let cropIds = [];
  133. // 处理cropIds参数(支持逗号分隔的多个ID)
  134. if(typeof query.cropIds === 'string'){
  135. cropIds = query.cropIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
  136. } else if(Array.isArray(query.cropIds)){
  137. cropIds = query.cropIds.map(id => parseInt(id)).filter(id => !isNaN(id));
  138. } else {
  139. cropIds = [parseInt(query.cropIds)].filter(id => !isNaN(id));
  140. }
  141. if(cropIds.length > 0){
  142. // 获取所有选中的分类及其子分类的ID
  143. const allCropIds = await getAllCategoryIdsWithChildren(cropIds);
  144. whereConditions.crop = {
  145. [Op.in]: allCropIds
  146. };
  147. }
  148. }
  149. // 用户分类筛选 - 根据用户传递的category参数值查询
  150. if(query.categoryId){
  151. const categoryId = parseInt(query.categoryId);
  152. if(!isNaN(categoryId)){
  153. whereConditions.category = categoryId;
  154. }
  155. }
  156. // 推荐筛选 - 根据是否推荐进行筛选
  157. if(query.isRecommended !== undefined){
  158. const isRecommended = parseInt(query.isRecommended);
  159. if(!isNaN(isRecommended) && (isRecommended === 0 || isRecommended === 1)){
  160. whereConditions.isRecommended = isRecommended;
  161. }
  162. }
  163. // 文章类型筛选 - 支持多选
  164. if(query.newsTypeIds){
  165. let typeIds = [];
  166. // 处理newsTypeIds参数(支持逗号分隔的多个ID)
  167. if(typeof query.newsTypeIds === 'string'){
  168. typeIds = query.newsTypeIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
  169. } else if(Array.isArray(query.newsTypeIds)){
  170. typeIds = query.newsTypeIds.map(id => parseInt(id)).filter(id => !isNaN(id));
  171. } else {
  172. typeIds = [parseInt(query.newsTypeIds)].filter(id => !isNaN(id));
  173. }
  174. if(typeIds.length > 0){
  175. whereConditions.type = {
  176. [Op.in]: typeIds
  177. };
  178. }
  179. }
  180. // 如果有查询条件,添加到condition中
  181. if(Object.keys(whereConditions).length > 0){
  182. condition.where = whereConditions;
  183. }
  184. const {count ,rows} = await Article.findAndCountAll(condition)
  185. res.json({
  186. status:true,
  187. message:'成功',
  188. data:{
  189. articles:rows,
  190. pagination:{
  191. total:count,
  192. currentPage,
  193. pageSize
  194. }
  195. }
  196. });
  197. }catch(error){
  198. res.status(500).json({
  199. status:false,
  200. message:'失败',
  201. errors:[error.message]
  202. });
  203. }
  204. });
  205. /**
  206. * @api {get} /admin/articles/:id 查询文章详情
  207. * @apiName GetArticleById
  208. * @apiGroup Articles
  209. * @apiVersion 1.0.0
  210. *
  211. * @apiDescription 根据文章ID获取文章详细信息
  212. *
  213. * @apiParam {Number} id 文章ID(路径参数)
  214. *
  215. * @apiSuccess {Boolean} status 请求状态
  216. * @apiSuccess {String} message 响应消息
  217. * @apiSuccess {Object} data 文章详细信息
  218. * @apiSuccess {Number} data.id 文章ID
  219. * @apiSuccess {String} data.title 文章标题
  220. * @apiSuccess {String} [data.subtitle] 文章副标题
  221. * @apiSuccess {String} data.content 文章内容
  222. * @apiSuccess {Number} data.type 文章类型
  223. * @apiSuccess {String} [data.img] 文章图片
  224. * @apiSuccess {Date} [data.date] 文章日期
  225. * @apiSuccess {String} [data.author] 作者
  226. * @apiSuccess {Number} data.category 用户分类ID
  227. * @apiSuccess {Number} data.crop 作物分类ID
  228. * @apiSuccess {Number} data.isRecommended 是否推荐(0-不推荐,1-推荐)
  229. * @apiSuccess {String} [data.seoKeyword] SEO关键词
  230. * @apiSuccess {String} [data.seoDescription] SEO描述
  231. * @apiSuccess {Date} data.createdAt 创建时间
  232. * @apiSuccess {Date} data.updatedAt 更新时间
  233. * @apiSuccess {Object} [data.cropInfo] 作物信息
  234. * @apiSuccess {Number} data.cropInfo.id 作物ID
  235. * @apiSuccess {String} data.cropInfo.name 作物名称
  236. * @apiSuccess {Number} data.cropInfo.level 作物层级
  237. * @apiSuccess {Number} data.cropInfo.parentId 父级作物ID
  238. *
  239. * @apiSuccessExample {json} Success-Response:
  240. * HTTP/1.1 200 OK
  241. * {
  242. * "status": true,
  243. * "message": "成功",
  244. * "data": {
  245. * "id": 96,
  246. * "title": "测试文章",
  247. * "subtitle": "副标题",
  248. * "content": "<p>文章内容</p>",
  249. * "type": 1,
  250. * "img": null,
  251. * "date": null,
  252. * "author": "作者",
  253. * "category": 1,
  254. * "crop": 43,
  255. * "isRecommended": 1,
  256. * "seoKeyword": null,
  257. * "seoDescription": null,
  258. * "createdAt": "2025-09-14T09:21:24.000Z",
  259. * "updatedAt": "2025-09-14T09:21:24.000Z",
  260. * "cropInfo": {
  261. * "id": 43,
  262. * "name": "荔枝",
  263. * "level": 2,
  264. * "parentId": 40
  265. * }
  266. * }
  267. * }
  268. *
  269. * @apiErrorExample {json} Error-Response:
  270. * HTTP/1.1 404 Not Found
  271. * {
  272. * "status": false,
  273. * "message": "文章未找到"
  274. * }
  275. */
  276. router.get('/:id', async function(req, res, next) {
  277. try {
  278. //获取文章id
  279. const {id} = req.params
  280. //查询文章
  281. const article = await Article.findByPk(id, {
  282. include: [{
  283. model: Category,
  284. as: 'cropInfo',
  285. attributes: ['id', 'name', 'level', 'parentId'],
  286. required: false
  287. }]
  288. })
  289. if(article){
  290. res.json({
  291. status:true,
  292. message:'成功',
  293. data:article
  294. });
  295. }else{
  296. res.status(404).json({
  297. status:false,
  298. message:'文章未找到',
  299. });
  300. }
  301. }catch(error){
  302. res.status(500).json({
  303. status:false,
  304. message:'失败',
  305. errors:[error.message]
  306. });
  307. }
  308. });
  309. /**
  310. * @api {post} /admin/articles 创建文章
  311. * @apiName CreateArticle
  312. * @apiGroup Articles
  313. * @apiVersion 1.0.0
  314. *
  315. * @apiDescription 创建新的文章,支持富文本内容和图片
  316. *
  317. * @apiParam {String} title 文章标题(必填,1-500字符)
  318. * @apiParam {String} content 文章内容(必填,富文本格式,最大5MB)
  319. * @apiParam {Number} [type] 文章类型
  320. * @apiParam {String} [img] 文章图片URL
  321. * @apiParam {Date} [date] 文章发布日期
  322. * @apiParam {String} [author] 作者
  323. * @apiParam {Number} [category] 用户分类ID(用户传递的参数)
  324. * @apiParam {Number} [crop] 作物分类ID
  325. * @apiParam {Number} [isRecommended=0] 是否推荐,0-不推荐,1-推荐
  326. * @apiParam {String} [subtitle] 副标题(最大200字符)
  327. * @apiParam {String} [seoKeyword] SEO关键词
  328. * @apiParam {String} [seoDescription] SEO描述
  329. *
  330. * @apiSuccess {Boolean} status 请求状态
  331. * @apiSuccess {String} message 响应消息
  332. * @apiSuccess {Object} data 创建的文章信息
  333. * @apiSuccess {Number} data.id 文章ID
  334. * @apiSuccess {String} data.title 文章标题
  335. * @apiSuccess {String} [data.subtitle] 文章副标题
  336. * @apiSuccess {String} data.content 文章内容
  337. * @apiSuccess {Number} data.type 文章类型
  338. * @apiSuccess {String} [data.img] 文章图片
  339. * @apiSuccess {Date} [data.date] 文章日期
  340. * @apiSuccess {String} [data.author] 作者
  341. * @apiSuccess {Number} data.category 用户分类ID
  342. * @apiSuccess {Number} data.crop 作物分类ID
  343. * @apiSuccess {Number} data.isRecommended 是否推荐
  344. * @apiSuccess {String} [data.seoKeyword] SEO关键词
  345. * @apiSuccess {String} [data.seoDescription] SEO描述
  346. * @apiSuccess {Date} data.createdAt 创建时间
  347. * @apiSuccess {Date} data.updatedAt 更新时间
  348. *
  349. * @apiSuccessExample {json} Success-Response:
  350. * HTTP/1.1 201 Created
  351. * {
  352. * "status": true,
  353. * "message": "成功",
  354. * "data": {
  355. * "id": 97,
  356. * "title": "新文章标题",
  357. * "subtitle": "副标题",
  358. * "content": "<p>文章内容</p>",
  359. * "type": 1,
  360. * "img": null,
  361. * "date": null,
  362. * "author": "作者",
  363. * "category": 1,
  364. * "crop": 43,
  365. * "isRecommended": 1,
  366. * "seoKeyword": null,
  367. * "seoDescription": null,
  368. * "createdAt": "2025-09-14T09:21:49.333Z",
  369. * "updatedAt": "2025-09-14T09:21:49.333Z"
  370. * }
  371. * }
  372. *
  373. * @apiErrorExample {json} Error-Response:
  374. * HTTP/1.1 400 Bad Request
  375. * {
  376. * "status": false,
  377. * "message": "请求参数错误",
  378. * "errors": ["标题不能为空"]
  379. * }
  380. *
  381. * @apiErrorExample {json} Error-Response:
  382. * HTTP/1.1 400 Bad Request
  383. * {
  384. * "status": false,
  385. * "message": "请求参数错误",
  386. * "errors": ["推荐字段只能是0或1"]
  387. * }
  388. *
  389. * @apiErrorExample {json} Error-Response:
  390. * HTTP/1.1 400 Bad Request
  391. * {
  392. * "status": false,
  393. * "message": "请求参数错误",
  394. * "errors": ["副标题长度不能超过200个字符"]
  395. * }
  396. */
  397. router.post('/', async function(req, res, next) {
  398. try {
  399. // 添加请求日志
  400. console.log('=== 创建文章请求开始 ===');
  401. console.log('请求体大小:', JSON.stringify(req.body).length);
  402. console.log('Content字段长度:', req.body.content ? req.body.content.length : 0);
  403. console.log('Title字段长度:', req.body.title ? req.body.title.length : 0);
  404. //白名单过滤
  405. const body = filterBody(req)
  406. console.log('过滤后的数据:', {
  407. titleLength: body.title ? body.title.length : 0,
  408. contentLength: body.content ? body.content.length : 0,
  409. hasImage: !!body.img,
  410. type: body.type
  411. });
  412. const article = await Article.create(body)
  413. console.log('文章创建成功, ID:', article.id);
  414. console.log('=== 创建文章请求结束 ===');
  415. res.status(201).json({
  416. status:true,
  417. message:'成功',
  418. data:article
  419. });
  420. }catch(error){
  421. // 添加详细的错误日志
  422. console.error('=== 创建文章错误 ===');
  423. console.error('错误名称:', error.name);
  424. console.error('错误消息:', error.message);
  425. console.error('错误堆栈:', error.stack);
  426. console.error('请求体大小:', JSON.stringify(req.body).length);
  427. console.error('请求体:', JSON.stringify(req.body, null, 2));
  428. if(error.message === '标题不能为空' || error.message === '内容不能为空' ||
  429. error.message.includes('长度不能超过') || error.message.includes('不允许的脚本标签') ||
  430. error.message.includes('推荐字段只能是0或1') || error.message.includes('副标题长度不能超过')){
  431. res.status(400).json({
  432. status:false,
  433. message:'请求参数错误',
  434. errors:[error.message]
  435. });
  436. }else if(error.name === 'SequelizeValidationError'){
  437. const errors = error.errors.map(e => e.message)
  438. res.status(400).json({
  439. status:false,
  440. message:'数据验证失败',
  441. errors
  442. });
  443. }else if(error.name === 'SequelizeDatabaseError'){
  444. console.error('数据库错误详情:', error.original);
  445. res.status(500).json({
  446. status:false,
  447. message:'数据库错误',
  448. errors:['数据库操作失败,请稍后重试']
  449. });
  450. }else if(error.name === 'SequelizeConnectionError'){
  451. res.status(500).json({
  452. status:false,
  453. message:'数据库连接错误',
  454. errors:['数据库连接失败,请稍后重试']
  455. });
  456. }else{
  457. res.status(500).json({
  458. status:false,
  459. message:'服务器内部错误',
  460. errors:['服务器处理请求时发生错误,请稍后重试']
  461. });
  462. }
  463. }
  464. });
  465. /**
  466. * @api {delete} /admin/articles/:id 删除文章
  467. * @apiName DeleteArticle
  468. * @apiGroup Articles
  469. * @apiVersion 1.0.0
  470. *
  471. * @apiDescription 根据文章ID删除文章
  472. *
  473. * @apiParam {Number} id 文章ID(路径参数)
  474. *
  475. * @apiSuccess {Boolean} status 请求状态
  476. * @apiSuccess {String} message 响应消息
  477. *
  478. * @apiSuccessExample {json} Success-Response:
  479. * HTTP/1.1 200 OK
  480. * {
  481. * "status": true,
  482. * "message": "成功"
  483. * }
  484. *
  485. * @apiErrorExample {json} Error-Response:
  486. * HTTP/1.1 404 Not Found
  487. * {
  488. * "status": false,
  489. * "message": "文章未找到"
  490. * }
  491. */
  492. router.delete('/:id', async function(req, res, next) {
  493. try {
  494. //获取文章id
  495. const {id} = req.params
  496. //查询文章
  497. const article = await Article.findByPk(id)
  498. if(article){
  499. await article.destroy()
  500. res.json({
  501. status:true,
  502. message:'成功',
  503. });
  504. }else{
  505. res.status(404).json({
  506. status:false,
  507. message:'文章未找到',
  508. });
  509. }
  510. }catch(error){
  511. res.status(500).json({
  512. status:false,
  513. message:'失败',
  514. errors:[error.message]
  515. });
  516. }
  517. });
  518. /**
  519. * @api {put} /admin/articles/:id 更新文章
  520. * @apiName UpdateArticle
  521. * @apiGroup Articles
  522. * @apiVersion 1.0.0
  523. *
  524. * @apiDescription 根据文章ID更新文章信息
  525. *
  526. * @apiParam {Number} id 文章ID(路径参数)
  527. * @apiParam {String} [title] 文章标题(1-500字符)
  528. * @apiParam {String} [content] 文章内容(富文本格式,最大5MB)
  529. * @apiParam {Number} [type] 文章类型
  530. * @apiParam {String} [img] 文章图片URL
  531. * @apiParam {Date} [date] 文章发布日期
  532. * @apiParam {String} [author] 作者
  533. * @apiParam {Number} [category] 用户分类ID
  534. * @apiParam {Number} [crop] 作物分类ID
  535. * @apiParam {Number} [isRecommended] 是否推荐,0-不推荐,1-推荐
  536. * @apiParam {String} [subtitle] 副标题(最大200字符)
  537. * @apiParam {String} [seoKeyword] SEO关键词
  538. * @apiParam {String} [seoDescription] SEO描述
  539. *
  540. * @apiSuccess {Boolean} status 请求状态
  541. * @apiSuccess {String} message 响应消息
  542. * @apiSuccess {Object} data 更新后的文章信息
  543. *
  544. * @apiSuccessExample {json} Success-Response:
  545. * HTTP/1.1 200 OK
  546. * {
  547. * "status": true,
  548. * "message": "成功",
  549. * "data": {
  550. * "id": 97,
  551. * "title": "更新后的标题",
  552. * "subtitle": "更新后的副标题",
  553. * "content": "<p>更新后的内容</p>",
  554. * "type": 1,
  555. * "img": null,
  556. * "date": null,
  557. * "author": "作者",
  558. * "category": 1,
  559. * "crop": 43,
  560. * "isRecommended": 1,
  561. * "seoKeyword": null,
  562. * "seoDescription": null,
  563. * "createdAt": "2025-09-14T09:21:49.333Z",
  564. * "updatedAt": "2025-09-14T09:22:10.000Z"
  565. * }
  566. * }
  567. *
  568. * @apiErrorExample {json} Error-Response:
  569. * HTTP/1.1 404 Not Found
  570. * {
  571. * "status": false,
  572. * "message": "文章未找到"
  573. * }
  574. *
  575. * @apiErrorExample {json} Error-Response:
  576. * HTTP/1.1 400 Bad Request
  577. * {
  578. * "status": false,
  579. * "message": "请求参数错误",
  580. * "errors": ["推荐字段只能是0或1"]
  581. * }
  582. */
  583. router.put('/:id', async function(req, res, next) {
  584. try {
  585. //获取文章id
  586. const {id} = req.params
  587. //查询文章
  588. const article = await Article.findByPk(id)
  589. //白名单过滤
  590. const body = filterBody(req)
  591. if(article){
  592. await article.update(body)
  593. res.json({
  594. status:true,
  595. message:'成功',
  596. data:article
  597. });
  598. }else{
  599. res.status(404).json({
  600. status:false,
  601. message:'文章未找到',
  602. });
  603. }
  604. }catch(error){
  605. res.status(500).json({
  606. status:false,
  607. message:'失败',
  608. errors:[error.message]
  609. });
  610. }
  611. });
  612. /**
  613. * 获取分类及其所有子分类的ID列表
  614. * @param {Array} categoryIds - 分类ID数组
  615. * @returns {Promise<Array>} 包含所有分类ID的数组
  616. */
  617. async function getAllCategoryIdsWithChildren(categoryIds) {
  618. try {
  619. let allIds = [...categoryIds];
  620. // 递归获取所有子分类
  621. async function getChildrenIds(parentIds) {
  622. if (parentIds.length === 0) return [];
  623. const children = await Category.findAll({
  624. where: {
  625. parentId: {
  626. [Op.in]: parentIds
  627. }
  628. },
  629. attributes: ['id']
  630. });
  631. const childrenIds = children.map(child => child.id);
  632. if (childrenIds.length > 0) {
  633. allIds = allIds.concat(childrenIds);
  634. // 递归获取子分类的子分类
  635. const grandChildrenIds = await getChildrenIds(childrenIds);
  636. allIds = allIds.concat(grandChildrenIds);
  637. }
  638. return childrenIds;
  639. }
  640. await getChildrenIds(categoryIds);
  641. // 去重并返回
  642. return [...new Set(allIds)];
  643. } catch (error) {
  644. console.error('获取分类ID列表错误:', error);
  645. // 如果出错,返回原始ID列表
  646. return categoryIds;
  647. }
  648. }
  649. function filterBody(req){
  650. try {
  651. // 数据清理和验证
  652. const body = {
  653. title: req.body.title ? String(req.body.title).trim() : null,
  654. content: req.body.content ? String(req.body.content) : null,
  655. type: req.body.type !== undefined ? parseInt(req.body.type) : null,
  656. img: req.body.img ? String(req.body.img).trim() : null,
  657. date: req.body.date ? new Date(req.body.date) : null,
  658. author: req.body.author ? String(req.body.author).trim() : null,
  659. category: req.body.category !== undefined ? parseInt(req.body.category) : null,
  660. crop: req.body.crop !== undefined ? parseInt(req.body.crop) : null,
  661. isRecommended: req.body.isRecommended !== undefined ? parseInt(req.body.isRecommended) : 0,
  662. subtitle: req.body.subtitle ? String(req.body.subtitle).trim() : null,
  663. seoKeyword: req.body.seoKeyword ? String(req.body.seoKeyword).trim() : null,
  664. seoDescription: req.body.seoDescription ? String(req.body.seoDescription).trim() : null
  665. };
  666. // 验证必填字段
  667. if (!body.title) {
  668. throw new Error('标题不能为空');
  669. }
  670. if (!body.content) {
  671. throw new Error('内容不能为空');
  672. }
  673. // 验证标题长度 - 放宽限制以适应富文本编辑器
  674. if (body.title.length > 500) {
  675. throw new Error('标题长度不能超过500个字符');
  676. }
  677. // 验证内容长度 - 防止过大的内容
  678. if (body.content.length > 5000000) { // 5MB限制
  679. throw new Error('内容过长,请减少内容长度');
  680. }
  681. // 验证推荐字段 - 只能是0或1
  682. if (body.isRecommended !== 0 && body.isRecommended !== 1) {
  683. throw new Error('推荐字段只能是0或1');
  684. }
  685. // 验证副标题长度
  686. if (body.subtitle && body.subtitle.length > 200) {
  687. throw new Error('副标题长度不能超过200个字符');
  688. }
  689. // 检查富文本内容是否包含危险标签或脚本
  690. const dangerousTags = /<script[^>]*>.*?<\/script>/gi;
  691. if (dangerousTags.test(body.content)) {
  692. throw new Error('内容包含不允许的脚本标签');
  693. }
  694. return body;
  695. } catch (error) {
  696. console.error('filterBody错误:', error);
  697. throw error;
  698. }
  699. }
  700. module.exports = router;