GeneraterAction.java 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. /**
  2. * The MIT License (MIT)
  3. * Copyright (c) 2012-present 铭软科技(mingsoft.net)
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy of
  5. * this software and associated documentation files (the "Software"), to deal in
  6. * the Software without restriction, including without limitation the rights to
  7. * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  8. * the Software, and to permit persons to whom the Software is furnished to do so,
  9. * subject to the following conditions:
  10. * The above copyright notice and this permission notice shall be included in all
  11. * copies or substantial portions of the Software.
  12. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  13. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  14. * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  15. * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  16. * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  17. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  18. */
  19. package net.mingsoft.cms.action;
  20. import cn.hutool.core.bean.BeanUtil;
  21. import cn.hutool.core.bean.copier.CopyOptions;
  22. import cn.hutool.core.collection.CollUtil;
  23. import cn.hutool.core.date.DateException;
  24. import cn.hutool.core.date.DateUtil;
  25. import cn.hutool.core.io.FileUtil;
  26. import cn.hutool.core.util.StrUtil;
  27. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  28. import io.swagger.annotations.Api;
  29. import io.swagger.annotations.ApiImplicitParam;
  30. import io.swagger.annotations.ApiImplicitParams;
  31. import io.swagger.annotations.ApiOperation;
  32. import net.mingsoft.base.entity.ResultData;
  33. import net.mingsoft.basic.annotation.LogAnn;
  34. import net.mingsoft.basic.bean.EUListBean;
  35. import net.mingsoft.basic.constant.e.BusinessTypeEnum;
  36. import net.mingsoft.basic.entity.AppEntity;
  37. import net.mingsoft.basic.util.BasicUtil;
  38. import net.mingsoft.cms.bean.CategoryBean;
  39. import net.mingsoft.cms.bean.ContentBean;
  40. import net.mingsoft.cms.biz.ICategoryBiz;
  41. import net.mingsoft.cms.biz.IContentBiz;
  42. import net.mingsoft.cms.constant.e.CategoryDisplayEnum;
  43. import net.mingsoft.cms.constant.e.CategoryTypeEnum;
  44. import net.mingsoft.cms.entity.CategoryEntity;
  45. import net.mingsoft.cms.util.CmsParserUtil;
  46. import net.mingsoft.mdiy.util.ParserUtil;
  47. import org.apache.commons.lang3.StringUtils;
  48. import org.apache.shiro.authz.annotation.RequiresPermissions;
  49. import org.slf4j.Logger;
  50. import org.slf4j.LoggerFactory;
  51. import org.springframework.beans.factory.annotation.Autowired;
  52. import org.springframework.beans.factory.annotation.Value;
  53. import org.springframework.context.annotation.Scope;
  54. import org.springframework.stereotype.Controller;
  55. import org.springframework.ui.ModelMap;
  56. import org.springframework.web.bind.annotation.*;
  57. import springfox.documentation.annotations.ApiIgnore;
  58. import javax.servlet.http.HttpServletRequest;
  59. import javax.servlet.http.HttpServletResponse;
  60. import java.io.File;
  61. import java.io.IOException;
  62. import java.util.ArrayList;
  63. import java.util.Date;
  64. import java.util.List;
  65. import java.util.stream.Collectors;
  66. /**
  67. * @ClassName: GeneraterAction
  68. * @Description:TODO 生成器
  69. * @author: 铭飞开发团队
  70. * @date: 2018年1月31日 下午2:52:07
  71. * @Copyright: 2018 www.mingsoft.net Inc. All rights reserved.
  72. */
  73. @Api(tags={"后端-静态化"})
  74. @Controller("cmsGenerater")
  75. @RequestMapping("/${ms.manager.path}/cms/generate")
  76. @Scope("request")
  77. public class GeneraterAction extends BaseAction {
  78. /*
  79. * log4j日志记录
  80. */
  81. protected final Logger LOG = LoggerFactory.getLogger(this.getClass());
  82. /**
  83. * 文章管理业务层
  84. */
  85. @Autowired
  86. private IContentBiz contentBiz;
  87. /**
  88. * 栏目管理业务层
  89. */
  90. @Autowired
  91. private ICategoryBiz categoryBiz;
  92. @Value("${ms.diy.html-dir:html}")
  93. private String htmlDir;
  94. /**
  95. * /**
  96. * 更新主页
  97. *
  98. * @return
  99. */
  100. @GetMapping("/index")
  101. public String index(HttpServletRequest request, ModelMap model) {
  102. return "/cms/generate/index";
  103. }
  104. /**
  105. * 获取所有栏目数据
  106. * 重写接口说明:
  107. * 处理静态化生成页无法获取所有分类信息问题
  108. * 目前栏目作为一个公共数据,方便以后拓展其他业务
  109. * @param category 分类实体
  110. */
  111. @ApiOperation(value = "查询分类列表接口")
  112. @ApiImplicitParams({
  113. @ApiImplicitParam(name = "categoryTitle", value = "栏目管理名称", required = false, paramType = "query"),
  114. @ApiImplicitParam(name = "categoryParentId", value = "父类型编号", required = false, paramType = "query"),
  115. })
  116. @RequestMapping(value = "/list", method = {RequestMethod.GET, RequestMethod.POST})
  117. @ResponseBody
  118. public ResultData list(@ModelAttribute @ApiIgnore CategoryEntity category) {
  119. List categoryList = categoryBiz.list(new LambdaQueryWrapper<CategoryEntity>(category));
  120. return ResultData.build().success(new EUListBean(categoryList, categoryList.size()));
  121. }
  122. /**
  123. * 生成主页
  124. *
  125. * @param request
  126. * @param response
  127. */
  128. @ApiOperation(value = "生成主页接口")
  129. @RequestMapping(value = "/generateIndex", method = {RequestMethod.GET, RequestMethod.POST})
  130. @RequiresPermissions("cms:generate:index")
  131. @LogAnn(title = "生成主页", businessType = BusinessTypeEnum.UPDATE)
  132. @ResponseBody
  133. public ResultData generateIndex(HttpServletRequest request, HttpServletResponse response) throws IOException {
  134. // 模板文件名称
  135. String tmpFileName = request.getParameter("url");
  136. // 生成后的文件名称
  137. String generateFileName = request.getParameter("position");
  138. // 防止篡改主页
  139. if (tmpFileName.contains("..") || tmpFileName.contains("../") || tmpFileName.contains("\\..")){
  140. return ResultData.build().error(getResString("template.file"));
  141. }
  142. if (generateFileName.contains("..") || generateFileName.contains("../") || generateFileName.contains("\\..")){
  143. return ResultData.build().error(getResString("template.file"));
  144. }
  145. // 获取文件所在路径 首先判断用户输入的模板文件是否存在
  146. if (!FileUtil.exist(ParserUtil.buildTemplatePath())) {
  147. return ResultData.build().error(getResString("template.file"));
  148. } else {
  149. CmsParserUtil.generate(tmpFileName, generateFileName, htmlDir);
  150. return ResultData.build().success();
  151. }
  152. }
  153. /**
  154. * 生成列表的静态页面
  155. *
  156. * @param request
  157. * @param response
  158. * @param categoryId
  159. */
  160. @ApiOperation(value = "生成栏目接口")
  161. @RequestMapping(value = "/{categoryId}/generateColumn", method = {RequestMethod.GET, RequestMethod.POST})
  162. @LogAnn(title = "生成栏目", businessType = BusinessTypeEnum.UPDATE)
  163. @RequiresPermissions("cms:generate:column")
  164. @ResponseBody
  165. public ResultData generateColumn(HttpServletRequest request, HttpServletResponse response, @PathVariable String categoryId) throws IOException {
  166. // 获取站点id
  167. AppEntity app = BasicUtil.getApp();
  168. //栏目列表
  169. List<CategoryEntity> columns = new ArrayList<CategoryEntity>();
  170. if ("0".equals(categoryId)) {// 0更新所有栏目
  171. CategoryEntity categoryEntity = new CategoryEntity();
  172. columns = categoryBiz.query(categoryEntity);
  173. } else { //选择栏目更新
  174. CategoryEntity categoryEntity = new CategoryEntity();
  175. categoryEntity.setId(categoryId);
  176. columns = categoryBiz.queryChildren(categoryEntity);
  177. }
  178. //文章列表
  179. List<CategoryBean> articleIdList = null;
  180. // 获取栏目列表模板
  181. for (CategoryEntity column : columns) {
  182. // 如果栏目被禁用则跳过
  183. if (CategoryDisplayEnum.DISABLE.toString().equalsIgnoreCase(column.getCategoryDisplay())){
  184. continue;
  185. }
  186. //如果是链接就跳过生成
  187. if (column.getCategoryType().equals(CategoryTypeEnum.LINK.toString())) {
  188. continue;
  189. }
  190. ContentBean contentBean = new ContentBean();
  191. contentBean.setCategoryId(column.getId());
  192. contentBean.setCategoryType(column.getCategoryType());
  193. articleIdList = contentBiz.queryIdsByCategoryIdForParser(contentBean);
  194. // 判断列表类型
  195. switch (CategoryTypeEnum.get(column.getCategoryType())) {
  196. //TODO 暂时先用字符串代替
  197. case LIST: // 列表
  198. // 判断模板文件是否存在
  199. if (StringUtils.isEmpty(column.getCategoryListUrl()) || !FileUtil.exist(ParserUtil.buildTemplatePath(column.getCategoryListUrl()))) {
  200. LOG.error("{} 模板不存在:{}", column.getCategoryTitle(), column.getCategoryUrl());
  201. continue;
  202. }
  203. CmsParserUtil.generateList(column, articleIdList.size(), htmlDir);
  204. break;
  205. case COVER:// 单页
  206. // 判断模板文件是否存在
  207. if (StringUtils.isEmpty(column.getCategoryUrl()) || !FileUtil.exist(ParserUtil.buildTemplatePath(column.getCategoryUrl()))) {
  208. LOG.error("{} 模板不存在:{}", column.getCategoryTitle(), column.getCategoryUrl());
  209. continue;
  210. }
  211. if (articleIdList.size() == 0) {
  212. CategoryBean columnArticleIdBean = new CategoryBean();
  213. CopyOptions copyOptions = CopyOptions.create();
  214. copyOptions.setIgnoreError(true);
  215. BeanUtil.copyProperties(column, columnArticleIdBean, copyOptions);
  216. articleIdList.add(columnArticleIdBean);
  217. }
  218. CmsParserUtil.generateBasic(articleIdList, htmlDir,null);
  219. break;
  220. default:
  221. // 为了方面拓展其他静态化栏目,都默认走这个业务
  222. // 判断模板文件是否存在
  223. if (StringUtils.isEmpty(column.getCategoryUrl()) || !FileUtil.exist(ParserUtil.buildTemplatePath(column.getCategoryUrl()))) {
  224. LOG.error("{} 模板不存在:{}", column.getCategoryTitle(), column.getCategoryUrl());
  225. continue;
  226. }
  227. // TODO: 2024/11/26 目前只根据内容模版生成静态文件
  228. CmsParserUtil.generate(column.getCategoryUrl(), column.getCategoryPinyin(), htmlDir);
  229. break;
  230. }
  231. }
  232. return ResultData.build().success();
  233. }
  234. /**
  235. * 根据栏目id更新所有的文章
  236. *
  237. * @param request
  238. * @param response
  239. * @param columnId
  240. */
  241. @ApiOperation(value = "生成文章接口")
  242. @RequestMapping(value = "/{columnId}/generateArticle", method = {RequestMethod.GET, RequestMethod.POST})
  243. @RequiresPermissions("cms:generate:article")
  244. @LogAnn(title = "生成文章", businessType = BusinessTypeEnum.UPDATE)
  245. @ResponseBody
  246. public ResultData generateArticle(HttpServletRequest request, HttpServletResponse response, @PathVariable String columnId) throws IOException {
  247. String dateTime = request.getParameter("dateTime");
  248. // 网站风格物理路径
  249. List<CategoryBean> articleIdList = null;
  250. List<CategoryEntity> categoryList = new ArrayList<CategoryEntity>();
  251. ContentBean contentBean = new ContentBean();
  252. contentBean.setBeginTime(dateTime);
  253. // 时间格式化
  254. Date contentUpdateTime = null;
  255. try {
  256. contentUpdateTime = DateUtil.parse(dateTime);
  257. } catch (DateException e) {
  258. e.printStackTrace();
  259. return ResultData.build().error(getResString("err.error",this.getResString("datetime.format")));
  260. }
  261. if ("0".equals(columnId)) {
  262. // 根据leaf进行升序排序,让叶子栏目最后静态化 保证文章上下篇关系正确
  263. LambdaQueryWrapper<CategoryEntity> wrapper = new LambdaQueryWrapper<CategoryEntity>().orderByAsc(CategoryEntity::getLeaf);
  264. categoryList = categoryBiz.list(wrapper);
  265. } else { //选择栏目更新
  266. CategoryEntity categoryEntity = new CategoryEntity();
  267. categoryEntity.setId(columnId);
  268. categoryList = categoryBiz.queryChildren(categoryEntity);
  269. }
  270. // todo 这里延续之前的详情上下篇思路(不考虑跨栏目),只对栏目内的文章进行上下篇处理,且单篇没有上下篇;
  271. // 获取叶子节点栏目
  272. categoryList = categoryList.stream().filter(item->{
  273. return item.getLeaf();
  274. }).collect(Collectors.toList());
  275. for (CategoryEntity category : categoryList) {
  276. // 如果栏目被禁用则跳过
  277. if (CategoryDisplayEnum.DISABLE.toString().equalsIgnoreCase(category.getCategoryDisplay())){
  278. continue;
  279. }
  280. //如果是链接就跳过生成
  281. if (category.getCategoryType().equals(CategoryTypeEnum.LINK.toString())) {
  282. continue;
  283. }
  284. // 如果是单篇文章跳过生成
  285. if (category.getCategoryType().equals(CategoryTypeEnum.COVER.toString())) {
  286. continue;
  287. }
  288. contentBean.setCategoryId(category.getId());
  289. contentBean.setCategoryType(category.getCategoryType());
  290. contentBean.setOrderBy("date");
  291. //将文章列表标签中的中的参数
  292. articleIdList = contentBiz.queryIdsByCategoryIdForParser(contentBean);
  293. // 分类是列表
  294. if (category.getCategoryType().equals(CategoryTypeEnum.LIST.toString())) {
  295. // 判断模板文件是否存在
  296. if (!FileUtil.exist(ParserUtil.buildTemplatePath(category.getCategoryListUrl())) || StringUtils.isEmpty(category.getCategoryListUrl())) {
  297. LOG.error("{} 模板不存在:{}", category.getCategoryTitle(), category.getCategoryListUrl());
  298. continue;
  299. }
  300. // 有符合条件的就更新
  301. if (articleIdList.size() > 0) {
  302. CmsParserUtil.generateBasic(articleIdList, htmlDir,contentUpdateTime);
  303. }
  304. }
  305. }
  306. return ResultData.build().success();
  307. }
  308. /**
  309. * 用户预览主页
  310. *
  311. * @param request
  312. * @return
  313. */
  314. @ApiOperation(value = "预览主页接口")
  315. @RequestMapping(value = "/{position}/viewIndex", method = {RequestMethod.GET, RequestMethod.POST})
  316. public String viewIndex(HttpServletRequest request, @PathVariable String position, HttpServletResponse response) {
  317. AppEntity app = BasicUtil.getApp();
  318. // 组织主页预览地址
  319. String indexPosition = app.getAppHostUrl() + htmlDir + File.separator + app.getAppDir()
  320. + File.separator + position + ParserUtil.HTML_SUFFIX;
  321. return "redirect:" + indexPosition;
  322. }
  323. /**
  324. * 删除页面
  325. * <p>
  326. * 页面名称
  327. *
  328. * @param request 响应
  329. */
  330. @ApiOperation(value = "删除页面")
  331. @ApiImplicitParam(name = "fileName", value = "主页名称", required = true, paramType = "query")
  332. @LogAnn(title = "删除页面", businessType = BusinessTypeEnum.DELETE)
  333. @PostMapping("/delete")
  334. @ResponseBody
  335. @RequiresPermissions("cms:generate:del")
  336. public ResultData delete(HttpServletRequest request) {
  337. String deletePath = BasicUtil.getString("deletePath");
  338. if (StrUtil.isBlank(deletePath)) {
  339. return ResultData.build().error("请先输入要删除的文件路径");
  340. }
  341. // 确定以html开头
  342. if (!deletePath.startsWith(htmlDir) && !deletePath.startsWith("/" + htmlDir)) {
  343. return ResultData.build().error("删除路径请以" + htmlDir + "开头");
  344. }
  345. if (deletePath.contains("..") || deletePath.contains("../") || deletePath.contains("..\\")) {
  346. return ResultData.build().error("非法路径");
  347. }
  348. String appDir = BasicUtil.getApp().getAppDir();
  349. // 用户输入的实际路径
  350. String realPath = BasicUtil.getRealPath(deletePath);
  351. //站点的真实路径 html/web(站点id)
  352. String webRealPath = BasicUtil.getRealPath(htmlDir + "/" + appDir + "/");
  353. String htmlRealPath = BasicUtil.getRealPath(htmlDir);
  354. // 取html下的文件
  355. List<String> paths = FileUtil.listFileNames(htmlRealPath);
  356. // 暂不考虑 html/web/index 与 html/index短链 同时存在; 只处理html/web/index 与 html/index短链 单独存在的场景
  357. if (CollUtil.isEmpty(paths)) {
  358. // 为空则说为目录 就有web;
  359. // 判断传入的路径是否以 (html+/+站点+/ yaml配置中) 开头;是则为同一个站,不是则跨站
  360. if (!realPath.startsWith(webRealPath)) {
  361. return ResultData.build().error("不允许跨站删除");
  362. }
  363. }
  364. // 只剩下情况 文件 html/web(本站)/index 和 html/index 判断直接删除文件
  365. if (FileUtil.exist(realPath)) {
  366. FileUtil.del(realPath);
  367. return ResultData.build().success();
  368. } else {
  369. return ResultData.build().error("文件不存在");
  370. }
  371. }
  372. }