dev #3

Merged
admin merged 101 commits from dev into master 2025-04-27 03:42:51 +00:00
336 changed files with 22649 additions and 4229 deletions

30
.dockerignore Normal file
View File

@ -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/**

View File

@ -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

75
Dockerfile Normal file
View File

@ -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"]

View File

@ -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

View File

@ -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}

7
NuGet.Config Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>

77
PackageAndPublish.bat Normal file
View File

@ -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

View File

@ -0,0 +1,287 @@
using System;
using System.Collections.Generic;
namespace JiShe.CollectBus.PluginFileWatcher
{
/// <summary>
/// 文件监控程序的配置类
/// </summary>
public class FileMonitorConfig
{
/// <summary>
/// 基本配置
/// </summary>
public GeneralConfig General { get; set; } = new GeneralConfig();
/// <summary>
/// 文件过滤配置
/// </summary>
public FileFiltersConfig FileFilters { get; set; } = new FileFiltersConfig();
/// <summary>
/// 性能相关配置
/// </summary>
public PerformanceConfig Performance { get; set; } = new PerformanceConfig();
/// <summary>
/// 健壮性相关配置
/// </summary>
public RobustnessConfig Robustness { get; set; } = new RobustnessConfig();
/// <summary>
/// 事件存储和回放配置
/// </summary>
public EventStorageConfig EventStorage { get; set; } = new EventStorageConfig();
/// <summary>
/// 文件系统通知过滤器配置
/// </summary>
public List<string> NotifyFilters { get; set; } = new List<string>();
/// <summary>
/// 日志配置
/// </summary>
public LoggingConfig Logging { get; set; } = new LoggingConfig();
}
/// <summary>
/// 常规配置
/// </summary>
public class GeneralConfig
{
/// <summary>
/// 是否启用文件过滤
/// </summary>
public bool EnableFileFiltering { get; set; } = true;
/// <summary>
/// 内存监控间隔(分钟)
/// </summary>
public int MemoryMonitorIntervalMinutes { get; set; } = 1;
/// <summary>
/// 默认监控路径
/// </summary>
public string DefaultMonitorPath { get; set; } = string.Empty;
}
/// <summary>
/// 文件过滤配置
/// </summary>
public class FileFiltersConfig
{
/// <summary>
/// 允许监控的文件扩展名
/// </summary>
public string[] AllowedExtensions { get; set; } = new[] { ".dll" };
/// <summary>
/// 排除的目录
/// </summary>
public string[] ExcludedDirectories { get; set; } = new[] { "bin", "obj", "node_modules" };
/// <summary>
/// 是否包含子目录
/// </summary>
public bool IncludeSubdirectories { get; set; } = true;
}
/// <summary>
/// 性能相关配置
/// </summary>
public class PerformanceConfig
{
/// <summary>
/// 内存清理阈值(事件数)
/// </summary>
public int MemoryCleanupThreshold { get; set; } = 5000;
/// <summary>
/// 通道容量
/// </summary>
public int ChannelCapacity { get; set; } = 1000;
/// <summary>
/// 事件去抖时间(秒)
/// </summary>
public int EventDebounceTimeSeconds { get; set; } = 3;
/// <summary>
/// 最大字典大小
/// </summary>
public int MaxDictionarySize { get; set; } = 10000;
/// <summary>
/// 清理间隔(秒)
/// </summary>
public int CleanupIntervalSeconds { get; set; } = 5;
/// <summary>
/// 处理延迟(毫秒)
/// </summary>
public int ProcessingDelayMs { get; set; } = 5;
}
/// <summary>
/// 健壮性相关配置
/// </summary>
public class RobustnessConfig
{
/// <summary>
/// 是否启用自动恢复机制
/// </summary>
public bool EnableAutoRecovery { get; set; } = true;
/// <summary>
/// 监控器健康检查间隔(秒)
/// </summary>
public int WatcherHealthCheckIntervalSeconds { get; set; } = 30;
/// <summary>
/// 监控器无响应超时时间(秒)
/// </summary>
public int WatcherTimeoutSeconds { get; set; } = 60;
/// <summary>
/// 监控器重启尝试最大次数
/// </summary>
public int MaxRestartAttempts { get; set; } = 3;
/// <summary>
/// 重启尝试之间的延迟(秒)
/// </summary>
public int RestartDelaySeconds { get; set; } = 5;
/// <summary>
/// 是否启用文件锁检测
/// </summary>
public bool EnableFileLockDetection { get; set; } = true;
/// <summary>
/// 对锁定文件的处理策略: Skip(跳过), Retry(重试), Log(仅记录)
/// </summary>
public string LockedFileStrategy { get; set; } = "Retry";
/// <summary>
/// 文件锁定重试次数
/// </summary>
public int FileLockRetryCount { get; set; } = 3;
/// <summary>
/// 文件锁定重试间隔(毫秒)
/// </summary>
public int FileLockRetryDelayMs { get; set; } = 500;
}
/// <summary>
/// 事件存储和回放配置
/// </summary>
public class EventStorageConfig
{
/// <summary>
/// 是否启用事件存储
/// </summary>
public bool EnableEventStorage { get; set; } = true;
/// <summary>
/// 存储类型SQLite 或 File
/// </summary>
public string StorageType { get; set; } = "SQLite";
/// <summary>
/// 事件存储目录
/// </summary>
public string StorageDirectory { get; set; } = "D:/EventLogs";
/// <summary>
/// SQLite数据库文件路径
/// </summary>
public string DatabasePath { get; set; } = "D:/EventLogs/events.db";
/// <summary>
/// SQLite连接字符串
/// </summary>
public string ConnectionString { get; set; } = "Data Source=D:/EventLogs/events.db";
/// <summary>
/// 数据库命令超时(秒)
/// </summary>
public int CommandTimeout { get; set; } = 30;
/// <summary>
/// 事件日志文件名格式 (使用DateTime.ToString格式)
/// </summary>
public string LogFileNameFormat { get; set; } = "FileEvents_{0:yyyy-MM-dd}.json";
/// <summary>
/// 存储间隔(秒),多久将缓存的事件写入一次存储
/// </summary>
public int StorageIntervalSeconds { get; set; } = 60;
/// <summary>
/// 事件批量写入大小,达到此数量时立即写入存储
/// </summary>
public int BatchSize { get; set; } = 100;
/// <summary>
/// 最大保留事件记录条数
/// </summary>
public int MaxEventRecords { get; set; } = 100000;
/// <summary>
/// 数据保留天数
/// </summary>
public int DataRetentionDays { get; set; } = 30;
/// <summary>
/// 最大保留日志文件数
/// </summary>
public int MaxLogFiles { get; set; } = 30;
/// <summary>
/// 是否压缩存储的事件日志
/// </summary>
public bool CompressLogFiles { get; set; } = true;
/// <summary>
/// 是否可以回放事件
/// </summary>
public bool EnableEventReplay { get; set; } = true;
/// <summary>
/// 回放时间间隔(毫秒)
/// </summary>
public int ReplayIntervalMs { get; set; } = 100;
/// <summary>
/// 回放速度倍率大于1加速小于1减速
/// </summary>
public double ReplaySpeedFactor { get; set; } = 1.0;
}
/// <summary>
/// 日志相关配置
/// </summary>
public class LoggingConfig
{
/// <summary>
/// 日志级别Verbose、Debug、Information、Warning、Error、Fatal
/// </summary>
public string LogLevel { get; set; } = "Information";
/// <summary>
/// 是否记录文件事件处理详情
/// </summary>
public bool LogFileEventDetails { get; set; } = false;
/// <summary>
/// 日志文件保留天数
/// </summary>
public int RetainedLogDays { get; set; } = 30;
/// <summary>
/// 日志文件目录
/// </summary>
public string LogDirectory { get; set; } = "Logs";
}
}

View File

@ -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
{
/// <summary>
/// 数据库操作工具类,用于命令行测试数据库功能
/// </summary>
public class DbUtility
{
private readonly EventDatabaseManager _dbManager;
private readonly ILogger _logger;
private readonly FileMonitorConfig _config;
/// <summary>
/// 初始化数据库工具类
/// </summary>
/// <param name="configPath">配置文件路径</param>
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);
}
/// <summary>
/// 执行数据库维护操作
/// </summary>
/// <param name="args">命令行参数</param>
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;
}
}
/// <summary>
/// 显示帮助信息
/// </summary>
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条");
}
/// <summary>
/// 显示数据库统计信息
/// </summary>
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, "获取数据库统计信息时发生错误");
}
}
/// <summary>
/// 查询事件
/// </summary>
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<FileEventType>(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, "查询事件时发生错误");
}
}
/// <summary>
/// 生成测试数据
/// </summary>
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<FileEvent>();
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, "生成测试数据时发生错误");
}
}
}
}

View File

@ -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"]

View File

@ -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
{
/// <summary>
/// SQLite数据库管理器用于管理文件事件的存储和检索
/// </summary>
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;
/// <summary>
/// 初始化数据库管理器
/// </summary>
/// <param name="config">配置对象</param>
/// <param name="logger">日志记录器</param>
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();
}
/// <summary>
/// 初始化数据库,确保必要的表已创建
/// </summary>
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;
}
}
/// <summary>
/// 保存文件事件到数据库
/// </summary>
/// <param name="events">要保存的事件列表</param>
public async Task SaveEventsAsync(List<FileEvent> 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;
}
}
/// <summary>
/// 查询事件
/// </summary>
/// <param name="queryParams">查询参数</param>
/// <returns>查询结果</returns>
public async Task<EventQueryResult> 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<string>();
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<int>(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<dynamic>(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<dynamic>(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;
}
}
/// <summary>
/// 清理旧数据
/// </summary>
/// <param name="retentionDays">数据保留天数</param>
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;
}
}
/// <summary>
/// 获取数据库统计信息
/// </summary>
/// <returns>数据库统计信息</returns>
public async Task<DatabaseStats> 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<int>("SELECT COUNT(*) FROM FileEvents", commandTimeout: _commandTimeout);
// 获取最早和最新事件时间
stats.OldestEventTime = await connection.ExecuteScalarAsync<DateTime?>("SELECT Timestamp FROM FileEvents ORDER BY Timestamp ASC LIMIT 1", commandTimeout: _commandTimeout);
stats.NewestEventTime = await connection.ExecuteScalarAsync<DateTime?>("SELECT Timestamp FROM FileEvents ORDER BY Timestamp DESC LIMIT 1", commandTimeout: _commandTimeout);
// 获取事件类型分布
var eventTypeCounts = await connection.QueryAsync<dynamic>("SELECT EventType, COUNT(*) AS Count FROM FileEvents GROUP BY EventType", commandTimeout: _commandTimeout);
stats.EventTypeCounts = new Dictionary<FileEventType, int>();
foreach (var item in eventTypeCounts)
{
stats.EventTypeCounts[(FileEventType)item.EventType] = item.Count;
}
// 获取扩展名分布前10个
var extensionCounts = await connection.QueryAsync<dynamic>(
"SELECT Extension, COUNT(*) AS Count FROM FileEvents GROUP BY Extension ORDER BY Count DESC LIMIT 10",
commandTimeout: _commandTimeout);
stats.TopExtensions = new Dictionary<string, int>();
foreach (var item in extensionCounts)
{
stats.TopExtensions[item.Extension] = item.Count;
}
return stats;
}
catch (Exception ex)
{
_logger.LogError(ex, "获取数据库统计信息时发生错误");
throw;
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
}
}
/// <summary>
/// 数据库统计信息
/// </summary>
public class DatabaseStats
{
/// <summary>
/// 事件总数
/// </summary>
public int TotalEvents { get; set; }
/// <summary>
/// 最早事件时间
/// </summary>
public DateTime? OldestEventTime { get; set; }
/// <summary>
/// 最新事件时间
/// </summary>
public DateTime? NewestEventTime { get; set; }
/// <summary>
/// 事件类型计数
/// </summary>
public Dictionary<FileEventType, int> EventTypeCounts { get; set; }
/// <summary>
/// 排名前列的文件扩展名
/// </summary>
public Dictionary<string, int> TopExtensions { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// 负责文件事件的存储、查询和回放
/// </summary>
public class EventStorage : IDisposable
{
private readonly FileMonitorConfig _config;
private readonly ILogger _logger;
private readonly ConcurrentQueue<FileEvent> _eventQueue;
private readonly Timer _storageTimer;
private readonly SemaphoreSlim _storageLock = new SemaphoreSlim(1, 1);
private readonly string _storageDirectory;
private readonly EventDatabaseManager _dbManager;
private bool _disposed;
/// <summary>
/// 创建新的事件存储管理器实例
/// </summary>
/// <param name="config">文件监控配置</param>
/// <param name="logger">日志记录器</param>
public EventStorage(FileMonitorConfig config, ILogger logger)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_eventQueue = new ConcurrentQueue<FileEvent>();
// 确保存储目录存在
_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}秒");
}
}
/// <summary>
/// 记录一个文件事件
/// </summary>
/// <param name="fileEvent">文件事件</param>
public void RecordEvent(FileEvent fileEvent)
{
if (fileEvent == null || !_config.EventStorage.EnableEventStorage) return;
_eventQueue.Enqueue(fileEvent);
_logger.LogDebug($"文件事件已加入队列:{fileEvent.EventType} - {fileEvent.FullPath}");
}
/// <summary>
/// 从FileSystemEventArgs记录事件
/// </summary>
/// <param name="e">文件系统事件参数</param>
public void RecordEvent(FileSystemEventArgs e)
{
if (e == null || !_config.EventStorage.EnableEventStorage) return;
var fileEvent = FileEvent.FromFileSystemEventArgs(e);
RecordEvent(fileEvent);
}
/// <summary>
/// 定时将事件保存到文件
/// </summary>
private async void SaveEventsTimerCallback(object state)
{
if (_disposed || _eventQueue.IsEmpty) return;
try
{
// 防止多个定时器回调同时执行
if (!await _storageLock.WaitAsync(0))
{
return;
}
try
{
// 从队列中取出事件
List<FileEvent> eventsToSave = new List<FileEvent>();
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, "保存事件时发生错误");
}
}
/// <summary>
/// 将事件保存到文件
/// </summary>
/// <param name="events">要保存的事件列表</param>
private async Task SaveEventsToFileAsync(List<FileEvent> 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;
}
}
/// <summary>
/// 压缩并保存字符串到文件
/// </summary>
/// <param name="content">要保存的内容</param>
/// <param name="filePath">文件路径</param>
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);
}
/// <summary>
/// 清理过多的日志文件
/// </summary>
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;
}
/// <summary>
/// 查询历史事件
/// </summary>
/// <param name="queryParams">查询参数</param>
/// <returns>查询结果</returns>
public async Task<EventQueryResult> 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<FileEvent> allEvents = new List<FileEvent>();
// 加载所有日志文件中的事件
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<FileEvent> 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;
}
}
/// <summary>
/// 从文件加载事件
/// </summary>
/// <param name="filePath">文件路径</param>
/// <returns>事件列表</returns>
private async Task<List<FileEvent>> LoadEventsFromFileAsync(string filePath)
{
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
{
return new List<FileEvent>();
}
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<EventLogFile>(jsonContent);
return logFile?.Events ?? new List<FileEvent>();
}
catch (Exception ex)
{
_logger.LogError(ex, $"从文件加载事件失败:{filePath}");
return new List<FileEvent>();
}
}
/// <summary>
/// 启动事件回放会话
/// </summary>
/// <param name="queryParams">查询参数,定义要回放的事件</param>
/// <param name="replayHandler">回放处理回调</param>
/// <param name="cancellationToken">取消标记</param>
/// <returns>回放会话控制器</returns>
public async Task<EventReplaySession> StartReplayAsync(
EventQueryParams queryParams,
Func<FileEvent, Task> 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;
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_storageTimer?.Dispose();
_storageLock?.Dispose();
_dbManager?.Dispose();
// 尝试保存剩余事件
if (_config.EventStorage.EnableEventStorage && !_eventQueue.IsEmpty)
{
var remainingEvents = new List<FileEvent>();
while (!_eventQueue.IsEmpty && _eventQueue.TryDequeue(out var evt))
{
remainingEvents.Add(evt);
}
if (remainingEvents.Count > 0)
{
SaveEventsToFileAsync(remainingEvents).GetAwaiter().GetResult();
}
}
GC.SuppressFinalize(this);
}
}
/// <summary>
/// 事件回放会话
/// </summary>
public class EventReplaySession : IDisposable
{
private readonly List<FileEvent> _events;
private readonly Func<FileEvent, Task> _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);
/// <summary>
/// 回放进度0-100
/// </summary>
public int Progress { get; private set; }
/// <summary>
/// 当前回放的事件索引
/// </summary>
public int CurrentIndex { get; private set; }
/// <summary>
/// 事件总数
/// </summary>
public int TotalEvents => _events?.Count ?? 0;
/// <summary>
/// 回放是否已完成
/// </summary>
public bool IsCompleted { get; private set; }
/// <summary>
/// 回放是否已暂停
/// </summary>
public bool IsPaused => _isPaused;
/// <summary>
/// 回放已处理的事件数
/// </summary>
public int ProcessedEvents { get; private set; }
/// <summary>
/// 回放开始时间
/// </summary>
public DateTime StartTime { get; private set; }
/// <summary>
/// 回放结束时间(如果已完成)
/// </summary>
public DateTime? EndTime { get; private set; }
/// <summary>
/// 创建新的事件回放会话
/// </summary>
/// <param name="events">要回放的事件</param>
/// <param name="replayHandler">回放处理回调</param>
/// <param name="replayIntervalMs">回放间隔(毫秒)</param>
/// <param name="speedFactor">速度因子</param>
/// <param name="logger">日志记录器</param>
/// <param name="cancellationToken">取消标记</param>
public EventReplaySession(
List<FileEvent> events,
Func<FileEvent, Task> 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);
}
/// <summary>
/// 启动回放
/// </summary>
public async Task StartAsync()
{
if (_replayTask != null) return;
StartTime = DateTime.Now;
_replayTask = Task.Run(ReplayEventsAsync, _linkedCts.Token);
await Task.CompletedTask;
}
/// <summary>
/// 暂停回放
/// </summary>
public async Task PauseAsync()
{
if (_isPaused || IsCompleted) return;
await _pauseSemaphore.WaitAsync();
try
{
_isPaused = true;
}
finally
{
_pauseSemaphore.Release();
}
_logger.LogInformation("事件回放已暂停");
}
/// <summary>
/// 恢复回放
/// </summary>
public async Task ResumeAsync()
{
if (!_isPaused || IsCompleted) return;
await _pauseSemaphore.WaitAsync();
try
{
_isPaused = false;
// 释放信号量以允许回放任务继续
_pauseSemaphore.Release();
}
catch
{
_pauseSemaphore.Release();
throw;
}
_logger.LogInformation("事件回放已恢复");
}
/// <summary>
/// 停止回放
/// </summary>
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, "停止事件回放时发生错误");
}
}
/// <summary>
/// 回放事件处理
/// </summary>
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;
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_linkedCts?.Cancel();
_linkedCts?.Dispose();
_pauseSemaphore?.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@ -0,0 +1,254 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json.Serialization;
namespace JiShe.CollectBus.PluginFileWatcher
{
/// <summary>
/// 表示一个文件系统事件的数据模型,用于序列化和存储
/// </summary>
public class FileEvent
{
/// <summary>
/// 事件唯一标识
/// </summary>
public Guid Id { get; set; } = Guid.NewGuid();
/// <summary>
/// 事件发生时间
/// </summary>
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
/// <summary>
/// 事件类型
/// </summary>
public FileEventType EventType { get; set; }
/// <summary>
/// 文件完整路径
/// </summary>
public string FullPath { get; set; }
/// <summary>
/// 文件名
/// </summary>
public string FileName { get; set; }
/// <summary>
/// 文件所在目录
/// </summary>
public string Directory { get; set; }
/// <summary>
/// 文件扩展名
/// </summary>
public string Extension { get; set; }
/// <summary>
/// 重命名前的旧文件名(仅在重命名事件中有效)
/// </summary>
public string OldFileName { get; set; }
/// <summary>
/// 重命名前的旧路径(仅在重命名事件中有效)
/// </summary>
public string OldFullPath { get; set; }
/// <summary>
/// 文件大小(字节),如果可获取
/// </summary>
public long? FileSize { get; set; }
/// <summary>
/// 自定义属性,可用于存储其他元数据
/// </summary>
public Dictionary<string, string> Metadata { get; set; } = new Dictionary<string, string>();
/// <summary>
/// 从FileSystemEventArgs创建FileEvent
/// </summary>
/// <param name="e">FileSystemEventArgs参数</param>
/// <returns>FileEvent对象</returns>
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;
}
/// <summary>
/// 将WatcherChangeTypes转换为FileEventType
/// </summary>
/// <param name="changeType">WatcherChangeTypes枚举值</param>
/// <returns>对应的FileEventType</returns>
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
};
}
}
/// <summary>
/// 文件事件类型
/// </summary>
public enum FileEventType
{
/// <summary>
/// 文件被创建
/// </summary>
Created,
/// <summary>
/// 文件被修改
/// </summary>
Modified,
/// <summary>
/// 文件被删除
/// </summary>
Deleted,
/// <summary>
/// 文件被重命名
/// </summary>
Renamed,
/// <summary>
/// 其他类型事件
/// </summary>
Other
}
/// <summary>
/// 表示一个事件日志文件
/// </summary>
public class EventLogFile
{
/// <summary>
/// 日志文件创建时间
/// </summary>
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// 日志文件包含的事件列表
/// </summary>
public List<FileEvent> Events { get; set; } = new List<FileEvent>();
}
/// <summary>
/// 事件查询结果
/// </summary>
public class EventQueryResult
{
/// <summary>
/// 查询到的事件列表
/// </summary>
public List<FileEvent> Events { get; set; } = new List<FileEvent>();
/// <summary>
/// 匹配的事件总数
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// 查询是否有更多结果
/// </summary>
public bool HasMore { get; set; }
/// <summary>
/// 查询时间范围的开始时间
/// </summary>
public DateTime StartTime { get; set; }
/// <summary>
/// 查询时间范围的结束时间
/// </summary>
public DateTime EndTime { get; set; }
}
/// <summary>
/// 事件查询参数
/// </summary>
public class EventQueryParams
{
/// <summary>
/// 查询开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 查询结束时间
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 事件类型过滤
/// </summary>
public FileEventType? EventType { get; set; }
/// <summary>
/// 文件路径过滤(支持包含关系)
/// </summary>
public string PathFilter { get; set; }
/// <summary>
/// 文件扩展名过滤
/// </summary>
public string ExtensionFilter { get; set; }
/// <summary>
/// 分页大小
/// </summary>
public int PageSize { get; set; } = 100;
/// <summary>
/// 分页索引从0开始
/// </summary>
public int PageIndex { get; set; } = 0;
/// <summary>
/// 排序方向true为升序false为降序
/// </summary>
public bool AscendingOrder { get; set; } = false;
}
}

View File

@ -0,0 +1,157 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace JiShe.CollectBus.PluginFileWatcher
{
/// <summary>
/// 文件监控相关工具类
/// </summary>
public static class FileWatcherUtils
{
/// <summary>
/// 检测文件是否被锁定
/// </summary>
/// <param name="filePath">要检查的文件路径</param>
/// <returns>如果文件被锁定则返回true否则返回false</returns>
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;
}
}
/// <summary>
/// 尝试处理一个可能被锁定的文件
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="action">成功解锁后要执行的操作</param>
/// <param name="config">健壮性配置</param>
/// <returns>处理结果true表示成功处理false表示处理失败</returns>
public static async Task<bool> TryHandleLockedFileAsync(string filePath, Func<Task> 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;
}
}
/// <summary>
/// 检查文件系统监控器是否健康
/// </summary>
/// <param name="watcher">要检查的监控器</param>
/// <param name="lastEventTime">最后一次事件的时间</param>
/// <param name="config">健壮性配置</param>
/// <returns>如果监控器健康则返回true否则返回false</returns>
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;
}
}
}

View File

@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
<!--<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />-->
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
{
"profiles": {
"JiShe.CollectBus.PluginFileWatcher": {
"commandName": "Project"
},
"Container (Dockerfile)": {
"commandName": "Docker"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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<CassandraQueryOptimizer> _logger;
private readonly IMemoryCache _cache;
private readonly ConcurrentDictionary<string, PreparedStatement> _preparedStatements;
private readonly int _batchSize;
private readonly TimeSpan _cacheExpiration;
public CassandraQueryOptimizer(
ISession session,
ILogger<CassandraQueryOptimizer> logger,
IMemoryCache cache,
int batchSize = 100,
TimeSpan? cacheExpiration = null)
{
_session = session;
_logger = logger;
_cache = cache;
_preparedStatements = new ConcurrentDictionary<string, PreparedStatement>();
_batchSize = batchSize;
_cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(5);
}
public async Task<PreparedStatement> 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<BoundStatement> 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<T> GetOrSetFromCacheAsync<T>(string cacheKey, Func<Task<T>> 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<IEnumerable<T>> ExecutePagedQueryAsync<T>(
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<T>(IEnumerable<T> items, string tableName)
{
var mapper = new Mapper(_session);
var batch = new List<BoundStatement>();
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);
}
}
}

View File

@ -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<TEntity, TKey>
: ICassandraRepository<TEntity, TKey>
where TEntity : class
where TEntity : class, ICassandraEntity<TKey>
{
private readonly ICassandraProvider _cassandraProvider;
public CassandraRepository(ICassandraProvider cassandraProvider, MappingConfiguration mappingConfig)
@ -27,12 +21,29 @@ namespace JiShe.CollectBus.Cassandra
public virtual async Task<TEntity> GetAsync(TKey id)
{
return await Mapper.SingleOrDefaultAsync<TEntity>("WHERE id = ?", id);
return await GetAsync("WHERE id = ?", id);
}
public virtual async Task<List<TEntity>> GetListAsync()
public virtual async Task<TEntity?> GetAsync(string cql, params object[] args)
{
return (await Mapper.FetchAsync<TEntity>()).ToList();
return await Mapper.SingleAsync<TEntity?>(cql, args);
}
public virtual async Task<TEntity> FirstOrDefaultAsync(TKey id)
{
return await FirstOrDefaultAsync("WHERE id = ?", id);
}
public virtual async Task<TEntity?> FirstOrDefaultAsync(string cql, params object[] args)
{
return await Mapper.FirstOrDefaultAsync<TEntity>(cql, args);
}
public virtual async Task<List<TEntity>?> GetListAsync(string? cql = null, params object[] args)
{
return cql.IsNullOrWhiteSpace() ? (await Mapper.FetchAsync<TEntity>()).ToList() : (await Mapper.FetchAsync<TEntity>(cql, args)).ToList();
}
public virtual async Task<TEntity> InsertAsync(TEntity entity)

View File

@ -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;

View File

@ -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()));
}
}
}

View File

@ -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<CassandraTableAttribute>();
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<KeyAttribute>() != null);
// 分区键设置
var primaryKey = properties.FirstOrDefault(p => p.GetCustomAttribute<PartitionKeyAttribute>() != null);
if (primaryKey == null)
{
throw new InvalidOperationException($"No primary key defined for type {type.Name}");
}
// 集群键设置
var clusteringKeys = properties.Where(p => p.GetCustomAttribute<ClusteringKeyAttribute>() != 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<text,text>";
// 处理集合类型
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];

View File

@ -10,7 +10,10 @@ namespace JiShe.CollectBus.Cassandra
public interface ICassandraRepository<TEntity, TKey> where TEntity : class
{
Task<TEntity> GetAsync(TKey id);
Task<List<TEntity>> GetListAsync();
Task<TEntity?> GetAsync(string cql, params object[] args);
Task<TEntity> FirstOrDefaultAsync(TKey id);
Task<TEntity?> FirstOrDefaultAsync(string cql, params object[] args);
Task<List<TEntity>?> GetListAsync(string? cql = null, params object[] args);
Task<TEntity> InsertAsync(TEntity entity);
Task<TEntity> UpdateAsync(TEntity entity);
Task DeleteAsync(TEntity entity);

View File

@ -15,8 +15,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\services\JiShe.CollectBus.Domain\JiShe.CollectBus.Domain.csproj" />
<ProjectReference Include="..\..\shared\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj" />
<ProjectReference Include="..\..\shared\JiShe.CollectBus.Domain.Shared\JiShe.CollectBus.Domain.Shared.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,19 @@
using JiShe.CollectBus.IoTDB.Enums;
namespace JiShe.CollectBus.IoTDB.Attribute
{
/// <summary>
/// IoTDB实体类型特性
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class EntityTypeAttribute : System.Attribute
{
public EntityTypeEnum EntityType { get; }
public EntityTypeAttribute(EntityTypeEnum entityType)
{
EntityType = entityType;
}
}
}

View File

@ -1,7 +1,7 @@
namespace JiShe.CollectBus.IoTDB.Attribute
{
/// <summary>
/// 用于标识当前实体为单侧点模式单侧点模式只有一个Filed标识字段,类型是Tuple<string,object>,Item1=>测点名称Item2=>测点值,泛型
/// 用于标识当前实体为单侧点模式单侧点模式只有一个Filed标识字段,类型是Tuple<string,T>,Item1=>测点名称Item2=>测点值,泛型
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class SingleMeasuringAttribute : System.Attribute

View File

@ -0,0 +1,18 @@
using JiShe.CollectBus.IoTDB.Enums;
namespace JiShe.CollectBus.IoTDB.Attribute
{
/// <summary>
/// IoTDB实体存储路径或表名称一般用于已经明确的存储路径或表名称例如日志存储
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class TableNameOrTreePathAttribute : System.Attribute
{
public string TableNameOrTreePath { get; }
public TableNameOrTreePathAttribute(string tableNameOrTreePath)
{
TableNameOrTreePath = tableNameOrTreePath;
}
}
}

View File

@ -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
{
public class CollectBusIoTDBModule : AbpModule
namespace JiShe.CollectBus.IoTDB;
/// <summary>
/// CollectBusIoTDBModule
/// </summary>
public class CollectBusIoTDbModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<IoTDBOptions>(options =>
{
configuration.GetSection(nameof(IoTDBOptions)).Bind(options);
});
// 注册上下文为Scoped
context.Services.AddScoped<IoTDBRuntimeContext>();
// 注册Session工厂
context.Services.AddSingleton<IIoTDBSessionFactory, IoTDBSessionFactory>();
// 注册Provider
context.Services.AddScoped<IIoTDBProvider, IoTDBProvider>();
Configure<IoTDbOptions>(options => { configuration.GetSection(nameof(IoTDbOptions)).Bind(options); });
}
//// 注册上下文为Scoped
//context.Services.AddScoped<IoTDBRuntimeContext>();
}
}

View File

@ -1,23 +1,24 @@
using JiShe.CollectBus.IoTDB.Options;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace JiShe.CollectBus.IoTDB.Context
{
/// <summary>
/// IoTDB SessionPool 运行时上下文
/// </summary>
public class IoTDBRuntimeContext
public class IoTDBRuntimeContext: IScopedDependency
{
private readonly bool _defaultValue;
public IoTDBRuntimeContext(IOptions<IoTDBOptions> options)
public IoTDBRuntimeContext(IOptions<IoTDbOptions> options)
{
_defaultValue = options.Value.UseTableSessionPoolByDefault;
UseTableSessionPool = _defaultValue;
}
/// <summary>
/// 是否使用表模型存储, 默认false使用tree模型存储
/// 存储模型切换标识是否使用table模型存储, 默认为false标识tree模型存储
/// </summary>
public bool UseTableSessionPool { get; set; }

View File

@ -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
{
/// <summary>
/// IoTDB实体类型枚举
/// </summary>
public enum EntityTypeEnum
{
/// <summary>
/// 树模型
/// </summary>
TreeModel = 1,
/// <summary>
/// 表模型
/// </summary>
TableModel = 2,
}
}

View File

@ -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
/// <summary>
/// IoTDB数据源,数据库能同时存多个时序模型,但数据是完全隔离的,不能跨时序模型查询,通过连接字符串配置
/// </summary>
public interface IIoTDBProvider
public interface IIoTDbProvider
{
///// <summary>
///// 切换 SessionPool
@ -31,6 +32,15 @@ namespace JiShe.CollectBus.IoTDB.Interface
/// <returns></returns>
Task BatchInsertAsync<T>(IEnumerable<T> entities) where T : IoTEntity;
/// <summary>
/// 批量插入数据
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="deviceMetadata">设备元数据</param>
/// <param name="entities"></param>
/// <returns></returns>
Task BatchInsertAsync<T>(DeviceMetadata deviceMetadata,IEnumerable<T> entities) where T : IoTEntity;
/// <summary>
/// 删除数据
@ -38,7 +48,14 @@ namespace JiShe.CollectBus.IoTDB.Interface
/// <typeparam name="T"></typeparam>
/// <param name="options"></param>
/// <returns></returns>
Task<object> DeleteAsync<T>(QueryOptions options) where T : IoTEntity;
Task<object> DeleteAsync<T>(IoTDBQueryOptions options) where T : IoTEntity;
/// <summary>
/// 获取设备元数据
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
Task<DeviceMetadata> GetMetadata<T>() where T : IoTEntity;
/// <summary>
/// 查询数据
@ -46,6 +63,6 @@ namespace JiShe.CollectBus.IoTDB.Interface
/// <typeparam name="T"></typeparam>
/// <param name="options"></param>
/// <returns></returns>
Task<BusPagedResult<T>> QueryAsync<T>(QueryOptions options) where T : IoTEntity, new();
Task<BusPagedResult<T>> QueryAsync<T>(IoTDBQueryOptions options) where T : IoTEntity, new();
}
}

View File

@ -3,8 +3,8 @@
/// <summary>
/// Session 工厂接口
/// </summary>
public interface IIoTDBSessionFactory:IDisposable
public interface IIoTDbSessionFactory:IDisposable
{
IIoTDBSessionPool GetSessionPool(bool useTableSession);
IIoTDbSessionPool GetSessionPool(bool useTableSession);
}
}

View File

@ -5,7 +5,7 @@ namespace JiShe.CollectBus.IoTDB.Interface
/// <summary>
/// Session 连接池
/// </summary>
public interface IIoTDBSessionPool : IDisposable
public interface IIoTDbSessionPool : IDisposable
{
/// <summary>
/// 打开连接池
@ -13,6 +13,12 @@ namespace JiShe.CollectBus.IoTDB.Interface
/// <returns></returns>
Task OpenAsync();
/// <summary>
/// 关闭连接池
/// </summary>
/// <returns></returns>
Task CloseAsync();
/// <summary>
/// 插入数据
/// </summary>

View File

@ -6,7 +6,6 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!--<PackageReference Include="Apache.IoTDB" Version="1.3.3.1" />-->
<PackageReference Include="Apache.IoTDB" Version="2.0.2" />
<PackageReference Include="Volo.Abp" Version="8.3.3" />
</ItemGroup>

View File

@ -1,9 +1,9 @@
using JiShe.CollectBus.IoTDB.Attribute;
namespace JiShe.CollectBus.IoTDB.Provider
namespace JiShe.CollectBus.IoTDB.Model
{
/// <summary>
/// IoT实体基类
/// IoT实体基类,此类适用于多个数据测点记录场景,单个测点请使用子类 SingleMeasuring
/// </summary>
public abstract class IoTEntity
{
@ -17,7 +17,7 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// 项目编码
/// </summary>
[TAGColumn]
public string ProjectCode { get; set; }
public string ProjectId { get; set; }
/// <summary>
/// 设备类型集中器、电表、水表、流量计、传感器等
@ -26,13 +26,13 @@ namespace JiShe.CollectBus.IoTDB.Provider
public string DeviceType { get; set; }
/// <summary>
/// 设备ID
/// 设备ID,数据生成者例如集中器ID,电表ID、水表ID、流量计ID、传感器ID等
/// </summary>
[TAGColumn]
public string DeviceId { get; set; }
/// <summary>
/// 当前时间戳,单位毫秒
/// 时标,也就是业务时间戳,单位毫秒,必须通过DateTimeOffset获取
/// </summary>
public long Timestamps { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using JiShe.CollectBus.IoTDB.Attribute;
using JiShe.CollectBus.IoTDB.Enums;
using JiShe.CollectBus.IoTDB.Provider;
namespace JiShe.CollectBus.IoTDB.Model
{
/// <summary>
/// Table模型单项数据实体
/// </summary>
[EntityType(EntityTypeEnum.TableModel)]
public class TableModelSingleMeasuringEntity<T> : IoTEntity
{
/// <summary>
/// 单项数据键值对
/// </summary>
[SingleMeasuring(nameof(SingleColumn))]
public required Tuple<string, T> SingleColumn { get; set; }
}
}

View File

@ -4,19 +4,21 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using JiShe.CollectBus.IoTDB.Attribute;
using JiShe.CollectBus.IoTDB.Enums;
using JiShe.CollectBus.IoTDB.Provider;
namespace JiShe.CollectBus.IotSystems.AFNEntity
namespace JiShe.CollectBus.IoTDB.Model
{
/// <summary>
/// AFN单项数据实体
/// Tree模型单项数据实体
/// </summary>
public class SingleMeasuringAFNDataEntity<T> : IoTEntity
[EntityType(EntityTypeEnum.TreeModel)]
public class TreeModelSingleMeasuringEntity<T> : IoTEntity
{
/// <summary>
/// 单项数据
/// 单项数据键值
/// </summary>
[SingleMeasuring(nameof(SingleMeasuring))]
public Tuple<string, T> SingleMeasuring { get; set; }
public required Tuple<string, T> SingleMeasuring { get; set; }
}
}

View File

@ -3,7 +3,7 @@
/// <summary>
/// IOTDB配置
/// </summary>
public class IoTDBOptions
public class IoTDbOptions
{
/// <summary>
/// 数据库名称,表模型才有,树模型为空
@ -42,5 +42,15 @@
/// 是否使用表模型存储, 默认false使用tree模型存储
/// </summary>
public bool UseTableSessionPoolByDefault { get; set; } = false;
/// <summary>
/// 时区,默认为:"UTC+08:00"
/// </summary>
public string ZoneId { get; set; } = "UTC+08:00";
/// <summary>
/// 请求超时时间单位毫秒默认为50000
/// </summary>
public long Timeout { get; set; } = 50000;
}
}

View File

@ -3,7 +3,7 @@
/// <summary>
/// 查询条件
/// </summary>
public class QueryOptions
public class IoTDBQueryOptions
{
/// <summary>
/// 表模型的表名称或者树模型的设备路径
@ -13,7 +13,7 @@
/// <summary>
/// 分页
/// </summary>
public int Page { get; set; }
public int PageIndex { get; set; }
/// <summary>
/// 分页大小
@ -23,6 +23,6 @@
/// <summary>
/// 查询条件
/// </summary>
public List<QueryCondition> Conditions { get; } = new();
public List<QueryCondition> Conditions { get; set; } = new();
}
}

View File

@ -9,10 +9,17 @@
/// 字段
/// </summary>
public string Field { get; set; }
/// <summary>
/// 操作符
/// 操作符,>=<
/// </summary>
public string Operator { get; set; }
/// <summary>
/// 是否数值,如果是数值,则进行数值比较,否则进行字符串比较
/// </summary>
public bool IsNumber { get; set; } = false;
/// <summary>
/// 值
/// </summary>

View File

@ -1,4 +1,5 @@
using Apache.IoTDB;
using JiShe.CollectBus.IoTDB.Enums;
namespace JiShe.CollectBus.IoTDB.Provider
{
@ -7,6 +8,11 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// </summary>
public class DeviceMetadata
{
/// <summary>
/// IoTDB实体类型枚举
/// </summary>
public EntityTypeEnum EntityType { get; set; }
/// <summary>
/// 是否有单测量值
/// </summary>

View File

@ -1,4 +1,6 @@
namespace JiShe.CollectBus.IoTDB.Provider
using JiShe.CollectBus.IoTDB.Model;
namespace JiShe.CollectBus.IoTDB.Provider
{
/// <summary>
/// 设备路径构建器
@ -13,7 +15,7 @@
/// <returns></returns>
public static string GetDevicePath<T>(T entity) where T : IoTEntity
{
return $"root.{entity.SystemName.ToLower()}.`{entity.ProjectCode}`.`{entity.DeviceId}`";
return $"root.{entity.SystemName.ToLower()}.`{entity.ProjectId}`.`{entity.DeviceType}`.`{entity.DeviceId}`";
}
@ -28,6 +30,17 @@
var type = typeof(T);
return $"{type.Name.ToLower()}";
}
/// <summary>
/// 获取表名称,用作单侧点表模型特殊处理。
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="entity"></param>
/// <returns></returns>
public static string GetDeviceTableName<T>(T entity) where T : IoTEntity
{
return $"{entity.SystemName.ToLower()}.`{entity.ProjectId}`.`{entity.DeviceType}`.`{entity.DeviceId}`";
}
}
}

View File

@ -1,33 +1,49 @@
using System.Collections.Concurrent;
using System;
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Reflection.Metadata.Ecma335;
using System.Text;
using System.Threading.Tasks;
using Apache.IoTDB;
using Apache.IoTDB.DataStructure;
using JiShe.CollectBus.Common.Enums;
using JiShe.CollectBus.Common.Extensions;
using JiShe.CollectBus.Common.Helpers;
using JiShe.CollectBus.Common.Models;
using JiShe.CollectBus.IoTDB.Attribute;
using JiShe.CollectBus.IoTDB.Context;
using JiShe.CollectBus.IoTDB.Interface;
using JiShe.CollectBus.IoTDB.Model;
using JiShe.CollectBus.IoTDB.Options;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
namespace JiShe.CollectBus.IoTDB.Provider
{
/// <summary>
/// IoTDB数据源
/// </summary>
public class IoTDBProvider : IIoTDBProvider
public class IoTDbProvider : IIoTDbProvider, ITransientDependency
{
private static readonly ConcurrentDictionary<Type, DeviceMetadata> _metadataCache = new();
private readonly ILogger<IoTDBProvider> _logger;
private readonly IIoTDBSessionFactory _sessionFactory;
private static readonly ConcurrentDictionary<Type, DeviceMetadata> MetadataCache = new();
private readonly ILogger<IoTDbProvider> _logger;
private readonly IIoTDbSessionFactory _sessionFactory;
private readonly IoTDBRuntimeContext _runtimeContext;
private IIoTDBSessionPool CurrentSession =>
private IIoTDbSessionPool CurrentSession =>
_sessionFactory.GetSessionPool(_runtimeContext.UseTableSessionPool);
public IoTDBProvider(
ILogger<IoTDBProvider> logger,
IIoTDBSessionFactory sessionFactory,
/// <summary>
/// IoTDbProvider
/// </summary>
/// <param name="logger"></param>
/// <param name="sessionFactory"></param>
/// <param name="runtimeContext"></param>
public IoTDbProvider(
ILogger<IoTDbProvider> logger,
IIoTDbSessionFactory sessionFactory,
IoTDBRuntimeContext runtimeContext)
{
_logger = logger;
@ -44,17 +60,19 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <returns></returns>
public async Task InsertAsync<T>(T entity) where T : IoTEntity
{
var metadata = GetMetadata<T>();
try
{
var metadata = await GetMetadata<T>();
var tablet = BuildTablet(new[] { entity }, metadata);
await CurrentSession.InsertAsync(tablet);
//int result = await _currentSession.InsertAsync(tablet);
//if (result <= 0)
//{
// _logger.LogError($"{typeof(T).Name}插入数据没有成功");
//}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(InsertAsync)} 插入数据时发生异常");
throw;
}
}
/// <summary>
@ -64,7 +82,9 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <returns></returns>
public async Task BatchInsertAsync<T>(IEnumerable<T> entities) where T : IoTEntity
{
var metadata = GetMetadata<T>();
try
{
var metadata = await GetMetadata<T>();
var batchSize = 1000;
var batches = entities.Chunk(batchSize);
@ -73,11 +93,40 @@ namespace JiShe.CollectBus.IoTDB.Provider
{
var tablet = BuildTablet(batch, metadata);
await CurrentSession.InsertAsync(tablet);
//var result = await _currentSession.InsertAsync(tablet);
//if (result <= 0)
//{
// _logger.LogWarning($"{typeof(T).Name} 批量插入数据第{batch}批次没有成功,共{batches}批次。");
//}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(BatchInsertAsync)} 批量插入数据时发生异常");
throw;
}
}
/// <summary>
/// 批量插入数据
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="deviceMetadata">设备元数据</param>
/// <param name="entities"></param>
/// <returns></returns>
public async Task BatchInsertAsync<T>(DeviceMetadata deviceMetadata, IEnumerable<T> entities) where T : IoTEntity
{
try
{
var batchSize = 1000;
var batches = entities.Chunk(batchSize);
foreach (var batch in batches)
{
var tablet = BuildTablet(batch, deviceMetadata);
await CurrentSession.InsertAsync(tablet);
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(BatchInsertAsync)} 批量插入数据时发生异常");
throw;
}
}
@ -88,9 +137,11 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <typeparam name="T"></typeparam>
/// <param name="options"></param>
/// <returns></returns>
public async Task<object> DeleteAsync<T>(QueryOptions options) where T : IoTEntity
public async Task<object> DeleteAsync<T>(IoTDBQueryOptions options) where T : IoTEntity
{
var query = BuildDeleteSQL<T>(options);
try
{
var query = await BuildDeleteSQL<T>(options);
var sessionDataSet = await CurrentSession.ExecuteQueryStatementAsync(query);
if (!sessionDataSet.HasNext())
@ -103,6 +154,38 @@ namespace JiShe.CollectBus.IoTDB.Provider
var row = sessionDataSet.Next();
return row.Values[0];
}
catch (Exception ex)
{
_logger.LogError(ex, $"{nameof(DeleteAsync)} 删除数据时发生异常");
throw;
}
}
/// <summary>
/// 获取设备元数据
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public async Task<DeviceMetadata> GetMetadata<T>() where T : IoTEntity
{
var columns = CollectColumnMetadata(typeof(T));
var metadata = BuildDeviceMetadata<T>(columns);
var metaData = MetadataCache.AddOrUpdate(
typeof(T),
addValueFactory: t => metadata, // 如果键不存在,用此值添加
updateValueFactory: (t, existingValue) =>
{
var columns = CollectColumnMetadata(t);
var metadata = BuildDeviceMetadata<T>(columns);
//对现有值 existingValue 进行修改,返回新值
existingValue.ColumnNames = metadata.ColumnNames;
return existingValue;
}
);
return await Task.FromResult(metaData);
}
/// <summary>
/// 查询数据
@ -110,19 +193,34 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <typeparam name="T"></typeparam>
/// <param name="options"></param>
/// <returns></returns>
public async Task<BusPagedResult<T>> QueryAsync<T>(QueryOptions options) where T : IoTEntity, new()
public async Task<BusPagedResult<T>> QueryAsync<T>(IoTDBQueryOptions options) where T : IoTEntity, new()
{
var query = BuildQuerySQL<T>(options);
try
{
var query = await BuildQuerySQL<T>(options);
var sessionDataSet = await CurrentSession.ExecuteQueryStatementAsync(query);
var result = new BusPagedResult<T>
{
TotalCount = await GetTotalCount<T>(options),
Items = ParseResults<T>(sessionDataSet, options.PageSize)
Items = await ParseResults<T>(sessionDataSet, options.PageSize),
PageIndex = options.PageIndex,
PageSize = options.PageSize,
};
result.HasNext = result.TotalCount > 0 ? result.TotalCount < result.PageSize : false;
return result;
}
catch (Exception ex)
{
CurrentSession.Dispose();
_logger.LogError(ex, $"{nameof(QueryAsync)} IoTDB查询数据时发生异常");
throw;
}
}
/// <summary>
/// 构建Tablet
@ -139,10 +237,39 @@ namespace JiShe.CollectBus.IoTDB.Provider
List<string> tempColumnNames = new List<string>();
tempColumnNames.AddRange(metadata.ColumnNames);
var entityTypeAttribute = typeof(T).GetCustomAttribute<EntityTypeAttribute>();
if (entityTypeAttribute == null)
{
throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构{nameof(Tablet)}时 {nameof(T)}的EntityType 没有指定,属于异常情况,-101");
}
if (metadata.EntityType != entityTypeAttribute.EntityType)
{
throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构{nameof(Tablet)}时 {nameof(T)}的EntityType 和{nameof(DeviceMetadata)}的 EntityType 不一致,属于异常情况,-102");
}
if (metadata.EntityType == Enums.EntityTypeEnum.TreeModel && _runtimeContext.UseTableSessionPool == true)
{
throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构{nameof(Tablet)}时 tree模型不能使用table模型Session连接属于异常情况-103");
}
else if (metadata.EntityType == Enums.EntityTypeEnum.TableModel && _runtimeContext.UseTableSessionPool == false)
{
throw new ArgumentException($"{nameof(BuildTablet)} 构建存储结构{nameof(Tablet)}时 table模型不能使用tree模型Session连接属于异常情况-104");
}
string tableNameOrTreePath = string.Empty;
var tableNameOrTreePathAttribute = typeof(T).GetCustomAttribute<TableNameOrTreePathAttribute>();
if (tableNameOrTreePathAttribute != null)
{
tableNameOrTreePath = tableNameOrTreePathAttribute.TableNameOrTreePath;
}
foreach (var entity in entities)
{
timestamps.Add(entity.Timestamps);
var rowValues = new List<object>();
foreach (var measurement in tempColumnNames)
{
@ -153,7 +280,9 @@ namespace JiShe.CollectBus.IoTDB.Provider
}
var value = propertyInfo.GetValue(entity);
if (propertyInfo.IsDefined(typeof(SingleMeasuringAttribute), false) && value != null)//表示当前对象是单测点模式
if (propertyInfo.IsDefined(typeof(SingleMeasuringAttribute), false) && metadata.IsSingleMeasuring == true)//表示当前对象是单测点模式
{
if (value != null)
{
Type tupleType = value.GetType();
Type[] tupleArgs = tupleType.GetGenericArguments();
@ -169,27 +298,51 @@ namespace JiShe.CollectBus.IoTDB.Provider
metadata.ColumnNames[indexOf] = (string)item1!;
rowValues.Add(item2);
}
else
{
rowValues.Add(null);
}
//同时如果是单测点模式且是table模型存储路径只能通过DevicePathBuilder.GetDeviceTableName(entity)获取
if (_runtimeContext.UseTableSessionPool)
{
tableNameOrTreePath = DevicePathBuilder.GetDeviceTableName(entity);
}
}
else
{
//需要根据value的类型进行相应的值映射转换例如datetime转换为long的时间戳值
if (value != null)
{
Type tupleType = value.GetType();
var tempValue = tupleType.Name.ToUpper() switch
{
"DATETIME" => Convert.ToDateTime(value).GetDateTimeOffset().ToUnixTimeNanoseconds(),
_ => value
};
rowValues.Add(tempValue);
}
else
{
rowValues.Add(value);
}
else
{
//填充默认数据值
DataTypeDefaultValueMap.TryGetValue(propertyInfo.PropertyType.Name, out object defaultValue);
rowValues.Add(defaultValue);
}
}
}
values.Add(rowValues);
//如果指定了路径
if (!string.IsNullOrWhiteSpace(tableNameOrTreePath))
{
devicePaths.Add(tableNameOrTreePath);
}
else
{
if (!_runtimeContext.UseTableSessionPool)//树模型
{
devicePaths.Add(DevicePathBuilder.GetDevicePath(entity));
@ -200,6 +353,8 @@ namespace JiShe.CollectBus.IoTDB.Provider
}
}
}
if (devicePaths.Count > 1)
{
throw new Exception($"{nameof(BuildTablet)} 构建Tablet《{typeof(T).Name}》时,批量插入的设备路径不一致。");
@ -213,14 +368,15 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <summary>
/// 构建tree模型的Tablet
/// </summary>
/// <param name="metadata"></param>
/// <param name="devicePath"></param>
/// <param name="values"></param>
/// <param name="timestamps"></param>
/// <param name="metadata">已解析的设备数据元数据</param>
/// <param name="devicePath">设备路径</param>
/// <param name="values">数据集合</param>
/// <param name="timestamps">时间戳集合</param>
/// <returns></returns>
private Tablet BuildSessionTablet(DeviceMetadata metadata, string devicePath,
List<List<object>> values, List<long> timestamps)
private Tablet BuildSessionTablet(DeviceMetadata metadata, string devicePath, List<List<object>> values, List<long> timestamps)
{
//todo 树模型需要去掉TAG类型和ATTRIBUTE类型的字段只需要保留FIELD类型字段即可
return new Tablet(
devicePath,
metadata.ColumnNames,
@ -233,16 +389,15 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <summary>
/// 构建表模型的Tablet
/// </summary>
/// <param name="metadata"></param>
/// <param name="devicePath"></param>
/// <param name="values"></param>
/// <param name="timestamps"></param>
/// <param name="metadata">已解析的设备数据元数据</param>
/// <param name="tableName">表名称</param>
/// <param name="values">数据集合</param>
/// <param name="timestamps">时间戳集合</param>
/// <returns></returns>
private Tablet BuildTableSessionTablet(DeviceMetadata metadata, string devicePath,
List<List<object>> values, List<long> timestamps)
private Tablet BuildTableSessionTablet(DeviceMetadata metadata, string tableName, List<List<object>> values, List<long> timestamps)
{
var tablet = new Tablet(
devicePath,
tableName,
metadata.ColumnNames,
metadata.ColumnCategories,
metadata.DataTypes,
@ -259,10 +414,10 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <typeparam name="T"></typeparam>
/// <param name="options"></param>
/// <returns></returns>
private string BuildQuerySQL<T>(QueryOptions options) where T : IoTEntity
private async Task<string> BuildQuerySQL<T>(IoTDBQueryOptions options) where T : IoTEntity
{
var metadata = GetMetadata<T>();
var sb = new StringBuilder("SELECT ");
var metadata = await GetMetadata<T>();
var sb = new StringBuilder("SELECT TIME as Timestamps,");
sb.AppendJoin(", ", metadata.ColumnNames);
sb.Append($" FROM {options.TableNameOrTreePath}");
@ -272,7 +427,7 @@ namespace JiShe.CollectBus.IoTDB.Provider
sb.AppendJoin(" AND ", options.Conditions.Select(TranslateCondition));
}
sb.Append($" LIMIT {options.PageSize} OFFSET {options.Page * options.PageSize}");
sb.Append($" LIMIT {options.PageSize} OFFSET {options.PageIndex * options.PageSize}");
return sb.ToString();
}
@ -282,9 +437,9 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <typeparam name="T"></typeparam>
/// <param name="options"></param>
/// <returns></returns>
private string BuildDeleteSQL<T>(QueryOptions options) where T : IoTEntity
private async Task<string> BuildDeleteSQL<T>(IoTDBQueryOptions options) where T : IoTEntity
{
var metadata = GetMetadata<T>();
var metadata = await GetMetadata<T>();
var sb = new StringBuilder();
if (!_runtimeContext.UseTableSessionPool)
@ -319,10 +474,10 @@ namespace JiShe.CollectBus.IoTDB.Provider
{
return condition.Operator switch
{
">" => $"{condition.Field} > {condition.Value}",
"<" => $"{condition.Field} < {condition.Value}",
"=" => $"{condition.Field} = '{condition.Value}'",
_ => throw new NotSupportedException($"Operator {condition.Operator} not supported")
">" => condition.IsNumber ? $"{condition.Field} > {condition.Value}" : $"{condition.Field} > '{condition.Value}'",
"<" => condition.IsNumber ? $"{condition.Field} < {condition.Value}" : $"{condition.Field} < '{condition.Value}'",
"=" => condition.IsNumber ? $"{condition.Field} = {condition.Value}" : $"{condition.Field} = '{condition.Value}'",
_ => throw new NotSupportedException($"{nameof(TranslateCondition)} 将查询条件转换为SQL语句时操作符 {condition.Operator} 属于异常情况")
};
}
@ -332,7 +487,7 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <typeparam name="T"></typeparam>
/// <param name="options"></param>
/// <returns></returns>
private async Task<int> GetTotalCount<T>(QueryOptions options) where T : IoTEntity
private async Task<int> GetTotalCount<T>(IoTDBQueryOptions options) where T : IoTEntity
{
var countQuery = $"SELECT COUNT(*) FROM {options.TableNameOrTreePath}";
if (options.Conditions.Any())
@ -341,7 +496,16 @@ namespace JiShe.CollectBus.IoTDB.Provider
}
var result = await CurrentSession.ExecuteQueryStatementAsync(countQuery);
return result.HasNext() ? Convert.ToInt32(result.Next().Values[0]) : 0;
if (result.HasNext())
{
await result.Close();
return 0;
}
var count = Convert.ToInt32(result.Next().Values[0]);
await result.Close();
return count;
}
/// <summary>
@ -351,13 +515,20 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <param name="dataSet"></param>
/// <param name="pageSize"></param>
/// <returns></returns>
private IEnumerable<T> ParseResults<T>(SessionDataSet dataSet, int pageSize) where T : IoTEntity, new()
private async Task<IEnumerable<T>> ParseResults<T>(SessionDataSet dataSet, int pageSize) where T : IoTEntity, new()
{
var results = new List<T>();
var metadata = GetMetadata<T>();
var metadata = await GetMetadata<T>();
var properties = typeof(T).GetProperties();
var columns = new List<string>() { "Timestamps" };
var dataTypes = new List<TSDataType>() { TSDataType.TIMESTAMP };
columns.AddRange(metadata.ColumnNames);
dataTypes.AddRange(metadata.DataTypes);
//metadata.ColumnNames.Insert(0, "Timestamps");
//metadata.DataTypes.Insert(0, TSDataType.TIMESTAMP);
while (dataSet.HasNext() && results.Count < pageSize)
{
var record = dataSet.Next();
@ -366,62 +537,37 @@ namespace JiShe.CollectBus.IoTDB.Provider
Timestamps = record.Timestamps
};
foreach (var measurement in metadata.ColumnNames)
foreach (var measurement in columns)
{
var value = record.Values;
int indexOf = columns.IndexOf(measurement);
var value = record.Values[indexOf];
TSDataType tSDataType = dataTypes[indexOf];
var prop = properties.FirstOrDefault(p =>
p.Name.Equals(measurement, StringComparison.OrdinalIgnoreCase));
if (prop != null)
if (prop != null && !(value is System.DBNull))
{
typeof(T).GetProperty(measurement)?.SetValue(entity, value);
dynamic tempValue = GetTSDataValue(tSDataType, value);
if (measurement.ToLower().EndsWith("time"))
{
typeof(T).GetProperty(measurement)?.SetValue(entity, TimestampHelper.ConvertToDateTime(tempValue, TimestampUnit.Nanoseconds));
}
else
{
typeof(T).GetProperty(measurement)?.SetValue(entity, tempValue);
}
}
}
results.Add(entity);
}
await dataSet.Close();
return results;
}
/// <summary>
/// 获取设备元数据
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
private DeviceMetadata GetMetadata<T>() where T : IoTEntity
{
var columns = CollectColumnMetadata(typeof(T));
var metadata = BuildDeviceMetadata(columns);
return _metadataCache.AddOrUpdate(
typeof(T),
addValueFactory: t => metadata, // 如果键不存在,用此值添加
updateValueFactory: (t, existingValue) =>
{
var columns = CollectColumnMetadata(t);
var metadata = BuildDeviceMetadata(columns);
//对现有值 existingValue 进行修改,返回新值
existingValue.ColumnNames = metadata.ColumnNames;
return existingValue;
}
);
//return _metadataCache.GetOrAdd(typeof(T), type =>
//{
// var columns = CollectColumnMetadata(type);
// var metadata = BuildDeviceMetadata(columns);
// //if (metadata.IsSingleMeasuring)
// //{
// // _metadataCache.Remove(typeof(T));
// //}
// return metadata;
//});
}
/// <summary>
/// 获取设备元数据的列
/// </summary>
@ -433,21 +579,36 @@ namespace JiShe.CollectBus.IoTDB.Provider
foreach (var prop in type.GetProperties())
{
string typeName = string.Empty;
Type declaredType = prop.PropertyType;
// 处理可空类型
if (declaredType.IsGenericType && declaredType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
Type underlyingType = Nullable.GetUnderlyingType(declaredType);
typeName = underlyingType.Name;
}
else
{
typeName = declaredType.Name;
}
//先获取Tag标签和属性标签
ColumnInfo? column = prop.GetCustomAttribute<TAGColumnAttribute>() is not null ? new ColumnInfo(
name: prop.Name,
category: ColumnCategory.TAG,
dataType: GetDataTypeFromTypeName(prop.PropertyType.Name),
dataType: GetDataTypeFromTypeName(typeName),
false
) : prop.GetCustomAttribute<ATTRIBUTEColumnAttribute>() is not null ? new ColumnInfo(
prop.Name,
ColumnCategory.ATTRIBUTE,
GetDataTypeFromTypeName(prop.PropertyType.Name),
GetDataTypeFromTypeName(typeName),
false
) : prop.GetCustomAttribute<FIELDColumnAttribute>() is not null ? new ColumnInfo(
prop.Name,
ColumnCategory.FIELD,
GetDataTypeFromTypeName(prop.PropertyType.Name),
GetDataTypeFromTypeName(typeName),
false)
: null;
@ -483,9 +644,10 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <summary>
/// 构建设备元数据
/// </summary>
/// <param name="columns"></param>
/// <param name="typeInfo">待解析的类</param>
/// <param name="columns">已处理好的数据列</param>
/// <returns></returns>
private DeviceMetadata BuildDeviceMetadata(List<ColumnInfo> columns)
private DeviceMetadata BuildDeviceMetadata<T>(List<ColumnInfo> columns) where T : IoTEntity
{
var metadata = new DeviceMetadata();
@ -504,6 +666,15 @@ namespace JiShe.CollectBus.IoTDB.Provider
ProcessCategory(groupedColumns, ColumnCategory.ATTRIBUTE, metadata);
ProcessCategory(groupedColumns, ColumnCategory.FIELD, metadata);
var entityTypeAttribute = typeof(T).GetCustomAttribute<EntityTypeAttribute>();
if (entityTypeAttribute == null)
{
throw new ArgumentException($"{nameof(BuildDeviceMetadata)} 构建设备元数据时 {nameof(IoTEntity)} 的EntityType 没有指定,属于异常情况,-101");
}
metadata.EntityType = entityTypeAttribute.EntityType;
return metadata;
}
@ -585,7 +756,7 @@ namespace JiShe.CollectBus.IoTDB.Provider
["DOUBLE"] = TSDataType.DOUBLE,
["TEXT"] = TSDataType.TEXT,
["NULLTYPE"] = TSDataType.NONE,
["TIMESTAMP"] = TSDataType.TIMESTAMP,
["DATETIME"] = TSDataType.TIMESTAMP,
["DATE"] = TSDataType.DATE,
["BLOB"] = TSDataType.BLOB,
["DECIMAL"] = TSDataType.STRING,
@ -605,11 +776,34 @@ namespace JiShe.CollectBus.IoTDB.Provider
["DOUBLE"] = 0.0d,
["TEXT"] = string.Empty,
["NULLTYPE"] = null,
["TIMESTAMP"] = null,
["DATETIME"] = null,
["DATE"] = null,
["BLOB"] = null,
["DECIMAL"] = "0.0",
["STRING"] = string.Empty
};
/// <summary>
/// IoTDB 数据类型与.net类型映射
/// </summary>
/// <param name="tSDataType"></param>
/// <param name="value"></param>
/// <returns></returns>
private dynamic GetTSDataValue(TSDataType tSDataType, object value) =>
tSDataType switch
{
TSDataType.BOOLEAN => Convert.ToBoolean(value),
TSDataType.INT32 => Convert.ToInt32(value),
TSDataType.INT64 => Convert.ToInt64(value),
TSDataType.FLOAT => Convert.ToDouble(value),
TSDataType.DOUBLE => Convert.ToDouble(value),
TSDataType.TEXT => Convert.ToString(value),
TSDataType.NONE => null,
TSDataType.TIMESTAMP => Convert.ToInt64(value),
TSDataType.DATE => Convert.ToDateTime(value),
TSDataType.BLOB => Convert.ToByte(value),
TSDataType.STRING => Convert.ToString(value),
_ => Convert.ToString(value)
};
}
}

View File

@ -2,6 +2,7 @@
using JiShe.CollectBus.IoTDB.Interface;
using JiShe.CollectBus.IoTDB.Options;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace JiShe.CollectBus.IoTDB.Provider
{
@ -9,25 +10,29 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <summary>
/// 实现带缓存的Session工厂
/// </summary>
public class IoTDBSessionFactory : IIoTDBSessionFactory
public class IoTDbSessionFactory : IIoTDbSessionFactory, ISingletonDependency
{
private readonly IoTDBOptions _options;
private readonly ConcurrentDictionary<bool, IIoTDBSessionPool> _pools = new();
private readonly IoTDbOptions _options;
private readonly ConcurrentDictionary<bool, IIoTDbSessionPool> _pools = new();
private bool _disposed;
public IoTDBSessionFactory(IOptions<IoTDBOptions> options)
/// <summary>
/// IoTDbSessionFactory
/// </summary>
/// <param name="options"></param>
public IoTDbSessionFactory(IOptions<IoTDbOptions> options)
{
_options = options.Value;
}
public IIoTDBSessionPool GetSessionPool(bool useTableSession)
public IIoTDbSessionPool GetSessionPool(bool useTableSession)
{
if (_disposed) throw new ObjectDisposedException(nameof(IoTDBSessionFactory));
if (_disposed) throw new ObjectDisposedException(nameof(IoTDbSessionFactory));
return _pools.GetOrAdd(useTableSession, key =>
{
var pool = key
? (IIoTDBSessionPool)new TableSessionPoolAdapter(_options)
? (IIoTDbSessionPool)new TableSessionPoolAdapter(_options)
: new SessionPoolAdapter(_options);
pool.OpenAsync().ConfigureAwait(false).GetAwaiter().GetResult(); ;

View File

@ -9,18 +9,23 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <summary>
/// 树模型连接池
/// </summary>
public class SessionPoolAdapter : IIoTDBSessionPool
public class SessionPoolAdapter : IIoTDbSessionPool
{
private readonly SessionPool _sessionPool;
private readonly IoTDBOptions _options;
private readonly IoTDbOptions _options;
public SessionPoolAdapter(IoTDBOptions options)
/// <summary>
/// SessionPoolAdapter
/// </summary>
/// <param name="options"></param>
public SessionPoolAdapter(IoTDbOptions options)
{
_options = options;
_sessionPool = new SessionPool.Builder()
.SetNodeUrl(options.ClusterList)
.SetUsername(options.UserName)
.SetPassword(options.Password)
.SetZoneId(options.ZoneId)
.SetFetchSize(options.FetchSize)
.SetPoolSize(options.PoolSize)
.Build();
@ -42,6 +47,19 @@ namespace JiShe.CollectBus.IoTDB.Provider
}
}
/// <summary>
/// 关闭连接池
/// </summary>
/// <returns></returns>
public async Task CloseAsync()
{
if (_sessionPool == null)
{
return;
}
await _sessionPool.Close();
}
/// <summary>
/// 批量插入对齐时间序列数据
/// </summary>
@ -52,9 +70,9 @@ namespace JiShe.CollectBus.IoTDB.Provider
var result = await _sessionPool.InsertAlignedTabletAsync(tablet);
if (result != 0)
{
throw new Exception($"{nameof(TableSessionPoolAdapter)} ");
throw new Exception($"{nameof(SessionPoolAdapter)} Tree模型数据入库没有成功返回结果为{result}");
}
//await CloseAsync();
return result;
}
@ -65,7 +83,10 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <returns></returns>
public async Task<SessionDataSet> ExecuteQueryStatementAsync(string sql)
{
return await _sessionPool.ExecuteQueryStatementAsync(sql);
var result = await _sessionPool.ExecuteQueryStatementAsync(sql, _options.Timeout);
//await result.Close();
//await CloseAsync();
return result;
}
public void Dispose()

View File

@ -9,18 +9,23 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <summary>
/// 表模型Session连接池
/// </summary>
public class TableSessionPoolAdapter : IIoTDBSessionPool
public class TableSessionPoolAdapter : IIoTDbSessionPool
{
private readonly TableSessionPool _sessionPool;
private readonly IoTDBOptions _options;
private readonly IoTDbOptions _options;
public TableSessionPoolAdapter(IoTDBOptions options)
/// <summary>
/// TableSessionPoolAdapter
/// </summary>
/// <param name="options"></param>
public TableSessionPoolAdapter(IoTDbOptions options)
{
_options = options;
_sessionPool = new TableSessionPool.Builder()
.SetNodeUrls(options.ClusterList)
.SetUsername(options.UserName)
.SetPassword(options.Password)
.SetZoneId(options.ZoneId)
.SetFetchSize(options.FetchSize)
.SetPoolSize(options.PoolSize)
.SetDatabase(options.DataBaseName)
@ -40,6 +45,19 @@ namespace JiShe.CollectBus.IoTDB.Provider
}
}
/// <summary>
/// 关闭连接池
/// </summary>
/// <returns></returns>
public async Task CloseAsync()
{
if (_sessionPool == null)
{
return;
}
await _sessionPool.Close();
}
/// <summary>
/// 批量插入
/// </summary>
@ -50,9 +68,10 @@ namespace JiShe.CollectBus.IoTDB.Provider
var result = await _sessionPool.InsertAsync(tablet);
if (result != 0)
{
throw new Exception($"{nameof(TableSessionPoolAdapter)} ");
throw new Exception($"{nameof(TableSessionPoolAdapter)} table模型数据入库没有成功返回结果为{result}");
}
//await CloseAsync();
return result;
}
@ -63,7 +82,10 @@ namespace JiShe.CollectBus.IoTDB.Provider
/// <returns></returns>
public async Task<SessionDataSet> ExecuteQueryStatementAsync(string sql)
{
return await _sessionPool.ExecuteQueryStatementAsync(sql);
var result = await _sessionPool.ExecuteQueryStatementAsync(sql,_options.Timeout);
//await result.Close();
//await CloseAsync();
return result;
}
public void Dispose()

View File

@ -3,6 +3,7 @@ using JiShe.CollectBus.Common.Enums;
using JiShe.CollectBus.Common.Models;
using JiShe.CollectBus.IotSystems.MessageReceiveds;
using JiShe.CollectBus.Kafka.Attributes;
using JiShe.CollectBus.Kafka.Internal;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
@ -16,53 +17,54 @@ namespace JiShe.CollectBus.Kafka.Test
{
public class KafkaSubscribeTest: IKafkaSubscribe
{
[KafkaSubscribe(ProtocolConst.TESTTOPIC, EnableBatch=false,BatchSize=1000)]
[KafkaSubscribe(ProtocolConst.TESTTOPIC, EnableBatch = false, BatchSize = 10)]
public async Task<ISubscribeAck> KafkaSubscribeAsync(object obj)
public async Task<ISubscribeAck> KafkaSubscribeAsync(TestTopic obj)
//public async Task<ISubscribeAck> KafkaSubscribeAsync(IEnumerable<int> obj)
{
Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(obj)}");
return SubscribeAck.Success();
}
[KafkaSubscribe(ProtocolConst.SubscriberLoginIssuedEventName)]
//[CapSubscribe(ProtocolConst.SubscriberLoginIssuedEventName)]
public async Task<ISubscribeAck> LoginIssuedEvent(IssuedEventMessage issuedEventMessage)
{
Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(issuedEventMessage)}");
return SubscribeAck.Success();
}
//[KafkaSubscribe(ProtocolConst.SubscriberLoginIssuedEventName)]
////[CapSubscribe(ProtocolConst.SubscriberLoginIssuedEventName)]
//public async Task<ISubscribeAck> LoginIssuedEvent(IssuedEventMessage issuedEventMessage)
//{
// Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(issuedEventMessage)}");
// return SubscribeAck.Success();
//}
[KafkaSubscribe(ProtocolConst.SubscriberHeartbeatIssuedEventName)]
//[CapSubscribe(ProtocolConst.SubscriberHeartbeatIssuedEventName)]
public async Task<ISubscribeAck> HeartbeatIssuedEvent(IssuedEventMessage issuedEventMessage)
{
Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(issuedEventMessage)}");
return SubscribeAck.Success();
}
//[KafkaSubscribe(ProtocolConst.SubscriberHeartbeatIssuedEventName)]
////[CapSubscribe(ProtocolConst.SubscriberHeartbeatIssuedEventName)]
//public async Task<ISubscribeAck> HeartbeatIssuedEvent(IssuedEventMessage issuedEventMessage)
//{
// Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(issuedEventMessage)}");
// return SubscribeAck.Success();
//}
[KafkaSubscribe(ProtocolConst.SubscriberReceivedEventName)]
//[CapSubscribe(ProtocolConst.SubscriberReceivedEventName)]
public async Task<ISubscribeAck> ReceivedEvent(MessageReceived receivedMessage)
{
Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(receivedMessage)}");
return SubscribeAck.Success();
}
//[KafkaSubscribe(ProtocolConst.SubscriberReceivedEventName)]
////[CapSubscribe(ProtocolConst.SubscriberReceivedEventName)]
//public async Task<ISubscribeAck> ReceivedEvent(MessageReceived receivedMessage)
//{
// Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(receivedMessage)}");
// return SubscribeAck.Success();
//}
[KafkaSubscribe(ProtocolConst.SubscriberHeartbeatReceivedEventName)]
//[CapSubscribe(ProtocolConst.SubscriberHeartbeatReceivedEventName)]
public async Task<ISubscribeAck> ReceivedHeartbeatEvent(MessageReceivedHeartbeat receivedHeartbeatMessage)
{
Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(receivedHeartbeatMessage)}");
return SubscribeAck.Success();
}
//[KafkaSubscribe(ProtocolConst.SubscriberHeartbeatReceivedEventName)]
////[CapSubscribe(ProtocolConst.SubscriberHeartbeatReceivedEventName)]
//public async Task<ISubscribeAck> ReceivedHeartbeatEvent(MessageReceivedHeartbeat receivedHeartbeatMessage)
//{
// Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(receivedHeartbeatMessage)}");
// return SubscribeAck.Success();
//}
[KafkaSubscribe(ProtocolConst.SubscriberLoginReceivedEventName)]
//[CapSubscribe(ProtocolConst.SubscriberLoginReceivedEventName)]
public async Task<ISubscribeAck> ReceivedLoginEvent(MessageReceivedLogin receivedLoginMessage)
{
Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(receivedLoginMessage)}");
return SubscribeAck.Success();
}
//[KafkaSubscribe(ProtocolConst.SubscriberLoginReceivedEventName)]
////[CapSubscribe(ProtocolConst.SubscriberLoginReceivedEventName)]
//public async Task<ISubscribeAck> ReceivedLoginEvent(MessageReceivedLogin receivedLoginMessage)
//{
// Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(receivedLoginMessage)}");
// return SubscribeAck.Success();
//}
}
}

View File

@ -7,6 +7,7 @@ using JiShe.CollectBus.Common.Consts;
using JiShe.CollectBus.Kafka;
using JiShe.CollectBus.Kafka.AdminClient;
using JiShe.CollectBus.Kafka.Consumer;
using JiShe.CollectBus.Kafka.Internal;
using JiShe.CollectBus.Kafka.Producer;
using JiShe.CollectBus.Kafka.Test;
using Microsoft.AspNetCore.Builder;
@ -62,6 +63,7 @@ var host = Host.CreateDefaultBuilder(args)
services.AddSingleton<IAdminClientService, AdminClientService>();
services.AddSingleton<IProducerService, ProducerService>();
services.AddSingleton<IConsumerService, ConsumerService>();
services.AddSingleton<KafkaPollyPipeline>();
services.AddTransient<KafkaSubscribeTest>();
})
@ -86,12 +88,13 @@ var logger = loggerFactory.CreateLogger<Program>();
logger.LogInformation("程序启动");
var adminClientService = host.Services.GetRequiredService<IAdminClientService>();
var configuration = host.Services.GetRequiredService<IConfiguration>();
string topic = "test-topic";
string topic = ProtocolConst.TESTTOPIC;
//await adminClientService.DeleteTopicAsync(topic);
// 创建 topic
//await adminClientService.CreateTopicAsync(topic, configuration.GetValue<int>(CommonConst.NumPartitions), 3);
var consumerService = host.Services.GetRequiredService<IConsumerService>();
var producerService = host.Services.GetRequiredService<IProducerService>();
//var kafkaOptions = host.Services.GetRequiredService<IOptions<KafkaOptionConfig>>();
//await consumerService.SubscribeAsync<object>(topic, (message) =>
//{
@ -133,23 +136,29 @@ var consumerService = host.Services.GetRequiredService<IConsumerService>();
//stopwatch.Stop();
//Console.WriteLine($"耗时: {stopwatch.ElapsedMilliseconds} 毫秒,{stopwatch.ElapsedMilliseconds/1000} 秒");
var producerService = host.Services.GetRequiredService<IProducerService>();
//int num = 840;
//while (num <= 900)
int num = 1;
while (num <= 6)
{
await producerService.ProduceAsync<TestTopic>(topic, new TestTopic { Topic = topic, Val = num });
num++;
}
//int num = 2;
//while (num <= 4)
//{
// await producerService.ProduceAsync<string>(topic, num.ToString());
// num++;
//}
//await Task.Factory.StartNew(async() => {
// int num = 0;
// while (true)
// {
// //await producerService.ProduceAsync(topic, new TestTopic { Topic = topic, Val = i });
// await producerService.ProduceAsync<string>(topic, num.ToString());
// num++;
// }
await Task.Factory.StartNew(async() => {
int num = 0;
while (true)
{
//await producerService.ProduceAsync(topic, new TestTopic { Topic = topic, Val = i });
await producerService.ProduceAsync<string>(topic, num.ToString());
num++;
}
});
//});
Console.WriteLine("\n按Esc键退出");
while (true)
{

View File

@ -91,8 +91,8 @@
"SaslUserName": "lixiao",
"SaslPassword": "lixiao1980",
"KafkaReplicationFactor": 3,
"NumPartitions": 1,
"ServerTagName": "JiSheCollectBus2"
"NumPartitions": 30,
"ServerTagName": "JiSheCollectBus99"
//"Topic": {
// "ReplicationFactor": 3,
// "NumPartitions": 1000

View File

@ -1,30 +1,24 @@
using Confluent.Kafka;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Confluent.Kafka.Admin;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
namespace JiShe.CollectBus.Kafka.AdminClient
{
namespace JiShe.CollectBus.Kafka.AdminClient;
public class AdminClientService : IAdminClientService, IDisposable, ISingletonDependency
{
private readonly ILogger<AdminClientService> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="AdminClientService" /> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="logger">The logger.</param>
/// <param name="configuration"></param>
/// <param name="logger"></param>
public AdminClientService(IConfiguration configuration, ILogger<AdminClientService> logger)
{
_logger = logger;
GetInstance(configuration);
Instance = GetInstance(configuration);
}
/// <summary>
@ -33,70 +27,17 @@ namespace JiShe.CollectBus.Kafka.AdminClient
/// <value>
/// The instance.
/// </value>
public IAdminClient Instance { get; set; } = default;
public IAdminClient Instance { get; set; }
/// <summary>
/// Gets the instance.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <returns></returns>
public IAdminClient GetInstance(IConfiguration configuration)
{
ArgumentNullException.ThrowIfNullOrWhiteSpace(configuration["Kafka:EnableAuthorization"]);
var enableAuthorization = bool.Parse(configuration["Kafka:EnableAuthorization"]!);
var adminClientConfig = new AdminClientConfig()
{
BootstrapServers = configuration["Kafka:BootstrapServers"],
};
if (enableAuthorization)
{
adminClientConfig.SecurityProtocol = SecurityProtocol.SaslPlaintext;
adminClientConfig.SaslMechanism = SaslMechanism.Plain;
adminClientConfig.SaslUsername = configuration["Kafka:SaslUserName"];
adminClientConfig.SaslPassword = configuration["Kafka:SaslPassword"];
}
Instance = new AdminClientBuilder(adminClientConfig).Build();
return Instance;
}
/// <summary>
/// Checks the topic asynchronous.
/// </summary>
/// <param name="topic">The topic.</param>
/// <returns></returns>
public async Task<bool> CheckTopicAsync(string topic)
{
var metadata = Instance.GetMetadata(TimeSpan.FromSeconds(5));
return await Task.FromResult(metadata.Topics.Exists(a => a.Topic == topic));
}
/// <summary>
/// 判断Kafka主题是否存在
/// </summary>
/// <param name="topic">主题名称</param>
/// <param name="numPartitions">副本数量不能高于Brokers数量</param>
/// <returns></returns>
public async Task<bool> CheckTopicAsync(string topic,int numPartitions)
{
var metadata = Instance.GetMetadata(TimeSpan.FromSeconds(5));
if(numPartitions > metadata.Brokers.Count)
{
throw new Exception($"{nameof(CheckTopicAsync)} 主题检查时,副本数量大于了节点数量。") ;
}
return await Task.FromResult(metadata.Topics.Exists(a => a.Topic == topic));
}
//// <summary>
/// 创建Kafka主题
/// </summary>
/// <param name="topic">主题名称</param>
/// <param name="numPartitions">主题分区数量</param>
/// <param name="replicationFactor">副本数量不能高于Brokers数量</param>
/// <param name="topic"></param>
/// <param name="numPartitions"></param>
/// <param name="replicationFactor"></param>
/// <returns></returns>
public async Task CreateTopicAsync(string topic, int numPartitions, short replicationFactor)
{
try
{
if (await CheckTopicAsync(topic)) return;
@ -114,10 +55,7 @@ namespace JiShe.CollectBus.Kafka.AdminClient
}
catch (CreateTopicsException e)
{
if (e.Results[0].Error.Code != ErrorCode.TopicAlreadyExists)
{
throw;
}
if (e.Results[0].Error.Code != ErrorCode.TopicAlreadyExists) throw;
}
}
@ -200,5 +138,53 @@ namespace JiShe.CollectBus.Kafka.AdminClient
{
Instance?.Dispose();
}
/// <summary>
/// Gets the instance.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <returns></returns>
public IAdminClient GetInstance(IConfiguration configuration)
{
ArgumentException.ThrowIfNullOrWhiteSpace(configuration["Kafka:EnableAuthorization"]);
var enableAuthorization = bool.Parse(configuration["Kafka:EnableAuthorization"]!);
var adminClientConfig = new AdminClientConfig
{
BootstrapServers = configuration["Kafka:BootstrapServers"]
};
if (enableAuthorization)
{
adminClientConfig.SecurityProtocol = SecurityProtocol.SaslPlaintext;
adminClientConfig.SaslMechanism = SaslMechanism.Plain;
adminClientConfig.SaslUsername = configuration["Kafka:SaslUserName"];
adminClientConfig.SaslPassword = configuration["Kafka:SaslPassword"];
}
return new AdminClientBuilder(adminClientConfig).Build();
}
/// <summary>
/// Checks the topic asynchronous.
/// </summary>
/// <param name="topic">The topic.</param>
/// <returns></returns>
public async Task<bool> CheckTopicAsync(string topic)
{
var metadata = Instance.GetMetadata(TimeSpan.FromSeconds(5));
return await Task.FromResult(metadata.Topics.Exists(a => a.Topic == topic));
}
/// <summary>
/// 判断Kafka主题是否存在
/// </summary>
/// <param name="topic">主题名称</param>
/// <param name="numPartitions">副本数量不能高于Brokers数量</param>
/// <returns></returns>
public async Task<bool> CheckTopicAsync(string topic, int numPartitions)
{
var metadata = Instance.GetMetadata(TimeSpan.FromSeconds(5));
if (numPartitions > metadata.Brokers.Count)
throw new Exception($"{nameof(CheckTopicAsync)} 主题检查时,副本数量大于了节点数量。");
return await Task.FromResult(metadata.Topics.Exists(a => a.Topic == topic));
}
}

View File

@ -1,14 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace JiShe.CollectBus.Kafka.Attributes;
namespace JiShe.CollectBus.Kafka.Attributes
{
[AttributeUsage(AttributeTargets.Method)]
public class KafkaSubscribeAttribute : Attribute
{
/// <summary>
/// 订阅主题
/// </summary>
/// <param name="batchTimeout"></param>
public KafkaSubscribeAttribute(string topic)
{
Topic = topic;
}
/// <summary>
/// 订阅主题
/// </summary>
public KafkaSubscribeAttribute(string topic, int partition)
{
Topic = topic;
Partition = partition;
}
/// <summary>
/// 订阅的主题
/// </summary>
@ -22,7 +34,7 @@ namespace JiShe.CollectBus.Kafka.Attributes
/// <summary>
/// 消费者组
/// </summary>
public string GroupId { get; set; } = "default";
public string? GroupId { get; set; } = null; //"default"
/// <summary>
/// 任务数(默认是多少个分区多少个任务)
@ -45,24 +57,4 @@ namespace JiShe.CollectBus.Kafka.Attributes
/// 格式:("00:05:00")
/// </summary>
public TimeSpan? BatchTimeout { get; set; } = null;
/// <summary>
/// 订阅主题
/// </summary>
/// <param name="batchTimeout"></param>
public KafkaSubscribeAttribute(string topic)
{
this.Topic = topic;
}
/// <summary>
/// 订阅主题
/// </summary>
public KafkaSubscribeAttribute(string topic, int partition)
{
this.Topic = topic;
this.Partition = partition;
}
}
}

View File

@ -1,11 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace JiShe.CollectBus.Kafka.Attributes;
namespace JiShe.CollectBus.Kafka.Attributes
{
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class TopicAttribute : Attribute
{
@ -26,4 +20,3 @@ namespace JiShe.CollectBus.Kafka.Attributes
/// </value>
public string Name { get; set; }
}
}

View File

@ -1,6 +1,7 @@
using Confluent.Kafka;
using JiShe.CollectBus.Common.Consts;
using JiShe.CollectBus.Kafka.Consumer;
using JiShe.CollectBus.Kafka.Internal;
using JiShe.CollectBus.Kafka.Producer;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
@ -40,13 +41,25 @@ namespace JiShe.CollectBus.Kafka
// 注册Consumer
context.Services.AddSingleton<IConsumerService, ConsumerService>();
// 注册Polly
context.Services.AddSingleton<KafkaPollyPipeline>();
//context.Services.AddHostedService<HostedService>();
}
/// <summary>
/// 在初始化之前,初始化Kafka Topic
/// </summary>
/// <param name="context"></param>
public override void OnPreApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
app.ApplicationServices.UseInitKafkaTopic();
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
// 注册Subscriber
app.ApplicationServices.UseKafkaSubscribe();

View File

@ -1,27 +1,51 @@
using Confluent.Kafka;
using JiShe.CollectBus.Common;
using JiShe.CollectBus.Common.Consts;
using JiShe.CollectBus.Kafka.Internal;
using JiShe.CollectBus.Kafka.Serialization;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
namespace JiShe.CollectBus.Kafka.Consumer
{
public class ConsumerService : IConsumerService, IDisposable
{
private readonly ILogger<ConsumerService> _logger;
private readonly IConfiguration _configuration;
private readonly ConcurrentDictionary<Type, (object Consumer, CancellationTokenSource CTS)>
/// <summary>
/// 消费者存储
/// Key 格式:{groupId}_{topic}_{TKey}_{TValue}
/// </summary>
private readonly ConcurrentDictionary<string, (object Consumer, CancellationTokenSource CTS)>
_consumerStore = new();
private readonly KafkaOptionConfig _kafkaOptionConfig;
private class KafkaConsumer<TKey, TValue> where TKey : notnull where TValue : class { }
public ConsumerService(IConfiguration configuration, ILogger<ConsumerService> logger, IOptions<KafkaOptionConfig> kafkaOptionConfig)
/// <summary>
/// 消费完或者无数据时的延迟时间
/// </summary>
private TimeSpan DelayTime => TimeSpan.FromMilliseconds(100);
private readonly KafkaOptionConfig _kafkaOptionConfig;
private readonly ServerApplicationOptions _applicationOptions;
private readonly KafkaPollyPipeline _kafkaPollyPipeline;
/// <summary>
/// ConsumerService
/// </summary>
/// <param name="logger"></param>
/// <param name="kafkaOptionConfig"></param>
public ConsumerService(ILogger<ConsumerService> logger, IOptions<KafkaOptionConfig> kafkaOptionConfig, KafkaPollyPipeline kafkaPollyPipeline, IOptions<ServerApplicationOptions> applicationOptions)
{
_configuration = configuration;
_logger = logger;
_kafkaOptionConfig = kafkaOptionConfig.Value;
_applicationOptions = applicationOptions.Value;
_kafkaPollyPipeline = kafkaPollyPipeline;
}
#region private
@ -47,11 +71,11 @@ namespace JiShe.CollectBus.Kafka.Consumer
var config = new ConsumerConfig
{
BootstrapServers = _kafkaOptionConfig.BootstrapServers,
GroupId = groupId ?? "default",
GroupId = groupId ?? _applicationOptions.ServerTagName,
AutoOffsetReset = AutoOffsetReset.Earliest,
EnableAutoCommit = false, // 禁止AutoCommit
EnablePartitionEof = true, // 启用分区末尾标记
AllowAutoCreateTopics = true, // 启用自动创建
//AllowAutoCreateTopics = true, // 启用自动创建
FetchMaxBytes = 1024 * 1024 * 50 // 增加拉取大小50MB
};
@ -103,18 +127,21 @@ namespace JiShe.CollectBus.Kafka.Consumer
/// <returns></returns>
public async Task SubscribeAsync<TKey, TValue>(string[] topics, Func<TKey, TValue, Task<bool>> messageHandler, string? groupId = null) where TKey : notnull where TValue : class
{
var consumerKey = typeof(KafkaConsumer<TKey, TValue>);
await _kafkaPollyPipeline.KafkaPipeline.ExecuteAsync(async token =>
{
var consumerKey = $"{groupId}_{string.Join("_", topics)}_{typeof(TKey).Name}_{typeof(TValue).Name}";
var cts = new CancellationTokenSource();
//var consumer = _consumerStore.GetOrAdd(consumerKey, _ =>
//(
// CreateConsumer<TKey, TValue>(groupId),
// cts
//)).Consumer as IConsumer<TKey, TValue>;
var consumer = CreateConsumer<TKey, TValue>(groupId);
var consumer = _consumerStore.GetOrAdd(consumerKey, _ =>
(
CreateConsumer<TKey, TValue>(groupId),
cts
)).Consumer as IConsumer<TKey, TValue>;
consumer!.Subscribe(topics);
await Task.Run(async () =>
_ = Task.Run(async () =>
{
while (!cts.IsCancellationRequested)
{
@ -124,38 +151,51 @@ namespace JiShe.CollectBus.Kafka.Consumer
var result = consumer.Consume(cts.Token);
if (result == null || result.Message == null || result.Message.Value == null)
{
await Task.Delay(DelayTime, cts.Token);
continue;
}
if (result.IsPartitionEOF)
{
#if DEBUG
_logger.LogInformation("Kafka消费: {Topic} 分区 {Partition} 已消费完", result.Topic, result.Partition);
await Task.Delay(TimeSpan.FromSeconds(1),cts.Token);
#endif
await Task.Delay(DelayTime, cts.Token);
continue;
}
if (_kafkaOptionConfig.EnableFilter)
{
var headersFilter = new HeadersFilter { { "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) } };
var headersFilter = new HeadersFilter { { "route-key", Encoding.UTF8.GetBytes(_applicationOptions.ServerTagName) } };
// 检查 Header 是否符合条件
if (!headersFilter.Match(result.Message.Headers))
{
//consumer.Commit(result); // 提交偏移量
consumer.Commit(result); // 提交偏移量
// 跳过消息
continue;
}
}
bool sucess = await messageHandler(result.Message.Key, result.Message.Value);
if (sucess)
{
consumer.Commit(result); // 手动提交
}
}
catch (ConsumeException ex)
catch (ConsumeException ex) when (KafkaPollyPipeline.IsRecoverableError(ex))
{
_logger.LogError(ex, $"{string.Join("", topics)}消息消费失败: {ex.Error.Reason}");
throw; // 抛出异常,以便重试
}
catch (OperationCanceledException)
{
//ignore
}
catch (Exception ex)
{
_logger.LogError(ex, "处理消息时发生未知错误");
}
}
});
}, cts.Token);
await Task.CompletedTask;
});
}
@ -163,27 +203,23 @@ namespace JiShe.CollectBus.Kafka.Consumer
/// <summary>
/// 订阅消息
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <param name="topics"></param>
/// <param name="messageHandler"></param>
/// <param name="groupId"></param>
/// <returns></returns>
public async Task SubscribeAsync<TValue>(string[] topics, Func<TValue, Task<bool>> messageHandler, string? groupId) where TValue : class
{
try {
var consumerKey = typeof(KafkaConsumer<Ignore, TValue>);
await _kafkaPollyPipeline.KafkaPipeline.ExecuteAsync(async token =>
{
var consumerKey = $"{groupId}_{string.Join("_", topics)}_{typeof(Ignore).Name}_{typeof(TValue).Name}";
var cts = new CancellationTokenSource();
//if (topics.Contains(ProtocolConst.SubscriberLoginReceivedEventName))
//{
// string ssss = "";
//}
//var consumer = _consumerStore.GetOrAdd(consumerKey, _ =>
//(
// CreateConsumer<string, TValue>(groupId),
// cts
//)).Consumer as IConsumer<string, TValue>;
var consumer = _consumerStore.GetOrAdd(consumerKey, _ =>
(
CreateConsumer<Ignore, TValue>(groupId),
cts
)).Consumer as IConsumer<Ignore, TValue>;
var consumer = CreateConsumer<Ignore, TValue>(groupId);
consumer!.Subscribe(topics);
_ = Task.Run(async () =>
@ -198,24 +234,25 @@ namespace JiShe.CollectBus.Kafka.Consumer
var result = consumer.Consume(cts.Token);
if (result == null || result.Message == null || result.Message.Value == null)
{
await Task.Delay(500, cts.Token);
await Task.Delay(DelayTime, cts.Token);
continue;
}
if (result.IsPartitionEOF)
{
#if DEBUG
_logger.LogInformation("Kafka消费: {Topic} 分区 {Partition} 已消费完", result.Topic, result.Partition);
await Task.Delay(100, cts.Token);
#endif
await Task.Delay(DelayTime, cts.Token);
continue;
}
if (_kafkaOptionConfig.EnableFilter)
{
var headersFilter = new HeadersFilter { { "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) } };
var headersFilter = new HeadersFilter { { "route-key", Encoding.UTF8.GetBytes(_applicationOptions.ServerTagName) } };
// 检查 Header 是否符合条件
if (!headersFilter.Match(result.Message.Headers))
{
await Task.Delay(500, cts.Token);
//consumer.Commit(result); // 提交偏移量
consumer.Commit(result); // 提交偏移量
// 跳过消息
continue;
}
@ -223,22 +260,26 @@ namespace JiShe.CollectBus.Kafka.Consumer
bool sucess = await messageHandler(result.Message.Value);
if (sucess)
consumer.Commit(result); // 手动提交
else
consumer.StoreOffset(result);
//else
// consumer.StoreOffset(result);
}
catch (ConsumeException ex)
catch (ConsumeException ex) when (KafkaPollyPipeline.IsRecoverableError(ex))
{
_logger.LogError(ex, $"{string.Join("", topics)}消息消费失败: {ex.Error.Reason}");
throw; // 抛出异常,以便重试
}
}
});
} catch (Exception ex)
catch (OperationCanceledException)
{
_logger.LogWarning($"Kafka消费异常: {ex.Message}");
//ignore
}
catch (Exception ex)
{
_logger.LogError(ex, "处理消息时发生未知错误");
}
}
}, cts.Token);
await Task.CompletedTask;
});
}
@ -252,7 +293,7 @@ namespace JiShe.CollectBus.Kafka.Consumer
/// <param name="groupId">消费组ID</param>
/// <param name="batchSize">批次大小</param>
/// <param name="batchTimeout">批次超时时间</param>
public async Task SubscribeBatchAsync<TKey, TValue>(string topic, Func<IEnumerable<TValue>, Task<bool>> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null) where TKey : notnull where TValue : class
public async Task SubscribeBatchAsync<TKey, TValue>(string topic, Func<List<TValue>, Task<bool>> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null) where TKey : notnull where TValue : class
{
await SubscribeBatchAsync<TKey, TValue>(new[] { topic }, messageBatchHandler, groupId, batchSize, batchTimeout);
}
@ -267,9 +308,12 @@ namespace JiShe.CollectBus.Kafka.Consumer
/// <param name="groupId">消费组ID</param>
/// <param name="batchSize">批次大小</param>
/// <param name="batchTimeout">批次超时时间</param>
public async Task SubscribeBatchAsync<TKey, TValue>(string[] topics,Func<IEnumerable<TValue>, Task<bool>> messageBatchHandler, string? groupId = null,int batchSize = 100, TimeSpan? batchTimeout = null) where TKey : notnull where TValue : class
public async Task SubscribeBatchAsync<TKey, TValue>(string[] topics, Func<List<TValue>, Task<bool>> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null) where TKey : notnull where TValue : class
{
var consumerKey = typeof(KafkaConsumer<TKey, TValue>);
await _kafkaPollyPipeline.KafkaPipeline.ExecuteAsync(async token =>
{
var consumerKey = $"{groupId}_{string.Join("_", topics)}_{typeof(TKey).Name}_{typeof(TValue).Name}";
var cts = new CancellationTokenSource();
var consumer = _consumerStore.GetOrAdd(consumerKey, _ =>
@ -277,7 +321,6 @@ namespace JiShe.CollectBus.Kafka.Consumer
CreateConsumer<TKey, TValue>(groupId),
cts
)).Consumer as IConsumer<TKey, TValue>;
consumer!.Subscribe(topics);
var timeout = batchTimeout ?? TimeSpan.FromSeconds(5); // 默认超时时间调整为5秒
@ -300,37 +343,38 @@ namespace JiShe.CollectBus.Kafka.Consumer
{
if (result.IsPartitionEOF)
{
#if DEBUG
_logger.LogInformation("Kafka消费: {Topic} 分区 {Partition} 已消费完", result.Topic, result.Partition);
await Task.Delay(TimeSpan.FromSeconds(1), cts.Token);
#endif
await Task.Delay(DelayTime, cts.Token);
}
else if (result.Message.Value != null)
{
if (_kafkaOptionConfig.EnableFilter)
{
var headersFilter = new HeadersFilter { { "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) } };
var headersFilter = new HeadersFilter { { "route-key", Encoding.UTF8.GetBytes(_applicationOptions.ServerTagName) } };
// 检查 Header 是否符合条件
if (!headersFilter.Match(result.Message.Headers))
{
//consumer.Commit(result); // 提交偏移量
consumer.Commit(result); // 提交偏移量
// 跳过消息
continue;
}
}
messages.Add((result.Message.Value, result.TopicPartitionOffset));
//messages.Add(result.Message.Value);
}
}
else
{
// 无消息时短暂等待
await Task.Delay(10, cts.Token);
await Task.Delay(DelayTime, cts.Token);
}
}
// 处理批次
if (messages.Count > 0)
{
bool success = await messageBatchHandler(messages.Select(m => m.Value));
bool success = await messageBatchHandler(messages.Select(m => m.Value).ToList());
if (success)
{
var offsetsByPartition = new Dictionary<TopicPartition, long>();
@ -354,13 +398,14 @@ namespace JiShe.CollectBus.Kafka.Consumer
startTime = DateTime.UtcNow;
}
catch (ConsumeException ex)
catch (ConsumeException ex) when (KafkaPollyPipeline.IsRecoverableError(ex))
{
_logger.LogError(ex, $"{string.Join("", topics)} 消息消费失败: {ex.Error.Reason}");
throw; // 抛出异常,以便重试
}
catch (OperationCanceledException)
{
// 任务取消,正常退出
//ignore
}
catch (Exception ex)
{
@ -370,6 +415,7 @@ namespace JiShe.CollectBus.Kafka.Consumer
}, cts.Token);
await Task.CompletedTask;
});
}
@ -383,9 +429,9 @@ namespace JiShe.CollectBus.Kafka.Consumer
/// <param name="batchSize">批次大小</param>
/// <param name="batchTimeout">批次超时时间</param>
/// <param name="consumeTimeout">消费等待时间</param>
public async Task SubscribeBatchAsync<TValue>(string topic, Func<IEnumerable<TValue>, Task<bool>> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null, TimeSpan? consumeTimeout = null) where TValue : class
public async Task SubscribeBatchAsync<TValue>(string topic, Func<List<TValue>, Task<bool>> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null, TimeSpan? consumeTimeout = null) where TValue : class
{
await SubscribeBatchAsync<TValue>(new[] { topic }, messageBatchHandler, groupId, batchSize, batchTimeout, consumeTimeout);
await SubscribeBatchAsync(new[] { topic }, messageBatchHandler, groupId, batchSize, batchTimeout, consumeTimeout);
}
@ -400,16 +446,19 @@ namespace JiShe.CollectBus.Kafka.Consumer
/// <param name="batchSize">批次大小</param>
/// <param name="batchTimeout">批次超时时间</param>
/// <param name="consumeTimeout">消费等待时间</param>
public async Task SubscribeBatchAsync<TValue>(string[] topics,Func<IEnumerable<TValue>, Task<bool>> messageBatchHandler, string? groupId = null, int batchSize = 100,TimeSpan? batchTimeout = null,TimeSpan? consumeTimeout = null)where TValue : class
public async Task SubscribeBatchAsync<TValue>(string[] topics, Func<List<TValue>, Task<bool>> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null, TimeSpan? consumeTimeout = null) where TValue : class
{
var consumerKey = typeof(KafkaConsumer<string, TValue>);
await _kafkaPollyPipeline.KafkaPipeline.ExecuteAsync(async token =>
{
var consumerKey = $"{groupId}_{string.Join("_", topics)}_{typeof(Ignore).Name}_{typeof(TValue).Name}";
var cts = new CancellationTokenSource();
var consumer = _consumerStore.GetOrAdd(consumerKey, _ =>
(
CreateConsumer<string, TValue>(groupId),
CreateConsumer<Ignore, TValue>(groupId),
cts
)).Consumer as IConsumer<string, TValue>;
)).Consumer as IConsumer<Ignore, TValue>;
consumer!.Subscribe(topics);
@ -418,7 +467,6 @@ namespace JiShe.CollectBus.Kafka.Consumer
_ = Task.Run(async () =>
{
var messages = new List<(TValue Value, TopicPartitionOffset Offset)>();
//var messages = new List<ConsumeResult<TKey, TValue>>();
var startTime = DateTime.UtcNow;
while (!cts.IsCancellationRequested)
@ -434,37 +482,36 @@ namespace JiShe.CollectBus.Kafka.Consumer
{
if (result.IsPartitionEOF)
{
_logger.LogInformation("Kafka消费: {Topic} 分区 {Partition} 已消费完", result.Topic, result.Partition);
await Task.Delay(TimeSpan.FromSeconds(1), cts.Token);
//_logger.LogInformation("Kafka消费: {Topic} 分区 {Partition} 已消费完", result.Topic, result.Partition);
await Task.Delay(DelayTime, cts.Token);
}
else if (result.Message.Value != null)
{
if (_kafkaOptionConfig.EnableFilter)
{
var headersFilter = new HeadersFilter { { "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) } };
var headersFilter = new HeadersFilter { { "route-key", Encoding.UTF8.GetBytes(_applicationOptions.ServerTagName) } };
// 检查 Header 是否符合条件
if (!headersFilter.Match(result.Message.Headers))
{
//consumer.Commit(result); // 提交偏移量
consumer.Commit(result); // 提交偏移量
// 跳过消息
continue;
}
}
messages.Add((result.Message.Value, result.TopicPartitionOffset));
//messages.Add(result.Message.Value);
}
}
else
{
// 无消息时短暂等待
await Task.Delay(10, cts.Token);
await Task.Delay(DelayTime, cts.Token);
}
}
// 处理批次
if (messages.Count > 0)
{
bool success = await messageBatchHandler(messages.Select(m => m.Value));
bool success = await messageBatchHandler(messages.Select(m => m.Value).ToList());
if (success)
{
var offsetsByPartition = new Dictionary<TopicPartition, long>();
@ -488,13 +535,14 @@ namespace JiShe.CollectBus.Kafka.Consumer
startTime = DateTime.UtcNow;
}
catch (ConsumeException ex)
catch (ConsumeException ex) when (KafkaPollyPipeline.IsRecoverableError(ex))
{
_logger.LogError(ex, $"消息消费失败: {ex.Error.Reason}");
throw; // 抛出异常,以便重试
}
catch (OperationCanceledException)
{
// 任务取消,正常退出
//ignore
}
catch (Exception ex)
{
@ -504,6 +552,7 @@ namespace JiShe.CollectBus.Kafka.Consumer
}, cts.Token);
await Task.CompletedTask;
});
}
@ -512,9 +561,9 @@ namespace JiShe.CollectBus.Kafka.Consumer
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
public void Unsubscribe<TKey, TValue>() where TKey : notnull where TValue : class
public void Unsubscribe<TKey, TValue>(string[] topics, string? groupId) where TKey : notnull where TValue : class
{
var consumerKey = typeof((TKey, TValue));
var consumerKey = $"{groupId}_{string.Join("_", topics)}_{typeof(TKey).Name}_{typeof(TValue).Name}";
if (_consumerStore.TryRemove(consumerKey, out var entry))
{
entry.CTS.Cancel();

View File

@ -1,15 +1,9 @@
using Confluent.Kafka;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace JiShe.CollectBus.Kafka.Consumer;
namespace JiShe.CollectBus.Kafka.Consumer
{
public interface IConsumerService
{
Task SubscribeAsync<TKey, TValue>(string topic, Func<TKey, TValue, Task<bool>> messageHandler, string? groupId=null) where TKey : notnull where TValue : class;
Task SubscribeAsync<TKey, TValue>(string topic, Func<TKey, TValue, Task<bool>> messageHandler,
string? groupId = null) where TKey : notnull where TValue : class;
/// <summary>
/// 订阅消息
@ -18,9 +12,11 @@ namespace JiShe.CollectBus.Kafka.Consumer
/// <param name="topic"></param>
/// <param name="messageHandler"></param>
/// <returns></returns>
Task SubscribeAsync<TValue>(string topic, Func<TValue, Task<bool>> messageHandler, string? groupId = null) where TValue : class;
Task SubscribeAsync<TValue>(string topic, Func<TValue, Task<bool>> messageHandler, string? groupId = null)
where TValue : class;
Task SubscribeAsync<TKey, TValue>(string[] topics, Func<TKey, TValue, Task<bool>> messageHandler, string? groupId) where TKey : notnull where TValue : class;
Task SubscribeAsync<TKey, TValue>(string[] topics, Func<TKey, TValue, Task<bool>> messageHandler, string? groupId)
where TKey : notnull where TValue : class;
/// <summary>
@ -31,16 +27,24 @@ namespace JiShe.CollectBus.Kafka.Consumer
/// <param name="topics"></param>
/// <param name="messageHandler"></param>
/// <returns></returns>
Task SubscribeAsync<TValue>(string[] topics, Func<TValue, Task<bool>> messageHandler, string? groupId = null) where TValue : class;
Task SubscribeAsync<TValue>(string[] topics, Func<TValue, Task<bool>> messageHandler, string? groupId = null)
where TValue : class;
Task SubscribeBatchAsync<TKey, TValue>(string[] topics, Func<IEnumerable<TValue>, Task<bool>> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null) where TKey : notnull where TValue : class;
Task SubscribeBatchAsync<TKey, TValue>(string[] topics, Func<List<TValue>, Task<bool>> messageBatchHandler,
string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null)
where TKey : notnull where TValue : class;
Task SubscribeBatchAsync<TKey, TValue>(string topic, Func<IEnumerable<TValue>, Task<bool>> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null) where TKey : notnull where TValue : class;
Task SubscribeBatchAsync<TKey, TValue>(string topic, Func<List<TValue>, Task<bool>> messageBatchHandler,
string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null)
where TKey : notnull where TValue : class;
Task SubscribeBatchAsync<TValue>(string topic, Func<IEnumerable<TValue>, Task<bool>> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null, TimeSpan? consumeTimeout = null) where TValue : class;
Task SubscribeBatchAsync<TValue>(string topic, Func<List<TValue>, Task<bool>> messageBatchHandler,
string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null, TimeSpan? consumeTimeout = null)
where TValue : class;
Task SubscribeBatchAsync<TValue>(string[] topics, Func<IEnumerable<TValue>, Task<bool>> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null, TimeSpan? consumeTimeout = null) where TValue : class;
Task SubscribeBatchAsync<TValue>(string[] topics, Func<List<TValue>, Task<bool>> messageBatchHandler,
string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null, TimeSpan? consumeTimeout = null)
where TValue : class;
void Unsubscribe<TKey, TValue>() where TKey : notnull where TValue : class;
}
void Unsubscribe<TKey, TValue>(string[] topics, string groupId) where TKey : notnull where TValue : class;
}

View File

@ -1,30 +0,0 @@
using Confluent.Kafka;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace JiShe.CollectBus.Kafka
{
/// <summary>
/// 消息头过滤器
/// </summary>
public class HeadersFilter : Dictionary<string, byte[]>
{
/// <summary>
/// 判断Headers是否匹配
/// </summary>
/// <param name="headers"></param>
/// <returns></returns>
public bool Match(Headers headers)
{
foreach (var kvp in this)
{
if (!headers.TryGetLastBytes(kvp.Key, out var value) || !value.SequenceEqual(kvp.Value))
return false;
}
return true;
}
}
}

View File

@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace JiShe.CollectBus.Kafka
{
/// <summary>
/// Kafka订阅者
/// <para>
/// 订阅者需要继承此接口并需要依赖注入,并使用<see cref="KafkaSubscribeAttribute"/>标记
/// </para>
/// </summary>
public interface IKafkaSubscribe
{
}
}

View File

@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace JiShe.CollectBus.Kafka
{
public interface ISubscribeAck
{
/// <summary>
/// 是否成功标记
/// </summary>
bool Ack { get; set; }
/// <summary>
/// 消息
/// </summary>
string? Msg { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using Confluent.Kafka;
namespace JiShe.CollectBus.Kafka.Internal;
/// <summary>
/// 消息头过滤器
/// </summary>
public class HeadersFilter : Dictionary<string, byte[]>
{
/// <summary>
/// 判断Headers是否匹配
/// </summary>
/// <param name="headers"></param>
/// <returns></returns>
public bool Match(Headers headers)
{
foreach (var kvp in this)
if (!headers.TryGetLastBytes(kvp.Key, out var value) || !value.SequenceEqual(kvp.Value))
return false;
return true;
}
}

View File

@ -0,0 +1,11 @@
namespace JiShe.CollectBus.Kafka.Internal;
/// <summary>
/// Kafka订阅者
/// <para>
/// 订阅者需要继承此接口并需要依赖注入,并使用<see cref="KafkaSubscribeAttribute" />标记
/// </para>
/// </summary>
public interface IKafkaSubscribe
{
}

View File

@ -0,0 +1,14 @@
namespace JiShe.CollectBus.Kafka.Internal;
public interface ISubscribeAck
{
/// <summary>
/// 是否成功标记
/// </summary>
bool Ack { get; set; }
/// <summary>
/// 消息
/// </summary>
string? Msg { get; set; }
}

View File

@ -0,0 +1,52 @@
using Confluent.Kafka;
namespace JiShe.CollectBus.Kafka.Internal;
public class KafkaOptionConfig
{
/// <summary>
/// kafka地址
/// </summary>
public string BootstrapServers { get; set; } = null!;
/// <summary>
/// kafka主题副本数量
/// </summary>
public short KafkaReplicationFactor { get; set; }
/// <summary>
/// kafka主题分区数量
/// </summary>
public int NumPartitions { get; set; }
/// <summary>
/// 是否开启过滤器
/// </summary>
public bool EnableFilter { get; set; } = true;
/// <summary>
/// 是否开启认证
/// </summary>
public bool EnableAuthorization { get; set; } = false;
/// <summary>
/// 安全协议
/// </summary>
public SecurityProtocol SecurityProtocol { get; set; } = SecurityProtocol.SaslPlaintext;
/// <summary>
/// 认证方式
/// </summary>
public SaslMechanism SaslMechanism { get; set; } = SaslMechanism.Plain;
/// <summary>
/// 用户名
/// </summary>
public string? SaslUserName { get; set; }
/// <summary>
/// 密码
/// </summary>
public string? SaslPassword { get; set; }
}

View File

@ -0,0 +1,111 @@
using Confluent.Kafka;
using Polly.CircuitBreaker;
using Polly.Retry;
using Polly;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Polly.Contrib.WaitAndRetry;
using Volo.Abp.DependencyInjection;
using Microsoft.Extensions.Logging;
using JiShe.CollectBus.Kafka.Producer;
namespace JiShe.CollectBus.Kafka.Internal
{
public class KafkaPollyPipeline
{
private readonly ILogger<KafkaPollyPipeline> _logger;
public KafkaPollyPipeline(ILogger<KafkaPollyPipeline> logger)
{
_logger= logger;
}
/// <summary>
/// 判断是否可恢复的异常
/// </summary>
/// <param name="ex"></param>
/// <returns></returns>
public static bool IsRecoverableError(Exception ex)
{
var errorList= new List<ErrorCode>
{
ErrorCode.GroupLoadInProgress,
ErrorCode.Local_Retry,
ErrorCode.Local_MaxPollExceeded,
ErrorCode.RequestTimedOut,
ErrorCode.LeaderNotAvailable,
ErrorCode.NotLeaderForPartition,
ErrorCode.RebalanceInProgress,
ErrorCode.NotCoordinatorForGroup,
ErrorCode.NetworkException,
ErrorCode.GroupCoordinatorNotAvailable
};
return ex switch
{
ConsumeException kafkaEx => errorList.Contains(kafkaEx.Error.Code),
KafkaException kafkaEx =>kafkaEx.Error.IsFatal && errorList.Contains(kafkaEx.Error.Code),
_ => false
};
}
/// <summary>
/// 创建重试 + 断路器
/// </summary>
/// <returns></returns>
public ResiliencePipeline KafkaPipeline
{
get
{
// 组合重试 + 断路器
ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = args => args.Outcome.Exception switch
{
not null when IsRecoverableError(args.Outcome.Exception) =>
PredicateResult.True(),
_ => PredicateResult.False()
},
Delay = TimeSpan.FromSeconds(2),
OnRetry = args =>
{
_logger.LogWarning($"重试中... 第 {args.AttemptNumber} 次,原因: {args.Outcome.Exception?.Message}");
return default;
}
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
ShouldHandle = args => args.Outcome.Exception switch
{
not null when IsRecoverableError(args.Outcome.Exception) =>
PredicateResult.True(),
_ => PredicateResult.False()
},
FailureRatio = 0.8, // 80% 失败触发熔断
SamplingDuration = TimeSpan.FromSeconds(10),
MinimumThroughput = 4, // 至少4次调用才计算失败率
BreakDuration = TimeSpan.FromSeconds(10),
OnOpened = args =>
{
_logger.LogWarning($"熔断器开启,等待 {args.BreakDuration} 后重试");
return default;
},
OnClosed = _ =>
{
_logger.LogWarning("熔断器关闭,再次开始重试");
return default;
}
})
.Build();
return pipeline;
}
}
}
}

View File

@ -0,0 +1,103 @@
using System.Collections;
using System.Reflection;
namespace JiShe.CollectBus.Kafka.Internal;
/// <summary>
/// 反射辅助类
/// </summary>
public static class ReflectionHelper
{
/// <summary>
/// 集合类型
/// Item1参数类型
/// Item2集合元素类型
/// </summary>
public static Tuple<Type, Type?> GetParameterTypeInfo(this MethodInfo method, int parameterIndex = 0)
{
// 参数校验
if (method == null) throw new ArgumentNullException(nameof(method));
var parameters = method.GetParameters();
if (parameterIndex < 0 || parameterIndex >= parameters.Length)
throw new ArgumentOutOfRangeException(nameof(parameterIndex));
var param = parameters[parameterIndex];
var paramType = param.ParameterType;
Type? elementType = null;
// 判断是否是集合类型(排除字符串)
if (paramType != typeof(string) && IsEnumerableType(paramType))
elementType = GetEnumerableElementType(paramType);
return Tuple.Create(paramType, elementType);
}
/// <summary>
/// 判断是否是集合类型(排除字符串)
/// </summary>
public static bool IsEnumerableType(this Type type)
{
return type.IsArray
|| (type.IsGenericType && type.GetInterfaces()
.Any(t => t.IsGenericType
&& t.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
|| type.GetInterfaces().Any(t => t == typeof(IEnumerable));
}
/// <summary>
/// 获取集合元素的类型
/// </summary>
public static Type? GetEnumerableElementType(this Type type)
{
// 处理数组类型
if (type.IsArray)
return type.GetElementType();
// 处理直接实现IEnumerable<T>的类型如IEnumerable<int>本身)
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
return type.GetGenericArguments()[0];
// 处理通过接口实现IEnumerable<T>的泛型集合如List<T>
var genericEnumerable = type.GetInterfaces()
.FirstOrDefault(t => t.IsGenericType
&& t.GetGenericTypeDefinition() == typeof(IEnumerable<>));
if (genericEnumerable != null)
return genericEnumerable.GetGenericArguments()[0];
// 处理非泛型集合类型(如 ArrayList
if (typeof(IEnumerable).IsAssignableFrom(type) && type == typeof(ArrayList))
return typeof(ArrayList);
// 返回null表示无法确定元素类型
return null;
}
/// <summary>
/// 判断是否使用强转换
/// </summary>
/// <param name="targetType"></param>
/// <returns></returns>
public static bool IsConvertType(this Type targetType)
{
// 处理可空类型
var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
// 情况1值类型或基元类型如 int、DateTime
if (underlyingType.IsValueType || underlyingType.IsPrimitive)
return true;
// 情况2字符串类型直接赋值
if (underlyingType == typeof(string))
return true;
// 情况3枚举类型处理
//else if (underlyingType.IsEnum)
//{
// if (Enum.IsDefined(underlyingType, msg))
// {
// convertedValue = Enum.Parse(underlyingType, msg.ToString());
// return true;
// }
// return false;
//}
return false;
}
}

View File

@ -0,0 +1,62 @@
namespace JiShe.CollectBus.Kafka.Internal;
public class SubscribeResult : ISubscribeAck
{
/// <summary>
/// 是否成功
/// </summary>
public bool Ack { get; set; }
/// <summary>
/// 消息
/// </summary>
public string? Msg { get; set; }
/// <summary>
/// 成功
/// </summary>
/// <param name="msg">消息</param>
public SubscribeResult Success(string? msg = null)
{
Ack = true;
Msg = msg;
return this;
}
/// <summary>
/// 失败
/// </summary>
/// <param name="msg"></param>
/// <returns></returns>
public SubscribeResult Fail(string? msg = null)
{
Msg = msg;
Ack = false;
return this;
}
}
public static class SubscribeAck
{
/// <summary>
/// 成功
/// </summary>
/// <param name="msg">消息</param>
/// <returns></returns>
public static ISubscribeAck Success(string? msg = null)
{
return new SubscribeResult().Success(msg);
}
/// <summary>
/// 失败
/// </summary>
/// <param name="msg">消息</param>
/// <returns></returns>
public static ISubscribeAck Fail(string? msg = null)
{
return new SubscribeResult().Fail(msg);
}
}

View File

@ -8,6 +8,8 @@
<ItemGroup>
<PackageReference Include="Confluent.Kafka" Version="2.9.0" />
<PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.1.1" />
<PackageReference Include="Polly.Core" Version="8.5.2" />
<PackageReference Include="Volo.Abp.AspNetCore" Version="8.3.3" />
<PackageReference Include="Volo.Abp.Core" Version="8.3.3" />
</ItemGroup>

View File

@ -1,63 +0,0 @@
using Confluent.Kafka;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace JiShe.CollectBus.Kafka
{
public class KafkaOptionConfig
{
/// <summary>
/// kafka地址
/// </summary>
public string BootstrapServers { get; set; } = null!;
/// <summary>
/// 服务器标识
/// </summary>
public string ServerTagName { get; set; }= "KafkaFilterKey";
/// <summary>
/// kafka主题副本数量
/// </summary>
public short KafkaReplicationFactor { get; set; }
/// <summary>
/// kafka主题分区数量
/// </summary>
public int NumPartitions { get; set; }
/// <summary>
/// 是否开启过滤器
/// </summary>
public bool EnableFilter { get; set; }= true;
/// <summary>
/// 是否开启认证
/// </summary>
public bool EnableAuthorization { get; set; } = false;
/// <summary>
/// 安全协议
/// </summary>
public SecurityProtocol SecurityProtocol { get; set; } = SecurityProtocol.SaslPlaintext;
/// <summary>
/// 认证方式
/// </summary>
public SaslMechanism SaslMechanism { get; set; }= SaslMechanism.Plain;
/// <summary>
/// 用户名
/// </summary>
public string? SaslUserName { get; set; }
/// <summary>
/// 密码
/// </summary>
public string? SaslPassword { get; set; }
}
}

View File

@ -5,46 +5,49 @@ using JiShe.CollectBus.Common.Helpers;
using JiShe.CollectBus.Kafka.AdminClient;
using JiShe.CollectBus.Kafka.Attributes;
using JiShe.CollectBus.Kafka.Consumer;
using JiShe.CollectBus.Kafka.Internal;
using JiShe.CollectBus.Kafka.Serialization;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Collections;
using System.ComponentModel;
using System.Reflection;
using System.Threading.Tasks;
namespace JiShe.CollectBus.Kafka
{
public static class KafkaSubcribesExtensions
public static class KafkaSubscribeExtensions
{
/// <summary>
/// 添加Kafka订阅
/// </summary>
/// <param name="app"></param>
/// <param name="assembly"></param>
public static void UseKafkaSubscribe(this IServiceProvider provider)
{
var lifetime = provider.GetRequiredService<IHostApplicationLifetime>();
public static void UseInitKafkaTopic(this IServiceProvider provider)
{
//初始化主题信息
var kafkaAdminClient = provider.GetRequiredService<IAdminClientService>();
var kafkaOptions = provider.GetRequiredService<IOptions<KafkaOptionConfig>>();
List<string> topics = ProtocolConstExtensions.GetAllTopicNamesByIssued();
var topics = ProtocolConstExtensions.GetAllTopicNamesByIssued();
topics.AddRange(ProtocolConstExtensions.GetAllTopicNamesByReceived());
foreach (var item in topics)
{
kafkaAdminClient.CreateTopicAsync(item, kafkaOptions.Value.NumPartitions, kafkaOptions.Value.KafkaReplicationFactor).ConfigureAwait(false).GetAwaiter().GetResult();
}
}
/// <summary>
/// 添加Kafka订阅
/// </summary>
public static void UseKafkaSubscribe(this IServiceProvider provider)
{
var lifetime = provider.GetRequiredService<IHostApplicationLifetime>();
var kafkaOptions = provider.GetRequiredService<IOptions<KafkaOptionConfig>>();
lifetime.ApplicationStarted.Register(() =>
{
var logger = provider.GetRequiredService<ILogger<CollectBusKafkaModule>>();
int threadCount = 0;
int topicCount = 0;
var threadCount = 0;
var topicCount = 0;
var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
if (string.IsNullOrWhiteSpace(assemblyPath))
{
@ -62,7 +65,7 @@ namespace JiShe.CollectBus.Kafka
// 实现IKafkaSubscribe接口
var subscribeTypes = assembly.GetTypes().Where(type =>
typeof(IKafkaSubscribe).IsAssignableFrom(type) &&
!type.IsAbstract && !type.IsInterface).ToList(); ;
!type.IsAbstract && !type.IsInterface).ToList();
if (subscribeTypes.Count == 0)
continue;
@ -84,21 +87,14 @@ namespace JiShe.CollectBus.Kafka
});
}
/// <summary>
/// 添加Kafka订阅
/// </summary>
public static void UseKafkaSubscribersAsync(this IApplicationBuilder app, Assembly assembly)
{
var provider = app.ApplicationServices;
var lifetime = provider.GetRequiredService<IHostApplicationLifetime>();
//初始化主题信息
var kafkaAdminClient = provider.GetRequiredService<IAdminClientService>();
var kafkaOptions = provider.GetRequiredService<IOptions<KafkaOptionConfig>>();
List<string> topics = ProtocolConstExtensions.GetAllTopicNamesByIssued();
topics.AddRange(ProtocolConstExtensions.GetAllTopicNamesByReceived());
foreach (var item in topics)
{
kafkaAdminClient.CreateTopicAsync(item, kafkaOptions.Value.NumPartitions, kafkaOptions.Value.KafkaReplicationFactor).ConfigureAwait(false).GetAwaiter().GetResult();
}
lifetime.ApplicationStarted.Register(() =>
{
var logger = provider.GetRequiredService<ILogger<CollectBusKafkaModule>>();
@ -130,8 +126,6 @@ namespace JiShe.CollectBus.Kafka
/// <summary>
/// 构建Kafka订阅
/// </summary>
/// <param name="subscribe"></param>
/// <param name="provider"></param>
private static Tuple<int, int> BuildKafkaSubscribe(object subscribe, IServiceProvider provider, ILogger<CollectBusKafkaModule> logger, KafkaOptionConfig kafkaOptionConfig)
{
var subscribedMethods = subscribe.GetType().GetMethods()
@ -165,23 +159,18 @@ namespace JiShe.CollectBus.Kafka
/// <summary>
/// 启动后台消费线程
/// </summary>
/// <param name="config"></param>
/// <param name="attr"></param>
/// <param name="method"></param>
/// <param name="consumerInstance"></param>
/// <returns></returns>
private static async Task StartConsumerAsync(IServiceProvider provider, KafkaSubscribeAttribute attr, MethodInfo method, object subscribe, ILogger<CollectBusKafkaModule> logger)
{
var consumerService = provider.GetRequiredService<IConsumerService>();
if (attr.EnableBatch)
{
await consumerService.SubscribeBatchAsync<object>(attr.Topic, async (message) =>
await consumerService.SubscribeBatchAsync<dynamic>(attr.Topic, async (message) =>
{
try
{
#if DEBUG
logger.LogInformation($"kafka批量消费消息:{message}");
logger.LogInformation($"kafka批量消费消息:{message.Serialize()}");
#endif
// 处理消息
return await ProcessMessageAsync(message.ToList(), method, subscribe);
@ -196,7 +185,7 @@ namespace JiShe.CollectBus.Kafka
}
else
{
await consumerService.SubscribeAsync<object>(attr.Topic, async (message) =>
await consumerService.SubscribeAsync<dynamic>(attr.Topic, async (message) =>
{
try
{
@ -221,30 +210,112 @@ namespace JiShe.CollectBus.Kafka
/// <summary>
/// 处理消息
/// </summary>
/// <param name="message"></param>
/// <param name="method"></param>
/// <param name="subscribe"></param>
/// <returns></returns>
private static async Task<bool> ProcessMessageAsync(List<object> messages, MethodInfo method, object subscribe)
private static async Task<bool> ProcessMessageAsync(List<dynamic> messages, MethodInfo method, object subscribe)
{
var parameters = method.GetParameters();
bool isGenericTask = method.ReturnType.IsGenericType
&& method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>);
bool existParameters = parameters.Length > 0;
List<object>? messageObj = null;
object[]? executeParameters = null;
if (existParameters)
{
messageObj = new List<object>();
var paramType = parameters[0].ParameterType;
foreach (var msg in messages)
IList? list = null;
Tuple<Type, Type?> tuple = method.GetParameterTypeInfo();
bool isEnumerable = false;
if (tuple.Item2 != null)
{
var data = paramType != typeof(string) ? msg?.ToString()?.Deserialize(paramType) : msg;
if (data != null)
messageObj.Add(data);
Type listType = typeof(List<>).MakeGenericType(tuple.Item2);
list = (IList)Activator.CreateInstance(listType)!;
isEnumerable = tuple.Item2.IsConvertType();
}
else
{
isEnumerable = tuple.Item1.IsConvertType();
}
#region
//foreach (var msg in messages)
//{
// if (tuple.Item2 != null)
// {
// if (isEnumerable)
// {
// var parameterType = parameters[0].ParameterType;
// var data=messages?.Serialize().Deserialize(parameterType);
// messageObj = data!=null? new[] { data }:null;
// break;
// }
// else
// {
// // 集合类型
// var data = msg?.Serialize().Deserialize(tuple.Item2) /*isEnumerable ? Convert.ChangeType(msg, tuple.Item2) : msg?.Serialize().Deserialize(tuple.Item2)*/;
// if (data != null)
// list?.Add(data);
// }
// }
// else
// {
// // (dynamic)Convert.ChangeType(msg, tuple.Item1)
// using (var stream = new MemoryStream(msg))
// {
// var data1= System.Text.Json.JsonSerializer.Deserialize(stream, tuple.Item1);
// }
// var data = isEnumerable ? System.Text.Json.JsonSerializer.Deserialize(msg, tuple.Item1): msg?.ToString()?.Deserialize(tuple.Item1);
// if (data != null)
// messageObj = new[] { data };
// }
//}
//if (tuple.Item2 != null && list != null && list.Count > 0)
//{
// messageObj = new[] { list };
//}
#endregion
var parameterDescriptors = method.GetParameters();
executeParameters = new object?[parameterDescriptors.Length];
for (var i = 0; i < parameterDescriptors.Length; i++)
{
foreach (var item in messages)
{
object? tempParameter=null;
var parameterDescriptor = parameterDescriptors[i];
if (KafkaSerialization.IsJsonType(item))
{
tempParameter = KafkaSerialization.Deserialize(item, tuple.Item2 != null? tuple.Item2: parameterDescriptor.ParameterType);
}
else
{
var converter = TypeDescriptor.GetConverter(parameterDescriptor.ParameterType);
if (converter.CanConvertFrom(item.GetType()))
{
tempParameter = converter.ConvertFrom(item);
}
else
{
if (parameterDescriptor.ParameterType.IsInstanceOfType(item))
tempParameter = item;
else
tempParameter =Convert.ChangeType(item, parameterDescriptor.ParameterType);
}
}
if (tuple.Item2 == null)
{
executeParameters[i] = tempParameter;
}
else
{
list.Add(tempParameter);
}
}
if(list!=null && list.Count>0)
executeParameters[i] = list;
}
}
var result = method.Invoke(subscribe, messageObj?.ToArray());
var result = method.Invoke(subscribe, executeParameters);
if (result is Task<ISubscribeAck> genericTask)
{
await genericTask.ConfigureAwait(false);
@ -261,6 +332,7 @@ namespace JiShe.CollectBus.Kafka
}
return false;
}
}
}
}

