feat: 视频预览/数据看板/下载配置 三项业务功能

- 新增视频预览:VideoPlayer 组件、VideoController/DyCollectVideoService/Repository、VideoPageRequestDTO 对应后端改动
- 新增数据看板:workplace/statics.vue、RecordTable、相关 store/http/axios 改动
- 新增下载配置:DownloadConfigService、ConfigController、AppSet.vue、AppConfig
- 其他:DyHttpClientService / 同步 Job(Collect, Favorit) / Cookie 表 / CommonService / ServiceExtension 的配套调整
- 仓库卫生:.gitignore 追加 feiniu 过滤;运维脚本归档到 docs/scripts/
This commit is contained in:
yuming
2026-04-25 00:22:33 +08:00
parent ca530312d0
commit b328e82eb2
27 changed files with 3602 additions and 348 deletions
+201 -6
View File
@@ -1,5 +1,6 @@
using dy.net.model;
using SqlSugar;
using System.Linq.Expressions;
namespace dy.net.repository
{
@@ -17,10 +18,14 @@ namespace dy.net.repository
/// </summary>
/// <param name="pageIndex">页码(从1开始)</param>
/// <param name="pageSize">每页数量</param>
/// <param name="tag">可选标签过滤</param>
/// <param name="tag">可选标签过滤(单个,兼容旧版本)</param>
/// <param name="tags">可选标签过滤(多个)</param>
/// <param name="author">可选作者过滤</param>
/// <param name="isDeleted">删除状态筛选:null=全部,true=已删除(videoSavePath为空),false=未删除(videoSavePath不为空)</param>
/// <param name="sortField">排序字段:syncTime-同步时间, viedoType-同步类型, viedoCate-视频类型, author-作者, videoTitle-视频名称, dyUser-用户</param>
/// <param name="sortOrder">排序方向:asc-升序, desc-降序</param>
/// <returns>分页结果(视频列表和总数)</returns>
public async Task<(List<DyCollectVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, string author = null,string viedoType=null, List<string> dates = null)
public async Task<(List<DyCollectVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, List<string> tags = null, string author = null,string viedoType=null, List<string> dates = null, bool? isDeleted = null, string sortField = null, string sortOrder = null)
{
DateTime? start = null;
@@ -35,16 +40,206 @@ namespace dy.net.repository
start = Convert.ToDateTime(dates[0]);
}
var where = this.Db.Queryable<DyCollectVideo>()
.WhereIF(!string.IsNullOrWhiteSpace(tag), x => x.Tag1 == tag)
var where = this.Db.Queryable<DyCollectVideo>();
// 支持多个标签筛选(优先使用 tags,如果为空则使用 tag)
// 注意:tags 匹配的是 viedoCate(Tag1/Tag2 组合),格式如 "美食/美食展示"
// 特殊值 "未分类" 表示 Tag1 和 Tag2 都为空的视频
if (tags != null && tags.Any())
{
// 过滤空值
var validTags = tags.Where(t => !string.IsNullOrWhiteSpace(t)).ToList();
if (validTags.Any())
{
// 检查是否包含"未分类"选项
var hasEmptyCategory = validTags.Contains("未分类");
var otherTags = validTags.Where(t => t != "未分类").ToList();
// 构建 OR 条件:对于每个 tag,匹配对应的 Tag1/Tag2 组合
if (hasEmptyCategory && otherTags.Any())
{
// 同时包含"未分类"和其他类型
// 使用内存过滤的方式,确保逻辑正确
var matchingVideoIds = new List<string>();
// 获取所有视频(先应用其他筛选条件)
var allVideos = await this.Db.Queryable<DyCollectVideo>()
.WhereIF(!string.IsNullOrWhiteSpace(author), x => x.Author == author)
.WhereIF(start.HasValue, x => x.SyncTime >= start.Value)
.WhereIF(end.HasValue, x => x.SyncTime <= end.Value)
.WhereIF(!string.IsNullOrWhiteSpace(viedoType) && viedoType != "*", x => x.ViedoType == viedoType)
.WhereIF(isDeleted.HasValue && isDeleted.Value, x => string.IsNullOrWhiteSpace(x.VideoSavePath))
.WhereIF(isDeleted.HasValue && !isDeleted.Value, x => !string.IsNullOrWhiteSpace(x.VideoSavePath))
.ToListAsync();
// 在内存中过滤 viedoCate
foreach (var video in allVideos)
{
var viedoCate = (string.IsNullOrWhiteSpace(video.Tag1) ? "" : video.Tag1) + "/" + (string.IsNullOrWhiteSpace(video.Tag2) ? "" : video.Tag2);
// 检查是否是"未分类"或匹配其他标签
bool isMatch = (string.IsNullOrWhiteSpace(video.Tag1) && string.IsNullOrWhiteSpace(video.Tag2)) || // 未分类
otherTags.Contains(viedoCate); // 其他类型
if (isMatch)
{
matchingVideoIds.Add(video.Id);
}
}
if (matchingVideoIds.Any())
{
// 使用 ID 列表重新构建查询
where = this.Db.Queryable<DyCollectVideo>()
.Where(x => matchingVideoIds.Contains(x.Id));
}
else
{
// 如果没有匹配的数据,返回空结果
where = this.Db.Queryable<DyCollectVideo>().Where(x => false);
}
}
else if (hasEmptyCategory)
{
// 只选择了"未分类"
where = where.Where(x => string.IsNullOrWhiteSpace(x.Tag1) && string.IsNullOrWhiteSpace(x.Tag2));
}
else
{
// 只选择了其他类型,构建所有条件的 OR
// 使用更简单的方法:先获取所有符合条件的视频 ID,然后使用 In 方法
// 这样可以避免复杂的表达式树构建
var matchingVideoIds = new List<string>();
// 获取所有视频
var allVideos = await this.Db.Queryable<DyCollectVideo>()
.WhereIF(!string.IsNullOrWhiteSpace(author), x => x.Author == author)
.WhereIF(start.HasValue, x => x.SyncTime >= start.Value)
.WhereIF(end.HasValue, x => x.SyncTime <= end.Value)
.WhereIF(!string.IsNullOrWhiteSpace(viedoType) && viedoType != "*", x => x.ViedoType == viedoType)
.WhereIF(isDeleted.HasValue && isDeleted.Value, x => string.IsNullOrWhiteSpace(x.VideoSavePath))
.WhereIF(isDeleted.HasValue && !isDeleted.Value, x => !string.IsNullOrWhiteSpace(x.VideoSavePath))
.ToListAsync();
// 在内存中过滤 viedoCate
foreach (var video in allVideos)
{
var viedoCate = (string.IsNullOrWhiteSpace(video.Tag1) ? "" : video.Tag1) + "/" + (string.IsNullOrWhiteSpace(video.Tag2) ? "" : video.Tag2);
if (otherTags.Contains(viedoCate))
{
matchingVideoIds.Add(video.Id);
}
}
if (matchingVideoIds.Any())
{
// 使用 ID 列表重新构建查询
where = this.Db.Queryable<DyCollectVideo>()
.Where(x => matchingVideoIds.Contains(x.Id));
}
else
{
// 如果没有匹配的数据,返回空结果
where = this.Db.Queryable<DyCollectVideo>().Where(x => false);
}
}
}
}
else if (!string.IsNullOrWhiteSpace(tag))
{
// 兼容旧版本的单标签筛选
if (tag.Contains("/"))
{
var parts = tag.Split('/', 2);
var tag1 = parts[0];
var tag2 = parts.Length > 1 ? parts[1] : "";
where = where.Where(x => x.Tag1 == tag1 && (string.IsNullOrWhiteSpace(tag2) || x.Tag2 == tag2 || string.IsNullOrWhiteSpace(x.Tag2)));
}
else
{
where = where.Where(x => x.Tag1 == tag);
}
}
// 应用其他筛选条件
where = where
.WhereIF(!string.IsNullOrWhiteSpace(author), x => x.Author == author)
.WhereIF(start.HasValue, x => x.SyncTime >= start.Value)
.WhereIF(end.HasValue, x => x.SyncTime <= end.Value)
.WhereIF(!string.IsNullOrWhiteSpace(viedoType) && viedoType != "*", x => x.ViedoType == viedoType);
.WhereIF(!string.IsNullOrWhiteSpace(viedoType) && viedoType != "*", x => x.ViedoType == viedoType)
.WhereIF(isDeleted.HasValue && isDeleted.Value, x => string.IsNullOrWhiteSpace(x.VideoSavePath)) // 已删除:videoSavePath为空
.WhereIF(isDeleted.HasValue && !isDeleted.Value, x => !string.IsNullOrWhiteSpace(x.VideoSavePath)); // 未删除:videoSavePath不为空
var totalCount = await where.CountAsync();
var list = await where.OrderByDescending(x=>x.SyncTime).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
// 构建排序逻辑
var isAsc = !string.IsNullOrWhiteSpace(sortOrder) && sortOrder.ToLower() == "asc";
var query = where;
// 调试日志
Console.WriteLine($"Repository排序参数 - sortField: {sortField}, sortOrder: {sortOrder}, isAsc: {isAsc}");
if (!string.IsNullOrWhiteSpace(sortField))
{
switch (sortField.ToLower())
{
case "synctime":
query = isAsc ? query.OrderBy(x => x.SyncTime) : query.OrderByDescending(x => x.SyncTime);
Console.WriteLine($"应用排序: SyncTime {(isAsc ? "升序" : "降序")}");
break;
case "viedotype":
query = isAsc ? query.OrderBy(x => x.ViedoType) : query.OrderByDescending(x => x.ViedoType);
Console.WriteLine($"应用排序: ViedoType {(isAsc ? "升序" : "降序")}");
break;
case "viedocate":
// 视频类型需要按Tag1和Tag2排序,SqlSugar使用OrderBy链式调用
if (isAsc)
{
query = query.OrderBy(x => x.Tag1).OrderBy(x => x.Tag2);
}
else
{
query = query.OrderByDescending(x => x.Tag1).OrderByDescending(x => x.Tag2);
}
Console.WriteLine($"应用排序: ViedoCate {(isAsc ? "升序" : "降序")}");
break;
case "author":
query = isAsc ? query.OrderBy(x => x.Author) : query.OrderByDescending(x => x.Author);
Console.WriteLine($"应用排序: Author {(isAsc ? "升序" : "降序")}");
break;
case "videotitle":
query = isAsc ? query.OrderBy(x => x.VideoTitle) : query.OrderByDescending(x => x.VideoTitle);
Console.WriteLine($"应用排序: VideoTitle {(isAsc ? "升序" : "降序")}");
break;
case "dyuser":
// 用户字段需要先查询用户信息,这里先按CookieId排序,后续会设置DyUser
query = isAsc ? query.OrderBy(x => x.CookieId) : query.OrderByDescending(x => x.CookieId);
Console.WriteLine($"应用排序: DyUser {(isAsc ? "升序" : "降序")}");
break;
default:
// 默认按同步时间降序
query = query.OrderByDescending(x => x.SyncTime);
Console.WriteLine($"未识别的排序字段,使用默认排序: SyncTime 降序");
break;
}
}
else
{
// 默认按同步时间降序
query = query.OrderByDescending(x => x.SyncTime);
Console.WriteLine("无排序字段,使用默认排序: SyncTime 降序");
}
var list = await query.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
Console.WriteLine($"查询完成,返回 {list.Count} 条记录");
if (list.Any())
{
Console.WriteLine($"第一条记录的同步时间: {list.First().SyncTime}");
Console.WriteLine($"最后一条记录的同步时间: {list.Last().SyncTime}");
}
if (list.Any()) {
var users= await this.Db.Queryable<DyUserCookies>().ToListAsync();
foreach (var item in list)