Sitemap.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // |
  4. // +----------------------------------------------------------------------
  5. // |
  6. // +----------------------------------------------------------------------
  7. // |
  8. // +----------------------------------------------------------------------
  9. // |
  10. // +----------------------------------------------------------------------
  11. // +----------------------------------------------------------------------
  12. // | sitemap管理
  13. // +----------------------------------------------------------------------
  14. namespace app\admin\controller;
  15. use addons\sitemap\library\Sitemap as SitemapLib;
  16. use app\admin\model\general\Lang as LangModel;
  17. use app\common\controller\Backend;
  18. use think\facade\Db;
  19. class Sitemap extends Backend
  20. {
  21. protected $directory;
  22. protected $url_mode = 1;
  23. protected function tableQuery($table)
  24. {
  25. $prefix = config('database.connections.mysql.prefix', '');
  26. $tableName = strpos($table, $prefix) === 0 ? $table : $prefix . $table;
  27. return Db::table($tableName);
  28. }
  29. protected function enabledLangs()
  30. {
  31. return LangModel::getEnabledList();
  32. }
  33. protected function defaultLang()
  34. {
  35. return LangModel::getDefaultMark();
  36. }
  37. protected function withLangPrefix($url, $lang)
  38. {
  39. $defaultLang = $this->defaultLang();
  40. if (!$lang || $lang === $defaultLang) {
  41. return $url;
  42. }
  43. $parts = parse_url($url);
  44. $path = $parts['path'] ?? '/';
  45. $path = '/' . ltrim($path, '/');
  46. if (preg_match('#^/' . preg_quote($lang, '#') . '(/|$)#i', $path)) {
  47. return $url;
  48. }
  49. $newPath = '/' . $lang . ($path === '/' ? '' : $path);
  50. $result = '';
  51. if (!empty($parts['scheme'])) {
  52. $result .= $parts['scheme'] . '://';
  53. }
  54. if (!empty($parts['user'])) {
  55. $result .= $parts['user'];
  56. if (!empty($parts['pass'])) {
  57. $result .= ':' . $parts['pass'];
  58. }
  59. $result .= '@';
  60. }
  61. if (!empty($parts['host'])) {
  62. $result .= $parts['host'];
  63. }
  64. if (!empty($parts['port'])) {
  65. $result .= ':' . $parts['port'];
  66. }
  67. $result .= $newPath;
  68. if (!empty($parts['query'])) {
  69. $result .= '?' . $parts['query'];
  70. }
  71. if (!empty($parts['fragment'])) {
  72. $result .= '#' . $parts['fragment'];
  73. }
  74. return $result;
  75. }
  76. protected function contentExtTableExists($table)
  77. {
  78. static $tables = null;
  79. if ($tables === null) {
  80. $tables = array_map('current', Db::query('SHOW TABLES'));
  81. }
  82. return in_array(config('database.connections.mysql.prefix') . $table, $tables, true) || in_array($table, $tables, true);
  83. }
  84. protected function tableFields($table)
  85. {
  86. static $cache = [];
  87. if (!isset($cache[$table])) {
  88. $prefix = config('database.connections.mysql.prefix', '');
  89. $tableName = strpos($table, $prefix) === 0 ? $table : $prefix . $table;
  90. $cache[$table] = array_map('strtolower', array_keys(Db::getFields($tableName)));
  91. }
  92. return $cache[$table];
  93. }
  94. protected function pickLastmod(array $row, $currentTime)
  95. {
  96. if (isset($row['update_time']) && $row['update_time']) {
  97. return $row['update_time'];
  98. }
  99. if (isset($row['create_time']) && $row['create_time']) {
  100. return $row['create_time'];
  101. }
  102. return $currentTime;
  103. }
  104. protected function initialize()
  105. {
  106. parent::initialize();
  107. $this->url_mode = (int) get_addon_config('cms')['site_url_mode'];
  108. $this->directory = public_path();
  109. }
  110. public function index()
  111. {
  112. if ($this->request->isPost()) {
  113. $info = get_addon_info('cms');
  114. if (!$info || $info['status'] != 1) {
  115. $this->error("请在后台插件管理中安装《内容管理系统》并启用后再尝试");
  116. }
  117. $data = $this->request->post();
  118. $maxUrlsPerFile = intval($data['max_url'] ?? 50000);
  119. if ($maxUrlsPerFile <= 0) {
  120. $this->error("每个sitemap文件的最大URL数量必须大于0");
  121. }
  122. if ($maxUrlsPerFile > 50000) {
  123. $this->error("每个sitemap文件最多不能超过50000个URL");
  124. }
  125. $totalUrls = $this->calculateTotalUrls();
  126. if ($totalUrls <= $maxUrlsPerFile) {
  127. // 如果URL数量不超过限制,生成单个文件
  128. $result = $this->generateSingleSitemap($data);
  129. $this->success($result['message']);
  130. } else {
  131. // 如果超过限制,生成多个文件 + 索引文件
  132. $result = $this->generateMultipleSitemaps($data, $totalUrls, $maxUrlsPerFile);
  133. $this->success($result['message']);
  134. }
  135. } else {
  136. $fileInfo = $this->getSitemapFileInfo();
  137. $this->assign($fileInfo);
  138. return $this->fetch();
  139. }
  140. }
  141. /**
  142. * 计算总URL数量
  143. */
  144. private function calculateTotalUrls()
  145. {
  146. $itemsByLang = $this->collectAllUrls([
  147. 'index' => ['priority' => 0, 'changefreq' => 'daily'],
  148. 'category' => ['priority' => 0, 'changefreq' => 'daily'],
  149. 'content' => ['priority' => 0, 'changefreq' => 'daily'],
  150. 'tag' => ['priority' => 0, 'changefreq' => 'daily'],
  151. ], $this->request->domain());
  152. $count = 0;
  153. foreach ($itemsByLang as $langItems) {
  154. $count += count($langItems);
  155. }
  156. return $count;
  157. }
  158. /**
  159. * 生成单个sitemap文件
  160. */
  161. private function generateSingleSitemap($data)
  162. {
  163. $rootUrl = $this->request->domain();
  164. $itemsByLang = $this->collectAllUrls($data, $rootUrl);
  165. $allItems = [];
  166. foreach ($itemsByLang as $lang => $langItems) {
  167. foreach ($langItems as $item) {
  168. $allItems[] = $item;
  169. }
  170. }
  171. if (!$data['type']) {
  172. $langs = array_keys($itemsByLang);
  173. if (count($langs) <= 1) {
  174. $Sitemap = new SitemapLib();
  175. foreach ($allItems as $item) {
  176. $Sitemap->AddItem($item['loc'], $item['priority'], $item['changefreq'], $item['lastmod']);
  177. }
  178. $filename = 'sitemap.xml';
  179. $Sitemap->SaveToFile($this->directory . $filename);
  180. $message = "生成成功!共 " . count($allItems) . " 个URL,文件: {$filename}";
  181. } else {
  182. $sitemapFiles = [];
  183. foreach ($itemsByLang as $lang => $langItems) {
  184. $filename = 'sitemap-' . $lang . '.xml';
  185. $Sitemap = new SitemapLib();
  186. foreach ($langItems as $item) {
  187. $Sitemap->AddItem($item['loc'], $item['priority'], $item['changefreq'], $item['lastmod']);
  188. }
  189. $Sitemap->SaveToFile($this->directory . $filename);
  190. $sitemapFiles[] = [
  191. 'filename' => $filename,
  192. 'url' => $rootUrl . '/' . $filename,
  193. 'lastmod' => date('Y-m-d\TH:i:s+00:00'),
  194. ];
  195. }
  196. $this->generateSitemapIndex($sitemapFiles, $rootUrl, 'sitemap.xml');
  197. $message = "生成成功!共 " . count($allItems) . " 个URL,按语言生成 " . count($sitemapFiles) . " 个文件,索引文件: sitemap.xml";
  198. }
  199. } else {
  200. $langs = array_keys($itemsByLang);
  201. if (count($langs) <= 1) {
  202. $filename = 'sitemap.txt';
  203. $content = $this->generateTxtContent($allItems);
  204. file_put_contents($this->directory . $filename, $content);
  205. $message = "生成成功!共 " . count($allItems) . " 个URL,文件: {$filename}";
  206. } else {
  207. foreach ($itemsByLang as $lang => $langItems) {
  208. file_put_contents($this->directory . 'sitemap-' . $lang . '.txt', $this->generateTxtContent($langItems));
  209. }
  210. $message = "生成成功!共 " . count($allItems) . " 个URL,已按语言生成 TXT 文件。";
  211. }
  212. }
  213. return [
  214. 'success' => true,
  215. 'message' => $message,
  216. ];
  217. }
  218. /**
  219. * 生成多个sitemap文件 + 索引文件
  220. */
  221. private function generateMultipleSitemaps($data, $totalUrls, $maxUrlsPerFile)
  222. {
  223. $rootUrl = $this->request->domain();
  224. $itemsByLang = $this->collectAllUrls($data, $rootUrl);
  225. $sitemapFiles = [];
  226. $totalItems = 0;
  227. foreach ($itemsByLang as $langItems) {
  228. $totalItems += count($langItems);
  229. }
  230. if (!$data['type']) {
  231. foreach ($itemsByLang as $lang => $langItems) {
  232. $chunks = array_chunk($langItems, $maxUrlsPerFile);
  233. foreach ($chunks as $index => $chunkItems) {
  234. $filename = count($chunks) > 1 ? ('sitemap-' . $lang . '-' . ($index + 1) . '.xml') : ('sitemap-' . $lang . '.xml');
  235. $Sitemap = new SitemapLib();
  236. foreach ($chunkItems as $item) {
  237. $Sitemap->AddItem($item['loc'], $item['priority'], $item['changefreq'], $item['lastmod']);
  238. }
  239. $Sitemap->SaveToFile($this->directory . $filename);
  240. $sitemapFiles[] = [
  241. 'filename' => $filename,
  242. 'url' => $rootUrl . '/' . $filename,
  243. 'lastmod' => date('Y-m-d\TH:i:s+00:00'),
  244. ];
  245. }
  246. }
  247. $this->generateSitemapIndex($sitemapFiles, $rootUrl, 'sitemap.xml');
  248. $message = "生成成功!共 {$totalItems} 个URL,按语言/分片生成 " . count($sitemapFiles) . " 个文件,索引文件: sitemap.xml";
  249. } else {
  250. foreach ($itemsByLang as $lang => $langItems) {
  251. $chunks = array_chunk($langItems, $maxUrlsPerFile);
  252. foreach ($chunks as $index => $chunkItems) {
  253. $filename = count($chunks) > 1 ? ('sitemap-' . $lang . '-' . ($index + 1) . '.txt') : ('sitemap-' . $lang . '.txt');
  254. file_put_contents($this->directory . $filename, $this->generateTxtContent($chunkItems));
  255. }
  256. }
  257. $message = "生成成功!共 {$totalItems} 个URL,已按语言生成 TXT 文件。";
  258. }
  259. return [
  260. 'success' => true,
  261. 'message' => $message,
  262. ];
  263. }
  264. /**
  265. * 收集所有URL数据
  266. */
  267. private function collectAllUrls($data, $rootUrl)
  268. {
  269. $itemsByLang = [];
  270. $category = cache('Category');
  271. $currentTime = time();
  272. $defaultLang = $this->defaultLang();
  273. $langs = $this->enabledLangs();
  274. foreach ($langs as $langItem) {
  275. $lang = $langItem->mark;
  276. $itemsByLang[$lang] = [];
  277. $itemsByLang[$lang][] = [
  278. 'loc' => $this->withLangPrefix($rootUrl, $lang),
  279. 'priority' => intval($data['index']['priority']),
  280. 'changefreq' => $data['index']['changefreq'],
  281. 'lastmod' => $currentTime,
  282. ];
  283. }
  284. $categoryFields = $this->tableFields('cms_category');
  285. $categorySelectFields = ['id', 'url', 'catdir'];
  286. if (in_array('update_time', $categoryFields, true)) {
  287. $categorySelectFields[] = 'update_time';
  288. } elseif (in_array('create_time', $categoryFields, true)) {
  289. $categorySelectFields[] = 'create_time';
  290. }
  291. $categoryList = $this->tableQuery('cms_category')->where('status', 1)->order('id desc')->field(implode(',', $categorySelectFields))->select();
  292. if (!empty($categoryList)) {
  293. foreach ($categoryList as $vo) {
  294. foreach ($langs as $langItem) {
  295. $lang = $langItem->mark;
  296. if ($lang !== $defaultLang) {
  297. $hasTranslate = $this->tableQuery('cms_category_data')->where(['catid' => $vo['id'], 'lang' => $lang])->count() > 0;
  298. if (!$hasTranslate) {
  299. continue;
  300. }
  301. }
  302. $cat = $this->url_mode == 1 ? $vo['id'] : $vo['catdir'];
  303. $itemsByLang[$lang][] = [
  304. 'loc' => $this->withLangPrefix($rootUrl . buildCatUrl($cat), $lang),
  305. 'priority' => intval($data['category']['priority']),
  306. 'changefreq' => $data['category']['changefreq'],
  307. 'lastmod' => $this->pickLastmod((array)$vo, $currentTime),
  308. ];
  309. }
  310. }
  311. }
  312. $modelList = cache('Model');
  313. if (!empty($modelList)) {
  314. foreach ($modelList as $vo) {
  315. if ($vo['module'] != 'cms') {
  316. continue;
  317. }
  318. $contentFields = $this->tableFields($vo['tablename']);
  319. $contentSelectFields = ['id', 'catid'];
  320. $orderField = 'id';
  321. if (in_array('update_time', $contentFields, true)) {
  322. $contentSelectFields[] = 'update_time';
  323. $orderField = 'update_time';
  324. } elseif (in_array('create_time', $contentFields, true)) {
  325. $contentSelectFields[] = 'create_time';
  326. $orderField = 'create_time';
  327. }
  328. $contentList = $this->tableQuery($vo['tablename'])
  329. ->where('status', 1)
  330. ->order($orderField . ' desc')
  331. ->field(implode(',', $contentSelectFields))
  332. ->select();
  333. if (empty($contentList)) {
  334. continue;
  335. }
  336. $extTable = $vo['tablename'] . '_data';
  337. $hasExtTable = $this->contentExtTableExists($extTable);
  338. $extFields = $hasExtTable ? $this->tableFields($extTable) : [];
  339. $hasLangColumn = in_array('lang', $extFields, true);
  340. foreach ($contentList as $v) {
  341. foreach ($langs as $langItem) {
  342. $lang = $langItem->mark;
  343. if ($lang !== $defaultLang && $hasExtTable && $hasLangColumn) {
  344. $hasTranslate = $this->tableQuery($extTable)->where(['did' => $v['id'], 'lang' => $lang])->count() > 0;
  345. if (!$hasTranslate) {
  346. continue;
  347. }
  348. } elseif ($lang !== $defaultLang && $hasExtTable && !$hasLangColumn) {
  349. continue;
  350. }
  351. $cat = $this->url_mode == 1 ? $v['catid'] : (isset($category[$v['catid']]) ? $category[$v['catid']]['catdir'] : getCategory($v['catid'], 'catdir'));
  352. $itemsByLang[$lang][] = [
  353. 'loc' => $this->withLangPrefix($rootUrl . buildContentUrl($cat, $v['id']), $lang),
  354. 'priority' => intval($data['content']['priority']),
  355. 'changefreq' => $data['content']['changefreq'],
  356. 'lastmod' => $this->pickLastmod((array)$v, $currentTime),
  357. ];
  358. }
  359. }
  360. }
  361. }
  362. $tagFields = $this->tableFields('cms_tags');
  363. $tagSelectFields = ['tag'];
  364. $tagOrderField = 'tag';
  365. if (in_array('update_time', $tagFields, true)) {
  366. $tagSelectFields[] = 'update_time';
  367. $tagOrderField = 'update_time';
  368. } elseif (in_array('create_time', $tagFields, true)) {
  369. $tagSelectFields[] = 'create_time';
  370. $tagOrderField = 'create_time';
  371. }
  372. $tags = $this->tableQuery('cms_tags')->order($tagOrderField . ' desc')->field(implode(',', $tagSelectFields))->select();
  373. if (!empty($tags)) {
  374. foreach ($tags as $vo) {
  375. $itemsByLang[$defaultLang][] = [
  376. 'loc' => $rootUrl . addon_url('cms/tags/index', [':tag' => $vo['tag']]),
  377. 'priority' => intval($data['tag']['priority']),
  378. 'changefreq' => $data['tag']['changefreq'],
  379. 'lastmod' => $this->pickLastmod((array)$vo, $currentTime),
  380. ];
  381. }
  382. }
  383. return $itemsByLang;
  384. }
  385. /**
  386. * 生成sitemap索引文件
  387. */
  388. private function generateSitemapIndex($sitemapFiles, $rootUrl, $filename = 'sitemap_index.xml')
  389. {
  390. $xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
  391. $xml .= '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
  392. foreach ($sitemapFiles as $file) {
  393. $xml .= " <sitemap>\n";
  394. $xml .= " <loc>{$file['url']}</loc>\n";
  395. $xml .= " <lastmod>{$file['lastmod']}</lastmod>\n";
  396. $xml .= " </sitemap>\n";
  397. }
  398. $xml .= '</sitemapindex>';
  399. file_put_contents($this->directory . $filename, $xml);
  400. }
  401. /**
  402. * 生成TXT格式内容
  403. */
  404. private function generateTxtContent($items)
  405. {
  406. $content = '';
  407. foreach ($items as $item) {
  408. $content .= $item['loc'] . PHP_EOL;
  409. }
  410. return $content;
  411. }
  412. /**
  413. * 获取sitemap文件信息
  414. */
  415. private function getSitemapFileInfo()
  416. {
  417. $fileInfo = [];
  418. if (is_file($this->directory . 'sitemap.xml')) {
  419. $fileInfo['make_xml_time'] = date('Y-m-d H:i:s', filemtime($this->directory . 'sitemap.xml'));
  420. $fileInfo['xml_size'] = round(filesize($this->directory . 'sitemap.xml') / 1024, 2);
  421. }
  422. if (is_file($this->directory . 'sitemap.txt')) {
  423. $fileInfo['make_txt_time'] = date('Y-m-d H:i:s', filemtime($this->directory . 'sitemap.txt'));
  424. $fileInfo['txt_size'] = round(filesize($this->directory . 'sitemap.txt') / 1024, 2);
  425. }
  426. if (is_file($this->directory . 'sitemap_index.xml')) {
  427. $fileInfo['make_index_time'] = date('Y-m-d H:i:s', filemtime($this->directory . 'sitemap_index.xml'));
  428. $fileInfo['index_size'] = round(filesize($this->directory . 'sitemap_index.xml') / 1024, 2);
  429. }
  430. // 获取所有sitemap分割文件
  431. $sitemapFiles = glob($this->directory . 'sitemap_*.xml');
  432. if (!empty($sitemapFiles)) {
  433. $fileInfo['split_files'] = count($sitemapFiles);
  434. }
  435. return $fileInfo;
  436. }
  437. }