View File

@ -1,9 +1,4 @@
using Confluent.Kafka;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace JiShe.CollectBus.Kafka.Producer
{

View File

@ -5,7 +5,10 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Confluent.Kafka;
using JiShe.CollectBus.Common;
using JiShe.CollectBus.Kafka.Consumer;
using JiShe.CollectBus.Kafka.Internal;
using JiShe.CollectBus.Kafka.Serialization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -21,11 +24,19 @@ namespace JiShe.CollectBus.Kafka.Producer
private readonly ConcurrentDictionary<Type, object> _producerCache = new();
private class KafkaProducer<TKey, TValue> where TKey : notnull where TValue : class { }
private readonly KafkaOptionConfig _kafkaOptionConfig;
public ProducerService(IConfiguration configuration,ILogger<ProducerService> logger, IOptions<KafkaOptionConfig> kafkaOptionConfig)
private readonly ServerApplicationOptions _applicationOptions;
/// <summary>
/// ProducerService
/// </summary>
/// <param name="configuration"></param>
/// <param name="logger"></param>
/// <param name="kafkaOptionConfig"></param>
public ProducerService(IConfiguration configuration,ILogger<ProducerService> logger, IOptions<KafkaOptionConfig> kafkaOptionConfig, IOptions<ServerApplicationOptions> applicationOptions)
{
_configuration = configuration;
_logger = logger;
_kafkaOptionConfig = kafkaOptionConfig.Value;
_applicationOptions = applicationOptions.Value;
}
#region private
@ -56,7 +67,7 @@ namespace JiShe.CollectBus.Kafka.Producer
var config = new ProducerConfig
{
BootstrapServers = _kafkaOptionConfig.BootstrapServers,
AllowAutoCreateTopics = true,
//AllowAutoCreateTopics = true,
QueueBufferingMaxKbytes = 2_097_151, // 修改缓冲区最大为2GB默认为1GB
CompressionType = CompressionType.Lz4, // 配置使用压缩算法LZ4其他gzip/snappy/zstd
BatchSize = 32_768, // 修改批次大小为32K
@ -110,7 +121,7 @@ namespace JiShe.CollectBus.Kafka.Producer
Key = key,
Value = value,
Headers = new Headers{
{ "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) }
{ "route-key", Encoding.UTF8.GetBytes(_applicationOptions.ServerTagName) }
}
};
await producer.ProduceAsync(topic, message);
@ -132,7 +143,7 @@ namespace JiShe.CollectBus.Kafka.Producer
//Key= _kafkaOptionConfig.ServerTagName,
Value = value,
Headers = new Headers{
{ "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) }
{ "route-key", Encoding.UTF8.GetBytes(_applicationOptions.ServerTagName) }
}
};
await producer.ProduceAsync(topic, message);
@ -156,7 +167,7 @@ namespace JiShe.CollectBus.Kafka.Producer
Key = key,
Value = value,
Headers = new Headers{
{ "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) }
{ "route-key", Encoding.UTF8.GetBytes(_applicationOptions.ServerTagName) }
}
};
var typeKey = typeof(KafkaProducer<TKey, TValue>);
@ -191,7 +202,7 @@ namespace JiShe.CollectBus.Kafka.Producer
//Key = _kafkaOptionConfig.ServerTagName,
Value = value,
Headers = new Headers{
{ "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) }
{ "route-key", Encoding.UTF8.GetBytes(_applicationOptions.ServerTagName) }
}
};
var typeKey = typeof(KafkaProducer<Null, TValue>);

View File

@ -8,7 +8,7 @@ using Confluent.Kafka;
using System.Text.Json.Serialization;
using System.Text.Encodings.Web;
namespace JiShe.CollectBus.Kafka
namespace JiShe.CollectBus.Kafka.Serialization
{
/// <summary>
/// JSON 序列化器(支持泛型)
@ -49,10 +49,11 @@ namespace JiShe.CollectBus.Kafka
{
if (isNull)
return default;
try
{
return JsonSerializer.Deserialize<T>(data, _options);
if (data.IsEmpty)
return default;
return JsonSerializer.Deserialize<T>(data, _options)!;
}
catch (Exception ex)
{
@ -85,4 +86,40 @@ namespace JiShe.CollectBus.Kafka
writer.WriteStringValue(value.ToString(_dateFormatString));
}
}
public static class KafkaSerialization
{
/// <summary>
/// 判断是否是json类型
/// </summary>
/// <param name="jsonObject"></param>
/// <returns></returns>
public static bool IsJsonType(this object jsonObject)
{
return jsonObject is JsonElement;
}
public static object? Deserialize(object value, Type valueType)
{
var _jsonSerializerOptions = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
WriteIndented = false,// 设置格式化输出
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,// 允许特殊字符
IgnoreReadOnlyFields = true,
IgnoreReadOnlyProperties = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString, // 允许数字字符串
AllowTrailingCommas = true, // 忽略尾随逗号
ReadCommentHandling = JsonCommentHandling.Skip, // 忽略注释
PropertyNameCaseInsensitive = true, // 属性名称大小写不敏感
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // 属性名称使用驼峰命名规则
Converters = { new DateTimeJsonConverter() } // 注册你的自定义转换器,
};
if (value is JsonElement jsonElement) return jsonElement.Deserialize(valueType, _jsonSerializerOptions);
throw new NotSupportedException("Type is not of type JsonElement");
}
}
}

View File

@ -1,75 +0,0 @@
using Confluent.Kafka;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace JiShe.CollectBus.Kafka
{
public class SubscribeResult: ISubscribeAck
{
/// <summary>
/// 是否成功
/// </summary>
public bool Ack { get; set; }
/// <summary>
/// 消息
/// </summary>
public string? Msg { get; set; }
/// <summary>
/// 成功
/// </summary>
/// <param name="msg">消息</param>
public SubscribeResult Success(string? msg = null)
{
Ack = true;
Msg = msg;
return this;
}
/// <summary>
/// 失败
/// </summary>
/// <param name="code"></param>
/// <param name="msg"></param>
/// <param name="data"></param>
/// <returns></returns>
public SubscribeResult Fail(string? msg = null)
{
Msg = msg;
Ack = false;
return this;
}
}
public static partial class SubscribeAck
{
/// <summary>
/// 成功
/// </summary>
/// <param name="msg">消息</param>
/// <returns></returns>
public static ISubscribeAck Success(string? msg = null)
{
return new SubscribeResult().Success(msg);
}
/// <summary>
/// 失败
/// </summary>
/// <param name="msg">消息</param>
/// <returns></returns>
public static ISubscribeAck Fail(string? msg = null)
{
return new SubscribeResult().Fail(msg);
}
}
}

View File

@ -1,23 +0,0 @@
using System;
using System.Threading.Tasks;
using JiShe.CollectBus.Common.Models;
using JiShe.CollectBus.IotSystems.MessageReceiveds;
using JiShe.CollectBus.IotSystems.Protocols;
using JiShe.CollectBus.Protocol.Contracts.Models;
using TouchSocket.Sockets;
namespace JiShe.CollectBus.Protocol.Contracts.Interfaces
{
public interface IProtocolPlugin
{
Task<ProtocolInfo> GetAsync();
Task AddAsync();
Task<T> AnalyzeAsync<T>(MessageReceived messageReceived, Action<byte[]>? sendAction = null) where T : TB3761;
Task LoginAsync(MessageReceivedLogin messageReceived);
Task HeartbeatAsync(MessageReceivedHeartbeat messageReceived);
}
}

View File

@ -1,29 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Extensions\**" />
<EmbeddedResource Remove="Extensions\**" />
<None Remove="Extensions\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DotNetCore.CAP" Version="8.3.1" />
<PackageReference Include="MassTransit.Abstractions" Version="8.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="TouchSocket" Version="2.1.9" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\modules\JiShe.CollectBus.Kafka\JiShe.CollectBus.Kafka.csproj" />
<ProjectReference Include="..\..\services\JiShe.CollectBus.Domain\JiShe.CollectBus.Domain.csproj" />
<ProjectReference Include="..\..\shared\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj" />
</ItemGroup>
</Project>

View File

@ -1,55 +0,0 @@
using JiShe.CollectBus.Common.Enums;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace JiShe.CollectBus.Protocol.Contracts.Models
{
public class TB3761
{
public int Id { get; set; }
public AFN Afn { get; set; }
public List<TB3761FN> FnList { get; set; }
}
public class TB3761FN
{
public int Id { get; set; }
public int Fn { get; set; }
public string Text { get; set; }
public List<TB3761UP> UpList { get; set; }
}
public class TB3761UP
{
public int Id { get; set; }
public string Name { get; set; }
public string? Value { get; set; }
public string DataType { get; set; }
public int DataIndex { get; set; }
//public int DataIndex { get; set; }
public int DataCount { get; set; }
//public int ParentId { get; set; }
public int Sort { get; set; }
public List<TB3761UP> Tb3761UpChildlList { get; set; }
}
}

View File

@ -0,0 +1,6 @@
namespace JiShe.CollectBus.Protocol.Contracts.ProtocolPools
{
public interface IPluginContainer
{
}
}

View File

@ -0,0 +1,26 @@
using JiShe.CollectBus.Interfaces;
namespace JiShe.CollectBus.Protocol.Contracts.ProtocolPools
{
public class PluginContainer: IPluginContainer
{
public Dictionary<string, object> ProtocolPools;
public PluginContainer()
{
}
}
public static class ServiceProviderKeyedServiceExtensions
{
//public static Task AddKeyedSingleton<TImp>(this IServiceProvider provider, string key)
//{
// //var aa = Activator.CreateInstance<TImp>();
//}
}
}

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Volo.Abp.Core" Version="8.3.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\services\JiShe.CollectBus.Domain\JiShe.CollectBus.Domain.csproj" />
<ProjectReference Include="..\..\shared\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj" />
<ProjectReference Include="..\JiShe.CollectBus.Protocol.T37612012\JiShe.CollectBus.Protocol.T37612012.csproj" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="copy $(TargetDir)JiShe.CollectBus.Protocol.T1882018.dll $(ProjectDir)..\..\web\JiShe.CollectBus.Host\Plugins\" />
</Target>
</Project>

View File

@ -0,0 +1,28 @@
using JiShe.CollectBus.Protocol.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp;
using Volo.Abp.Modularity;
namespace JiShe.CollectBus.Protocol.T1882018
{
public class JiSheCollectBusProtocolT1882018Module : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddKeyedSingleton<IProtocolPlugin, T1882018ProtocolPlugin>(nameof(T1882018ProtocolPlugin));
}
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
{
Console.WriteLine($"{nameof(T1882018ProtocolPlugin)} OnApplicationInitializationAsync");
var standardProtocol = context.ServiceProvider.GetRequiredKeyedService<IProtocolPlugin>(nameof(T1882018ProtocolPlugin));
await standardProtocol.LoadAsync();
}
public override void OnApplicationShutdown(ApplicationShutdownContext context)
{
Console.WriteLine($"{nameof(T1882018ProtocolPlugin)} OnApplicationShutdown");
base.OnApplicationShutdown(context);
}
}
}

