diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..fe1152b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,30 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
+!**/.gitignore
+!.git/HEAD
+!.git/config
+!.git/packed-refs
+!.git/refs/heads/**
\ No newline at end of file
diff --git a/Delete-BIN-OBJ-Folders.bat b/Delete-BIN-OBJ-Folders.bat
new file mode 100644
index 0000000..02cfaa5
--- /dev/null
+++ b/Delete-BIN-OBJ-Folders.bat
@@ -0,0 +1,20 @@
+@ECHO off
+cls
+
+ECHO Deleting all BIN and OBJ folders...
+ECHO.
+
+FOR /d /r . %%d in (bin,obj) DO (
+ IF EXIST "%%d" (
+ ECHO %%d | FIND /I "\node_modules\" > Nul && (
+ ECHO.Skipping: %%d
+ ) || (
+ ECHO.Deleting: %%d
+ rd /s/q "%%d"
+ )
+ )
+)
+
+ECHO.
+ECHO.BIN and OBJ folders have been successfully deleted. Press any key to exit.
+pause > nul
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..d4999c8
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,75 @@
+# FROM mcr.microsoft.com/dotnet/aspnet:8.0
+
+# # 创建目录
+# RUN mkdir /app
+
+# COPY publish /app
+
+
+
+# # 设置工作目录
+# WORKDIR /app
+
+# # 暴露80端口
+# EXPOSE 80
+# # 设置时区 .net6 才有这个问题
+# ENV TZ=Asia/Shanghai
+
+# # 设置环境变量
+# ENV ASPNETCORE_ENVIRONMENT=Production
+
+# ENTRYPOINT ["dotnet", "JiShe.IOT.HttpApi.Host.dll"]
+
+
+
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
+WORKDIR /app
+EXPOSE 80
+EXPOSE 443
+ENV TZ=Asia/Shanghai
+ENV ASPNETCORE_ENVIRONMENT=Production
+
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+WORKDIR /src
+COPY ["JiShe.CollectBus.sln", "."]
+COPY ["common.props", "."]
+COPY ["NuGet.Config", "."]
+COPY ["web/", "web/"]
+COPY ["modules/", "modules/"]
+COPY ["services/", "services/"]
+COPY ["shared/", "shared/"]
+COPY ["protocols/", "protocols/"]
+
+# 恢复项目依赖
+RUN dotnet restore "JiShe.CollectBus.sln"
+
+# 构建项目
+WORKDIR "/src/web/JiShe.CollectBus.Host"
+RUN dotnet build "JiShe.CollectBus.Host.csproj" -c Release -o /app/build
+
+# 发布项目
+FROM build AS publish
+RUN dotnet publish "JiShe.CollectBus.Host.csproj" -c Release -o /app/publish /p:UseAppHost=false
+
+# 创建最终镜像
+FROM base AS final
+WORKDIR /app
+
+# 创建Plugins目录
+RUN mkdir -p /app/Plugins
+
+# 复制发布内容
+COPY --from=publish /app/publish .
+
+# 健康检查
+HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
+ CMD curl -f http://localhost:80/health || exit 1
+
+# 设置入口点
+ENTRYPOINT ["dotnet", "JiShe.CollectBus.Host.dll"]
+
+# 启动命令
+# 可选:添加命令行参数
+# CMD ["--urls", "http://+:80"]
+
+
diff --git a/JiShe.CollectBus.Protocols.sln b/JiShe.CollectBus.Protocols.sln
new file mode 100644
index 0000000..55e4109
--- /dev/null
+++ b/JiShe.CollectBus.Protocols.sln
@@ -0,0 +1,179 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.9.34728.123
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Domain.Shared", "shared\JiShe.CollectBus.Domain.Shared\JiShe.CollectBus.Domain.Shared.csproj", "{D64C1577-4929-4B60-939E-96DE1534891A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Domain", "services\JiShe.CollectBus.Domain\JiShe.CollectBus.Domain.csproj", "{F2840BC7-0188-4606-9126-DADD0F5ABF7A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Application.Contracts", "services\JiShe.CollectBus.Application.Contracts\JiShe.CollectBus.Application.Contracts.csproj", "{BD65D04F-08D5-40C1-8C24-77CA0BACB877}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Application", "services\JiShe.CollectBus.Application\JiShe.CollectBus.Application.csproj", "{78040F9E-3501-4A40-82DF-00A597710F35}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.MongoDB", "modules\JiShe.CollectBus.MongoDB\JiShe.CollectBus.MongoDB.csproj", "{F1C58097-4C08-4D88-8976-6B3389391481}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.HttpApi", "web\JiShe.CollectBus.HttpApi\JiShe.CollectBus.HttpApi.csproj", "{077AA5F8-8B61-420C-A6B5-0150A66FDB34}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Host", "web\JiShe.CollectBus.Host\JiShe.CollectBus.Host.csproj", "{35829A15-4127-4F69-8BDE-9405DEAACA9A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Common", "shared\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj", "{AD2F1928-4411-4511-B564-5FB996EC08B9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.DbMigrator", "services\JiShe.CollectBus.DbMigrator\JiShe.CollectBus.DbMigrator.csproj", "{8BA01C3D-297D-42DF-BD63-EF07202A0A67}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.FreeSql", "modules\JiShe.CollectBus.FreeSql\JiShe.CollectBus.FreeSql.csproj", "{FE0457D9-4038-4A17-8808-DCAD06CFC0A0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.FreeRedis", "modules\JiShe.CollectBus.FreeRedis\JiShe.CollectBus.FreeRedis.csproj", "{C06C4082-638F-2996-5FED-7784475766C1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Kafka", "modules\JiShe.CollectBus.Kafka\JiShe.CollectBus.Kafka.csproj", "{F0288175-F0EC-48BD-945F-CF1512850943}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.IoTDB", "modules\JiShe.CollectBus.IoTDB\JiShe.CollectBus.IoTDB.csproj", "{A3F3C092-0A25-450B-BF6A-5983163CBEF5}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Protocol.Test", "protocols\JiShe.CollectBus.Protocol.Test\JiShe.CollectBus.Protocol.Test.csproj", "{A377955E-7EA1-6F29-8CF7-774569E93925}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Cassandra", "modules\JiShe.CollectBus.Cassandra\JiShe.CollectBus.Cassandra.csproj", "{443B4549-0AC0-4493-8F3E-49C83225DD76}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1.Web", "1.Web", "{A02F7D8A-04DC-44D6-94D4-3F65712D6B94}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4.Modules", "4.Modules", "{2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "6.Protocols", "6.Protocols", "{3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2.Services", "2.Services", "{BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3.Shared", "3.Shared", "{EBF7C01F-9B4F-48E6-8418-2CBFDA51EB0B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JiShe.CollectBus.Kafka.Test", "modules\JiShe.CollectBus.Kafka.Test\JiShe.CollectBus.Kafka.Test.csproj", "{6D6A2A58-7406-9C8C-7B23-3E442CCE3E6B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JiShe.CollectBus.Protocol.T1882018", "protocols\JiShe.CollectBus.Protocol.T1882018\JiShe.CollectBus.Protocol.T1882018.csproj", "{430D298B-377E-49B8-83AA-ADC7C0EBDB0F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JiShe.CollectBus.Protocol.T37612012", "protocols\JiShe.CollectBus.Protocol.T37612012\JiShe.CollectBus.Protocol.T37612012.csproj", "{8A61DF78-069B-40B5-8811-614E2960443E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JiShe.CollectBus.Protocol", "protocols\JiShe.CollectBus.Protocol\JiShe.CollectBus.Protocol.csproj", "{E27377CC-E2D3-4237-060F-96EA214D3129}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JiShe.CollectBus.Protocol.T6452007", "protocols\JiShe.CollectBus.Protocol.T6452007\JiShe.CollectBus.Protocol.T6452007.csproj", "{75B7D419-C261-577D-58D6-AA3ACED9129F}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0.Docs", "0.Docs", "{D8346C4C-55B8-43E8-A6B8-E59D56FE6D92}"
+ ProjectSection(SolutionItems) = preProject
+ .gitignore = .gitignore
+ Dockerfile = Dockerfile
+ NuGet.Config = NuGet.Config
+ readme.md = readme.md
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D64C1577-4929-4B60-939E-96DE1534891A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D64C1577-4929-4B60-939E-96DE1534891A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D64C1577-4929-4B60-939E-96DE1534891A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D64C1577-4929-4B60-939E-96DE1534891A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F2840BC7-0188-4606-9126-DADD0F5ABF7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F2840BC7-0188-4606-9126-DADD0F5ABF7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F2840BC7-0188-4606-9126-DADD0F5ABF7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F2840BC7-0188-4606-9126-DADD0F5ABF7A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BD65D04F-08D5-40C1-8C24-77CA0BACB877}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BD65D04F-08D5-40C1-8C24-77CA0BACB877}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BD65D04F-08D5-40C1-8C24-77CA0BACB877}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BD65D04F-08D5-40C1-8C24-77CA0BACB877}.Release|Any CPU.Build.0 = Release|Any CPU
+ {78040F9E-3501-4A40-82DF-00A597710F35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {78040F9E-3501-4A40-82DF-00A597710F35}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {78040F9E-3501-4A40-82DF-00A597710F35}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {78040F9E-3501-4A40-82DF-00A597710F35}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F1C58097-4C08-4D88-8976-6B3389391481}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F1C58097-4C08-4D88-8976-6B3389391481}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F1C58097-4C08-4D88-8976-6B3389391481}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F1C58097-4C08-4D88-8976-6B3389391481}.Release|Any CPU.Build.0 = Release|Any CPU
+ {077AA5F8-8B61-420C-A6B5-0150A66FDB34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {077AA5F8-8B61-420C-A6B5-0150A66FDB34}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {077AA5F8-8B61-420C-A6B5-0150A66FDB34}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {077AA5F8-8B61-420C-A6B5-0150A66FDB34}.Release|Any CPU.Build.0 = Release|Any CPU
+ {35829A15-4127-4F69-8BDE-9405DEAACA9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {35829A15-4127-4F69-8BDE-9405DEAACA9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {35829A15-4127-4F69-8BDE-9405DEAACA9A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {35829A15-4127-4F69-8BDE-9405DEAACA9A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AD2F1928-4411-4511-B564-5FB996EC08B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AD2F1928-4411-4511-B564-5FB996EC08B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AD2F1928-4411-4511-B564-5FB996EC08B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AD2F1928-4411-4511-B564-5FB996EC08B9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8BA01C3D-297D-42DF-BD63-EF07202A0A67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8BA01C3D-297D-42DF-BD63-EF07202A0A67}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8BA01C3D-297D-42DF-BD63-EF07202A0A67}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8BA01C3D-297D-42DF-BD63-EF07202A0A67}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FE0457D9-4038-4A17-8808-DCAD06CFC0A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FE0457D9-4038-4A17-8808-DCAD06CFC0A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FE0457D9-4038-4A17-8808-DCAD06CFC0A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FE0457D9-4038-4A17-8808-DCAD06CFC0A0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C06C4082-638F-2996-5FED-7784475766C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C06C4082-638F-2996-5FED-7784475766C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C06C4082-638F-2996-5FED-7784475766C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C06C4082-638F-2996-5FED-7784475766C1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F0288175-F0EC-48BD-945F-CF1512850943}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F0288175-F0EC-48BD-945F-CF1512850943}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F0288175-F0EC-48BD-945F-CF1512850943}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F0288175-F0EC-48BD-945F-CF1512850943}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A3F3C092-0A25-450B-BF6A-5983163CBEF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A3F3C092-0A25-450B-BF6A-5983163CBEF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A3F3C092-0A25-450B-BF6A-5983163CBEF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A3F3C092-0A25-450B-BF6A-5983163CBEF5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A377955E-7EA1-6F29-8CF7-774569E93925}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A377955E-7EA1-6F29-8CF7-774569E93925}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A377955E-7EA1-6F29-8CF7-774569E93925}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A377955E-7EA1-6F29-8CF7-774569E93925}.Release|Any CPU.Build.0 = Release|Any CPU
+ {443B4549-0AC0-4493-8F3E-49C83225DD76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {443B4549-0AC0-4493-8F3E-49C83225DD76}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {443B4549-0AC0-4493-8F3E-49C83225DD76}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {443B4549-0AC0-4493-8F3E-49C83225DD76}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6D6A2A58-7406-9C8C-7B23-3E442CCE3E6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6D6A2A58-7406-9C8C-7B23-3E442CCE3E6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6D6A2A58-7406-9C8C-7B23-3E442CCE3E6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6D6A2A58-7406-9C8C-7B23-3E442CCE3E6B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {430D298B-377E-49B8-83AA-ADC7C0EBDB0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {430D298B-377E-49B8-83AA-ADC7C0EBDB0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {430D298B-377E-49B8-83AA-ADC7C0EBDB0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {430D298B-377E-49B8-83AA-ADC7C0EBDB0F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8A61DF78-069B-40B5-8811-614E2960443E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8A61DF78-069B-40B5-8811-614E2960443E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8A61DF78-069B-40B5-8811-614E2960443E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8A61DF78-069B-40B5-8811-614E2960443E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E27377CC-E2D3-4237-060F-96EA214D3129}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E27377CC-E2D3-4237-060F-96EA214D3129}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E27377CC-E2D3-4237-060F-96EA214D3129}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E27377CC-E2D3-4237-060F-96EA214D3129}.Release|Any CPU.Build.0 = Release|Any CPU
+ {75B7D419-C261-577D-58D6-AA3ACED9129F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {75B7D419-C261-577D-58D6-AA3ACED9129F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {75B7D419-C261-577D-58D6-AA3ACED9129F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {75B7D419-C261-577D-58D6-AA3ACED9129F}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {D64C1577-4929-4B60-939E-96DE1534891A} = {EBF7C01F-9B4F-48E6-8418-2CBFDA51EB0B}
+ {F2840BC7-0188-4606-9126-DADD0F5ABF7A} = {BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23}
+ {BD65D04F-08D5-40C1-8C24-77CA0BACB877} = {BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23}
+ {78040F9E-3501-4A40-82DF-00A597710F35} = {BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23}
+ {F1C58097-4C08-4D88-8976-6B3389391481} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}
+ {077AA5F8-8B61-420C-A6B5-0150A66FDB34} = {A02F7D8A-04DC-44D6-94D4-3F65712D6B94}
+ {35829A15-4127-4F69-8BDE-9405DEAACA9A} = {A02F7D8A-04DC-44D6-94D4-3F65712D6B94}
+ {AD2F1928-4411-4511-B564-5FB996EC08B9} = {EBF7C01F-9B4F-48E6-8418-2CBFDA51EB0B}
+ {8BA01C3D-297D-42DF-BD63-EF07202A0A67} = {BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23}
+ {FE0457D9-4038-4A17-8808-DCAD06CFC0A0} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}
+ {C06C4082-638F-2996-5FED-7784475766C1} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}
+ {F0288175-F0EC-48BD-945F-CF1512850943} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}
+ {A3F3C092-0A25-450B-BF6A-5983163CBEF5} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}
+ {A377955E-7EA1-6F29-8CF7-774569E93925} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}
+ {443B4549-0AC0-4493-8F3E-49C83225DD76} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}
+ {6D6A2A58-7406-9C8C-7B23-3E442CCE3E6B} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}
+ {430D298B-377E-49B8-83AA-ADC7C0EBDB0F} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}
+ {8A61DF78-069B-40B5-8811-614E2960443E} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}
+ {E27377CC-E2D3-4237-060F-96EA214D3129} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}
+ {75B7D419-C261-577D-58D6-AA3ACED9129F} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {4324B3B4-B60B-4E3C-91D8-59576B4E26DD}
+ EndGlobalSection
+EndGlobal
diff --git a/JiShe.CollectBus.sln b/JiShe.CollectBus.sln
index d4e6591..03bd207 100644
--- a/JiShe.CollectBus.sln
+++ b/JiShe.CollectBus.sln
@@ -19,10 +19,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Host", "we
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Common", "shared\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj", "{AD2F1928-4411-4511-B564-5FB996EC08B9}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Protocol", "protocols\JiShe.CollectBus.Protocol\JiShe.CollectBus.Protocol.csproj", "{C62EFF95-5C32-435F-BD78-6977E828F894}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Protocol.Contracts", "protocols\JiShe.CollectBus.Protocol.Contracts\JiShe.CollectBus.Protocol.Contracts.csproj", "{38C1808B-009A-418B-B17B-AB3626341B5D}"
-EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.DbMigrator", "services\JiShe.CollectBus.DbMigrator\JiShe.CollectBus.DbMigrator.csproj", "{8BA01C3D-297D-42DF-BD63-EF07202A0A67}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.FreeSql", "modules\JiShe.CollectBus.FreeSql\JiShe.CollectBus.FreeSql.csproj", "{FE0457D9-4038-4A17-8808-DCAD06CFC0A0}"
@@ -41,11 +37,34 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1.Web", "1.Web", "{A02F7D8A
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "4.Modules", "4.Modules", "{2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3.Protocols", "3.Protocols", "{3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "6.Protocols", "6.Protocols", "{3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2.Services", "2.Services", "{BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5.Shared", "5.Shared", "{EBF7C01F-9B4F-48E6-8418-2CBFDA51EB0B}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3.Shared", "3.Shared", "{EBF7C01F-9B4F-48E6-8418-2CBFDA51EB0B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JiShe.CollectBus.Kafka.Test", "modules\JiShe.CollectBus.Kafka.Test\JiShe.CollectBus.Kafka.Test.csproj", "{6D6A2A58-7406-9C8C-7B23-3E442CCE3E6B}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5.External", "5.External", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JiShe.CollectBus.Protocol.T1882018", "protocols\JiShe.CollectBus.Protocol.T1882018\JiShe.CollectBus.Protocol.T1882018.csproj", "{430D298B-377E-49B8-83AA-ADC7C0EBDB0F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JiShe.CollectBus.Protocol.T37612012", "protocols\JiShe.CollectBus.Protocol.T37612012\JiShe.CollectBus.Protocol.T37612012.csproj", "{8A61DF78-069B-40B5-8811-614E2960443E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JiShe.CollectBus.Protocol", "protocols\JiShe.CollectBus.Protocol\JiShe.CollectBus.Protocol.csproj", "{E27377CC-E2D3-4237-060F-96EA214D3129}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JiShe.CollectBus.Protocol.T6452007", "protocols\JiShe.CollectBus.Protocol.T6452007\JiShe.CollectBus.Protocol.T6452007.csproj", "{75B7D419-C261-577D-58D6-AA3ACED9129F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JiShe.CollectBus.PluginFileWatcher", "external\JiShe.CollectBus.PluginFileWatcher\JiShe.CollectBus.PluginFileWatcher.csproj", "{0F67A493-A4DF-550E-AB4D-95F55144C706}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0.Docs", "0.Docs", "{D8346C4C-55B8-43E8-A6B8-E59D56FE6D92}"
+ ProjectSection(SolutionItems) = preProject
+ .gitignore = .gitignore
+ Dockerfile = Dockerfile
+ NuGet.Config = NuGet.Config
+ PackageAndPublish.bat = PackageAndPublish.bat
+ readme.md = readme.md
+ EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -85,14 +104,6 @@ Global
{AD2F1928-4411-4511-B564-5FB996EC08B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD2F1928-4411-4511-B564-5FB996EC08B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD2F1928-4411-4511-B564-5FB996EC08B9}.Release|Any CPU.Build.0 = Release|Any CPU
- {C62EFF95-5C32-435F-BD78-6977E828F894}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C62EFF95-5C32-435F-BD78-6977E828F894}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C62EFF95-5C32-435F-BD78-6977E828F894}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C62EFF95-5C32-435F-BD78-6977E828F894}.Release|Any CPU.Build.0 = Release|Any CPU
- {38C1808B-009A-418B-B17B-AB3626341B5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {38C1808B-009A-418B-B17B-AB3626341B5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {38C1808B-009A-418B-B17B-AB3626341B5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {38C1808B-009A-418B-B17B-AB3626341B5D}.Release|Any CPU.Build.0 = Release|Any CPU
{8BA01C3D-297D-42DF-BD63-EF07202A0A67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8BA01C3D-297D-42DF-BD63-EF07202A0A67}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BA01C3D-297D-42DF-BD63-EF07202A0A67}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -121,6 +132,30 @@ Global
{443B4549-0AC0-4493-8F3E-49C83225DD76}.Debug|Any CPU.Build.0 = Debug|Any CPU
{443B4549-0AC0-4493-8F3E-49C83225DD76}.Release|Any CPU.ActiveCfg = Release|Any CPU
{443B4549-0AC0-4493-8F3E-49C83225DD76}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6D6A2A58-7406-9C8C-7B23-3E442CCE3E6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6D6A2A58-7406-9C8C-7B23-3E442CCE3E6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6D6A2A58-7406-9C8C-7B23-3E442CCE3E6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6D6A2A58-7406-9C8C-7B23-3E442CCE3E6B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {430D298B-377E-49B8-83AA-ADC7C0EBDB0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {430D298B-377E-49B8-83AA-ADC7C0EBDB0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {430D298B-377E-49B8-83AA-ADC7C0EBDB0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {430D298B-377E-49B8-83AA-ADC7C0EBDB0F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8A61DF78-069B-40B5-8811-614E2960443E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8A61DF78-069B-40B5-8811-614E2960443E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8A61DF78-069B-40B5-8811-614E2960443E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8A61DF78-069B-40B5-8811-614E2960443E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E27377CC-E2D3-4237-060F-96EA214D3129}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E27377CC-E2D3-4237-060F-96EA214D3129}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E27377CC-E2D3-4237-060F-96EA214D3129}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E27377CC-E2D3-4237-060F-96EA214D3129}.Release|Any CPU.Build.0 = Release|Any CPU
+ {75B7D419-C261-577D-58D6-AA3ACED9129F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {75B7D419-C261-577D-58D6-AA3ACED9129F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {75B7D419-C261-577D-58D6-AA3ACED9129F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {75B7D419-C261-577D-58D6-AA3ACED9129F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0F67A493-A4DF-550E-AB4D-95F55144C706}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0F67A493-A4DF-550E-AB4D-95F55144C706}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0F67A493-A4DF-550E-AB4D-95F55144C706}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0F67A493-A4DF-550E-AB4D-95F55144C706}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -134,8 +169,6 @@ Global
{077AA5F8-8B61-420C-A6B5-0150A66FDB34} = {A02F7D8A-04DC-44D6-94D4-3F65712D6B94}
{35829A15-4127-4F69-8BDE-9405DEAACA9A} = {A02F7D8A-04DC-44D6-94D4-3F65712D6B94}
{AD2F1928-4411-4511-B564-5FB996EC08B9} = {EBF7C01F-9B4F-48E6-8418-2CBFDA51EB0B}
- {C62EFF95-5C32-435F-BD78-6977E828F894} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}
- {38C1808B-009A-418B-B17B-AB3626341B5D} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}
{8BA01C3D-297D-42DF-BD63-EF07202A0A67} = {BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23}
{FE0457D9-4038-4A17-8808-DCAD06CFC0A0} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}
{C06C4082-638F-2996-5FED-7784475766C1} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}
@@ -143,6 +176,12 @@ Global
{A3F3C092-0A25-450B-BF6A-5983163CBEF5} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}
{A377955E-7EA1-6F29-8CF7-774569E93925} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}
{443B4549-0AC0-4493-8F3E-49C83225DD76} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}
+ {6D6A2A58-7406-9C8C-7B23-3E442CCE3E6B} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}
+ {430D298B-377E-49B8-83AA-ADC7C0EBDB0F} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}
+ {8A61DF78-069B-40B5-8811-614E2960443E} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}
+ {E27377CC-E2D3-4237-060F-96EA214D3129} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}
+ {75B7D419-C261-577D-58D6-AA3ACED9129F} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}
+ {0F67A493-A4DF-550E-AB4D-95F55144C706} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4324B3B4-B60B-4E3C-91D8-59576B4E26DD}
diff --git a/NuGet.Config b/NuGet.Config
new file mode 100644
index 0000000..44f8e7f
--- /dev/null
+++ b/NuGet.Config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PackageAndPublish.bat b/PackageAndPublish.bat
new file mode 100644
index 0000000..0bb73cd
--- /dev/null
+++ b/PackageAndPublish.bat
@@ -0,0 +1,77 @@
+@echo off
+setlocal enabledelayedexpansion
+
+set VERSION=1.0.0
+set CONFIGURATION=Release
+set OUTPUT_DIR=%~dp0\nupkgs
+set API_KEY=your-nuget-api-key
+
+REM 创建输出目录
+if not exist "%OUTPUT_DIR%" mkdir "%OUTPUT_DIR%"
+
+REM 清理
+echo 清理解决方案...
+dotnet clean JiShe.CollectBus.sln -c %CONFIGURATION%
+
+REM 删除之前的包
+echo 删除之前的包...
+if exist "%OUTPUT_DIR%\*.nupkg" del /q "%OUTPUT_DIR%\*.nupkg"
+
+REM 打包项目
+echo 开始打包项目...
+
+REM 打包 Protocol 项目
+echo 打包 Protocol 项目...
+call :PackProject protocols\JiShe.CollectBus.Protocol\JiShe.CollectBus.Protocol.csproj
+call :PackProject protocols\JiShe.CollectBus.Protocol.Contracts\JiShe.CollectBus.Protocol.Contracts.csproj
+call :PackProject protocols\JiShe.CollectBus.Protocol.T1882018\JiShe.CollectBus.Protocol.T1882018.csproj
+call :PackProject protocols\JiShe.CollectBus.Protocol.T37612012\JiShe.CollectBus.Protocol.T37612012.csproj
+call :PackProject protocols\JiShe.CollectBus.Protocol.T6452007\JiShe.CollectBus.Protocol.T6452007.csproj
+
+REM 打包 Modules 项目
+echo 打包 Modules 项目...
+call :PackProject modules\JiShe.CollectBus.FreeRedis\JiShe.CollectBus.FreeRedis.csproj
+call :PackProject modules\JiShe.CollectBus.Kafka\JiShe.CollectBus.Kafka.csproj
+call :PackProject modules\JiShe.CollectBus.IoTDB\JiShe.CollectBus.IoTDB.csproj
+call :PackProject modules\JiShe.CollectBus.MongoDB\JiShe.CollectBus.MongoDB.csproj
+call :PackProject modules\JiShe.CollectBus.FreeSql\JiShe.CollectBus.FreeSql.csproj
+call :PackProject modules\JiShe.CollectBus.Cassandra\JiShe.CollectBus.Cassandra.csproj
+call :PackProject modules\JiShe.CollectBusMultiProcessing\JiShe.CollectBusMultiProcessing.csproj
+
+REM 打包 Shared 项目
+echo 打包 Shared 项目...
+call :PackProject shared\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj
+call :PackProject shared\JiShe.CollectBus.Domain.Shared\JiShe.CollectBus.Domain.Shared.csproj
+
+REM 打包 Services 项目
+echo 打包 Services 项目...
+call :PackProject services\JiShe.CollectBus.Domain\JiShe.CollectBus.Domain.csproj
+call :PackProject services\JiShe.CollectBus.Application.Contracts\JiShe.CollectBus.Application.Contracts.csproj
+call :PackProject services\JiShe.CollectBus.Application\JiShe.CollectBus.Application.csproj
+call :PackProject services\JiShe.CollectBus.EntityFrameworkCore\JiShe.CollectBus.EntityFrameworkCore.csproj
+
+echo.
+echo 是否要发布包到 NuGet? (Y/N)
+set /p PUBLISH_CHOICE=
+
+if /i "%PUBLISH_CHOICE%"=="Y" (
+ echo 开始发布包...
+ for %%f in ("%OUTPUT_DIR%\*.nupkg") do (
+ echo 发布: %%f
+ dotnet nuget push "%%f" --api-key %API_KEY% --source https://api.nuget.org/v3/index.json --skip-duplicate
+ )
+ echo 所有包已发布完成!
+) else (
+ echo 跳过发布操作。所有包都在 %OUTPUT_DIR% 目录中。
+)
+
+goto :eof
+
+:PackProject
+if exist "%~1" (
+ echo 打包: %~1
+ dotnet pack "%~1" -c %CONFIGURATION% --no-build --include-symbols -p:SymbolPackageFormat=snupkg -p:PackageVersion=%VERSION% -o "%OUTPUT_DIR%"
+) else (
+ echo 警告: 项目不存在 - %~1
+)
+goto :eof
\ No newline at end of file
diff --git a/external/JiShe.CollectBus.PluginFileWatcher/Config.cs b/external/JiShe.CollectBus.PluginFileWatcher/Config.cs
new file mode 100644
index 0000000..8de55f6
--- /dev/null
+++ b/external/JiShe.CollectBus.PluginFileWatcher/Config.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+
+namespace JiShe.CollectBus.PluginFileWatcher
+{
+ ///
+ /// 文件监控程序的配置类
+ ///
+ public class FileMonitorConfig
+ {
+ ///
+ /// 基本配置
+ ///
+ public GeneralConfig General { get; set; } = new GeneralConfig();
+
+ ///
+ /// 文件过滤配置
+ ///
+ public FileFiltersConfig FileFilters { get; set; } = new FileFiltersConfig();
+
+ ///
+ /// 性能相关配置
+ ///
+ public PerformanceConfig Performance { get; set; } = new PerformanceConfig();
+
+ ///
+ /// 健壮性相关配置
+ ///
+ public RobustnessConfig Robustness { get; set; } = new RobustnessConfig();
+
+ ///
+ /// 事件存储和回放配置
+ ///
+ public EventStorageConfig EventStorage { get; set; } = new EventStorageConfig();
+
+ ///
+ /// 文件系统通知过滤器配置
+ ///
+ public List NotifyFilters { get; set; } = new List();
+
+ ///
+ /// 日志配置
+ ///
+ public LoggingConfig Logging { get; set; } = new LoggingConfig();
+ }
+
+ ///
+ /// 常规配置
+ ///
+ public class GeneralConfig
+ {
+ ///
+ /// 是否启用文件过滤
+ ///
+ public bool EnableFileFiltering { get; set; } = true;
+
+ ///
+ /// 内存监控间隔(分钟)
+ ///
+ public int MemoryMonitorIntervalMinutes { get; set; } = 1;
+
+ ///
+ /// 默认监控路径
+ ///
+ public string DefaultMonitorPath { get; set; } = string.Empty;
+ }
+
+ ///
+ /// 文件过滤配置
+ ///
+ public class FileFiltersConfig
+ {
+ ///
+ /// 允许监控的文件扩展名
+ ///
+ public string[] AllowedExtensions { get; set; } = new[] { ".dll" };
+
+ ///
+ /// 排除的目录
+ ///
+ public string[] ExcludedDirectories { get; set; } = new[] { "bin", "obj", "node_modules" };
+
+ ///
+ /// 是否包含子目录
+ ///
+ public bool IncludeSubdirectories { get; set; } = true;
+ }
+
+ ///
+ /// 性能相关配置
+ ///
+ public class PerformanceConfig
+ {
+ ///
+ /// 内存清理阈值(事件数)
+ ///
+ public int MemoryCleanupThreshold { get; set; } = 5000;
+
+ ///
+ /// 通道容量
+ ///
+ public int ChannelCapacity { get; set; } = 1000;
+
+ ///
+ /// 事件去抖时间(秒)
+ ///
+ public int EventDebounceTimeSeconds { get; set; } = 3;
+
+ ///
+ /// 最大字典大小
+ ///
+ public int MaxDictionarySize { get; set; } = 10000;
+
+ ///
+ /// 清理间隔(秒)
+ ///
+ public int CleanupIntervalSeconds { get; set; } = 5;
+
+ ///
+ /// 处理延迟(毫秒)
+ ///
+ public int ProcessingDelayMs { get; set; } = 5;
+ }
+
+ ///
+ /// 健壮性相关配置
+ ///
+ public class RobustnessConfig
+ {
+ ///
+ /// 是否启用自动恢复机制
+ ///
+ public bool EnableAutoRecovery { get; set; } = true;
+
+ ///
+ /// 监控器健康检查间隔(秒)
+ ///
+ public int WatcherHealthCheckIntervalSeconds { get; set; } = 30;
+
+ ///
+ /// 监控器无响应超时时间(秒)
+ ///
+ public int WatcherTimeoutSeconds { get; set; } = 60;
+
+ ///
+ /// 监控器重启尝试最大次数
+ ///
+ public int MaxRestartAttempts { get; set; } = 3;
+
+ ///
+ /// 重启尝试之间的延迟(秒)
+ ///
+ public int RestartDelaySeconds { get; set; } = 5;
+
+ ///
+ /// 是否启用文件锁检测
+ ///
+ public bool EnableFileLockDetection { get; set; } = true;
+
+ ///
+ /// 对锁定文件的处理策略: Skip(跳过), Retry(重试), Log(仅记录)
+ ///
+ public string LockedFileStrategy { get; set; } = "Retry";
+
+ ///
+ /// 文件锁定重试次数
+ ///
+ public int FileLockRetryCount { get; set; } = 3;
+
+ ///
+ /// 文件锁定重试间隔(毫秒)
+ ///
+ public int FileLockRetryDelayMs { get; set; } = 500;
+ }
+
+ ///
+ /// 事件存储和回放配置
+ ///
+ public class EventStorageConfig
+ {
+ ///
+ /// 是否启用事件存储
+ ///
+ public bool EnableEventStorage { get; set; } = true;
+
+ ///
+ /// 存储类型:SQLite 或 File
+ ///
+ public string StorageType { get; set; } = "SQLite";
+
+ ///
+ /// 事件存储目录
+ ///
+ public string StorageDirectory { get; set; } = "D:/EventLogs";
+
+ ///
+ /// SQLite数据库文件路径
+ ///
+ public string DatabasePath { get; set; } = "D:/EventLogs/events.db";
+
+ ///
+ /// SQLite连接字符串
+ ///
+ public string ConnectionString { get; set; } = "Data Source=D:/EventLogs/events.db";
+
+ ///
+ /// 数据库命令超时(秒)
+ ///
+ public int CommandTimeout { get; set; } = 30;
+
+ ///
+ /// 事件日志文件名格式 (使用DateTime.ToString格式)
+ ///
+ public string LogFileNameFormat { get; set; } = "FileEvents_{0:yyyy-MM-dd}.json";
+
+ ///
+ /// 存储间隔(秒),多久将缓存的事件写入一次存储
+ ///
+ public int StorageIntervalSeconds { get; set; } = 60;
+
+ ///
+ /// 事件批量写入大小,达到此数量时立即写入存储
+ ///
+ public int BatchSize { get; set; } = 100;
+
+ ///
+ /// 最大保留事件记录条数
+ ///
+ public int MaxEventRecords { get; set; } = 100000;
+
+ ///
+ /// 数据保留天数
+ ///
+ public int DataRetentionDays { get; set; } = 30;
+
+ ///
+ /// 最大保留日志文件数
+ ///
+ public int MaxLogFiles { get; set; } = 30;
+
+ ///
+ /// 是否压缩存储的事件日志
+ ///
+ public bool CompressLogFiles { get; set; } = true;
+
+ ///
+ /// 是否可以回放事件
+ ///
+ public bool EnableEventReplay { get; set; } = true;
+
+ ///
+ /// 回放时间间隔(毫秒)
+ ///
+ public int ReplayIntervalMs { get; set; } = 100;
+
+ ///
+ /// 回放速度倍率,大于1加速,小于1减速
+ ///
+ public double ReplaySpeedFactor { get; set; } = 1.0;
+ }
+
+ ///
+ /// 日志相关配置
+ ///
+ public class LoggingConfig
+ {
+ ///
+ /// 日志级别:Verbose、Debug、Information、Warning、Error、Fatal
+ ///
+ public string LogLevel { get; set; } = "Information";
+
+ ///
+ /// 是否记录文件事件处理详情
+ ///
+ public bool LogFileEventDetails { get; set; } = false;
+
+ ///
+ /// 日志文件保留天数
+ ///
+ public int RetainedLogDays { get; set; } = 30;
+
+ ///
+ /// 日志文件目录
+ ///
+ public string LogDirectory { get; set; } = "Logs";
+ }
+}
\ No newline at end of file
diff --git a/external/JiShe.CollectBus.PluginFileWatcher/DbUtility.cs b/external/JiShe.CollectBus.PluginFileWatcher/DbUtility.cs
new file mode 100644
index 0000000..c2dd7a5
--- /dev/null
+++ b/external/JiShe.CollectBus.PluginFileWatcher/DbUtility.cs
@@ -0,0 +1,277 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Serilog;
+using Serilog.Extensions.Logging;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+namespace JiShe.CollectBus.PluginFileWatcher
+{
+ ///
+ /// 数据库操作工具类,用于命令行测试数据库功能
+ ///
+ public class DbUtility
+ {
+ private readonly EventDatabaseManager _dbManager;
+ private readonly ILogger _logger;
+ private readonly FileMonitorConfig _config;
+
+ ///
+ /// 初始化数据库工具类
+ ///
+ /// 配置文件路径
+ public DbUtility(string configPath = "appsettings.json")
+ {
+ // 从配置文件加载配置
+ var configuration = new ConfigurationBuilder()
+ .AddJsonFile(configPath, optional: false, reloadOnChange: true)
+ .Build();
+
+ // 初始化日志
+ var serilogLogger = new LoggerConfiguration()
+ .ReadFrom.Configuration(configuration)
+ .CreateLogger();
+
+ // 将Serilog适配为Microsoft.Extensions.Logging.ILogger
+ _logger = new SerilogLoggerFactory(serilogLogger).CreateLogger("DbUtility");
+
+ // 创建配置对象
+ _config = new FileMonitorConfig();
+ configuration.GetSection("FileMonitor").Bind(_config);
+
+ // 确保SQLite存储已启用
+ _config.EventStorage.EnableEventStorage = true;
+ _config.EventStorage.StorageType = "SQLite";
+
+ // 创建数据库管理器
+ _dbManager = new EventDatabaseManager(_config, _logger);
+ }
+
+ ///
+ /// 执行数据库维护操作
+ ///
+ /// 命令行参数
+ public async Task ExecuteAsync(string[] args)
+ {
+ if (args.Length == 0)
+ {
+ PrintUsage();
+ return;
+ }
+
+ string command = args[0].ToLower();
+
+ switch (command)
+ {
+ case "stats":
+ await ShowStatisticsAsync();
+ break;
+
+ case "cleanup":
+ int days = args.Length > 1 && int.TryParse(args[1], out int d) ? d : _config.EventStorage.DataRetentionDays;
+ await _dbManager.CleanupOldDataAsync(days);
+ Console.WriteLine($"已清理 {days} 天前的旧数据");
+ break;
+
+ case "query":
+ await QueryEventsAsync(args);
+ break;
+
+ case "test":
+ await GenerateTestDataAsync(args);
+ break;
+
+ default:
+ Console.WriteLine($"未知命令: {command}");
+ PrintUsage();
+ break;
+ }
+ }
+
+ ///
+ /// 显示帮助信息
+ ///
+ private void PrintUsage()
+ {
+ Console.WriteLine("数据库工具使用方法:");
+ Console.WriteLine(" stats - 显示数据库统计信息");
+ Console.WriteLine(" cleanup [days] - 清理指定天数之前的数据(默认使用配置值)");
+ Console.WriteLine(" query [limit] - 查询最近的事件(默认10条)");
+ Console.WriteLine(" query type:X ext:Y - 按类型和扩展名查询事件");
+ Console.WriteLine(" test [count] - 生成测试数据(默认100条)");
+ }
+
+ ///
+ /// 显示数据库统计信息
+ ///
+ private async Task ShowStatisticsAsync()
+ {
+ try
+ {
+ var stats = await _dbManager.GetDatabaseStatsAsync();
+
+ Console.WriteLine("数据库统计信息:");
+ Console.WriteLine($"事件总数: {stats.TotalEvents}");
+ Console.WriteLine($"最早事件时间: {stats.OldestEventTime?.ToLocalTime()}");
+ Console.WriteLine($"最新事件时间: {stats.NewestEventTime?.ToLocalTime()}");
+
+ Console.WriteLine("\n事件类型分布:");
+ if (stats.EventTypeCounts != null)
+ {
+ foreach (var item in stats.EventTypeCounts)
+ {
+ Console.WriteLine($" {item.Key}: {item.Value}");
+ }
+ }
+
+ Console.WriteLine("\n扩展名分布 (Top 10):");
+ if (stats.TopExtensions != null)
+ {
+ foreach (var item in stats.TopExtensions)
+ {
+ Console.WriteLine($" {item.Key}: {item.Value}");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"获取统计信息出错: {ex.Message}");
+ _logger.LogError(ex, "获取数据库统计信息时发生错误");
+ }
+ }
+
+ ///
+ /// 查询事件
+ ///
+ private async Task QueryEventsAsync(string[] args)
+ {
+ try
+ {
+ var queryParams = new EventQueryParams
+ {
+ PageSize = 10,
+ PageIndex = 0
+ };
+
+ // 解析查询参数
+ if (args.Length > 1)
+ {
+ foreach (var arg in args.Skip(1))
+ {
+ if (int.TryParse(arg, out int limit))
+ {
+ queryParams.PageSize = limit;
+ }
+ else if (arg.StartsWith("type:", StringComparison.OrdinalIgnoreCase))
+ {
+ string typeValue = arg.Substring(5);
+ if (Enum.TryParse(typeValue, true, out var eventType))
+ {
+ queryParams.EventType = eventType;
+ }
+ }
+ else if (arg.StartsWith("ext:", StringComparison.OrdinalIgnoreCase))
+ {
+ queryParams.ExtensionFilter = arg.Substring(4);
+ if (!queryParams.ExtensionFilter.StartsWith("."))
+ {
+ queryParams.ExtensionFilter = "." + queryParams.ExtensionFilter;
+ }
+ }
+ else if (arg.StartsWith("path:", StringComparison.OrdinalIgnoreCase))
+ {
+ queryParams.PathFilter = arg.Substring(5);
+ }
+ }
+ }
+
+ // 执行查询
+ var result = await _dbManager.QueryEventsAsync(queryParams);
+
+ Console.WriteLine($"查询结果 (总数: {result.TotalCount}):");
+ foreach (var evt in result.Events)
+ {
+ string typeStr = evt.EventType.ToString();
+ string timestamp = evt.Timestamp.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
+ Console.WriteLine($"[{timestamp}] {typeStr,-10} {evt.FileName} ({evt.Extension})");
+ }
+
+ if (result.HasMore)
+ {
+ Console.WriteLine($"... 还有更多结果,共 {result.TotalCount} 条");
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"查询事件出错: {ex.Message}");
+ _logger.LogError(ex, "查询事件时发生错误");
+ }
+ }
+
+ ///
+ /// 生成测试数据
+ ///
+ private async Task GenerateTestDataAsync(string[] args)
+ {
+ try
+ {
+ int count = args.Length > 1 && int.TryParse(args[1], out int c) ? c : 100;
+
+ var events = new List();
+ var rnd = new Random();
+ string[] extensions = { ".dll", ".exe", ".txt", ".cs", ".xml", ".json", ".png", ".jpg" };
+ string[] directories = { "C:\\Temp", "D:\\Work", "C:\\Program Files", "D:\\Projects", "E:\\Data" };
+
+ DateTime startTime = DateTime.Now.AddHours(-24);
+
+ for (int i = 0; i < count; i++)
+ {
+ var eventType = (FileEventType)rnd.Next(0, 5);
+ var ext = extensions[rnd.Next(extensions.Length)];
+ var dir = directories[rnd.Next(directories.Length)];
+ var fileName = $"TestFile_{i:D5}{ext}";
+ var timestamp = startTime.AddMinutes(i);
+
+ var fileEvent = new FileEvent
+ {
+ Id = Guid.NewGuid(),
+ Timestamp = timestamp,
+ EventType = eventType,
+ FullPath = $"{dir}\\{fileName}",
+ FileName = fileName,
+ Directory = dir,
+ Extension = ext,
+ FileSize = rnd.Next(1024, 1024 * 1024)
+ };
+
+ // 如果是重命名事件,添加旧文件名
+ if (eventType == FileEventType.Renamed)
+ {
+ fileEvent.OldFileName = $"OldFile_{i:D5}{ext}";
+ fileEvent.OldFullPath = $"{dir}\\{fileEvent.OldFileName}";
+ }
+
+ // 添加一些元数据
+ fileEvent.Metadata["CreationTime"] = timestamp.AddMinutes(-rnd.Next(1, 60)).ToString("o");
+ fileEvent.Metadata["LastWriteTime"] = timestamp.ToString("o");
+ fileEvent.Metadata["IsReadOnly"] = (rnd.Next(10) < 2).ToString();
+ fileEvent.Metadata["TestData"] = $"测试数据 {i}";
+
+ events.Add(fileEvent);
+ }
+
+ Console.WriteLine($"正在生成 {count} 条测试数据...");
+ await _dbManager.SaveEventsAsync(events);
+ Console.WriteLine("测试数据生成完成!");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"生成测试数据出错: {ex.Message}");
+ _logger.LogError(ex, "生成测试数据时发生错误");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/external/JiShe.CollectBus.PluginFileWatcher/Dockerfile b/external/JiShe.CollectBus.PluginFileWatcher/Dockerfile
new file mode 100644
index 0000000..88a0552
--- /dev/null
+++ b/external/JiShe.CollectBus.PluginFileWatcher/Dockerfile
@@ -0,0 +1,28 @@
+# 请参阅 https://aka.ms/customizecontainer 以了解如何自定义调试容器,以及 Visual Studio 如何使用此 Dockerfile 生成映像以更快地进行调试。
+
+# 此阶段用于在快速模式(默认为调试配置)下从 VS 运行时
+FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
+USER $APP_UID
+WORKDIR /app
+
+
+# 此阶段用于生成服务项目
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["external/JiShe.CollectBus.PluginFileWatcher/JiShe.CollectBus.PluginFileWatcher.csproj", "external/JiShe.CollectBus.PluginFileWatcher/"]
+RUN dotnet restore "./external/JiShe.CollectBus.PluginFileWatcher/JiShe.CollectBus.PluginFileWatcher.csproj"
+COPY . .
+WORKDIR "/src/external/JiShe.CollectBus.PluginFileWatcher"
+RUN dotnet build "./JiShe.CollectBus.PluginFileWatcher.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+# 此阶段用于发布要复制到最终阶段的服务项目
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./JiShe.CollectBus.PluginFileWatcher.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+# 此阶段在生产中使用,或在常规模式下从 VS 运行时使用(在不使用调试配置时为默认值)
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "JiShe.CollectBus.PluginFileWatcher.dll"]
\ No newline at end of file
diff --git a/external/JiShe.CollectBus.PluginFileWatcher/EventDatabaseManager.cs b/external/JiShe.CollectBus.PluginFileWatcher/EventDatabaseManager.cs
new file mode 100644
index 0000000..9b0f257
--- /dev/null
+++ b/external/JiShe.CollectBus.PluginFileWatcher/EventDatabaseManager.cs
@@ -0,0 +1,481 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Dapper;
+using Microsoft.Data.Sqlite;
+using Microsoft.Extensions.Logging;
+
+namespace JiShe.CollectBus.PluginFileWatcher
+{
+ ///
+ /// SQLite数据库管理器,用于管理文件事件的存储和检索
+ ///
+ public class EventDatabaseManager : IDisposable
+ {
+ private readonly FileMonitorConfig _config;
+ private readonly ILogger _logger;
+ private readonly string _connectionString;
+ private readonly string _databasePath;
+ private readonly int _commandTimeout;
+ private bool _disposed;
+
+ ///
+ /// 初始化数据库管理器
+ ///
+ /// 配置对象
+ /// 日志记录器
+ public EventDatabaseManager(FileMonitorConfig config, ILogger logger)
+ {
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+ // 确保使用配置中的设置
+ _databasePath = config.EventStorage.DatabasePath;
+ _connectionString = config.EventStorage.ConnectionString;
+ _commandTimeout = config.EventStorage.CommandTimeout;
+
+ // 确保数据库目录存在
+ string dbDirectory = Path.GetDirectoryName(_databasePath);
+ if (!string.IsNullOrEmpty(dbDirectory) && !Directory.Exists(dbDirectory))
+ {
+ Directory.CreateDirectory(dbDirectory);
+ }
+
+ // 初始化数据库
+ InitializeDatabase().GetAwaiter().GetResult();
+ }
+
+ ///
+ /// 初始化数据库,确保必要的表已创建
+ ///
+ private async Task InitializeDatabase()
+ {
+ try
+ {
+ using var connection = new SqliteConnection(_connectionString);
+ await connection.OpenAsync();
+
+ // 启用外键约束
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = "PRAGMA foreign_keys = ON;";
+ await command.ExecuteNonQueryAsync();
+ }
+
+ // 创建文件事件表
+ string createTableSql = @"
+ CREATE TABLE IF NOT EXISTS FileEvents (
+ Id TEXT PRIMARY KEY,
+ Timestamp TEXT NOT NULL,
+ EventType INTEGER NOT NULL,
+ FullPath TEXT NOT NULL,
+ FileName TEXT NOT NULL,
+ Directory TEXT NOT NULL,
+ Extension TEXT NOT NULL,
+ OldFileName TEXT,
+ OldFullPath TEXT,
+ FileSize INTEGER,
+ CreatedAt TEXT NOT NULL
+ );
+
+ CREATE INDEX IF NOT EXISTS idx_events_timestamp ON FileEvents(Timestamp);
+ CREATE INDEX IF NOT EXISTS idx_events_eventtype ON FileEvents(EventType);
+ CREATE INDEX IF NOT EXISTS idx_events_extension ON FileEvents(Extension);";
+
+ await connection.ExecuteAsync(createTableSql, commandTimeout: _commandTimeout);
+
+ // 创建元数据表
+ string createMetadataTableSql = @"
+ CREATE TABLE IF NOT EXISTS EventMetadata (
+ Id INTEGER PRIMARY KEY AUTOINCREMENT,
+ EventId TEXT NOT NULL,
+ MetadataKey TEXT NOT NULL,
+ MetadataValue TEXT,
+ FOREIGN KEY (EventId) REFERENCES FileEvents(Id) ON DELETE CASCADE
+ );
+
+ CREATE INDEX IF NOT EXISTS idx_metadata_eventid ON EventMetadata(EventId);
+ CREATE INDEX IF NOT EXISTS idx_metadata_key ON EventMetadata(MetadataKey);";
+
+ await connection.ExecuteAsync(createMetadataTableSql, commandTimeout: _commandTimeout);
+
+ _logger.LogInformation("数据库初始化成功");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "初始化数据库失败");
+ throw;
+ }
+ }
+
+ ///
+ /// 保存文件事件到数据库
+ ///
+ /// 要保存的事件列表
+ public async Task SaveEventsAsync(List events)
+ {
+ if (events == null || events.Count == 0)
+ return;
+
+ try
+ {
+ using var connection = new SqliteConnection(_connectionString);
+ await connection.OpenAsync();
+
+ // 启用外键约束
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = "PRAGMA foreign_keys = ON;";
+ await command.ExecuteNonQueryAsync();
+ }
+
+ // 开始事务
+ using var transaction = connection.BeginTransaction();
+
+ try
+ {
+ foreach (var fileEvent in events)
+ {
+ // 插入事件数据
+ string insertEventSql = @"
+ INSERT INTO FileEvents (
+ Id, Timestamp, EventType, FullPath, FileName,
+ Directory, Extension, OldFileName, OldFullPath,
+ FileSize, CreatedAt
+ ) VALUES (
+ @Id, @Timestamp, @EventType, @FullPath, @FileName,
+ @Directory, @Extension, @OldFileName, @OldFullPath,
+ @FileSize, @CreatedAt
+ )";
+
+ await connection.ExecuteAsync(insertEventSql, new
+ {
+ Id = fileEvent.Id.ToString(), // 确保ID始终以字符串形式保存
+ Timestamp = fileEvent.Timestamp.ToString("o"),
+ EventType = (int)fileEvent.EventType,
+ fileEvent.FullPath,
+ fileEvent.FileName,
+ fileEvent.Directory,
+ fileEvent.Extension,
+ fileEvent.OldFileName,
+ fileEvent.OldFullPath,
+ fileEvent.FileSize,
+ CreatedAt = DateTime.UtcNow.ToString("o")
+ }, transaction, _commandTimeout);
+
+ // 插入元数据
+ if (fileEvent.Metadata != null && fileEvent.Metadata.Count > 0)
+ {
+ string insertMetadataSql = @"
+ INSERT INTO EventMetadata (EventId, MetadataKey, MetadataValue)
+ VALUES (@EventId, @MetadataKey, @MetadataValue)";
+
+ foreach (var metadata in fileEvent.Metadata)
+ {
+ await connection.ExecuteAsync(insertMetadataSql, new
+ {
+ EventId = fileEvent.Id.ToString(), // 确保ID以相同格式保存
+ MetadataKey = metadata.Key,
+ MetadataValue = metadata.Value
+ }, transaction, _commandTimeout);
+ }
+ }
+ }
+
+ // 提交事务
+ transaction.Commit();
+ _logger.LogInformation($"已成功保存 {events.Count} 个事件到数据库");
+ }
+ catch (Exception ex)
+ {
+ // 回滚事务
+ transaction.Rollback();
+ _logger.LogError(ex, "保存事件到数据库时发生错误");
+ throw;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "连接数据库失败");
+ throw;
+ }
+ }
+
+ ///
+ /// 查询事件
+ ///
+ /// 查询参数
+ /// 查询结果
+ public async Task QueryEventsAsync(EventQueryParams queryParams)
+ {
+ if (queryParams == null)
+ throw new ArgumentNullException(nameof(queryParams));
+
+ var result = new EventQueryResult
+ {
+ StartTime = queryParams.StartTime ?? DateTime.MinValue,
+ EndTime = queryParams.EndTime ?? DateTime.MaxValue
+ };
+
+ try
+ {
+ using var connection = new SqliteConnection(_connectionString);
+ await connection.OpenAsync();
+
+ // 启用外键约束
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = "PRAGMA foreign_keys = ON;";
+ await command.ExecuteNonQueryAsync();
+ }
+
+ // 构建查询条件
+ var conditions = new List();
+ var parameters = new DynamicParameters();
+
+ if (queryParams.StartTime.HasValue)
+ {
+ conditions.Add("Timestamp >= @StartTime");
+ parameters.Add("@StartTime", queryParams.StartTime.Value.ToString("o"));
+ }
+
+ if (queryParams.EndTime.HasValue)
+ {
+ conditions.Add("Timestamp <= @EndTime");
+ parameters.Add("@EndTime", queryParams.EndTime.Value.ToString("o"));
+ }
+
+ if (queryParams.EventType.HasValue)
+ {
+ conditions.Add("EventType = @EventType");
+ parameters.Add("@EventType", (int)queryParams.EventType.Value);
+ }
+
+ if (!string.IsNullOrEmpty(queryParams.PathFilter))
+ {
+ conditions.Add("FullPath LIKE @PathFilter");
+ parameters.Add("@PathFilter", $"%{queryParams.PathFilter}%");
+ }
+
+ if (!string.IsNullOrEmpty(queryParams.ExtensionFilter))
+ {
+ conditions.Add("Extension = @ExtensionFilter");
+ parameters.Add("@ExtensionFilter", queryParams.ExtensionFilter);
+ }
+
+ // 构建WHERE子句
+ string whereClause = conditions.Count > 0
+ ? $"WHERE {string.Join(" AND ", conditions)}"
+ : string.Empty;
+
+ // 构建ORDER BY子句
+ string orderByClause = queryParams.AscendingOrder
+ ? "ORDER BY Timestamp ASC"
+ : "ORDER BY Timestamp DESC";
+
+ // 获取总记录数
+ string countSql = $"SELECT COUNT(*) FROM FileEvents {whereClause}";
+ result.TotalCount = await connection.ExecuteScalarAsync(countSql, parameters, commandTimeout: _commandTimeout);
+
+ // 应用分页
+ string paginationClause = $"LIMIT @PageSize OFFSET @Offset";
+ parameters.Add("@PageSize", queryParams.PageSize);
+ parameters.Add("@Offset", queryParams.PageIndex * queryParams.PageSize);
+
+ // 查询事件数据
+ string querySql = $@"
+ SELECT Id,
+ Timestamp,
+ EventType,
+ FullPath,
+ FileName,
+ Directory,
+ Extension,
+ OldFileName,
+ OldFullPath,
+ FileSize
+ FROM FileEvents
+ {whereClause}
+ {orderByClause}
+ {paginationClause}";
+
+ var events = await connection.QueryAsync(querySql, parameters, commandTimeout: _commandTimeout);
+
+ // 处理查询结果
+ foreach (var eventData in events)
+ {
+ var fileEvent = new FileEvent
+ {
+ Id = Guid.Parse(eventData.Id),
+ Timestamp = DateTime.Parse(eventData.Timestamp),
+ EventType = (FileEventType)eventData.EventType,
+ FullPath = eventData.FullPath,
+ FileName = eventData.FileName,
+ Directory = eventData.Directory,
+ Extension = eventData.Extension,
+ OldFileName = eventData.OldFileName,
+ OldFullPath = eventData.OldFullPath,
+ FileSize = eventData.FileSize
+ };
+
+ // 获取元数据
+ string metadataSql = "SELECT MetadataKey, MetadataValue FROM EventMetadata WHERE EventId = @EventId";
+ var metadata = await connection.QueryAsync(metadataSql, new { EventId = fileEvent.Id.ToString() }, commandTimeout: _commandTimeout);
+
+ foreach (var item in metadata)
+ {
+ fileEvent.Metadata[item.MetadataKey] = item.MetadataValue;
+ }
+
+ result.Events.Add(fileEvent);
+ }
+
+ result.HasMore = (queryParams.PageIndex + 1) * queryParams.PageSize < result.TotalCount;
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "查询事件时发生错误");
+ throw;
+ }
+ }
+
+ ///
+ /// 清理旧数据
+ ///
+ /// 数据保留天数
+ public async Task CleanupOldDataAsync(int retentionDays)
+ {
+ if (retentionDays <= 0)
+ return;
+
+ try
+ {
+ DateTime cutoffDate = DateTime.UtcNow.AddDays(-retentionDays);
+ string cutoffDateStr = cutoffDate.ToString("o");
+
+ using var connection = new SqliteConnection(_connectionString);
+ await connection.OpenAsync();
+
+ // 启用外键约束
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = "PRAGMA foreign_keys = ON;";
+ await command.ExecuteNonQueryAsync();
+ }
+
+ // 删除旧事件(级联删除元数据)
+ string deleteSql = "DELETE FROM FileEvents WHERE Timestamp < @CutoffDate";
+ int deletedCount = await connection.ExecuteAsync(deleteSql, new { CutoffDate = cutoffDateStr }, commandTimeout: _commandTimeout);
+
+ _logger.LogInformation($"已清理 {deletedCount} 条旧事件数据({retentionDays}天前)");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "清理旧数据时发生错误");
+ throw;
+ }
+ }
+
+ ///
+ /// 获取数据库统计信息
+ ///
+ /// 数据库统计信息
+ public async Task GetDatabaseStatsAsync()
+ {
+ try
+ {
+ using var connection = new SqliteConnection(_connectionString);
+ await connection.OpenAsync();
+
+ // 启用外键约束
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = "PRAGMA foreign_keys = ON;";
+ await command.ExecuteNonQueryAsync();
+ }
+
+ var stats = new DatabaseStats();
+
+ // 获取事件总数
+ stats.TotalEvents = await connection.ExecuteScalarAsync("SELECT COUNT(*) FROM FileEvents", commandTimeout: _commandTimeout);
+
+ // 获取最早和最新事件时间
+ stats.OldestEventTime = await connection.ExecuteScalarAsync("SELECT Timestamp FROM FileEvents ORDER BY Timestamp ASC LIMIT 1", commandTimeout: _commandTimeout);
+ stats.NewestEventTime = await connection.ExecuteScalarAsync("SELECT Timestamp FROM FileEvents ORDER BY Timestamp DESC LIMIT 1", commandTimeout: _commandTimeout);
+
+ // 获取事件类型分布
+ var eventTypeCounts = await connection.QueryAsync("SELECT EventType, COUNT(*) AS Count FROM FileEvents GROUP BY EventType", commandTimeout: _commandTimeout);
+ stats.EventTypeCounts = new Dictionary();
+
+ foreach (var item in eventTypeCounts)
+ {
+ stats.EventTypeCounts[(FileEventType)item.EventType] = item.Count;
+ }
+
+ // 获取扩展名分布(前10个)
+ var extensionCounts = await connection.QueryAsync(
+ "SELECT Extension, COUNT(*) AS Count FROM FileEvents GROUP BY Extension ORDER BY Count DESC LIMIT 10",
+ commandTimeout: _commandTimeout);
+ stats.TopExtensions = new Dictionary();
+
+ foreach (var item in extensionCounts)
+ {
+ stats.TopExtensions[item.Extension] = item.Count;
+ }
+
+ return stats;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "获取数据库统计信息时发生错误");
+ throw;
+ }
+ }
+
+ ///
+ /// 释放资源
+ ///
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
+
+ _disposed = true;
+ }
+ }
+
+ ///
+ /// 数据库统计信息
+ ///
+ public class DatabaseStats
+ {
+ ///
+ /// 事件总数
+ ///
+ public int TotalEvents { get; set; }
+
+ ///
+ /// 最早事件时间
+ ///
+ public DateTime? OldestEventTime { get; set; }
+
+ ///
+ /// 最新事件时间
+ ///
+ public DateTime? NewestEventTime { get; set; }
+
+ ///
+ /// 事件类型计数
+ ///
+ public Dictionary EventTypeCounts { get; set; }
+
+ ///
+ /// 排名前列的文件扩展名
+ ///
+ public Dictionary TopExtensions { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/external/JiShe.CollectBus.PluginFileWatcher/EventStorage.cs b/external/JiShe.CollectBus.PluginFileWatcher/EventStorage.cs
new file mode 100644
index 0000000..9bbf619
--- /dev/null
+++ b/external/JiShe.CollectBus.PluginFileWatcher/EventStorage.cs
@@ -0,0 +1,745 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace JiShe.CollectBus.PluginFileWatcher
+{
+ ///
+ /// 负责文件事件的存储、查询和回放
+ ///
+ public class EventStorage : IDisposable
+ {
+ private readonly FileMonitorConfig _config;
+ private readonly ILogger _logger;
+ private readonly ConcurrentQueue _eventQueue;
+ private readonly Timer _storageTimer;
+ private readonly SemaphoreSlim _storageLock = new SemaphoreSlim(1, 1);
+ private readonly string _storageDirectory;
+ private readonly EventDatabaseManager _dbManager;
+ private bool _disposed;
+
+ ///
+ /// 创建新的事件存储管理器实例
+ ///
+ /// 文件监控配置
+ /// 日志记录器
+ public EventStorage(FileMonitorConfig config, ILogger logger)
+ {
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _eventQueue = new ConcurrentQueue();
+
+ // 确保存储目录存在
+ _storageDirectory = !string.IsNullOrEmpty(_config.EventStorage.StorageDirectory)
+ ? _config.EventStorage.StorageDirectory
+ : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "EventLogs");
+
+ if (!Directory.Exists(_storageDirectory))
+ {
+ Directory.CreateDirectory(_storageDirectory);
+ }
+
+ // 创建数据库管理器(如果配置为SQLite存储类型)
+ if (config.EventStorage.EnableEventStorage &&
+ config.EventStorage.StorageType.Equals("SQLite", StringComparison.OrdinalIgnoreCase))
+ {
+ _dbManager = new EventDatabaseManager(config, logger);
+ _logger.LogInformation("已初始化SQLite事件存储");
+ }
+
+ // 初始化存储定时器(如果启用)
+ if (_config.EventStorage.EnableEventStorage)
+ {
+ var intervalMs = _config.EventStorage.StorageIntervalSeconds * 1000;
+ _storageTimer = new Timer(SaveEventsTimerCallback, null, intervalMs, intervalMs);
+ _logger.LogInformation($"事件存储已初始化,存储目录:{_storageDirectory},存储间隔:{_config.EventStorage.StorageIntervalSeconds}秒");
+ }
+ }
+
+ ///
+ /// 记录一个文件事件
+ ///
+ /// 文件事件
+ public void RecordEvent(FileEvent fileEvent)
+ {
+ if (fileEvent == null || !_config.EventStorage.EnableEventStorage) return;
+
+ _eventQueue.Enqueue(fileEvent);
+ _logger.LogDebug($"文件事件已加入队列:{fileEvent.EventType} - {fileEvent.FullPath}");
+ }
+
+ ///
+ /// 从FileSystemEventArgs记录事件
+ ///
+ /// 文件系统事件参数
+ public void RecordEvent(FileSystemEventArgs e)
+ {
+ if (e == null || !_config.EventStorage.EnableEventStorage) return;
+
+ var fileEvent = FileEvent.FromFileSystemEventArgs(e);
+ RecordEvent(fileEvent);
+ }
+
+ ///
+ /// 定时将事件保存到文件
+ ///
+ private async void SaveEventsTimerCallback(object state)
+ {
+ if (_disposed || _eventQueue.IsEmpty) return;
+
+ try
+ {
+ // 防止多个定时器回调同时执行
+ if (!await _storageLock.WaitAsync(0))
+ {
+ return;
+ }
+
+ try
+ {
+ // 从队列中取出事件
+ List eventsToSave = new List();
+ int batchSize = _config.EventStorage.BatchSize;
+
+ while (eventsToSave.Count < batchSize && !_eventQueue.IsEmpty)
+ {
+ if (_eventQueue.TryDequeue(out var fileEvent))
+ {
+ eventsToSave.Add(fileEvent);
+ }
+ }
+
+ if (eventsToSave.Count > 0)
+ {
+ await SaveEventsToFileAsync(eventsToSave);
+ _logger.LogInformation($"已成功保存 {eventsToSave.Count} 个事件");
+ }
+
+ // 如果有配置,清理旧日志文件
+ if (_config.EventStorage.MaxLogFiles > 0)
+ {
+ await CleanupOldLogFilesAsync();
+ }
+
+ // 如果有配置,清理旧数据库记录
+ if (_dbManager != null && _config.EventStorage.DataRetentionDays > 0)
+ {
+ await _dbManager.CleanupOldDataAsync(_config.EventStorage.DataRetentionDays);
+ }
+ }
+ finally
+ {
+ _storageLock.Release();
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "保存事件时发生错误");
+ }
+ }
+
+ ///
+ /// 将事件保存到文件
+ ///
+ /// 要保存的事件列表
+ private async Task SaveEventsToFileAsync(List events)
+ {
+ if (events == null || events.Count == 0) return;
+
+ try
+ {
+ // 根据存储类型选择保存方式
+ if (_config.EventStorage.StorageType.Equals("SQLite", StringComparison.OrdinalIgnoreCase) && _dbManager != null)
+ {
+ // 保存到SQLite数据库
+ await _dbManager.SaveEventsAsync(events);
+ }
+ else
+ {
+ // 保存到文件
+ string fileName = string.Format(
+ _config.EventStorage.LogFileNameFormat,
+ DateTime.Now);
+
+ string filePath = Path.Combine(_storageDirectory, fileName);
+
+ // 创建事件日志文件对象
+ var logFile = new EventLogFile
+ {
+ CreatedTime = DateTime.UtcNow,
+ Events = events
+ };
+
+ // 序列化为JSON
+ string jsonContent = JsonSerializer.Serialize(logFile, new JsonSerializerOptions
+ {
+ WriteIndented = true
+ });
+
+ // 是否启用压缩
+ if (_config.EventStorage.CompressLogFiles)
+ {
+ string gzFilePath = $"{filePath}.gz";
+ await CompressAndSaveStringAsync(jsonContent, gzFilePath);
+ _logger.LogInformation($"已将事件保存到压缩文件:{gzFilePath}");
+ }
+ else
+ {
+ await File.WriteAllTextAsync(filePath, jsonContent);
+ _logger.LogInformation($"已将事件保存到文件:{filePath}");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "保存事件到文件时发生错误");
+ throw;
+ }
+ }
+
+ ///
+ /// 压缩并保存字符串到文件
+ ///
+ /// 要保存的内容
+ /// 文件路径
+ private static async Task CompressAndSaveStringAsync(string content, string filePath)
+ {
+ using var fileStream = new FileStream(filePath, FileMode.Create);
+ using var gzipStream = new GZipStream(fileStream, CompressionLevel.Optimal);
+ using var writer = new StreamWriter(gzipStream);
+
+ await writer.WriteAsync(content);
+ }
+
+ ///
+ /// 清理过多的日志文件
+ ///
+ private async Task CleanupOldLogFilesAsync()
+ {
+ try
+ {
+ // 检查是否需要清理
+ if (_config.EventStorage.MaxLogFiles <= 0) return;
+
+ var directory = new DirectoryInfo(_storageDirectory);
+ var logFiles = directory.GetFiles("*.*")
+ .Where(f => f.Name.EndsWith(".json") || f.Name.EndsWith(".gz"))
+ .OrderByDescending(f => f.CreationTime)
+ .ToArray();
+
+ // 如果文件数量超过最大值,删除最旧的文件
+ if (logFiles.Length > _config.EventStorage.MaxLogFiles)
+ {
+ int filesToDelete = logFiles.Length - _config.EventStorage.MaxLogFiles;
+ var filesToRemove = logFiles.Skip(logFiles.Length - filesToDelete).ToArray();
+
+ foreach (var file in filesToRemove)
+ {
+ try
+ {
+ file.Delete();
+ _logger.LogInformation($"已删除旧的事件日志文件:{file.Name}");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, $"删除旧日志文件失败:{file.FullName}");
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "清理旧日志文件时发生错误");
+ }
+
+ await Task.CompletedTask;
+ }
+
+ ///
+ /// 查询历史事件
+ ///
+ /// 查询参数
+ /// 查询结果
+ public async Task QueryEventsAsync(EventQueryParams queryParams)
+ {
+ if (queryParams == null) throw new ArgumentNullException(nameof(queryParams));
+
+ // 如果是SQLite存储且数据库管理器可用,使用数据库查询
+ if (_config.EventStorage.StorageType.Equals("SQLite", StringComparison.OrdinalIgnoreCase) && _dbManager != null)
+ {
+ return await _dbManager.QueryEventsAsync(queryParams);
+ }
+
+ var result = new EventQueryResult
+ {
+ StartTime = queryParams.StartTime ?? DateTime.MinValue,
+ EndTime = queryParams.EndTime ?? DateTime.MaxValue
+ };
+
+ try
+ {
+ await _storageLock.WaitAsync();
+
+ try
+ {
+ // 获取所有日志文件
+ var directory = new DirectoryInfo(_storageDirectory);
+ if (!directory.Exists)
+ {
+ return result;
+ }
+
+ var logFiles = directory.GetFiles("*.*")
+ .Where(f => f.Name.EndsWith(".json") || f.Name.EndsWith(".gz"))
+ .OrderByDescending(f => f.CreationTime)
+ .ToArray();
+
+ List allEvents = new List();
+
+ // 加载所有日志文件中的事件
+ foreach (var file in logFiles)
+ {
+ try
+ {
+ var events = await LoadEventsFromFileAsync(file.FullName);
+ if (events != null && events.Count > 0)
+ {
+ allEvents.AddRange(events);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, $"从文件加载事件失败:{file.FullName}");
+ }
+ }
+
+ // 内存中队列的事件也包含在查询中
+ FileEvent[] queuedEvents = _eventQueue.ToArray();
+ allEvents.AddRange(queuedEvents);
+
+ // 应用查询过滤条件
+ var filteredEvents = allEvents
+ .Where(e => (queryParams.StartTime == null || e.Timestamp >= queryParams.StartTime) &&
+ (queryParams.EndTime == null || e.Timestamp <= queryParams.EndTime) &&
+ (queryParams.EventType == null || e.EventType == queryParams.EventType.Value) &&
+ (string.IsNullOrEmpty(queryParams.PathFilter) || e.FullPath.Contains(queryParams.PathFilter, StringComparison.OrdinalIgnoreCase)) &&
+ (string.IsNullOrEmpty(queryParams.ExtensionFilter) || e.Extension.Equals(queryParams.ExtensionFilter, StringComparison.OrdinalIgnoreCase)))
+ .ToList();
+
+ // 应用排序
+ IEnumerable orderedEvents = queryParams.AscendingOrder
+ ? filteredEvents.OrderBy(e => e.Timestamp)
+ : filteredEvents.OrderByDescending(e => e.Timestamp);
+
+ // 计算总数
+ result.TotalCount = filteredEvents.Count;
+
+ // 应用分页
+ int skip = queryParams.PageIndex * queryParams.PageSize;
+ int take = queryParams.PageSize;
+
+ result.Events = orderedEvents.Skip(skip).Take(take).ToList();
+ result.HasMore = (skip + take) < result.TotalCount;
+
+ return result;
+ }
+ finally
+ {
+ _storageLock.Release();
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "查询事件时发生错误");
+ throw;
+ }
+ }
+
+ ///
+ /// 从文件加载事件
+ ///
+ /// 文件路径
+ /// 事件列表
+ private async Task> LoadEventsFromFileAsync(string filePath)
+ {
+ if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
+ {
+ return new List();
+ }
+
+ try
+ {
+ string jsonContent;
+
+ // 处理压缩文件
+ if (filePath.EndsWith(".gz"))
+ {
+ using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
+ using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress);
+ using var reader = new StreamReader(gzipStream);
+
+ jsonContent = await reader.ReadToEndAsync();
+ }
+ else
+ {
+ jsonContent = await File.ReadAllTextAsync(filePath);
+ }
+
+ var logFile = JsonSerializer.Deserialize(jsonContent);
+ return logFile?.Events ?? new List();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, $"从文件加载事件失败:{filePath}");
+ return new List();
+ }
+ }
+
+ ///
+ /// 启动事件回放会话
+ ///
+ /// 查询参数,定义要回放的事件
+ /// 回放处理回调
+ /// 取消标记
+ /// 回放会话控制器
+ public async Task StartReplayAsync(
+ EventQueryParams queryParams,
+ Func replayHandler,
+ CancellationToken cancellationToken = default)
+ {
+ if (replayHandler == null) throw new ArgumentNullException(nameof(replayHandler));
+
+ // 查询要回放的事件
+ var queryResult = await QueryEventsAsync(queryParams);
+
+ // 创建并启动回放会话
+ var session = new EventReplaySession(
+ queryResult.Events,
+ replayHandler,
+ _config.EventStorage.ReplayIntervalMs,
+ _config.EventStorage.ReplaySpeedFactor,
+ _logger,
+ cancellationToken);
+
+ await session.StartAsync();
+ return session;
+ }
+
+ ///
+ /// 释放资源
+ ///
+ public void Dispose()
+ {
+ if (_disposed) return;
+
+ _disposed = true;
+ _storageTimer?.Dispose();
+ _storageLock?.Dispose();
+ _dbManager?.Dispose();
+
+ // 尝试保存剩余事件
+ if (_config.EventStorage.EnableEventStorage && !_eventQueue.IsEmpty)
+ {
+ var remainingEvents = new List();
+ while (!_eventQueue.IsEmpty && _eventQueue.TryDequeue(out var evt))
+ {
+ remainingEvents.Add(evt);
+ }
+
+ if (remainingEvents.Count > 0)
+ {
+ SaveEventsToFileAsync(remainingEvents).GetAwaiter().GetResult();
+ }
+ }
+
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ ///
+ /// 事件回放会话
+ ///
+ public class EventReplaySession : IDisposable
+ {
+ private readonly List _events;
+ private readonly Func _replayHandler;
+ private readonly int _replayIntervalMs;
+ private readonly double _speedFactor;
+ private readonly ILogger _logger;
+ private readonly CancellationToken _cancellationToken;
+ private CancellationTokenSource _linkedCts;
+ private Task _replayTask;
+ private bool _disposed;
+ private bool _isPaused;
+ private readonly SemaphoreSlim _pauseSemaphore = new SemaphoreSlim(1, 1);
+
+ ///
+ /// 回放进度(0-100)
+ ///
+ public int Progress { get; private set; }
+
+ ///
+ /// 当前回放的事件索引
+ ///
+ public int CurrentIndex { get; private set; }
+
+ ///
+ /// 事件总数
+ ///
+ public int TotalEvents => _events?.Count ?? 0;
+
+ ///
+ /// 回放是否已完成
+ ///
+ public bool IsCompleted { get; private set; }
+
+ ///
+ /// 回放是否已暂停
+ ///
+ public bool IsPaused => _isPaused;
+
+ ///
+ /// 回放已处理的事件数
+ ///
+ public int ProcessedEvents { get; private set; }
+
+ ///
+ /// 回放开始时间
+ ///
+ public DateTime StartTime { get; private set; }
+
+ ///
+ /// 回放结束时间(如果已完成)
+ ///
+ public DateTime? EndTime { get; private set; }
+
+ ///
+ /// 创建新的事件回放会话
+ ///
+ /// 要回放的事件
+ /// 回放处理回调
+ /// 回放间隔(毫秒)
+ /// 速度因子
+ /// 日志记录器
+ /// 取消标记
+ public EventReplaySession(
+ List events,
+ Func replayHandler,
+ int replayIntervalMs,
+ double speedFactor,
+ ILogger logger,
+ CancellationToken cancellationToken = default)
+ {
+ _events = events ?? throw new ArgumentNullException(nameof(events));
+ _replayHandler = replayHandler ?? throw new ArgumentNullException(nameof(replayHandler));
+ _replayIntervalMs = Math.Max(1, replayIntervalMs);
+ _speedFactor = Math.Max(0.1, speedFactor);
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _cancellationToken = cancellationToken;
+
+ _linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ }
+
+ ///
+ /// 启动回放
+ ///
+ public async Task StartAsync()
+ {
+ if (_replayTask != null) return;
+
+ StartTime = DateTime.Now;
+ _replayTask = Task.Run(ReplayEventsAsync, _linkedCts.Token);
+ await Task.CompletedTask;
+ }
+
+ ///
+ /// 暂停回放
+ ///
+ public async Task PauseAsync()
+ {
+ if (_isPaused || IsCompleted) return;
+
+ await _pauseSemaphore.WaitAsync();
+ try
+ {
+ _isPaused = true;
+ }
+ finally
+ {
+ _pauseSemaphore.Release();
+ }
+
+ _logger.LogInformation("事件回放已暂停");
+ }
+
+ ///
+ /// 恢复回放
+ ///
+ public async Task ResumeAsync()
+ {
+ if (!_isPaused || IsCompleted) return;
+
+ await _pauseSemaphore.WaitAsync();
+ try
+ {
+ _isPaused = false;
+ // 释放信号量以允许回放任务继续
+ _pauseSemaphore.Release();
+ }
+ catch
+ {
+ _pauseSemaphore.Release();
+ throw;
+ }
+
+ _logger.LogInformation("事件回放已恢复");
+ }
+
+ ///
+ /// 停止回放
+ ///
+ public async Task StopAsync()
+ {
+ if (IsCompleted) return;
+
+ try
+ {
+ // 取消回放任务
+ _linkedCts?.Cancel();
+
+ // 如果暂停中,先恢复以允许取消
+ if (_isPaused)
+ {
+ await ResumeAsync();
+ }
+
+ // 等待任务完成
+ if (_replayTask != null)
+ {
+ await Task.WhenAny(_replayTask, Task.Delay(1000));
+ }
+
+ IsCompleted = true;
+ EndTime = DateTime.Now;
+
+ _logger.LogInformation("事件回放已手动停止");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "停止事件回放时发生错误");
+ }
+ }
+
+ ///
+ /// 回放事件处理
+ ///
+ private async Task ReplayEventsAsync()
+ {
+ try
+ {
+ _logger.LogInformation($"开始回放{_events.Count}个事件,速度因子:{_speedFactor}");
+
+ if (_events.Count == 0)
+ {
+ IsCompleted = true;
+ EndTime = DateTime.Now;
+ return;
+ }
+
+ DateTime? lastEventTime = null;
+
+ for (int i = 0; i < _events.Count; i++)
+ {
+ // 检查是否取消
+ if (_linkedCts.Token.IsCancellationRequested)
+ {
+ _logger.LogInformation("事件回放已取消");
+ break;
+ }
+
+ // 检查暂停状态
+ if (_isPaused)
+ {
+ // 等待恢复信号
+ await _pauseSemaphore.WaitAsync(_linkedCts.Token);
+ _pauseSemaphore.Release();
+ }
+
+ var currentEvent = _events[i];
+ CurrentIndex = i;
+
+ // 计算等待时间(根据事件之间的实际时间差和速度因子)
+ if (lastEventTime.HasValue && i > 0)
+ {
+ var actualTimeDiff = currentEvent.Timestamp - lastEventTime.Value;
+ var waitTimeMs = (int)(actualTimeDiff.TotalMilliseconds / _speedFactor);
+
+ // 应用最小等待时间
+ waitTimeMs = Math.Max(_replayIntervalMs, waitTimeMs);
+
+ // 等待指定时间
+ await Task.Delay(waitTimeMs, _linkedCts.Token);
+ }
+
+ // 处理当前事件
+ try
+ {
+ await _replayHandler(currentEvent);
+ ProcessedEvents++;
+
+ // 更新进度
+ Progress = (int)((i + 1) * 100.0 / _events.Count);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, $"处理回放事件时发生错误:{currentEvent.EventType} - {currentEvent.FullPath}");
+ }
+
+ lastEventTime = currentEvent.Timestamp;
+ }
+
+ // 完成回放
+ IsCompleted = true;
+ Progress = 100;
+ EndTime = DateTime.Now;
+
+ _logger.LogInformation($"事件回放已完成,共处理{ProcessedEvents}个事件,耗时:{(EndTime.Value - StartTime).TotalSeconds:F2}秒");
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.LogInformation("事件回放已取消");
+ IsCompleted = true;
+ EndTime = DateTime.Now;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "事件回放过程中发生错误");
+ IsCompleted = true;
+ EndTime = DateTime.Now;
+ }
+ }
+
+ ///
+ /// 释放资源
+ ///
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ _linkedCts?.Cancel();
+ _linkedCts?.Dispose();
+ _pauseSemaphore?.Dispose();
+
+ GC.SuppressFinalize(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/external/JiShe.CollectBus.PluginFileWatcher/FileEvent.cs b/external/JiShe.CollectBus.PluginFileWatcher/FileEvent.cs
new file mode 100644
index 0000000..4e8d905
--- /dev/null
+++ b/external/JiShe.CollectBus.PluginFileWatcher/FileEvent.cs
@@ -0,0 +1,254 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json.Serialization;
+
+namespace JiShe.CollectBus.PluginFileWatcher
+{
+ ///
+ /// 表示一个文件系统事件的数据模型,用于序列化和存储
+ ///
+ public class FileEvent
+ {
+ ///
+ /// 事件唯一标识
+ ///
+ public Guid Id { get; set; } = Guid.NewGuid();
+
+ ///
+ /// 事件发生时间
+ ///
+ public DateTime Timestamp { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// 事件类型
+ ///
+ public FileEventType EventType { get; set; }
+
+ ///
+ /// 文件完整路径
+ ///
+ public string FullPath { get; set; }
+
+ ///
+ /// 文件名
+ ///
+ public string FileName { get; set; }
+
+ ///
+ /// 文件所在目录
+ ///
+ public string Directory { get; set; }
+
+ ///
+ /// 文件扩展名
+ ///
+ public string Extension { get; set; }
+
+ ///
+ /// 重命名前的旧文件名(仅在重命名事件中有效)
+ ///
+ public string OldFileName { get; set; }
+
+ ///
+ /// 重命名前的旧路径(仅在重命名事件中有效)
+ ///
+ public string OldFullPath { get; set; }
+
+ ///
+ /// 文件大小(字节),如果可获取
+ ///
+ public long? FileSize { get; set; }
+
+ ///
+ /// 自定义属性,可用于存储其他元数据
+ ///
+ public Dictionary Metadata { get; set; } = new Dictionary();
+
+ ///
+ /// 从FileSystemEventArgs创建FileEvent
+ ///
+ /// FileSystemEventArgs参数
+ /// FileEvent对象
+ public static FileEvent FromFileSystemEventArgs(FileSystemEventArgs e)
+ {
+ var fileEvent = new FileEvent
+ {
+ EventType = GetEventType(e.ChangeType),
+ FullPath = e.FullPath,
+ FileName = e.Name ?? Path.GetFileName(e.FullPath),
+ Directory = Path.GetDirectoryName(e.FullPath),
+ Extension = Path.GetExtension(e.FullPath)
+ };
+
+ // 如果是重命名事件,添加旧文件名信息
+ if (e is RenamedEventArgs renamedEvent)
+ {
+ fileEvent.OldFileName = Path.GetFileName(renamedEvent.OldFullPath);
+ fileEvent.OldFullPath = renamedEvent.OldFullPath;
+ }
+
+ // 尝试获取文件大小(如果文件存在且可访问)
+ try
+ {
+ if (File.Exists(e.FullPath) && e.ChangeType != WatcherChangeTypes.Deleted)
+ {
+ var fileInfo = new FileInfo(e.FullPath);
+ fileEvent.FileSize = fileInfo.Length;
+
+ // 添加一些额外的元数据
+ fileEvent.Metadata["CreationTime"] = fileInfo.CreationTime.ToString("o");
+ fileEvent.Metadata["LastWriteTime"] = fileInfo.LastWriteTime.ToString("o");
+ fileEvent.Metadata["IsReadOnly"] = fileInfo.IsReadOnly.ToString();
+ }
+ }
+ catch
+ {
+ // 忽略任何获取文件信息时的错误
+ }
+
+ return fileEvent;
+ }
+
+ ///
+ /// 将WatcherChangeTypes转换为FileEventType
+ ///
+ /// WatcherChangeTypes枚举值
+ /// 对应的FileEventType
+ public static FileEventType GetEventType(WatcherChangeTypes changeType)
+ {
+ return changeType switch
+ {
+ WatcherChangeTypes.Created => FileEventType.Created,
+ WatcherChangeTypes.Deleted => FileEventType.Deleted,
+ WatcherChangeTypes.Changed => FileEventType.Modified,
+ WatcherChangeTypes.Renamed => FileEventType.Renamed,
+ _ => FileEventType.Other
+ };
+ }
+ }
+
+ ///
+ /// 文件事件类型
+ ///
+ public enum FileEventType
+ {
+ ///
+ /// 文件被创建
+ ///
+ Created,
+
+ ///
+ /// 文件被修改
+ ///
+ Modified,
+
+ ///
+ /// 文件被删除
+ ///
+ Deleted,
+
+ ///
+ /// 文件被重命名
+ ///
+ Renamed,
+
+ ///
+ /// 其他类型事件
+ ///
+ Other
+ }
+
+ ///
+ /// 表示一个事件日志文件
+ ///
+ public class EventLogFile
+ {
+ ///
+ /// 日志文件创建时间
+ ///
+ public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// 日志文件包含的事件列表
+ ///
+ public List Events { get; set; } = new List();
+ }
+
+ ///
+ /// 事件查询结果
+ ///
+ public class EventQueryResult
+ {
+ ///
+ /// 查询到的事件列表
+ ///
+ public List Events { get; set; } = new List();
+
+ ///
+ /// 匹配的事件总数
+ ///
+ public int TotalCount { get; set; }
+
+ ///
+ /// 查询是否有更多结果
+ ///
+ public bool HasMore { get; set; }
+
+ ///
+ /// 查询时间范围的开始时间
+ ///
+ public DateTime StartTime { get; set; }
+
+ ///
+ /// 查询时间范围的结束时间
+ ///
+ public DateTime EndTime { get; set; }
+ }
+
+ ///
+ /// 事件查询参数
+ ///
+ public class EventQueryParams
+ {
+ ///
+ /// 查询开始时间
+ ///
+ public DateTime? StartTime { get; set; }
+
+ ///
+ /// 查询结束时间
+ ///
+ public DateTime? EndTime { get; set; }
+
+ ///
+ /// 事件类型过滤
+ ///
+ public FileEventType? EventType { get; set; }
+
+ ///
+ /// 文件路径过滤(支持包含关系)
+ ///
+ public string PathFilter { get; set; }
+
+ ///
+ /// 文件扩展名过滤
+ ///
+ public string ExtensionFilter { get; set; }
+
+ ///
+ /// 分页大小
+ ///
+ public int PageSize { get; set; } = 100;
+
+ ///
+ /// 分页索引(从0开始)
+ ///
+ public int PageIndex { get; set; } = 0;
+
+ ///
+ /// 排序方向,true为升序,false为降序
+ ///
+ public bool AscendingOrder { get; set; } = false;
+ }
+}
\ No newline at end of file
diff --git a/external/JiShe.CollectBus.PluginFileWatcher/FileWatcherUtils.cs b/external/JiShe.CollectBus.PluginFileWatcher/FileWatcherUtils.cs
new file mode 100644
index 0000000..aeb7f90
--- /dev/null
+++ b/external/JiShe.CollectBus.PluginFileWatcher/FileWatcherUtils.cs
@@ -0,0 +1,157 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace JiShe.CollectBus.PluginFileWatcher
+{
+ ///
+ /// 文件监控相关工具类
+ ///
+ public static class FileWatcherUtils
+ {
+ ///
+ /// 检测文件是否被锁定
+ ///
+ /// 要检查的文件路径
+ /// 如果文件被锁定则返回true,否则返回false
+ public static bool IsFileLocked(string filePath)
+ {
+ if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
+ return false;
+
+ try
+ {
+ using (FileStream stream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
+ {
+ // 文件可以被完全访问,没有被锁定
+ stream.Close();
+ }
+ return false;
+ }
+ catch (IOException)
+ {
+ // 文件被锁定或正在被其他进程使用
+ return true;
+ }
+ catch (Exception)
+ {
+ // 其他错误(权限不足、路径无效等)
+ return true;
+ }
+ }
+
+ ///
+ /// 尝试处理一个可能被锁定的文件
+ ///
+ /// 文件路径
+ /// 成功解锁后要执行的操作
+ /// 健壮性配置
+ /// 处理结果:true表示成功处理,false表示处理失败
+ public static async Task TryHandleLockedFileAsync(string filePath, Func action, RobustnessConfig config)
+ {
+ if (!config.EnableFileLockDetection)
+ {
+ // 如果禁用了锁检测,直接执行操作
+ try
+ {
+ await action();
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ // 如果文件不存在或不是锁定状态,直接执行操作
+ if (!File.Exists(filePath) || !IsFileLocked(filePath))
+ {
+ try
+ {
+ await action();
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ // 文件被锁定,根据策略处理
+ switch (config.LockedFileStrategy.ToLower())
+ {
+ case "skip":
+ // 跳过这个文件
+ Console.WriteLine($"文件被锁定,已跳过: {filePath}");
+ return false;
+
+ case "retry":
+ // 重试几次
+ for (int i = 0; i < config.FileLockRetryCount; i++)
+ {
+ await Task.Delay(config.FileLockRetryDelayMs);
+
+ if (!IsFileLocked(filePath))
+ {
+ try
+ {
+ await action();
+ Console.WriteLine($"文件锁已释放,成功处理: {filePath}");
+ return true;
+ }
+ catch
+ {
+ // 继续重试
+ }
+ }
+ }
+ Console.WriteLine($"文件仍然被锁定,重试{config.FileLockRetryCount}次后放弃: {filePath}");
+ return false;
+
+ case "log":
+ default:
+ // 只记录不处理
+ Console.WriteLine($"文件被锁定,已记录: {filePath}");
+ return false;
+ }
+ }
+
+ ///
+ /// 检查文件系统监控器是否健康
+ ///
+ /// 要检查的监控器
+ /// 最后一次事件的时间
+ /// 健壮性配置
+ /// 如果监控器健康则返回true,否则返回false
+ public static bool IsWatcherHealthy(FileSystemWatcher watcher, DateTime lastEventTime, RobustnessConfig config)
+ {
+ if (watcher == null || !watcher.EnableRaisingEvents)
+ return false;
+
+ // 如果配置了超时时间,检查是否超时
+ if (config.WatcherTimeoutSeconds > 0)
+ {
+ // 如果最后事件时间超过了超时时间,认为监控器可能已经失效
+ TimeSpan timeSinceLastEvent = DateTime.UtcNow - lastEventTime;
+ if (timeSinceLastEvent.TotalSeconds > config.WatcherTimeoutSeconds)
+ {
+ // 执行一个简单的测试:尝试改变一些属性看是否抛出异常
+ try
+ {
+ var currentFilter = watcher.Filter;
+ watcher.Filter = currentFilter;
+ return true; // 如果没有异常,认为监控器仍然正常
+ }
+ catch
+ {
+ return false; // 抛出异常,认为监控器已经失效
+ }
+ }
+ }
+
+ // 默认情况下认为监控器健康
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/external/JiShe.CollectBus.PluginFileWatcher/JiShe.CollectBus.PluginFileWatcher.csproj b/external/JiShe.CollectBus.PluginFileWatcher/JiShe.CollectBus.PluginFileWatcher.csproj
new file mode 100644
index 0000000..193c217
--- /dev/null
+++ b/external/JiShe.CollectBus.PluginFileWatcher/JiShe.CollectBus.PluginFileWatcher.csproj
@@ -0,0 +1,39 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ Linux
+ ..\..
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/external/JiShe.CollectBus.PluginFileWatcher/Program.cs b/external/JiShe.CollectBus.PluginFileWatcher/Program.cs
new file mode 100644
index 0000000..92c47b8
--- /dev/null
+++ b/external/JiShe.CollectBus.PluginFileWatcher/Program.cs
@@ -0,0 +1,1110 @@
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using System.Linq;
+using Microsoft.Extensions.Configuration;
+using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Serilog;
+using Serilog.Events;
+using Serilog.Extensions.Logging;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+namespace JiShe.CollectBus.PluginFileWatcher
+{
+ // 删除配置类的定义
+
+ class Program
+ {
+ // 配置对象,从appsettings.json加载
+ private static FileMonitorConfig _config = new FileMonitorConfig();
+
+ // 全局计数器和动态配置参数
+ private static long _totalEventsProcessed = 0;
+
+ // 监控器健康状态追踪
+ private static DateTime _lastEventTime = DateTime.UtcNow;
+ private static int _watcherRestartAttempts = 0;
+ private static FileSystemWatcher _activeWatcher = null;
+
+ // 事件存储和回放
+ private static EventStorage _eventStorage = null;
+ private static EventReplaySession _currentReplaySession = null;
+ private static ILogger _logger = NullLogger.Instance;
+
+ static async Task Main(string[] args)
+ {
+ Console.OutputEncoding = System.Text.Encoding.UTF8;
+
+ // 检查是否以数据库工具模式运行
+ if (args.Length > 0 && args[0].Equals("db", StringComparison.OrdinalIgnoreCase))
+ {
+ // 运行数据库工具
+ await RunDatabaseUtilityAsync(args.Skip(1).ToArray());
+ return;
+ }
+
+ // 加载配置文件
+ LoadConfiguration();
+
+ // 配置Serilog
+ ConfigureSerilog();
+
+ // 初始化日志记录器
+ _logger = new SerilogLoggerFactory().CreateLogger("FileMonitor");
+
+ Log.Information("高性能文件监控程序启动中...");
+
+ // 初始化事件存储
+ if (_config.EventStorage.EnableEventStorage)
+ {
+ _eventStorage = new EventStorage(_config, _logger);
+ Log.Information("事件存储已启用,存储目录:{Directory}", _config.EventStorage.StorageDirectory);
+ }
+
+ // 打印配置信息
+ LogConfiguration();
+
+ // 获取监控路径:优先命令行参数,其次配置文件,最后使用当前目录
+ string pathToMonitor = args.Length > 0 ? args[0] :
+ !string.IsNullOrEmpty(_config.General.DefaultMonitorPath) ?
+ _config.General.DefaultMonitorPath : Directory.GetCurrentDirectory();
+
+ // 命令行参数可以覆盖配置文件
+ if (args.Length > 1 && args[1].Equals("--no-filter", StringComparison.OrdinalIgnoreCase))
+ {
+ _config.General.EnableFileFiltering = false;
+ Log.Information("通过命令行参数禁用文件类型过滤,将监控所有文件");
+ }
+ else if (_config.General.EnableFileFiltering)
+ {
+ Log.Information("已启用文件类型过滤,仅监控以下类型: {Extensions}", string.Join(", ", _config.FileFilters.AllowedExtensions));
+ }
+
+ if (!Directory.Exists(pathToMonitor))
+ {
+ Log.Warning("错误:监控目录 '{Path}' 不存在。", pathToMonitor);
+ Log.Information("尝试创建该目录...");
+
+ try
+ {
+ Directory.CreateDirectory(pathToMonitor);
+ Log.Information("已成功创建目录: {Path}", pathToMonitor);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "创建目录失败: {Message}", ex.Message);
+ Log.Information("将使用当前目录作为监控路径。");
+ pathToMonitor = Directory.GetCurrentDirectory();
+ }
+ }
+
+ Log.Information("开始监控目录: {Path}", pathToMonitor);
+ Log.Information("按 'Q' 退出程序,按 'R' 重新加载配置,按 'H' 检查监控器健康状态。");
+ if (_config.EventStorage.EnableEventStorage && _config.EventStorage.EnableEventReplay)
+ {
+ Log.Information("按 'P' 开始/暂停回放,按 'S' 停止回放,按 'B' 查询事件。");
+ }
+
+ // 创建一个容量由配置文件指定的有界通道
+ var channel = Channel.CreateBounded(new BoundedChannelOptions(_config.Performance.ChannelCapacity)
+ {
+ FullMode = BoundedChannelFullMode.DropOldest // 当通道满时丢弃旧事件
+ });
+
+ using var cts = new CancellationTokenSource();
+
+ // 启动内存监控
+ var memoryMonitorTask = StartMemoryMonitorAsync(cts.Token);
+
+ // 启动文件监控器
+ var monitorTask = StartFileMonitorAsync(pathToMonitor, channel.Writer, cts.Token);
+
+ // 启动健康监控任务
+ var healthMonitorTask = _config.Robustness.EnableAutoRecovery ?
+ StartWatcherHealthMonitorAsync(pathToMonitor, channel.Writer, cts.Token) : Task.CompletedTask;
+
+ // 启动事件处理器
+ var processorTask = ProcessEventsAsync(channel.Reader, cts.Token);
+
+ // 等待用户按下键退出或重新加载配置
+ bool needRestart = false;
+ while (true)
+ {
+ var key = Console.ReadKey(true).Key;
+ if (key == ConsoleKey.Q)
+ {
+ break; // 退出循环
+ }
+ else if (key == ConsoleKey.R)
+ {
+ Log.Information("正在重新加载配置...");
+ LoadConfiguration();
+ // 重新配置Serilog
+ ConfigureSerilog();
+ LogConfiguration();
+ Log.Information("配置已重新加载,部分配置需要重启程序才能生效。");
+ }
+ else if (key == ConsoleKey.H)
+ {
+ // 手动检查监控器健康状态
+ bool isHealthy = _activeWatcher != null &&
+ FileWatcherUtils.IsWatcherHealthy(_activeWatcher, _lastEventTime, _config.Robustness);
+ Log.Information("监控器健康状态: {Status}", (isHealthy ? "正常" : "异常"));
+ Log.Information("上次事件时间: {Time}", _lastEventTime);
+ Log.Information("重启次数: {Count}/{MaxCount}", _watcherRestartAttempts, _config.Robustness.MaxRestartAttempts);
+ Log.Information("已处理事件总数: {Count}", _totalEventsProcessed);
+ }
+ else if (key == ConsoleKey.P && _config.EventStorage.EnableEventStorage && _config.EventStorage.EnableEventReplay)
+ {
+ await HandleEventReplayToggleAsync();
+ }
+ else if (key == ConsoleKey.S && _config.EventStorage.EnableEventStorage && _config.EventStorage.EnableEventReplay)
+ {
+ await StopReplayAsync();
+ }
+ else if (key == ConsoleKey.B && _config.EventStorage.EnableEventStorage)
+ {
+ await QueryEventsAsync();
+ }
+
+ await Task.Delay(100);
+ }
+
+ // 取消所有任务
+ cts.Cancel();
+
+ // 停止回放(如果正在进行)
+ await StopReplayAsync();
+
+ // 释放事件存储
+ _eventStorage?.Dispose();
+
+ try
+ {
+ var tasks = new List { monitorTask, processorTask, memoryMonitorTask };
+ if (_config.Robustness.EnableAutoRecovery)
+ {
+ tasks.Add(healthMonitorTask);
+ }
+
+ await Task.WhenAll(tasks);
+ }
+ catch (OperationCanceledException)
+ {
+ Log.Information("程序已正常退出。");
+ }
+
+ // 关闭Serilog
+ Log.CloseAndFlush();
+ }
+
+ // 配置Serilog
+ private static void ConfigureSerilog()
+ {
+ var configuration = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
+ .Build();
+
+ var loggerConfig = new LoggerConfiguration()
+ .ReadFrom.Configuration(configuration);
+
+ // 根据配置设置最小日志级别
+ LogEventLevel minimumLevel = LogEventLevel.Information;
+ if (!string.IsNullOrEmpty(_config.Logging?.LogLevel))
+ {
+ switch (_config.Logging.LogLevel.ToLower())
+ {
+ case "verbose":
+ minimumLevel = LogEventLevel.Verbose;
+ break;
+ case "debug":
+ minimumLevel = LogEventLevel.Debug;
+ break;
+ case "information":
+ minimumLevel = LogEventLevel.Information;
+ break;
+ case "warning":
+ minimumLevel = LogEventLevel.Warning;
+ break;
+ case "error":
+ minimumLevel = LogEventLevel.Error;
+ break;
+ case "fatal":
+ minimumLevel = LogEventLevel.Fatal;
+ break;
+ }
+ }
+
+ // 如果配置文件中没有Serilog节,使用默认配置
+ var hasConfig = configuration.GetSection("Serilog").Exists();
+ if (!hasConfig)
+ {
+ loggerConfig
+ .MinimumLevel.Is(minimumLevel)
+ .WriteTo.Console(
+ outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u3}] {Message:lj}{NewLine}{Exception}"
+ )
+ .WriteTo.File(
+ Path.Combine(_config.Logging?.LogDirectory ?? "Logs", "filemonitor-.log"),
+ rollingInterval: RollingInterval.Day,
+ outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u3}] {Message:lj}{NewLine}{Exception}",
+ retainedFileCountLimit: _config.Logging?.RetainedLogDays ?? 30
+ );
+ }
+
+ Log.Logger = loggerConfig.CreateLogger();
+ }
+
+ // 查询事件
+ private static async Task QueryEventsAsync()
+ {
+ if (_eventStorage == null)
+ {
+ Log.Warning("事件存储未启用");
+ return;
+ }
+
+ try
+ {
+ Log.Information("\n================ 事件查询 ================");
+ Console.WriteLine("请输入查询参数,直接回车使用默认值");
+
+ // 收集查询参数
+ var queryParams = new EventQueryParams();
+
+ Console.Write("开始时间 (yyyy-MM-dd HH:mm:ss),默认24小时前: ");
+ string startTimeStr = Console.ReadLine();
+ if (!string.IsNullOrEmpty(startTimeStr))
+ {
+ if (DateTime.TryParse(startTimeStr, out var startTime))
+ {
+ queryParams.StartTime = startTime;
+ }
+ else
+ {
+ Log.Warning("无效的日期格式,使用默认值");
+ queryParams.StartTime = DateTime.Now.AddDays(-1);
+ }
+ }
+ else
+ {
+ queryParams.StartTime = DateTime.Now.AddDays(-1);
+ }
+
+ Console.Write("结束时间 (yyyy-MM-dd HH:mm:ss),默认当前时间: ");
+ string endTimeStr = Console.ReadLine();
+ if (!string.IsNullOrEmpty(endTimeStr))
+ {
+ if (DateTime.TryParse(endTimeStr, out var endTime))
+ {
+ queryParams.EndTime = endTime;
+ }
+ else
+ {
+ Log.Warning("无效的日期格式,使用默认值");
+ }
+ }
+
+ Console.Write("事件类型 (Created, Modified, Deleted, Renamed, All),默认All: ");
+ string eventTypeStr = Console.ReadLine();
+ if (!string.IsNullOrEmpty(eventTypeStr) && eventTypeStr.ToLower() != "all")
+ {
+ if (Enum.TryParse(eventTypeStr, true, out var eventType))
+ {
+ queryParams.EventType = eventType;
+ }
+ else
+ {
+ Log.Warning("无效的事件类型,使用默认值All");
+ }
+ }
+
+ Console.Write("路径过滤 (包含此文本的路径),默认无: ");
+ string pathFilter = Console.ReadLine();
+ if (!string.IsNullOrEmpty(pathFilter))
+ {
+ queryParams.PathFilter = pathFilter;
+ }
+
+ Console.Write("文件扩展名过滤 (.txt, .exe等),默认无: ");
+ string extensionFilter = Console.ReadLine();
+ if (!string.IsNullOrEmpty(extensionFilter))
+ {
+ queryParams.ExtensionFilter = extensionFilter;
+ }
+
+ Console.Write("每页显示数量,默认20: ");
+ string pageSizeStr = Console.ReadLine();
+ if (!string.IsNullOrEmpty(pageSizeStr) && int.TryParse(pageSizeStr, out var pageSize))
+ {
+ queryParams.PageSize = pageSize;
+ }
+ else
+ {
+ queryParams.PageSize = 20;
+ }
+
+ // 执行查询
+ var result = await _eventStorage.QueryEventsAsync(queryParams);
+
+ Log.Information("查询结果 (共{Count}条记录):", result.TotalCount);
+ Log.Information("时间范围: {StartTime} 至 {EndTime}", result.StartTime, result.EndTime);
+ Console.WriteLine("---------------------------------------------");
+
+ if (result.Events.Count == 0)
+ {
+ Log.Warning("未找到符合条件的事件");
+ }
+ else
+ {
+ foreach (var evt in result.Events)
+ {
+ string eventType = evt.EventType.ToString();
+ string timeStr = evt.Timestamp.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss.fff");
+
+ Console.WriteLine($"[{timeStr}] [{eventType}] {evt.FileName}");
+ Console.WriteLine($" 路径: {evt.Directory}");
+
+ if (evt.EventType == FileEventType.Renamed && !string.IsNullOrEmpty(evt.OldFileName))
+ {
+ Console.WriteLine($" 原名称: {evt.OldFileName}");
+ }
+
+ Console.WriteLine();
+ }
+
+ if (result.HasMore)
+ {
+ Log.Warning("... 还有更多记录未显示 ...");
+ }
+ }
+
+ // 询问是否开始回放
+ if (result.Events.Count > 0 && _config.EventStorage.EnableEventReplay)
+ {
+ Console.Write("\n是否要回放这些事件? (Y/N): ");
+ string replayAnswer = Console.ReadLine();
+ if (!string.IsNullOrEmpty(replayAnswer) && replayAnswer.ToUpper().StartsWith("Y"))
+ {
+ await StartReplayAsync(queryParams);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "查询事件时出错: {Message}", ex.Message);
+ }
+
+ Console.WriteLine("=============================================\n");
+ }
+
+ // 处理回放切换(开始/暂停)
+ private static async Task HandleEventReplayToggleAsync()
+ {
+ if (_eventStorage == null) return;
+
+ try
+ {
+ if (_currentReplaySession == null)
+ {
+ // 如果没有活动的回放会话,创建一个简单的查询参数并开始回放
+ var queryParams = new EventQueryParams
+ {
+ StartTime = DateTime.Now.AddHours(-1),
+ PageSize = 1000
+ };
+
+ await StartReplayAsync(queryParams);
+ }
+ else if (_currentReplaySession.IsPaused)
+ {
+ // 恢复回放
+ await _currentReplaySession.ResumeAsync();
+ Log.Information("已恢复事件回放");
+ }
+ else
+ {
+ // 暂停回放
+ await _currentReplaySession.PauseAsync();
+ Log.Information("已暂停事件回放");
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "处理回放时出错: {Message}", ex.Message);
+ }
+ }
+
+ // 开始回放
+ private static async Task StartReplayAsync(EventQueryParams queryParams)
+ {
+ if (_eventStorage == null || !_config.EventStorage.EnableEventReplay) return;
+
+ try
+ {
+ // 停止现有回放
+ await StopReplayAsync();
+
+ // 创建回放处理器
+ Func replayHandler = async (e) =>
+ {
+ // 在控制台上显示回放的事件
+ string eventType = e.EventType.ToString();
+ string timeStr = e.Timestamp.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss.fff");
+
+ Log.Information("[回放] [{Time}] [{Type}] {FileName}", timeStr, eventType, e.FileName);
+
+ if (_config.Logging.LogFileEventDetails)
+ {
+ Log.Debug(" 路径: {Path}", e.Directory);
+
+ if (e.EventType == FileEventType.Renamed && !string.IsNullOrEmpty(e.OldFileName))
+ {
+ Log.Debug(" 原名称: {OldName}", e.OldFileName);
+ }
+ }
+
+ // 这里可以添加实际的文件操作,比如创建、删除文件等
+ // 但在此示例中,我们只是显示事件而不执行实际操作
+
+ await Task.CompletedTask;
+ };
+
+ // 开始回放会话
+ _currentReplaySession = await _eventStorage.StartReplayAsync(
+ queryParams,
+ replayHandler,
+ CancellationToken.None);
+
+ // 如果会话创建成功,开始回放
+ if (_currentReplaySession != null)
+ {
+ await _currentReplaySession.StartAsync();
+ Log.Information("开始回放事件,共 {Count} 个事件", _currentReplaySession.TotalEvents);
+ Log.Information("回放速度: {Speed}", (_config.EventStorage.ReplaySpeedFactor > 1 ?
+ $"{_config.EventStorage.ReplaySpeedFactor}x (加速)" :
+ $"{_config.EventStorage.ReplaySpeedFactor}x (减速)"));
+ }
+ else
+ {
+ Log.Warning("未找到符合条件的事件,回放未开始");
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "开始回放时出错: {Message}", ex.Message);
+ }
+ }
+
+ // 停止回放
+ private static async Task StopReplayAsync()
+ {
+ if (_currentReplaySession != null)
+ {
+ try
+ {
+ await _currentReplaySession.StopAsync();
+ Log.Information("已停止事件回放");
+
+ // 输出回放统计
+ Log.Information("回放统计: 共处理 {Processed}/{Total} 个事件",
+ _currentReplaySession.ProcessedEvents,
+ _currentReplaySession.TotalEvents);
+
+ TimeSpan duration = (_currentReplaySession.EndTime ?? DateTime.Now) - _currentReplaySession.StartTime;
+ Log.Information("回放持续时间: {Duration:F1} 秒", duration.TotalSeconds);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "停止回放时出错: {Message}", ex.Message);
+ }
+ finally
+ {
+ _currentReplaySession.Dispose();
+ _currentReplaySession = null;
+ }
+ }
+ }
+
+ // 加载配置文件
+ private static void LoadConfiguration()
+ {
+ try
+ {
+ // 检查配置文件是否存在
+ string configPath = Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json");
+ if (!File.Exists(configPath))
+ {
+ Log.Warning("配置文件不存在,使用默认配置。");
+
+ // 使用默认配置
+ _config = new FileMonitorConfig
+ {
+ General = new GeneralConfig
+ {
+ EnableFileFiltering = true,
+ MemoryMonitorIntervalMinutes = 1,
+ DefaultMonitorPath = Path.Combine(Directory.GetCurrentDirectory(), "MonitorFiles")
+ },
+ FileFilters = new FileFiltersConfig
+ {
+ AllowedExtensions = new[] { ".dll" },
+ ExcludedDirectories = new[] { "bin", "obj", "node_modules" },
+ IncludeSubdirectories = true
+ },
+ Performance = new PerformanceConfig
+ {
+ MemoryCleanupThreshold = 5000,
+ ChannelCapacity = 1000,
+ EventDebounceTimeSeconds = 3,
+ MaxDictionarySize = 10000,
+ CleanupIntervalSeconds = 5,
+ ProcessingDelayMs = 5
+ },
+ Robustness = new RobustnessConfig
+ {
+ EnableAutoRecovery = true,
+ WatcherHealthCheckIntervalSeconds = 30,
+ WatcherTimeoutSeconds = 60,
+ MaxRestartAttempts = 3,
+ RestartDelaySeconds = 5,
+ EnableFileLockDetection = true,
+ LockedFileStrategy = "Retry",
+ FileLockRetryCount = 3,
+ FileLockRetryDelayMs = 500
+ },
+ NotifyFilters = new List { "LastWrite", "FileName", "DirectoryName", "CreationTime" },
+ EventStorage = new EventStorageConfig
+ {
+ EnableEventStorage = false,
+ StorageDirectory = Path.Combine(Directory.GetCurrentDirectory(), "EventStorage"),
+ EnableEventReplay = false,
+ ReplaySpeedFactor = 1
+ },
+ Logging = new LoggingConfig
+ {
+ LogLevel = "Information",
+ LogFileEventDetails = false,
+ RetainedLogDays = 30,
+ LogDirectory = "Logs"
+ }
+ };
+
+ return;
+ }
+
+ var builder = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
+
+ var configuration = builder.Build();
+
+ // 绑定配置到对象
+ var config = new FileMonitorConfig();
+ configuration.GetSection("FileMonitorConfig").Bind(config);
+
+ // 更新全局配置
+ _config = config;
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "加载配置文件失败: {Message}", ex.Message);
+ Log.Warning("将使用默认配置。");
+
+ // 使用默认配置
+ _config = new FileMonitorConfig
+ {
+ General = new GeneralConfig
+ {
+ EnableFileFiltering = true,
+ MemoryMonitorIntervalMinutes = 1,
+ DefaultMonitorPath = Path.Combine(Directory.GetCurrentDirectory(), "MonitorFiles")
+ },
+ FileFilters = new FileFiltersConfig
+ {
+ AllowedExtensions = new[] { ".dll" },
+ ExcludedDirectories = new[] { "bin", "obj", "node_modules" },
+ IncludeSubdirectories = true
+ },
+ Performance = new PerformanceConfig
+ {
+ MemoryCleanupThreshold = 5000,
+ ChannelCapacity = 1000,
+ EventDebounceTimeSeconds = 3,
+ MaxDictionarySize = 10000,
+ CleanupIntervalSeconds = 5,
+ ProcessingDelayMs = 5
+ },
+ Robustness = new RobustnessConfig
+ {
+ EnableAutoRecovery = true,
+ WatcherHealthCheckIntervalSeconds = 30,
+ WatcherTimeoutSeconds = 60,
+ MaxRestartAttempts = 3,
+ RestartDelaySeconds = 5,
+ EnableFileLockDetection = true,
+ LockedFileStrategy = "Retry",
+ FileLockRetryCount = 3,
+ FileLockRetryDelayMs = 500
+ },
+ NotifyFilters = new List { "LastWrite", "FileName", "DirectoryName", "CreationTime" },
+ EventStorage = new EventStorageConfig
+ {
+ EnableEventStorage = false,
+ StorageDirectory = Path.Combine(Directory.GetCurrentDirectory(), "EventStorage"),
+ EnableEventReplay = false,
+ ReplaySpeedFactor = 1
+ },
+ Logging = new LoggingConfig
+ {
+ LogLevel = "Information",
+ LogFileEventDetails = false,
+ RetainedLogDays = 30,
+ LogDirectory = "Logs"
+ }
+ };
+ }
+ }
+
+ // 打印当前配置信息
+ private static void LogConfiguration()
+ {
+ Log.Information("当前配置信息:");
+ Log.Information(" 默认监控路径: {Path}", _config.General.DefaultMonitorPath);
+ Log.Information(" 文件过滤: {Status}", (_config.General.EnableFileFiltering ? "启用" : "禁用"));
+ if (_config.General.EnableFileFiltering)
+ {
+ Log.Information(" 监控文件类型: {Types}", string.Join(", ", _config.FileFilters.AllowedExtensions));
+ Log.Information(" 排除目录: {Dirs}", string.Join(", ", _config.FileFilters.ExcludedDirectories));
+ }
+ Log.Information(" 内存监控间隔: {Minutes}分钟", _config.General.MemoryMonitorIntervalMinutes);
+ Log.Information(" 内存清理阈值: 每{Threshold}个事件", _config.Performance.MemoryCleanupThreshold);
+ Log.Information(" 通道容量: {Capacity}", _config.Performance.ChannelCapacity);
+ Log.Information(" 事件去抖时间: {Seconds}秒", _config.Performance.EventDebounceTimeSeconds);
+ Log.Information(" 自动恢复: {Status}", (_config.Robustness.EnableAutoRecovery ? "启用" : "禁用"));
+ Log.Information(" 文件锁检测: {Status}", (_config.Robustness.EnableFileLockDetection ? "启用" : "禁用"));
+ if (_config.Robustness.EnableFileLockDetection)
+ {
+ Log.Information(" 锁定文件策略: {Strategy}", _config.Robustness.LockedFileStrategy);
+ }
+ Log.Information(" 日志级别: {Level}", _config.Logging?.LogLevel ?? "Information");
+ }
+
+ // 检查文件是否应该被处理的方法
+ private static bool ShouldProcessFile(string filePath)
+ {
+ if (!_config.General.EnableFileFiltering)
+ return true;
+
+ if (string.IsNullOrEmpty(filePath))
+ return false;
+
+ // 检查是否在排除目录中
+ foreach (var excludedDir in _config.FileFilters.ExcludedDirectories)
+ {
+ if (filePath.Contains($"{Path.DirectorySeparatorChar}{excludedDir}{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase) ||
+ filePath.EndsWith($"{Path.DirectorySeparatorChar}{excludedDir}", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+
+ string extension = Path.GetExtension(filePath);
+ return _config.FileFilters.AllowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
+ }
+
+ // 添加内存监控方法
+ private static async Task StartMemoryMonitorAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ // 等待指定时间
+ await Task.Delay(TimeSpan.FromMinutes(_config.General.MemoryMonitorIntervalMinutes), cancellationToken);
+
+ // 获取当前内存使用情况
+ long currentMemory = GC.GetTotalMemory(false) / (1024 * 1024); // MB
+
+ Log.Information("内存使用: {Memory} MB, 已处理事件: {EventCount}", currentMemory, _totalEventsProcessed);
+
+ // 强制执行垃圾回收
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+
+ // 显示垃圾回收后的内存
+ long afterGCMemory = GC.GetTotalMemory(true) / (1024 * 1024); // MB
+ Log.Debug("内存清理后: {Memory} MB (释放: {Released} MB)", afterGCMemory, currentMemory - afterGCMemory);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // 预期的取消异常
+ }
+ }
+
+ // 创建文件监控器
+ private static FileSystemWatcher CreateFileWatcher(string path, ChannelWriter writer, CancellationToken cancellationToken)
+ {
+ // 创建一个新的文件系统监控器
+ var watcher = new FileSystemWatcher(path);
+
+ // 从配置设置NotifyFilters
+ NotifyFilters notifyFilters = NotifyFilters.LastWrite; // 默认值
+ foreach (var filterName in _config.NotifyFilters)
+ {
+ if (Enum.TryParse(filterName, out var filter))
+ {
+ notifyFilters |= filter;
+ }
+ }
+
+ watcher.NotifyFilter = notifyFilters;
+ watcher.IncludeSubdirectories = _config.FileFilters.IncludeSubdirectories;
+ watcher.EnableRaisingEvents = true;
+
+ // 设置文件过滤器,如果启用了过滤
+ if (_config.General.EnableFileFiltering && _config.FileFilters.AllowedExtensions.Length > 0)
+ {
+ // 在FileSystemWatcher级别设置过滤器,这样可以减少系统生成的事件数量
+ // 只能设置一个扩展名作为Filter,所以我们选择第一个
+ string firstExtension = _config.FileFilters.AllowedExtensions[0];
+ watcher.Filter = $"*{firstExtension}";
+ Log.Information("已设置文件系统监控过滤器: *{Filter}", firstExtension);
+
+ if (_config.FileFilters.AllowedExtensions.Length > 1)
+ {
+ Log.Warning("注意: FileSystemWatcher只支持单一过滤器,其他文件类型将在事件处理时过滤。");
+ }
+ }
+
+ // 注册事件处理程序
+ watcher.Created += (sender, e) => HandleFileEvent(sender, e, writer, cancellationToken);
+ watcher.Changed += (sender, e) => HandleFileEvent(sender, e, writer, cancellationToken);
+ watcher.Deleted += (sender, e) => HandleFileEvent(sender, e, writer, cancellationToken);
+ watcher.Renamed += (sender, e) => HandleFileEvent(sender, e, writer, cancellationToken);
+ watcher.Error += (sender, e) =>
+ {
+ Log.Error(e.GetException(), "文件监控错误: {Message}", e.GetException().Message);
+ };
+
+ return watcher;
+ }
+
+ // 文件事件处理
+ private static void HandleFileEvent(object sender, FileSystemEventArgs e, ChannelWriter writer, CancellationToken cancellationToken)
+ {
+ // 更新最后事件时间
+ _lastEventTime = DateTime.UtcNow;
+
+ // 如果启用了事件存储,记录事件
+ _eventStorage?.RecordEvent(e);
+
+ try
+ {
+ // 如果是退出信号,忽略后续处理
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ // 只处理指定类型的文件,如果启用了文件过滤
+ if (!ShouldProcessFile(e.FullPath))
+ {
+ return;
+ }
+
+ // 尝试将事件写入通道,如果满了就丢弃
+ // 不等待,以免阻塞文件系统事件
+ if (!writer.TryWrite(e))
+ {
+ Log.Warning("警告: 事件处理队列已满,部分事件被丢弃");
+ }
+ }
+ catch (Exception ex)
+ {
+ // 捕获任何异常,防止崩溃
+ Log.Error(ex, "处理文件事件时出错: {Message}", ex.Message);
+ }
+ }
+
+ private static async Task StartFileMonitorAsync(string path, ChannelWriter writer, CancellationToken cancellationToken)
+ {
+ // 使用TaskCompletionSource来控制任务的完成
+ var tcs = new TaskCompletionSource();
+
+ try
+ {
+ // 创建并启动监控器
+ _activeWatcher = CreateFileWatcher(path, writer, cancellationToken);
+
+ // 注册取消回调
+ cancellationToken.Register(() =>
+ {
+ try
+ {
+ if (_activeWatcher != null)
+ {
+ // 确保所有资源正确清理
+ _activeWatcher.EnableRaisingEvents = false;
+ _activeWatcher.Dispose();
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "关闭文件监控器时出错: {Message}", ex.Message);
+ }
+ finally
+ {
+ tcs.TrySetResult(true);
+ }
+ });
+
+ // 等待任务被取消或完成
+ await tcs.Task;
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "启动文件监控器时出错: {Message}", ex.Message);
+ tcs.TrySetException(ex);
+ throw;
+ }
+ }
+
+ private static async Task ProcessEventsAsync(ChannelReader reader, CancellationToken cancellationToken)
+ {
+ try
+ {
+ // 使用配置的字典大小
+ var recentlyProcessed = new ConcurrentDictionary();
+
+ // 清理计时器,频率由配置决定
+ using var cleanupTimer = new Timer(_ =>
+ {
+ try
+ {
+ var cutoff = DateTime.UtcNow.AddSeconds(-_config.Performance.EventDebounceTimeSeconds);
+ int removedCount = 0;
+
+ // 限制字典大小
+ if (recentlyProcessed.Count > _config.Performance.MaxDictionarySize)
+ {
+ // 找出最旧的条目删除
+ var oldestItems = recentlyProcessed
+ .OrderBy(kv => kv.Value)
+ .Take(recentlyProcessed.Count - _config.Performance.MaxDictionarySize / 2);
+
+ foreach (var item in oldestItems)
+ {
+ DateTime dummy;
+ if (recentlyProcessed.TryRemove(item.Key, out dummy))
+ {
+ removedCount++;
+ }
+ }
+ }
+
+ // 常规清理
+ foreach (var key in recentlyProcessed.Keys)
+ {
+ if (recentlyProcessed.TryGetValue(key, out var time) && time < cutoff)
+ {
+ DateTime dummy;
+ if (recentlyProcessed.TryRemove(key, out dummy))
+ {
+ removedCount++;
+ }
+ }
+ }
+
+ if (removedCount > 0)
+ {
+ Log.Debug("已清理 {Count} 个过期事件记录", removedCount);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "清理定时器错误: {Message}", ex.Message);
+ }
+ }, null, TimeSpan.FromSeconds(_config.Performance.CleanupIntervalSeconds),
+ TimeSpan.FromSeconds(_config.Performance.CleanupIntervalSeconds));
+
+ // 从通道读取事件并处理
+ await foreach (var e in reader.ReadAllAsync(cancellationToken))
+ {
+ // 再次确认文件扩展名,双重检查
+ if (!ShouldProcessFile(e.FullPath))
+ {
+ continue;
+ }
+
+ // 增加计数器
+ long count = Interlocked.Increment(ref _totalEventsProcessed);
+
+ // 周期性清理内存
+ if (count % _config.Performance.MemoryCleanupThreshold == 0)
+ {
+ // 在后台线程执行垃圾回收,但直接等待完成避免警告
+ await Task.Run(() =>
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ });
+ }
+
+ // 只使用文件路径作为键,忽略事件类型,这样同一文件的创建和修改事件会被视为同一事件
+ var key = e.FullPath;
+
+ // 对于创建事件,如果之前有相同文件的其他事件,则跳过
+ if (e.ChangeType == WatcherChangeTypes.Changed &&
+ recentlyProcessed.TryGetValue(key, out var _))
+ {
+ // 如果是修改事件且文件最近被处理过,则跳过
+ continue;
+ }
+
+ // 更新或添加文件处理时间,使用原子操作
+ recentlyProcessed[key] = DateTime.UtcNow;
+
+ string eventType = e.ChangeType switch
+ {
+ WatcherChangeTypes.Created => "创建",
+ WatcherChangeTypes.Deleted => "删除",
+ WatcherChangeTypes.Changed => "修改",
+ WatcherChangeTypes.Renamed => "重命名",
+ _ => "未知"
+ };
+
+ string fileName = e.Name ?? Path.GetFileName(e.FullPath);
+ string directoryName = Path.GetDirectoryName(e.FullPath) ?? string.Empty;
+
+ Log.Information("{EventType}: {FileName}", eventType, fileName);
+
+ if (_config.Logging.LogFileEventDetails)
+ {
+ Log.Debug(" 路径: {Path}", directoryName);
+
+ var extension = Path.GetExtension(e.FullPath);
+ Log.Debug(" 类型: {Extension}", extension);
+
+ if (e is RenamedEventArgs renamedEvent)
+ {
+ Log.Debug(" 原名称: {OldName}", Path.GetFileName(renamedEvent.OldFullPath));
+ }
+ }
+
+ // 延迟可配置
+ await Task.Delay(_config.Performance.ProcessingDelayMs, cancellationToken);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // 预期的取消异常,正常退出
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "处理事件时发生错误: {Message}", ex.Message);
+ }
+ finally
+ {
+ // 确保资源释放
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+ }
+
+ // 健康监控任务,监视文件监控器的状态并在需要时重启它
+ private static async Task StartWatcherHealthMonitorAsync(
+ string path, ChannelWriter writer, CancellationToken cancellationToken)
+ {
+ Log.Information("已启动监控器健康检查,间隔: {Seconds}秒", _config.Robustness.WatcherHealthCheckIntervalSeconds);
+
+ try
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ // 等待指定的健康检查间隔
+ await Task.Delay(TimeSpan.FromSeconds(_config.Robustness.WatcherHealthCheckIntervalSeconds),
+ cancellationToken);
+
+ // 如果监控器不健康且重启次数未超过上限,尝试重启
+ if (_activeWatcher != null &&
+ !FileWatcherUtils.IsWatcherHealthy(_activeWatcher, _lastEventTime, _config.Robustness) &&
+ _watcherRestartAttempts < _config.Robustness.MaxRestartAttempts)
+ {
+ // 停止当前的监控器
+ Log.Warning("检测到监控器异常,准备重启...");
+
+ try
+ {
+ _activeWatcher.EnableRaisingEvents = false;
+ _activeWatcher.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "停止异常监控器时出错: {Message}", ex.Message);
+ }
+
+ _watcherRestartAttempts++;
+ Log.Warning("正在重启监控器,尝试次数: {Current}/{Max}",
+ _watcherRestartAttempts, _config.Robustness.MaxRestartAttempts);
+
+ // 等待指定的重启延迟时间
+ await Task.Delay(TimeSpan.FromSeconds(_config.Robustness.RestartDelaySeconds),
+ cancellationToken);
+
+ // 创建新的监控器
+ try
+ {
+ _activeWatcher = CreateFileWatcher(path, writer, cancellationToken);
+ _lastEventTime = DateTime.UtcNow; // 重置最后事件时间
+ Log.Information("监控器已成功重启");
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "重启监控器失败: {Message}", ex.Message);
+ }
+ }
+ else if (_watcherRestartAttempts >= _config.Robustness.MaxRestartAttempts)
+ {
+ Log.Error("警告: 已达到最大重启次数({Max}),不再尝试重启", _config.Robustness.MaxRestartAttempts);
+ Log.Warning("请检查文件系统或手动重启程序");
+ break;
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // 预期的取消异常,正常退出
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "健康监控任务异常: {Message}", ex.Message);
+ }
+ }
+
+ ///
+ /// 运行数据库工具
+ ///
+ private static async Task RunDatabaseUtilityAsync(string[] args)
+ {
+ Console.WriteLine("正在启动SQLite数据库管理工具...");
+
+ try
+ {
+ var dbUtility = new DbUtility();
+ await dbUtility.ExecuteAsync(args);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"运行数据库工具时发生错误: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/external/JiShe.CollectBus.PluginFileWatcher/Properties/launchSettings.json b/external/JiShe.CollectBus.PluginFileWatcher/Properties/launchSettings.json
new file mode 100644
index 0000000..9293f4d
--- /dev/null
+++ b/external/JiShe.CollectBus.PluginFileWatcher/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "JiShe.CollectBus.PluginFileWatcher": {
+ "commandName": "Project"
+ },
+ "Container (Dockerfile)": {
+ "commandName": "Docker"
+ }
+ }
+}
\ No newline at end of file
diff --git a/external/JiShe.CollectBus.PluginFileWatcher/appsettings.json b/external/JiShe.CollectBus.PluginFileWatcher/appsettings.json
new file mode 100644
index 0000000..f0eedfa
--- /dev/null
+++ b/external/JiShe.CollectBus.PluginFileWatcher/appsettings.json
@@ -0,0 +1,88 @@
+{
+ "Serilog": {
+ "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "System": "Warning"
+ }
+ },
+ "WriteTo": [
+ {
+ "Name": "Console",
+ "Args": {
+ "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u3}] {Message:lj}{NewLine}{Exception}"
+ }
+ },
+ {
+ "Name": "File",
+ "Args": {
+ "path": "Logs/filemonitor-.log",
+ "rollingInterval": "Day",
+ "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u3}] {Message:lj}{NewLine}{Exception}",
+ "retainedFileCountLimit": 31
+ }
+ }
+ ],
+ "Enrich": [ "FromLogContext" ]
+ },
+ "FileMonitorConfig": {
+ "General": {
+ "EnableFileFiltering": true,
+ "MemoryMonitorIntervalMinutes": 1,
+ "DefaultMonitorPath": "D:\\MonitorFiles"
+ },
+ "FileFilters": {
+ "AllowedExtensions": [ ".dll" ],
+ "ExcludedDirectories": [ "bin", "obj", "node_modules" ],
+ "IncludeSubdirectories": true
+ },
+ "Performance": {
+ "MemoryCleanupThreshold": 5000,
+ "ChannelCapacity": 1000,
+ "EventDebounceTimeSeconds": 3,
+ "MaxDictionarySize": 10000,
+ "CleanupIntervalSeconds": 5,
+ "ProcessingDelayMs": 5
+ },
+ "Robustness": {
+ "EnableAutoRecovery": true,
+ "WatcherHealthCheckIntervalSeconds": 30,
+ "WatcherTimeoutSeconds": 60,
+ "MaxRestartAttempts": 3,
+ "RestartDelaySeconds": 5,
+ "EnableFileLockDetection": true,
+ "LockedFileStrategy": "Retry",
+ "FileLockRetryCount": 3,
+ "FileLockRetryDelayMs": 500
+ },
+ "EventStorage": {
+ "EnableEventStorage": true,
+ "StorageType": "SQLite",
+ "StorageDirectory": "D:/EventLogs",
+ "DatabasePath": "D:/EventLogs/events.db",
+ "ConnectionString": "Data Source=D:/EventLogs/events.db;Foreign Keys=True",
+ "CommandTimeout": 30,
+ "LogFileNameFormat": "FileEvents_{0:yyyy-MM-dd}.json",
+ "StorageIntervalSeconds": 60,
+ "BatchSize": 100,
+ "MaxEventRecords": 100000,
+ "DataRetentionDays": 30,
+ "MaxLogFiles": 30,
+ "CompressLogFiles": true,
+ "EnableEventReplay": true,
+ "ReplayIntervalMs": 100,
+ "ReplaySpeedFactor": 1.0
+ },
+ "NotifyFilters": [
+ "LastWrite",
+ "FileName",
+ "DirectoryName",
+ "CreationTime"
+ ],
+ "Logging": {
+ "LogLevel": "Information"
+ }
+ }
+}
diff --git a/modules/JiShe.CollectBus.Cassandra/CassandraQueryOptimizer.cs b/modules/JiShe.CollectBus.Cassandra/CassandraQueryOptimizer.cs
deleted file mode 100644
index 0ea1b56..0000000
--- a/modules/JiShe.CollectBus.Cassandra/CassandraQueryOptimizer.cs
+++ /dev/null
@@ -1,156 +0,0 @@
-using System.Collections.Concurrent;
-using Cassandra;
-using Cassandra.Mapping;
-using Microsoft.Extensions.Caching.Memory;
-using Microsoft.Extensions.Logging;
-
-namespace JiShe.CollectBus.Cassandra
-{
- public class CassandraQueryOptimizer
- {
- private readonly ISession _session;
- private readonly ILogger _logger;
- private readonly IMemoryCache _cache;
- private readonly ConcurrentDictionary _preparedStatements;
- private readonly int _batchSize;
- private readonly TimeSpan _cacheExpiration;
-
- public CassandraQueryOptimizer(
- ISession session,
- ILogger logger,
- IMemoryCache cache,
- int batchSize = 100,
- TimeSpan? cacheExpiration = null)
- {
- _session = session;
- _logger = logger;
- _cache = cache;
- _preparedStatements = new ConcurrentDictionary();
- _batchSize = batchSize;
- _cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(5);
- }
-
- public async Task GetOrPrepareStatementAsync(string cql)
- {
- return _preparedStatements.GetOrAdd(cql, key =>
- {
- try
- {
- var statement = _session.Prepare(key);
- _logger.LogDebug($"Prepared statement for CQL: {key}");
- return statement;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, $"Failed to prepare statement for CQL: {key}");
- throw;
- }
- });
- }
-
- public async Task ExecuteBatchAsync(IEnumerable statements)
- {
- var batch = new BatchStatement();
- var count = 0;
-
- foreach (var statement in statements)
- {
- batch.Add(statement);
- count++;
-
- if (count >= _batchSize)
- {
- await ExecuteBatchAsync(batch);
- batch = new BatchStatement();
- count = 0;
- }
- }
-
- if (count > 0)
- {
- await ExecuteBatchAsync(batch);
- }
- }
-
- private async Task ExecuteBatchAsync(BatchStatement batch)
- {
- try
- {
- await _session.ExecuteAsync(batch);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to execute batch statement");
- throw;
- }
- }
-
- public async Task GetOrSetFromCacheAsync(string cacheKey, Func> getData)
- {
- if (_cache.TryGetValue(cacheKey, out T cachedValue))
- {
- _logger.LogDebug($"Cache hit for key: {cacheKey}");
- return cachedValue;
- }
-
- var data = await getData();
- _cache.Set(cacheKey, data, _cacheExpiration);
- _logger.LogDebug($"Cache miss for key: {cacheKey}, data cached");
- return data;
- }
-
- public async Task> ExecutePagedQueryAsync(
- string cql,
- object[] parameters,
- int pageSize = 100,
- string pagingState = null) where T : class
- {
- var statement = await GetOrPrepareStatementAsync(cql);
- var boundStatement = statement.Bind(parameters);
-
- if (!string.IsNullOrEmpty(pagingState))
- {
- boundStatement.SetPagingState(Convert.FromBase64String(pagingState));
- }
-
- boundStatement.SetPageSize(pageSize);
-
- try
- {
- var result = await _session.ExecuteAsync(boundStatement);
- //TODO: RETURN OBJECT
- throw new NotImplementedException();
- //result.GetRows()
- //return result.Select(row => row);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, $"Failed to execute paged query: {cql}");
- throw;
- }
- }
-
- public async Task BulkInsertAsync(IEnumerable items, string tableName)
- {
- var mapper = new Mapper(_session);
- var batch = new List();
- var cql = $"INSERT INTO {tableName} ({{0}}) VALUES ({{1}})";
-
- foreach (var chunk in items.Chunk(_batchSize))
- {
- var statements = chunk.Select(item =>
- {
- var props = typeof(T).GetProperties();
- var columns = string.Join(", ", props.Select(p => p.Name));
- var values = string.Join(", ", props.Select(p => "?"));
- var statement = _session.Prepare(string.Format(cql, columns, values));
- return statement.Bind(props.Select(p => p.GetValue(item)).ToArray());
- });
-
- batch.AddRange(statements);
- }
-
- await ExecuteBatchAsync(batch);
- }
- }
-}
\ No newline at end of file
diff --git a/modules/JiShe.CollectBus.Cassandra/CassandraRepository.cs b/modules/JiShe.CollectBus.Cassandra/CassandraRepository.cs
index 25a51e3..1d6cc3c 100644
--- a/modules/JiShe.CollectBus.Cassandra/CassandraRepository.cs
+++ b/modules/JiShe.CollectBus.Cassandra/CassandraRepository.cs
@@ -1,19 +1,13 @@
-using Cassandra;
-using Cassandra.Data.Linq;
+using System.Linq.Expressions;
using Cassandra.Mapping;
using JiShe.CollectBus.Cassandra.Extensions;
-using JiShe.CollectBus.Common.Attributes;
-using Microsoft.AspNetCore.Http;
-using System.Reflection;
-using Thrift.Protocol.Entities;
using Volo.Abp.Domain.Entities;
-using Volo.Abp.Domain.Repositories;
namespace JiShe.CollectBus.Cassandra
{
public class CassandraRepository
: ICassandraRepository
- where TEntity : class
+ where TEntity : class, ICassandraEntity
{
private readonly ICassandraProvider _cassandraProvider;
public CassandraRepository(ICassandraProvider cassandraProvider, MappingConfiguration mappingConfig)
@@ -27,12 +21,29 @@ namespace JiShe.CollectBus.Cassandra
public virtual async Task GetAsync(TKey id)
{
- return await Mapper.SingleOrDefaultAsync("WHERE id = ?", id);
+ return await GetAsync("WHERE id = ?", id);
}
- public virtual async Task> GetListAsync()
+ public virtual async Task GetAsync(string cql, params object[] args)
{
- return (await Mapper.FetchAsync()).ToList();
+ return await Mapper.SingleAsync(cql, args);
+ }
+
+
+ public virtual async Task FirstOrDefaultAsync(TKey id)
+ {
+ return await FirstOrDefaultAsync("WHERE id = ?", id);
+ }
+
+ public virtual async Task FirstOrDefaultAsync(string cql, params object[] args)
+ {
+ return await Mapper.FirstOrDefaultAsync(cql, args);
+ }
+
+
+ public virtual async Task?> GetListAsync(string? cql = null, params object[] args)
+ {
+ return cql.IsNullOrWhiteSpace() ? (await Mapper.FetchAsync()).ToList() : (await Mapper.FetchAsync(cql, args)).ToList();
}
public virtual async Task InsertAsync(TEntity entity)
diff --git a/modules/JiShe.CollectBus.Cassandra/CollectBusCassandraModule.cs b/modules/JiShe.CollectBus.Cassandra/CollectBusCassandraModule.cs
index b5274f7..bf57bce 100644
--- a/modules/JiShe.CollectBus.Cassandra/CollectBusCassandraModule.cs
+++ b/modules/JiShe.CollectBus.Cassandra/CollectBusCassandraModule.cs
@@ -1,7 +1,4 @@
-using Cassandra;
-using Cassandra.Mapping;
-using JiShe.CollectBus.Cassandra.Mappers;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
using Volo.Abp;
using Volo.Abp.Autofac;
using Volo.Abp.Modularity;
diff --git a/modules/JiShe.CollectBus.Cassandra/Extensions/ServiceCollectionExtensions.cs b/modules/JiShe.CollectBus.Cassandra/Extensions/ServiceCollectionExtensions.cs
index fe69268..c9ec0de 100644
--- a/modules/JiShe.CollectBus.Cassandra/Extensions/ServiceCollectionExtensions.cs
+++ b/modules/JiShe.CollectBus.Cassandra/Extensions/ServiceCollectionExtensions.cs
@@ -1,10 +1,4 @@
-using Autofac.Core;
-using Cassandra;
-using Cassandra.Mapping;
-using JiShe.CollectBus.Cassandra;
-using JiShe.CollectBus.Cassandra.Mappers;
-using Microsoft.Extensions.Options;
-using System.Reflection;
+using JiShe.CollectBus.Cassandra;
using Volo.Abp;
using Volo.Abp.Modularity;
@@ -26,8 +20,6 @@ namespace Microsoft.Extensions.DependencyInjection
public static void AddCassandra(this ServiceConfigurationContext context)
{
context.Services.AddTransient(typeof(ICassandraRepository<,>), typeof(CassandraRepository<,>));
- context.Services.AddSingleton(new MappingConfiguration()
- .Define(new CollectBusMapping()));
}
}
}
diff --git a/modules/JiShe.CollectBus.Cassandra/Extensions/SessionExtension.cs b/modules/JiShe.CollectBus.Cassandra/Extensions/SessionExtension.cs
index c313d0c..dd0ff66 100644
--- a/modules/JiShe.CollectBus.Cassandra/Extensions/SessionExtension.cs
+++ b/modules/JiShe.CollectBus.Cassandra/Extensions/SessionExtension.cs
@@ -3,9 +3,7 @@ using System.Text;
using Cassandra;
using System.ComponentModel.DataAnnotations;
using JiShe.CollectBus.Common.Attributes;
-using Cassandra.Mapping;
-using Cassandra.Data.Linq;
-using Thrift.Protocol.Entities;
+using Volo.Abp.Data;
namespace JiShe.CollectBus.Cassandra.Extensions
{
@@ -16,17 +14,26 @@ namespace JiShe.CollectBus.Cassandra.Extensions
var type = typeof(TEntity);
var tableAttribute = type.GetCustomAttribute();
var tableName = tableAttribute?.Name ?? type.Name.ToLower();
- //var tableKeyspace = tableAttribute?.Keyspace ?? defaultKeyspace;
var tableKeyspace = session.Keyspace;
var properties = type.GetProperties();
- var primaryKey = properties.FirstOrDefault(p => p.GetCustomAttribute() != null);
+
+ // 分区键设置
+ var primaryKey = properties.FirstOrDefault(p => p.GetCustomAttribute() != null);
if (primaryKey == null)
{
throw new InvalidOperationException($"No primary key defined for type {type.Name}");
}
+ // 集群键设置
+ var clusteringKeys = properties.Where(p => p.GetCustomAttribute() != null).Select(a=>a.Name).ToList();
+ var clusteringKeyCql = string.Empty;
+ if (clusteringKeys.Any())
+ {
+ clusteringKeyCql = $", {string.Join(", ", clusteringKeys)}";
+ }
+
var cql = new StringBuilder();
cql.Append($"CREATE TABLE IF NOT EXISTS {tableKeyspace}.{tableName} (");
@@ -40,7 +47,7 @@ namespace JiShe.CollectBus.Cassandra.Extensions
cql.Append($"{columnName} {cqlType}, ");
}
cql.Length -= 2; // Remove last comma and space
- cql.Append($", PRIMARY KEY ({primaryKey.Name.ToLower()}))");
+ cql.Append($", PRIMARY KEY (({primaryKey.Name.ToLower()}){clusteringKeyCql}))");
session.Execute(cql.ToString());
}
@@ -61,6 +68,7 @@ namespace JiShe.CollectBus.Cassandra.Extensions
if (type == typeof(Guid)) return "uuid";
if (type == typeof(DateTimeOffset)) return "timestamp";
if (type == typeof(Byte[])) return "blob";
+ if (type == typeof(ExtraPropertyDictionary)) return "map";
// 处理集合类型
if (type.IsGenericType)
@@ -72,6 +80,8 @@ namespace JiShe.CollectBus.Cassandra.Extensions
return $"list<{GetCassandraType(elementType)}>";
if (genericType == typeof(HashSet<>))
return $"set<{GetCassandraType(elementType)}>";
+ if (genericType == typeof(Nullable<>))
+ return GetCassandraType(elementType);
if (genericType == typeof(Dictionary<,>))
{
var keyType = type.GetGenericArguments()[0];
diff --git a/modules/JiShe.CollectBus.Cassandra/ICassandraRepository.cs b/modules/JiShe.CollectBus.Cassandra/ICassandraRepository.cs
index 6dd474f..5f3b862 100644
--- a/modules/JiShe.CollectBus.Cassandra/ICassandraRepository.cs
+++ b/modules/JiShe.CollectBus.Cassandra/ICassandraRepository.cs
@@ -10,7 +10,10 @@ namespace JiShe.CollectBus.Cassandra
public interface ICassandraRepository where TEntity : class
{
Task GetAsync(TKey id);
- Task> GetListAsync();
+ Task GetAsync(string cql, params object[] args);
+ Task FirstOrDefaultAsync(TKey id);
+ Task FirstOrDefaultAsync(string cql, params object[] args);
+ Task?> GetListAsync(string? cql = null, params object[] args);
Task InsertAsync(TEntity entity);
Task UpdateAsync(TEntity entity);
Task DeleteAsync(TEntity entity);
diff --git a/modules/JiShe.CollectBus.Cassandra/JiShe.CollectBus.Cassandra.csproj b/modules/JiShe.CollectBus.Cassandra/JiShe.CollectBus.Cassandra.csproj
index edc303c..a3360e3 100644
--- a/modules/JiShe.CollectBus.Cassandra/JiShe.CollectBus.Cassandra.csproj
+++ b/modules/JiShe.CollectBus.Cassandra/JiShe.CollectBus.Cassandra.csproj
@@ -15,8 +15,8 @@
-
+
diff --git a/modules/JiShe.CollectBus.IoTDB/Attribute/EntityTypeAttribute.cs b/modules/JiShe.CollectBus.IoTDB/Attribute/EntityTypeAttribute.cs
new file mode 100644
index 0000000..3610c00
--- /dev/null
+++ b/modules/JiShe.CollectBus.IoTDB/Attribute/EntityTypeAttribute.cs
@@ -0,0 +1,19 @@
+using JiShe.CollectBus.IoTDB.Enums;
+
+namespace JiShe.CollectBus.IoTDB.Attribute
+{
+ ///
+ /// IoTDB实体类型特性
+ ///
+ [AttributeUsage(AttributeTargets.Class)]
+ public class EntityTypeAttribute : System.Attribute
+ {
+ public EntityTypeEnum EntityType { get; }
+
+
+ public EntityTypeAttribute(EntityTypeEnum entityType)
+ {
+ EntityType = entityType;
+ }
+ }
+}
diff --git a/modules/JiShe.CollectBus.IoTDB/Attribute/SingleMeasuringAttribute.cs b/modules/JiShe.CollectBus.IoTDB/Attribute/SingleMeasuringAttribute.cs
index cebb85a..5f0ca07 100644
--- a/modules/JiShe.CollectBus.IoTDB/Attribute/SingleMeasuringAttribute.cs
+++ b/modules/JiShe.CollectBus.IoTDB/Attribute/SingleMeasuringAttribute.cs
@@ -1,7 +1,7 @@
namespace JiShe.CollectBus.IoTDB.Attribute
{
///
- /// 用于标识当前实体为单侧点模式,单侧点模式只有一个Filed标识字段,类型是Tuple,Item1=>测点名称,Item2=>测点值,泛型
+ /// 用于标识当前实体为单侧点模式,单侧点模式只有一个Filed标识字段,类型是Tuple,Item1=>测点名称,Item2=>测点值,泛型
///
[AttributeUsage(AttributeTargets.Property)]
public class SingleMeasuringAttribute : System.Attribute
diff --git a/modules/JiShe.CollectBus.IoTDB/Attribute/TableNameOrTreePathAttribute.cs b/modules/JiShe.CollectBus.IoTDB/Attribute/TableNameOrTreePathAttribute.cs
new file mode 100644
index 0000000..1b4f4f0
--- /dev/null
+++ b/modules/JiShe.CollectBus.IoTDB/Attribute/TableNameOrTreePathAttribute.cs
@@ -0,0 +1,18 @@
+using JiShe.CollectBus.IoTDB.Enums;
+
+namespace JiShe.CollectBus.IoTDB.Attribute
+{
+ ///
+ /// IoTDB实体存储路径或表名称,一般用于已经明确的存储路径或表名称,例如日志存储
+ ///
+ [AttributeUsage(AttributeTargets.Class)]
+ public class TableNameOrTreePathAttribute : System.Attribute
+ {
+ public string TableNameOrTreePath { get; }
+
+ public TableNameOrTreePathAttribute(string tableNameOrTreePath)
+ {
+ TableNameOrTreePath = tableNameOrTreePath;
+ }
+ }
+}
diff --git a/modules/JiShe.CollectBus.IoTDB/CollectBusIoTDBModule.cs b/modules/JiShe.CollectBus.IoTDB/CollectBusIoTDBModule.cs
index 93bbbfe..6d26bdc 100644
--- a/modules/JiShe.CollectBus.IoTDB/CollectBusIoTDBModule.cs
+++ b/modules/JiShe.CollectBus.IoTDB/CollectBusIoTDBModule.cs
@@ -1,33 +1,22 @@
using JiShe.CollectBus.IoTDB.Context;
-using JiShe.CollectBus.IoTDB.Interface;
using JiShe.CollectBus.IoTDB.Options;
-using JiShe.CollectBus.IoTDB.Provider;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
-namespace JiShe.CollectBus.IoTDB
+namespace JiShe.CollectBus.IoTDB;
+
+///
+/// CollectBusIoTDBModule
+///
+public class CollectBusIoTDbModule : AbpModule
{
- public class CollectBusIoTDBModule : AbpModule
+ public override void ConfigureServices(ServiceConfigurationContext context)
{
- public override void ConfigureServices(ServiceConfigurationContext context)
- {
-
- var configuration = context.Services.GetConfiguration();
- Configure(options =>
- {
- configuration.GetSection(nameof(IoTDBOptions)).Bind(options);
- });
+ var configuration = context.Services.GetConfiguration();
+ Configure(options => { configuration.GetSection(nameof(IoTDbOptions)).Bind(options); });
- // 注册上下文为Scoped
- context.Services.AddScoped();
-
- // 注册Session工厂
- context.Services.AddSingleton();
-
- // 注册Provider
- context.Services.AddScoped();
-
- }
+ //// 注册上下文为Scoped
+ //context.Services.AddScoped();
}
-}
+}
\ No newline at end of file
diff --git a/modules/JiShe.CollectBus.IoTDB/Context/IoTDBRuntimeContext.cs b/modules/JiShe.CollectBus.IoTDB/Context/IoTDBRuntimeContext.cs
index cd99b00..ef68325 100644
--- a/modules/JiShe.CollectBus.IoTDB/Context/IoTDBRuntimeContext.cs
+++ b/modules/JiShe.CollectBus.IoTDB/Context/IoTDBRuntimeContext.cs
@@ -1,23 +1,24 @@
using JiShe.CollectBus.IoTDB.Options;
using Microsoft.Extensions.Options;
+using Volo.Abp.DependencyInjection;
namespace JiShe.CollectBus.IoTDB.Context
{
///
/// IoTDB SessionPool 运行时上下文
///
- public class IoTDBRuntimeContext
+ public class IoTDBRuntimeContext: IScopedDependency
{
private readonly bool _defaultValue;
- public IoTDBRuntimeContext(IOptions options)
+ public IoTDBRuntimeContext(IOptions options)
{
_defaultValue = options.Value.UseTableSessionPoolByDefault;
UseTableSessionPool = _defaultValue;
}
///
- /// 是否使用表模型存储, 默认false,使用tree模型存储
+ /// 存储模型切换标识,是否使用table模型存储, 默认为false,标识tree模型存储
///
public bool UseTableSessionPool { get; set; }
diff --git a/modules/JiShe.CollectBus.IoTDB/EnumInfo/EntityTypeEnum.cs b/modules/JiShe.CollectBus.IoTDB/EnumInfo/EntityTypeEnum.cs
new file mode 100644
index 0000000..26c6645
--- /dev/null
+++ b/modules/JiShe.CollectBus.IoTDB/EnumInfo/EntityTypeEnum.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace JiShe.CollectBus.IoTDB.Enums
+{
+ ///
+ /// IoTDB实体类型枚举
+ ///
+ public enum EntityTypeEnum
+ {
+ ///
+ /// 树模型
+ ///
+ TreeModel = 1,
+
+ ///
+ /// 表模型
+ ///
+ TableModel = 2,
+ }
+}
diff --git a/modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBProvider.cs b/modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBProvider.cs
index bb47841..82a0d47 100644
--- a/modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBProvider.cs
+++ b/modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBProvider.cs
@@ -1,4 +1,5 @@
using JiShe.CollectBus.Common.Models;
+using JiShe.CollectBus.IoTDB.Model;
using JiShe.CollectBus.IoTDB.Options;
using JiShe.CollectBus.IoTDB.Provider;
@@ -7,7 +8,7 @@ namespace JiShe.CollectBus.IoTDB.Interface
///
/// IoTDB数据源,数据库能同时存多个时序模型,但数据是完全隔离的,不能跨时序模型查询,通过连接字符串配置
///
- public interface IIoTDBProvider
+ public interface IIoTDbProvider
{
/////
///// 切换 SessionPool
@@ -31,6 +32,15 @@ namespace JiShe.CollectBus.IoTDB.Interface
///
Task BatchInsertAsync(IEnumerable entities) where T : IoTEntity;
+ ///
+ /// 批量插入数据
+ ///
+ ///
+ /// 设备元数据
+ ///
+ ///
+ Task BatchInsertAsync(DeviceMetadata deviceMetadata,IEnumerable entities) where T : IoTEntity;
+
///
/// 删除数据
@@ -38,7 +48,14 @@ namespace JiShe.CollectBus.IoTDB.Interface
///
///
///
- Task