mirror of
https://github.com/fish2018/GoComicMosaic.git
synced 2025-11-25 03:15:02 +08:00
修复部分bug
This commit is contained in:
165
README.md
165
README.md
@@ -35,35 +35,47 @@ docker run -d --name dongman \
|
||||
```
|
||||
|
||||
|
||||
## 适配移动端样式
|
||||
移动端也可以更好的体验美漫共建小站了
|
||||
|
||||

|
||||
|
||||
## 首页
|
||||

|
||||
|
||||
可以根据资源中文名、英文名、简介进行搜索
|
||||

|
||||

|
||||
|
||||
## 详情页
|
||||

|
||||
|
||||
可以切换查看图片,选择网盘标签,一键复制网盘链接密码
|
||||

|
||||
|
||||
## 关于本站
|
||||

|
||||
点击「盘搜」按钮,一键搜索各种网盘资源
|
||||
|
||||

|
||||
|
||||
点击「剧集探索」按钮,可以查看分季分集信息
|
||||

|
||||
|
||||
可以一键生成分享海报和链接
|
||||

|
||||
|
||||
一键在线点播
|
||||
|
||||

|
||||
|
||||
也可以直接在`https://域名/streams`页面点播,支持解析线路和自定义爬虫
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 全面支持管理后台设置网站信息和采集解析源
|
||||
目前美漫共建官网内置30条数据源
|
||||

|
||||
|
||||
## 支持外挂在线播放数据源(自定义爬虫)
|
||||
会写爬虫的用户可以自己添加数据源,更加灵活。参考[外接数据源开发者文档](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)
|
||||

|
||||
|
||||
## 提交资源
|
||||
这个才是资源共建平台的核心,点击右上角的'提交资源',用户可以随意提交自己喜欢的动漫资源,如果网站还不存该美漫时,会是一个新建资源的表单,需要填写中文名、英文名、类型、简介等基础信息。提交后,要等管理员在后台审批完才会在首页显示
|
||||
|
||||
### 提交-新建资源
|
||||

|
||||

|
||||
|
||||
网盘链接和图片都可以提交多个
|
||||

|
||||
支持从TMDB搜索、预览、一键导入资源
|
||||

|
||||
|
||||
### 提交-补充资源
|
||||
顾名思义,就是对已经存在的动漫资源补充一些信息,主要是图片、资源链接
|
||||
@@ -75,11 +87,6 @@ docker run -d --name dongman \
|
||||
从资源详情页点击'补充资源'按钮,不用自己再搜索选择了,自动绑定对应的动漫
|
||||

|
||||
|
||||
## 管理员登录
|
||||
不用多说了,就是输入账号密码,初始密码登录后可修改
|
||||
|
||||

|
||||
|
||||
## 管理控制台
|
||||
主要用于审批用户提交的资源
|
||||
|
||||
@@ -95,120 +102,8 @@ docker run -d --name dongman \
|
||||
|
||||

|
||||
|
||||
## 新增喜欢按钮
|
||||
在详情页可以点击喜欢
|
||||

|
||||
首页可以根据喜欢数量排序,默认按最新发布排序
|
||||

|
||||
|
||||
## 新增分页
|
||||

|
||||
|
||||
## 新增在线点播功能
|
||||

|
||||
|
||||
## 优化检测
|
||||

|
||||
|
||||
## 调整底栏
|
||||
- 添加在线点播
|
||||
- 添加访问统计
|
||||
- 添加友链
|
||||

|
||||
|
||||
## 详情页剧照点击放大查看
|
||||

|
||||
|
||||
## 全面支持管理后台设置网站信息和采集解析源
|
||||
目前美漫共建官网内置30条数据源
|
||||

|
||||
|
||||
## 支持外挂在线播放数据源
|
||||
会写爬虫的用户可以自己添加数据源,更加灵活。参考[外接数据源开发者文档](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)
|
||||

|
||||
|
||||
## 支持从TMDB一键导入资源库
|
||||
可以从TMDB搜索、预览、一键导入资源
|
||||

|
||||
|
||||
## 支持一键分享
|
||||
资源详情页可以一键生成分享海报和链接
|
||||

|
||||
|
||||
## 新增「剧集探索」功能,支持查看分季分集信息
|
||||
资源详情页可以一键生成分享海报和链接
|
||||

|
||||
|
||||
---
|
||||
|
||||
# 更新日志
|
||||
-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'
|
||||
✅ 调整后台所有图片预览模态框,保持全站风格一致,审批通过的图片,点击也可以放大看
|
||||
✅ 优化搜索框样式
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
BASE_URL=https://dm.xueximeng.com
|
||||
ASSETS_PATH=../assets
|
||||
# ASSETS_PATH=../data/assets
|
||||
#ASSETS_PATH=../data/assets
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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_, ¬null, &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_, ¬null, &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
|
||||
}
|
||||
@@ -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, ¬Null, &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 数据库测试完成")
|
||||
}
|
||||
@@ -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("所有测试完成!")
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"dongman/internal/models"
|
||||
"dongman/internal/utils"
|
||||
"dongman/internal/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"dongman/internal/utils"
|
||||
"dongman/internal/config"
|
||||
"dongman/internal/utils"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
// 创建目标目录
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -397,7 +397,6 @@ func LoadTMDBConfig() {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("未找到TMDB API密钥配置,将使用默认值")
|
||||
}
|
||||
|
||||
// GetTMDBStatus 获取TMDB功能是否启用,只返回enabled状态,不返回API密钥
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"dongman/internal/utils"
|
||||
"dongman/internal/models"
|
||||
"dongman/internal/utils"
|
||||
)
|
||||
|
||||
// GetTMDBSeasons 获取动漫季节信息
|
||||
|
||||
@@ -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 {
|
||||
// 更新时间戳
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user