View File

@ -0,0 +1,75 @@
using System.Reflection;
using JiShe.CollectBus.Common.BuildSendDatas;
namespace JiShe.CollectBus.Protocol.T1882018.SendData
{
/// <summary>
/// 构建188-2018下发报文
/// </summary>
public static class Telemetry1882018PacketBuilder
{
/// <summary>
/// 构建报文的委托
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public delegate Telemetry1882018PacketResponse T1882018Delegate(Telemetry1882018PacketRequest request);
/// <summary>
/// 编码与方法的映射表
/// </summary>
public static readonly Dictionary<string, T1882018Delegate> T1882018ControlHandlers = new();
static Telemetry1882018PacketBuilder()
{
// 初始化时自动注册所有符合命名规则的方法
var methods = typeof(Telemetry1882018PacketBuilder).GetMethods(BindingFlags.Static | BindingFlags.Public);
foreach (var method in methods)
{
if (method.Name.StartsWith("CTR") && method.Name.EndsWith("_Send"))
{
string code = method.Name;
var delegateInstance = (T1882018Delegate)Delegate.CreateDelegate(typeof(T1882018Delegate), method);
T1882018ControlHandlers[code] = delegateInstance;
}
}
}
#region
/// <summary>
/// 读取计量数据
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public static Telemetry1882018PacketResponse CTR_01_Send(Telemetry1882018PacketRequest request)
{
var itemCodeArr = request.ItemCode.Split('_');
var c_data = itemCodeArr[0];//01
var d_data = itemCodeArr[1];//91 或者 90
var dataUnit = new List<string>() { "1F", d_data, "00" };
var dataList = Build188SendData.Build188SendCommand(request.MeterAddress, c_data, dataUnit);
return new Telemetry1882018PacketResponse() { Data = dataList };
}
#endregion
#region
/// <summary>
/// 阀控
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public static Telemetry1882018PacketResponse CTR_04_Send(Telemetry1882018PacketRequest request)
{
var itemCodeArr = request.ItemCode.Split('_');
var c_data = itemCodeArr[0];//01
var d_data = itemCodeArr[1];//55 或者 99
var dataUnit = new List<string>() { "A0", "17", "00", d_data };
var dataList = Build188SendData.Build188SendCommand(request.MeterAddress, c_data, dataUnit);
return new Telemetry1882018PacketResponse() { Data = dataList };
}
#endregion
}
}

