# swipeflow
**Repository Path**: singlr/swipeflow
## Basic Information
- **Project Name**: swipeflow
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-09-07
- **Last Updated**: 2026-09-09
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 刷Flow (SwipeFlow)
**轻量级网页版短视频播放器 · 类抖音交互体验 · 多数据源聚合**








---
## 项目简介
**刷Flow** 是一个基于 Web 的短视频播放器应用,提供类似抖音的竖屏全屏、上下滑动切换的沉浸式浏览体验。支持对接多种数据源——包括通过用户 Token 自动同步今日头条收藏视频、后台手动录入等,可灵活扩展内容来源。
### 核心特性
- **沉浸式播放** — 竖屏全屏、上下滑动切换、自动播放/暂停
- **移动端适配** — 手势操作、内联播放、触屏优化
- **多数据源** — 支持头条收藏同步(用户 Token)、后台手动录入、批量导入
- **用户互动** — 前端用户登录后可点赞、收藏视频
- **流畅体验** — 视频预加载、IntersectionObserver 智能播放控制
- **后台管理** — 基于 Vben Admin 5 + Element Plus 的可视化内容管理系统
- **高性能后端** — .NET 8 + Furion + SqlSugar,零配置 RESTful API 自动生成
## 技术架构
```
┌──────────────────────────────────────────────────┐
│ 前端播放器 (SPA) │
│ Vue 3 + Vite + CSS Scroll Snap + xgplayer │
└──────────────────────┬───────────────────────────┘
│ RESTful API
┌──────────────────────▼───────────────────────────┐
│ 后端服务层 (.NET 8) │
│ Furion 4.9+ (动态 WebAPI · JWT 鉴权 · │
│ 数据验证 · 事件总线 · 定时任务 · 缓存) │
├──────────────────────────────────────────────────┤
│ ORM 层 (SqlSugar 5.1+) │
│ CodeFirst · 链式查询 · 分页 · SQLite 适配 │
└──────────────────────┬───────────────────────────┘
│
┌──────────────────────▼───────────────────────────┐
│ 数据源层 │
│ SQLite (主库) · 头条用户 Token 同步 · OSS/CDN │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ 管理后台 (SPA) │
│ Vben Admin 5.0 + Element Plus + TypeScript │
│ Monorepo (pnpm + Turborepo) │
│ 动态路由 · 按钮级权限 · 国际化 · 主题切换 │
└──────────────────────────────────────────────────┘
```
### 技术栈详情
| 层级 | 技术选型 | 说明 |
|------|----------|------|
| **后端框架** | .NET 8 + Furion 4.9+ | 动态 WebAPI、依赖注入、数据验证、JWT 鉴权、事件总线、缓存、定时任务 |
| **ORM** | SqlSugar 5.1+ | 高性能 ORM,CodeFirst 自动建表、链式查询、分页、SQLite 适配 |
| **数据库** | SQLite | 轻量级嵌入式数据库,零配置部署,适合中小规模应用 |
| **管理后台** | Vben Admin 5.0 + Element Plus | Monorepo 架构,动态路由、细粒度权限、国际化、多主题 |
| **前端播放器** | Vue 3 + Vite + xgplayer | 竖屏滑动播放器,CSS Scroll Snap + IntersectionObserver |
| **播放器内核** | xgplayer(西瓜播放器) | 字节跳动开源,中文文档完善,自定义插件丰富 |
## 快速开始
### 环境要求
| 工具 | 版本要求 | 说明 |
|------|----------|------|
| .NET SDK | >= 8.0 | 后端运行环境 |
| Node.js | >= 18.x(推荐 18.18+ LTS) | 前端 & 管理后台运行环境 |
| pnpm | >= 8.x | Vben Admin 5 强制使用 pnpm 管理依赖 |
| Git | 最新版 | 代码版本管理 |
### 安装步骤
#### 1. 克隆项目
```bash
git clone https://gitee.com/singlr/swipeflow.git
cd swipeflow
```
#### 2. 后端启动(.NET 8 + Furion + SqlSugar)
```bash
cd server/SwipeFlow.Web.Entry
# 还原 NuGet 包
dotnet restore
# 启动项目(首次启动自动创建 SQLite 数据库及表结构)
dotnet run
```
后端默认运行在 `http://localhost:5002`,Swagger 文档地址:`http://localhost:5002/swagger`
> Furion 的 SqlSugar 集成支持 CodeFirst 模式,首次启动时会自动根据实体类创建 SQLite 数据库和表结构,无需手动建表。
#### 3. 管理后台启动(Vben Admin 5 + Element Plus)
```bash
cd admin
# 安装依赖(仅支持 pnpm)
pnpm install
# 启动 Element Plus 版本
pnpm dev:ele
```
管理后台默认运行在 `http://localhost:8003`
#### 4. 前端播放器启动
```bash
cd client
# 安装依赖
pnpm install
# 启动开发服务器
pnpm dev
```
前端播放器默认运行在 `http://localhost:8004`
## 项目结构(规划)
```
swipeflow/
├── server/ # 后端项目(.NET 8 + Furion + SqlSugar)
│ ├── SwipeFlow.Web.Entry/ # Web 入口层(启动项目)
│ │ ├── Program.cs # 应用入口(Serve.Run())
│ │ ├── appsettings.json # 应用配置
│ │ └── appsettings.Development.json
│ │
│ ├── SwipeFlow.Application/ # 应用层(业务逻辑)
│ │ ├── Controllers/ # 动态 WebAPI 控制器
│ │ │ ├── VideoController.cs # 视频管理接口
│ │ │ ├── DataSourceController.cs # 数据源管理接口
│ │ │ ├── AuthController.cs # 认证接口
│ │ │ ├── UserController.cs # 用户管理接口
│ │ │ ├── DeptController.cs # 部门管理接口
│ │ │ ├── RoleController.cs # 角色管理接口
│ │ │ ├── MenuController.cs # 菜单管理接口
│ │ │ └── ConfigController.cs # 配置管理接口
│ │ ├── Services/ # 业务服务
│ │ │ ├── VideoService.cs # 视频业务逻辑
│ │ │ ├── BaseSyncService.cs # 同步服务抽象基类
│ │ │ ├── ToutiaoSyncService.cs # 头条收藏同步服务
│ │ │ ├── DouyinSyncService.cs # 抖音收藏同步服务
│ │ │ ├── InteractionService.cs # 点赞/收藏业务逻辑
│ │ │ ├── DataSourceService.cs # 数据源聚合逻辑
│ │ │ ├── UserService.cs # 用户管理服务
│ │ │ ├── DeptService.cs # 部门管理服务
│ │ │ ├── RoleService.cs # 角色管理服务
│ │ │ ├── MenuService.cs # 菜单管理服务
│ │ │ └── ConfigService.cs # 配置管理服务
│ │ └── Dtos/ # 数据传输对象
│ │
│ ├── SwipeFlow.Core/ # 核心层(实体 & 配置)
│ │ ├── Entities/ # 数据库实体(SqlSugar CodeFirst)
│ │ │ ├── VideoInfo.cs # 视频信息实体
│ │ │ ├── VideoCategory.cs # 视频分类(频道)实体
│ │ │ ├── ThirdPartyAuth.cs # 第三方登录配置实体
│ │ │ ├── VideoLike.cs # 用户点赞记录
│ │ │ ├── VideoFavorite.cs # 用户收藏记录
│ │ │ ├── DataSource.cs # 数据源配置实体
│ │ │ ├── SysUser.cs # 系统用户实体
│ │ │ ├── SysDept.cs # 部门实体
│ │ │ ├── SysRole.cs # 角色实体
│ │ │ ├── SysMenu.cs # 菜单实体
│ │ │ ├── SysConfig.cs # 系统配置实体
│ │ │ ├── SysUserRole.cs # 用户角色关联
│ │ │ └── SysRoleMenu.cs # 角色菜单关联
│ │ ├── Enums/ # 枚举定义
│ │ └── Options/ # 配置选项类
│ │
│ ├── SwipeFlow.Database/ # 数据库层
│ │ ├── SqlSugarSetup.cs # SqlSugar 服务注册与配置
│ │ ├── DbContext.cs # 数据库上下文
│ │ └── SeedData.cs # 种子数据初始化
│ │
│ └── SwipeFlow.sln # 解决方案文件
│
├── admin/ # 管理后台(Vben Admin 5 + Element Plus)
│ ├── apps/
│ │ └── web-ele/ # Element Plus 子应用
│ │ ├── src/
│ │ │ ├── api/ # 接口请求(对接后端 Furion API)
│ │ │ │ ├── video.ts # 视频管理接口
│ │ │ │ ├── datasource.ts # 数据源接口
│ │ │ │ ├── auth.ts # 认证接口
│ │ │ │ ├── user.ts # 用户管理接口
│ │ │ │ ├── dept.ts # 部门管理接口
│ │ │ │ ├── role.ts # 角色管理接口
│ │ │ │ ├── menu.ts # 菜单管理接口
│ │ │ │ └── config.ts # 配置管理接口
│ │ │ ├── views/ # 页面视图
│ │ │ │ ├── video/ # 视频管理页面
│ │ │ │ ├── datasource/ # 数据源管理页面
│ │ │ │ ├── dashboard/ # 仪表盘
│ │ │ │ └── system/ # 系统管理
│ │ │ │ ├── user/ # 用户管理
│ │ │ │ ├── dept/ # 部门管理
│ │ │ │ ├── role/ # 角色管理
│ │ │ │ ├── menu/ # 菜单管理
│ │ │ │ └── config/ # 配置管理
│ │ │ ├── router/ # 路由配置
│ │ │ └── store/ # Pinia 状态管理
│ │ ├── .env.development # 开发环境变量
│ │ └── vite.config.mts # Vite 配置
│ ├── packages/ # Monorepo 共享包
│ │ ├── @core/ # 核心基础包
│ │ └── effects/ # 副作用包(权限、Hooks、布局)
│ ├── pnpm-workspace.yaml # pnpm workspace 配置
│ ├── turbo.json # Turborepo 配置
│ └── package.json
│
├── client/ # 前端播放器(Vue 3 + Vite)
│ ├── src/
│ │ ├── components/
│ │ │ ├── VideoPlayer/ # 播放器核心组件(基于 xgplayer)
│ │ │ ├── VideoFeed/ # 视频流容器(CSS Scroll Snap 滑动切换)
│ │ │ ├── ActionBar/ # 侧边操作栏(点赞/收藏/分享)
│ │ │ ├── UserInfo/ # 用户信息浮层
│ │ │ └── AuthPanel/ # 用户登录面板
│ │ ├── composables/
│ │ │ ├── useVideoPlayer.ts # 播放逻辑 Hook
│ │ │ ├── useSwipe.ts # 滑动手势 Hook
│ │ │ └── usePreload.ts # 预加载策略 Hook
│ │ ├── api/ # 接口请求封装
│ │ │ ├── video.ts # 视频接口
│ │ │ ├── auth.ts # 用户认证接口
│ │ │ └── interaction.ts # 点赞/收藏接口
│ │ ├── stores/ # Pinia 状态管理
│ │ │ ├── video.ts # 视频列表状态
│ │ │ └── user.ts # 当前用户状态
│ │ ├── App.vue
│ │ └── main.ts
│ ├── index.html
│ ├── vite.config.ts
│ └── package.json
│
├── docs/ # 项目文档
├── docker-compose.yml # Docker 一键部署
├── LICENSE
└── README.md
```
## 后端配置说明
### SqlSugar 数据库配置(appsettings.json)
```json
{
"ConnectionConfigs": [
{
"DbType": "Sqlite",
"ConnectionString": "DataSource=./swipeflow.db",
"EnableUnderLine": false,
"EnableDiffLog": false,
"IsAutoCloseConnection": true,
"InitKeyType": "Attribute"
}
]
}
```
### Furion 服务注册(SqlSugarSetup.cs)
```csharp
public static void AddSqlSugarSetup(this IServiceCollection services)
{
var config = App.Configuration.GetSection("ConnectionConfigs")
.Get>();
var db = new SqlSugarScope(config, db =>
{
// 开发环境输出 SQL 日志
db.Aop.OnLogExecuting = (sql, pars) =>
{
App.GetService>()
.LogInformation("SQL: {Sql}", sql);
};
});
services.AddSingleton(db);
}
```
### 实体示例(CodeFirst 自动建表)
```csharp
[SugarTable("video_info", TableDescription = "视频信息表")]
public class VideoInfo : EntityBase
{
[SugarColumn(ColumnName = "title", ColumnDescription = "视频标题", Length = 200)]
public string Title { get; set; }
[SugarColumn(ColumnName = "cover_url", ColumnDescription = "封面地址", Length = 500)]
public string CoverUrl { get; set; }
[SugarColumn(ColumnName = "video_url", ColumnDescription = "视频播放地址", Length = 500)]
public string VideoUrl { get; set; }
[SugarColumn(ColumnName = "author", ColumnDescription = "作者", Length = 100)]
public string Author { get; set; }
[SugarColumn(ColumnName = "source", ColumnDescription = "来源标识", Length = 50)]
public string Source { get; set; }
[SugarColumn(ColumnName = "duration", ColumnDescription = "时长(秒)")]
public int Duration { get; set; }
[SugarColumn(ColumnName = "channel_id", ColumnDescription = "所属频道ID")]
public int? ChannelId { get; set; }
[SugarColumn(ColumnName = "like_count", ColumnDescription = "点赞数")]
public int LikeCount { get; set; } = 0;
[SugarColumn(ColumnName = "favorite_count", ColumnDescription = "收藏数")]
public int FavoriteCount { get; set; } = 0;
[SugarColumn(ColumnName = "sort_order", ColumnDescription = "排序")]
public int SortOrder { get; set; }
[SugarColumn(ColumnName = "status", ColumnDescription = "状态: 0正常 1垃圾箱 2已删除")]
public int Status { get; set; } = 0;
}
```
### 用户互动实体示例
```csharp
[SugarTable("video_like", TableDescription = "用户点赞记录表")]
public class VideoLike
{
[SugarColumn(IsPrimaryKey = true, ColumnName = "video_id", ColumnDescription = "视频ID")]
public long VideoId { get; set; }
[SugarColumn(IsPrimaryKey = true, ColumnName = "user_id", ColumnDescription = "用户ID")]
public long UserId { get; set; }
[SugarColumn(ColumnName = "create_time", ColumnDescription = "点赞时间")]
public DateTime CreateTime { get; set; } = DateTime.Now;
}
[SugarTable("video_favorite", TableDescription = "用户收藏记录表")]
public class VideoFavorite
{
[SugarColumn(IsPrimaryKey = true, ColumnName = "video_id", ColumnDescription = "视频ID")]
public long VideoId { get; set; }
[SugarColumn(IsPrimaryKey = true, ColumnName = "user_id", ColumnDescription = "用户ID")]
public long UserId { get; set; }
[SugarColumn(ColumnName = "create_time", ColumnDescription = "收藏时间")]
public DateTime CreateTime { get; set; } = DateTime.Now;
}
```
### 系统管理实体示例
```csharp
[SugarTable("sys_dept", TableDescription = "部门表")]
public class SysDept : EntityBase
{
[SugarColumn(ColumnName = "parent_id", ColumnDescription = "上级部门ID")]
public long? ParentId { get; set; }
[SugarColumn(ColumnName = "dept_name", ColumnDescription = "部门名称", Length = 100)]
public string DeptName { get; set; }
[SugarColumn(ColumnName = "order_num", ColumnDescription = "排序")]
public int OrderNum { get; set; }
[SugarColumn(ColumnName = "status", ColumnDescription = "状态: 0正常 1停用")]
public int Status { get; set; } = 0;
}
[SugarTable("sys_role", TableDescription = "角色表")]
public class SysRole : EntityBase
{
[SugarColumn(ColumnName = "role_name", ColumnDescription = "角色名称", Length = 100)]
public string RoleName { get; set; }
[SugarColumn(ColumnName = "role_key", ColumnDescription = "角色权限标识", Length = 100)]
public string RoleKey { get; set; }
[SugarColumn(ColumnName = "order_num", ColumnDescription = "排序")]
public int OrderNum { get; set; }
[SugarColumn(ColumnName = "status", ColumnDescription = "状态: 0正常 1停用")]
public int Status { get; set; } = 0;
}
[SugarTable("sys_menu", TableDescription = "菜单表")]
public class SysMenu : EntityBase
{
[SugarColumn(ColumnName = "parent_id", ColumnDescription = "上级菜单ID")]
public long? ParentId { get; set; }
[SugarColumn(ColumnName = "menu_name", ColumnDescription = "菜单名称", Length = 100)]
public string MenuName { get; set; }
[SugarColumn(ColumnName = "menu_type", ColumnDescription = "菜单类型: M目录 C菜单 F按钮")]
public string MenuType { get; set; }
[SugarColumn(ColumnName = "path", ColumnDescription = "路由地址", Length = 200)]
public string Path { get; set; }
[SugarColumn(ColumnName = "component", ColumnDescription = "组件路径", Length = 200)]
public string Component { get; set; }
[SugarColumn(ColumnName = "perms", ColumnDescription = "权限标识", Length = 200)]
public string Perms { get; set; }
[SugarColumn(ColumnName = "icon", ColumnDescription = "菜单图标", Length = 100)]
public string Icon { get; set; }
[SugarColumn(ColumnName = "order_num", ColumnDescription = "排序")]
public int OrderNum { get; set; }
[SugarColumn(ColumnName = "status", ColumnDescription = "状态: 0正常 1停用")]
public int Status { get; set; } = 0;
}
[SugarTable("sys_config", TableDescription = "系统配置表")]
public class SysConfig : EntityBase
{
[SugarColumn(ColumnName = "config_name", ColumnDescription = "配置名称", Length = 100)]
public string ConfigName { get; set; }
[SugarColumn(ColumnName = "config_key", ColumnDescription = "配置键", Length = 100)]
public string ConfigKey { get; set; }
[SugarColumn(ColumnName = "config_value", ColumnDescription = "配置值", Length = 500)]
public string ConfigValue { get; set; }
[SugarColumn(ColumnName = "config_type", ColumnDescription = "配置类型: 0系统 1自定义")]
public int ConfigType { get; set; } = 1;
[SugarColumn(ColumnName = "remark", ColumnDescription = "备注", Length = 500)]
public string Remark { get; set; }
}
[SugarTable("sys_user_role", TableDescription = "用户角色关联表")]
public class SysUserRole
{
[SugarColumn(IsPrimaryKey = true, ColumnName = "user_id", ColumnDescription = "用户ID")]
public long UserId { get; set; }
[SugarColumn(IsPrimaryKey = true, ColumnName = "role_id", ColumnDescription = "角色ID")]
public long RoleId { get; set; }
}
[SugarTable("sys_role_menu", TableDescription = "角色菜单关联表")]
public class SysRoleMenu
{
[SugarColumn(IsPrimaryKey = true, ColumnName = "role_id", ColumnDescription = "角色ID")]
public long RoleId { get; set; }
[SugarColumn(IsPrimaryKey = true, ColumnName = "menu_id", ColumnDescription = "菜单ID")]
public long MenuId { get; set; }
}
```
### 第三方登录配置实体示例
```csharp
[SugarTable("third_party_auth", TableDescription = "第三方登录配置表")]
public class ThirdPartyAuth : EntityBase
{
[SugarColumn(ColumnName = "site_name", ColumnDescription = "站点名称", Length = 100)]
public string SiteName { get; set; }
[SugarColumn(ColumnName = "username", ColumnDescription = "用户名", Length = 100)]
public string Username { get; set; }
[SugarColumn(ColumnName = "password", ColumnDescription = "密码", Length = 200)]
public string Password { get; set; }
[SugarColumn(ColumnName = "site_url", ColumnDescription = "站点网址", Length = 500)]
public string SiteUrl { get; set; }
[SugarColumn(ColumnName = "api_url", ColumnDescription = "接口地址", Length = 500)]
public string ApiUrl { get; set; }
[SugarColumn(ColumnName = "cookies", ColumnDescription = "登录Cookie", Length = -1)]
public string Cookies { get; set; }
[SugarColumn(ColumnName = "token", ColumnDescription = "访问令牌", Length = 500)]
public string Token { get; set; }
[SugarColumn(ColumnName = "login_time", ColumnDescription = "登录时间")]
public DateTime? LoginTime { get; set; }
[SugarColumn(ColumnName = "expire_time", ColumnDescription = "失效时间")]
public DateTime? ExpireTime { get; set; }
[SugarColumn(ColumnName = "is_valid", ColumnDescription = "是否有效: 0无效 1有效")]
public int IsValid { get; set; } = 1;
[SugarColumn(ColumnName = "remark", ColumnDescription = "备注", Length = 500)]
public string Remark { get; set; }
}
```
### 动态 WebAPI 示例(Furion)
```csharp
public class VideoService : IDynamicApiController
{
private readonly ISqlSugarClient _db;
public VideoService(ISqlSugarClient db)
{
_db = db;
}
///
/// 获取视频列表(分页)
///
public async Task> GetList(int page = 1, int size = 10)
{
var total = 0;
var list = await _db.Queryable()
.Where(v => v.Status == 1)
.OrderBy(v => v.SortOrder)
.ToPageListAsync(page, size, total);
return new PagedResult
{
Items = list.Adapt>(),
Total = total
};
}
///
/// 新增视频
///
[HttpPost]
public async Task Add([Required] VideoCreateDto dto)
{
var entity = dto.Adapt();
return await _db.Insertable(entity).ExecuteCommandAsync() > 0;
}
}
```
> Furion 的 `IDynamicApiController` 接口会自动将 `VideoService` 的方法映射为 RESTful API 端点,无需手动编写 Controller。
## 数据源接入
> **重要说明**:今日头条、抖音等平台没有面向第三方开发者的开放平台 API。以下方案基于用户身份登录获取 Token,通过调用平台内部接口获取个人收藏数据,仅限个人学习研究使用。
### 方式一:第三方收藏同步
#### 整体架构
支持接入多个第三方平台的收藏列表,每个平台有独立的同步服务,通过 `SysConfig` 配置开关控制同步任务的开启与关闭:
```
┌──────────────────────────────────────────────────┐
│ 定时同步调度器 │
│ 遍历所有平台,检查 SysConfig 开关,执行同步任务 │
└──────┬──────────┬──────────┬──────────────────────┘
│ │ │
┌──────▼──┐ ┌─────▼────┐ ┌──▼─────────┐
│ 头条同步 │ │ 抖音同步 │ │ ... 扩展 │
│ 服务 │ │ 服务 │ │ 更多平台 │
└─────────┘ └──────────┘ └────────────┘
│ │ │
└──────────┴──────────┘
│
┌────────────▼────────────┐
│ third_party_auth 表 │
│ (各平台认证信息) │
└─────────────────────────┘
```
#### 同步开关配置
在 `SysConfig` 中按平台名称配置同步开关,可在管理后台的「配置管理」页面中开启或关闭:
| 配置键 | 配置值 | 说明 |
|--------|--------|------|
| `sync_toutiao_enabled` | true/false | 是否开启今日头条收藏同步 |
| `sync_douyin_enabled` | true/false | 是否开启抖音收藏同步 |
| `sync_toutiao_interval` | 3600 | 头条同步间隔(秒) |
| `sync_douyin_interval` | 3600 | 抖音同步间隔(秒) |
#### 通用同步接口
```csharp
///
/// 平台同步服务抽象基类
///
public abstract class BaseSyncService
{
protected readonly ISqlSugarClient _db;
protected readonly IHttpClientFactory _httpClientFactory;
/// 平台名称,用于读取认证配置和同步开关
public abstract string PlatformName { get; }
/// 来源标识,写入视频的 source 字段
public abstract string SourceTag { get; }
/// 拉取指定平台收藏列表
protected abstract Task> FetchFavoritesAsync(ThirdPartyAuth auth);
///
/// 执行同步(检查开关 -> 读取认证 -> 拉取列表 -> 去重写入)
///
public async Task SyncFavorites()
{
// 1. 检查同步开关
var enabled = await _db.Queryable()
.Where(c => c.ConfigKey == $"sync_{SourceTag}_enabled"
&& c.ConfigValue == "true")
.AnyAsync();
if (!enabled)
throw new Exception($"平台 {PlatformName} 同步未开启,请在系统配置中启用");
// 2. 读取认证信息
var auth = await _db.Queryable()
.Where(a => a.SiteName == PlatformName && a.IsValid == 1)
.FirstAsync();
if (auth == null || string.IsNullOrEmpty(auth.Token))
throw new Exception($"未配置有效的 {PlatformName} 登录信息");
// 3. 拉取收藏列表
var allVideos = await FetchFavoritesAsync(auth);
// 4. 按视频 URL 去重,仅新增本地不存在的视频
var existingUrls = _db.Queryable()
.Where(v => v.Source == SourceTag)
.Select(v => v.VideoUrl)
.ToList();
var newVideos = allVideos
.Where(v => !existingUrls.Contains(v.VideoUrl))
.ToList();
if (newVideos.Any())
{
await _db.Insertable(newVideos).ExecuteCommandAsync();
}
// 5. 更新认证表中的登录时间
auth.LoginTime = DateTime.Now;
await _db.Updateable(auth).UpdateColumns(a => new { a.LoginTime }).ExecuteCommandAsync();
return new SyncResult
{
TotalFetched = allVideos.Count,
NewAdded = newVideos.Count
};
}
}
```
#### 今日头条同步实现
```csharp
public class ToutiaoSyncService : BaseSyncService
{
public override string PlatformName => "今日头条";
public override string SourceTag => "toutiao";
public ToutiaoSyncService(ISqlSugarClient db, IHttpClientFactory httpClientFactory)
: base(db, httpClientFactory) { }
protected override async Task> FetchFavoritesAsync(ThirdPartyAuth auth)
{
var client = _httpClientFactory.CreateClient("Toutiao");
client.DefaultRequestHeaders.Add("Cookie",
$"sessionid={auth.Token}; ttwid={auth.Cookies}");
client.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...");
var allVideos = new List();
long maxBehotTime = 0;
bool hasMore = true;
while (hasMore)
{
var url = $"https://www.toutiao.com/api/pc/list/user/feed"
+ $"?category=pc_profile_favorites"
+ $"&max_behot_time={maxBehotTime}"
+ $"&count=20";
var response = await client.GetAsync(url);
var json = await response.Content.ReadFromJsonAsync();
if (json?.Data == null || !json.Data.Any()) break;
foreach (var item in json.Data)
{
if (item.VideoUrl == null) continue;
allVideos.Add(new VideoInfo
{
Title = item.Title,
CoverUrl = item.CoverUrl,
VideoUrl = item.VideoUrl,
Author = item.Author,
Source = SourceTag,
Duration = item.Duration,
Status = 0,
ChannelId = null
});
}
hasMore = json.HasMore;
maxBehotTime = json.NextMaxBehotTime;
}
return allVideos;
}
}
```
#### 抖音收藏同步实现
```csharp
public class DouyinSyncService : BaseSyncService
{
public override string PlatformName => "抖音";
public override string SourceTag => "douyin";
public DouyinSyncService(ISqlSugarClient db, IHttpClientFactory httpClientFactory)
: base(db, httpClientFactory) { }
protected override async Task> FetchFavoritesAsync(ThirdPartyAuth auth)
{
var client = _httpClientFactory.CreateClient("Douyin");
client.DefaultRequestHeaders.Add("Cookie",
$"sessionid={auth.Token}; ttwid={auth.Cookies}");
client.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...");
var allVideos = new List();
// 抖音收藏接口需根据实际抓包分析实现
// 接口地址和参数可能随版本更新变化
return allVideos;
}
}
```
#### 同步调度器(Furion 定时任务)
```csharp
public class SyncSchedulerJob : IJob
{
private readonly ISqlSugarClient _db;
private readonly IEnumerable _syncServices;
public SyncSchedulerJob(ISqlSugarClient db,
IEnumerable syncServices)
{
_db = db;
_syncServices = syncServices;
}
public async Task ExecuteAsync()
{
foreach (var service in _syncServices)
{
try
{
// 检查该平台同步开关
var enabled = await _db.Queryable()
.Where(c => c.ConfigKey == $"sync_{service.SourceTag}_enabled"
&& c.ConfigValue == "true")
.AnyAsync();
if (!enabled) continue;
// 检查同步间隔
var interval = await _db.Queryable()
.Where(c => c.ConfigKey == $"sync_{service.SourceTag}_interval")
.Select(c => c.ConfigValue)
.FirstAsync();
// 检查上次同步时间(此处省略间隔判断逻辑)
var result = await service.SyncFavorites();
Log.Information("{Platform} 同步完成: 获取 {Total} 条, 新增 {New} 条",
service.PlatformName, result.TotalFetched, result.NewAdded);
}
catch (Exception ex)
{
Log.Error(ex, "{Platform} 同步异常", service.PlatformName);
}
}
}
}
```
#### 同步流程总结
1. 定时任务触发,遍历所有已注册的同步服务
2. 每个服务先检查 `SysConfig` 中对应的同步开关(`sync_{source}_enabled`)是否为 `true`
3. 开关关闭则跳过,开关开启则从 `third_party_auth` 表读取认证信息
4. 调用平台内部接口拉取收藏列表
5. 按 `video_url` 去重,仅新增本地不存在的视频
6. 线上已移除的视频,本地保留不动
7. 更新认证表的登录时间
8. 手动同步时,可在管理后台选择指定平台触发同步
### 方式二:后台手动录入
通过 Vben Admin 5 管理后台直接添加视频信息,新增时按 `video_url` 检测是否已存在,防止重复录入:
| 字段 | 类型 | 说明 |
|------|------|------|
| `title` | string | 视频标题 |
| `coverUrl` | string | 封面图地址 |
| `videoUrl` | string | 视频播放地址(MP4/HLS) |
| `author` | string | 作者名称 |
| `source` | string | 来源标识 |
| `duration` | int | 视频时长(秒) |
| `sortOrder` | int | 排序权重 |
### 方式三:批量导入
支持通过 JSON 文件批量导入视频数据,导入时按 `video_url` 去重,已存在的视频自动跳过:
```bash
dotnet run --project server/SwipeFlow.Web.Entry -- --import ./data/videos.json
```
#### 去重策略
所有视频导入方式(同步、手动录入、批量导入)均基于 `video_url` 字段进行去重:
| 导入方式 | 去重依据 | 重复处理 | 覆盖更新 |
|----------|----------|----------|----------|
| 头条同步 | `video_url` | 跳过,保留本地记录 | 否 |
| 后台手动录入 | `video_url` | 提示已存在,不允许提交 | 否 |
| 批量导入 | `video_url` | 自动跳过,日志记录被跳过的条目 | 否 |
## 播放器核心能力
| 能力 | 实现方式 |
|------|----------|
| 竖屏全屏 | `100vh` + `object-fit: cover` |
| 滑动切换 | CSS `scroll-snap-type: y mandatory` |
| 自动播放/暂停 | `IntersectionObserver`(阈值 50%) |
| iOS 内联播放 | `playsinline` + `webkit-playsinline` |
| 静音起播 | 首帧 `muted`,用户交互后开声音 |
| 预加载 | 当前视频前后各预载 1 个 |
| 播放器内核 | xgplayer(西瓜播放器),支持 MP4/HLS |
## 用户互动功能
| 功能 | 说明 |
|------|------|
| 用户注册/登录 | 前端播放器独立账号,JWT 鉴权 |
| 点赞 | 点击爱心图标点赞/取消点赞,实时更新计数 |
| 收藏 | 点击收藏图标收藏视频,可在个人中心查看 |
| 操作状态同步 | 已点赞/已收藏的视频状态随页面刷新保持 |
| 未登录引导 | 未登录用户点击操作按钮时弹出登录面板 |
## 开发路线图
### Phase 1 — 基础框架搭建
- [ ] 后端 .NET 8 + Furion + SqlSugar + SQLite 项目初始化
- [ ] 管理后台 Vben Admin 5 (Element Plus) 项目初始化
- [ ] 前端播放器 Vue 3 + Vite + xgplayer 项目初始化
- [ ] 视频实体 CodeFirst 自动建表
- [ ] 视频 CRUD 动态 WebAPI
### Phase 2 — 核心功能
- [ ] 竖屏滑动播放器完整实现(Scroll Snap + xgplayer)
- [ ] 管理后台视频管理页面(列表/新增/编辑/删除)
- [ ] JWT 鉴权 + 管理后台登录对接
- [ ] 视频文件上传(本地/OSS)
- [ ] 前端用户注册/登录及点赞收藏功能
### Phase 3 — 系统管理(RBAC)
- [ ] 部门管理:树形结构、增删改查
- [ ] 角色管理:角色 CRUD、权限标识配置
- [ ] 菜单管理:树形菜单、目录/菜单/按钮三级类型
- [ ] 用户管理:用户 CRUD、分配角色、所属部门
- [ ] 角色菜单关联:按角色分配菜单权限
- [ ] 配置管理:系统参数键值对维护
### Phase 4 — 头条收藏同步
- [ ] 头条 Token 配置管理页面(Vben Admin)
- [ ] 头条收藏接口对接与数据解析
- [ ] 同步服务 + 去重逻辑
- [ ] 定时任务自动同步
- [ ] Token 过期检测与提醒
### Phase 5 — 体验优化
- [ ] 视频预加载与缓存策略
- [ ] 骨架屏与加载动画
- [ ] 点赞、收藏、分享交互
- [ ] 视频转码与自适应码率(HLS)
### Phase 6 — 部署与运维
- [ ] Docker 一键部署(docker-compose)
- [ ] Nginx 反向代理配置
- [ ] CI/CD 自动化部署
- [ ] PWA 离线支持
## Docker 部署
```yaml
# docker-compose.yml
version: '3.8'
services:
server:
build:
context: ./server
dockerfile: Dockerfile
ports:
- "5000:8080"
volumes:
- ./data:/app/Data # SQLite 数据库持久化
- ./uploads:/app/uploads # 上传文件持久化
environment:
- ASPNETCORE_ENVIRONMENT=Production
admin:
build:
context: ./admin
dockerfile: Dockerfile
ports:
- "5777:80"
depends_on:
- server
client:
build:
context: ./client
dockerfile: Dockerfile
ports:
- "5173:80"
depends_on:
- server
```
## 贡献指南
欢迎提交 Issue 和 Pull Request!
1. Fork 本仓库
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'feat: add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 开启 Pull Request
## 开源协议
本项目基于 MIT License 开源。
- 后端框架 Furion — MIT/Apache-2.0 双重协议
- ORM 框架 SqlSugar — MIT 协议
- 管理后台 Vben Admin — MIT 协议
> **合规提醒**:通过用户 Token 获取的今日头条收藏数据仅限个人学习研究使用,不得用于商业用途。请遵守今日头条的服务条款,尊重内容创作者的版权。Token 信息请妥善保管,避免泄露。
---
**如果这个项目对你有帮助,欢迎在 [Gitee](https://gitee.com/singlr/swipeflow) 上 Star 支持一下!**