修复部分bug

This commit is contained in:
www.xueximeng.com
2025-08-07 19:06:22 +08:00
parent 6d871a4f05
commit 8e3b385dd9
32 changed files with 578 additions and 2549 deletions

165
README.md
View File

@@ -35,35 +35,47 @@ docker run -d --name dongman \
```
## 适配移动端样式
移动端也可以更好的体验美漫共建小站了
![移动端|237x499](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/1.gif)
## 首页
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/2.jpg)
可以根据资源中文名、英文名、简介进行搜索
![首页|690x392](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/3.gif)
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/1.jpg)
## 详情页
![image|690x398](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/4.jpg)
可以切换查看图片,选择网盘标签,一键复制网盘链接密码
![详情页|690x392](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/5.gif)
## 关于本站
![关于|690x392](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/6.gif)
点击「盘搜」按钮,一键搜索各种网盘资源
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/pansou.gif)
点击「剧集探索」按钮,可以查看分季分集信息
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/30.gif)
可以一键生成分享海报和链接
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/29.gif)
一键在线点播
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/dianbo.gif)
也可以直接在`https://域名/streams`页面点播,支持解析线路和自定义爬虫
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/streams.gif)
## 全面支持管理后台设置网站信息和采集解析源
目前美漫共建官网内置30条数据源
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/26.gif)
## 支持外挂在线播放数据源(自定义爬虫)
会写爬虫的用户可以自己添加数据源,更加灵活。参考[外接数据源开发者文档](https://github.com/fish2018/GoComicMosaic/blob/main/docs/%E5%A4%96%E6%8E%A5%E6%95%B0%E6%8D%AE%E6%BA%90%E5%BC%80%E5%8F%91%E6%96%87%E6%A1%A3.md),提供[爬虫示例及模板](https://github.com/fish2018/GoComicMosaic/tree/main/docs/%E5%A4%96%E6%8E%A5%E6%95%B0%E6%8D%AE%E6%BA%90%E7%A4%BA%E4%BE%8B%E5%8F%8A%E6%A8%A1%E6%9D%BF)
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/27.gif)
## 提交资源
这个才是资源共建平台的核心,点击右上角的'提交资源',用户可以随意提交自己喜欢的动漫资源,如果网站还不存该美漫时,会是一个新建资源的表单,需要填写中文名、英文名、类型、简介等基础信息。提交后,要等管理员在后台审批完才会在首页显示
### 提交-新建资源
![image|690x384](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/7.jpg)
![image|690x379](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/8.jpg)
网盘链接和图片都可以提交多个
![提交资源|690x392](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/9.gif)
支持从TMDB搜索、预览、一键导入资源
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/28.gif)
### 提交-补充资源
顾名思义,就是对已经存在的动漫资源补充一些信息,主要是图片、资源链接
@@ -75,11 +87,6 @@ docker run -d --name dongman \
从资源详情页点击'补充资源'按钮,不用自己再搜索选择了,自动绑定对应的动漫
![详情页补充|690x392](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/12.gif)
## 管理员登录
不用多说了,就是输入账号密码,初始密码登录后可修改
![image|690x401](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/13.jpg)
## 管理控制台
主要用于审批用户提交的资源
@@ -95,120 +102,8 @@ docker run -d --name dongman \
![详情编辑|690x391](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/18.gif)
## 新增喜欢按钮
在详情页可以点击喜欢
![image|690x370](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/19.jpg)
首页可以根据喜欢数量排序,默认按最新发布排序
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/20.jpg)
## 新增分页
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/21.jpg)
## 新增在线点播功能
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/22.gif)
## 优化检测
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/23.jpg)
## 调整底栏
- 添加在线点播
- 添加访问统计
- 添加友链
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/24.jpg)
## 详情页剧照点击放大查看
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/25.gif)
## 全面支持管理后台设置网站信息和采集解析源
目前美漫共建官网内置30条数据源
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/26.gif)
## 支持外挂在线播放数据源
会写爬虫的用户可以自己添加数据源,更加灵活。参考[外接数据源开发者文档](https://github.com/fish2018/GoComicMosaic/blob/main/docs/%E5%A4%96%E6%8E%A5%E6%95%B0%E6%8D%AE%E6%BA%90%E5%BC%80%E5%8F%91%E6%96%87%E6%A1%A3.md),提供[爬虫示例及模板](https://github.com/fish2018/GoComicMosaic/tree/main/docs/%E5%A4%96%E6%8E%A5%E6%95%B0%E6%8D%AE%E6%BA%90%E7%A4%BA%E4%BE%8B%E5%8F%8A%E6%A8%A1%E6%9D%BF)
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/27.gif)
## 支持从TMDB一键导入资源库
可以从TMDB搜索、预览、一键导入资源
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/28.gif)
## 支持一键分享
资源详情页可以一键生成分享海报和链接
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/29.gif)
## 新增「剧集探索」功能,支持查看分季分集信息
资源详情页可以一键生成分享海报和链接
![image|690x397](https://raw.githubusercontent.com/fishforks/imgs/refs/heads/main/gcm/30.gif)
---
# 更新日志
-202507162228
✅ 集成网盘搜索功能,资源详情页点击`盘搜`按钮可以自动搜索各类网盘链接
-202507161526
✅ 优化TMDB搜索显示结果列表页选择具体资源后再显示详情支持电影搜索
✅ 变更访问人数统计工具
-202507091318
✅ 新增贴纸功能,可以在详情页显示透明贴纸,用户自由拖拽、旋转
-202507061001
✅ 外接数据源开发支持跨域代理返回Cookies功能
✅ 外接数据源开发支持桥接存储 (localStorage)
✅ 外接数据源开发支持二次请求播放地址支持 (getPlayUrl)
✅ 优化数据源加载机制,只有在流媒体播放页面切换数据源时才开始加载
✅ 新增bilibili外接数据源
✅ 内置一键图片清晰AI工具
✅ 调整vite.config.js配置proxy为`/app`(原`/api`),避免歧义
-202506230806
✅ 新增文章功能支持markdown
✅ 图片支持拖拽排序
✅ 支持通过链接添加图片
-202506211815
✅ 新增滑动切换图片功能,对移动端体验友好
✅ 调整网盘显示顺序
-202506211422
✅ 大幅优化「剧集探索」加载速度,切换季、集、剧照更丝滑
✅ 在线点播页面新增主页推荐功能
-202506191159
✅ 新增「剧集探索」功能,支持查看电视剧分季和分集信息
-202506180813
✅ 后台网站设置增加免责声明模块支持html代码
✅ 播放器增加倍速选择、增加快捷键功能
-202506151233
✅ TMDB搜索后支持直接编辑然后再一键导入
✅ 调整TMDB搜索预览界面和实际资源详情页保持风格一致
-202506141634
✅ 去掉点播时的质量设置,提升加载速度
✅ 播放器界面切换数据源时,不再自动搜索
✅ 资源详情页大图预览区限制高度,避免出现竖图时过于突兀
✅ 梳理文档集中放到docs目录下
-202506121410
✅ 后端跨域转发代理支持所有请求方法支持透传headers
✅ 重新优化外接数据源解决跨域问题提供外接数据源爬虫示例lanmei.js、rebo.js和模板
-202506112021
✅ 修复TMDB_API_KEY泄露问题
✅ 详情页增加一键分享功能
✅ 修复GO使用sqlite3时开启WAL导致数据丢失问题
-202506100835
✅ 后台读取到环境变量配置的TMDB_API_KEY会自动保存到数据库
✅ 支持开启/关闭TMDB功能自由控制顶栏显示
-202506091954
✅ 支持从TMDB一键导入资源库
✅ 支持从环境变量、管理后台配置TDMB_API_KEY
-202506081248
✅ 后台网站设置改为标签切换配置
✅ 后台支持配置采集解析数据源
✅ 拆分独立CSS文件
✅ 独立icon文件包含2000多图标
-202506071917
✅ 全面支持管理后台设置网站信息
-202506061607
✅ 优化悬浮按钮样式问题
✅ 修复最近播放恢复播放失败问题
✅ 新增的资源,如果没有批准任何图片和链接,则代表审核不通过,直接删除该条数据
✅ 修复编辑资源时,将新上传的图片设置为海报失败问题
-2020506051132
✅ 增加golang版动态生成sitemap工具`sitemap-generator`,为将来容器化做准备
✅ 允许通过环境变量指定assets和数据库路径为将来容器化做准备
✅ 自动判断vite.config.js中是否需要启用`base: '/static/',`,只有正式编译时启用,本地开发不会启用,避免每次编译手动修改一遍
✅ 使用express代理访问本地静态资源路径根据.env.production配置中的ASSETS_PATH自动设置默认路径'../assets'
✅ 调整后台所有图片预览模态框,保持全站风格一致,审批通过的图片,点击也可以放大看
✅ 优化搜索框样式

View File

@@ -1,3 +1,3 @@
BASE_URL=https://dm.xueximeng.com
ASSETS_PATH=../assets
# ASSETS_PATH=../data/assets
#ASSETS_PATH=../data/assets

View File

@@ -2,25 +2,25 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://dm.xueximeng.com/</loc>
<lastmod>2025-07-19</lastmod>
<lastmod>2025-08-07</lastmod>
<changefreq>daily</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/submit</loc>
<lastmod>2025-07-19</lastmod>
<lastmod>2025-08-07</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/about</loc>
<lastmod>2025-07-19</lastmod>
<lastmod>2025-08-07</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/11</loc>
<lastmod>2025-07-17</lastmod>
<lastmod>2025-07-26</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -38,7 +38,7 @@
</url>
<url>
<loc>https://dm.xueximeng.com/resource/46</loc>
<lastmod>2025-06-27</lastmod>
<lastmod>2025-07-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -68,7 +68,7 @@
</url>
<url>
<loc>https://dm.xueximeng.com/resource/82</loc>
<lastmod>2025-07-07</lastmod>
<lastmod>2025-08-07</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -78,6 +78,12 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/10</loc>
<lastmod>2025-08-04</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/30</loc>
<lastmod>2025-06-27</lastmod>
@@ -85,14 +91,14 @@
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/10</loc>
<lastmod>2025-06-18</lastmod>
<loc>https://dm.xueximeng.com/resource/26</loc>
<lastmod>2025-07-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/26</loc>
<lastmod>2025-07-18</lastmod>
<loc>https://dm.xueximeng.com/resource/23</loc>
<lastmod>2025-07-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -109,14 +115,20 @@
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/23</loc>
<lastmod>2025-06-27</lastmod>
<loc>https://dm.xueximeng.com/resource/31</loc>
<lastmod>2025-06-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/31</loc>
<lastmod>2025-06-18</lastmod>
<loc>https://dm.xueximeng.com/resource/55</loc>
<lastmod>2025-06-20</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/7</loc>
<lastmod>2025-08-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -128,7 +140,7 @@
</url>
<url>
<loc>https://dm.xueximeng.com/resource/37</loc>
<lastmod>2025-07-07</lastmod>
<lastmod>2025-07-28</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -139,14 +151,26 @@
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/55</loc>
<lastmod>2025-06-20</lastmod>
<loc>https://dm.xueximeng.com/resource/47</loc>
<lastmod>2025-07-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/107</loc>
<lastmod>2025-07-06</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/109</loc>
<lastmod>2025-07-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/137</loc>
<lastmod>2025-06-23</lastmod>
<lastmod>2025-07-20</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -169,20 +193,32 @@
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/47</loc>
<lastmod>2025-06-27</lastmod>
<loc>https://dm.xueximeng.com/resource/48</loc>
<lastmod>2025-06-20</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/107</loc>
<lastmod>2025-07-06</lastmod>
<loc>https://dm.xueximeng.com/resource/61</loc>
<lastmod>2025-07-27</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/109</loc>
<lastmod>2025-06-27</lastmod>
<loc>https://dm.xueximeng.com/resource/161</loc>
<lastmod>2025-07-17</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/184</loc>
<lastmod>2025-07-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/185</loc>
<lastmod>2025-07-16</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -193,14 +229,20 @@
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/7</loc>
<lastmod>2025-06-22</lastmod>
<loc>https://dm.xueximeng.com/resource/16</loc>
<lastmod>2025-07-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/16</loc>
<lastmod>2025-06-19</lastmod>
<loc>https://dm.xueximeng.com/resource/27</loc>
<lastmod>2025-07-26</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/29</loc>
<lastmod>2025-06-20</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -210,12 +252,6 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/48</loc>
<lastmod>2025-06-20</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/75</loc>
<lastmod>2025-06-27</lastmod>
@@ -235,14 +271,14 @@
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/184</loc>
<lastmod>2025-07-18</lastmod>
<loc>https://dm.xueximeng.com/resource/99</loc>
<lastmod>2025-08-05</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/185</loc>
<lastmod>2025-07-16</lastmod>
<loc>https://dm.xueximeng.com/resource/157</loc>
<lastmod>2025-07-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -252,24 +288,24 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/15</loc>
<lastmod>2025-07-16</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/21</loc>
<lastmod>2025-06-28</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/22</loc>
<lastmod>2025-07-17</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/27</loc>
<lastmod>2025-06-21</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/29</loc>
<lastmod>2025-06-20</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/32</loc>
<lastmod>2025-06-20</lastmod>
@@ -294,15 +330,9 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/61</loc>
<lastmod>2025-06-27</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/62</loc>
<lastmod>2025-06-21</lastmod>
<lastmod>2025-07-20</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -318,12 +348,6 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/99</loc>
<lastmod>2025-06-27</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/104</loc>
<lastmod>2025-06-21</lastmod>
@@ -336,12 +360,42 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/162</loc>
<lastmod>2025-07-20</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/169</loc>
<lastmod>2025-07-02</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/174</loc>
<lastmod>2025-07-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/180</loc>
<lastmod>2025-07-19</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/194</loc>
<lastmod>2025-07-26</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/196</loc>
<lastmod>2025-07-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/2</loc>
<lastmod>2025-06-21</lastmod>
@@ -354,12 +408,6 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/15</loc>
<lastmod>2025-07-16</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/20</loc>
<lastmod>2025-06-27</lastmod>
@@ -420,6 +468,12 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/70</loc>
<lastmod>2025-06-17</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/74</loc>
<lastmod>2025-06-14</lastmod>
@@ -492,27 +546,9 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/157</loc>
<lastmod>2025-06-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/159</loc>
<lastmod>2025-06-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/161</loc>
<lastmod>2025-07-17</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/162</loc>
<lastmod>2025-06-26</lastmod>
<lastmod>2025-08-07</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -523,14 +559,20 @@
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/169</loc>
<lastmod>2025-07-02</lastmod>
<loc>https://dm.xueximeng.com/resource/173</loc>
<lastmod>2025-07-17</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/174</loc>
<lastmod>2025-07-03</lastmod>
<loc>https://dm.xueximeng.com/resource/187</loc>
<lastmod>2025-07-19</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/198</loc>
<lastmod>2025-07-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -546,12 +588,6 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/21</loc>
<lastmod>2025-06-28</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/25</loc>
<lastmod>2025-05-31</lastmod>
@@ -596,7 +632,7 @@
</url>
<url>
<loc>https://dm.xueximeng.com/resource/63</loc>
<lastmod>2025-06-21</lastmod>
<lastmod>2025-07-20</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -612,12 +648,6 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/70</loc>
<lastmod>2025-06-17</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/71</loc>
<lastmod>2025-06-16</lastmod>
@@ -626,7 +656,7 @@
</url>
<url>
<loc>https://dm.xueximeng.com/resource/72</loc>
<lastmod>2025-06-13</lastmod>
<lastmod>2025-07-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -662,7 +692,7 @@
</url>
<url>
<loc>https://dm.xueximeng.com/resource/96</loc>
<lastmod>2025-06-19</lastmod>
<lastmod>2025-07-26</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -782,7 +812,7 @@
</url>
<url>
<loc>https://dm.xueximeng.com/resource/132</loc>
<lastmod>2025-06-27</lastmod>
<lastmod>2025-07-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -836,13 +866,13 @@
</url>
<url>
<loc>https://dm.xueximeng.com/resource/151</loc>
<lastmod>2025-06-25</lastmod>
<lastmod>2025-07-24</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/152</loc>
<lastmod>2025-06-27</lastmod>
<lastmod>2025-07-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -918,12 +948,6 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/173</loc>
<lastmod>2025-07-17</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/175</loc>
<lastmod>2025-07-03</lastmod>
@@ -960,15 +984,9 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/187</loc>
<lastmod>2025-07-18</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/188</loc>
<lastmod>2025-07-18</lastmod>
<lastmod>2025-07-19</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
@@ -984,4 +1002,28 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/195</loc>
<lastmod>2025-07-26</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/197</loc>
<lastmod>2025-07-27</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/199</loc>
<lastmod>2025-08-07</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dm.xueximeng.com/resource/202</loc>
<lastmod>2025-08-02</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
</urlset>

View File

@@ -500,6 +500,22 @@ onMounted(() => {
checkAuthState();
// 监听登录成功事件
const handleLoginSuccess = () => {
checkAuthState();
};
window.addEventListener('login-success', handleLoginSuccess);
// 监听TMDB配置更新事件
const handleTmdbConfigUpdated = () => {
loadTMDBConfig();
};
window.addEventListener('tmdb-config-updated', handleTmdbConfigUpdated);
// 将事件处理器保存到组件实例,以便在卸载时移除
window.handleLoginSuccess = handleLoginSuccess;
window.handleTmdbConfigUpdated = handleTmdbConfigUpdated;
// 初始加载时设置meta信息
updateMetaInfo(route);
@@ -565,6 +581,18 @@ onUnmounted(() => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('beforeunload', clearPaginationStorage);
// 移除登录成功事件监听器
if (window.handleLoginSuccess) {
window.removeEventListener('login-success', window.handleLoginSuccess);
delete window.handleLoginSuccess;
}
// 移除TMDB配置更新事件监听器
if (window.handleTmdbConfigUpdated) {
window.removeEventListener('tmdb-config-updated', window.handleTmdbConfigUpdated);
delete window.handleTmdbConfigUpdated;
}
// 移除路由监听
if (routeWatcher && typeof routeWatcher === 'function') {
routeWatcher();

View File

@@ -25,7 +25,7 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, defineProps, defineEmits } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { getImageUrl } from '@/utils/imageUtils'
const props = defineProps({

View File

@@ -73,7 +73,7 @@
</template>
<script>
import { ref, computed, defineProps, defineEmits, watch, nextTick } from 'vue';
import { ref, computed, watch, nextTick } from 'vue';
export default {
name: 'EpisodeSelector',

View File

@@ -45,7 +45,7 @@
</template>
<script setup>
import { ref, watch, defineExpose, onMounted } from 'vue'
import { ref, watch, onMounted } from 'vue'
import QRCode from 'qrcode'
import { getImageUrl } from '@/utils/imageUtils'
import infoManager from '@/utils/InfoManager'

View File

@@ -32,7 +32,7 @@
</template>
<script setup>
import { ref, defineProps, defineEmits, onMounted } from 'vue'
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { getImageUrl } from '@/utils/imageUtils'

View File

@@ -273,10 +273,12 @@
}
.custom-table thead {
position: sticky; /* 设置表头为粘性定位 */
top: 0; /* 固定在容器顶部 */
z-index: 1; /* 确保表头在内容上层 */
background: rgba(255, 255, 255, 0.95); /* 确保表头背景不透明 */
position: sticky;
top: 0;
z-index: 10;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.custom-table th {
@@ -286,9 +288,29 @@
padding: 1rem;
text-align: left;
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
position: sticky; /* 确保每个th元素也保持粘性定位 */
top: 0; /* 固定在容器顶部 */
z-index: 2; /* 确保高于tbody内容 */
position: sticky;
top: 0;
z-index: 11;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
/* 移动端粘性表头优化 */
@media (max-width: 768px) {
.custom-table thead {
position: static; /* 移动端取消粘性定位 */
z-index: auto;
background: rgba(255, 255, 255, 0.95); /* 简化背景 */
backdrop-filter: none; /* 移除模糊效果 */
-webkit-backdrop-filter: none;
}
.custom-table th {
position: static; /* 移动端取消粘性定位 */
z-index: auto;
backdrop-filter: none; /* 移除模糊效果 */
-webkit-backdrop-filter: none;
}
}
.custom-table td {
@@ -364,7 +386,9 @@
.actions-cell {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-end;
}
.view-images-btn {
@@ -987,11 +1011,55 @@
}
}
/* @media (max-width: 768px) { */
@media (max-width: 1200px) {
/* 移动端基础样式 */
@media (max-width: 768px) {
/* 移动端基础样式和滚动优化 */
.admin-container {
padding: 0 0.5rem;
touch-action: manipulation; /* 允许滚动和缩放,但禁用双击缩放 */
-webkit-overflow-scrolling: touch; /* iOS平滑滚动 */
overflow-x: hidden; /* 防止水平滚动条 */
}
/* 确保body在移动端可以正常滚动 */
body {
touch-action: manipulation;
-webkit-overflow-scrolling: touch;
overflow-x: hidden;
}
/* 移动端优化卡片性能 */
.admin-card {
backdrop-filter: none; /* 移除模糊效果 */
-webkit-backdrop-filter: none;
background: rgba(255, 255, 255, 0.95); /* 使用更简单的背景 */
transform: none; /* 移除变换 */
transition: none; /* 移除动画 */
}
.admin-card:hover {
transform: none; /* 移除悬停效果 */
box-shadow:
0 10px 20px rgba(0, 0, 0, 0.08),
inset 0 -2px 6px rgba(255, 255, 255, 0.7),
inset 2px 2px 6px rgba(255, 255, 255, 1);
}
/* 移动端禁用按钮变换效果 */
.btn-custom:hover {
transform: none !important;
}
/* 移动端禁用其他悬停效果 */
.detail-section:hover,
.image-preview-item:hover {
transform: none !important;
}
/* 移动端模态框优化 */
.custom-modal {
backdrop-filter: none; /* 移除模糊效果 */
-webkit-backdrop-filter: none;
background: rgba(0, 0, 0, 0.7); /* 使用更简单的背景 */
}
.admin-hero {
@@ -1009,47 +1077,47 @@
.card-header {
padding: 1rem;
flex-wrap: nowrap; /* 强制不换行 */
gap: 10px;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: space-between;
align-items: center;
}
.card-header h4 {
font-size: 1.1rem;
max-width: 80%; /* 进一步增加宽度确保文字显示完整 */
overflow: visible; /* 确保文字不会被截断 */
white-space: nowrap; /* 不允许文字换行,保持在一行 */
text-overflow: ellipsis; /* 超出部分显示省略号 */
flex: 1;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.header-left h4 {
.header-actions {
flex-shrink: 0;
display: flex;
align-items: center;
flex-wrap: nowrap; /* 不允许换行 */
gap: 0.25rem; /* 减小图标和文字间的间距 */
}
.header-left h4 i {
margin-right: 0.25rem; /* 减少图标右侧边距 */
gap: 0.5rem;
}
.card-body {
padding: 1rem;
}
/* 表格样式 - 增强横向显示 */
.custom-table {
min-width: 1000px !important;
width: 100% !important;
white-space: nowrap !important;
border-collapse: collapse !important;
/* 表格容器优化 - 移除强制样式和高度限制 */
.table-container {
margin: 0;
width: 100%;
overflow-x: auto;
overflow-y: visible; /* 移动端不限制垂直滚动 */
max-height: none; /* 移除高度限制 */
padding: 0;
border-radius: var(--border-radius);
background: rgba(255, 255, 255, 0.5);
}
.table-container {
margin: 0 -1rem !important;
width: calc(100% + 2rem) !important;
overflow-x: auto !important;
padding: 0 0.5rem !important;
.custom-table {
min-width: 800px;
width: 100%;
white-space: nowrap;
}
.custom-table td,
@@ -1057,75 +1125,39 @@
padding: 0.75rem 0.5rem;
}
/* 按钮布局优化 */
.header-actions {
margin-left: auto;
white-space: nowrap;
/* 按钮样式优化 */
.btn-custom {
padding: 0.5rem;
min-width: 40px;
height: 40px;
border-radius: var(--border-radius);
display: inline-flex;
align-items: center;
justify-content: center;
}
/* 优化按钮在移动端的布局 */
.btn-custom.btn-sm {
padding: 0.4rem;
font-size: 0.8rem;
min-width: 36px;
height: 36px;
justify-content: center;
font-size: 0.85rem;
}
/* 改善下拉菜单在移动端的可用性 */
.dropdown-menu {
min-width: 200px;
}
/* 改善模态框在移动端的显示 */
.custom-modal .modal-dialog {
width: 95%;
max-width: none;
}
/* 修改表单在移动端的布局 */
.form-group {
margin-bottom: 1.25rem;
}
/* 修改密码按钮样式优化 */
.form-actions {
display: flex;
justify-content: center;
gap: 0.75rem;
}
.form-actions button {
width: auto;
border-radius: 50%;
min-width: 38px;
height: 38px;
padding: 0.5rem;
}
/* 移动端按钮仅显示图标,不显示文字 */
/* 移动端按钮文本控制 - 隐藏所有按钮文本 */
.btn-text {
display: none;
}
.btn-custom {
padding: 0.5rem;
min-width: 38px;
height: 38px;
justify-content: center;
}
.btn-custom i {
font-size: 1.1rem;
font-size: 1rem;
margin-right: 0;
}
/* 确保带有徽章的按钮能正常显示 */
/* 徽章样式调整 */
.btn-custom .badge-count {
display: inline-flex;
position: absolute;
top: -8px;
right: -8px;
margin-left: 0.25rem;
position: static;
min-width: 20px;
height: 20px;
border-radius: 50%;
@@ -1134,17 +1166,23 @@
justify-content: center;
}
/* 特殊处理某些需要文本的按钮 */
.close-btn .btn-text,
.confirmation-btn .btn-text {
display: inline-block;
/* 模态框优化 */
.custom-modal .modal-dialog {
width: 95%;
max-width: none;
margin: 1rem;
}
/* 批量删除按钮需要更多空间 */
.btn-custom.btn-accent.btn-sm {
position: relative;
min-width: 38px;
padding: 0.4rem;
/* 表单布局优化 */
.form-group {
margin-bottom: 1.25rem;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
flex-wrap: wrap;
}
}
@@ -1213,29 +1251,33 @@
/* 调整按钮大小和间距 */
.btn-custom {
padding: 0.45rem;
min-width: 34px;
height: 34px;
border-radius: 50%;
padding: 0.4rem;
min-width: 36px;
height: 36px;
border-radius: var(--border-radius);
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-custom.btn-sm {
padding: 0.35rem;
min-width: 30px;
height: 30px;
padding: 0.3rem;
min-width: 32px;
height: 32px;
}
.btn-custom i {
font-size: 1rem;
font-size: 0.9rem;
}
/* 垂直堆叠操作按钮 */
/* 操作按钮布局优化 */
.actions-cell {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
flex-wrap: nowrap;
gap: 0.4rem;
justify-content: flex-end;
align-items: center;
}
/* 优化弹窗提示和确认 */
@@ -1258,7 +1300,7 @@
}
.modal-footer .btn-custom .btn-text {
display: inline-block;
display: none;
}
}
@@ -1268,21 +1310,26 @@
display: inline-block;
}
/* 优化审批详情中查看公开页面按钮在移动端的样式 */
/* 审批详情模态框按钮优化 */
@media (max-width: 768px) {
.modal-actions .btn-custom {
padding: 0.5rem;
width: 42px;
height: 42px;
border-radius: 50%;
display: flex;
padding: 0.5rem 0.75rem;
min-width: auto;
height: auto;
border-radius: var(--border-radius);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
}
.modal-actions .btn-custom i {
margin: 0;
font-size: 1.25rem;
font-size: 1rem;
}
.modal-actions .btn-custom .btn-text {
display: none;
}
}
@@ -1668,6 +1715,24 @@
width: 100%;
}
/* 数据源列表移动端适配 */
.datasource-header {
display: grid;
grid-template-columns: 40px 1fr 1fr 80px 60px;
}
.datasource-item {
grid-template-columns: 40px 1fr 40px;
padding: 8px;
}
.datasource-fields {
display: grid;
grid-template-columns: 1fr 1fr 80px 1fr;
gap: 8px;
width: 100%;
}
/* 移除字段垂直堆叠样式 */
.link-field {
display: flex;
@@ -1730,47 +1795,57 @@
}
}
/* 添加更小屏幕的特殊处理 */
/* @media (max-width: 576px) { */
@media (max-width: 1200px) {
/* 移动端链接管理样式优化 */
@media (max-width: 768px) {
.links-container {
min-width: 650px; /* 确保最小宽度足够显示所有内容 */
min-width: 600px;
}
.remove-link-btn {
margin-top: 0; /* 移除按钮上方的额外边距 */
}
/* 优化移动端链接项内容显示 */
.link-fields {
display: grid;
grid-template-columns: 1fr 1fr 80px 1fr;
gap: 4px; /* 减小间距 */
gap: 0.5rem;
}
.link-field input {
width: 100%; /* 确保输入框宽度为100% */
min-width: 0; /* 防止输入框最小宽度导致溢出 */
font-size: 0.85rem; /* 减小字体大小 */
padding: 0.5rem 0.75rem; /* 减小内边距 */
text-overflow: ellipsis; /* 文本溢出时显示省略号 */
white-space: nowrap; /* 防止文本换行 */
overflow: hidden; /* 隐藏溢出内容 */
width: 100%;
min-width: 0;
font-size: 0.9rem;
padding: 0.5rem 0.75rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
/* 数据源列表768px断点优化 */
.datasource-fields {
display: grid;
grid-template-columns: 1fr 1fr 80px 1fr;
gap: 0.5rem;
}
.datasource-fields input {
width: 100%;
min-width: 0;
font-size: 0.9rem;
padding: 0.5rem 0.75rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
/* 修复图标按钮样式,确保居中显示不溢出 */
.icon-selector-button {
width: 36px; /* 减小图标选择按钮尺寸 */
width: 36px;
height: 36px;
min-width: 36px; /* 确保最小宽度 */
min-width: 36px;
padding: 0;
}
.icon-field {
display: flex;
justify-content: center;
min-width: 36px; /* 确保最小宽度 */
max-width: 80px; /* 限制最大宽度 */
min-width: 36px;
max-width: 80px;
}
}
@@ -2064,25 +2139,19 @@
min-width: 650px; /* 确保在移动端有足够宽度显示全部内容 */
}
/* 在所有设备上覆盖移动端样式 */
/* 添加链接按钮在移动端的特殊样式 */
@media (max-width: 768px) {
/* 移除其他按钮文本的隐藏样式 */
.btn-text {
display: none;
}
/* 但保持添加链接按钮文本显示 */
.add-link-btn {
height: auto !important;
width: auto !important;
display: inline-flex !important;
padding: 0.5rem 1rem !important;
height: auto;
width: auto;
display: inline-flex;
padding: 0.5rem 1rem;
min-width: 150px;
border-radius: var(--border-radius) !important;
border-radius: var(--border-radius);
}
.add-link-btn .btn-text {
display: inline !important;
display: inline;
}
}
@@ -2117,18 +2186,7 @@
margin-left: 0.35rem; /* 增加图标和文字间距 */
}
/* 在所有设备上覆盖移动端样式 */
@media (max-width: 768px) {
/* 但保持添加链接按钮文本显示并居中 */
.add-link-btn {
height: auto !important;
width: auto !important;
display: inline-flex !important;
padding: 0.5rem 1.5rem !important;
min-width: 150px;
border-radius: var(--border-radius) !important;
}
}
/* 表单帮助文本样式 */
.form-text {
@@ -2463,11 +2521,15 @@ textarea.custom-input {
}
.datasource-header {
display: flex;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid rgba(124, 58, 237, 0.1);
display: grid;
grid-template-columns: 40px 1fr 1fr 80px 60px;
gap: 8px;
padding: 0 10px;
margin-bottom: 10px;
font-weight: 600;
color: var(--primary-color);
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
padding-bottom: 8px;
}
.datasource-header-fields {
@@ -2793,14 +2855,23 @@ textarea.custom-input {
/* 数据源列表项样式 */
.datasource-item {
display: flex;
display: grid;
grid-template-columns: 40px 1fr 40px;
align-items: center;
background: rgba(255, 255, 255, 0.5);
border-radius: var(--border-radius);
padding: 8px;
margin-bottom: 10px;
border: 1px solid rgba(99, 102, 241, 0.1);
transition: all 0.3s ease;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.03);
}
.datasource-fields {
display: flex;
flex: 1;
align-items: center;
display: grid;
grid-template-columns: 1fr 1fr 80px 1fr;
gap: 8px;
width: 100%;
}
/* .actions-field {

View File

@@ -3111,6 +3111,13 @@ const saveTMDBSettings = async () => {
}
});
// 清除TMDB状态缓存确保前端状态立即更新
const TmdbStatusService = (await import('../services/TmdbStatusService')).default;
TmdbStatusService.clearCache();
// 通知App.vue重新加载TMDB状态
window.dispatchEvent(new Event('tmdb-config-updated'));
// 显示成功消息
tmdbSuccess.value = true;
setTimeout(() => {

View File

@@ -143,7 +143,12 @@ TMDB_API_KEY=your_tmdb_api_key # 此处可选,也可通过管理界面配置
### 运行
开发测试运行
开发测试运行默认为Release模式
```
go run cmd/api/main.go
```
开发调试运行启用Debug模式
```
GIN_MODE=debug go run cmd/api/main.go
```

View File

@@ -17,37 +17,17 @@ import (
"dongman/internal/handlers"
"dongman/internal/models"
"dongman/internal/config"
"dongman/internal/utils"
)
func main() {
// 设置日志
log.Println("启动动漫资源共享平台API服务...")
// 检查数据库文件
dbPath := utils.GetDbPath()
log.Printf("数据库文件路径: %s", dbPath)
dbPath := config.GetDbPath()
// 检查主数据库文件是否存在
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
log.Printf("警告: 数据库文件不存在,将创建新的数据库")
} else {
// 检查WAL文件是否存在
walPath := dbPath + "-wal"
if _, err := os.Stat(walPath); os.IsNotExist(err) {
log.Printf("注意: WAL文件不存在这可能是正常的如果是首次启动或数据库被正常关闭")
} else {
log.Printf("检测到WAL文件: %s", walPath)
}
// 检查SHM文件是否存在
shmPath := dbPath + "-shm"
if _, err := os.Stat(shmPath); os.IsNotExist(err) {
log.Printf("注意: SHM文件不存在")
} else {
log.Printf("检测到SHM文件: %s", shmPath)
}
}
// log.Printf("警告: 数据库文件不存在,将创建新的数据库")
}
// 初始化数据库
db, err := models.InitDB()
@@ -57,20 +37,15 @@ func main() {
// 不再使用defer db.Close(),我们会在收到信号时手动关闭
// 执行WAL检查点确保启动时数据已同步
log.Println("执行启动时数据库检查点...")
_, err = db.Exec("PRAGMA wal_checkpoint(RESTART);")
if err != nil {
log.Printf("启动时执行检查点失败: %v", err)
} else {
log.Println("启动时检查点执行成功")
}
}
// 设置WAL自动检查点阈值页数
_, err = db.Exec("PRAGMA wal_autocheckpoint=500;")
if err != nil {
log.Printf("设置WAL自动检查点阈值失败: %v", err)
} else {
log.Println("WAL自动检查点阈值设置成功")
}
// 创建初始管理员账号
@@ -83,6 +58,14 @@ func main() {
log.Printf("初始化网站设置失败: %v", err)
}
// 设置Gin模式默认为release模式除非明确设置为debug
ginMode := os.Getenv("GIN_MODE")
if ginMode == "" {
gin.SetMode(gin.ReleaseMode)
} else {
log.Printf("Gin模式: %s", ginMode)
}
// 创建Gin应用
router := gin.Default()

View File

@@ -1,382 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"dongman/internal/models"
)
func main() {
log.Println("开始数据库诊断...")
// 初始化数据库
db, err := models.InitDB()
if err != nil {
log.Fatalf("数据库初始化失败: %v", err)
}
defer db.Close()
// 检查数据库位置
workDir, err := os.Getwd()
if err != nil {
log.Fatalf("获取工作目录失败: %v", err)
}
log.Printf("当前工作目录: %s", workDir)
// 检查表结构
log.Println("\n===== 检查数据库表结构 =====")
pragmaRows, err := db.Query("PRAGMA table_info(resources)")
if err != nil {
log.Fatalf("查询表结构失败: %v", err)
}
defer pragmaRows.Close()
fmt.Println("资源表结构:")
fmt.Printf("%-4s %-20s %-10s %-8s %-10s\n", "序号", "字段名", "类型", "可空", "默认值")
fmt.Println(strings.Repeat("-", 60))
for pragmaRows.Next() {
var cid int
var name, type_ string
var notnull, dfltValue, pk interface{}
pragmaRows.Scan(&cid, &name, &type_, &notnull, &dfltValue, &pk)
fmt.Printf("%-4d %-20s %-10s %-8v %-10v\n", cid, name, type_, notnull, dfltValue)
}
fmt.Println()
// 检查资源数量
log.Println("\n===== 检查资源数量 =====")
var totalCount int
err = db.Get(&totalCount, "SELECT COUNT(*) FROM resources")
if err != nil {
log.Fatalf("查询资源总数失败: %v", err)
}
log.Printf("数据库中总共有 %d 条资源记录", totalCount)
// 检查资源状态分布
log.Println("\n===== 检查资源状态分布 =====")
rows, err := db.Query("SELECT status, COUNT(*) FROM resources GROUP BY status")
if err != nil {
log.Fatalf("查询资源状态分布失败: %v", err)
}
defer rows.Close()
for rows.Next() {
var status string
var count int
rows.Scan(&status, &count)
log.Printf("状态 [%s]: %d 条记录", status, count)
}
// 检查是否存在approved且非supplement的资源
log.Println("\n===== 检查已批准的非补充资源 =====")
var approvedCount int
err = db.Get(&approvedCount,
"SELECT COUNT(*) FROM resources WHERE status = ? AND is_supplement_approval = 0",
"APPROVED")
if err != nil {
log.Fatalf("查询已批准的非补充资源数量失败: %v", err)
}
log.Printf("已批准的非补充资源数量: %d", approvedCount)
// 显示几条样例资源
log.Println("\n===== 样例资源数据 =====")
var sampleResources []struct {
ID int `db:"id"`
Title string `db:"title"`
Status string `db:"status"`
IsSupplementApproval bool `db:"is_supplement_approval"`
LikesCount int `db:"likes_count"`
CreatedAt time.Time `db:"created_at"`
}
err = db.Select(&sampleResources, "SELECT id, title, status, is_supplement_approval, likes_count, created_at FROM resources LIMIT 3")
if err != nil {
log.Fatalf("查询样例资源失败: %v", err)
}
for i, resource := range sampleResources {
log.Printf("样例 #%d:", i+1)
log.Printf(" ID: %d", resource.ID)
log.Printf(" 标题: %s", resource.Title)
log.Printf(" 状态: %s", resource.Status)
log.Printf(" 是否补充批准: %v", resource.IsSupplementApproval)
log.Printf(" 点赞数: %d", resource.LikesCount)
log.Printf(" 创建时间: %v", resource.CreatedAt)
}
// 尝试执行前端请求的查询
log.Println("\n===== 测试前端查询 =====")
var frontendResources []struct {
ID int `db:"id"`
Title string `db:"title"`
LikesCount int `db:"likes_count"`
}
query := `SELECT id, title, likes_count FROM resources WHERE status = ? ORDER BY likes_count DESC LIMIT ? OFFSET ?`
args := []interface{}{"APPROVED", 4, 0}
err = db.Select(&frontendResources, query, args...)
if err != nil {
log.Fatalf("执行前端查询失败: %v", err)
}
log.Printf("前端查询找到 %d 条资源", len(frontendResources))
for i, resource := range frontendResources {
log.Printf("结果 #%d: [ID: %d] %s (点赞: %d)",
i+1, resource.ID, resource.Title, resource.LikesCount)
}
log.Println("\n===== 检查users表结构 =====")
userPragmaRows, err := db.Query("PRAGMA table_info(users)")
if err != nil {
log.Fatalf("查询users表结构失败: %v", err)
}
defer userPragmaRows.Close()
fmt.Println("用户表结构:")
fmt.Printf("%-4s %-20s %-10s %-8s %-10s\n", "序号", "字段名", "类型", "可空", "默认值")
fmt.Println(strings.Repeat("-", 60))
for userPragmaRows.Next() {
var cid int
var name, type_ string
var notnull, dfltValue, pk interface{}
userPragmaRows.Scan(&cid, &name, &type_, &notnull, &dfltValue, &pk)
fmt.Printf("%-4d %-20s %-10s %-8v %-10v\n", cid, name, type_, notnull, dfltValue)
}
fmt.Println()
// 检查是否有用户
log.Println("\n===== 检查用户数量 =====")
var userCount int
err = db.Get(&userCount, "SELECT COUNT(*) FROM users")
if err != nil {
log.Fatalf("查询用户总数失败: %v", err)
}
log.Printf("数据库中总共有 %d 个用户", userCount)
// 如果有用户,显示第一个用户信息(不显示密码)
if userCount > 0 {
var users []struct {
ID int `db:"id"`
Username string `db:"username"`
IsAdmin bool `db:"is_admin"`
CreatedAt time.Time `db:"created_at"`
}
err = db.Select(&users, "SELECT id, username, is_admin, created_at FROM users LIMIT 3")
if err != nil {
log.Fatalf("查询用户信息失败: %v", err)
}
for i, user := range users {
log.Printf("用户 #%d:", i+1)
log.Printf(" ID: %d", user.ID)
log.Printf(" 用户名: %s", user.Username)
log.Printf(" 是否管理员: %v", user.IsAdmin)
log.Printf(" 创建时间: %v", user.CreatedAt)
}
}
// 尝试获取admin用户信息
log.Println("\n===== 检查admin用户 =====")
// 分开获取admin用户信息和密码
var adminUser struct {
ID int `db:"id"`
Username string `db:"username"`
IsAdmin bool `db:"is_admin"`
CreatedAt time.Time `db:"created_at"`
}
err = db.Get(&adminUser, "SELECT id, username, is_admin, created_at FROM users WHERE username = ?", "admin")
if err != nil {
log.Printf("查询admin用户失败: %v", err)
} else {
// 单独获取密码哈希长度
var hashedPassword string
err = db.Get(&hashedPassword, "SELECT hashed_password FROM users WHERE username = ?", "admin")
if err != nil {
log.Printf("获取admin密码哈希失败: %v", err)
}
log.Printf("找到admin用户:")
log.Printf(" ID: %d", adminUser.ID)
log.Printf(" 用户名: %s", adminUser.Username)
log.Printf(" 密码哈希长度: %d", len(hashedPassword))
log.Printf(" 是否管理员: %v", adminUser.IsAdmin)
log.Printf(" 创建时间: %v", adminUser.CreatedAt)
}
// 查询前端请求的表单
log.Println("\n===== 前端登录请求格式参考 =====")
log.Printf(`请确保前端发送的登录请求格式如下:
{
"username": "admin",
"password": "admin123"
}
登录API地址: POST /api/auth/token`)
// 测试登录请求
log.Println("\n===== 测试登录API =====")
log.Printf("现在将使用默认账号 admin/admin123 测试登录API")
// 创建测试HTTP客户端
log.Printf("请确保API服务器正在运行测试将向 http://localhost:8000/api/auth/token 发送请求")
log.Printf("请求头: Content-Type: application/json")
log.Printf("请求体: {\"username\":\"admin\",\"password\":\"admin123\"}")
// 构建请求体
loginReq := struct {
Username string `json:"username"`
Password string `json:"password"`
}{
Username: "admin",
Password: "admin123",
}
loginReqJSON, err := json.Marshal(loginReq)
if err != nil {
log.Printf("序列化登录请求失败: %v", err)
} else {
// 发送HTTP请求
resp, err := http.Post(
"http://localhost:8000/api/auth/token",
"application/json",
bytes.NewBuffer(loginReqJSON),
)
if err != nil {
log.Printf("发送登录请求失败: %v", err)
log.Printf("这可能是因为API服务器未运行请确保服务器已启动")
} else {
defer resp.Body.Close()
// 读取响应
var respBody map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&respBody)
log.Printf("HTTP状态码: %d", resp.StatusCode)
if err != nil {
log.Printf("解析响应失败: %v", err)
// 尝试读取原始响应
buf := new(bytes.Buffer)
buf.ReadFrom(resp.Body)
log.Printf("原始响应: %s", buf.String())
} else {
log.Printf("响应体: %v", respBody)
if resp.StatusCode == 200 {
log.Printf("登录成功!获取到令牌")
} else {
log.Printf("登录失败,请检查用户名和密码")
}
}
}
}
log.Printf(`要在命令行测试登录,请执行以下命令:
curl -X POST http://localhost:8000/api/auth/token \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'`)
log.Printf(`如果前端使用fetch或axios发送请求请确保格式如下:
fetch('http://localhost:8000/api/auth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'admin',
password: 'admin123'
})
})`)
log.Println("\n诊断完成!")
}
func resetResourceData() error {
// 删除所有现有资源
_, err := models.DB.Exec("DELETE FROM resources")
if err != nil {
return fmt.Errorf("清空资源表失败: %w", err)
}
// 重置自增ID
_, err = models.DB.Exec("DELETE FROM sqlite_sequence WHERE name='resources'")
if err != nil {
log.Printf("重置资源表自增ID失败 (非致命错误): %v", err)
}
// 创建新的示例数据
return createSampleResources()
}
func createSampleResources() error {
sampleResources := []models.Resource{
{
Title: "进击的巨人",
TitleEn: "Attack on Titan",
Description: "人类与巨人的生存之战",
ResourceType: "anime",
Status: models.ResourceStatusApproved,
Images: []string{"/assets/imgs/1/sample1.jpg"},
Links: models.JsonMap{"官网": "https://example.com/aot"},
LikesCount: 120,
IsSupplementApproval: false,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
{
Title: "鬼灭之刃",
TitleEn: "Demon Slayer",
Description: "少年与恶魔的战斗故事",
ResourceType: "anime",
Status: models.ResourceStatusApproved,
Images: []string{"/assets/imgs/2/sample2.jpg"},
Links: models.JsonMap{"官网": "https://example.com/ds"},
LikesCount: 150,
IsSupplementApproval: false,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
{
Title: "海贼王",
TitleEn: "One Piece",
Description: "寻找传说中的大秘宝「ONE PIECE」的海洋冒险故事",
ResourceType: "anime",
Status: models.ResourceStatusApproved,
Images: []string{"/assets/imgs/3/sample3.jpg"},
Links: models.JsonMap{"官网": "https://example.com/op"},
LikesCount: 200,
IsSupplementApproval: false,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
}
// 插入示例数据
for _, resource := range sampleResources {
_, err := models.DB.Exec(
`INSERT INTO resources (
title, title_en, description, resource_type, images, links,
status, likes_count, is_supplement_approval, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
resource.Title, resource.TitleEn, resource.Description, resource.ResourceType,
resource.Images, resource.Links, resource.Status, resource.LikesCount,
resource.IsSupplementApproval, resource.CreatedAt, resource.UpdatedAt,
)
if err != nil {
return fmt.Errorf("插入示例资源失败: %w", err)
}
}
return nil
}

View File

@@ -1,118 +0,0 @@
package main
import (
"database/sql"
"fmt"
"log"
"os"
"path/filepath"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// 获取当前工作目录
wd, err := os.Getwd()
if err != nil {
log.Fatalf("获取工作目录失败: %v", err)
}
log.Printf("当前工作目录: %s", wd)
// 构建数据库路径 - 尝试在多个位置查找
possiblePaths := []string{
"./resource_hub.db", // 当前目录
"../../resource_hub.db", // 项目根目录
filepath.Join(wd, "resource_hub.db"), // 绝对路径
filepath.Join(wd, "../../resource_hub.db"), // 绝对路径到项目根目录
}
var db *sql.DB
var dbPath string
// 尝试打开数据库
for _, path := range possiblePaths {
log.Printf("尝试打开数据库: %s", path)
if _, err := os.Stat(path); err == nil {
log.Printf("找到数据库文件: %s", path)
db, err = sql.Open("sqlite3", path)
if err == nil {
dbPath = path
log.Printf("成功打开数据库: %s", path)
break
} else {
log.Printf("打开数据库失败: %v", err)
}
} else {
log.Printf("数据库文件不存在: %s", path)
}
}
if db == nil {
log.Fatalf("未能找到或打开任何数据库文件")
}
log.Printf("使用数据库文件: %s", dbPath)
// 测试数据库连接
if err := db.Ping(); err != nil {
log.Fatalf("数据库连接测试失败: %v", err)
}
log.Printf("数据库连接测试成功")
// 检查资源表结构
log.Println("检查资源表结构:")
rows, err := db.Query("PRAGMA table_info(resources)")
if err != nil {
log.Fatalf("查询表结构失败: %v", err)
}
defer rows.Close()
var (
cid int
name string
dataType string
notNull int
dfltValue interface{}
primaryKey int
)
for rows.Next() {
if err := rows.Scan(&cid, &name, &dataType, &notNull, &dfltValue, &primaryKey); err != nil {
log.Fatalf("扫描表结构数据失败: %v", err)
}
log.Printf("列: %s, 类型: %s, 非空: %d, 默认值: %v, 主键: %d", name, dataType, notNull, dfltValue, primaryKey)
}
// 统计资源数量
var count int
if err := db.QueryRow("SELECT COUNT(*) FROM resources").Scan(&count); err != nil {
log.Fatalf("统计资源失败: %v", err)
}
log.Printf("资源总数: %d", count)
// 查询已批准的资源数量
if err := db.QueryRow("SELECT COUNT(*) FROM resources WHERE status = 'APPROVED'").Scan(&count); err != nil {
log.Fatalf("统计已批准资源失败: %v", err)
}
log.Printf("已批准资源数: %d", count)
// 检查ID为1的资源记录
log.Println("查询ID为1的资源记录:")
rows, err = db.Query("SELECT id, title, title_en, description, resource_type, status FROM resources WHERE id = 1")
if err != nil {
log.Fatalf("查询资源失败: %v", err)
}
defer rows.Close()
for rows.Next() {
var id int
var title, titleEn, description, resourceType, status string
if err := rows.Scan(&id, &title, &titleEn, &description, &resourceType, &status); err != nil {
log.Fatalf("扫描资源数据失败: %v", err)
}
log.Printf("资源ID: %d, 标题: %s, 英文标题: %s, 类型: %s, 状态: %s", id, title, titleEn, resourceType, status)
log.Printf("描述: %s", description)
}
fmt.Println("SQLite 数据库测试完成")
}

View File

@@ -1,85 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"dongman/internal/models"
"dongman/internal/utils"
)
func main() {
// 初始化数据库连接以确保可以正常使用utils包
db, err := models.InitDB()
if err != nil {
log.Fatalf("数据库初始化失败: %v", err)
}
defer db.Close()
log.Println("WebP转换工具测试")
// 示例图片路径 - 如果该路径不存在,请替换为实际存在的图片路径
testImgPath := "imgs/1/test.jpg"
// 获取工作目录构建assets目录路径
workDir, err := os.Getwd()
if err != nil {
log.Fatalf("获取工作目录失败: %v", err)
}
// 构建测试目录
assetsDir := filepath.Join(workDir, "..", "..", "assets")
testDir := filepath.Join(assetsDir, "imgs", "1")
// 确保测试目录存在
if err := os.MkdirAll(testDir, 0755); err != nil {
log.Fatalf("创建测试目录失败: %v", err)
}
// 将测试图片从实例目录复制到测试目录 - 请确保原图片存在
srcImagePath := filepath.Join(workDir, "..", "..", "assets", "uploads")
files, err := filepath.Glob(filepath.Join(srcImagePath, "*", "*.jpg"))
if err != nil || len(files) == 0 {
log.Fatalf("未找到测试图片: %v", err)
}
// 使用找到的第一张图片
testSrcPath := files[0]
testDstPath := filepath.Join(testDir, "test.jpg")
// 复制测试图片
if err := utils.CopyFile(testSrcPath, testDstPath); err != nil {
log.Fatalf("复制测试图片失败: %v", err)
}
log.Printf("已复制测试图片: %s -> %s", testSrcPath, testDstPath)
// 测试固定尺寸的转换
log.Println("测试1: 固定尺寸转换(600x900)")
webpPath, err := utils.ConvertToWebP(testImgPath)
if err != nil {
log.Fatalf("WebP转换失败: %v", err)
}
log.Printf("转换成功! WebP文件路径: %s", webpPath)
// 测试保持宽高比的转换
log.Println("测试2: 保持宽高比转换(最大600x900)")
webpRatioPath, err := utils.ConvertToWebPWithRatio(testImgPath, 600, 900, true)
if err != nil {
log.Fatalf("保持宽高比的WebP转换失败: %v", err)
}
log.Printf("转换成功! 保持宽高比的WebP文件路径: %s", webpRatioPath)
// 测试完整路径转换
log.Println("测试3: 完整路径转换")
fullPath := filepath.Join(assetsDir, testImgPath)
webpFullPath, err := utils.ConvertToWebPFromPath(fullPath)
if err != nil {
log.Fatalf("完整路径的WebP转换失败: %v", err)
}
log.Printf("转换成功! 完整路径的WebP文件: %s", webpFullPath)
fmt.Println("所有测试完成!")
}

View File

@@ -8,7 +8,7 @@ import (
"path/filepath"
"dongman/internal/models"
"dongman/internal/utils"
"dongman/internal/config"
)
func main() {

View File

@@ -6,13 +6,37 @@ import (
"path/filepath"
)
var AssetPath string
var (
// 全局配置变量
DbPath string
AssetsDir string
AssetPath string // 保留原有变量以保持兼容性
)
// 初始化配置
func init() {
// 优先读取环境变量指定的资源目录
// 初始化数据库路径
if envPath := os.Getenv("DB_PATH"); envPath != "" {
DbPath = envPath
log.Printf("使用环境变量指定的数据库路径: %s", DbPath)
} else {
// 获取当前工作目录
workDir, err := os.Getwd()
if err != nil {
log.Printf("获取工作目录失败: %v使用默认路径", err)
workDir = "."
}
// 使用默认数据库文件路径
DbPath = filepath.Join(workDir, "resource_hub.db")
log.Printf("使用默认数据库路径: %s", DbPath)
}
// 初始化资源目录
if envPath := os.Getenv("ASSETS_PATH"); envPath != "" {
AssetPath = envPath
log.Printf("使用环境变量指定的资源目录: %s", AssetPath)
AssetsDir = envPath
AssetPath = envPath // 保持兼容性
log.Printf("使用环境变量指定的资源目录: %s", AssetsDir)
} else {
// 获取当前工作目录
workDir, err := os.Getwd()
@@ -22,22 +46,32 @@ func init() {
}
// 使用默认资源目录路径
AssetPath = filepath.Join(workDir, "..", "assets")
log.Printf("使用默认资源目录: %s", AssetPath)
AssetsDir = filepath.Join(workDir, "..", "assets")
AssetPath = AssetsDir // 保持兼容性
log.Printf("使用默认资源目录: %s", AssetsDir)
}
// 确保目录存在
ensureDirExists(filepath.Dir(DbPath))
ensureDirExists(AssetsDir)
ensureDirExists(filepath.Join(AssetsDir, "uploads"))
ensureDirExists(filepath.Join(AssetsDir, "imgs"))
ensureDirExists(filepath.Join(AssetsDir, "public"))
}
// 如果目录存在,尝试创建
if _, err := os.Stat(AssetPath); os.IsNotExist(err) {
if err := os.MkdirAll(AssetPath, 0755); err != nil {
log.Printf("无法创建资源目录: %v", err)
}
// 确保目录存在
func ensureDirExists(dir string) {
if err := os.MkdirAll(dir, 0755); err != nil {
log.Printf("创建目录失败 %s: %v", dir, err)
}
}
// 确保public子目录存在
publicDir := filepath.Join(AssetPath, "public")
if _, err := os.Stat(publicDir); os.IsNotExist(err) {
if err := os.MkdirAll(publicDir, 0755); err != nil {
log.Printf("无法创建public目录: %v", err)
}
}
// GetAssetsDir 获取资源目录路径
func GetAssetsDir() string {
return AssetsDir
}
// GetDbPath 获取数据库路径
func GetDbPath() string {
return DbPath
}

View File

@@ -74,8 +74,6 @@ func Login(c *gin.Context) {
// GetCurrentUserInfo 获取当前用户信息
func GetCurrentUserInfo(c *gin.Context) {
log.Printf("获取当前用户信息, Authorization: %s", c.GetHeader("Authorization"))
user, err := GetCurrentUser(c)
if err != nil {
log.Printf("获取用户信息失败: %v", err)

View File

@@ -1,8 +1,8 @@
package handlers
import (
"dongman/internal/utils"
"dongman/internal/config"
"dongman/internal/utils"
"fmt"
"io"
"net/http"

View File

@@ -16,7 +16,6 @@ func JWTAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取Authorization头
authHeader := c.GetHeader("Authorization")
log.Printf("Authorization头: %s", authHeader)
if authHeader == "" {
log.Printf("缺少Authorization头")
@@ -46,7 +45,7 @@ func JWTAuthMiddleware() gin.HandlerFunc {
return
}
log.Printf("令牌验证成功,用户: %s, 是否管理员: %v", claims.Username, claims.IsAdmin)
// log.Printf("令牌验证成功,用户: %s, 是否管理员: %v", claims.Username, claims.IsAdmin)
// 存储用户信息到上下文
c.Set("username", claims.Username)

View File

@@ -2,6 +2,7 @@ package handlers
import (
"dongman/internal/models"
"dongman/internal/config"
"dongman/internal/utils"
"encoding/json"
"fmt"
@@ -88,7 +89,7 @@ func ApproveResource(c *gin.Context) {
log.Printf("[DEBUG] 原始图片路径: %v", approval.ApprovedImages)
// 获取assets目录路径
assetsDir := utils.GetAssetsDir()
assetsDir := config.GetAssetsDir()
log.Printf("[DEBUG] Assets目录路径: %s", assetsDir)
// 创建目标目录
@@ -486,7 +487,7 @@ func approveResourceSupplement(c *gin.Context, resourceID int, resource models.R
log.Printf("[DEBUG] 原始图片路径: %v", approval.ApprovedImages)
// 获取assets目录路径
assetsDir := utils.GetAssetsDir()
assetsDir := config.GetAssetsDir()
log.Printf("[DEBUG] Assets目录路径: %s", assetsDir)
// 创建目标目录

View File

@@ -226,8 +226,8 @@ func GetPublicResources(c *gin.Context) {
}
// 打印请求参数用于调试
log.Printf("获取公开资源skip=%d, limit=%d, search=%s, sortBy=%s, sortOrder=%s, countOnly=%v",
params.Skip, params.Limit, params.Search, params.SortBy, params.SortOrder, params.CountOnly)
// log.Printf("获取公开资源skip=%d, limit=%d, search=%s, sortBy=%s, sortOrder=%s, countOnly=%v",
// params.Skip, params.Limit, params.Search, params.SortBy, params.SortOrder, params.CountOnly)
// 设置默认排序
if params.SortBy == "" {
@@ -255,7 +255,6 @@ func GetPublicResources(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "计算资源总数失败"})
return
}
log.Printf("符合条件的资源总数: %d", count)
// 如果只需要计数,直接返回
if params.CountOnly {
@@ -265,7 +264,7 @@ func GetPublicResources(c *gin.Context) {
// 如果没有记录,返回空数组
if count == 0 {
log.Printf("数据库中没有符合条件的记录")
// log.Printf("数据库中没有符合条件的记录")
c.JSON(http.StatusOK, []interface{}{})
return
}
@@ -332,7 +331,7 @@ func GetPublicResources(c *gin.Context) {
resources[0].TotalCount = &count
}
log.Printf("查询成功,返回 %d 条记录", len(resources))
// log.Printf("查询成功,返回 %d 条记录", len(resources))
// 返回结果
c.JSON(http.StatusOK, resources)
@@ -347,8 +346,6 @@ func GetResourceByID(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的资源ID"})
return
}
log.Printf("尝试获取资源ID: %d", resourceID)
isAdminView := c.DefaultQuery("is_admin_view", "false") == "true"
@@ -368,7 +365,6 @@ func GetResourceByID(c *gin.Context) {
return
}
log.Printf("资源ID: %d 存在性检查结果: %d", resourceID, count)
if count == 0 {
log.Printf("资源ID: %d 不存在", resourceID)
@@ -378,7 +374,7 @@ func GetResourceByID(c *gin.Context) {
// 查询资源
var resource models.Resource
log.Printf("执行查询: SELECT * FROM resources WHERE id = %d", resourceID)
// log.Printf("执行查询: SELECT * FROM resources WHERE id = %d", resourceID)
err = models.DB.Get(&resource, `SELECT * FROM resources WHERE id = ?`, resourceID)
if err != nil {
log.Printf("查询资源ID: %d 失败: %v", resourceID, err)
@@ -386,7 +382,7 @@ func GetResourceByID(c *gin.Context) {
return
}
log.Printf("成功获取资源ID: %d, 标题: %s, 状态: %s", resource.ID, resource.Title, resource.Status)
// log.Printf("成功获取资源ID: %d, 标题: %s, 状态: %s", resource.ID, resource.Title, resource.Status)
// 只有在公开页面访问时(非管理页面),才重定向补充审批记录到原始资源
if !isAdminView && resource.IsSupplementApproval && resource.OriginalResourceID != nil {

View File

@@ -397,7 +397,6 @@ func LoadTMDBConfig() {
return
}
log.Printf("未找到TMDB API密钥配置将使用默认值")
}
// GetTMDBStatus 获取TMDB功能是否启用只返回enabled状态不返回API密钥

View File

@@ -8,8 +8,8 @@ import (
"github.com/gin-gonic/gin"
"dongman/internal/utils"
"dongman/internal/models"
"dongman/internal/utils"
)
// GetTMDBSeasons 获取动漫季节信息

View File

@@ -10,7 +10,7 @@ import (
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"golang.org/x/crypto/bcrypt"
"dongman/internal/utils"
"dongman/internal/config"
)
// DB 是全局数据库连接
@@ -37,6 +37,8 @@ CREATE TABLE IF NOT EXISTS resources (
is_supplement_approval BOOLEAN DEFAULT 'False',
likes_count INTEGER DEFAULT '0' NOT NULL,
tmdb_id INTEGER,
stickers TEXT DEFAULT '{}' NOT NULL,
media_type VARCHAR,
PRIMARY KEY (id)
);
@@ -44,6 +46,7 @@ CREATE INDEX IF NOT EXISTS ix_resources_id ON resources (id);
CREATE INDEX IF NOT EXISTS ix_resources_title ON resources (title);
CREATE INDEX IF NOT EXISTS ix_resources_title_en ON resources (title_en);
CREATE INDEX IF NOT EXISTS ix_resources_tmdb_id ON resources (tmdb_id);
CREATE INDEX IF NOT EXISTS ix_resources_media_type ON resources (media_type);
CREATE TABLE IF NOT EXISTS approval_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -88,8 +91,8 @@ CREATE INDEX IF NOT EXISTS idx_site_settings_key ON site_settings(setting_key);
// InitDB 初始化数据库连接
func InitDB() (*sqlx.DB, error) {
// 从utils包获取数据库路径
dbPath := utils.GetDbPath()
log.Printf("连接数据库: %s", dbPath)
dbPath := config.GetDbPath()
// 连接SQLite数据库使用WAL模式并添加额外的健壮性参数
// _journal=WAL: 使用WAL模式
@@ -108,87 +111,10 @@ func InitDB() (*sqlx.DB, error) {
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(time.Minute * 30)
// 分步执行初始化表结构,避免一次性执行可能导致的错误
// 1. 创建resources表不包含media_type字段该字段将通过迁移添加
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS resources (
id INTEGER NOT NULL,
title VARCHAR,
title_en VARCHAR,
description TEXT,
images JSON,
poster_image VARCHAR,
resource_type VARCHAR,
status VARCHAR(8),
hidden_from_admin BOOLEAN,
created_at DATETIME,
updated_at DATETIME,
links JSON,
original_resource_id INTEGER,
supplement JSON,
approval_history JSON,
is_supplement_approval BOOLEAN DEFAULT 'False',
likes_count INTEGER DEFAULT '0' NOT NULL,
PRIMARY KEY (id)
);
`)
// 执行初始化表结构
_, err = db.Exec(initSQL)
if err != nil {
return nil, fmt.Errorf("创建resources表失败: %w", err)
}
// 2. 创建基本索引不包含media_type的索引该索引将在迁移中创建
_, err = db.Exec(`
CREATE INDEX IF NOT EXISTS ix_resources_id ON resources (id);
CREATE INDEX IF NOT EXISTS ix_resources_title ON resources (title);
CREATE INDEX IF NOT EXISTS ix_resources_title_en ON resources (title_en);
`)
if err != nil {
return nil, fmt.Errorf("创建resources表索引失败: %w", err)
}
// 3. 创建其他表
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS approval_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
resource_id INTEGER NOT NULL,
status VARCHAR(8) NOT NULL,
field_approvals JSON,
field_rejections JSON,
approved_images JSON,
rejected_images JSON,
poster_image VARCHAR,
notes TEXT,
approved_links JSON,
rejected_links JSON,
is_supplement_approval BOOLEAN DEFAULT 'False',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_approval_records_resource_id ON approval_records(resource_id);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
hashed_password TEXT NOT NULL,
is_admin BOOLEAN DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE TABLE IF NOT EXISTS site_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
setting_key TEXT NOT NULL UNIQUE,
setting_value JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_site_settings_key ON site_settings(setting_key);
`)
if err != nil {
return nil, fmt.Errorf("创建其他表失败: %w", err)
return nil, fmt.Errorf("创建数据库表失败: %w", err)
}
// 设置自定义类型映射
@@ -197,45 +123,13 @@ func InitDB() (*sqlx.DB, error) {
// 保存全局数据库连接
DB = db
// 执行所有数据库迁移
if err := MigrateDatabase(); err != nil {
return nil, fmt.Errorf("数据库迁移失败: %w", err)
}
// 启动定期检查点执行
go PerformPeriodicCheckpoints()
return db, nil
}
// MigrateDatabase 执行所有数据库迁移
func MigrateDatabase() error {
log.Println("开始运行数据库迁移...")
// 按顺序执行所有迁移
migrations := []struct {
name string
fn func() error
}{
{"添加tmdb_id列", AddTmdbIDColumn},
{"添加stickers列", AddStickersColumn},
{"添加media_type列", AddMediaTypeColumn},
// 未来可以在这里添加更多迁移
}
// 执行所有迁移
for _, migration := range migrations {
log.Printf("执行迁移: %s", migration.name)
if err := migration.fn(); err != nil {
log.Printf("迁移失败 [%s]: %v", migration.name, err)
return err
}
log.Printf("迁移成功: %s", migration.name)
}
log.Println("所有迁移已成功完成")
return nil
}
// GetDB 获取数据库连接
func GetDB() *sqlx.DB {
@@ -282,21 +176,14 @@ func CreateInitialAdmin() error {
// RestoreImagesPath 检查并恢复图片路径
func RestoreImagesPath() error {
// 查询所有资源明确指定列名排除tmdb_id字段
// 查询所有资源
resources := []Resource{}
if err := DB.Select(&resources, `
SELECT
id, title, title_en, description, images, poster_image,
resource_type, status, hidden_from_admin, created_at, updated_at,
links, original_resource_id, supplement, approval_history,
is_supplement_approval, likes_count
FROM resources
`); err != nil {
if err := DB.Select(&resources, `SELECT * FROM resources`); err != nil {
return fmt.Errorf("查询资源失败: %w", err)
}
// 获取资源目录
assetsDir := utils.GetAssetsDir()
assetsDir := config.GetAssetsDir()
log.Printf("资源目录: %s", assetsDir)
// 扫描所有图片文件
@@ -414,17 +301,9 @@ func isValidJson(data []byte) bool {
func ConvertJsonFieldsToText() error {
log.Printf("开始修复数据库中的JSON字段...")
// 查询所有资源明确指定列名排除tmdb_id字段
// 查询所有资源
var resources []Resource
err := DB.Select(&resources, `
SELECT
id, title, title_en, description, images, poster_image,
resource_type, status, hidden_from_admin, created_at, updated_at,
links, original_resource_id, supplement, approval_history,
is_supplement_approval, likes_count
FROM resources
`)
if err != nil {
if err := DB.Select(&resources, `SELECT * FROM resources`); err != nil {
return fmt.Errorf("查询资源失败: %w", err)
}
@@ -468,7 +347,6 @@ func InitSiteSettings() error {
// 如果已经有info设置不进行覆盖
if count == 0 {
log.Printf("未检测到网站基本信息设置,创建默认设置...")
// 默认的页脚设置
footerSettings := JsonMap{
"links": []map[string]interface{}{
@@ -508,11 +386,7 @@ func InitSiteSettings() error {
if err != nil {
return fmt.Errorf("保存info设置失败: %w", err)
}
log.Printf("网站设置初始化完成")
} else {
log.Printf("检测到已有网站设置,跳过初始化")
}
}
return nil
}
@@ -565,133 +439,6 @@ func PerformPeriodicCheckpoints() {
}
}
// AddTmdbIDColumn 向resources表添加tmdb_id字段
func AddTmdbIDColumn() error {
log.Printf("检查resources表是否需要添加tmdb_id字段...")
// 先检查resources表是否存在
var tableExists int
err := DB.Get(&tableExists, `SELECT count(*) FROM sqlite_master WHERE type='table' AND name='resources'`)
if err != nil {
return fmt.Errorf("检查resources表是否存在失败: %w", err)
}
if tableExists == 0 {
log.Printf("resources表不存在无需添加tmdb_id字段")
return nil
}
// 检查tmdb_id字段是否已存在
var count int
err = DB.Get(&count, `SELECT COUNT(*) FROM pragma_table_info('resources') WHERE name = 'tmdb_id'`)
if err != nil {
return fmt.Errorf("检查tmdb_id字段是否存在失败: %w", err)
}
// 如果字段不存在,则添加
if count == 0 {
log.Printf("tmdb_id字段不存在正在添加...")
_, err = DB.Exec(`ALTER TABLE resources ADD COLUMN tmdb_id INTEGER`)
if err != nil {
return fmt.Errorf("添加tmdb_id字段失败: %w", err)
}
// 创建索引
_, err = DB.Exec(`CREATE INDEX IF NOT EXISTS ix_resources_tmdb_id ON resources (tmdb_id)`)
if err != nil {
return fmt.Errorf("创建tmdb_id索引失败: %w", err)
}
log.Printf("tmdb_id字段添加成功")
}
return nil
}
// AddStickersColumn 添加stickers字段到resources表
func AddStickersColumn() error {
log.Printf("检查resources表是否需要添加stickers字段...")
// 先检查resources表是否存在
var tableExists int
err := DB.Get(&tableExists, `SELECT count(*) FROM sqlite_master WHERE type='table' AND name='resources'`)
if err != nil {
return fmt.Errorf("检查resources表是否存在失败: %w", err)
}
if tableExists == 0 {
log.Printf("resources表不存在无需添加stickers字段")
return nil
}
// 检查stickers字段是否已存在
var count int
err = DB.Get(&count, `SELECT COUNT(*) FROM pragma_table_info('resources') WHERE name = 'stickers'`)
if err != nil {
return fmt.Errorf("检查stickers字段是否存在失败: %w", err)
}
// 如果字段不存在,则添加
if count == 0 {
log.Printf("stickers字段不存在正在添加...")
_, err = DB.Exec(`ALTER TABLE resources ADD COLUMN stickers TEXT DEFAULT '{}' NOT NULL`)
if err != nil {
return fmt.Errorf("添加stickers字段失败: %w", err)
}
log.Printf("stickers字段添加成功")
} else {
log.Printf("stickers字段已存在无需添加")
}
return nil
}
// AddMediaTypeColumn 添加media_type字段到resources表
func AddMediaTypeColumn() error {
log.Printf("检查resources表是否需要添加media_type字段...")
// 先检查resources表是否存在
var tableExists int
err := DB.Get(&tableExists, `SELECT count(*) FROM sqlite_master WHERE type='table' AND name='resources'`)
if err != nil {
return fmt.Errorf("检查resources表是否存在失败: %w", err)
}
if tableExists == 0 {
log.Printf("resources表不存在无需添加media_type字段")
return nil
}
// 检查media_type字段是否已存在
var count int
err = DB.Get(&count, `SELECT COUNT(*) FROM pragma_table_info('resources') WHERE name = 'media_type'`)
if err != nil {
return fmt.Errorf("检查media_type字段是否存在失败: %w", err)
}
// 如果字段不存在,则添加
if count == 0 {
log.Printf("media_type字段不存在正在添加...")
_, err = DB.Exec(`ALTER TABLE resources ADD COLUMN media_type VARCHAR`)
if err != nil {
return fmt.Errorf("添加media_type字段失败: %w", err)
}
// 创建索引
_, err = DB.Exec(`CREATE INDEX IF NOT EXISTS ix_resources_media_type ON resources (media_type)`)
if err != nil {
return fmt.Errorf("创建media_type索引失败: %w", err)
}
log.Printf("media_type字段添加成功")
} else {
log.Printf("media_type字段已存在无需添加")
}
return nil
}
// UpdateResourceWithStickers 更新资源并支持贴纸数据
func UpdateResourceWithStickers(resource *Resource) error {
// 更新时间戳

View File

@@ -1,73 +0,0 @@
package utils
import (
"log"
"os"
"path/filepath"
)
var (
// 全局配置变量
DbPath string
AssetsDir string
)
// 初始化配置
func init() {
// 初始化数据库路径
if envPath := os.Getenv("DB_PATH"); envPath != "" {
DbPath = envPath
log.Printf("使用环境变量指定的数据库路径: %s", DbPath)
} else {
// 获取当前工作目录
workDir, err := os.Getwd()
if err != nil {
log.Printf("获取工作目录失败: %v使用默认路径", err)
workDir = "."
}
// 使用默认数据库文件路径
DbPath = filepath.Join(workDir, "resource_hub.db")
log.Printf("使用默认数据库路径: %s", DbPath)
}
// 初始化资源目录
if envPath := os.Getenv("ASSETS_PATH"); envPath != "" {
AssetsDir = envPath
log.Printf("使用环境变量指定的资源目录: %s", AssetsDir)
} else {
// 获取当前工作目录
workDir, err := os.Getwd()
if err != nil {
log.Printf("获取工作目录失败: %v使用默认路径", err)
workDir = "."
}
// 使用默认资源目录路径
AssetsDir = filepath.Join(workDir, "..", "assets")
log.Printf("使用默认资源目录: %s", AssetsDir)
}
// 确保目录存在
ensureDirExists(filepath.Dir(DbPath))
ensureDirExists(AssetsDir)
ensureDirExists(filepath.Join(AssetsDir, "uploads"))
ensureDirExists(filepath.Join(AssetsDir, "imgs"))
}
// 确保目录存在
func ensureDirExists(dir string) {
if err := os.MkdirAll(dir, 0755); err != nil {
log.Printf("创建目录失败 %s: %v", dir, err)
}
}
// GetAssetsDir 获取资源目录路径
func GetAssetsDir() string {
return AssetsDir
}
// GetDbPath 获取数据库路径
func GetDbPath() string {
return DbPath
}

View File

@@ -11,6 +11,7 @@ import (
"time"
"github.com/google/uuid"
"dongman/internal/config"
)
// CalculateFileHash 计算文件内容的SHA-256哈希值并将文件指针重置到开头
@@ -78,7 +79,7 @@ func MoveApprovedImages(resourceID int, imagePaths []string) ([]string, error) {
}
// 获取资源目录
assetsDir := GetAssetsDir()
assetsDir := config.GetAssetsDir()
// 创建目标目录
imgsDir := filepath.Join(assetsDir, "imgs", fmt.Sprintf("%d", resourceID))
@@ -133,7 +134,7 @@ func MoveApprovedImage(resourceID int, imagePath string) (string, error) {
}
// 获取资源目录
assetsDir := GetAssetsDir()
assetsDir := config.GetAssetsDir()
// 创建目标目录
imgsDir := filepath.Join(assetsDir, "imgs", fmt.Sprintf("%d", resourceID))
@@ -216,7 +217,7 @@ func moveFile(src, dst string) error {
// ensureUploadDir 确保上传目录存在
func ensureUploadDir() (string, error) {
// 获取资源目录
assetsDir := GetAssetsDir()
assetsDir := config.GetAssetsDir()
// 按日期创建上传目录
dateDir := time.Now().Format("20060102")

View File

@@ -1,85 +0,0 @@
# 动漫资源站点地图生成器
一个用Go语言编写的动漫资源网站站点地图(sitemap.xml)生成工具替代原有的Node.js实现。
## 功能特性
- 生成符合标准的sitemap.xml文件
- 支持从API获取资源数据
- 支持分页和并发请求处理大量数据
- 自动处理多种API响应格式
- 支持生成分割站点地图和站点地图索引
- 完全兼容原始Node.js实现的功能
## 安装
### 从源码编译
```bash
# 克隆仓库
git clone https://github.com/fish2018/GoComicMosaic/
cd GoComicMosaic/sitemap-generator
# 安装依赖并构建
go mod tidy
go build -o sitemap-generator
# 可选:安装到系统路径
sudo mv sitemap-generator /usr/local/bin/
```
### 在Docker环境中使用
将工具添加到Dockerfile中:
```dockerfile
# 将sitemap生成器添加到构建阶段
COPY sitemap-generator/ /app/sitemap-generator/
WORKDIR /app/sitemap-generator
RUN go mod download
RUN go build -o sitemap-generator
# 复制到最终镜像
COPY --from=builder /app/sitemap-generator/sitemap-generator /app/
```
## 使用方法
```bash
# 基本用法
./sitemap-generator
# 指定基础URL
./sitemap-generator --baseurl https://yourdomain.com
# 指定API URL和输出目录
./sitemap-generator --api https://api.yourdomain.com --output ./public
```
## Docker集成
在您的Dockerfile中可以这样集成
```dockerfile
# 在构建阶段
COPY sitemap-generator/ /app/sitemap-generator/
WORKDIR /app/sitemap-generator
RUN go mod download
RUN go build -o sitemap-generator
# 复制到最终镜像
COPY --from=builder /app/sitemap-generator/sitemap-generator /app/
```
## 自动化定时生成
可以在容器启动脚本中添加:
```bash
# 生成初始sitemap
/app/sitemap-generator -b ${DOMAIN} -o /usr/share/nginx/html/static
# 设置定时任务
echo "0 3 * * * /app/sitemap-generator -b ${DOMAIN} -o /usr/share/nginx/html/static" > /etc/crontabs/root
crond
```

View File

@@ -1,11 +0,0 @@
module github.com/fish2018/sitemap-generator
go 1.21
require github.com/urfave/cli/v2 v2.25.7
require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
)

View File

@@ -1,8 +0,0 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=

View File

@@ -1,826 +0,0 @@
package main
import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/urfave/cli/v2"
)
// 配置结构体
type Config struct {
BaseURL string // 网站基础URL
OutputDir string // 输出目录
APIBaseURL string // API基础URL
TestMode bool // 是否为测试模式
BatchSize int // 每次API请求的资源数量限制
ConcurrentRequests int // 并发请求数量
RequestDelay int // 并发请求间隔时间(毫秒)
MaxURLsPerSitemap int // 每个sitemap文件中的最大URL数量
CreateSitemapIndex bool // 是否创建sitemap索引
StaticRoutes []Route // 静态路由
}
// 路由结构体
type Route struct {
Path string // 路径
Changefreq string // 更新频率
Priority float64 // 优先级
}
// Sitemap结构体
type URLSet struct {
XMLName xml.Name `xml:"urlset"`
Xmlns string `xml:"xmlns,attr"`
URLs []URL `xml:"url"`
}
// URL结构体
type URL struct {
Loc string `xml:"loc"`
LastMod string `xml:"lastmod,omitempty"`
ChangeFreq string `xml:"changefreq,omitempty"`
Priority string `xml:"priority,omitempty"`
}
// Sitemap索引结构体
type SitemapIndex struct {
XMLName xml.Name `xml:"sitemapindex"`
Xmlns string `xml:"xmlns,attr"`
Sitemaps []Sitemap `xml:"sitemap"`
}
// Sitemap引用结构体
type Sitemap struct {
Loc string `xml:"loc"`
LastMod string `xml:"lastmod"`
}
// Resource 资源结构体
type Resource struct {
ID interface{} `json:"id"`
ResourceID interface{} `json:"resourceId"`
Title string `json:"title"`
UpdatedAt interface{} `json:"updated_at"`
CreatedAt interface{} `json:"created_at"`
UpdateTime interface{} `json:"updateTime"`
CreateTime interface{} `json:"createTime"`
TotalCount interface{} `json:"total_count,omitempty"` // 总数计数,仅在第一个资源中可能存在
}
// API响应结构体
type APIResponse struct {
Resources []Resource `json:"resources"`
Data []Resource `json:"data"`
Total int `json:"total"`
}
// 测试资源
var testResources = []Resource{
{ID: 1, Title: "测试资源1", UpdatedAt: time.Now().Format(time.RFC3339)},
{ID: 2, Title: "测试资源2", UpdatedAt: time.Now().Format(time.RFC3339)},
{ID: 3, Title: "测试资源3", UpdatedAt: time.Now().Format(time.RFC3339)},
{ID: 4, Title: "测试资源4", UpdatedAt: time.Now().Format(time.RFC3339)},
{ID: 5, Title: "测试资源5", UpdatedAt: time.Now().Format(time.RFC3339)},
}
func main() {
app := &cli.App{
Name: "sitemap-generator",
Usage: "生成动漫资源网站的sitemap.xml文件",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "baseurl",
Aliases: []string{"b"},
Value: "https://example.com",
Usage: "设置站点域名",
EnvVars: []string{"BASE_URL"},
},
&cli.StringFlag{
Name: "api",
Aliases: []string{"a"},
Value: "",
Usage: "设置API基础URL (默认为baseurl/api或http://localhost:8000/api)",
},
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Value: "public",
Usage: "输出目录路径",
},
&cli.BoolFlag{
Name: "test",
Aliases: []string{"t"},
Value: false,
Usage: "使用测试模式",
},
&cli.IntFlag{
Name: "batch-size",
Value: 100,
Usage: "每次API请求的资源数量限制",
},
&cli.IntFlag{
Name: "concurrent",
Value: 10,
Usage: "并发请求数量",
},
&cli.IntFlag{
Name: "delay",
Value: 100,
Usage: "并发请求间隔时间(毫秒)",
},
&cli.IntFlag{
Name: "max-urls",
Value: 50000,
Usage: "每个sitemap文件中的最大URL数量",
},
&cli.BoolFlag{
Name: "create-index",
Value: true,
Usage: "是否创建sitemap索引",
},
},
Action: func(c *cli.Context) error {
baseURL := c.String("baseurl")
apiBaseURL := c.String("api")
outputDir := c.String("output")
// 确保baseURL不以/结尾
baseURL = strings.TrimSuffix(baseURL, "/")
// 如果未指定API URL则使用默认值
if apiBaseURL == "" {
apiBaseURL = baseURL + "/api"
// 如果baseURL是默认值使用本地开发URL
if baseURL == "https://example.com" {
apiBaseURL = "http://localhost:8000/api"
}
}
// 确保apiBaseURL不以/结尾
apiBaseURL = strings.TrimSuffix(apiBaseURL, "/")
config := Config{
BaseURL: baseURL,
OutputDir: outputDir,
APIBaseURL: apiBaseURL,
TestMode: c.Bool("test"),
BatchSize: c.Int("batch-size"),
ConcurrentRequests: c.Int("concurrent"),
RequestDelay: c.Int("delay"),
MaxURLsPerSitemap: c.Int("max-urls"),
CreateSitemapIndex: c.Bool("create-index"),
StaticRoutes: []Route{
{Path: "/", Changefreq: "daily", Priority: 1.0},
{Path: "/submit", Changefreq: "weekly", Priority: 0.8},
{Path: "/about", Changefreq: "monthly", Priority: 0.7},
},
}
log.Println("站点地图生成器启动")
log.Printf("使用API基础URL: %s", config.APIBaseURL)
log.Printf("输出目录: %s", config.OutputDir)
log.Printf("测试模式: %v", config.TestMode)
return generateSitemap(config)
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
// 生成站点地图
func generateSitemap(config Config) error {
// 确保输出目录存在
if err := os.MkdirAll(config.OutputDir, 0755); err != nil {
return fmt.Errorf("创建输出目录失败: %w", err)
}
log.Println("开始生成站点地图...")
// 生成静态URL
staticURLs := generateStaticURLs(config)
// 尝试生成动态URL
dynamicURLs, err := generateDynamicURLs(config)
if err != nil {
log.Printf("无法获取动态URL只生成静态站点地图: %v", err)
dynamicURLs = []URL{}
}
// 计算总URL数量
totalURLs := len(staticURLs) + len(dynamicURLs)
log.Printf("站点地图包含: %d 个静态URL + %d 个动态URL = 共 %d 个URL",
len(staticURLs), len(dynamicURLs), totalURLs)
// 所有URLs
allURLs := append(staticURLs, dynamicURLs...)
// 如果URL数量较少直接生成单个sitemap文件
if totalURLs <= config.MaxURLsPerSitemap {
urlset := URLSet{
Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
URLs: allURLs,
}
// 写入文件
outputPath := filepath.Join(config.OutputDir, "sitemap.xml")
if err := writeSitemapToFile(urlset, outputPath); err != nil {
return fmt.Errorf("写入sitemap文件失败: %w", err)
}
log.Printf("站点地图已生成: %s", outputPath)
} else {
// 如果URL数量较多需要分割sitemap
log.Printf("URL数量(%d)超过每个sitemap的最大限制(%d)将分割生成多个sitemap文件",
totalURLs, config.MaxURLsPerSitemap)
sitemapFiles := []struct {
FileName string
Count int
}{}
// 分批生成sitemap文件
batchCount := (totalURLs + config.MaxURLsPerSitemap - 1) / config.MaxURLsPerSitemap
log.Printf("需要生成 %d 个sitemap文件", batchCount)
for i := 0; i < batchCount; i++ {
startIndex := i * config.MaxURLsPerSitemap
endIndex := (i + 1) * config.MaxURLsPerSitemap
if endIndex > totalURLs {
endIndex = totalURLs
}
batchURLs := allURLs[startIndex:endIndex]
urlset := URLSet{
Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
URLs: batchURLs,
}
// 文件名格式: sitemap-1.xml, sitemap-2.xml, ...
sitemapFileName := fmt.Sprintf("sitemap-%d.xml", i+1)
outputPath := filepath.Join(config.OutputDir, sitemapFileName)
if err := writeSitemapToFile(urlset, outputPath); err != nil {
return fmt.Errorf("写入分割sitemap文件失败: %w", err)
}
sitemapFiles = append(sitemapFiles, struct {
FileName string
Count int
}{
FileName: sitemapFileName,
Count: len(batchURLs),
})
log.Printf("生成sitemap文件 %d/%d: %s (包含 %d 个URL)",
i+1, batchCount, outputPath, len(batchURLs))
}
// 如果需要创建sitemap索引文件
if config.CreateSitemapIndex {
today := time.Now().Format("2006-01-02")
sitemaps := make([]Sitemap, len(sitemapFiles))
for i, file := range sitemapFiles {
sitemaps[i] = Sitemap{
Loc: fmt.Sprintf("%s/%s", config.BaseURL, file.FileName),
LastMod: today,
}
}
sitemapIndex := SitemapIndex{
Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
Sitemaps: sitemaps,
}
indexPath := filepath.Join(config.OutputDir, "sitemap.xml")
if err := writeSitemapIndexToFile(sitemapIndex, indexPath); err != nil {
return fmt.Errorf("写入sitemap索引文件失败: %w", err)
}
log.Printf("生成sitemap索引文件: %s (引用 %d 个sitemap文件)",
indexPath, len(sitemapFiles))
}
}
log.Println("站点地图生成完成!")
return nil
}
// 生成静态URL
func generateStaticURLs(config Config) []URL {
today := time.Now().Format("2006-01-02")
urls := make([]URL, len(config.StaticRoutes))
for i, route := range config.StaticRoutes {
urls[i] = URL{
Loc: fmt.Sprintf("%s%s", config.BaseURL, route.Path),
LastMod: today,
ChangeFreq: route.Changefreq,
Priority: fmt.Sprintf("%.1f", route.Priority),
}
}
return urls
}
// 获取单个批次的资源
func fetchResourceBatch(apiBaseURL string, skip, limit int, sortBy, sortOrder string) ([]Resource, int, error) {
apiUrl := fmt.Sprintf("%s/resources/public?skip=%d&limit=%d&sort_by=%s&sort_order=%s",
apiBaseURL, skip, limit, sortBy, sortOrder)
log.Printf("请求资源数据: skip=%d, limit=%d", skip, limit)
resp, err := http.Get(apiUrl)
if err != nil {
return nil, 0, fmt.Errorf("API请求失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, 0, fmt.Errorf("API返回非200状态码: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, fmt.Errorf("读取响应体失败: %w", err)
}
// 首先尝试解析为资源数组新的API返回格式
var resources []Resource
if err := json.Unmarshal(body, &resources); err == nil {
// 成功解析为数组
log.Printf("成功解析为资源数组,获取到资源(skip=%d): %d条", skip, len(resources))
// 从第一个资源的TotalCount字段获取总数
total := 0
if len(resources) > 0 && resources[0].TotalCount != nil {
// 尝试转换TotalCount为整数
switch v := resources[0].TotalCount.(type) {
case float64:
total = int(v)
case int:
total = v
case int64:
total = int(v)
case string:
if t, err := strconv.Atoi(v); err == nil {
total = t
}
}
log.Printf("从第一个资源TotalCount字段获取总数: %d", total)
}
// 如果无法从TotalCount获取则使用资源数组长度作为总数
if total == 0 {
total = len(resources)
log.Printf("无法获取总数信息,使用资源数组长度作为总数: %d", total)
}
return resources, total, nil
}
// 如果解析为数组失败,尝试旧的对象格式
var apiResp APIResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
// 尝试不同的响应结构
var rawResp map[string]interface{}
if jsonErr := json.Unmarshal(body, &rawResp); jsonErr != nil {
return nil, 0, fmt.Errorf("解析JSON失败: %w", err)
}
// 检查不同可能的数据结构
resources := []Resource{}
total := 0
// 尝试从resources字段获取
if resourcesData, ok := rawResp["resources"].([]interface{}); ok {
for _, r := range resourcesData {
if resourceMap, ok := r.(map[string]interface{}); ok {
resources = append(resources, mapToResource(resourceMap))
}
}
}
// 尝试从data字段获取
if resourcesData, ok := rawResp["data"].([]interface{}); ok && len(resources) == 0 {
for _, r := range resourcesData {
if resourceMap, ok := r.(map[string]interface{}); ok {
resources = append(resources, mapToResource(resourceMap))
}
}
}
// 获取总数
if totalValue, ok := rawResp["total"].(float64); ok {
total = int(totalValue)
}
log.Printf("获取到资源(skip=%d): %d条", skip, len(resources))
return resources, total, nil
}
// 从标准响应结构获取资源
resultResources := []Resource{}
if len(apiResp.Resources) > 0 {
resultResources = apiResp.Resources
} else if len(apiResp.Data) > 0 {
resultResources = apiResp.Data
}
log.Printf("获取到资源(skip=%d): %d条", skip, len(resultResources))
return resultResources, apiResp.Total, nil
}
// 将map转换为Resource结构体
func mapToResource(m map[string]interface{}) Resource {
return Resource{
ID: m["id"],
ResourceID: m["resourceId"],
Title: toString(m["title"]),
UpdatedAt: m["updated_at"],
CreatedAt: m["created_at"],
UpdateTime: m["updateTime"],
CreateTime: m["createTime"],
}
}
// 将任意值转换为字符串
func toString(v interface{}) string {
if v == nil {
return ""
}
return fmt.Sprintf("%v", v)
}
// 生成动态URL
func generateDynamicURLs(config Config) ([]URL, error) {
// 如果是测试模式,使用测试数据
if config.TestMode {
log.Println("使用测试模式生成测试资源URL")
urls := make([]URL, len(testResources))
for i, resource := range testResources {
updatedAt := ""
if str, ok := resource.UpdatedAt.(string); ok {
updatedAt = strings.Split(str, "T")[0]
} else {
updatedAt = time.Now().Format("2006-01-02")
}
urls[i] = URL{
Loc: fmt.Sprintf("%s/resource/%v", config.BaseURL, resource.ID),
LastMod: updatedAt,
ChangeFreq: "weekly",
Priority: "0.9",
}
}
return urls, nil
}
try := func() ([]URL, error) {
// 分批次获取所有资源
log.Println("开始获取所有资源数据...")
// 发送第一个请求获取第一批数据
log.Println("发送首次请求获取数据...")
firstBatchSize := config.BatchSize
firstBatch, total, err := fetchResourceBatch(config.APIBaseURL, 0, firstBatchSize, "likes_count", "desc")
if err != nil {
return nil, fmt.Errorf("获取第一批数据失败: %w", err)
}
// 如果第一批数据已经不足批次大小,则表明数据已全部获取
if len(firstBatch) < firstBatchSize {
log.Printf("首次请求获取 %d 条数据,少于批次大小 %d表明已获取全部数据",
len(firstBatch), firstBatchSize)
return resourcesAsURLs(firstBatch, config.BaseURL), nil
}
// 如果还需要更多数据,继续并发请求
log.Printf("首次请求获取 %d 条数据,达到批次大小,需要继续获取更多数据", len(firstBatch))
allResources := firstBatch
// 估计总数量
totalCount := total
if totalCount == 0 {
// 如果API没有返回总数则推测总数
totalCount = max(1000, len(firstBatch)*2)
}
log.Printf("估计总资源数量: %d", totalCount)
// 计算需要的批次数
batchSize := config.BatchSize
remainingBatches := (totalCount - len(firstBatch) + batchSize - 1) / batchSize
log.Printf("已获取第一批 %d 条,预计还需要 %d 个批次请求", len(firstBatch), remainingBatches)
// 准备剩余的请求
remainingRequests := make([]struct {
Skip int
Limit int
}, remainingBatches)
for i := 0; i < remainingBatches; i++ {
skip := firstBatchSize + i*batchSize
remainingRequests[i] = struct {
Skip int
Limit int
}{
Skip: skip,
Limit: batchSize,
}
}
// 如果没有剩余请求,直接返回第一批数据
if len(remainingRequests) == 0 {
log.Println("无需进一步请求,已获取所有数据")
return resourcesAsURLs(allResources, config.BaseURL), nil
}
// 分组进行并发请求
concurrentBatchSize := config.ConcurrentRequests
dataCompletelyFetched := false
// 按并发数量分组处理请求
for batchIndex := 0; batchIndex < len(remainingRequests) && !dataCompletelyFetched; batchIndex += concurrentBatchSize {
endIndex := batchIndex + concurrentBatchSize
if endIndex > len(remainingRequests) {
endIndex = len(remainingRequests)
}
currentBatchRequests := remainingRequests[batchIndex:endIndex]
log.Printf("处理并发批次 %d/%d, 包含%d个请求",
batchIndex/concurrentBatchSize+1,
(len(remainingRequests)+concurrentBatchSize-1)/concurrentBatchSize,
len(currentBatchRequests))
// 并发执行当前批次的所有请求
var wg sync.WaitGroup
resultsChan := make(chan struct {
Resources []Resource
Index int
Complete bool
}, len(currentBatchRequests))
for i, req := range currentBatchRequests {
wg.Add(1)
go func(i int, skip, limit int) {
defer wg.Done()
resources, _, err := fetchResourceBatch(config.APIBaseURL, skip, limit, "likes_count", "desc")
complete := false
if err != nil {
log.Printf("请求失败(skip=%d): %v", skip, err)
} else if len(resources) < limit {
// 返回数据少于请求数量,表示已到达数据末尾
complete = true
}
resultsChan <- struct {
Resources []Resource
Index int
Complete bool
}{
Resources: resources,
Index: i,
Complete: complete,
}
}(i, req.Skip, req.Limit)
}
// 等待所有goroutine完成
go func() {
wg.Wait()
close(resultsChan)
}()
// 收集结果
resourcesInBatch := 0
for result := range resultsChan {
if len(result.Resources) > 0 {
allResources = append(allResources, result.Resources...)
resourcesInBatch += len(result.Resources)
if result.Complete {
log.Printf("请求 %d/%d 返回数据不足 %d 条,表明已到达数据末尾",
currentBatchRequests[result.Index].Skip,
currentBatchRequests[result.Index].Limit,
batchSize)
dataCompletelyFetched = true
}
}
}
log.Printf("当前批次获取到%d条资源累计%d条", resourcesInBatch, len(allResources))
// 如果当前批次获取的资源数量小于预期,很可能已经获取了所有数据
expectedResourcesInBatch := min(
len(currentBatchRequests)*batchSize,
totalCount-batchIndex*batchSize,
)
if resourcesInBatch < expectedResourcesInBatch {
log.Printf("资源数量小于预期(%d < %d),标记为已获取全部数据",
resourcesInBatch, expectedResourcesInBatch)
dataCompletelyFetched = true
}
// 添加延迟,避免请求过于频繁
if !dataCompletelyFetched && batchIndex+concurrentBatchSize < len(remainingRequests) {
log.Printf("等待%d毫秒后继续下一批请求...", config.RequestDelay)
time.Sleep(time.Duration(config.RequestDelay) * time.Millisecond)
}
}
log.Printf("成功获取所有资源,共%d条", len(allResources))
// 如果未获取到任何资源,使用测试数据
if len(allResources) == 0 {
log.Println("未获取到任何资源数据,将使用测试数据")
return generateDynamicURLs(Config{
BaseURL: config.BaseURL,
TestMode: true,
})
}
// 对资源进行去重
uniqueResources := []Resource{}
resourceIDs := make(map[string]bool)
for _, resource := range allResources {
id := getResourceID(resource)
if id != "" && !resourceIDs[id] {
resourceIDs[id] = true
uniqueResources = append(uniqueResources, resource)
}
}
log.Printf("去重后资源数量: %d", len(uniqueResources))
// 将资源转换为URL
return resourcesAsURLs(uniqueResources, config.BaseURL), nil
}
urls, err := try()
if err != nil {
log.Printf("获取动态资源失败: %v", err)
log.Println("由于API请求失败使用测试数据生成资源URL")
return generateDynamicURLs(Config{
BaseURL: config.BaseURL,
TestMode: true,
})
}
return urls, nil
}
// 获取资源ID
func getResourceID(resource Resource) string {
if resource.ID != nil {
return fmt.Sprintf("%v", resource.ID)
}
if resource.ResourceID != nil {
return fmt.Sprintf("%v", resource.ResourceID)
}
return ""
}
// 将资源转换为URL
func resourcesAsURLs(resources []Resource, baseURL string) []URL {
urls := make([]URL, 0, len(resources))
for _, resource := range resources {
id := getResourceID(resource)
if id == "" {
continue
}
lastmod := getLastModifiedDate(resource)
urls = append(urls, URL{
Loc: fmt.Sprintf("%s/resource/%s", baseURL, id),
LastMod: lastmod,
ChangeFreq: "weekly",
Priority: "0.9",
})
}
return urls
}
// 获取最后修改日期
func getLastModifiedDate(resource Resource) string {
// 尝试多种可能的日期字段
for _, field := range []interface{}{
resource.UpdatedAt,
resource.UpdateTime,
resource.CreatedAt,
resource.CreateTime,
} {
if field == nil {
continue
}
// 如果是字符串,尝试解析
if dateStr, ok := field.(string); ok && dateStr != "" {
if t, err := time.Parse(time.RFC3339, dateStr); err == nil {
return t.Format("2006-01-02")
}
// 可能是其他格式的日期字符串截取前10个字符
if len(dateStr) >= 10 {
return dateStr[:10]
}
return dateStr
}
// 如果是数字Unix时间戳
if timestamp, ok := field.(float64); ok {
return time.Unix(int64(timestamp), 0).Format("2006-01-02")
}
// 如果是整数时间戳
if timestamp, ok := field.(int64); ok {
return time.Unix(timestamp, 0).Format("2006-01-02")
}
}
// 如果都没有找到,使用当前日期
return time.Now().Format("2006-01-02")
}
// 写入sitemap文件
func writeSitemapToFile(urlset URLSet, outputPath string) error {
// 创建输出文件
file, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("创建输出文件失败: %w", err)
}
defer file.Close()
// 写入XML头
file.WriteString(xml.Header)
// 编码并写入XML
encoder := xml.NewEncoder(file)
encoder.Indent("", " ")
if err := encoder.Encode(urlset); err != nil {
return fmt.Errorf("XML编码失败: %w", err)
}
return nil
}
// 写入sitemap索引文件
func writeSitemapIndexToFile(index SitemapIndex, outputPath string) error {
// 创建输出文件
file, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("创建输出文件失败: %w", err)
}
defer file.Close()
// 写入XML头
file.WriteString(xml.Header)
// 编码并写入XML
encoder := xml.NewEncoder(file)
encoder.Indent("", " ")
if err := encoder.Encode(index); err != nil {
return fmt.Errorf("XML编码失败: %w", err)
}
return nil
}
// min returns the smaller of x or y.
func min(x, y int) int {
if x < y {
return x
}
return y
}
// max returns the larger of x or y.
func max(x, y int) int {
if x > y {
return x
}
return y
}

View File

@@ -1,189 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com/</loc>
<lastmod>2025-06-04</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://example.com/submit</loc>
<lastmod>2025-06-04</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://example.com/about</loc>
<lastmod>2025-06-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://example.com/resource/4</loc>
<lastmod>2025-05-31</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/5</loc>
<lastmod>2025-05-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/11</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/12</loc>
<lastmod>2025-05-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/1</loc>
<lastmod>2025-06-02</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/6</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/7</loc>
<lastmod>2025-05-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/13</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/14</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/18</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/26</loc>
<lastmod>2025-06-02</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/2</loc>
<lastmod>2025-05-24</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/3</loc>
<lastmod>2025-05-24</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/8</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/9</loc>
<lastmod>2025-05-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/10</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/15</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/16</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/17</loc>
<lastmod>2025-05-30</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/19</loc>
<lastmod>2025-05-31</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/20</loc>
<lastmod>2025-05-31</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/21</loc>
<lastmod>2025-05-31</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/22</loc>
<lastmod>2025-05-31</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/23</loc>
<lastmod>2025-05-31</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/24</loc>
<lastmod>2025-06-03</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/25</loc>
<lastmod>2025-05-31</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/27</loc>
<lastmod>2025-06-03</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://example.com/resource/28</loc>
<lastmod>2025-06-03</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
</urlset>