View File

@ -0,0 +1,23 @@
namespace JiShe.CollectBus.Protocol.T1882018.SendData
{
/// <summary>
/// 构建645报文参数
/// </summary>
public class Telemetry1882018PacketRequest
{
/// <summary>
/// 表地址
/// </summary>
public required string MeterAddress { get; set; }
/// <summary>
/// 密码
/// </summary>
public required string Password { get; set; }
/// <summary>
/// 操作码
/// </summary>
public required string ItemCode { get; set; }
}
}

View File

@ -0,0 +1,13 @@
namespace JiShe.CollectBus.Protocol.T1882018.SendData
{
/// <summary>
/// 返回645报文结果
/// </summary>
public class Telemetry1882018PacketResponse
{
/// <summary>
/// 报文体
/// </summary>
public List<string> Data { get; set; }
}
}

View File

@ -0,0 +1,140 @@
using JiShe.CollectBus.Common.Consts;
using JiShe.CollectBus.Common.Enums;
using JiShe.CollectBus.Common.Extensions;
using JiShe.CollectBus.IotSystems.Protocols;
using JiShe.CollectBus.Protocol.Models;
using JiShe.CollectBus.Protocol.T1882018.SendData;
using JiShe.CollectBus.Protocol.T37612012;
using JiShe.CollectBus.Protocol.T37612012.SendData;
using Mapster;
using Microsoft.Extensions.Logging;
using TouchSocket.Sockets;
namespace JiShe.CollectBus.Protocol.T1882018
{
/// <summary>
/// T1882018协议插件
/// </summary>
public class T1882018ProtocolPlugin : T37612012ProtocolPlugin
{
private readonly ILogger<T1882018ProtocolPlugin> _logger;
public readonly Dictionary<string, Telemetry1882018PacketBuilder.T1882018Delegate> T188ControlHandlers;
/// <summary>
/// Initializes a new instance of the <see cref="T1882018ProtocolPlugin"/> class.
/// </summary>
/// <param name="serviceProvider">The service provider.</param>
public T1882018ProtocolPlugin(IServiceProvider serviceProvider, ILogger<T1882018ProtocolPlugin> logger, ITcpService tcpService) : base(serviceProvider, logger, tcpService)
{
_logger = logger;
T188ControlHandlers = Telemetry1882018PacketBuilder.T1882018ControlHandlers;
}
public sealed override ProtocolInfo Info => new(nameof(T1882018ProtocolPlugin), "376.1/188-2018", "TCP", "376.1/188-2018协议", "HJ-LXS-15 DN15");
public override async Task<T> AnalyzeAsync<T>(ITcpSessionClient client, string messageReceived, Action<T>? sendAction = null)
{
//TB3761? tB3761 = Analysis3761(messageReceived);
//if (tB3761 != null)
//{
// if (tB3761.AFN_FC?.AFN == (int)AFN.链路接口检测)
// {
// if (tB3761.A == null || tB3761.A.Code.IsNullOrWhiteSpace() || tB3761.A.A3?.D1_D7 == null || tB3761.SEQ?.PSEQ == null)
// {
// _logger.LogError($"解析AFN.链路接口检测报文失败,报文:{messageReceived},TB3761:{tB3761.Serialize()}");
// }
// else
// {
// if (tB3761.DT?.Fn == (int)FN.登录)
// {
// // 登录回复
// if (tB3761.SEQ.CON == (int)CON.需要对该帧进行确认)
// await LoginAsync(client, messageReceived, tB3761.A.Code, tB3761.A.A3?.D1_D7, tB3761.SEQ?.PSEQ);
// }
// else if (tB3761.DT?.Fn == (int)FN.心跳)
// {
// // 心跳回复
// //心跳帧有两种情况:
// //1. 集中器先有登录帧,再有心跳帧
// //2. 集中器没有登录帧,只有心跳帧
// await HeartbeatAsync(client, messageReceived, tB3761.A.Code, tB3761.A.A3?.D1_D7, tB3761.SEQ?.PSEQ);
// }
// }
// }
// await OnTcpNormalReceived(client, tB3761);
//}
//return (tB3761 as T)!;
return null;
}
/// <summary>
/// 组装报文
/// </summary>
/// <param name="request">报文构建参数</param>
/// <returns></returns>
public override async Task<ProtocolBuildResponse> BuildAsync(ProtocolBuildRequest request)
{
if (request == null)
{
_logger.LogError($"{nameof(T1882018ProtocolPlugin)} 报文构建失败,参数为空");
return new ProtocolBuildResponse();
}
var itemCodeArr = request.ItemCode.Split('_');
var aFNStr = itemCodeArr[0];
var aFN = (AFN)aFNStr.HexToDec();
var fn = int.Parse(itemCodeArr[1]);
Telemetry3761PacketResponse builderResponse = null;
List<string> dataUnit = new List<string>();
//数据转发场景 10H_F1
if (aFNStr == "10" && request.SubProtocolRequest != null && string.IsNullOrWhiteSpace(request.SubProtocolRequest.ItemCode) == false)
{
var t188PacketHandlerName = $"{T1882018PacketItemCodeConst.BasicT1882018}_{request.SubProtocolRequest.ItemCode}_Send";
Telemetry1882018PacketResponse t645PacketResponse = null;
if (T188ControlHandlers != null && T188ControlHandlers.TryGetValue(t188PacketHandlerName
, out var t645PacketHandler))
{
t645PacketResponse = t645PacketHandler(new Telemetry1882018PacketRequest()
{
MeterAddress = request.SubProtocolRequest.MeterAddress,
Password = request.SubProtocolRequest.Password,
ItemCode = request.SubProtocolRequest.ItemCode,
});
}
if (t645PacketResponse != null)
{
dataUnit = t645PacketResponse.Data;
}
}
string afnMethonCode = $"AFN{aFNStr}_Fn_Send";
if (base.T3761AFNHandlers != null && base.T3761AFNHandlers.TryGetValue(afnMethonCode
, out var handler))
{
builderResponse = handler(new Telemetry3761PacketRequest()
{
FocusAddress = request.FocusAddress,
Fn = fn,
Pn = request.Pn,
DataUnit = dataUnit,
});
}
if (builderResponse == null)
{
return new ProtocolBuildResponse();
}
var result = builderResponse.Adapt<ProtocolBuildResponse>();
result.IsSuccess = true;
return await Task.FromResult(result);
}
}
}

