dev #3
30
.dockerignore
Normal file
30
.dockerignore
Normal 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/**
|
||||
20
Delete-BIN-OBJ-Folders.bat
Normal file
20
Delete-BIN-OBJ-Folders.bat
Normal 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
75
Dockerfile
Normal 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"]
|
||||
|
||||
|
||||
179
JiShe.CollectBus.Protocols.sln
Normal file
179
JiShe.CollectBus.Protocols.sln
Normal 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
|
||||
@ -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
7
NuGet.Config
Normal 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
77
PackageAndPublish.bat
Normal 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
|
||||
287
external/JiShe.CollectBus.PluginFileWatcher/Config.cs
vendored
Normal file
287
external/JiShe.CollectBus.PluginFileWatcher/Config.cs
vendored
Normal 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";
|
||||
}
|
||||
}
|
||||
277
external/JiShe.CollectBus.PluginFileWatcher/DbUtility.cs
vendored
Normal file
277
external/JiShe.CollectBus.PluginFileWatcher/DbUtility.cs
vendored
Normal 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, "生成测试数据时发生错误");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
external/JiShe.CollectBus.PluginFileWatcher/Dockerfile
vendored
Normal file
28
external/JiShe.CollectBus.PluginFileWatcher/Dockerfile
vendored
Normal 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"]
|
||||
481
external/JiShe.CollectBus.PluginFileWatcher/EventDatabaseManager.cs
vendored
Normal file
481
external/JiShe.CollectBus.PluginFileWatcher/EventDatabaseManager.cs
vendored
Normal 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; }
|
||||
}
|
||||
}
|
||||
745
external/JiShe.CollectBus.PluginFileWatcher/EventStorage.cs
vendored
Normal file
745
external/JiShe.CollectBus.PluginFileWatcher/EventStorage.cs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
254
external/JiShe.CollectBus.PluginFileWatcher/FileEvent.cs
vendored
Normal file
254
external/JiShe.CollectBus.PluginFileWatcher/FileEvent.cs
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
157
external/JiShe.CollectBus.PluginFileWatcher/FileWatcherUtils.cs
vendored
Normal file
157
external/JiShe.CollectBus.PluginFileWatcher/FileWatcherUtils.cs
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
external/JiShe.CollectBus.PluginFileWatcher/JiShe.CollectBus.PluginFileWatcher.csproj
vendored
Normal file
39
external/JiShe.CollectBus.PluginFileWatcher/JiShe.CollectBus.PluginFileWatcher.csproj
vendored
Normal 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>
|
||||
1110
external/JiShe.CollectBus.PluginFileWatcher/Program.cs
vendored
Normal file
1110
external/JiShe.CollectBus.PluginFileWatcher/Program.cs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
10
external/JiShe.CollectBus.PluginFileWatcher/Properties/launchSettings.json
vendored
Normal file
10
external/JiShe.CollectBus.PluginFileWatcher/Properties/launchSettings.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"profiles": {
|
||||
"JiShe.CollectBus.PluginFileWatcher": {
|
||||
"commandName": "Project"
|
||||
},
|
||||
"Container (Dockerfile)": {
|
||||
"commandName": "Docker"
|
||||
}
|
||||
}
|
||||
}
|
||||
88
external/JiShe.CollectBus.PluginFileWatcher/appsettings.json
vendored
Normal file
88
external/JiShe.CollectBus.PluginFileWatcher/appsettings.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>();
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
|
||||
|
||||
24
modules/JiShe.CollectBus.IoTDB/EnumInfo/EntityTypeEnum.cs
Normal file
24
modules/JiShe.CollectBus.IoTDB/EnumInfo/EntityTypeEnum.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
/// <summary>
|
||||
/// Session 工厂接口
|
||||
/// </summary>
|
||||
public interface IIoTDBSessionFactory:IDisposable
|
||||
public interface IIoTDbSessionFactory:IDisposable
|
||||
{
|
||||
IIoTDBSessionPool GetSessionPool(bool useTableSession);
|
||||
IIoTDbSessionPool GetSessionPool(bool useTableSession);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}`";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(); ;
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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();
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -91,8 +91,8 @@
|
||||
"SaslUserName": "lixiao",
|
||||
"SaslPassword": "lixiao1980",
|
||||
"KafkaReplicationFactor": 3,
|
||||
"NumPartitions": 1,
|
||||
"ServerTagName": "JiSheCollectBus2"
|
||||
"NumPartitions": 30,
|
||||
"ServerTagName": "JiSheCollectBus99"
|
||||
//"Topic": {
|
||||
// "ReplicationFactor": 3,
|
||||
// "NumPartitions": 1000
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
22
modules/JiShe.CollectBus.Kafka/Internal/HeadersFilter.cs
Normal file
22
modules/JiShe.CollectBus.Kafka/Internal/HeadersFilter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
11
modules/JiShe.CollectBus.Kafka/Internal/IKafkaSubscribe.cs
Normal file
11
modules/JiShe.CollectBus.Kafka/Internal/IKafkaSubscribe.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace JiShe.CollectBus.Kafka.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Kafka订阅者
|
||||
/// <para>
|
||||
/// 订阅者需要继承此接口并需要依赖注入,并使用<see cref="KafkaSubscribeAttribute" />标记
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface IKafkaSubscribe
|
||||
{
|
||||
}
|
||||
14
modules/JiShe.CollectBus.Kafka/Internal/ISubscribeAck.cs
Normal file
14
modules/JiShe.CollectBus.Kafka/Internal/ISubscribeAck.cs
Normal 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; }
|
||||
}
|
||||
52
modules/JiShe.CollectBus.Kafka/Internal/KafkaOptionConfig.cs
Normal file
52
modules/JiShe.CollectBus.Kafka/Internal/KafkaOptionConfig.cs
Normal 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; }
|
||||
|
||||
}
|
||||
111
modules/JiShe.CollectBus.Kafka/Internal/KafkaPollyPipeline.cs
Normal file
111
modules/JiShe.CollectBus.Kafka/Internal/KafkaPollyPipeline.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
103
modules/JiShe.CollectBus.Kafka/Internal/ReflectionHelper.cs
Normal file
103
modules/JiShe.CollectBus.Kafka/Internal/ReflectionHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
62
modules/JiShe.CollectBus.Kafka/Internal/SubscribeResult.cs
Normal file
62
modules/JiShe.CollectBus.Kafka/Internal/SubscribeResult.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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>);
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
namespace JiShe.CollectBus.Protocol.Contracts.ProtocolPools
|
||||
{
|
||||
public interface IPluginContainer
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -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>();
|
||||
|
||||
//}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
namespace JiShe.CollectBus.Protocol.T1882018.SendData
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回645报文结果
|
||||
/// </summary>
|
||||
public class Telemetry1882018PacketResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 报文体
|
||||
/// </summary>
|
||||
public List<string> Data { get; set; }
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 "其他协议";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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:当前正向有功电能示值(总、费率1~M)
|
||||
/// </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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)电能示值(总、费率 1~M)
|
||||
/// </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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@ -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:当前反向有功电能示值(总、费率 1~M)
|
||||
/// </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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@ -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)电能示值(总、费率 1~M)
|
||||
/// </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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@ -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:当月正向有功最大需量及发生时间(总、费率 1~M)
|
||||
/// </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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@ -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:上月(上一结算日)正向有功最大需量及发生时间(总、费率 1~M)
|
||||
/// </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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user