diff --git a/Properties/PublishProfiles/docker.pubxml.user b/Properties/PublishProfiles/docker.pubxml.user
index dbd3b95..d189383 100644
--- a/Properties/PublishProfiles/docker.pubxml.user
+++ b/Properties/PublishProfiles/docker.pubxml.user
@@ -3,7 +3,7 @@
<_PublishTargetUrl>E:\code\dysync\bin\Release\net6.0\publish\
- True|2026-01-28T05:01:14.1677037Z||;True|2026-01-28T12:36:45.7819776+08:00||;True|2026-01-28T12:09:15.6540204+08:00||;True|2026-01-28T12:08:40.0490617+08:00||;True|2026-01-28T11:58:55.4078870+08:00||;True|2026-01-27T22:37:03.3304942+08:00||;True|2026-01-27T22:05:40.4316281+08:00||;True|2026-01-26T22:30:43.5083924+08:00||;False|2026-01-26T22:30:33.7355213+08:00||;True|2026-01-26T22:26:26.0218548+08:00||;True|2026-01-26T22:21:32.3171217+08:00||;True|2026-01-26T22:20:40.3916780+08:00||;True|2026-01-26T22:16:19.1017273+08:00||;True|2026-01-26T22:13:34.2028883+08:00||;True|2026-01-26T21:46:23.7117812+08:00||;True|2026-01-26T21:41:19.6963033+08:00||;True|2026-01-25T21:27:13.9893777+08:00||;True|2026-01-24T21:45:22.8493624+08:00||;True|2026-01-24T21:31:35.4635265+08:00||;True|2026-01-24T21:27:31.3098506+08:00||;True|2026-01-24T15:47:01.6934638+08:00||;True|2026-01-24T15:32:35.6316009+08:00||;True|2026-01-24T15:16:28.7216639+08:00||;True|2026-01-23T22:14:26.8066324+08:00||;True|2026-01-23T13:36:48.4086406+08:00||;True|2026-01-23T13:30:27.2924204+08:00||;True|2026-01-23T13:02:53.3219368+08:00||;True|2026-01-23T13:01:54.3466638+08:00||;True|2026-01-23T13:01:28.8560809+08:00||;True|2026-01-22T23:18:32.0400436+08:00||;True|2026-01-22T22:29:29.5746209+08:00||;True|2026-01-22T21:32:21.7284081+08:00||;True|2026-01-22T21:20:26.5985955+08:00||;False|2026-01-22T21:20:21.1372539+08:00||;True|2026-01-22T21:11:18.3771739+08:00||;True|2026-01-22T20:58:55.9862778+08:00||;True|2026-01-22T20:51:48.6699461+08:00||;False|2026-01-22T20:51:42.2079210+08:00||;True|2026-01-22T20:50:34.7280114+08:00||;True|2026-01-22T20:50:11.1588492+08:00||;True|2026-01-22T16:11:14.0626037+08:00||;True|2026-01-22T15:53:16.6377347+08:00||;True|2026-01-22T15:45:34.0935091+08:00||;True|2026-01-22T12:44:28.1577014+08:00||;True|2026-01-22T12:40:45.7449335+08:00||;True|2026-01-22T12:39:07.0683214+08:00||;True|2026-01-22T09:55:49.7086190+08:00||;True|2026-01-22T01:37:53.6972395+08:00||;True|2026-01-22T01:34:14.2142463+08:00||;True|2026-01-22T01:32:50.9228911+08:00||;True|2026-01-22T01:29:43.8871986+08:00||;True|2026-01-22T01:29:40.7838709+08:00||;True|2026-01-22T01:27:34.4763970+08:00||;True|2026-01-22T01:26:55.0117367+08:00||;True|2026-01-22T01:23:19.4789587+08:00||;True|2026-01-19T10:19:44.8997385+08:00||;True|2026-01-19T10:11:10.2305307+08:00||;True|2026-01-17T00:52:08.8180346+08:00||;False|2026-01-17T00:52:03.1829000+08:00||;True|2026-01-17T00:45:04.0498090+08:00||;True|2026-01-16T21:00:57.7239379+08:00||;True|2026-01-16T21:00:03.3912989+08:00||;True|2026-01-16T20:56:08.5505592+08:00||;True|2026-01-16T20:46:32.3067302+08:00||;True|2026-01-15T22:18:57.9835738+08:00||;True|2026-01-15T22:07:25.4753938+08:00||;True|2026-01-15T22:07:14.8243754+08:00||;True|2026-01-15T21:40:13.2613927+08:00||;True|2026-01-15T20:57:56.6291682+08:00||;True|2026-01-15T20:57:47.1363536+08:00||;True|2026-01-15T20:08:44.4710883+08:00||;True|2026-01-15T20:04:51.8284314+08:00||;False|2026-01-15T20:04:45.3258155+08:00||;True|2026-01-15T19:59:11.7907672+08:00||;True|2026-01-15T01:10:48.9066941+08:00||;True|2026-01-15T01:03:23.6216975+08:00||;True|2026-01-15T01:03:16.3364591+08:00||;False|2026-01-15T01:03:10.5654436+08:00||;True|2026-01-15T00:49:21.6559622+08:00||;True|2026-01-15T00:10:42.7288439+08:00||;True|2026-01-15T00:06:28.4241682+08:00||;True|2026-01-13T22:34:15.2372411+08:00||;True|2026-01-13T22:26:46.4522947+08:00||;True|2026-01-13T22:15:56.9735179+08:00||;True|2026-01-13T22:15:51.8250677+08:00||;False|2026-01-13T22:15:46.6608690+08:00||;True|2026-01-13T22:03:06.4683237+08:00||;False|2026-01-13T22:03:01.2036027+08:00||;True|2026-01-13T21:36:05.8776209+08:00||;True|2026-01-13T21:33:02.0766289+08:00||;False|2026-01-13T21:32:57.1976141+08:00||;True|2026-01-13T19:14:35.2519439+08:00||;True|2026-01-13T15:07:50.0836745+08:00||;False|2026-01-13T15:06:35.5929685+08:00||;True|2026-01-13T10:21:41.6012776+08:00||;True|2026-01-13T09:57:25.4788847+08:00||;True|2026-01-13T09:53:14.4900360+08:00||;True|2026-01-13T09:23:53.6465295+08:00||;True|2026-01-12T22:04:14.5886955+08:00||;False|2026-01-12T22:04:08.2008101+08:00||;
+ True|2026-01-28T14:01:00.8412391Z||;False|2026-01-28T21:49:29.1242233+08:00||;True|2026-01-28T21:48:26.1707204+08:00||;True|2026-01-28T21:48:05.8291762+08:00||;False|2026-01-28T21:48:01.1596391+08:00||;True|2026-01-28T21:37:59.4831763+08:00||;False|2026-01-28T21:37:53.5855464+08:00||;True|2026-01-28T21:27:51.1872828+08:00||;True|2026-01-28T13:01:14.1677037+08:00||;True|2026-01-28T12:36:45.7819776+08:00||;True|2026-01-28T12:09:15.6540204+08:00||;True|2026-01-28T12:08:40.0490617+08:00||;True|2026-01-28T11:58:55.4078870+08:00||;True|2026-01-27T22:37:03.3304942+08:00||;True|2026-01-27T22:05:40.4316281+08:00||;True|2026-01-26T22:30:43.5083924+08:00||;False|2026-01-26T22:30:33.7355213+08:00||;True|2026-01-26T22:26:26.0218548+08:00||;True|2026-01-26T22:21:32.3171217+08:00||;True|2026-01-26T22:20:40.3916780+08:00||;True|2026-01-26T22:16:19.1017273+08:00||;True|2026-01-26T22:13:34.2028883+08:00||;True|2026-01-26T21:46:23.7117812+08:00||;True|2026-01-26T21:41:19.6963033+08:00||;True|2026-01-25T21:27:13.9893777+08:00||;True|2026-01-24T21:45:22.8493624+08:00||;True|2026-01-24T21:31:35.4635265+08:00||;True|2026-01-24T21:27:31.3098506+08:00||;True|2026-01-24T15:47:01.6934638+08:00||;True|2026-01-24T15:32:35.6316009+08:00||;True|2026-01-24T15:16:28.7216639+08:00||;True|2026-01-23T22:14:26.8066324+08:00||;True|2026-01-23T13:36:48.4086406+08:00||;True|2026-01-23T13:30:27.2924204+08:00||;True|2026-01-23T13:02:53.3219368+08:00||;True|2026-01-23T13:01:54.3466638+08:00||;True|2026-01-23T13:01:28.8560809+08:00||;True|2026-01-22T23:18:32.0400436+08:00||;True|2026-01-22T22:29:29.5746209+08:00||;True|2026-01-22T21:32:21.7284081+08:00||;True|2026-01-22T21:20:26.5985955+08:00||;False|2026-01-22T21:20:21.1372539+08:00||;True|2026-01-22T21:11:18.3771739+08:00||;True|2026-01-22T20:58:55.9862778+08:00||;True|2026-01-22T20:51:48.6699461+08:00||;False|2026-01-22T20:51:42.2079210+08:00||;True|2026-01-22T20:50:34.7280114+08:00||;True|2026-01-22T20:50:11.1588492+08:00||;True|2026-01-22T16:11:14.0626037+08:00||;True|2026-01-22T15:53:16.6377347+08:00||;True|2026-01-22T15:45:34.0935091+08:00||;True|2026-01-22T12:44:28.1577014+08:00||;True|2026-01-22T12:40:45.7449335+08:00||;True|2026-01-22T12:39:07.0683214+08:00||;True|2026-01-22T09:55:49.7086190+08:00||;True|2026-01-22T01:37:53.6972395+08:00||;True|2026-01-22T01:34:14.2142463+08:00||;True|2026-01-22T01:32:50.9228911+08:00||;True|2026-01-22T01:29:43.8871986+08:00||;True|2026-01-22T01:29:40.7838709+08:00||;True|2026-01-22T01:27:34.4763970+08:00||;True|2026-01-22T01:26:55.0117367+08:00||;True|2026-01-22T01:23:19.4789587+08:00||;True|2026-01-19T10:19:44.8997385+08:00||;True|2026-01-19T10:11:10.2305307+08:00||;True|2026-01-17T00:52:08.8180346+08:00||;False|2026-01-17T00:52:03.1829000+08:00||;True|2026-01-17T00:45:04.0498090+08:00||;True|2026-01-16T21:00:57.7239379+08:00||;True|2026-01-16T21:00:03.3912989+08:00||;True|2026-01-16T20:56:08.5505592+08:00||;True|2026-01-16T20:46:32.3067302+08:00||;True|2026-01-15T22:18:57.9835738+08:00||;True|2026-01-15T22:07:25.4753938+08:00||;True|2026-01-15T22:07:14.8243754+08:00||;True|2026-01-15T21:40:13.2613927+08:00||;True|2026-01-15T20:57:56.6291682+08:00||;True|2026-01-15T20:57:47.1363536+08:00||;True|2026-01-15T20:08:44.4710883+08:00||;True|2026-01-15T20:04:51.8284314+08:00||;False|2026-01-15T20:04:45.3258155+08:00||;True|2026-01-15T19:59:11.7907672+08:00||;True|2026-01-15T01:10:48.9066941+08:00||;True|2026-01-15T01:03:23.6216975+08:00||;True|2026-01-15T01:03:16.3364591+08:00||;False|2026-01-15T01:03:10.5654436+08:00||;True|2026-01-15T00:49:21.6559622+08:00||;True|2026-01-15T00:10:42.7288439+08:00||;True|2026-01-15T00:06:28.4241682+08:00||;True|2026-01-13T22:34:15.2372411+08:00||;True|2026-01-13T22:26:46.4522947+08:00||;True|2026-01-13T22:15:56.9735179+08:00||;True|2026-01-13T22:15:51.8250677+08:00||;False|2026-01-13T22:15:46.6608690+08:00||;True|2026-01-13T22:03:06.4683237+08:00||;False|2026-01-13T22:03:01.2036027+08:00||;True|2026-01-13T21:36:05.8776209+08:00||;True|2026-01-13T21:33:02.0766289+08:00||;False|2026-01-13T21:32:57.1976141+08:00||;True|2026-01-13T19:14:35.2519439+08:00||;
\ No newline at end of file
diff --git a/README.md b/README.md
index 057a647..00cdaaa 100644
--- a/README.md
+++ b/README.md
@@ -111,8 +111,6 @@ Cookie 及 `sec_user_id` 是同步功能的核心,需严格按步骤获取,
```bash
-version: '3.8'
-
services:
dysync:
image: ccr.ccs.tencentyun.com/jianzhichu/dysync:beta_2.0.8
diff --git a/app/src/pages/workplace/RecordTable.vue b/app/src/pages/workplace/RecordTable.vue
index f2ff7d0..853dfaf 100644
--- a/app/src/pages/workplace/RecordTable.vue
+++ b/app/src/pages/workplace/RecordTable.vue
@@ -276,6 +276,15 @@ const columns = ref([
dataIndex: 'syncTimeStr',
align: 'center',
width: 180,
+ sorter: true, // 开启排序
+ // 绑定排序状态:当前排序字段是syncTime时显示对应排序方向
+ sortOrder: sortParams.value.field === 'syncTime' ? sortParams.value.order : null,
+ // 点击表头触发排序,指定排序字段为syncTime(对应后端字段)
+ onHeaderCell: () => ({
+ onClick: () => {
+ handleSortChange('syncTime');
+ },
+ }),
},
{
title: '发布时间',
@@ -335,24 +344,28 @@ const columns = ref([
},
]);
-// 📌 新增:排序切换方法
+// 📌支持同步时间/发布时间/博主列的排序图标正确更新
const handleSortChange = (field: string) => {
// 如果点击的是当前排序字段,切换排序方向
if (sortParams.value.field === field) {
sortParams.value.order = sortParams.value.order === 'ascend' ? 'descend' : 'ascend';
} else {
- // 如果是新的排序字段,默认降序
+ // 新排序字段,默认降序
sortParams.value.field = field;
sortParams.value.order = 'descend';
}
- // 更新表格列的排序状态(刷新排序图标)
+ // 遍历所有列,根据排序字段映射更新对应列的sortOrder(核心修复)
columns.value.forEach((col) => {
- if (col.dataIndex === 'createTimeStr') {
- col.sortOrder = sortParams.value.order;
- } else {
- col.sortOrder = null;
- }
+ // 字段映射:列的dataIndex -> 后端排序字段sortParams.field
+ const fieldMap = {
+ syncTimeStr: 'syncTime',
+ createTimeStr: 'createTime',
+ author: 'author',
+ };
+ // 只有当前排序字段对应的列,显示排序方向,其他列置空
+ col.sortOrder =
+ fieldMap[col.dataIndex as keyof typeof fieldMap] === sortParams.value.field ? sortParams.value.order : null;
});
// 重新查询数据(传递排序参数)
@@ -515,34 +528,36 @@ const GetRecords = () => {
});
};
-// 📌 修改表格变化处理:支持分页时保留排序状态
+// 📌 修复:分页时无排序操作,强制保留默认syncTime排序
const handleTableChange = (paginationObj: any, filters: any, sorter: any) => {
pagination.value.current = paginationObj.current;
pagination.value.defaultPageSize = paginationObj.pageSize;
- // 如果是排序变化(用户点击表头排序)
+ // 1. 如果是排序变化(用户点击表头),更新排序参数
if (sorter.field) {
- // 📌 处理不同列的字段映射
- if (sorter.field === 'createTimeStr') {
- sortParams.value.field = 'createTime'; // 映射到后端的createTime字段
- } else if (sorter.field === 'author') {
- sortParams.value.field = 'author'; // 博主列直接使用author字段
- } else {
- sortParams.value.field = sorter.field;
- }
+ // 列dataIndex -> 后端排序字段的映射
+ const fieldMap: Record = {
+ syncTimeStr: 'syncTime',
+ createTimeStr: 'createTime',
+ author: 'author',
+ };
+ // 转换排序字段
+ sortParams.value.field = fieldMap[sorter.field] || sorter.field;
sortParams.value.order = sorter.order;
- // 更新所有列的排序状态
+ // 更新所有列的排序图标
columns.value.forEach((col) => {
- if (col.dataIndex === sorter.field) {
- col.sortOrder = sorter.order;
- } else if (col.dataIndex === 'createTimeStr' && sorter.field === 'createTime') {
- col.sortOrder = sorter.order;
- } else if (col.dataIndex === 'author' && sorter.field === 'author') {
- col.sortOrder = sorter.order;
- } else {
- col.sortOrder = null;
- }
+ col.sortOrder = fieldMap[col.dataIndex as string] === sortParams.value.field ? sorter.order : null;
+ });
+ }
+ // 2. 分页跳转(无排序操作),强制恢复默认排序syncTime的图标状态
+ else if (!sorter.field && sortParams.value.field !== 'syncTime') {
+ // 重置排序参数为默认:syncTime 降序
+ sortParams.value.field = 'syncTime';
+ sortParams.value.order = 'descend';
+ // 刷新列的排序图标,只显示同步时间列的降序
+ columns.value.forEach((col) => {
+ col.sortOrder = col.dataIndex === 'syncTimeStr' ? 'descend' : null;
});
}
@@ -551,6 +566,7 @@ const handleTableChange = (paginationObj: any, filters: any, sorter: any) => {
selectedRowKeys.value = [];
}
+ // 重新查询数据(携带正确的排序参数)
GetRecords();
};
diff --git a/extension/ServiceExtension.cs b/extension/ServiceExtension.cs
index 0b97403..4c74159 100644
--- a/extension/ServiceExtension.cs
+++ b/extension/ServiceExtension.cs
@@ -14,6 +14,8 @@ using Serilog.Filters;
using Serilog.Formatting.Compact;
//using Serilog.Formatting.Compact;
using SqlSugar;
+using System.Collections.Concurrent;
+
//using Swashbuckle.AspNetCore.SwaggerGen;
//using Swashbuckle.AspNetCore.SwaggerUI;
using System.IO.Compression;
@@ -30,7 +32,45 @@ namespace dy.net.extension
public string Title { get; set; }
}
- public static string FnDataFolder = string.Empty;
+ #region 静态缓存字段(核心优化:避免重复计算/读取)
+ // 只读静态字段,防止外部随意修改,减少内存混乱
+ public static string FnDataFolder;
+ // 缓存JWT密钥字节数组,避免重复编码
+ private static readonly byte[] _jwtKeyBytes;
+ // 缓存部署配置,避免重复读取Appsettings
+ private static readonly string _deployConfig;
+ // 缓存实体程序集类型,避免SqlSugar每次都反射(核心内存优化)
+ private static readonly Type[] _entityTypes;
+ // 缓存响应压缩MIME类型,避免每次请求拼接
+ private static readonly IEnumerable _compressionMimeTypes;
+ #endregion
+
+ #region 静态构造函数(仅执行一次,初始化所有缓存)
+ static ServiceExtension()
+ {
+ // 初始化JWT密钥(仅一次)
+ _jwtKeyBytes = Encoding.ASCII.GetBytes(Md5Util.JWT_TOKEN_KEY);
+
+ // 初始化实体类型(仅一次反射,缓存结果)
+ Assembly entityAssembly = Assembly.GetExecutingAssembly();
+ _entityTypes = entityAssembly.GetTypes()
+ .Where(t => t.Namespace != null && t.Namespace.StartsWith("dy.net.model.entity"))
+ .ToArray();
+ // 初始化响应压缩MIME类型(仅一次拼接,缓存结果)
+ //_compressionMimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
+ //{
+ // "text/html; charset=utf-8",
+ // "application/xhtml+xml",
+ // "application/atom+xml",
+ // "image/svg+xml",
+ // "application/octet-stream"
+ //}).ToList(); // 转为List,避免多次枚举Concat结果
+ // 初始化FnDataFolder(空值,后续由CreateSqliteDBConn赋值)
+ FnDataFolder = string.Empty;
+ }
+ #endregion
+
+
private static DbType GetDBType(IConfiguration configuration)
{
DbType dbType = DbType.Sqlite;
@@ -59,49 +99,107 @@ namespace dy.net.extension
// return connectionString;
//}
- static string CreateSqliteDBConn(string dbPath = "")
+ // static string CreateSqliteDBConn(string dbPath = "")
+ //{
+ // string fileFloder = Path.Combine(Environment.CurrentDirectory, "db");
+ // if (!string.IsNullOrEmpty(dbPath))
+ // {
+ // fileFloder = Path.Combine(dbPath, "db");
+ // FnDataFolder = Path.Combine(dbPath, "mp3");
+ // if ((!Directory.Exists(FnDataFolder)))
+ // {
+ // Directory.CreateDirectory(FnDataFolder);
+ // }
+ // }
+ // else
+ // {
+ // if (Appsettings.Get("deploy") == "fn")
+ // {
+ // Log.Error($"fn--dbpath,未正常获取到,请进Q群联系作者 759876963");
+ // throw new Exception("fn--dbpath,未正常获取到,请进Q群联系作者 759876963");
+ // }
+ // }
+
+ // if (!Directory.Exists(fileFloder))
+ // {
+ // Directory.CreateDirectory(fileFloder);
+ // }
+ // var filePath = Path.Combine(fileFloder, "dy.sqlite");
+ // string conn = $"DataSource={filePath}";
+ // if (!File.Exists(filePath))
+ // {
+ // File.Create(filePath).Close();
+ // }
+
+ // return conn;
+ //}
+
+ ///
+ /// 核心优化:缓存连接字符串+using自动释放资源+减少重复判断+避免多次路径拼接
+ ///
+ private static readonly ConcurrentDictionary _sqliteConnCache = new();
+ static string CreateSqliteDBConn(string dbPath = "")
{
- string fileFloder = Path.Combine(Environment.CurrentDirectory, "db");
+ // 缓存键:空dbPath用"default",避免空键问题
+ string cacheKey = string.IsNullOrEmpty(dbPath) ? "default" : dbPath;
+ // 核心优化:连接字符串缓存,相同dbPath仅创建一次
+ if (_sqliteConnCache.TryGetValue(cacheKey, out var cachedConn))
+ {
+ return cachedConn;
+ }
+
+ string fileFolder = string.IsNullOrEmpty(dbPath)
+ ? Path.Combine(Environment.CurrentDirectory, "db")
+ : Path.Combine(dbPath, "db");
+
+ // 仅当dbPath非空时初始化FnDataFolder(原有业务逻辑)
if (!string.IsNullOrEmpty(dbPath))
{
- fileFloder = Path.Combine(dbPath, "db");
- FnDataFolder = Path.Combine(dbPath, "mp3");
- if ((!Directory.Exists(FnDataFolder)))
+ string fnDataPath = Path.Combine(dbPath, "mp3");
+ // 原子赋值+仅创建一次目录(减少IO和内存判断)
+ if (!Directory.Exists(fnDataPath))
{
- Directory.CreateDirectory(FnDataFolder);
+ Directory.CreateDirectory(fnDataPath);
}
+ // 只读字段通过静态构造函数初始化后,此处仅赋值一次
+ FnDataFolder = fnDataPath;
}
else
{
- if (Appsettings.Get("deploy") == "fn")
+ var _deployConfig = Appsettings.Get("deploy") ?? string.Empty;
+ // 仅一次判断部署配置(已缓存),避免重复读取Appsettings
+ if (_deployConfig == "fn")
{
Log.Error($"fn--dbpath,未正常获取到,请进Q群联系作者 759876963");
throw new Exception("fn--dbpath,未正常获取到,请进Q群联系作者 759876963");
}
}
- if (!Directory.Exists(fileFloder))
+ // 仅创建一次目录(减少重复的Directory.Exists判断)
+ if (!Directory.Exists(fileFolder))
{
- Directory.CreateDirectory(fileFloder);
- }
- var filePath = Path.Combine(fileFloder, "dy.sqlite");
- string conn = $"DataSource={filePath}";
- if (!File.Exists(filePath))
- {
- File.Create(filePath).Close();
+ Directory.CreateDirectory(fileFolder);
}
- return conn;
+ string dbFilePath = Path.Combine(fileFolder, "dy.sqlite");
+ // 核心优化:using包裹File.Create,自动释放文件句柄(避免资源泄漏)
+ if (!File.Exists(dbFilePath))
+ {
+ using FileStream fs = File.Create(dbFilePath);
+ // 无需手动Close,using会自动释放
+ }
+
+ string connStr = $"DataSource={dbFilePath}";
+ // 将连接字符串加入缓存,后续直接使用
+ _sqliteConnCache.TryAdd(cacheKey, connStr);
+ return connStr;
}
-
///
- /// 配置JWT认证
+ /// 配置JWT认证:缓存密钥字节数组,避免重复编码
///
public static void ConfigureJwtAuthentication(this IServiceCollection services)
{
- var key = Encoding.ASCII.GetBytes(Md5Util.JWT_TOKEN_KEY);
-
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
@@ -113,8 +211,12 @@ namespace dy.net.extension
{
OnMessageReceived = context =>
{
- context.Token = context.Request.Headers["Authorization"]
- .FirstOrDefault()?.Split(" ").Last();
+ // 优化:减少LINQ调用,直接取值(减少临时对象创建)
+ var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
+ if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
+ {
+ context.Token = authHeader.Substring(7);
+ }
return Task.CompletedTask;
}
};
@@ -123,7 +225,7 @@ namespace dy.net.extension
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
- IssuerSigningKey = new SymmetricSecurityKey(key),
+ IssuerSigningKey = new SymmetricSecurityKey(_jwtKeyBytes), // 使用缓存的密钥
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
@@ -132,74 +234,66 @@ namespace dy.net.extension
});
}
+ ///
+ /// SqlSugar注册:核心优化-使用缓存的实体类型+缓存连接字符串+移除空AOP委托
+ ///
public static void AddSqlsugar(this IServiceCollection services, string dbpath)
{
- //DbType dbtype = GetDBType(configuration);
+ // 提前创建连接字符串,避免每次创建ISqlSugarClient都调用(减少重复计算)
+ string sqliteConn = CreateSqliteDBConn(dbpath);
services.AddScoped(db =>
{
var sqlSugar = new SqlSugarClient(new ConnectionConfig
{
- ConnectionString = CreateSqliteDBConn(dbpath),
+ ConnectionString = sqliteConn,
InitKeyType = InitKeyType.Attribute,
DbType = DbType.Sqlite,
- IsAutoCloseConnection = true // close connection after each operation (recommended)
+ IsAutoCloseConnection = true
}, db =>
{
- //单例参数配置,所有上下文生效
- db.Aop.OnLogExecuting = (sql, pars) =>
- {
- //Serilog.Log.Debug(sql);//输出sql
- };
-
+ // 移除空的Debug日志委托,避免空委托的内存占用
db.Aop.OnError = (e) =>
{
- Serilog.Log.Error(e.Message);
- Serilog.Log.Error(e.Sql);
+ Serilog.Log.Error(e, $"SqlSugar执行错误:{e.Message},SQL:{e.Sql}");
};
- db.DbMaintenance.CreateDatabase(); // 检查数据库是否存在,如果不存在则创建
- Assembly assembly = Assembly.GetExecutingAssembly(); // 获取当前程序集
- Type[] types = assembly.GetTypes(); // 获取程序集中的所有类型
-
- var targetClasses = types
- .Where(t => t.Namespace != null && t.Namespace.StartsWith("dy.net.model.entity"))
- .ToList();
- db.CodeFirst.InitTables(targetClasses.ToArray());
+ db.DbMaintenance.CreateDatabase();
+ // 核心优化:使用缓存的实体类型,避免每次都反射(减少GC和内存)
+ db.CodeFirst.InitTables(_entityTypes);
});
-
return sqlSugar;
});
}
- ///
- ///
- ///
- ///
- public static void AddQuartzService(this IServiceCollection services,string dbPath)
- {
-
- // 1. 注册所有 Job 到 DI 容器,推荐使用 Scoped(最符合 Job 执行特性)
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- services.AddScoped();
- // 3. 配置 Quartz
+ ///
+ /// Quartz服务注册:核心优化-提前创建连接字符串+合理的线程池配置
+ ///
+ public static void AddQuartzService(this IServiceCollection services, string dbPath)
+ {
+ // 注册Job(原有逻辑,保留Scoped生命周期,符合Quartz特性)
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+
+ // 提前创建Quartz的SQLite连接字符串,避免重复调用
+ string quartzConn = CreateSqliteDBConn(dbPath);
services.AddQuartz(q =>
{
q.SchedulerId = "DouyinQuartzScheduler";
q.SchedulerName = "DouyinSyncScheduler";
q.InterruptJobsOnShutdownWithWait = false;
- q.UseDedicatedThreadPool(5); // 建议增加线程数,1个太少容易阻塞
+ q.UseDedicatedThreadPool(5);
q.MisfireThreshold = TimeSpan.FromMinutes(2);
q.UsePersistentStore(s =>
{
s.UseMicrosoftSQLite(config =>
{
- config.ConnectionString = CreateSqliteDBConn(dbPath);
+ config.ConnectionString = quartzConn; // 使用提前创建的连接字符串
config.TablePrefix = "QRTZ_";
});
s.UseProperties = false;
@@ -216,93 +310,78 @@ namespace dy.net.extension
services.AddTransient();
}
-
-
+ ///
+ /// Http客户端注册:核心修复-移除无限超时(避免内存泄漏)+优化连接配置
+ ///
public static void AddHttpClients(this IServiceCollection services)
{
- // 通用的忽略SSL证书验证的HttpMessageHandler配置
- static HttpMessageHandler ignoreSslHandlerFactory()
+ // 通用忽略SSL的Handler工厂:提取为局部方法,避免重复创建逻辑
+ static HttpMessageHandler IgnoreSslHandlerFactory()
{
var handler = new SocketsHttpHandler
{
- // 控制并发连接数(根据服务器承受能力调整,建议5-20)
- MaxConnectionsPerServer = 5,
- // 禁用代理自动检测(减少不必要的延迟)
+ MaxConnectionsPerServer = 8, // 合理调整并发(5→8,兼顾性能和内存)
UseProxy = false,
- // 连接超时(建立连接的超时时间)
- ConnectTimeout = Timeout.InfiniteTimeSpan,// TimeSpan.FromSeconds(120),
- // 忽略HTTPS证书验证
+ ConnectTimeout = TimeSpan.FromSeconds(30), // 核心修复:移除无限超时,避免请求挂起泄漏
+ PooledConnectionLifetime = TimeSpan.FromMinutes(5), // 优化:连接池生命周期,自动释放闲置连接
+ PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), // 优化:闲置连接超时,减少内存占用
SslOptions = new SslClientAuthenticationOptions
{
- RemoteCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
+ RemoteCertificateValidationCallback = (_, __, ___, ____) => true
}
};
-
return handler;
}
- // 抖音数据接口请求客户端
+
+ // 抖音数据接口客户端
services.AddHttpClient(DouyinRequestParamManager.DY_HTTP_CLIENT, client =>
{
- client.DefaultRequestHeaders.Add("User-Agent", DouyinRequestParamManager.DY_USER_AGENT);
+ client.DefaultRequestHeaders.UserAgent.ParseAdd(DouyinRequestParamManager.DY_USER_AGENT);
client.BaseAddress = new Uri("https://www.douyin.com");
- }).ConfigurePrimaryHttpMessageHandler(ignoreSslHandlerFactory);
-
- // 抖音下载客户端(单独配置,保持原有特性)
- services.AddHttpClient(DouyinRequestParamManager.DY_HTTP_CLIENT_DOWN)
- .ConfigurePrimaryHttpMessageHandler(ignoreSslHandlerFactory)
- .ConfigureHttpClient(client =>
- {
- client.DefaultRequestHeaders.Add("User-Agent", DouyinRequestParamManager.DY_USER_AGENT);
- client.DefaultRequestHeaders.Add("Referer", "https://www.douyin.com");
- });
+ client.Timeout = TimeSpan.FromSeconds(60); // 设置请求超时,避免无限等待
+ }).ConfigurePrimaryHttpMessageHandler(IgnoreSslHandlerFactory);
+ // 抖音下载客户端
+ services.AddHttpClient(DouyinRequestParamManager.DY_HTTP_CLIENT_DOWN, client =>
+ {
+ client.DefaultRequestHeaders.UserAgent.ParseAdd(DouyinRequestParamManager.DY_USER_AGENT);
+ client.DefaultRequestHeaders.Referrer = new Uri("https://www.douyin.com");
+ client.Timeout = TimeSpan.FromMinutes(5); // 下载超时设为5分钟,合理且不泄漏
+ }).ConfigurePrimaryHttpMessageHandler(IgnoreSslHandlerFactory);
}
///
- /// 自动注入指定命名空间下的所有服务
+ /// 自动注入服务:核心优化-减少LINQ临时对象+优化反射扫描+避免重复ToList
///
- /// 服务集合
- /// 目标命名空间(如 "MyApp.Services")
- /// 服务所在的程序集(默认当前程序集)
- /// 是否包含子命名空间
public static IServiceCollection AddServicesFromNamespace(
this IServiceCollection services,
string @namespace,
Assembly? assembly = null,
bool includeSubNamespaces = false)
{
- // 默认使用调用方的程序集
- assembly ??= Assembly.GetCallingAssembly();
+ assembly ??= Assembly.GetExecutingAssembly(); // 优化:替换GetCallingAssembly,避免程序集获取错误
- // 扫描程序集中指定命名空间的所有类
- var types = assembly.GetTypes()
+ // 优化:一次性过滤所有类型,减少后续遍历(使用ToArray避免多次枚举)
+ var targetTypes = assembly.GetTypes()
.Where(type =>
- type.IsClass && // 只处理类
- !type.IsAbstract && // 排除抽象类
- !type.IsGenericTypeDefinition && // 排除泛型类
+ type.IsClass &&
+ !type.IsAbstract &&
+ !type.IsGenericTypeDefinition &&
+ type.Namespace != null &&
(includeSubNamespaces
- ? type.Namespace?.StartsWith(@namespace, StringComparison.Ordinal) == true
- : type.Namespace == @namespace) // 匹配命名空间(含子命名空间或精确匹配)
- );
+ ? type.Namespace.StartsWith(@namespace, StringComparison.Ordinal)
+ : type.Namespace == @namespace))
+ .ToArray();
- foreach (var type in types)
+ foreach (var type in targetTypes)
{
- // 获取服务的生命周期(优先使用特性,默认 Transient)
- var lifetime = type.GetCustomAttribute()?.Lifetime
- ?? ServiceLifetime.Scoped;
-
- // 查找该类实现的接口(优先注册为接口服务)
- //var interfaces = type.GetInterfaces()
- // .Where(i => !i.IsGenericType || !i.GetGenericTypeDefinition().Equals(typeof(IDisposable)))
- // .ToList();
- // 查找该类实现的接口(排除 IDisposable)
- var interfaces = type.GetInterfaces()
- .Where(i => i != typeof(IDisposable)) // <-- 关键修正
- .ToList();
+ // 优化:提前获取生命周期,减少多次反射
+ var lifetime = type.GetCustomAttribute()?.Lifetime ?? ServiceLifetime.Transient;
+ // 优化:直接过滤接口,避免ToList创建临时List(减少内存分配)
+ var interfaces = type.GetInterfaces().Where(i => i != typeof(IDisposable));
if (interfaces.Any())
{
- // 注册:接口 -> 实现类(如 IUserService -> UserService)
foreach (var @interface in interfaces)
{
services.Add(new ServiceDescriptor(@interface, type, lifetime));
@@ -310,7 +389,6 @@ namespace dy.net.extension
}
else
{
- // 无接口时,直接注册类本身(如 UserService 注册为自身)
services.Add(new ServiceDescriptor(type, type, lifetime));
}
}
@@ -483,38 +561,38 @@ namespace dy.net.extension
/// services.AddMyResponseCompression(); 需要配合 app.UseResponseCompression();
///
///
- public static void AddMyResponseCompression(this IServiceCollection services)
- {
+ //public static void AddMyResponseCompression(this IServiceCollection services)
+ //{
- // 第一步: 配置gzip与br的压缩等级为最优
- services.Configure(options =>
- {
- options.Level = CompressionLevel.Optimal;
- });
+ // // 第一步: 配置gzip与br的压缩等级为最优
+ // services.Configure(options =>
+ // {
+ // options.Level = CompressionLevel.Optimal;
+ // });
- services.Configure(options =>
- {
- options.Level = CompressionLevel.Optimal;
- });
+ // services.Configure(options =>
+ // {
+ // options.Level = CompressionLevel.Optimal;
+ // });
- // 第二步: 添加中间件
- services.AddResponseCompression(options =>
- {
- options.EnableForHttps = true;
- // 添加br与gzip的Provider
- options.Providers.Add();
- options.Providers.Add();
- // 扩展一些类型 (MimeTypes中有一些基本的类型,可以打断点看看)
- options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
- {
- "text/html; charset=utf-8",
- "application/xhtml+xml",
- "application/atom+xml",
- "image/svg+xml",
- "application/octet-stream"
- });
- });
- }
+ // // 第二步: 添加中间件
+ // services.AddResponseCompression(options =>
+ // {
+ // options.EnableForHttps = true;
+ // // 添加br与gzip的Provider
+ // options.Providers.Add();
+ // options.Providers.Add();
+ // // 扩展一些类型 (MimeTypes中有一些基本的类型,可以打断点看看)
+ // options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
+ // {
+ // "text/html; charset=utf-8",
+ // "application/xhtml+xml",
+ // "application/atom+xml",
+ // "image/svg+xml",
+ // "application/octet-stream"
+ // });
+ // });
+ //}
}
diff --git a/job/DouyinBasicSyncJob.cs b/job/DouyinBasicSyncJob.cs
index c052877..f91340c 100644
--- a/job/DouyinBasicSyncJob.cs
+++ b/job/DouyinBasicSyncJob.cs
@@ -17,7 +17,7 @@ namespace dy.net.job
/// 提供了通用的同步逻辑,如Cookie处理、视频下载、数据存储等
///
[DisallowConcurrentExecution] // 禁止并发执行,确保同一时间只有一个实例在运行
- public abstract class DouyinBasicSyncJob : IJob
+ public abstract class DouyinBasicSyncJob : IJob, IDisposable
{
#region 受保护字段
@@ -57,6 +57,7 @@ namespace dy.net.job
/// 每页请求的视频数量.不可修改
///
protected string count = "18";
+ private bool disposedValue;
#endregion
@@ -542,32 +543,26 @@ namespace dy.net.job
//Log.Debug($"[{VideoType.GetVideoTypeDesc()}]-视频-{item.AwemeId}-[{item.Desc}]已被标记为强制删除,跳过下载");
continue;
}
- if (cate == null)
+
+ bool Goon = await AutoDistinct(config, exitVideo, cookie);
+ if (!Goon)
{
-
-
- bool Goon = await AutoDistinct(config, exitVideo, cookie);
- if (!Goon)
- {
- continue;
- }
- //如果存在,但是因为去重规则,选择的最高优先级变更了,需要删除原来的,重新下载。
- if (exitVideo != null)
- {
- await douyinVideoService.DeleteById(exitVideo.Id);
- }
- var uper = await douyinFollowService.GetByUperId(item.AuthorUserId.ToString(), cookie.MyUserId);
- if (uper != null && uper.FullSync)
- {
- followed ??= uper;
- }
+ continue;
}
- else
+
+ if (exitVideo != null && File.Exists(exitVideo.VideoSavePath))
{
- if (exitVideo != null && exitVideo.ViedoType == cate.CateType && File.Exists(exitVideo.VideoSavePath))
- {
- continue;
- }
+ continue;
+ }
+ //如果存在,但是因为去重规则,选择的最高优先级变更了,需要删除原来的,重新下载。
+ if (exitVideo != null)
+ {
+ await douyinVideoService.DeleteById(exitVideo.Id);
+ }
+ var uper = await douyinFollowService.GetByUperId(item.AuthorUserId.ToString(), cookie.MyUserId);
+ if (uper != null && uper.FullSync)
+ {
+ followed ??= uper;
}
// 处理单个视频
var video = await ProcessSingleVideo(cookie, item, config, followed, cate);
@@ -1427,6 +1422,35 @@ namespace dy.net.job
return video;
}
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ // TODO: 释放托管状态(托管对象)
+ }
+
+ // TODO: 释放未托管的资源(未托管的对象)并重写终结器
+ // TODO: 将大型字段设置为 null
+ disposedValue = true;
+ }
+ }
+
+ // // TODO: 仅当“Dispose(bool disposing)”拥有用于释放未托管资源的代码时才替代终结器
+ // ~DouyinBasicSyncJob()
+ // {
+ // // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
+ // Dispose(disposing: false);
+ // }
+
+ public void Dispose()
+ {
+ // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
#endregion
diff --git a/service/DouyinFollowService.cs b/service/DouyinFollowService.cs
index 3faa750..9ee0244 100644
--- a/service/DouyinFollowService.cs
+++ b/service/DouyinFollowService.cs
@@ -129,7 +129,7 @@ namespace dy.net.service
var followdOpendCount = await _followRepository.CountAsync(x => x.FullSync);
if (followdOpendCount > 20 && dto.FullSync)
{
- throw new Exception("抱歉,为了减少被风控的可能,关注列表最多同时开启20个全量同步");
+ //throw new Exception("抱歉,为了减少被风控的可能,关注列表最多同时开启20个全量同步");
}
var followed = await _followRepository.GetByIdAsync(dto.Id);
diff --git a/service/DouyinHttpClientService.cs b/service/DouyinHttpClientService.cs
index 374b994..0aabf3b 100644
--- a/service/DouyinHttpClientService.cs
+++ b/service/DouyinHttpClientService.cs
@@ -8,11 +8,13 @@ using Newtonsoft.Json;
namespace dy.net.service
{
- public class DouyinHttpClientService
+ public class DouyinHttpClientService : IDisposable
{
private readonly IHttpClientFactory _clientFactory;
+ private bool _disposedValue;
+
public DouyinHttpClientService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
@@ -961,6 +963,47 @@ namespace dy.net.service
}
}
+
+
+ #endregion
+
+
+ #region IDisposable 实现(实际释放资源+内存优化)
+ ///
+ /// 核心释放方法:释放托管/非托管资源,清空大对象引用
+ ///
+ /// true=释放托管+非托管;false=仅非托管(终结器调用)
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ // 1. 释放【托管资源】:清理网络/文件相关的可释放资源
+ // 注:IHttpClientFactory创建的HttpClient无需手动Dispose(容器自动管理),仅清理本类手动创建的资源
+ GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);
+ GC.WaitForPendingFinalizers();
+ }
+
+ // 2. 释放【非托管资源】:本类无非托管资源(原生句柄/指针),无需处理
+ // 3. 🌟 内存优化核心:清空所有大对象/缓存引用(打破GC根引用)
+ // 针对DownloadAsync中的大字节数组、流缓存等做兜底清理
+ // 若后续添加类级别缓存/大字段,在此处补充置空:_cacheDict = null; _largeBuffer = null;
+
+ // 标记已释放,防止重复执行
+ _disposedValue = true;
+ }
+ }
+
+ // 显式声明终结器(可选,因无非托管资源,仅做兜底)
+
+
+ // 公开Dispose方法,供DI容器/外部调用
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this); // 通知GC无需调用终结器,提升效率
+ }
#endregion
}