View File

@ -0,0 +1,43 @@
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.T37612012.AnalysisData.AFN_00H
{
/// <summary>
/// 5.1.3.1 F1全部确认对收到报文中的全部数据单元标识进行确认
/// </summary>
public class AFN0_F1_Analysis: IAnalysisStrategy<TB3761, UnitDataAnalysis<bool>>
{
private readonly ILogger<AFN0_F1_Analysis> _logger;
public AFN0_F1_Analysis(ILogger<AFN0_F1_Analysis> logger)
{
_logger = logger;
}
public Task<UnitDataAnalysis<bool>> ExecuteAsync(TB3761 tB3761)
{
try
{
ArgumentNullException.ThrowIfNull(nameof(tB3761));
UnitDataAnalysis<bool> dto = new UnitDataAnalysis<bool>
{
Code = tB3761.A.Code,
AFN = tB3761.AFN_FC.AFN,
Fn = tB3761.DT.Fn,
Pn = tB3761.DA.Pn,
Data= true
};
return Task.FromResult(dto);
}
catch (Exception ex)
{
_logger.LogError(ex, $"00_1解析失败:{tB3761.A.Code}-{tB3761.DT.Fn}-{tB3761.BaseHexMessage.HexMessageString},{ex.Message}");
return null;
}
}
}
}

View File

@ -0,0 +1,41 @@
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.T37612012.AnalysisData.AFN_00H
{
/// <summary>
/// 5.1.3.2 F2全部否认
/// </summary>
public class AFN0_F2_Analysis : IAnalysisStrategy<TB3761, UnitDataAnalysis<bool>>
{
private readonly ILogger<AFN0_F2_Analysis> _logger;
public AFN0_F2_Analysis(ILogger<AFN0_F2_Analysis> logger)
{
_logger = logger;
}
public Task<UnitDataAnalysis<bool>> ExecuteAsync(TB3761 input)
{
try
{
ArgumentNullException.ThrowIfNull(input);
UnitDataAnalysis<bool> dto = new UnitDataAnalysis<bool>
{
Code = input.A.Code,
AFN = input.AFN_FC.AFN,
Fn = input.DT.Fn,
Pn = input.DA.Pn,
Data = false,
};
return Task.FromResult(dto);
}
catch (Exception ex)
{
_logger.LogError(ex, $"00_2解析失败:{input.A.Code}-{input.DT.Fn}-{input.BaseHexMessage.HexMessageString},{ex.Message}");
return null;
}
}
}
}

View File

@ -0,0 +1,66 @@
using System.Text;
using JiShe.CollectBus.Common.Extensions;
using JiShe.CollectBus.Protocol.Contracts.Protocol.Dto;
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.AnalysisData.AFN_09H
{
/// <summary>
/// 5.9.1.2 F1:终端版本信息
/// </summary>
public class AFN9_F1_Analysis : IAnalysisStrategy<TB3761, UnitDataAnalysis<AFN9_F1_AnalysisDto>>
{
private readonly ILogger<AFN9_F1_Analysis> _logger;
public AFN9_F1_Analysis(ILogger<AFN9_F1_Analysis> logger)
{
_logger = logger;
}
public Task<UnitDataAnalysis<AFN9_F1_AnalysisDto>> ExecuteAsync(TB3761 input)
{
try
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(input.UnitData.HexMessageList);
var data = AnalysisDataUnit(input.UnitData.HexMessageList);
data.AreaCode = input.A.Code?.Substring(0, 4);
data.Address = input.A.Code?.Substring(4, 5);
UnitDataAnalysis<AFN9_F1_AnalysisDto> dto = new UnitDataAnalysis<AFN9_F1_AnalysisDto>
{
Code = input.A.Code,
AFN = input.AFN_FC.AFN,
Fn = input.DT.Fn,
Pn = input.DA.Pn,
Data= data
};
return Task.FromResult(dto);
}
catch (Exception ex)
{
_logger.LogError(ex, $"09_1解析失败:{input.A?.Code}-{input.DT?.Fn ?? 0}-{input?.BaseHexMessage?.HexMessageString},{ex.Message}");
return null;
}
}
private AFN9_F1_AnalysisDto AnalysisDataUnit(List<string> hexMessageList)
{
AFN9_F1_AnalysisDto version = new AFN9_F1_AnalysisDto();
version.MakerNo = Encoding.ASCII.GetString(string.Join("", hexMessageList.Skip(4).Take(4).ToList()).HexToByte());//厂商代号
version.DeviceNo = Encoding.ASCII.GetString(string.Join("", hexMessageList.Skip(8).Take(8).ToList()).HexToByte()).Replace("\0", "");//设备编号
version.SoftwareVersion = Encoding.ASCII.GetString(string.Join("", hexMessageList.Skip(16).Take(4).ToList()).HexToByte());//终端软件版本号
version.HardwareVersion = Encoding.ASCII.GetString(string.Join("", hexMessageList.Skip(38).Take(4).ToList()).HexToByte());//终端硬件版本号
version.AddDate = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var dateArr = hexMessageList.Skip(20).Take(3).ToList();
var dateArr2 = hexMessageList.Skip(42).Take(3).ToList();
dateArr.Reverse();
dateArr2.Reverse();
version.SoftwareReleaseDate = $"{DateTime.Now.Year.ToString().Substring(0, 2)}{string.Join("-", dateArr)}";//终端软件发布日期:日月年
version.HardwareReleaseDate = $"{DateTime.Now.Year.ToString().Substring(0, 2)}{string.Join("-", dateArr2)}";//终端硬件发布日期:日月年
return version;
}
}
}

View File

@ -0,0 +1,45 @@
using System.Text;
using JiShe.CollectBus.Common.Extensions;
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.T37612012.AnalysisData.AFN_09H
{
/// <summary>
/// 5.9.2.4.9 F9远程通信模块版本信息(只读解析SIM卡号)
/// </summary>
public class AFN9_F9_Analysis : IAnalysisStrategy<TB3761, UnitDataAnalysis<string>>
{
private readonly ILogger<AFN9_F9_Analysis> _logger;
public AFN9_F9_Analysis(ILogger<AFN9_F9_Analysis> logger)
{
_logger = logger;
}
public Task<UnitDataAnalysis<string>> ExecuteAsync(TB3761 input)
{
try
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(input.UnitData.HexMessageList);
UnitDataAnalysis<string> dto = new UnitDataAnalysis<string>
{
Code = input.A.Code,
AFN = input.AFN_FC.AFN,
Fn = input.DT.Fn,
Pn = input.DA.Pn,
Data = Encoding.ASCII.GetString(string.Join("", input.UnitData.HexMessageList.Skip(30).Take(20).ToList()).HexToByte()).Replace("\0", "") //SIM卡
};
return Task.FromResult(dto);
}
catch (Exception ex)
{
_logger.LogError(ex, $"00_1解析失败:{input.A?.Code}-{input.DT?.Fn ?? 0}-{input?.BaseHexMessage?.HexMessageString},{ex.Message}");
return null;
}
}
}
}

View File

@ -0,0 +1,158 @@
using JiShe.CollectBus.Common.Extensions;
using JiShe.CollectBus.Protocol.Contracts.Protocol.Dto;
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.T37612012.AnalysisData.AFN_0AH
{
/// <summary>
/// 5.10.1.3.1 F10终端电能表/交流采样装置配置参数
/// </summary>
internal class AFN10_F10_Analysis : IAnalysisStrategy<TB3761, UnitDataAnalysis<AFN10_F10_AnalysisDto>>
{
private readonly ILogger<AFN10_F10_Analysis> _logger;
public AFN10_F10_Analysis(ILogger<AFN10_F10_Analysis> logger)
{
_logger = logger;
}
public Task<UnitDataAnalysis<AFN10_F10_AnalysisDto>> ExecuteAsync(TB3761 input)
{
try
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(input.UnitData.HexMessageList);
Tuple<int, List<AFN10F10Entity>> tuple = AFN10F10EntityAnalysis(input.UnitData.HexMessageList);
UnitDataAnalysis<AFN10_F10_AnalysisDto> dto = new UnitDataAnalysis<AFN10_F10_AnalysisDto>
{
Code = input.A.Code,
AFN = input.AFN_FC.AFN,
Fn = input.DT.Fn,
Pn = input.DA.Pn,
Data=new AFN10_F10_AnalysisDto()
{
AFN10F10Entitys = tuple.Item2,
ConfigNum = tuple.Item1
}
};
return Task.FromResult(dto);
}
catch (Exception ex)
{
_logger.LogError(ex, $"0A_10解析失败:{input.A?.Code}-{input.DT?.Fn ?? 0}-{input?.BaseHexMessage?.HexMessageString},{ex.Message}");
return null;
}
}
public Tuple<int, List<AFN10F10Entity>> AFN10F10EntityAnalysis(List<string> hexMessageList)
{
List<Dictionary<string, string>> meterList = new List<Dictionary<string, string>>();
int total = $"{hexMessageList[5]}{hexMessageList[4]}".HexToDec();
List<AFN10F10Entity> aFN10F10Entitys = new List<AFN10F10Entity>();
for (int i = 0; i < total; i++)
{
List<string> sArray = hexMessageList.GetRange(6 + 27 * i, 27);
AFN10F10Entity aFN10F10Entity = new AFN10F10Entity()
{
SerialNum = $"{sArray[1]}{sArray[0]}".HexToDec(),
Point = $"{sArray[3]}{sArray[2]}".HexToDec(),
RuleType= GetProtocol(sArray[5]),
ComAddress= $"{sArray[11]}{sArray[10]}{sArray[9]}{sArray[8]}{sArray[7]}{sArray[6]}",
ComPwd= $"{sArray[17]}{sArray[16]}{sArray[15]}{sArray[14]}{sArray[13]}{sArray[12]}".Substring(6, 6),
ElectricityRatesNum= sArray[18].HexToBin().Substring(2, 6).BinToDec(),
CollectorAddress = $"{sArray[25]}{sArray[24]}{sArray[23]}{sArray[22]}{sArray[21]}{sArray[20]}",
};
string baudPort = sArray[4].HexToBin().PadLeft(8, '0'); //波特率和端口号放在一个字节内
aFN10F10Entity.BaudRate = GetBaudrate(baudPort.Substring(0, 3));
aFN10F10Entity.Port = baudPort.Substring(3, 5).BinToDec();
string dataDigit = sArray[19].HexToBin().PadLeft(8, '0'); //有功电能示值整数位及小数位个数
aFN10F10Entity.IntegerBitsNum = dataDigit.Substring(4, 2).BinToDec() + 4;
aFN10F10Entity.DecimalPlacesNum = dataDigit.Substring(6, 2).BinToDec() + 1;
string classNo = sArray[26].HexToBin().PadLeft(8, '0');//用户大类号及用户小类号
aFN10F10Entity.UserCategoryNum = classNo.Substring(0, 4).BinToDec() + 1;
aFN10F10Entity.UserCategoryNum = classNo.Substring(4, 4).BinToDec() + 1;
aFN10F10Entitys.Add(aFN10F10Entity);
}
return Tuple.Create(total, aFN10F10Entitys);
}
/// <summary>
/// 获取波特率
/// </summary>
/// <param name="binBaud"></param>
/// <returns></returns>
private int GetBaudrate(string binBaud)
{
int baudRate = 0;
switch (binBaud)
{
case "001":
baudRate = 600;
break;
case "010":
baudRate = 1200;
break;
case "011":
baudRate = 2400;
break;
case "100":
baudRate = 4800;
break;
case "101":
baudRate = 7200;
break;
case "110":
baudRate = 9600;
break;
case "111":
baudRate = 19200;
break;
default:
baudRate = 0;
break;
}
return baudRate;
}
/// <summary>
/// 获取通信协议文本说明
/// </summary>
/// <param name="protocol"></param>
/// <returns></returns>
private string GetProtocol(string protocol)
{
int dataUnit = protocol.HexToDec();
if (dataUnit == 1)
{
return "DL/T 645—1997";
}
if (dataUnit == 2)
{
return "交流采样装置通信协议";
}
if (dataUnit == 30)
{
return "DL/T 645—2007";
}
if (dataUnit == 31)
{
return "串行接口连接窄带低压载波通信模块";
}
if (dataUnit == 32)
{
return "CJ/T 188—2018协议";
}
return "其他协议";
}
}
}

View File

@ -0,0 +1,82 @@
using JiShe.CollectBus.Common.Extensions;
using JiShe.CollectBus.Protocol.Contracts.Protocol.Dto;
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.T37612012.AnalysisData.AFN_0AH
{
/// <summary>
/// 5.5.1.3.53 F66定时上报 2 类数据任务设置
/// </summary>
public class AFN10_F66_Analysis : IAnalysisStrategy<TB3761, UnitDataAnalysis<AFN10_F66_AnalysisDto>>
{
private readonly ILogger<AFN10_F66_Analysis> _logger;
private readonly AnalysisStrategyContext _analysisStrategyContext;
public AFN10_F66_Analysis(ILogger<AFN10_F66_Analysis> logger, AnalysisStrategyContext analysisStrategyContext)
{
_logger = logger;
_analysisStrategyContext= analysisStrategyContext;
}
public async Task<UnitDataAnalysis<AFN10_F66_AnalysisDto>> ExecuteAsync(TB3761 input)
{
try
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(input.UnitData.HexMessageList);
var data = await GenerateFinalResult(input.UnitData.HexMessageList);
data.Pn = input.DA.Pn;
UnitDataAnalysis<AFN10_F66_AnalysisDto> dto = new UnitDataAnalysis<AFN10_F66_AnalysisDto>
{
Code = input.A.Code,
AFN = input.AFN_FC.AFN,
Fn = input.DT.Fn,
Pn = input.DA.Pn,
Data= data
};
return await Task.FromResult(dto);
}
catch (Exception ex)
{
_logger.LogError(ex, $"0A_66解析失败:{input.A?.Code}-{input.DT?.Fn ?? 0}-{input?.BaseHexMessage?.HexMessageString},{ex.Message}");
return null;
}
}
public async Task<AFN10_F66_AnalysisDto> GenerateFinalResult(List<string> hexMessageList)
{
AFN10_F66_AnalysisDto entity = new AFN10_F66_AnalysisDto();
var cycleBin = hexMessageList[4].HexToBin().PadLeft(8, '0');
var cycleUnitBin = cycleBin.Substring(0, 2);
var cycleValueBin = cycleBin.Substring(2, 6);
entity.Cycle = cycleValueBin.BinToDec();//定时发送周期
entity.Unit = cycleUnitBin.BinToDec();//定时发送周期(单位)
//TODO:发送基准时间
var arrBaseTime = hexMessageList.GetRange(5, 6);
var baseTimeArrStr = await _analysisStrategyContext.ExecuteAsync<List<string>, string>("Appendix_A1", arrBaseTime);
var baseTimeArr= baseTimeArrStr.Split('_');
//entity.BaseTime = DateTime.Parse($"{DateTime.Now.Year.ToString().Substring(0, 2)}{arrBaseTime[0]}-{arrBaseTime[1]}-{arrBaseTime[2]} {arrBaseTime[3]}:{arrBaseTime[4]}:{arrBaseTime[5]}");
entity.BaseTime = Convert.ToDateTime(baseTimeArr[0]);
entity.CurveRatio = hexMessageList[11].HexToDec();
var count = hexMessageList[12].HexToDec();
var dataArr = hexMessageList.GetRange(13, 4 * count);
for (int i = 0; i < count; i++)
{
var pnfnArr = dataArr.GetRange(0, 4);
var tempPn = T37612012ProtocolPlugin.CalculatePn(pnfnArr[0], pnfnArr[1]);
var tempFn = T37612012ProtocolPlugin.CalculateFn(pnfnArr[2], pnfnArr[3]);
entity.Details.Add(new SetAutoItemCodeDetails() { Fn = tempFn, Pn = tempPn });
dataArr.RemoveRange(0, 4);
}
return entity;
}
}
}

View File

@ -0,0 +1,43 @@
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.T37612012.AnalysisData.AFN_0AH
{
/// <summary>
/// 5.5.1.3.55 F68定时上报 2 类数据任务启动/停止设置
/// </summary>
public class AFN10_F68_Analysis : IAnalysisStrategy<TB3761, UnitDataAnalysis<bool>>
{
private readonly ILogger<AFN10_F68_Analysis> _logger;
public AFN10_F68_Analysis(ILogger<AFN10_F68_Analysis> logger)
{
_logger = logger;
}
public Task<UnitDataAnalysis<bool>> ExecuteAsync(TB3761 input)
{
try
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(input.UnitData.HexMessageList);
UnitDataAnalysis<bool> dto = new UnitDataAnalysis<bool>
{
Code = input.A.Code,
AFN = input.AFN_FC.AFN,
Fn = input.DT.Fn,
Pn = input.DA.Pn,
Data = input.UnitData.HexMessageList[4].Equals("55")
};
return Task.FromResult(dto);
}
catch (Exception ex)
{
_logger.LogError(ex, $"0A_68解析失败:{input.A?.Code}-{input.DT?.Fn ?? 0}-{input?.BaseHexMessage?.HexMessageString},{ex.Message}");
return null;
}
}
}
}

View File

@ -0,0 +1,105 @@
using JiShe.CollectBus.IoTDB.Interface;
using JiShe.CollectBus.Protocol.Contracts.Protocol.Dto;
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol.T37612012.Appendix;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.T37612012.AnalysisData.AFN_0CH
{
/// <summary>
/// 5.12.2.4.85 F129当前正向有功电能示值总、费率1M
/// </summary>
public class AFN12_F129_Analysis : IAnalysisStrategy<TB3761, UnitDataAnalysis<List<AFN12_F129_AnalysisDto>>>
{
private readonly ILogger<AFN12_F129_Analysis> _logger;
private readonly AnalysisStrategyContext _analysisStrategyContext;
private readonly IIoTDbProvider _dbProvider;
public AFN12_F129_Analysis(ILogger<AFN12_F129_Analysis> logger, AnalysisStrategyContext analysisStrategyContext, IIoTDbProvider dbProvider)
{
_logger = logger;
_analysisStrategyContext = analysisStrategyContext;
_dbProvider= dbProvider;
}
public async Task<UnitDataAnalysis<List<AFN12_F129_AnalysisDto>>> ExecuteAsync(TB3761 input)
{
try
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(input.UnitData?.HexMessageList);
ArgumentNullException.ThrowIfNull(input.A.A3?.D1_D7);
UnitDataAnalysis<List<AFN12_F129_AnalysisDto>> unitDataAnalysis = new UnitDataAnalysis<List<AFN12_F129_AnalysisDto>>
{
Code = input.A.Code!,
AFN = input.AFN_FC.AFN,
Fn = input.DT.Fn,
Pn = input.DA.Pn,
MSA= input.A.A3.D1_D7,
PSEQ= input.SEQ.PSEQ,
};
List<string> datas = await AnalysisDataUnitAsync(input.UnitData.HexMessageList);
List<AFN12_F129_AnalysisDto> list = GenerateFinalResult(2, datas, "正向有功电能示值", input.AFN_FC.AFN, input.DT.Fn);
unitDataAnalysis.Data= list;
return await Task.FromResult(unitDataAnalysis);
}
catch (Exception ex)
{
_logger.LogError(ex, $"0C_129解析失败:{input.A.Code}-{input.DT.Fn}-{input.BaseHexMessage.HexMessageString},{ex.Message}");
return null;
}
}
#region
/// <summary>
/// 单元数据值解析
/// </summary>
/// <returns></returns>
private async Task<List<string>> AnalysisDataUnitAsync(List<string> hexMessageList)
{
List<string> values = new List<string>();
values.Add(hexMessageList.GetReadTime(4, 5));
int rationgCount = hexMessageList.GetRatingCount(9, 1);
values.Add(rationgCount.ToString());
for (int i = 0; i < rationgCount + 1; i++)
{
var arr = hexMessageList.GetRange(10 + (i * 5), 5);
var errorCode = arr.CheckErrorCode();
if (errorCode != null)
values.Add(errorCode.Item1);
else
{
var value = await _analysisStrategyContext.ExecuteAsync<List<string>, decimal>(nameof(Appendix_A14), arr); //从第10个开始每加5个字节为下一个值的开始
values.Add(value.ToString());
}
}
return values;
}
#endregion
public List<AFN12_F129_AnalysisDto> GenerateFinalResult(int index, List<string> data, string remark = "", int afn = 0, int fn = 0)
{
List<AFN12_F129_AnalysisDto> list = new List<AFN12_F129_AnalysisDto>();
for (int i = index; i < data.Count; i++)
{
AFN12_F129_AnalysisDto meter = new AFN12_F129_AnalysisDto();
decimal value = 0;
var errorCode = data[i].CheckErrorCode();
if (errorCode != null)
meter.ValidData = false;
else
decimal.TryParse(data[i], out value);
meter.DataValue = value;
meter.DataType = $"{afn.ToString().PadLeft(2, '0')}_{fn}_{i - index}";
meter.ReadTime = Convert.ToDateTime($"{data[0].Substring(0, 4)}-{data[0].Substring(4, 2)}-{data[0].Substring(6, 2)} {data[0].Substring(8, 2)}:{data[0].Substring(10, 2)}:00");
list.Add(meter);
}
return list;
}
}
}

View File

@ -0,0 +1,93 @@
using JiShe.CollectBus.Protocol.Contracts.Protocol.Dto;
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol.T37612012.Appendix;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.T37612012.AnalysisData.AFN_0CH
{
/// <summary>
/// 5.12.2.4.86 F130当前正向无功组合无功 1电能示值总、费率 1M
/// </summary>
public class AFN12_F130_Analysis : IAnalysisStrategy<TB3761, UnitDataAnalysis<List<AFN12_F130_AnalysisDto>>>
{
private readonly ILogger<AFN12_F130_Analysis> _logger;
private readonly AnalysisStrategyContext _analysisStrategyContext;
public AFN12_F130_Analysis(ILogger<AFN12_F130_Analysis> logger, AnalysisStrategyContext analysisStrategyContext)
{
_logger = logger;
_analysisStrategyContext = analysisStrategyContext;
}
public async Task<UnitDataAnalysis<List<AFN12_F130_AnalysisDto>>> ExecuteAsync(TB3761 input)
{
try
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(input.UnitData?.HexMessageList);
List<string> datas = await AnalysisDataUnitAsync(input.UnitData.HexMessageList);
List<AFN12_F130_AnalysisDto> list = GenerateFinalResult(2, datas, "正向无功电能示值", input.AFN_FC.AFN, input.DT.Fn);
UnitDataAnalysis<List<AFN12_F130_AnalysisDto>> unitDataAnalysis = new UnitDataAnalysis<List<AFN12_F130_AnalysisDto>>
{
Code = input.A.Code,
AFN = input.AFN_FC.AFN,
Fn = input.DT.Fn,
Pn = input.DA.Pn,
Data = list
};
return await Task.FromResult(unitDataAnalysis);
}
catch (Exception ex)
{
_logger.LogError(ex, $"0C_130解析失败:{input.A.Code}-{input.DT.Fn}-{input.BaseHexMessage.HexMessageString},{ex.Message}");
return null;
}
}
private async Task<List<string>> AnalysisDataUnitAsync(List<string> hexMessageList)
{
List<string> values = new List<string>();
values.Add(hexMessageList.GetReadTime(4, 5));
int ratingCount = hexMessageList.GetRatingCount(9, 1);
values.Add(ratingCount.ToString());
for (int i = 0; i < ratingCount + 1; i++)
{
var arr = hexMessageList.GetRange(10 + (i * 4), 4);
var errorCode = arr.CheckErrorCode();
if (errorCode != null)
values.Add(errorCode.Item1);
else
{
var value = await _analysisStrategyContext.ExecuteAsync<List<string>, decimal>(nameof(Appendix_A11), arr); //从第10个开始每加5个字节为下一个值的开始
values.Add(value.ToString());
}
}
return values;
}
public List<AFN12_F130_AnalysisDto> GenerateFinalResult(int index, List<string> data, string remark = "", int afn = 0, int fn = 0)
{
List<AFN12_F130_AnalysisDto> list = new List<AFN12_F130_AnalysisDto>();
for (int i = index; i < data.Count; i++)
{
AFN12_F130_AnalysisDto meter = new AFN12_F130_AnalysisDto();
decimal value = 0;
var errorCode = data[i].CheckErrorCode();
if (errorCode != null)
meter.ValidData = false;
else
decimal.TryParse(data[i], out value);
meter.DataValue = value;
meter.DataType = $"{afn.ToString().PadLeft(2, '0')}_{fn}_{i - index}";
meter.ReadTime = Convert.ToDateTime($"{data[0].Substring(0, 4)}-{data[0].Substring(4, 2)}-{data[0].Substring(6, 2)} {data[0].Substring(8, 2)}:{data[0].Substring(10, 2)}:00");
list.Add(meter);
}
return list;
}
}
}

View File

@ -0,0 +1,93 @@
using JiShe.CollectBus.Protocol.Contracts.Protocol.Dto;
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol.T37612012.Appendix;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.T37612012.AnalysisData.AFN_0CH
{
/// <summary>
/// 5.12.2.4.87 F131当前反向有功电能示值总、费率 1M
/// </summary>
public class AFN12_F131_Analysis : IAnalysisStrategy<TB3761, UnitDataAnalysis<List<AFN12_F131_AnalysisDto>>>
{
private readonly ILogger<AFN12_F131_Analysis> _logger;
private readonly AnalysisStrategyContext _analysisStrategyContext;
public AFN12_F131_Analysis(ILogger<AFN12_F131_Analysis> logger, AnalysisStrategyContext analysisStrategyContext)
{
_logger = logger;
_analysisStrategyContext = analysisStrategyContext;
}
public async Task<UnitDataAnalysis<List<AFN12_F131_AnalysisDto>>> ExecuteAsync(TB3761 input)
{
try
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(input.UnitData?.HexMessageList);
List<string> datas = await AnalysisDataUnitAsync(input.UnitData.HexMessageList);
List<AFN12_F131_AnalysisDto> list = GenerateFinalResult(2, datas, "反向有功总电能示值", input.AFN_FC.AFN, input.DT.Fn);
UnitDataAnalysis<List<AFN12_F131_AnalysisDto>> unitDataAnalysis = new UnitDataAnalysis<List<AFN12_F131_AnalysisDto>>
{
Code = input.A.Code,
AFN = input.AFN_FC.AFN,
Fn = input.DT.Fn,
Pn = input.DA.Pn,
Data = list
};
return await Task.FromResult(unitDataAnalysis);
}
catch (Exception ex)
{
_logger.LogError(ex, $"0C_131解析失败:{input.A.Code}-{input.DT.Fn}-{input.BaseHexMessage.HexMessageString},{ex.Message}");
return null;
}
}
private async Task<List<string>> AnalysisDataUnitAsync(List<string> hexMessageList)
{
List<string> values = new List<string>();
values.Add(hexMessageList.GetReadTime(4, 5));
int ratingCount = hexMessageList.GetRatingCount(9, 1);
values.Add(ratingCount.ToString());
for (int i = 0; i < ratingCount + 1; i++)
{
var arr = hexMessageList.GetRange(10 + (i * 5), 5);
var errorCode = arr.CheckErrorCode();
if (errorCode != null)
values.Add(errorCode.Item1);
else
{
var value = await _analysisStrategyContext.ExecuteAsync<List<string>, decimal>(nameof(Appendix_A14), arr); //从第10个开始每加5个字节为下一个值的开始
values.Add(value.ToString());
}
}
return values;
}
public List<AFN12_F131_AnalysisDto> GenerateFinalResult(int index, List<string> data, string remark = "", int afn = 0, int fn = 0)
{
List<AFN12_F131_AnalysisDto> list = new List<AFN12_F131_AnalysisDto>();
for (int i = index; i < data.Count; i++)
{
AFN12_F131_AnalysisDto meter = new AFN12_F131_AnalysisDto();
decimal value = 0;
var errorCode = data[i].CheckErrorCode();
if (errorCode != null)
meter.ValidData = false;
else
decimal.TryParse(data[i], out value);
meter.DataValue = value;
meter.DataType = $"{afn.ToString().PadLeft(2, '0')}_{fn}_{i - index}";
meter.ReadTime = Convert.ToDateTime($"{data[0].Substring(0, 4)}-{data[0].Substring(4, 2)}-{data[0].Substring(6, 2)} {data[0].Substring(8, 2)}:{data[0].Substring(10, 2)}:00");
list.Add(meter);
}
return list;
}
}
}

View File

@ -0,0 +1,94 @@
using JiShe.CollectBus.Protocol.Contracts.Protocol.Dto;
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol.T37612012.Appendix;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.T37612012.AnalysisData.AFN_0CH
{
/// <summary>
/// 5.12.2.4.88 F132当前反向无功组合无功 2电能示值总、费率 1M
/// </summary>
public class AFN12_F132_Analysis : IAnalysisStrategy<TB3761, UnitDataAnalysis<List<AFN12_F132_AnalysisDto>>>
{
private readonly ILogger<AFN12_F132_Analysis> _logger;
private readonly AnalysisStrategyContext _analysisStrategyContext;
public AFN12_F132_Analysis(ILogger<AFN12_F132_Analysis> logger, AnalysisStrategyContext analysisStrategyContext)
{
_logger = logger;
_analysisStrategyContext = analysisStrategyContext;
}
public async Task<UnitDataAnalysis<List<AFN12_F132_AnalysisDto>>> ExecuteAsync(TB3761 input)
{
try
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(input.UnitData?.HexMessageList);
List<string> datas = await AnalysisDataUnitAsync(input.UnitData.HexMessageList);
List<AFN12_F132_AnalysisDto> list = GenerateFinalResult(2, datas, "反向无功电能示值", (int)input.AFN_FC.AFN!, (int)input.DT.Fn!);
UnitDataAnalysis<List<AFN12_F132_AnalysisDto>> unitDataAnalysis = new UnitDataAnalysis<List<AFN12_F132_AnalysisDto>>
{
Code = input.A.Code,
AFN = input.AFN_FC.AFN,
Fn = input.DT.Fn ,
Pn = input.DA.Pn,
Data = list
};
return await Task.FromResult(unitDataAnalysis);
}
catch (Exception ex)
{
_logger.LogError(ex, $"0C_132解析失败:{input.A.Code}-{input.DT.Fn}-{input.BaseHexMessage.HexMessageString},{ex.Message}");
return null;
}
}
private async Task<List<string>> AnalysisDataUnitAsync(List<string> hexMessageList)
{
List<string> values = new List<string>();
values.Add(hexMessageList.GetReadTime(4, 5));
int ratingCount = hexMessageList.GetRatingCount(9, 1);
values.Add(ratingCount.ToString());
for (int i = 0; i < ratingCount + 1; i++)
{
var arr = hexMessageList.GetRange(10 + (i * 4), 4);
var errorCode = arr.CheckErrorCode();
if (errorCode != null)
values.Add(errorCode.Item1);
else
{
var value = await _analysisStrategyContext.ExecuteAsync<List<string>, decimal>(nameof(Appendix_A11), arr); //从第10个开始每加4个字节为下一个值的开始
values.Add(value.ToString());
}
}
return values;
}
public List<AFN12_F132_AnalysisDto> GenerateFinalResult(int index, List<string> data, string remark = "",int afn=0,int fn=0)
{
List<AFN12_F132_AnalysisDto> list = new List<AFN12_F132_AnalysisDto>();
for (int i = index; i < data.Count; i++)
{
AFN12_F132_AnalysisDto meter = new AFN12_F132_AnalysisDto();
decimal value = 0;
var errorCode = data[i].CheckErrorCode();
if (errorCode != null)
meter.ValidData = false;
else
decimal.TryParse(data[i], out value);
meter.DataValue = value;
meter.DataType = $"{afn.ToString().PadLeft(2, '0')}_{fn}_{i - index}";
meter.ReadTime = Convert.ToDateTime($"{data[0].Substring(0, 4)}-{data[0].Substring(4, 2)}-{data[0].Substring(6, 2)} {data[0].Substring(8, 2)}:{data[0].Substring(10, 2)}:00");
list.Add(meter);
}
return list;
}
}
}

View File

@ -0,0 +1,107 @@
using JiShe.CollectBus.Common.Extensions;
using JiShe.CollectBus.Protocol.Contracts.Protocol.Dto;
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol.T37612012.Appendix;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.T37612012.AnalysisData.AFN_0CH
{
/// <summary>
/// 5.12.2.4.101 F145当月正向有功最大需量及发生时间总、费率 1M
/// </summary>
public class AFN12_F145_Analysis : IAnalysisStrategy<TB3761, UnitDataAnalysis<AFN12_F145_AnalysisDto>>
{
private readonly ILogger<AFN12_F145_Analysis> _logger;
private readonly AnalysisStrategyContext _analysisStrategyContext;
public AFN12_F145_Analysis(ILogger<AFN12_F145_Analysis> logger, AnalysisStrategyContext analysisStrategyContext)
{
_logger = logger;
_analysisStrategyContext = analysisStrategyContext;
}
public async Task<UnitDataAnalysis<AFN12_F145_AnalysisDto>> ExecuteAsync(TB3761 input)
{
try
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(input.UnitData?.HexMessageList);
List<string> datas = await AnalysisDataUnitAsync(input.UnitData.HexMessageList);
AFN12_F145_AnalysisDto data = GenerateFinalResult(2, datas, "当月正向有功最大需量及发生时间");
data.DataType = $"{input.AFN_FC.AFN.HexToDecStr().PadLeft(2, '0')}_{input.DT.Fn}";
UnitDataAnalysis<AFN12_F145_AnalysisDto> unitDataAnalysis = new UnitDataAnalysis<AFN12_F145_AnalysisDto>
{
Code = input.A.Code,
AFN = input.AFN_FC.AFN,
Fn = input.DT.Fn,
Pn = input.DA.Pn,
Data = data
};
return await Task.FromResult(unitDataAnalysis);
}
catch (Exception ex)
{
_logger.LogError(ex, $"0C_145解析失败:{input.A.Code}-{input.DT.Fn}-{input.BaseHexMessage.HexMessageString},{ex.Message}");
return null;
}
}
private async Task<List<string>> AnalysisDataUnitAsync(List<string> hexMessageList)
{
List<string> values = new List<string>();
values.Add(hexMessageList.GetReadTime(4, 5));
int ratingCount = hexMessageList.GetRatingCount(9, 1);
values.Add(ratingCount.ToString());
for (int i = 0; i < ratingCount + 1; i++)
{
var arr = hexMessageList.GetRange(10 + i * 7, 3);
var errorCode = arr.CheckErrorCode();
if (errorCode != null)
values.Add(errorCode.Item1);
else
{
var value = await _analysisStrategyContext.ExecuteAsync<List<string>, decimal>(nameof(Appendix_A23), arr);
values.Add(value.ToString());//正向有功总最大需量
}
arr = hexMessageList.GetRange(10 + i * 7 + 3, 4);
errorCode = arr.CheckErrorCode();
if (errorCode != null)
values.Add(errorCode.Item1);
else
{
var value = await _analysisStrategyContext.ExecuteAsync<List<string>, string>(nameof(Appendix_A17), arr);
values.Add(value.ToString());//正向有功总最大需量发生时间
}
}
return values;
}
public AFN12_F145_AnalysisDto GenerateFinalResult(int index, List<string> data, string remark = "")
{
AFN12_F145_AnalysisDto dto = new AFN12_F145_AnalysisDto();
decimal value = 0;
var errorCode = data[2].CheckErrorCode();
if (errorCode != null)
dto.ValidData = false;
else
decimal.TryParse(data[2], out value);
dto.DataValue = value;
dto.TimeSpan = $"{DateTime.Now.Year}-{data[3].Substring(0, 2)}-{data[3].Substring(2, 2)} {data[3].Substring(4, 2)}:{data[3].Substring(6, 2)}:00";//$"{data[0].Substring(0, 4)}-{data[0].Substring(4, 2)}-{data[0].Substring(6, 2)}";
if (DateTime.TryParse(dto.TimeSpan, out DateTime readingDate))
{
dto.ReadingDate = readingDate;
}
return dto;
}
}
}

View File

@ -0,0 +1,148 @@
using JiShe.CollectBus.Common.Consts;
using JiShe.CollectBus.Common.Enums;
using JiShe.CollectBus.Common.Extensions;
using JiShe.CollectBus.IotSystems.Ammeters;
using JiShe.CollectBus.Protocol.Contracts.Protocol.Dto;
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol.T37612012.AnalysisData;
using JiShe.CollectBus.Protocol.T37612012.Appendix;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.T37612012.AFN_0CH
{
/// <summary>
/// 5.12.2.4.105 F149上月上一结算日正向有功最大需量及发生时间总、费率 1M
/// </summary>
public class AFN12_F149_Analysis : IAnalysisStrategy<TB3761, UnitDataAnalysis<AnalysisBaseDto<decimal>>>
{
private readonly ILogger<AFN12_F149_Analysis> _logger;
private readonly AnalysisStrategyContext _analysisStrategyContext;
private readonly DataStorage _dataStorage;
public AFN12_F149_Analysis(ILogger<AFN12_F149_Analysis> logger, AnalysisStrategyContext analysisStrategyContext, DataStorage dataStorage)
{
_logger = logger;
_analysisStrategyContext = analysisStrategyContext;
_dataStorage= dataStorage;
}
public async Task<UnitDataAnalysis<AnalysisBaseDto<decimal>>> ExecuteAsync(TB3761 input)
{
try
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(input.UnitData?.HexMessageList);
List<string> datas = await AnalysisDataUnit(input.UnitData.HexMessageList);
string dataType = $"{input.AFN_FC.AFN.HexToDecStr().PadLeft(2, '0')}_{input.DT.Fn}";
AnalysisBaseDto<decimal> data = GenerateFinalResult(datas, dataType,"上月(上一结算日)正向有功最大需量及发生时间");
// 查询电表信息
AmmeterInfo ammeterInfo = await _dataStorage.GetMeterInfoAsync(data.MeterType.ToString(), "15");
if (ammeterInfo != null)
{
data.ProjectId = ammeterInfo.ProjectID;
data.MeterId = ammeterInfo.MeterId;
data.DatabaseBusiID=ammeterInfo.DatabaseBusiID;
data.MeterAddress= ammeterInfo.AmmerterAddress;
}
UnitDataAnalysis<AnalysisBaseDto<decimal>> unitDataAnalysis = new UnitDataAnalysis<AnalysisBaseDto<decimal>>
{
Code = input.A.Code!,
AFN = input.AFN_FC.AFN,
Fn = input.DT.Fn,
Pn = input.DA.Pn,
MSA=input.A.A3!.D1_D7!,
PSEQ=input.SEQ.PSEQ,
Data = data,
HexMessage=input.BaseHexMessage.HexMessageString,
MessageId=input.MessageId,
TimeDensity=1,
DensityUnit = DensityUnit.Month
};
await _dataStorage.SaveDataToIotDbAsync<decimal>(unitDataAnalysis);
return await Task.FromResult(unitDataAnalysis);
}
catch (Exception ex)
{
_logger.LogError(ex, $"0C_149解析失败:{input.A.Code}-{input.DT.Fn}-{input.BaseHexMessage.HexMessageString},{ex.Message}");
return null;
}
}
private async Task<List<string>> AnalysisDataUnit(List<string> hexMessageList)
{
List<string> values = new List<string>();
values.Add(hexMessageList.GetReadTime(4, 5));
int ratingCount = hexMessageList.GetRatingCount(9, 1);
values.Add(ratingCount.ToString());
for (int i = 0; i < ratingCount + 1; i++)
{
var arr = hexMessageList.GetRange(10 + i * 7, 3);
var errorCode = arr.CheckErrorCode();
if (errorCode!=null)
values.Add(errorCode.Item1);
else
{
var value = await _analysisStrategyContext.ExecuteAsync<List<string>, decimal>(nameof(Appendix_A23), arr);
values.Add(value.ToString());//正向有功总最大需量
}
arr = hexMessageList.GetRange(13 + i * 7, 4);
errorCode = arr.CheckErrorCode();
if (errorCode != null)
values.Add(errorCode.Item1);
else
{
var value = await _analysisStrategyContext.ExecuteAsync<List<string>, string>(nameof(Appendix_A17), arr);//正向有功总最大需量发生时间
values.Add(value);//正向有功总最大需量
}
}
return values;
}
public AnalysisBaseDto<decimal> GenerateFinalResult(List<string> data,string dataType, string filedDesc = "")
{
AnalysisBaseDto<decimal> dto = new AnalysisBaseDto<decimal>();
var errorCodeInfo = data[2].CheckErrorCode();
if (errorCodeInfo != null)
{
dto.ValidData = false;
dto.ErrorCodeMsg= errorCodeInfo.Item2;
}
decimal.TryParse(data[2], out decimal value);
dto.DataValue = value;
dto.MeterType= MeterTypeEnum.Ammeter;
//TODO:最大需量发生时间
errorCodeInfo = data[3].CheckErrorCode();
if (data[3].Length != 8 && errorCodeInfo != null)
{
dto.ValidData = false;
dto.ErrorCodeMsg = errorCodeInfo.Item2;
}
else
{
string timeSpan = $"{DateTime.Now.Year}-{data[3].Substring(0, 2)}-{data[3].Substring(2, 2)} {data[3].Substring(4, 2)}:{data[3].Substring(6, 2)}:00";
//TODO:时间标
if (!DateTime.TryParse(timeSpan, out DateTime dataTime))
dto.ValidData = false;
dto.DataTime = dataTime;
}
if (DateTime.Now.Month.Equals(1))//如果为1月份则日期减去1年
{
dto.DataTime = dto.DataTime.AddYears(-1);
dto.TimeSpan= dto.DataTime;
}
dto.FiledDesc = "上月(上一结算日)正向有功最大需量及发生时间";
dto.FiledName = dataType.GetDataFieldByGatherDataType() ?? string.Empty;
return dto;
}
}
}

View File

@ -0,0 +1,72 @@
using JiShe.CollectBus.Common.Extensions;
using JiShe.CollectBus.Protocol.Contracts.Protocol.Dto;
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.T37612012.AFN_0CH
{
/// <summary>
/// 水表抄读取
/// </summary>
public class AFN12_F188_Analysis : IAnalysisStrategy<TB3761, UnitDataAnalysis<AFN12_F149_AnalysisDto>>
{
private readonly ILogger<AFN12_F188_Analysis> _logger;
private readonly AnalysisStrategyContext _analysisStrategyContext;
public AFN12_F188_Analysis(ILogger<AFN12_F188_Analysis> logger, AnalysisStrategyContext analysisStrategyContext)
{
_logger = logger;
_analysisStrategyContext = analysisStrategyContext;
}
public async Task<UnitDataAnalysis<AFN12_F149_AnalysisDto>> ExecuteAsync(TB3761 input)
{
try
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(input.UnitData?.HexMessageList);
ArgumentNullException.ThrowIfNull(input.AFN_FC.AFN);
ArgumentNullException.ThrowIfNull(input.DT.Fn);
var data = GenerateFinalResult(input.UnitData.HexMessageList);
data.DataType = $"{input.AFN_FC.AFN.HexToDecStr().PadLeft(2, '0')}_{input.DT.Fn}";
UnitDataAnalysis<AFN12_F149_AnalysisDto> dto = new UnitDataAnalysis<AFN12_F149_AnalysisDto>
{
Code = input.A.Code,
AFN = input.AFN_FC.AFN,
Fn = input.DT.Fn,
Pn = input.DA.Pn,
Data = data
};
return await Task.FromResult(dto);
}
catch (Exception ex)
{
_logger.LogError(ex, $"0C_188解析失败:{input.A.Code}-{input.DT.Fn}-{input.BaseHexMessage?.HexMessageString},{ex.Message}");
return null;
}
}
public AFN12_F149_AnalysisDto GenerateFinalResult(List<string> hexMessageList)
{
AFN12_F149_AnalysisDto dto = new AFN12_F149_AnalysisDto();
decimal value = 0;
var arr = hexMessageList.GetRange(11, 4);
var errorCodeInfo = arr.CheckErrorCode();
if (errorCodeInfo != null)
{
dto.ValidData = false;
dto.ErrorCodeMsg = errorCodeInfo.Item2;
}
else
decimal.TryParse($"{arr[11]}{arr[12]}{arr[13]}.{arr[14]}", out value);
dto.DataValue = value;
//dto.ReadTime = Convert.ToDateTime($"{data[0].Substring(0, 4)}-{data[0].Substring(4, 2)}-{data[0].Substring(6, 2)} {data[0].Substring(8, 2)}:{data[0].Substring(10, 2)}:00");
return dto;
}
}
}

View File

@ -0,0 +1,113 @@
using JiShe.CollectBus.Common.Extensions;
using JiShe.CollectBus.Protocol.Contracts.Protocol.Dto;
using JiShe.CollectBus.Protocol.Dto;
using JiShe.CollectBus.Protocol.Interfaces;
using JiShe.CollectBus.Protocol.T37612012.Appendix;
using JiShe.CollectBus.Protocol3761;
using Microsoft.Extensions.Logging;
namespace JiShe.CollectBus.Protocol.T37612012.AFN_0CH
{
/// <summary>
/// 5.12.2.4.22 F25当前三相及总有/无功功率、功率因数,三相电压、电流、零序电流、视在功率
/// </summary>
public class AFN12_F25_Analysis : IAnalysisStrategy<TB3761, UnitDataAnalysis<List<AFN12_F25_AnalysisDto>>>
{
private readonly ILogger<AFN12_F25_Analysis> _logger;
private readonly AnalysisStrategyContext _analysisStrategyContext;
public AFN12_F25_Analysis(ILogger<AFN12_F25_Analysis> logger, AnalysisStrategyContext analysisStrategyContext)
{
_logger = logger;
_analysisStrategyContext = analysisStrategyContext;
}
public List<string> DataType { get; set; } = new List<string>() { "YGGL", "YGGL_A", "YGGL_B", "YGGL_C", "WGGL", "WGGL_A", "WGGL_B", "WGGL_C", "GLYS", "GLYS_A", "GLYS_B", "GLYS_C", "DY_A", "DY_B", "DY_C", "DL_A", "DL_B", "DL_C", "LXDL", "SZGL", "SZGL_A", "SZGL_B", "SZGL_C" };
public async Task<UnitDataAnalysis<List<AFN12_F25_AnalysisDto>>> ExecuteAsync(TB3761 input)
{
try
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(input.UnitData?.HexMessageList);
List<string> remarks = new List<string>() { "当前总有功功率", "当前A相有功功率", "当前B相有功功率", "当前C相有功功率", "当前总无功功率", "当前A相无功功率", "当前B相无功功率", "当前C相无功功率", "当前总功率因数", "当前A相功率因数", "当前B相功率因数", "当前C相功率因数", "当前A相电压", "当前B相电压", "当前C相电压", "当前A相电流", "当前C相电流", "当前 C相电流", "当前零序电流", "当前总视在功率", "当前A相视在功率", "当前B相视在功率", "当前C相视在功率" };
List<AFN12_F25_AnalysisDto> list = new List<AFN12_F25_AnalysisDto>();
List<string> data = await AnalysisDataUnitAsync(input.UnitData.HexMessageList);
for (int i = 1; i < data.Count; i++)
{
AFN12_F25_AnalysisDto dto = new AFN12_F25_AnalysisDto();
decimal value = 0;
var errorCodeInfo = data[i].CheckErrorCode();
if (errorCodeInfo != null)
{
dto.ValidData = false;
dto.ErrorCodeMsg = errorCodeInfo.Item2;
}
else
decimal.TryParse(data[i], out value);
dto.DataValue = value;
dto.DataType = $"{input.AFN_FC.AFN.HexToDecStr().PadLeft(2, '0')}_{input.DT.Fn}_{DataType[i-1]}";
dto.FiledName = DataType[i - 1];
dto.ReadTime = Convert.ToDateTime($"{data[0].Substring(0, 4)}-{data[0].Substring(4, 2)}-{data[0].Substring(6, 2)} {data[0].Substring(8, 2)}:{data[0].Substring(10, 2)}:00");
dto.FiledDesc = remarks[i - 1];
list.Add(dto);
}
UnitDataAnalysis<List<AFN12_F25_AnalysisDto>> unitDataAnalysis = new UnitDataAnalysis<List<AFN12_F25_AnalysisDto>>
{
Code = input.A.Code,
AFN = input.AFN_FC.AFN,
Fn = input.DT.Fn,
Pn = input.DA.Pn,
Data = list
};
return await Task.FromResult(unitDataAnalysis);
}
catch (Exception ex)
{
_logger.LogError(ex, $"0C_25解析失败:{input.A?.Code}-{input.DT?.Fn ?? 0}-{input?.BaseHexMessage?.HexMessageString},{ex.Message}");
return null;
}
}
private async Task<List<string>> AnalysisDataUnitAsync(List<string> hexMessageList)
{
List<string> values = new List<string>();
values.Add(hexMessageList.GetReadTime(4, 5));
values.AddRange(await CutOutAsync(9, 8, 3, nameof(Appendix_A9), hexMessageList));//解析 总/ABC相有功/无功功率
values.AddRange(await CutOutAsync(33, 4, 2, nameof(Appendix_A5), hexMessageList));//解析 总/ABC相功率因数
values.AddRange(await CutOutAsync(41, 3, 2, nameof(Appendix_A7), hexMessageList));//ABC相电压
values.AddRange(await CutOutAsync(47, 4, 3, nameof(Appendix_A25), hexMessageList));//ABC相/零序电流
values.AddRange(await CutOutAsync(59, 4, 3, nameof(Appendix_A9), hexMessageList));//总视\ABC相视在功率
return values;
}
/// <summary>
/// 截取值
/// </summary>
/// <param name="Index">开始位置</param>
/// <param name="count">总项数</param>
/// <param name="byteCount">每项字节数</param>
/// <param name="appendixName">解析方式</param>
/// <param name="hexMessageList">解析数据</param>
private async Task<List<string>> CutOutAsync(int Index, int count, int byteCount, string appendixName, List<string> hexMessageList)
{
List<string> values = new List<string>();
var temp = hexMessageList.GetRange(Index, count * byteCount);//截取位置(9),解析项*截取字节数(8*3)
for (int i = 0; i < count; i++)
{
var arr = temp.GetRange(i * byteCount, byteCount);
var errorCode = arr.CheckErrorCode();
if (errorCode!=null)
values.Add(errorCode.Item1);
else
{
var value = await _analysisStrategyContext.ExecuteAsync<List<string>, decimal>(appendixName, arr);
values.Add(value.ToString());
}
}
return values;
}
}
}

Some files were not shown because too many files have changed in this diff Show More