dev #2
4
.gitignore
vendored
4
.gitignore
vendored
@ -400,4 +400,6 @@ FodyWeavers.xsd
|
|||||||
|
|
||||||
# ABP Studio
|
# ABP Studio
|
||||||
**/.abpstudio/
|
**/.abpstudio/
|
||||||
/src/JiShe.CollectBus.Host/Plugins/*.dll
|
/web/JiShe.CollectBus.Host/Plugins/*.dll
|
||||||
|
/web/JiShe.CollectBus.Host/Plugins/JiShe.CollectBus.Protocol.dll
|
||||||
|
/web/JiShe.CollectBus.Host/Plugins/JiShe.CollectBus.Protocol.Test.dll
|
||||||
|
|||||||
@ -3,31 +3,49 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.9.34728.123
|
VisualStudioVersion = 17.9.34728.123
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Domain.Shared", "src\JiShe.CollectBus.Domain.Shared\JiShe.CollectBus.Domain.Shared.csproj", "{D64C1577-4929-4B60-939E-96DE1534891A}"
|
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
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Domain", "src\JiShe.CollectBus.Domain\JiShe.CollectBus.Domain.csproj", "{F2840BC7-0188-4606-9126-DADD0F5ABF7A}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Domain", "services\JiShe.CollectBus.Domain\JiShe.CollectBus.Domain.csproj", "{F2840BC7-0188-4606-9126-DADD0F5ABF7A}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Application.Contracts", "src\JiShe.CollectBus.Application.Contracts\JiShe.CollectBus.Application.Contracts.csproj", "{BD65D04F-08D5-40C1-8C24-77CA0BACB877}"
|
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
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Application", "src\JiShe.CollectBus.Application\JiShe.CollectBus.Application.csproj", "{78040F9E-3501-4A40-82DF-00A597710F35}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Application", "services\JiShe.CollectBus.Application\JiShe.CollectBus.Application.csproj", "{78040F9E-3501-4A40-82DF-00A597710F35}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{649A3FFA-182F-4E56-9717-E6A9A2BEC545}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.MongoDB", "modules\JiShe.CollectBus.MongoDB\JiShe.CollectBus.MongoDB.csproj", "{F1C58097-4C08-4D88-8976-6B3389391481}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.MongoDB", "src\JiShe.CollectBus.MongoDB\JiShe.CollectBus.MongoDB.csproj", "{F1C58097-4C08-4D88-8976-6B3389391481}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.HttpApi", "web\JiShe.CollectBus.HttpApi\JiShe.CollectBus.HttpApi.csproj", "{077AA5F8-8B61-420C-A6B5-0150A66FDB34}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.HttpApi", "src\JiShe.CollectBus.HttpApi\JiShe.CollectBus.HttpApi.csproj", "{077AA5F8-8B61-420C-A6B5-0150A66FDB34}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Host", "web\JiShe.CollectBus.Host\JiShe.CollectBus.Host.csproj", "{35829A15-4127-4F69-8BDE-9405DEAACA9A}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Host", "src\JiShe.CollectBus.Host\JiShe.CollectBus.Host.csproj", "{35829A15-4127-4F69-8BDE-9405DEAACA9A}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Common", "shared\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj", "{AD2F1928-4411-4511-B564-5FB996EC08B9}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Common", "src\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj", "{AD2F1928-4411-4511-B564-5FB996EC08B9}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Protocol", "protocols\JiShe.CollectBus.Protocol\JiShe.CollectBus.Protocol.csproj", "{C62EFF95-5C32-435F-BD78-6977E828F894}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Protocol", "src\JiShe.CollectBus.Protocol\JiShe.CollectBus.Protocol.csproj", "{C62EFF95-5C32-435F-BD78-6977E828F894}"
|
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
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Protocol.Contracts", "src\JiShe.CollectBus.Protocol.Contracts\JiShe.CollectBus.Protocol.Contracts.csproj", "{38C1808B-009A-418B-B17B-AB3626341B5D}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.DbMigrator", "services\JiShe.CollectBus.DbMigrator\JiShe.CollectBus.DbMigrator.csproj", "{8BA01C3D-297D-42DF-BD63-EF07202A0A67}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.DbMigrator", "src\JiShe.CollectBus.DbMigrator\JiShe.CollectBus.DbMigrator.csproj", "{8BA01C3D-297D-42DF-BD63-EF07202A0A67}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.FreeSql", "modules\JiShe.CollectBus.FreeSql\JiShe.CollectBus.FreeSql.csproj", "{FE0457D9-4038-4A17-8808-DCAD06CFC0A0}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.FreeSql", "src\JiShe.CollectBus.FreeSql\JiShe.CollectBus.FreeSql.csproj", "{FE0457D9-4038-4A17-8808-DCAD06CFC0A0}"
|
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}") = "3.Protocols", "3.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}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@ -83,23 +101,48 @@ Global
|
|||||||
{FE0457D9-4038-4A17-8808-DCAD06CFC0A0}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{FE0457D9-4038-4A17-8808-DCAD06CFC0A0}.Release|Any CPU.Build.0 = 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
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{D64C1577-4929-4B60-939E-96DE1534891A} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545}
|
{D64C1577-4929-4B60-939E-96DE1534891A} = {EBF7C01F-9B4F-48E6-8418-2CBFDA51EB0B}
|
||||||
{F2840BC7-0188-4606-9126-DADD0F5ABF7A} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545}
|
{F2840BC7-0188-4606-9126-DADD0F5ABF7A} = {BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23}
|
||||||
{BD65D04F-08D5-40C1-8C24-77CA0BACB877} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545}
|
{BD65D04F-08D5-40C1-8C24-77CA0BACB877} = {BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23}
|
||||||
{78040F9E-3501-4A40-82DF-00A597710F35} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545}
|
{78040F9E-3501-4A40-82DF-00A597710F35} = {BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23}
|
||||||
{F1C58097-4C08-4D88-8976-6B3389391481} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545}
|
{F1C58097-4C08-4D88-8976-6B3389391481} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59}
|
||||||
{077AA5F8-8B61-420C-A6B5-0150A66FDB34} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545}
|
{077AA5F8-8B61-420C-A6B5-0150A66FDB34} = {A02F7D8A-04DC-44D6-94D4-3F65712D6B94}
|
||||||
{35829A15-4127-4F69-8BDE-9405DEAACA9A} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545}
|
{35829A15-4127-4F69-8BDE-9405DEAACA9A} = {A02F7D8A-04DC-44D6-94D4-3F65712D6B94}
|
||||||
{AD2F1928-4411-4511-B564-5FB996EC08B9} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545}
|
{AD2F1928-4411-4511-B564-5FB996EC08B9} = {EBF7C01F-9B4F-48E6-8418-2CBFDA51EB0B}
|
||||||
{C62EFF95-5C32-435F-BD78-6977E828F894} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545}
|
{C62EFF95-5C32-435F-BD78-6977E828F894} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}
|
||||||
{38C1808B-009A-418B-B17B-AB3626341B5D} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545}
|
{38C1808B-009A-418B-B17B-AB3626341B5D} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC}
|
||||||
{8BA01C3D-297D-42DF-BD63-EF07202A0A67} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545}
|
{8BA01C3D-297D-42DF-BD63-EF07202A0A67} = {BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23}
|
||||||
{FE0457D9-4038-4A17-8808-DCAD06CFC0A0} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545}
|
{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}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {4324B3B4-B60B-4E3C-91D8-59576B4E26DD}
|
SolutionGuid = {4324B3B4-B60B-4E3C-91D8-59576B4E26DD}
|
||||||
|
|||||||
64
modules/JiShe.CollectBus.Cassandra/CassandraConfig.cs
Normal file
64
modules/JiShe.CollectBus.Cassandra/CassandraConfig.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Cassandra
|
||||||
|
{
|
||||||
|
public class CassandraConfig
|
||||||
|
{
|
||||||
|
public Node[] Nodes { get; set; }
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
public string Keyspace { get; set; }
|
||||||
|
public string ConsistencyLevel { get; set; }
|
||||||
|
public Pooling PoolingOptions { get; set; }
|
||||||
|
public Socket SocketOptions { get; set; }
|
||||||
|
public Query QueryOptions { get; set; }
|
||||||
|
|
||||||
|
public ReplicationStrategy ReplicationStrategy { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Pooling
|
||||||
|
{
|
||||||
|
public int CoreConnectionsPerHost { get; set; }
|
||||||
|
public int MaxConnectionsPerHost { get; set; }
|
||||||
|
public int MaxRequestsPerConnection { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Socket
|
||||||
|
{
|
||||||
|
public int ConnectTimeoutMillis { get; set; }
|
||||||
|
public int ReadTimeoutMillis { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Query
|
||||||
|
{
|
||||||
|
public string ConsistencyLevel { get; set; }
|
||||||
|
public string SerialConsistencyLevel { get; set; }
|
||||||
|
public bool DefaultIdempotence { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ReplicationStrategy
|
||||||
|
{
|
||||||
|
public string Class { get; set; }
|
||||||
|
public DataCenter[] DataCenters { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DataCenter
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int ReplicationFactor { get; set; }
|
||||||
|
public string Strategy { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Node
|
||||||
|
{
|
||||||
|
public string Host { get; set; }
|
||||||
|
public int Port { get; set; }
|
||||||
|
public string DataCenter { get; set; }
|
||||||
|
public string Rack { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
154
modules/JiShe.CollectBus.Cassandra/CassandraProvider.cs
Normal file
154
modules/JiShe.CollectBus.Cassandra/CassandraProvider.cs
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using Cassandra;
|
||||||
|
using Cassandra.Mapping;
|
||||||
|
using Cassandra.Data.Linq;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Volo.Abp.DependencyInjection;
|
||||||
|
using JiShe.CollectBus.Common.Attributes;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Cassandra
|
||||||
|
{
|
||||||
|
public class CassandraProvider : IDisposable, ICassandraProvider, ISingletonDependency
|
||||||
|
{
|
||||||
|
private readonly ILogger<CassandraProvider> _logger;
|
||||||
|
|
||||||
|
public Cluster Instance { get; set; }
|
||||||
|
|
||||||
|
public ISession Session { get; set; }
|
||||||
|
|
||||||
|
public CassandraConfig CassandraConfig { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
/// <param name="logger"></param>
|
||||||
|
public CassandraProvider(
|
||||||
|
IOptions<CassandraConfig> options,
|
||||||
|
ILogger<CassandraProvider> logger)
|
||||||
|
{
|
||||||
|
CassandraConfig = options.Value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task InitClusterAndSessionAsync()
|
||||||
|
{
|
||||||
|
InitClusterAndSession();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitClusterAndSession()
|
||||||
|
{
|
||||||
|
GetCluster((keyspace) =>
|
||||||
|
{
|
||||||
|
GetSession(keyspace);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cluster GetCluster(Action<string?>? callback=null)
|
||||||
|
{
|
||||||
|
var clusterBuilder = Cluster.Builder();
|
||||||
|
|
||||||
|
// 添加多个节点
|
||||||
|
foreach (var node in CassandraConfig.Nodes)
|
||||||
|
{
|
||||||
|
clusterBuilder.AddContactPoint(node.Host)
|
||||||
|
.WithPort(node.Port);
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterBuilder.WithCredentials(CassandraConfig.Username, CassandraConfig.Password);
|
||||||
|
|
||||||
|
// 优化连接池配置
|
||||||
|
var poolingOptions = new PoolingOptions()
|
||||||
|
.SetCoreConnectionsPerHost(HostDistance.Local, CassandraConfig.PoolingOptions.CoreConnectionsPerHost)
|
||||||
|
.SetMaxConnectionsPerHost(HostDistance.Local, CassandraConfig.PoolingOptions.MaxConnectionsPerHost)
|
||||||
|
.SetMaxRequestsPerConnection(CassandraConfig.PoolingOptions.MaxRequestsPerConnection)
|
||||||
|
.SetHeartBeatInterval(30000); // 30秒心跳
|
||||||
|
|
||||||
|
clusterBuilder.WithPoolingOptions(poolingOptions);
|
||||||
|
|
||||||
|
// 优化Socket配置
|
||||||
|
var socketOptions = new SocketOptions()
|
||||||
|
.SetConnectTimeoutMillis(CassandraConfig.SocketOptions.ConnectTimeoutMillis)
|
||||||
|
.SetReadTimeoutMillis(CassandraConfig.SocketOptions.ReadTimeoutMillis)
|
||||||
|
.SetTcpNoDelay(true) // 启用Nagle算法
|
||||||
|
.SetKeepAlive(true) // 启用TCP保活
|
||||||
|
.SetReceiveBufferSize(32768) // 32KB接收缓冲区
|
||||||
|
.SetSendBufferSize(32768); // 32KB发送缓冲区
|
||||||
|
|
||||||
|
clusterBuilder.WithSocketOptions(socketOptions);
|
||||||
|
|
||||||
|
// 优化查询选项
|
||||||
|
var queryOptions = new QueryOptions()
|
||||||
|
.SetConsistencyLevel((ConsistencyLevel)Enum.Parse(typeof(ConsistencyLevel), CassandraConfig.QueryOptions.ConsistencyLevel))
|
||||||
|
.SetSerialConsistencyLevel((ConsistencyLevel)Enum.Parse(typeof(ConsistencyLevel), CassandraConfig.QueryOptions.SerialConsistencyLevel))
|
||||||
|
.SetDefaultIdempotence(CassandraConfig.QueryOptions.DefaultIdempotence)
|
||||||
|
.SetPageSize(5000); // 增加页面大小
|
||||||
|
|
||||||
|
clusterBuilder.WithQueryOptions(queryOptions);
|
||||||
|
|
||||||
|
// 启用压缩
|
||||||
|
clusterBuilder.WithCompression(CompressionType.LZ4);
|
||||||
|
|
||||||
|
// 配置重连策略
|
||||||
|
clusterBuilder.WithReconnectionPolicy(new ExponentialReconnectionPolicy(1000, 10 * 60 * 1000));
|
||||||
|
Instance = clusterBuilder.Build();
|
||||||
|
callback?.Invoke(null);
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISession GetSession(string? keyspace = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(keyspace))
|
||||||
|
{
|
||||||
|
keyspace = CassandraConfig.Keyspace;
|
||||||
|
}
|
||||||
|
Session = Instance.Connect();
|
||||||
|
var replication = GetReplicationStrategy();
|
||||||
|
Session.CreateKeyspaceIfNotExists(keyspace, replication);
|
||||||
|
Session.ChangeKeyspace(keyspace);
|
||||||
|
return Session;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, string> GetReplicationStrategy()
|
||||||
|
{
|
||||||
|
var strategy = CassandraConfig.ReplicationStrategy.Class;
|
||||||
|
var dataCenters = CassandraConfig.ReplicationStrategy.DataCenters;
|
||||||
|
|
||||||
|
switch (strategy)
|
||||||
|
{
|
||||||
|
case "NetworkTopologyStrategy":
|
||||||
|
var networkDic = new Dictionary<string, string> { { "class", "NetworkTopologyStrategy" } };
|
||||||
|
foreach (var dataCenter in dataCenters)
|
||||||
|
{
|
||||||
|
networkDic.Add(dataCenter.Name, dataCenter.ReplicationFactor.ToString());
|
||||||
|
}
|
||||||
|
return networkDic;
|
||||||
|
case "SimpleStrategy":
|
||||||
|
var dic = new Dictionary<string, string> { { "class", "SimpleStrategy" } };
|
||||||
|
if (dataCenters.Length >= 1)
|
||||||
|
{
|
||||||
|
dic.Add("replication_factor", dataCenters[0].ReplicationFactor.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogError("SimpleStrategy 不支持多个数据中心!");
|
||||||
|
}
|
||||||
|
return dic;
|
||||||
|
default:
|
||||||
|
throw new ArgumentNullException($"Strategy", "Strategy配置错误!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Instance.Dispose();
|
||||||
|
Session.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
modules/JiShe.CollectBus.Cassandra/CassandraQueryOptimizer.cs
Normal file
156
modules/JiShe.CollectBus.Cassandra/CassandraQueryOptimizer.cs
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
modules/JiShe.CollectBus.Cassandra/CassandraRepository.cs
Normal file
75
modules/JiShe.CollectBus.Cassandra/CassandraRepository.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
using Cassandra;
|
||||||
|
using Cassandra.Data.Linq;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
private readonly ICassandraProvider _cassandraProvider;
|
||||||
|
public CassandraRepository(ICassandraProvider cassandraProvider, MappingConfiguration mappingConfig)
|
||||||
|
{
|
||||||
|
_cassandraProvider = cassandraProvider;
|
||||||
|
Mapper = new Mapper(cassandraProvider.Session, mappingConfig);
|
||||||
|
cassandraProvider.Session.CreateTable<TEntity>(cassandraProvider.CassandraConfig.Keyspace);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly IMapper Mapper;
|
||||||
|
|
||||||
|
public virtual async Task<TEntity> GetAsync(TKey id)
|
||||||
|
{
|
||||||
|
return await Mapper.SingleOrDefaultAsync<TEntity>("WHERE id = ?", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<List<TEntity>> GetListAsync()
|
||||||
|
{
|
||||||
|
return (await Mapper.FetchAsync<TEntity>()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<TEntity> InsertAsync(TEntity entity)
|
||||||
|
{
|
||||||
|
await Mapper.InsertAsync(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<TEntity> UpdateAsync(TEntity entity)
|
||||||
|
{
|
||||||
|
await Mapper.UpdateAsync(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task DeleteAsync(TEntity entity)
|
||||||
|
{
|
||||||
|
await Mapper.DeleteAsync(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task DeleteAsync(TKey id)
|
||||||
|
{
|
||||||
|
await Mapper.DeleteAsync<TEntity>("WHERE id = ?", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<List<TEntity>> GetPagedListAsync(
|
||||||
|
int skipCount,
|
||||||
|
int maxResultCount,
|
||||||
|
string sorting)
|
||||||
|
{
|
||||||
|
var cql = $"SELECT * FROM {typeof(TEntity).Name.ToLower()}";
|
||||||
|
if (!string.IsNullOrWhiteSpace(sorting))
|
||||||
|
{
|
||||||
|
cql += $" ORDER BY {sorting}";
|
||||||
|
}
|
||||||
|
cql += $" LIMIT {maxResultCount} OFFSET {skipCount}";
|
||||||
|
|
||||||
|
return (await Mapper.FetchAsync<TEntity>(cql)).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
using Cassandra;
|
||||||
|
using Cassandra.Mapping;
|
||||||
|
using JiShe.CollectBus.Cassandra.Mappers;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Volo.Abp;
|
||||||
|
using Volo.Abp.Autofac;
|
||||||
|
using Volo.Abp.Modularity;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Cassandra
|
||||||
|
{
|
||||||
|
[DependsOn(
|
||||||
|
typeof(AbpAutofacModule)
|
||||||
|
)]
|
||||||
|
public class CollectBusCassandraModule : AbpModule
|
||||||
|
{
|
||||||
|
public override Task ConfigureServicesAsync(ServiceConfigurationContext context)
|
||||||
|
{
|
||||||
|
Configure<CassandraConfig>(context.Services.GetConfiguration().GetSection("Cassandra"));
|
||||||
|
context.AddCassandra();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
|
||||||
|
{
|
||||||
|
await context.UseCassandra();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
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 Volo.Abp;
|
||||||
|
using Volo.Abp.Modularity;
|
||||||
|
|
||||||
|
// ReSharper disable once CheckNamespace
|
||||||
|
namespace Microsoft.Extensions.DependencyInjection
|
||||||
|
{
|
||||||
|
public static class ApplicationInitializationContextExtensions
|
||||||
|
{
|
||||||
|
public static async Task UseCassandra(this ApplicationInitializationContext context)
|
||||||
|
{
|
||||||
|
var service = context.ServiceProvider;
|
||||||
|
var cassandraProvider = service.GetRequiredService<ICassandraProvider>();
|
||||||
|
await cassandraProvider.InitClusterAndSessionAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static void AddCassandra(this ServiceConfigurationContext context)
|
||||||
|
{
|
||||||
|
context.Services.AddTransient(typeof(ICassandraRepository<,>), typeof(CassandraRepository<,>));
|
||||||
|
context.Services.AddSingleton(new MappingConfiguration()
|
||||||
|
.Define(new CollectBusMapping()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
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;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Cassandra.Extensions
|
||||||
|
{
|
||||||
|
public static class SessionExtension
|
||||||
|
{
|
||||||
|
public static void CreateTable<TEntity>(this ISession session,string? defaultKeyspace=null) where TEntity : class
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (primaryKey == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"No primary key defined for type {type.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var cql = new StringBuilder();
|
||||||
|
cql.Append($"CREATE TABLE IF NOT EXISTS {tableKeyspace}.{tableName} (");
|
||||||
|
|
||||||
|
foreach (var prop in properties)
|
||||||
|
{
|
||||||
|
var ignoreAttribute = prop.GetCustomAttribute<CassandraIgnoreAttribute>();
|
||||||
|
if (ignoreAttribute != null) continue;
|
||||||
|
var columnName = prop.Name.ToLower();
|
||||||
|
var cqlType = GetCassandraType(prop.PropertyType);
|
||||||
|
|
||||||
|
cql.Append($"{columnName} {cqlType}, ");
|
||||||
|
}
|
||||||
|
cql.Length -= 2; // Remove last comma and space
|
||||||
|
cql.Append($", PRIMARY KEY ({primaryKey.Name.ToLower()}))");
|
||||||
|
|
||||||
|
session.Execute(cql.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCassandraType(Type type)
|
||||||
|
{
|
||||||
|
// 基础类型处理
|
||||||
|
switch (Type.GetTypeCode(type))
|
||||||
|
{
|
||||||
|
case TypeCode.String: return "text";
|
||||||
|
case TypeCode.Int32: return "int";
|
||||||
|
case TypeCode.Int64: return "bigint";
|
||||||
|
case TypeCode.Boolean: return "boolean";
|
||||||
|
case TypeCode.DateTime: return "timestamp";
|
||||||
|
case TypeCode.Byte: return "tinyint";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == typeof(Guid)) return "uuid";
|
||||||
|
if (type == typeof(DateTimeOffset)) return "timestamp";
|
||||||
|
if (type == typeof(Byte[])) return "blob";
|
||||||
|
|
||||||
|
// 处理集合类型
|
||||||
|
if (type.IsGenericType)
|
||||||
|
{
|
||||||
|
var genericType = type.GetGenericTypeDefinition();
|
||||||
|
var elementType = type.GetGenericArguments()[0];
|
||||||
|
|
||||||
|
if (genericType == typeof(List<>))
|
||||||
|
return $"list<{GetCassandraType(elementType)}>";
|
||||||
|
if (genericType == typeof(HashSet<>))
|
||||||
|
return $"set<{GetCassandraType(elementType)}>";
|
||||||
|
if (genericType == typeof(Dictionary<,>))
|
||||||
|
{
|
||||||
|
var keyType = type.GetGenericArguments()[0];
|
||||||
|
var valueType = type.GetGenericArguments()[1];
|
||||||
|
return $"map<{GetCassandraType(keyType)}, {GetCassandraType(valueType)}>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotSupportedException($"不支持的类型: {type.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
26
modules/JiShe.CollectBus.Cassandra/ICassandraProvider.cs
Normal file
26
modules/JiShe.CollectBus.Cassandra/ICassandraProvider.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Cassandra;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Cassandra
|
||||||
|
{
|
||||||
|
public interface ICassandraProvider
|
||||||
|
{
|
||||||
|
Cluster Instance { get;}
|
||||||
|
|
||||||
|
ISession Session { get;}
|
||||||
|
|
||||||
|
CassandraConfig CassandraConfig { get; }
|
||||||
|
|
||||||
|
ISession GetSession(string? keyspace = null);
|
||||||
|
|
||||||
|
Cluster GetCluster(Action<string?>? callback = null);
|
||||||
|
|
||||||
|
void InitClusterAndSession();
|
||||||
|
|
||||||
|
Task InitClusterAndSessionAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
20
modules/JiShe.CollectBus.Cassandra/ICassandraRepository.cs
Normal file
20
modules/JiShe.CollectBus.Cassandra/ICassandraRepository.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Volo.Abp.Domain.Entities;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Cassandra
|
||||||
|
{
|
||||||
|
public interface ICassandraRepository<TEntity, TKey> where TEntity : class
|
||||||
|
{
|
||||||
|
Task<TEntity> GetAsync(TKey id);
|
||||||
|
Task<List<TEntity>> GetListAsync();
|
||||||
|
Task<TEntity> InsertAsync(TEntity entity);
|
||||||
|
Task<TEntity> UpdateAsync(TEntity entity);
|
||||||
|
Task DeleteAsync(TEntity entity);
|
||||||
|
Task DeleteAsync(TKey id);
|
||||||
|
Task<List<TEntity>> GetPagedListAsync(int skipCount, int maxResultCount, string sorting);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CassandraCSharpDriver" Version="3.22.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
||||||
|
<PackageReference Include="Volo.Abp.Autofac" Version="8.3.3" />
|
||||||
|
<PackageReference Include="Volo.Abp.Core" Version="8.3.3" />
|
||||||
|
<PackageReference Include="Volo.Abp.Ddd.Domain" 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" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
using Cassandra.Mapping;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using JiShe.CollectBus.IotSystems.MessageIssueds;
|
||||||
|
using static Cassandra.QueryTrace;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Cassandra.Mappers
|
||||||
|
{
|
||||||
|
public class CollectBusMapping: Mappings
|
||||||
|
{
|
||||||
|
public CollectBusMapping()
|
||||||
|
{
|
||||||
|
For<MessageIssued>()
|
||||||
|
.Column(e => e.Type, cm => cm.WithName("type").WithDbType<int>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
using JiShe.CollectBus.FreeRedis.Options;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Volo.Abp.Modularity;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.FreeRedis
|
||||||
|
{
|
||||||
|
public class CollectBusFreeRedisModule : AbpModule
|
||||||
|
{
|
||||||
|
public override void ConfigureServices(ServiceConfigurationContext context)
|
||||||
|
{
|
||||||
|
var configuration = context.Services.GetConfiguration();
|
||||||
|
|
||||||
|
Configure<FreeRedisOptions>(options =>
|
||||||
|
{
|
||||||
|
configuration.GetSection("Redis").Bind(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
502
modules/JiShe.CollectBus.FreeRedis/FreeRedisProvider.cs
Normal file
502
modules/JiShe.CollectBus.FreeRedis/FreeRedisProvider.cs
Normal file
@ -0,0 +1,502 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using FreeRedis;
|
||||||
|
using JiShe.CollectBus.Common.Helpers;
|
||||||
|
using JiShe.CollectBus.FreeRedis.Options;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Volo.Abp.DependencyInjection;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.FreeRedis
|
||||||
|
{
|
||||||
|
|
||||||
|
public class FreeRedisProvider : IFreeRedisProvider, ISingletonDependency
|
||||||
|
{
|
||||||
|
|
||||||
|
private readonly FreeRedisOptions _option;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// FreeRedis
|
||||||
|
/// </summary>
|
||||||
|
public FreeRedisProvider(IOptions<FreeRedisOptions> options)
|
||||||
|
{
|
||||||
|
_option = options.Value;
|
||||||
|
GetInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RedisClient Instance { get; set; } = new(string.Empty);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 FreeRedis 客户端
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public IRedisClient GetInstance()
|
||||||
|
{
|
||||||
|
|
||||||
|
var connectionString = $"{_option.Configuration},defaultdatabase={_option.DefaultDB},MaxPoolSize={_option.MaxPoolSize}";
|
||||||
|
Instance = new RedisClient(connectionString);
|
||||||
|
Instance.Serialize = obj => BusJsonSerializer.Serialize(obj);
|
||||||
|
Instance.Deserialize = (json, type) => BusJsonSerializer.Deserialize(json, type);
|
||||||
|
Instance.Notice += (s, e) => Trace.WriteLine(e.Log);
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
///// <summary>
|
||||||
|
///// 单个添加数据
|
||||||
|
///// </summary>
|
||||||
|
///// <typeparam name="T"></typeparam>
|
||||||
|
///// <param name="redisCacheKey">主数据存储Hash缓存Key</param>
|
||||||
|
///// <param name="redisCacheFocusIndexKey">集中器索引Set缓存Key</param>
|
||||||
|
///// <param name="redisCacheScoresIndexKey">集中器排序索引ZSET缓存Key</param>
|
||||||
|
///// <param name="redisCacheGlobalIndexKey">集中器采集频率分组全局索引ZSet缓存Key</param>
|
||||||
|
///// <param name="data">表计信息</param>
|
||||||
|
///// <param name="timestamp">可选时间戳</param>
|
||||||
|
///// <returns></returns>
|
||||||
|
//public async Task AddMeterCacheData<T>(
|
||||||
|
//string redisCacheKey,
|
||||||
|
//string redisCacheFocusIndexKey,
|
||||||
|
//string redisCacheScoresIndexKey,
|
||||||
|
//string redisCacheGlobalIndexKey,
|
||||||
|
//T data,
|
||||||
|
//DateTimeOffset? timestamp = null) where T : DeviceCacheBasicModel
|
||||||
|
//{
|
||||||
|
// // 参数校验增强
|
||||||
|
// if (data == null || string.IsNullOrWhiteSpace(redisCacheKey)
|
||||||
|
// || string.IsNullOrWhiteSpace(redisCacheFocusIndexKey)
|
||||||
|
// || string.IsNullOrWhiteSpace(redisCacheScoresIndexKey)
|
||||||
|
// || string.IsNullOrWhiteSpace(redisCacheGlobalIndexKey))
|
||||||
|
// {
|
||||||
|
// throw new ArgumentException($"{nameof(AddMeterCacheData)} 参数异常,-101");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 计算组合score(分类ID + 时间戳)
|
||||||
|
// var actualTimestamp = timestamp ?? DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
// long scoreValue = ((long)data.FocusId << 32) | (uint)actualTimestamp.Ticks;
|
||||||
|
|
||||||
|
// //全局索引写入
|
||||||
|
// long globalScore = actualTimestamp.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
// // 使用事务保证原子性
|
||||||
|
// using (var trans = Instance.Multi())
|
||||||
|
// {
|
||||||
|
// // 主数据存储Hash
|
||||||
|
// trans.HSet(redisCacheKey, data.MemberID, data.Serialize());
|
||||||
|
|
||||||
|
// // 分类索引
|
||||||
|
// trans.SAdd(redisCacheFocusIndexKey, data.MemberID);
|
||||||
|
|
||||||
|
// // 排序索引使用ZSET
|
||||||
|
// trans.ZAdd(redisCacheScoresIndexKey, scoreValue, data.MemberID);
|
||||||
|
|
||||||
|
// //全局索引
|
||||||
|
// trans.ZAdd(redisCacheGlobalIndexKey, globalScore, data.MemberID);
|
||||||
|
|
||||||
|
// var results = trans.Exec();
|
||||||
|
|
||||||
|
// if (results == null || results.Length <= 0)
|
||||||
|
// throw new Exception($"{nameof(AddMeterCacheData)} 事务提交失败,-102");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// await Task.CompletedTask;
|
||||||
|
//}
|
||||||
|
|
||||||
|
///// <summary>
|
||||||
|
///// 批量添加数据
|
||||||
|
///// </summary>
|
||||||
|
///// <typeparam name="T"></typeparam>
|
||||||
|
///// <param name="redisCacheKey">主数据存储Hash缓存Key</param>
|
||||||
|
///// <param name="redisCacheFocusIndexKey">集中器索引Set缓存Key</param>
|
||||||
|
///// <param name="redisCacheScoresIndexKey">集中器排序索引ZSET缓存Key</param>
|
||||||
|
///// <param name="redisCacheGlobalIndexKey">集中器采集频率分组全局索引ZSet缓存Key</param>
|
||||||
|
///// <param name="items">数据集合</param>
|
||||||
|
///// <param name="timestamp">可选时间戳</param>
|
||||||
|
///// <returns></returns>
|
||||||
|
//public async Task BatchAddMeterData<T>(
|
||||||
|
//string redisCacheKey,
|
||||||
|
//string redisCacheFocusIndexKey,
|
||||||
|
//string redisCacheScoresIndexKey,
|
||||||
|
//string redisCacheGlobalIndexKey,
|
||||||
|
//IEnumerable<T> items,
|
||||||
|
//DateTimeOffset? timestamp = null) where T : DeviceCacheBasicModel
|
||||||
|
//{
|
||||||
|
// if (items == null
|
||||||
|
// || items.Count() <=0
|
||||||
|
// || string.IsNullOrWhiteSpace(redisCacheKey)
|
||||||
|
// || string.IsNullOrWhiteSpace(redisCacheFocusIndexKey)
|
||||||
|
// || string.IsNullOrWhiteSpace(redisCacheScoresIndexKey)
|
||||||
|
// || string.IsNullOrWhiteSpace(redisCacheGlobalIndexKey))
|
||||||
|
// {
|
||||||
|
// throw new ArgumentException($"{nameof(BatchAddMeterData)} 参数异常,-101");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const int BATCH_SIZE = 1000; // 每批1000条
|
||||||
|
// var semaphore = new SemaphoreSlim(Environment.ProcessorCount * 2);
|
||||||
|
|
||||||
|
// foreach (var batch in items.Batch(BATCH_SIZE))
|
||||||
|
// {
|
||||||
|
// await semaphore.WaitAsync();
|
||||||
|
|
||||||
|
// _ = Task.Run(() =>
|
||||||
|
// {
|
||||||
|
// using (var pipe = Instance.StartPipe())
|
||||||
|
// {
|
||||||
|
// foreach (var item in batch)
|
||||||
|
// {
|
||||||
|
// // 计算组合score(分类ID + 时间戳)
|
||||||
|
// var actualTimestamp = timestamp ?? DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
// long scoreValue = ((long)item.FocusId << 32) | (uint)actualTimestamp.Ticks;
|
||||||
|
|
||||||
|
// //全局索引写入
|
||||||
|
// long globalScore = actualTimestamp.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
// // 主数据存储Hash
|
||||||
|
// pipe.HSet(redisCacheKey, item.MemberID, item.Serialize());
|
||||||
|
|
||||||
|
// // 分类索引Set
|
||||||
|
// pipe.SAdd(redisCacheFocusIndexKey, item.MemberID);
|
||||||
|
|
||||||
|
// // 排序索引使用ZSET
|
||||||
|
// pipe.ZAdd(redisCacheScoresIndexKey, scoreValue, item.MemberID);
|
||||||
|
|
||||||
|
// //全局索引
|
||||||
|
// pipe.ZAdd(redisCacheGlobalIndexKey, globalScore, item.MemberID);
|
||||||
|
// }
|
||||||
|
// pipe.EndPipe();
|
||||||
|
// }
|
||||||
|
// semaphore.Release();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// await Task.CompletedTask;
|
||||||
|
//}
|
||||||
|
|
||||||
|
///// <summary>
|
||||||
|
///// 删除指定redis缓存key的缓存数据
|
||||||
|
///// </summary>
|
||||||
|
///// <typeparam name="T"></typeparam>
|
||||||
|
///// <param name="redisCacheKey">主数据存储Hash缓存Key</param>
|
||||||
|
///// <param name="redisCacheFocusIndexKey">集中器索引Set缓存Key</param>
|
||||||
|
///// <param name="redisCacheScoresIndexKey">集中器排序索引ZSET缓存Key</param>
|
||||||
|
///// <param name="redisCacheGlobalIndexKey">集中器采集频率分组全局索引ZSet缓存Key</param>
|
||||||
|
///// <param name="data">表计信息</param>
|
||||||
|
///// <returns></returns>
|
||||||
|
//public async Task RemoveMeterData<T>(
|
||||||
|
//string redisCacheKey,
|
||||||
|
//string redisCacheFocusIndexKey,
|
||||||
|
//string redisCacheScoresIndexKey,
|
||||||
|
//string redisCacheGlobalIndexKey,
|
||||||
|
//T data) where T : DeviceCacheBasicModel
|
||||||
|
//{
|
||||||
|
|
||||||
|
// if (data == null
|
||||||
|
// || string.IsNullOrWhiteSpace(redisCacheKey)
|
||||||
|
// || string.IsNullOrWhiteSpace(redisCacheFocusIndexKey)
|
||||||
|
// || string.IsNullOrWhiteSpace(redisCacheScoresIndexKey)
|
||||||
|
// || string.IsNullOrWhiteSpace(redisCacheGlobalIndexKey))
|
||||||
|
// {
|
||||||
|
// throw new ArgumentException($"{nameof(RemoveMeterData)} 参数异常,-101");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const string luaScript = @"
|
||||||
|
// local mainKey = KEYS[1]
|
||||||
|
// local focusIndexKey = KEYS[2]
|
||||||
|
// local scoresIndexKey = KEYS[3]
|
||||||
|
// local globalIndexKey = KEYS[4]
|
||||||
|
// local member = ARGV[1]
|
||||||
|
|
||||||
|
// local deleted = 0
|
||||||
|
// if redis.call('HDEL', mainKey, member) > 0 then
|
||||||
|
// deleted = 1
|
||||||
|
// end
|
||||||
|
|
||||||
|
// redis.call('SREM', focusIndexKey, member)
|
||||||
|
// redis.call('ZREM', scoresIndexKey, member)
|
||||||
|
// redis.call('ZREM', globalIndexKey, member)
|
||||||
|
// return deleted
|
||||||
|
// ";
|
||||||
|
|
||||||
|
// var keys = new[]
|
||||||
|
// {
|
||||||
|
// redisCacheKey,
|
||||||
|
// redisCacheFocusIndexKey,
|
||||||
|
// redisCacheScoresIndexKey,
|
||||||
|
// redisCacheGlobalIndexKey
|
||||||
|
// };
|
||||||
|
|
||||||
|
// var result = await Instance.EvalAsync(luaScript, keys, new[] { data.MemberID });
|
||||||
|
|
||||||
|
// if ((int)result == 0)
|
||||||
|
// throw new KeyNotFoundException("指定数据不存在");
|
||||||
|
//}
|
||||||
|
|
||||||
|
///// <summary>
|
||||||
|
///// 修改表计缓存信息
|
||||||
|
///// </summary>
|
||||||
|
///// <typeparam name="T"></typeparam>
|
||||||
|
///// <param name="redisCacheKey">主数据存储Hash缓存Key</param>
|
||||||
|
///// <param name="oldRedisCacheFocusIndexKey">旧集中器索引Set缓存Key</param>
|
||||||
|
///// <param name="newRedisCacheFocusIndexKey">新集中器索引Set缓存Key</param>
|
||||||
|
///// <param name="redisCacheScoresIndexKey">集中器排序索引ZSET缓存Key</param>
|
||||||
|
///// <param name="redisCacheGlobalIndexKey">集中器采集频率分组全局索引ZSet缓存Key</param>
|
||||||
|
///// <param name="newData">表计信息</param>
|
||||||
|
///// <param name="newTimestamp">可选时间戳</param>
|
||||||
|
///// <returns></returns>
|
||||||
|
//public async Task UpdateMeterData<T>(
|
||||||
|
//string redisCacheKey,
|
||||||
|
//string oldRedisCacheFocusIndexKey,
|
||||||
|
//string newRedisCacheFocusIndexKey,
|
||||||
|
//string redisCacheScoresIndexKey,
|
||||||
|
//string redisCacheGlobalIndexKey,
|
||||||
|
//T newData,
|
||||||
|
//DateTimeOffset? newTimestamp = null) where T : DeviceCacheBasicModel
|
||||||
|
//{
|
||||||
|
// if (newData == null
|
||||||
|
// || string.IsNullOrWhiteSpace(redisCacheKey)
|
||||||
|
// || string.IsNullOrWhiteSpace(oldRedisCacheFocusIndexKey)
|
||||||
|
// || string.IsNullOrWhiteSpace(newRedisCacheFocusIndexKey)
|
||||||
|
// || string.IsNullOrWhiteSpace(redisCacheScoresIndexKey)
|
||||||
|
// || string.IsNullOrWhiteSpace(redisCacheGlobalIndexKey))
|
||||||
|
// {
|
||||||
|
// throw new ArgumentException($"{nameof(UpdateMeterData)} 参数异常,-101");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// var luaScript = @"
|
||||||
|
// local mainKey = KEYS[1]
|
||||||
|
// local oldFocusIndexKey = KEYS[2]
|
||||||
|
// local newFocusIndexKey = KEYS[3]
|
||||||
|
// local scoresIndexKey = KEYS[4]
|
||||||
|
// local globalIndexKey = KEYS[5]
|
||||||
|
// local member = ARGV[1]
|
||||||
|
// local newData = ARGV[2]
|
||||||
|
// local newScore = ARGV[3]
|
||||||
|
// local newGlobalScore = ARGV[4]
|
||||||
|
|
||||||
|
// -- 校验存在性
|
||||||
|
// if redis.call('HEXISTS', mainKey, member) == 0 then
|
||||||
|
// return 0
|
||||||
|
// end
|
||||||
|
|
||||||
|
// -- 更新主数据
|
||||||
|
// redis.call('HSET', mainKey, member, newData)
|
||||||
|
|
||||||
|
// -- 处理变更
|
||||||
|
// if newScore ~= '' then
|
||||||
|
// -- 删除旧索引
|
||||||
|
// redis.call('SREM', oldFocusIndexKey, member)
|
||||||
|
// redis.call('ZREM', scoresIndexKey, member)
|
||||||
|
|
||||||
|
// -- 添加新索引
|
||||||
|
// redis.call('SADD', newFocusIndexKey, member)
|
||||||
|
// redis.call('ZADD', scoresIndexKey, newScore, member)
|
||||||
|
// end
|
||||||
|
|
||||||
|
// -- 更新全局索引
|
||||||
|
// if newGlobalScore ~= '' then
|
||||||
|
// -- 删除旧索引
|
||||||
|
// redis.call('ZREM', globalIndexKey, member)
|
||||||
|
|
||||||
|
// -- 添加新索引
|
||||||
|
// redis.call('ZADD', globalIndexKey, newGlobalScore, member)
|
||||||
|
// end
|
||||||
|
|
||||||
|
// return 1
|
||||||
|
// ";
|
||||||
|
|
||||||
|
// var actualTimestamp = newTimestamp ?? DateTimeOffset.UtcNow;
|
||||||
|
// var newGlobalScore = actualTimestamp.ToUnixTimeMilliseconds();
|
||||||
|
// var newScoreValue = ((long)newData.FocusId << 32) | (uint)actualTimestamp.Ticks;
|
||||||
|
|
||||||
|
// var result = await Instance.EvalAsync(luaScript,
|
||||||
|
// new[]
|
||||||
|
// {
|
||||||
|
// redisCacheKey,
|
||||||
|
// oldRedisCacheFocusIndexKey,
|
||||||
|
// newRedisCacheFocusIndexKey,
|
||||||
|
// redisCacheScoresIndexKey,
|
||||||
|
// redisCacheGlobalIndexKey
|
||||||
|
// },
|
||||||
|
// new object[]
|
||||||
|
// {
|
||||||
|
// newData.MemberID,
|
||||||
|
// newData.Serialize(),
|
||||||
|
// newScoreValue.ToString() ?? "",
|
||||||
|
// newGlobalScore.ToString() ?? ""
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if ((int)result == 0)
|
||||||
|
// {
|
||||||
|
// throw new KeyNotFoundException($"{nameof(UpdateMeterData)}指定Key{redisCacheKey}的数据不存在");
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
//public async Task<BusPagedResult<T>> SingleGetMeterPagedData<T>(
|
||||||
|
//string redisCacheKey,
|
||||||
|
//string redisCacheScoresIndexKey,
|
||||||
|
//int focusId,
|
||||||
|
//int pageSize = 10,
|
||||||
|
//int pageIndex = 1,
|
||||||
|
//bool descending = true)
|
||||||
|
//{
|
||||||
|
// // 计算score范围
|
||||||
|
// long minScore = (long)focusId << 32;
|
||||||
|
// long maxScore = ((long)focusId + 1) << 32;
|
||||||
|
|
||||||
|
// // 分页参数计算
|
||||||
|
// int start = (pageIndex - 1) * pageSize;
|
||||||
|
|
||||||
|
// // 获取排序后的member列表
|
||||||
|
// var members = descending
|
||||||
|
// ? await Instance.ZRevRangeByScoreAsync(
|
||||||
|
// redisCacheScoresIndexKey,
|
||||||
|
// maxScore,
|
||||||
|
// minScore,
|
||||||
|
// start,
|
||||||
|
// pageSize)
|
||||||
|
// : await Instance.ZRangeByScoreAsync(
|
||||||
|
// redisCacheScoresIndexKey,
|
||||||
|
// minScore,
|
||||||
|
// maxScore,
|
||||||
|
// start,
|
||||||
|
// pageSize);
|
||||||
|
|
||||||
|
// // 批量获取实际数据
|
||||||
|
// var dataTasks = members.Select(m =>
|
||||||
|
// Instance.HGetAsync<T>(redisCacheKey, m)).ToArray();
|
||||||
|
// await Task.WhenAll(dataTasks);
|
||||||
|
|
||||||
|
// // 总数统计优化
|
||||||
|
// var total = await Instance.ZCountAsync(
|
||||||
|
// redisCacheScoresIndexKey,
|
||||||
|
// minScore,
|
||||||
|
// maxScore);
|
||||||
|
|
||||||
|
// return new BusPagedResult<T>
|
||||||
|
// {
|
||||||
|
// Items = dataTasks.Select(t => t.Result).ToList(),
|
||||||
|
// TotalCount = total,
|
||||||
|
// PageIndex = pageIndex,
|
||||||
|
// PageSize = pageSize
|
||||||
|
// };
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
//public async Task<BusPagedResult<T>> GetFocusPagedData<T>(
|
||||||
|
//string redisCacheKey,
|
||||||
|
//string redisCacheScoresIndexKey,
|
||||||
|
//int focusId,
|
||||||
|
//int pageSize = 10,
|
||||||
|
//long? lastScore = null,
|
||||||
|
//string lastMember = null,
|
||||||
|
//bool descending = true) where T : DeviceCacheBasicModel
|
||||||
|
//{
|
||||||
|
// // 计算分数范围
|
||||||
|
// long minScore = (long)focusId << 32;
|
||||||
|
// long maxScore = ((long)focusId + 1) << 32;
|
||||||
|
|
||||||
|
// // 获取成员列表
|
||||||
|
// var members = await GetSortedMembers(
|
||||||
|
// redisCacheScoresIndexKey,
|
||||||
|
// minScore,
|
||||||
|
// maxScore,
|
||||||
|
// pageSize,
|
||||||
|
// lastScore,
|
||||||
|
// lastMember,
|
||||||
|
// descending);
|
||||||
|
|
||||||
|
// // 批量获取数据
|
||||||
|
// var dataDict = await Instance.HMGetAsync<T>(redisCacheKey, members.CurrentItems);
|
||||||
|
|
||||||
|
// return new BusPagedResult<T>
|
||||||
|
// {
|
||||||
|
// Items = dataDict,
|
||||||
|
// TotalCount = await GetTotalCount(redisCacheScoresIndexKey, minScore, maxScore),
|
||||||
|
// HasNext = members.HasNext,
|
||||||
|
// NextScore = members.NextScore,
|
||||||
|
// NextMember = members.NextMember
|
||||||
|
// };
|
||||||
|
//}
|
||||||
|
|
||||||
|
//private async Task<(string[] CurrentItems, bool HasNext, decimal? NextScore, string NextMember)>
|
||||||
|
// GetSortedMembers(
|
||||||
|
// string zsetKey,
|
||||||
|
// long minScore,
|
||||||
|
// long maxScore,
|
||||||
|
// int pageSize,
|
||||||
|
// long? lastScore,
|
||||||
|
// string lastMember,
|
||||||
|
// bool descending)
|
||||||
|
//{
|
||||||
|
// var querySize = pageSize + 1;
|
||||||
|
// var (startScore, exclude) = descending
|
||||||
|
// ? (lastScore ?? maxScore, lastMember)
|
||||||
|
// : (lastScore ?? minScore, lastMember);
|
||||||
|
|
||||||
|
// var members = descending
|
||||||
|
// ? await Instance.ZRevRangeByScoreAsync(
|
||||||
|
// zsetKey,
|
||||||
|
// max: startScore,
|
||||||
|
// min: minScore,
|
||||||
|
// offset: 0,
|
||||||
|
// count: querySize)
|
||||||
|
// : await Instance.ZRangeByScoreAsync(
|
||||||
|
// zsetKey,
|
||||||
|
// min: startScore,
|
||||||
|
// max: maxScore,
|
||||||
|
// offset: 0,
|
||||||
|
// count: querySize);
|
||||||
|
|
||||||
|
// var hasNext = members.Length > pageSize;
|
||||||
|
// var currentItems = members.Take(pageSize).ToArray();
|
||||||
|
|
||||||
|
// var nextCursor = currentItems.Any()
|
||||||
|
// ? await GetNextCursor(zsetKey, currentItems.Last(), descending)
|
||||||
|
// : (null, null);
|
||||||
|
|
||||||
|
// return (currentItems, hasNext, nextCursor.score, nextCursor.member);
|
||||||
|
//}
|
||||||
|
|
||||||
|
//private async Task<long> GetTotalCount(string zsetKey, long min, long max)
|
||||||
|
//{
|
||||||
|
// // 缓存计数优化
|
||||||
|
// var cacheKey = $"{zsetKey}_count_{min}_{max}";
|
||||||
|
// var cached = await Instance.GetAsync<long?>(cacheKey);
|
||||||
|
|
||||||
|
// if (cached.HasValue)
|
||||||
|
// return cached.Value;
|
||||||
|
|
||||||
|
// var count = await Instance.ZCountAsync(zsetKey, min, max);
|
||||||
|
// await Instance.SetExAsync(cacheKey, 60, count); // 缓存60秒
|
||||||
|
// return count;
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
//public async Task<Dictionary<int, BusPagedResult<T>>> BatchGetMeterPagedData<T>(
|
||||||
|
//string redisCacheKey,
|
||||||
|
//string redisCacheScoresIndexKey,
|
||||||
|
//IEnumerable<int> focusIds,
|
||||||
|
//int pageSizePerFocus = 10) where T : DeviceCacheBasicModel
|
||||||
|
//{
|
||||||
|
// var results = new ConcurrentDictionary<int, BusPagedResult<T>>();
|
||||||
|
// var parallelOptions = new ParallelOptions
|
||||||
|
// {
|
||||||
|
// MaxDegreeOfParallelism = Environment.ProcessorCount * 2
|
||||||
|
// };
|
||||||
|
|
||||||
|
// await Parallel.ForEachAsync(focusIds, parallelOptions, async (focusId, _) =>
|
||||||
|
// {
|
||||||
|
// var data = await SingleGetMeterPagedData<T>(
|
||||||
|
// redisCacheKey,
|
||||||
|
// redisCacheScoresIndexKey,
|
||||||
|
// focusId,
|
||||||
|
// pageSizePerFocus);
|
||||||
|
|
||||||
|
// results.TryAdd(focusId, data);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return new Dictionary<int, BusPagedResult<T>>(results);
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
14
modules/JiShe.CollectBus.FreeRedis/IFreeRedisProvider.cs
Normal file
14
modules/JiShe.CollectBus.FreeRedis/IFreeRedisProvider.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using FreeRedis;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.FreeRedis
|
||||||
|
{
|
||||||
|
public interface IFreeRedisProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取客户端
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
RedisClient Instance { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FreeRedis" Version="1.3.6" />
|
||||||
|
<PackageReference Include="Volo.Abp" Version="8.3.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\shared\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
namespace JiShe.CollectBus.FreeRedis.Options
|
||||||
|
{
|
||||||
|
public class FreeRedisOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 连接字符串
|
||||||
|
/// </summary>
|
||||||
|
public string? Configuration { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最大连接数
|
||||||
|
/// </summary>
|
||||||
|
public string? MaxPoolSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认数据库
|
||||||
|
/// </summary>
|
||||||
|
public string? DefaultDB { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HangfireDB
|
||||||
|
/// </summary>
|
||||||
|
public string? HangfireDB { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
namespace JiShe.CollectBus.IoTDB.Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Column分类标记特性(ATTRIBUTE字段),也就是属性字段
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class ATTRIBUTEColumnAttribute : System.Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
namespace JiShe.CollectBus.IoTDB.Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Column分类标记特性(FIELD字段),数据列字段
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class FIELDColumnAttribute : System.Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
namespace JiShe.CollectBus.IoTDB.Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用于标识当前实体为单侧点模式,单侧点模式只有一个Filed标识字段,类型是Tuple<string,object>,Item1=>测点名称,Item2=>测点值,泛型
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class SingleMeasuringAttribute : System.Attribute
|
||||||
|
{
|
||||||
|
public string FieldName { get; set;}
|
||||||
|
|
||||||
|
public SingleMeasuringAttribute(string fieldName)
|
||||||
|
{
|
||||||
|
FieldName = fieldName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
namespace JiShe.CollectBus.IoTDB.Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Column分类标记特性(TAG字段),标签字段
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class TAGColumnAttribute : System.Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
33
modules/JiShe.CollectBus.IoTDB/CollectBusIoTDBModule.cs
Normal file
33
modules/JiShe.CollectBus.IoTDB/CollectBusIoTDBModule.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
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>();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
using JiShe.CollectBus.IoTDB.Options;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.IoTDB.Context
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// IoTDB SessionPool 运行时上下文
|
||||||
|
/// </summary>
|
||||||
|
public class IoTDBRuntimeContext
|
||||||
|
{
|
||||||
|
private readonly bool _defaultValue;
|
||||||
|
|
||||||
|
public IoTDBRuntimeContext(IOptions<IoTDBOptions> options)
|
||||||
|
{
|
||||||
|
_defaultValue = options.Value.UseTableSessionPoolByDefault;
|
||||||
|
UseTableSessionPool = _defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否使用表模型存储, 默认false,使用tree模型存储
|
||||||
|
/// </summary>
|
||||||
|
public bool UseTableSessionPool { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重置为默认值
|
||||||
|
/// </summary>
|
||||||
|
public void ResetToDefault()
|
||||||
|
{
|
||||||
|
UseTableSessionPool = _defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBProvider.cs
Normal file
51
modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBProvider.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
using JiShe.CollectBus.Common.Models;
|
||||||
|
using JiShe.CollectBus.IoTDB.Options;
|
||||||
|
using JiShe.CollectBus.IoTDB.Provider;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.IoTDB.Interface
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// IoTDB数据源,数据库能同时存多个时序模型,但数据是完全隔离的,不能跨时序模型查询,通过连接字符串配置
|
||||||
|
/// </summary>
|
||||||
|
public interface IIoTDBProvider
|
||||||
|
{
|
||||||
|
///// <summary>
|
||||||
|
///// 切换 SessionPool
|
||||||
|
///// </summary>
|
||||||
|
///// <param name="useTableSession">是否使用表模型</param>
|
||||||
|
//void SwitchSessionPool(bool useTableSession);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 插入数据
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="entity"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task InsertAsync<T>(T entity) where T : IoTEntity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量插入数据
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="entities"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task BatchInsertAsync<T>(IEnumerable<T> entities) where T : IoTEntity;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除数据
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<object> DeleteAsync<T>(QueryOptions options) where T : IoTEntity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询数据
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<BusPagedResult<T>> QueryAsync<T>(QueryOptions options) where T : IoTEntity, new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
namespace JiShe.CollectBus.IoTDB.Interface
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Session 工厂接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IIoTDBSessionFactory:IDisposable
|
||||||
|
{
|
||||||
|
IIoTDBSessionPool GetSessionPool(bool useTableSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
using Apache.IoTDB.DataStructure;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.IoTDB.Interface
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Session 连接池
|
||||||
|
/// </summary>
|
||||||
|
public interface IIoTDBSessionPool : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 打开连接池
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task OpenAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 插入数据
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tablet"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<int> InsertAsync(Tablet tablet);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询数据
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sql"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<SessionDataSet> ExecuteQueryStatementAsync(string sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
modules/JiShe.CollectBus.IoTDB/JiShe.CollectBus.IoTDB.csproj
Normal file
16
modules/JiShe.CollectBus.IoTDB/JiShe.CollectBus.IoTDB.csproj
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<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>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\shared\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
46
modules/JiShe.CollectBus.IoTDB/Options/IoTDBOptions.cs
Normal file
46
modules/JiShe.CollectBus.IoTDB/Options/IoTDBOptions.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
namespace JiShe.CollectBus.IoTDB.Options
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// IOTDB配置
|
||||||
|
/// </summary>
|
||||||
|
public class IoTDBOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库名称,表模型才有,树模型为空
|
||||||
|
/// </summary>
|
||||||
|
public string DataBaseName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 集群列表
|
||||||
|
/// </summary>
|
||||||
|
public List<string> ClusterList { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 用户名
|
||||||
|
/// </summary>
|
||||||
|
public string UserName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 密码
|
||||||
|
/// </summary>
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 连接池大小
|
||||||
|
/// </summary>
|
||||||
|
public int PoolSize { get; set; } = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询时,每次查询的数据量,默认1024
|
||||||
|
/// </summary>
|
||||||
|
public int FetchSize { get; set; } = 1024;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否开启调试模式,生产环境请关闭,因为底层的实现方式,可能会导致内存持续增长。
|
||||||
|
/// </summary>
|
||||||
|
public bool OpenDebugMode { get; set;}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否使用表模型存储, 默认false,使用tree模型存储
|
||||||
|
/// </summary>
|
||||||
|
public bool UseTableSessionPoolByDefault { get; set; } = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
modules/JiShe.CollectBus.IoTDB/Options/QueryCondition.cs
Normal file
21
modules/JiShe.CollectBus.IoTDB/Options/QueryCondition.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
namespace JiShe.CollectBus.IoTDB.Options
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 查询条件
|
||||||
|
/// </summary>
|
||||||
|
public class QueryCondition
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 字段
|
||||||
|
/// </summary>
|
||||||
|
public string Field { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 操作符
|
||||||
|
/// </summary>
|
||||||
|
public string Operator { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 值
|
||||||
|
/// </summary>
|
||||||
|
public object Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
28
modules/JiShe.CollectBus.IoTDB/Options/QueryOptions.cs
Normal file
28
modules/JiShe.CollectBus.IoTDB/Options/QueryOptions.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
namespace JiShe.CollectBus.IoTDB.Options
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 查询条件
|
||||||
|
/// </summary>
|
||||||
|
public class QueryOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 表模型的表名称或者树模型的设备路径
|
||||||
|
/// </summary>
|
||||||
|
public required string TableNameOrTreePath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页大小
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询条件
|
||||||
|
/// </summary>
|
||||||
|
public List<QueryCondition> Conditions { get; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
30
modules/JiShe.CollectBus.IoTDB/Provider/DeviceMetadata.cs
Normal file
30
modules/JiShe.CollectBus.IoTDB/Provider/DeviceMetadata.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using Apache.IoTDB;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.IoTDB.Provider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备元数据
|
||||||
|
/// </summary>
|
||||||
|
public class DeviceMetadata
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 是否有单测量值
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSingleMeasuring { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测量值集合,用于构建Table的测量值,也就是columnNames参数
|
||||||
|
/// </summary>
|
||||||
|
public List<string> ColumnNames { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 列类型集合,用于构建Table的列类型,也就是columnCategories参数
|
||||||
|
/// </summary>
|
||||||
|
public List<ColumnCategory> ColumnCategories { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 值类型集合,用于构建Table的值类型,也就是dataTypes参数
|
||||||
|
/// </summary>
|
||||||
|
public List<TSDataType> DataTypes { get; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
33
modules/JiShe.CollectBus.IoTDB/Provider/DevicePathBuilder.cs
Normal file
33
modules/JiShe.CollectBus.IoTDB/Provider/DevicePathBuilder.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
namespace JiShe.CollectBus.IoTDB.Provider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备路径构建器
|
||||||
|
/// </summary>
|
||||||
|
public static class DevicePathBuilder
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 构建设备路径,由于路径的层级约束规范不能是纯数字字符,所以需要做特殊处理。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="entity"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string GetDevicePath<T>(T entity) where T : IoTEntity
|
||||||
|
{
|
||||||
|
return $"root.{entity.SystemName.ToLower()}.`{entity.ProjectCode}`.`{entity.DeviceId}`";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取表名称
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="entity"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string GetTableName<T>() where T : IoTEntity
|
||||||
|
{
|
||||||
|
var type = typeof(T);
|
||||||
|
return $"{type.Name.ToLower()}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
615
modules/JiShe.CollectBus.IoTDB/Provider/IoTDBProvider.cs
Normal file
615
modules/JiShe.CollectBus.IoTDB/Provider/IoTDBProvider.cs
Normal file
@ -0,0 +1,615 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using Apache.IoTDB;
|
||||||
|
using Apache.IoTDB.DataStructure;
|
||||||
|
using JiShe.CollectBus.Common.Models;
|
||||||
|
using JiShe.CollectBus.IoTDB.Attribute;
|
||||||
|
using JiShe.CollectBus.IoTDB.Context;
|
||||||
|
using JiShe.CollectBus.IoTDB.Interface;
|
||||||
|
using JiShe.CollectBus.IoTDB.Options;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.IoTDB.Provider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// IoTDB数据源
|
||||||
|
/// </summary>
|
||||||
|
public class IoTDBProvider : IIoTDBProvider
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<Type, DeviceMetadata> _metadataCache = new();
|
||||||
|
private readonly ILogger<IoTDBProvider> _logger;
|
||||||
|
private readonly IIoTDBSessionFactory _sessionFactory;
|
||||||
|
private readonly IoTDBRuntimeContext _runtimeContext;
|
||||||
|
|
||||||
|
private IIoTDBSessionPool CurrentSession =>
|
||||||
|
_sessionFactory.GetSessionPool(_runtimeContext.UseTableSessionPool);
|
||||||
|
|
||||||
|
public IoTDBProvider(
|
||||||
|
ILogger<IoTDBProvider> logger,
|
||||||
|
IIoTDBSessionFactory sessionFactory,
|
||||||
|
IoTDBRuntimeContext runtimeContext)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_sessionFactory = sessionFactory;
|
||||||
|
_runtimeContext = runtimeContext;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 插入数据
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="entity"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task InsertAsync<T>(T entity) where T : IoTEntity
|
||||||
|
{
|
||||||
|
var metadata = 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}插入数据没有成功");
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量插入数据
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task BatchInsertAsync<T>(IEnumerable<T> entities) where T : IoTEntity
|
||||||
|
{
|
||||||
|
var metadata = GetMetadata<T>();
|
||||||
|
|
||||||
|
var batchSize = 1000;
|
||||||
|
var batches = entities.Chunk(batchSize);
|
||||||
|
|
||||||
|
foreach (var batch in batches)
|
||||||
|
{
|
||||||
|
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}批次。");
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除数据
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<object> DeleteAsync<T>(QueryOptions options) where T : IoTEntity
|
||||||
|
{
|
||||||
|
var query = BuildDeleteSQL<T>(options);
|
||||||
|
var sessionDataSet = await CurrentSession.ExecuteQueryStatementAsync(query);
|
||||||
|
|
||||||
|
if (!sessionDataSet.HasNext())
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"{typeof(T).Name} 删除数据时,没有返回受影响记录数量。");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取唯一结果行
|
||||||
|
var row = sessionDataSet.Next();
|
||||||
|
return row.Values[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询数据
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<BusPagedResult<T>> QueryAsync<T>(QueryOptions options) where T : IoTEntity, new()
|
||||||
|
{
|
||||||
|
var query = 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)
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建Tablet
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="entities">表实体</param>
|
||||||
|
/// <param name="metadata">设备元数据</param></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private Tablet BuildTablet<T>(IEnumerable<T> entities, DeviceMetadata metadata) where T : IoTEntity
|
||||||
|
{
|
||||||
|
var timestamps = new List<long>();
|
||||||
|
var values = new List<List<object>>();
|
||||||
|
var devicePaths = new HashSet<string>();
|
||||||
|
List<string> tempColumnNames = new List<string>();
|
||||||
|
tempColumnNames.AddRange(metadata.ColumnNames);
|
||||||
|
|
||||||
|
foreach (var entity in entities)
|
||||||
|
{
|
||||||
|
timestamps.Add(entity.Timestamps);
|
||||||
|
var rowValues = new List<object>();
|
||||||
|
foreach (var measurement in tempColumnNames)
|
||||||
|
{
|
||||||
|
|
||||||
|
PropertyInfo propertyInfo = typeof(T).GetProperty(measurement);
|
||||||
|
if (propertyInfo == null)
|
||||||
|
{
|
||||||
|
throw new Exception($"{nameof(BuildTablet)} 构建表模型{typeof(T).Name}时,没有找到{measurement}属性,属于异常情况,-101。");
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = propertyInfo.GetValue(entity);
|
||||||
|
if (propertyInfo.IsDefined(typeof(SingleMeasuringAttribute), false) && value != null)//表示当前对象是单测点模式
|
||||||
|
{
|
||||||
|
Type tupleType = value.GetType();
|
||||||
|
Type[] tupleArgs = tupleType.GetGenericArguments();
|
||||||
|
Type item2Type = tupleArgs[1]; // T 的实际类型
|
||||||
|
var item1 = tupleType.GetProperty("Item1")!.GetValue(value);
|
||||||
|
var item2 = tupleType.GetProperty("Item2")!.GetValue(value);
|
||||||
|
if (item1 == null || item2 == null)
|
||||||
|
{
|
||||||
|
throw new Exception($"{nameof(BuildTablet)} 构建表模型{typeof(T).Name}时,单测点模式构建失败,没有获取测点名称或者测点值,-102。");
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexOf = metadata.ColumnNames.IndexOf(measurement);
|
||||||
|
metadata.ColumnNames[indexOf] = (string)item1!;
|
||||||
|
|
||||||
|
rowValues.Add(item2);
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
rowValues.Add(value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//填充默认数据值
|
||||||
|
DataTypeDefaultValueMap.TryGetValue(propertyInfo.PropertyType.Name, out object defaultValue);
|
||||||
|
|
||||||
|
rowValues.Add(defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
values.Add(rowValues);
|
||||||
|
|
||||||
|
if (!_runtimeContext.UseTableSessionPool)//树模型
|
||||||
|
{
|
||||||
|
devicePaths.Add(DevicePathBuilder.GetDevicePath(entity));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
devicePaths.Add(DevicePathBuilder.GetTableName<T>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devicePaths.Count > 1)
|
||||||
|
{
|
||||||
|
throw new Exception($"{nameof(BuildTablet)} 构建Tablet《{typeof(T).Name}》时,批量插入的设备路径不一致。");
|
||||||
|
}
|
||||||
|
|
||||||
|
return _runtimeContext.UseTableSessionPool
|
||||||
|
? BuildTableSessionTablet(metadata, devicePaths.First(), values, timestamps)
|
||||||
|
: BuildSessionTablet(metadata, devicePaths.First(), values, timestamps);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建tree模型的Tablet
|
||||||
|
/// </summary>
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
return new Tablet(
|
||||||
|
devicePath,
|
||||||
|
metadata.ColumnNames,
|
||||||
|
metadata.DataTypes,
|
||||||
|
values,
|
||||||
|
timestamps
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建表模型的Tablet
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="metadata"></param>
|
||||||
|
/// <param name="devicePath"></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)
|
||||||
|
{
|
||||||
|
var tablet = new Tablet(
|
||||||
|
devicePath,
|
||||||
|
metadata.ColumnNames,
|
||||||
|
metadata.ColumnCategories,
|
||||||
|
metadata.DataTypes,
|
||||||
|
values,
|
||||||
|
timestamps
|
||||||
|
);
|
||||||
|
|
||||||
|
return tablet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建查询语句
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private string BuildQuerySQL<T>(QueryOptions options) where T : IoTEntity
|
||||||
|
{
|
||||||
|
var metadata = GetMetadata<T>();
|
||||||
|
var sb = new StringBuilder("SELECT ");
|
||||||
|
sb.AppendJoin(", ", metadata.ColumnNames);
|
||||||
|
sb.Append($" FROM {options.TableNameOrTreePath}");
|
||||||
|
|
||||||
|
if (options.Conditions.Any())
|
||||||
|
{
|
||||||
|
sb.Append(" WHERE ");
|
||||||
|
sb.AppendJoin(" AND ", options.Conditions.Select(TranslateCondition));
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append($" LIMIT {options.PageSize} OFFSET {options.Page * options.PageSize}");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建删除语句
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private string BuildDeleteSQL<T>(QueryOptions options) where T : IoTEntity
|
||||||
|
{
|
||||||
|
var metadata = GetMetadata<T>();
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
if (!_runtimeContext.UseTableSessionPool)
|
||||||
|
{
|
||||||
|
sb.Append("DELETE ");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append("DROP ");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append($" FROM {options.TableNameOrTreePath}");
|
||||||
|
|
||||||
|
sb.AppendJoin(", ", metadata.ColumnNames);
|
||||||
|
|
||||||
|
if (options.Conditions.Any())
|
||||||
|
{
|
||||||
|
sb.Append(" WHERE ");
|
||||||
|
sb.AppendJoin(" AND ", options.Conditions.Select(TranslateCondition));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将查询条件转换为SQL语句
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="condition"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="NotSupportedException"></exception>
|
||||||
|
private string TranslateCondition(QueryCondition condition)
|
||||||
|
{
|
||||||
|
return condition.Operator switch
|
||||||
|
{
|
||||||
|
">" => $"{condition.Field} > {condition.Value}",
|
||||||
|
"<" => $"{condition.Field} < {condition.Value}",
|
||||||
|
"=" => $"{condition.Field} = '{condition.Value}'",
|
||||||
|
_ => throw new NotSupportedException($"Operator {condition.Operator} not supported")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取查询条件的总数量
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private async Task<int> GetTotalCount<T>(QueryOptions options) where T : IoTEntity
|
||||||
|
{
|
||||||
|
var countQuery = $"SELECT COUNT(*) FROM {options.TableNameOrTreePath}";
|
||||||
|
if (options.Conditions.Any())
|
||||||
|
{
|
||||||
|
countQuery += " WHERE " + string.Join(" AND ", options.Conditions.Select(TranslateCondition));
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await CurrentSession.ExecuteQueryStatementAsync(countQuery);
|
||||||
|
return result.HasNext() ? Convert.ToInt32(result.Next().Values[0]) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析查询结果
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
/// <param name="dataSet"></param>
|
||||||
|
/// <param name="pageSize"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private IEnumerable<T> ParseResults<T>(SessionDataSet dataSet, int pageSize) where T : IoTEntity, new()
|
||||||
|
{
|
||||||
|
var results = new List<T>();
|
||||||
|
var metadata = GetMetadata<T>();
|
||||||
|
|
||||||
|
var properties = typeof(T).GetProperties();
|
||||||
|
|
||||||
|
while (dataSet.HasNext() && results.Count < pageSize)
|
||||||
|
{
|
||||||
|
var record = dataSet.Next();
|
||||||
|
var entity = new T
|
||||||
|
{
|
||||||
|
Timestamps = record.Timestamps
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var measurement in metadata.ColumnNames)
|
||||||
|
{
|
||||||
|
var value = record.Values;
|
||||||
|
|
||||||
|
var prop = properties.FirstOrDefault(p =>
|
||||||
|
p.Name.Equals(measurement, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (prop != null)
|
||||||
|
{
|
||||||
|
typeof(T).GetProperty(measurement)?.SetValue(entity, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
results.Add(entity);
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
/// <param name="type"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private List<ColumnInfo> CollectColumnMetadata(Type type)
|
||||||
|
{
|
||||||
|
var columns = new List<ColumnInfo>();
|
||||||
|
|
||||||
|
foreach (var prop in type.GetProperties())
|
||||||
|
{
|
||||||
|
//先获取Tag标签和属性标签
|
||||||
|
ColumnInfo? column = prop.GetCustomAttribute<TAGColumnAttribute>() is not null ? new ColumnInfo(
|
||||||
|
name: prop.Name,
|
||||||
|
category: ColumnCategory.TAG,
|
||||||
|
dataType: GetDataTypeFromTypeName(prop.PropertyType.Name),
|
||||||
|
false
|
||||||
|
) : prop.GetCustomAttribute<ATTRIBUTEColumnAttribute>() is not null ? new ColumnInfo(
|
||||||
|
prop.Name,
|
||||||
|
ColumnCategory.ATTRIBUTE,
|
||||||
|
GetDataTypeFromTypeName(prop.PropertyType.Name),
|
||||||
|
false
|
||||||
|
) : prop.GetCustomAttribute<FIELDColumnAttribute>() is not null ? new ColumnInfo(
|
||||||
|
prop.Name,
|
||||||
|
ColumnCategory.FIELD,
|
||||||
|
GetDataTypeFromTypeName(prop.PropertyType.Name),
|
||||||
|
false)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
//最先检查是不是单侧点模式
|
||||||
|
SingleMeasuringAttribute singleMeasuringAttribute = prop.GetCustomAttribute<SingleMeasuringAttribute>();
|
||||||
|
|
||||||
|
if (singleMeasuringAttribute != null && column == null)
|
||||||
|
{
|
||||||
|
//warning: 单侧点模式注意事项
|
||||||
|
//Entity实体 字段类型是 Tuple<string,T>,Item1=>测点名称,Item2=>测点值,泛型
|
||||||
|
//只有一个Filed字段。
|
||||||
|
//MeasuringName 默认为 SingleMeasuringAttribute.FieldName,以便于在获取对应的Value的时候重置为 Item1 的值。
|
||||||
|
|
||||||
|
Type tupleType = prop.PropertyType;
|
||||||
|
Type[] tupleArgs = tupleType.GetGenericArguments();
|
||||||
|
|
||||||
|
column = new ColumnInfo(
|
||||||
|
singleMeasuringAttribute.FieldName,
|
||||||
|
ColumnCategory.FIELD,
|
||||||
|
GetDataTypeFromTypeName(tupleArgs[1].Name),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (column.HasValue)
|
||||||
|
{
|
||||||
|
columns.Add(column.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建设备元数据
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="columns"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private DeviceMetadata BuildDeviceMetadata(List<ColumnInfo> columns)
|
||||||
|
{
|
||||||
|
var metadata = new DeviceMetadata();
|
||||||
|
|
||||||
|
//先检查是不是单侧点模型
|
||||||
|
if (columns.Any(c => c.IsSingleMeasuring))
|
||||||
|
{
|
||||||
|
metadata.IsSingleMeasuring = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//按业务逻辑顺序处理(TAG -> ATTRIBUTE -> FIELD)
|
||||||
|
var groupedColumns = columns
|
||||||
|
.GroupBy(c => c.Category)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
|
||||||
|
ProcessCategory(groupedColumns, ColumnCategory.TAG, metadata);
|
||||||
|
ProcessCategory(groupedColumns, ColumnCategory.ATTRIBUTE, metadata);
|
||||||
|
ProcessCategory(groupedColumns, ColumnCategory.FIELD, metadata);
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理不同列类型的逻辑
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="groupedColumns"></param>
|
||||||
|
/// <param name="category"></param>
|
||||||
|
/// <param name="metadata"></param>
|
||||||
|
private void ProcessCategory(IReadOnlyDictionary<ColumnCategory, List<ColumnInfo>> groupedColumns, ColumnCategory category, DeviceMetadata metadata)
|
||||||
|
{
|
||||||
|
if (groupedColumns.TryGetValue(category, out var cols))
|
||||||
|
{
|
||||||
|
metadata.ColumnNames.AddRange(cols.Select(c => c.Name));
|
||||||
|
metadata.ColumnCategories.AddRange(cols.Select(c => c.Category));
|
||||||
|
metadata.DataTypes.AddRange(cols.Select(c => c.DataType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据列结构
|
||||||
|
/// </summary>
|
||||||
|
private readonly struct ColumnInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 列名
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否是单测点
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSingleMeasuring { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 列类型
|
||||||
|
/// </summary>
|
||||||
|
public ColumnCategory Category { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据类型
|
||||||
|
/// </summary>
|
||||||
|
public TSDataType DataType { get; }
|
||||||
|
|
||||||
|
public ColumnInfo(string name, ColumnCategory category, TSDataType dataType, bool isSingleMeasuring)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Category = category;
|
||||||
|
DataType = dataType;
|
||||||
|
IsSingleMeasuring = isSingleMeasuring;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据类型名称获取对应的 IoTDB 数据类型
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="typeName">类型名称(不区分大小写)</param>
|
||||||
|
/// <returns>对应的 TSDataType,默认返回 TSDataType.STRING</returns>
|
||||||
|
private TSDataType GetDataTypeFromTypeName(string typeName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(typeName))
|
||||||
|
return TSDataType.STRING;
|
||||||
|
|
||||||
|
return DataTypeMap.TryGetValue(typeName.Trim(), out var dataType)
|
||||||
|
? dataType
|
||||||
|
: TSDataType.STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据类型名称获取 IoTDB 数据类型
|
||||||
|
/// </summary>
|
||||||
|
private readonly IReadOnlyDictionary<string, TSDataType> DataTypeMap =
|
||||||
|
new Dictionary<string, TSDataType>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["BOOLEAN"] = TSDataType.BOOLEAN,
|
||||||
|
["INT32"] = TSDataType.INT32,
|
||||||
|
["INT64"] = TSDataType.INT64,
|
||||||
|
["FLOAT"] = TSDataType.FLOAT,
|
||||||
|
["DOUBLE"] = TSDataType.DOUBLE,
|
||||||
|
["TEXT"] = TSDataType.TEXT,
|
||||||
|
["NULLTYPE"] = TSDataType.NONE,
|
||||||
|
["TIMESTAMP"] = TSDataType.TIMESTAMP,
|
||||||
|
["DATE"] = TSDataType.DATE,
|
||||||
|
["BLOB"] = TSDataType.BLOB,
|
||||||
|
["DECIMAL"] = TSDataType.STRING,
|
||||||
|
["STRING"] = TSDataType.STRING
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据类型名称获取 IoTDB 数据默认值
|
||||||
|
/// </summary>
|
||||||
|
private readonly IReadOnlyDictionary<string, object> DataTypeDefaultValueMap =
|
||||||
|
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["BOOLEAN"] = false,
|
||||||
|
["INT32"] = 0,
|
||||||
|
["INT64"] = 0,
|
||||||
|
["FLOAT"] = 0.0f,
|
||||||
|
["DOUBLE"] = 0.0d,
|
||||||
|
["TEXT"] = string.Empty,
|
||||||
|
["NULLTYPE"] = null,
|
||||||
|
["TIMESTAMP"] = null,
|
||||||
|
["DATE"] = null,
|
||||||
|
["BLOB"] = null,
|
||||||
|
["DECIMAL"] = "0.0",
|
||||||
|
["STRING"] = string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using JiShe.CollectBus.IoTDB.Interface;
|
||||||
|
using JiShe.CollectBus.IoTDB.Options;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.IoTDB.Provider
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实现带缓存的Session工厂
|
||||||
|
/// </summary>
|
||||||
|
public class IoTDBSessionFactory : IIoTDBSessionFactory
|
||||||
|
{
|
||||||
|
private readonly IoTDBOptions _options;
|
||||||
|
private readonly ConcurrentDictionary<bool, IIoTDBSessionPool> _pools = new();
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public IoTDBSessionFactory(IOptions<IoTDBOptions> options)
|
||||||
|
{
|
||||||
|
_options = options.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IIoTDBSessionPool GetSessionPool(bool useTableSession)
|
||||||
|
{
|
||||||
|
if (_disposed) throw new ObjectDisposedException(nameof(IoTDBSessionFactory));
|
||||||
|
|
||||||
|
return _pools.GetOrAdd(useTableSession, key =>
|
||||||
|
{
|
||||||
|
var pool = key
|
||||||
|
? (IIoTDBSessionPool)new TableSessionPoolAdapter(_options)
|
||||||
|
: new SessionPoolAdapter(_options);
|
||||||
|
|
||||||
|
pool.OpenAsync().ConfigureAwait(false).GetAwaiter().GetResult(); ;
|
||||||
|
return pool;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var pool in _pools.Values)
|
||||||
|
{
|
||||||
|
pool.Dispose();
|
||||||
|
}
|
||||||
|
_pools.Clear();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
modules/JiShe.CollectBus.IoTDB/Provider/IoTEntity.cs
Normal file
39
modules/JiShe.CollectBus.IoTDB/Provider/IoTEntity.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using JiShe.CollectBus.IoTDB.Attribute;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.IoTDB.Provider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// IoT实体基类
|
||||||
|
/// </summary>
|
||||||
|
public abstract class IoTEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 系统名称
|
||||||
|
/// </summary>
|
||||||
|
[TAGColumn]
|
||||||
|
public string SystemName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 项目编码
|
||||||
|
/// </summary>
|
||||||
|
[TAGColumn]
|
||||||
|
public string ProjectCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设备类型集中器、电表、水表、流量计、传感器等
|
||||||
|
/// </summary>
|
||||||
|
[TAGColumn]
|
||||||
|
public string DeviceType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设备ID
|
||||||
|
/// </summary>
|
||||||
|
[TAGColumn]
|
||||||
|
public string DeviceId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前时间戳,单位毫秒
|
||||||
|
/// </summary>
|
||||||
|
public long Timestamps { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
using Apache.IoTDB;
|
||||||
|
using Apache.IoTDB.DataStructure;
|
||||||
|
using JiShe.CollectBus.IoTDB.Interface;
|
||||||
|
using JiShe.CollectBus.IoTDB.Options;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.IoTDB.Provider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 树模型连接池
|
||||||
|
/// </summary>
|
||||||
|
public class SessionPoolAdapter : IIoTDBSessionPool
|
||||||
|
{
|
||||||
|
private readonly SessionPool _sessionPool;
|
||||||
|
private readonly IoTDBOptions _options;
|
||||||
|
|
||||||
|
public SessionPoolAdapter(IoTDBOptions options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
_sessionPool = new SessionPool.Builder()
|
||||||
|
.SetNodeUrl(options.ClusterList)
|
||||||
|
.SetUsername(options.UserName)
|
||||||
|
.SetPassword(options.Password)
|
||||||
|
.SetFetchSize(options.FetchSize)
|
||||||
|
.SetPoolSize(options.PoolSize)
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 打开连接池
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task OpenAsync()
|
||||||
|
{
|
||||||
|
await _sessionPool.Open(false);
|
||||||
|
if (_options.OpenDebugMode)
|
||||||
|
{
|
||||||
|
_sessionPool.OpenDebugMode(builder =>
|
||||||
|
{
|
||||||
|
builder.AddConsole();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量插入对齐时间序列数据
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tablet"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<int> InsertAsync(Tablet tablet)
|
||||||
|
{
|
||||||
|
var result = await _sessionPool.InsertAlignedTabletAsync(tablet);
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
throw new Exception($"{nameof(TableSessionPoolAdapter)} ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询数据
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sql"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<SessionDataSet> ExecuteQueryStatementAsync(string sql)
|
||||||
|
{
|
||||||
|
return await _sessionPool.ExecuteQueryStatementAsync(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_sessionPool?.Close().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
using Apache.IoTDB;
|
||||||
|
using Apache.IoTDB.DataStructure;
|
||||||
|
using JiShe.CollectBus.IoTDB.Interface;
|
||||||
|
using JiShe.CollectBus.IoTDB.Options;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.IoTDB.Provider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 表模型Session连接池
|
||||||
|
/// </summary>
|
||||||
|
public class TableSessionPoolAdapter : IIoTDBSessionPool
|
||||||
|
{
|
||||||
|
private readonly TableSessionPool _sessionPool;
|
||||||
|
private readonly IoTDBOptions _options;
|
||||||
|
|
||||||
|
public TableSessionPoolAdapter(IoTDBOptions options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
_sessionPool = new TableSessionPool.Builder()
|
||||||
|
.SetNodeUrls(options.ClusterList)
|
||||||
|
.SetUsername(options.UserName)
|
||||||
|
.SetPassword(options.Password)
|
||||||
|
.SetFetchSize(options.FetchSize)
|
||||||
|
.SetPoolSize(options.PoolSize)
|
||||||
|
.SetDatabase(options.DataBaseName)
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 打开连接池
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task OpenAsync()
|
||||||
|
{
|
||||||
|
await _sessionPool.Open(false);
|
||||||
|
if (_options.OpenDebugMode)
|
||||||
|
{
|
||||||
|
_sessionPool.OpenDebugMode(builder => builder.AddConsole());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量插入
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tablet"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<int> InsertAsync(Tablet tablet)
|
||||||
|
{
|
||||||
|
var result = await _sessionPool.InsertAsync(tablet);
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
throw new Exception($"{nameof(TableSessionPoolAdapter)} ");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询数据
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sql"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<SessionDataSet> ExecuteQueryStatementAsync(string sql)
|
||||||
|
{
|
||||||
|
return await _sessionPool.ExecuteQueryStatementAsync(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_sessionPool?.Close().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Kafka.Test
|
||||||
|
{
|
||||||
|
public class ConsoleApplicationBuilder: IApplicationBuilder
|
||||||
|
{
|
||||||
|
public IServiceProvider ApplicationServices { get; set; }
|
||||||
|
public IDictionary<string, object> Properties { get; set; } = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
public IFeatureCollection ServerFeatures => throw new NotImplementedException();
|
||||||
|
|
||||||
|
private readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares = new();
|
||||||
|
|
||||||
|
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
|
||||||
|
{
|
||||||
|
_middlewares.Add(middleware);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RequestDelegate Build()
|
||||||
|
{
|
||||||
|
RequestDelegate app = context => Task.CompletedTask;
|
||||||
|
foreach (var middleware in _middlewares)
|
||||||
|
{
|
||||||
|
app = middleware(app);
|
||||||
|
}
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IApplicationBuilder New()
|
||||||
|
{
|
||||||
|
return new ConsoleApplicationBuilder
|
||||||
|
{
|
||||||
|
ApplicationServices = this.ApplicationServices,
|
||||||
|
Properties = new Dictionary<string, object>(this.Properties)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class HostBuilderExtensions
|
||||||
|
{
|
||||||
|
public static IHostBuilder ConfigureConsoleAppBuilder(
|
||||||
|
this IHostBuilder hostBuilder,
|
||||||
|
Action<IApplicationBuilder> configure)
|
||||||
|
{
|
||||||
|
hostBuilder.ConfigureServices((context, services) =>
|
||||||
|
{
|
||||||
|
// 注册 ConsoleApplicationBuilder 到 DI 容器
|
||||||
|
services.AddSingleton<IApplicationBuilder>(provider =>
|
||||||
|
{
|
||||||
|
var appBuilder = new ConsoleApplicationBuilder
|
||||||
|
{
|
||||||
|
ApplicationServices = provider // 注入服务提供者
|
||||||
|
};
|
||||||
|
configure(appBuilder); // 执行配置委托
|
||||||
|
return appBuilder;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return hostBuilder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="appsettings.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0-preview.3.25171.5" />
|
||||||
|
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||||
|
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.1" />
|
||||||
|
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Volo.Abp.Core" Version="8.3.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\services\JiShe.CollectBus.Domain\JiShe.CollectBus.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\JiShe.CollectBus.Kafka\JiShe.CollectBus.Kafka.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!--<ItemGroup>
|
||||||
|
<Reference Include="JiShe.CollectBus.Kafka">
|
||||||
|
<HintPath>Lib\JiShe.CollectBus.Kafka.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>-->
|
||||||
|
|
||||||
|
</Project>
|
||||||
108
modules/JiShe.CollectBus.Kafka.Test/KafkaProduceBenchmark.cs
Normal file
108
modules/JiShe.CollectBus.Kafka.Test/KafkaProduceBenchmark.cs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using BenchmarkDotNet.Jobs;
|
||||||
|
using Confluent.Kafka;
|
||||||
|
using JiShe.CollectBus.Kafka.AdminClient;
|
||||||
|
using JiShe.CollectBus.Kafka.Consumer;
|
||||||
|
using JiShe.CollectBus.Kafka.Producer;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Serilog;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Kafka.Test
|
||||||
|
{
|
||||||
|
[SimpleJob(RuntimeMoniker.Net80)]
|
||||||
|
//[SimpleJob(RuntimeMoniker.NativeAot80)]
|
||||||
|
[RPlotExporter]
|
||||||
|
public class KafkaProduceBenchmark
|
||||||
|
{
|
||||||
|
|
||||||
|
// 每批消息数量
|
||||||
|
[Params(1000, 10000, 100000)]
|
||||||
|
public int N;
|
||||||
|
public ServiceProvider _serviceProvider;
|
||||||
|
public IConsumerService _consumerService;
|
||||||
|
public IProducerService _producerService;
|
||||||
|
public string topic = "test-topic1";
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
// 构建配置
|
||||||
|
var config = new ConfigurationBuilder()
|
||||||
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
|
.AddJsonFile("appsettings.json")
|
||||||
|
.Build();
|
||||||
|
// 直接读取配置项
|
||||||
|
var greeting = config["ServerTagName"];
|
||||||
|
Console.WriteLine(greeting); // 输出: Hello, World!
|
||||||
|
// 创建服务容器
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
// 注册 IConfiguration 实例
|
||||||
|
services.AddSingleton<IConfiguration>(config);
|
||||||
|
|
||||||
|
// 初始化日志
|
||||||
|
Log.Logger = new LoggerConfiguration()
|
||||||
|
.ReadFrom.Configuration(config) // 从 appsettings.json 读取配置
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
|
// 配置日志系统
|
||||||
|
services.AddLogging(logging =>
|
||||||
|
{
|
||||||
|
logging.ClearProviders();
|
||||||
|
logging.AddSerilog();
|
||||||
|
});
|
||||||
|
services.AddSingleton<IAdminClientService, AdminClientService>();
|
||||||
|
services.AddSingleton<IProducerService, ProducerService>();
|
||||||
|
services.AddSingleton<IConsumerService, ConsumerService>();
|
||||||
|
|
||||||
|
// 构建ServiceProvider
|
||||||
|
_serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
// 获取日志记录器工厂
|
||||||
|
var loggerFactory = _serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||||
|
var logger = loggerFactory.CreateLogger<Program>();
|
||||||
|
logger.LogInformation("程序启动");
|
||||||
|
|
||||||
|
var adminClientService = _serviceProvider.GetRequiredService<IAdminClientService>();
|
||||||
|
|
||||||
|
|
||||||
|
//await adminClientService.DeleteTopicAsync(topic);
|
||||||
|
// 创建 topic
|
||||||
|
adminClientService.CreateTopicAsync(topic, 3, 3).ConfigureAwait(false).GetAwaiter();
|
||||||
|
|
||||||
|
_consumerService = _serviceProvider.GetRequiredService<IConsumerService>();
|
||||||
|
|
||||||
|
_producerService = _serviceProvider.GetRequiredService<IProducerService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public async Task UseAsync()
|
||||||
|
{
|
||||||
|
List<Task> tasks = new();
|
||||||
|
for (int i = 0; i < N; ++i)
|
||||||
|
{
|
||||||
|
var task = _producerService.ProduceAsync<string>(topic, i.ToString());
|
||||||
|
tasks.Add(task);
|
||||||
|
}
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public async Task UseLibrd()
|
||||||
|
{
|
||||||
|
List<Task> tasks = new();
|
||||||
|
for (int i = 0; i < N; ++i)
|
||||||
|
{
|
||||||
|
var task = _producerService.ProduceAsync<string>(topic, i.ToString(),null);
|
||||||
|
}
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
modules/JiShe.CollectBus.Kafka.Test/KafkaSubscribeTest.cs
Normal file
68
modules/JiShe.CollectBus.Kafka.Test/KafkaSubscribeTest.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
using JiShe.CollectBus.Common.Consts;
|
||||||
|
using JiShe.CollectBus.Common.Enums;
|
||||||
|
using JiShe.CollectBus.Common.Models;
|
||||||
|
using JiShe.CollectBus.IotSystems.MessageReceiveds;
|
||||||
|
using JiShe.CollectBus.Kafka.Attributes;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Volo.Abp.Timing;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Kafka.Test
|
||||||
|
{
|
||||||
|
public class KafkaSubscribeTest: IKafkaSubscribe
|
||||||
|
{
|
||||||
|
[KafkaSubscribe(ProtocolConst.TESTTOPIC, EnableBatch=false,BatchSize=1000)]
|
||||||
|
|
||||||
|
public async Task<ISubscribeAck> KafkaSubscribeAsync(object 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.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.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
172
modules/JiShe.CollectBus.Kafka.Test/Program.cs
Normal file
172
modules/JiShe.CollectBus.Kafka.Test/Program.cs
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
// See https://aka.ms/new-console-template for more information
|
||||||
|
using BenchmarkDotNet.Configs;
|
||||||
|
using BenchmarkDotNet.Running;
|
||||||
|
using Confluent.Kafka;
|
||||||
|
using DeviceDetectorNET.Parser.Device;
|
||||||
|
using JiShe.CollectBus.Common.Consts;
|
||||||
|
using JiShe.CollectBus.Kafka;
|
||||||
|
using JiShe.CollectBus.Kafka.AdminClient;
|
||||||
|
using JiShe.CollectBus.Kafka.Consumer;
|
||||||
|
using JiShe.CollectBus.Kafka.Producer;
|
||||||
|
using JiShe.CollectBus.Kafka.Test;
|
||||||
|
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 Serilog;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Reflection.PortableExecutable;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
#region 基准测试
|
||||||
|
//var summary = BenchmarkRunner.Run<KafkaProduceBenchmark>();
|
||||||
|
//Console.WriteLine("压测完成");
|
||||||
|
//return;
|
||||||
|
#endregion 基准测试
|
||||||
|
|
||||||
|
|
||||||
|
var host = Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
// 构建配置
|
||||||
|
var config = new ConfigurationBuilder()
|
||||||
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
|
.AddJsonFile("appsettings.json")
|
||||||
|
.Build();
|
||||||
|
// 直接读取配置项
|
||||||
|
var greeting = config["Kafka:ServerTagName"];
|
||||||
|
Console.WriteLine(greeting); // 输出: Hello, World!
|
||||||
|
|
||||||
|
|
||||||
|
// 创建服务容器
|
||||||
|
//var services = new ServiceCollection();
|
||||||
|
// 注册 IConfiguration 实例
|
||||||
|
services.AddSingleton<IConfiguration>(config);
|
||||||
|
|
||||||
|
// 初始化日志
|
||||||
|
Log.Logger = new LoggerConfiguration()
|
||||||
|
.ReadFrom.Configuration(config) // 从 appsettings.json 读取配置
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
|
// 配置日志系统
|
||||||
|
services.AddLogging(logging =>
|
||||||
|
{
|
||||||
|
logging.ClearProviders();
|
||||||
|
logging.AddSerilog();
|
||||||
|
});
|
||||||
|
services.Configure<KafkaOptionConfig>(config.GetSection("Kafka"));
|
||||||
|
|
||||||
|
services.AddSingleton<IAdminClientService, AdminClientService>();
|
||||||
|
services.AddSingleton<IProducerService, ProducerService>();
|
||||||
|
services.AddSingleton<IConsumerService, ConsumerService>();
|
||||||
|
services.AddTransient<KafkaSubscribeTest>();
|
||||||
|
|
||||||
|
})
|
||||||
|
.ConfigureConsoleAppBuilder(appBuilder =>
|
||||||
|
{
|
||||||
|
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
|
||||||
|
await host.StartAsync();
|
||||||
|
var appBuilder = host.Services.GetRequiredService<IApplicationBuilder>();
|
||||||
|
appBuilder.ApplicationServices.UseKafkaSubscribe();
|
||||||
|
|
||||||
|
|
||||||
|
// 构建ServiceProvider
|
||||||
|
//var serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
// 获取日志记录器工厂
|
||||||
|
var loggerFactory = host.Services.GetRequiredService<ILoggerFactory>();
|
||||||
|
var logger = loggerFactory.CreateLogger<Program>();
|
||||||
|
logger.LogInformation("程序启动");
|
||||||
|
var adminClientService = host.Services.GetRequiredService<IAdminClientService>();
|
||||||
|
var configuration = host.Services.GetRequiredService<IConfiguration>();
|
||||||
|
string topic = "test-topic";
|
||||||
|
//await adminClientService.DeleteTopicAsync(topic);
|
||||||
|
// 创建 topic
|
||||||
|
//await adminClientService.CreateTopicAsync(topic, configuration.GetValue<int>(CommonConst.NumPartitions), 3);
|
||||||
|
|
||||||
|
var consumerService = host.Services.GetRequiredService<IConsumerService>();
|
||||||
|
//var kafkaOptions = host.Services.GetRequiredService<IOptions<KafkaOptionConfig>>();
|
||||||
|
//await consumerService.SubscribeAsync<object>(topic, (message) =>
|
||||||
|
//{
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// logger.LogInformation($"消费消息:{message}");
|
||||||
|
// return Task.FromResult(true);
|
||||||
|
|
||||||
|
// }
|
||||||
|
// catch (ConsumeException ex)
|
||||||
|
// {
|
||||||
|
// // 处理消费错误
|
||||||
|
// logger.LogError($"kafka消费异常:{ex.Message}");
|
||||||
|
// }
|
||||||
|
// return Task.FromResult(false);
|
||||||
|
//}, "default");
|
||||||
|
|
||||||
|
//Stopwatch stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
//for (int i = 0; i < 3; i++)
|
||||||
|
//{
|
||||||
|
// await consumerService.SubscribeBatchAsync<dynamic>(topic, (message) =>
|
||||||
|
// {
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// int index = 0;
|
||||||
|
// logger.LogInformation($"消费消息_{index}消费总数:{message.Count()}:{JsonSerializer.Serialize(message)}");
|
||||||
|
// return Task.FromResult(true);
|
||||||
|
|
||||||
|
// }
|
||||||
|
// catch (ConsumeException ex)
|
||||||
|
// {
|
||||||
|
// // 处理消费错误
|
||||||
|
// logger.LogError($"kafka消费异常:{ex.Message}");
|
||||||
|
// }
|
||||||
|
// return Task.FromResult(false);
|
||||||
|
// });
|
||||||
|
//}
|
||||||
|
//stopwatch.Stop();
|
||||||
|
//Console.WriteLine($"耗时: {stopwatch.ElapsedMilliseconds} 毫秒,{stopwatch.ElapsedMilliseconds/1000} 秒");
|
||||||
|
|
||||||
|
var producerService = host.Services.GetRequiredService<IProducerService>();
|
||||||
|
//int num = 840;
|
||||||
|
//while (num <= 900)
|
||||||
|
//{
|
||||||
|
// //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)
|
||||||
|
{
|
||||||
|
var key = Console.ReadKey(intercept: true); // intercept:true 隐藏按键显示
|
||||||
|
|
||||||
|
if (key.Key == ConsoleKey.Escape)
|
||||||
|
{
|
||||||
|
await host.StopAsync();
|
||||||
|
Console.WriteLine("\n程序已退出");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(host.Services as IDisposable)?.Dispose();
|
||||||
|
|
||||||
|
|
||||||
|
public class TestTopic
|
||||||
|
{
|
||||||
|
public string Topic { get; set; }
|
||||||
|
public int Val { get; set; }
|
||||||
|
}
|
||||||
180
modules/JiShe.CollectBus.Kafka.Test/appsettings.json
Normal file
180
modules/JiShe.CollectBus.Kafka.Test/appsettings.json
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
{
|
||||||
|
"Serilog": {
|
||||||
|
"Using": [
|
||||||
|
"Serilog.Sinks.Console",
|
||||||
|
"Serilog.Sinks.File"
|
||||||
|
],
|
||||||
|
"MinimumLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Override": {
|
||||||
|
"Microsoft": "Warning",
|
||||||
|
"Volo.Abp": "Warning",
|
||||||
|
"Hangfire": "Warning",
|
||||||
|
"DotNetCore.CAP": "Warning",
|
||||||
|
"Serilog.AspNetCore": "Information",
|
||||||
|
"Microsoft.EntityFrameworkCore": "Warning",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"WriteTo": [
|
||||||
|
{
|
||||||
|
"Name": "Console"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "File",
|
||||||
|
"Args": {
|
||||||
|
"path": "logs/logs-.txt",
|
||||||
|
"rollingInterval": "Day"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"App": {
|
||||||
|
"SelfUrl": "http://localhost:44315",
|
||||||
|
"CorsOrigins": "http://localhost:4200,http://localhost:3100"
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Default": "mongodb://admin:admin02023@118.190.144.92:37117,118.190.144.92:37119,118.190.144.92:37120/JiSheCollectBus?authSource=admin&maxPoolSize=400&minPoolSize=10&waitQueueTimeoutMS=5000",
|
||||||
|
"Kafka": "192.168.1.9:29092,192.168.1.9:39092,192.168.1.9:49092",
|
||||||
|
"PrepayDB": "server=118.190.144.92;database=jishe.sysdb;uid=sa;pwd=admin@2023;Encrypt=False;Trust Server Certificate=False",
|
||||||
|
"EnergyDB": "server=118.190.144.92;database=db_energy;uid=sa;pwd=admin@2023;Encrypt=False;Trust Server Certificate=False"
|
||||||
|
},
|
||||||
|
"Redis": {
|
||||||
|
"Configuration": "192.168.1.9:6380,password=1q2w3e!@#,syncTimeout=30000,abortConnect=false,connectTimeout=30000,allowAdmin=true",
|
||||||
|
"MaxPoolSize": "50",
|
||||||
|
"DefaultDB": "14",
|
||||||
|
"HangfireDB": "15"
|
||||||
|
},
|
||||||
|
"Jwt": {
|
||||||
|
"Audience": "JiShe.CollectBus",
|
||||||
|
"SecurityKey": "dzehzRz9a8asdfasfdadfasdfasdfafsdadfasbasdf=",
|
||||||
|
"Issuer": "JiShe.CollectBus",
|
||||||
|
"ExpirationTime": 2
|
||||||
|
},
|
||||||
|
"HealthCheck": {
|
||||||
|
"IsEnable": true,
|
||||||
|
"MySql": {
|
||||||
|
"IsEnable": true
|
||||||
|
},
|
||||||
|
"Pings": {
|
||||||
|
"IsEnable": true,
|
||||||
|
"Host": "https://www.baidu.com/",
|
||||||
|
"TimeOut": 5000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SwaggerConfig": [
|
||||||
|
{
|
||||||
|
"GroupName": "Basic",
|
||||||
|
"Title": "【后台管理】基础模块",
|
||||||
|
"Version": "V1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "Business",
|
||||||
|
"Title": "【后台管理】业务模块",
|
||||||
|
"Version": "V1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Cap": {
|
||||||
|
"RabbitMq": {
|
||||||
|
"HostName": "118.190.144.92",
|
||||||
|
"UserName": "collectbus",
|
||||||
|
"Password": "123456",
|
||||||
|
"Port": 5672
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Kafka": {
|
||||||
|
"BootstrapServers": "192.168.1.9:29092,192.168.1.9:39092,192.168.1.9:49092",
|
||||||
|
"EnableFilter": true,
|
||||||
|
"EnableAuthorization": false,
|
||||||
|
"SecurityProtocol": "SaslPlaintext",
|
||||||
|
"SaslMechanism": "Plain",
|
||||||
|
"SaslUserName": "lixiao",
|
||||||
|
"SaslPassword": "lixiao1980",
|
||||||
|
"KafkaReplicationFactor": 3,
|
||||||
|
"NumPartitions": 1,
|
||||||
|
"ServerTagName": "JiSheCollectBus2"
|
||||||
|
//"Topic": {
|
||||||
|
// "ReplicationFactor": 3,
|
||||||
|
// "NumPartitions": 1000
|
||||||
|
//}
|
||||||
|
},
|
||||||
|
//"Kafka": {
|
||||||
|
// "Connections": {
|
||||||
|
// "Default": {
|
||||||
|
// "BootstrapServers": "192.168.1.9:29092,192.168.1.9:39092,192.168.1.9:49092"
|
||||||
|
// // "SecurityProtocol": "SASL_PLAINTEXT",
|
||||||
|
// // "SaslMechanism": "PLAIN",
|
||||||
|
// // "SaslUserName": "lixiao",
|
||||||
|
// // "SaslPassword": "lixiao1980",
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "Consumer": {
|
||||||
|
// "GroupId": "JiShe.CollectBus"
|
||||||
|
// },
|
||||||
|
// "Producer": {
|
||||||
|
// "MessageTimeoutMs": 6000,
|
||||||
|
// "Acks": -1
|
||||||
|
// },
|
||||||
|
// "Topic": {
|
||||||
|
// "ReplicationFactor": 3,
|
||||||
|
// "NumPartitions": 1000
|
||||||
|
// },
|
||||||
|
// "EventBus": {
|
||||||
|
// "GroupId": "JiShe.CollectBus",
|
||||||
|
// "TopicName": "DefaultTopicName"
|
||||||
|
// }
|
||||||
|
//},
|
||||||
|
"IoTDBOptions": {
|
||||||
|
"UserName": "root",
|
||||||
|
"Password": "root",
|
||||||
|
"ClusterList": [ "192.168.1.9:6667" ],
|
||||||
|
"PoolSize": 2,
|
||||||
|
"DataBaseName": "energy",
|
||||||
|
"OpenDebugMode": true,
|
||||||
|
"UseTableSessionPoolByDefault": false
|
||||||
|
},
|
||||||
|
"ServerTagName": "JiSheCollectBus3",
|
||||||
|
"Cassandra": {
|
||||||
|
"ReplicationStrategy": {
|
||||||
|
"Class": "NetworkTopologyStrategy", //策略为NetworkTopologyStrategy时才会有多个数据中心,SimpleStrategy用在只有一个数据中心的情况下
|
||||||
|
"DataCenters": [
|
||||||
|
{
|
||||||
|
"Name": "dc1",
|
||||||
|
"ReplicationFactor": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Nodes": [
|
||||||
|
{
|
||||||
|
"Host": "192.168.1.9",
|
||||||
|
"Port": 9042,
|
||||||
|
"DataCenter": "dc1",
|
||||||
|
"Rack": "RAC1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Host": "192.168.1.9",
|
||||||
|
"Port": 9043,
|
||||||
|
"DataCenter": "dc1",
|
||||||
|
"Rack": "RAC2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Username": "admin",
|
||||||
|
"Password": "lixiao1980",
|
||||||
|
"Keyspace": "jishecollectbus",
|
||||||
|
"ConsistencyLevel": "Quorum",
|
||||||
|
"PoolingOptions": {
|
||||||
|
"CoreConnectionsPerHost": 4,
|
||||||
|
"MaxConnectionsPerHost": 8,
|
||||||
|
"MaxRequestsPerConnection": 2000
|
||||||
|
},
|
||||||
|
"SocketOptions": {
|
||||||
|
"ConnectTimeoutMillis": 10000,
|
||||||
|
"ReadTimeoutMillis": 20000
|
||||||
|
},
|
||||||
|
"QueryOptions": {
|
||||||
|
"ConsistencyLevel": "Quorum",
|
||||||
|
"SerialConsistencyLevel": "Serial",
|
||||||
|
"DefaultIdempotence": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
204
modules/JiShe.CollectBus.Kafka/AdminClient/AdminClientService.cs
Normal file
204
modules/JiShe.CollectBus.Kafka/AdminClient/AdminClientService.cs
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
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.Logging;
|
||||||
|
using Volo.Abp.DependencyInjection;
|
||||||
|
|
||||||
|
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>
|
||||||
|
public AdminClientService(IConfiguration configuration, ILogger<AdminClientService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
GetInstance(configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The instance.
|
||||||
|
/// </value>
|
||||||
|
public IAdminClient Instance { get; set; } = default;
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task CreateTopicAsync(string topic, int numPartitions, short replicationFactor)
|
||||||
|
{
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (await CheckTopicAsync(topic)) return;
|
||||||
|
|
||||||
|
|
||||||
|
await Instance.CreateTopicsAsync(new[]
|
||||||
|
{
|
||||||
|
new TopicSpecification
|
||||||
|
{
|
||||||
|
Name = topic,
|
||||||
|
NumPartitions = numPartitions,
|
||||||
|
ReplicationFactor = replicationFactor
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (CreateTopicsException e)
|
||||||
|
{
|
||||||
|
if (e.Results[0].Error.Code != ErrorCode.TopicAlreadyExists)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除Kafka主题
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task DeleteTopicAsync(string topic)
|
||||||
|
{
|
||||||
|
await Instance.DeleteTopicsAsync(new[] { topic });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取Kafka主题列表
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<List<string>> ListTopicsAsync()
|
||||||
|
{
|
||||||
|
var metadata = Instance.GetMetadata(TimeSpan.FromSeconds(10));
|
||||||
|
return new List<string>(metadata.Topics.Select(t => t.Topic));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断Kafka主题是否存在
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<bool> TopicExistsAsync(string topic)
|
||||||
|
{
|
||||||
|
var metadata = Instance.GetMetadata(TimeSpan.FromSeconds(10));
|
||||||
|
return metadata.Topics.Any(t => t.Topic == topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检测分区是否存在
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <param name="partitions"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Dictionary<int, bool> CheckPartitionsExists(string topic, int[] partitions)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<int, bool>();
|
||||||
|
var metadata = Instance.GetMetadata(topic, TimeSpan.FromSeconds(10));
|
||||||
|
if (metadata.Topics.Count == 0)
|
||||||
|
return partitions.ToDictionary(p => p, p => false);
|
||||||
|
var existingPartitions = metadata.Topics[0].Partitions.Select(p => p.PartitionId).ToHashSet();
|
||||||
|
return partitions.ToDictionary(p => p, p => existingPartitions.Contains(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检测分区是否存在
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <param name="targetPartition"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool CheckPartitionsExist(string topic, int targetPartition)
|
||||||
|
{
|
||||||
|
var metadata = Instance.GetMetadata(topic, TimeSpan.FromSeconds(10));
|
||||||
|
if (metadata.Topics.Count == 0)
|
||||||
|
return false;
|
||||||
|
var partitions = metadata.Topics[0].Partitions;
|
||||||
|
return partitions.Any(p => p.PartitionId == targetPartition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取主题的分区数量
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public int GetTopicPartitionsNum(string topic)
|
||||||
|
{
|
||||||
|
var metadata = Instance.GetMetadata(topic, TimeSpan.FromSeconds(10));
|
||||||
|
if (metadata.Topics.Count == 0)
|
||||||
|
return 0;
|
||||||
|
return metadata.Topics[0].Partitions.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Instance?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Kafka.AdminClient
|
||||||
|
{
|
||||||
|
public interface IAdminClientService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建Kafka主题
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic">主题名称</param>
|
||||||
|
/// <param name="numPartitions">主题分区数量</param>
|
||||||
|
/// <param name="replicationFactor">副本数量,不能高于Brokers数量</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task CreateTopicAsync(string topic, int numPartitions, short replicationFactor);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除Kafka主题
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task DeleteTopicAsync(string topic);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取Kafka主题列表
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<List<string>> ListTopicsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断Kafka主题是否存在
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<bool> TopicExistsAsync(string topic);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检测分区是否存在
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <param name="partitions"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Dictionary<int, bool> CheckPartitionsExists(string topic, int[] partitions);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检测分区是否存在
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <param name="targetPartition"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
bool CheckPartitionsExist(string topic, int targetPartition);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取主题的分区数量
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
int GetTopicPartitionsNum(string topic);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Kafka.Attributes
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public class KafkaSubscribeAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅的主题
|
||||||
|
/// </summary>
|
||||||
|
public string Topic { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分区
|
||||||
|
/// </summary>
|
||||||
|
public int Partition { get; set; } = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 消费者组
|
||||||
|
/// </summary>
|
||||||
|
public string GroupId { get; set; } = "default";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务数(默认是多少个分区多少个任务)
|
||||||
|
/// 如设置订阅指定Partition则任务数始终为1
|
||||||
|
/// </summary>
|
||||||
|
public int TaskCount { get; set; } = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量处理数量
|
||||||
|
/// </summary>
|
||||||
|
public int BatchSize { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用批量处理
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableBatch { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批次超时时间
|
||||||
|
/// 格式:("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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
modules/JiShe.CollectBus.Kafka/Attributes/TopicAttribute.cs
Normal file
29
modules/JiShe.CollectBus.Kafka/Attributes/TopicAttribute.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Kafka.Attributes
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
|
public class TopicAttribute: Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TopicAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name.</param>
|
||||||
|
public TopicAttribute(string name = "Default")
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The name.
|
||||||
|
/// </value>
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
57
modules/JiShe.CollectBus.Kafka/CollectBusKafkaModule.cs
Normal file
57
modules/JiShe.CollectBus.Kafka/CollectBusKafkaModule.cs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
using Confluent.Kafka;
|
||||||
|
using JiShe.CollectBus.Common.Consts;
|
||||||
|
using JiShe.CollectBus.Kafka.Consumer;
|
||||||
|
using JiShe.CollectBus.Kafka.Producer;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System.Reflection;
|
||||||
|
using Volo.Abp;
|
||||||
|
using Volo.Abp.DependencyInjection;
|
||||||
|
using Volo.Abp.Modularity;
|
||||||
|
using static Confluent.Kafka.ConfigPropertyNames;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Kafka
|
||||||
|
{
|
||||||
|
public class CollectBusKafkaModule : AbpModule
|
||||||
|
{
|
||||||
|
public override void ConfigureServices(ServiceConfigurationContext context)
|
||||||
|
{
|
||||||
|
var configuration = context.Services.GetConfiguration();
|
||||||
|
//var kafkaSection = configuration.GetSection(CommonConst.Kafka);
|
||||||
|
//KafkaOptionConfig kafkaOptionConfig = new KafkaOptionConfig ();
|
||||||
|
//kafkaSection.Bind(kafkaOptionConfig);
|
||||||
|
//if (configuration[CommonConst.ServerTagName] != null)
|
||||||
|
//{
|
||||||
|
// kafkaOptionConfig.ServerTagName = configuration[CommonConst.ServerTagName]!;
|
||||||
|
//}
|
||||||
|
//context.Services.AddSingleton(kafkaOptionConfig);
|
||||||
|
|
||||||
|
//context.Services.Configure<KafkaOptionConfig>(context.Services.GetConfiguration().GetSection(CommonConst.Kafka));
|
||||||
|
|
||||||
|
Configure<KafkaOptionConfig>(options =>
|
||||||
|
{
|
||||||
|
configuration.GetSection(CommonConst.Kafka).Bind(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 注册Producer
|
||||||
|
context.Services.AddSingleton<IProducerService, ProducerService>();
|
||||||
|
// 注册Consumer
|
||||||
|
context.Services.AddSingleton<IConsumerService, ConsumerService>();
|
||||||
|
|
||||||
|
//context.Services.AddHostedService<HostedService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnApplicationInitialization(ApplicationInitializationContext context)
|
||||||
|
{
|
||||||
|
var app = context.GetApplicationBuilder();
|
||||||
|
|
||||||
|
// 注册Subscriber
|
||||||
|
app.ApplicationServices.UseKafkaSubscribe();
|
||||||
|
|
||||||
|
// 获取程序集
|
||||||
|
//app.UseKafkaSubscribers(Assembly.Load("JiShe.CollectBus.Application"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
540
modules/JiShe.CollectBus.Kafka/Consumer/ConsumerService.cs
Normal file
540
modules/JiShe.CollectBus.Kafka/Consumer/ConsumerService.cs
Normal file
@ -0,0 +1,540 @@
|
|||||||
|
using Confluent.Kafka;
|
||||||
|
using JiShe.CollectBus.Common.Consts;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
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)>
|
||||||
|
_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)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
_kafkaOptionConfig = kafkaOptionConfig.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region private 私有方法
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建消费者
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey"></typeparam>
|
||||||
|
/// <typeparam name="TValue"></typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
private IConsumer<TKey, TValue> CreateConsumer<TKey, TValue>(string? groupId = null) where TKey : notnull where TValue : class
|
||||||
|
{
|
||||||
|
var config = BuildConsumerConfig(groupId);
|
||||||
|
return new ConsumerBuilder<TKey, TValue>(config)
|
||||||
|
.SetValueDeserializer(new JsonSerializer<TValue>())
|
||||||
|
.SetLogHandler((_, log) => _logger.LogInformation($"消费者Log: {log.Message}"))
|
||||||
|
.SetErrorHandler((_, e) => _logger.LogError($"消费者错误: {e.Reason}"))
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConsumerConfig BuildConsumerConfig(string? groupId = null)
|
||||||
|
{
|
||||||
|
var config = new ConsumerConfig
|
||||||
|
{
|
||||||
|
BootstrapServers = _kafkaOptionConfig.BootstrapServers,
|
||||||
|
GroupId = groupId ?? "default",
|
||||||
|
AutoOffsetReset = AutoOffsetReset.Earliest,
|
||||||
|
EnableAutoCommit = false, // 禁止AutoCommit
|
||||||
|
EnablePartitionEof = true, // 启用分区末尾标记
|
||||||
|
AllowAutoCreateTopics = true, // 启用自动创建
|
||||||
|
FetchMaxBytes = 1024 * 1024 * 50 // 增加拉取大小(50MB)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_kafkaOptionConfig.EnableAuthorization)
|
||||||
|
{
|
||||||
|
config.SecurityProtocol = _kafkaOptionConfig.SecurityProtocol;
|
||||||
|
config.SaslMechanism = _kafkaOptionConfig.SaslMechanism;
|
||||||
|
config.SaslUsername = _kafkaOptionConfig.SaslUserName;
|
||||||
|
config.SaslPassword = _kafkaOptionConfig.SaslPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅消息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey"></typeparam>
|
||||||
|
/// <typeparam name="TValue"></typeparam>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <param name="messageHandler"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task SubscribeAsync<TKey, TValue>(string topic, Func<TKey, TValue, Task<bool>> messageHandler, string? groupId = null) where TKey : notnull where TValue : class
|
||||||
|
{
|
||||||
|
await SubscribeAsync<TKey, TValue>(new[] { topic }, messageHandler, groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅消息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TValue"></typeparam>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <param name="messageHandler"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task SubscribeAsync<TValue>(string topic, Func<TValue, Task<bool>> messageHandler, string? groupId = null) where TValue : class
|
||||||
|
{
|
||||||
|
await SubscribeAsync<TValue>(new[] { topic }, messageHandler,groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅消息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey"></typeparam>
|
||||||
|
/// <typeparam name="TValue"></typeparam>
|
||||||
|
/// <param name="topics"></param>
|
||||||
|
/// <param name="messageHandler"></param>
|
||||||
|
/// <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>);
|
||||||
|
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);
|
||||||
|
consumer!.Subscribe(topics);
|
||||||
|
|
||||||
|
await Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (!cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//_logger.LogInformation($"Kafka消费: {string.Join("", topics)} 开始拉取消息....");
|
||||||
|
|
||||||
|
var result = consumer.Consume(cts.Token);
|
||||||
|
if (result == null || result.Message==null || result.Message.Value == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (result.IsPartitionEOF)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Kafka消费: {Topic} 分区 {Partition} 已消费完", result.Topic, result.Partition);
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1),cts.Token);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (_kafkaOptionConfig.EnableFilter)
|
||||||
|
{
|
||||||
|
var headersFilter = new HeadersFilter { { "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) } };
|
||||||
|
// 检查 Header 是否符合条件
|
||||||
|
if (!headersFilter.Match(result.Message.Headers))
|
||||||
|
{
|
||||||
|
//consumer.Commit(result); // 提交偏移量
|
||||||
|
// 跳过消息
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool sucess= await messageHandler(result.Message.Key, result.Message.Value);
|
||||||
|
if (sucess)
|
||||||
|
{
|
||||||
|
consumer.Commit(result); // 手动提交
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ConsumeException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"{string.Join("、", topics)}消息消费失败: {ex.Error.Reason}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅消息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey"></typeparam>
|
||||||
|
/// <typeparam name="TValue"></typeparam>
|
||||||
|
/// <param name="topics"></param>
|
||||||
|
/// <param name="messageHandler"></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>);
|
||||||
|
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 = CreateConsumer<Ignore, TValue>(groupId);
|
||||||
|
consumer!.Subscribe(topics);
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
while (!cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//_logger.LogInformation($"Kafka消费: {string.Join("", topics)}_{count} 开始拉取消息....");
|
||||||
|
count++;
|
||||||
|
var result = consumer.Consume(cts.Token);
|
||||||
|
if (result == null || result.Message == null || result.Message.Value == null)
|
||||||
|
{
|
||||||
|
await Task.Delay(500, cts.Token);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.IsPartitionEOF)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Kafka消费: {Topic} 分区 {Partition} 已消费完", result.Topic, result.Partition);
|
||||||
|
await Task.Delay(100, cts.Token);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (_kafkaOptionConfig.EnableFilter)
|
||||||
|
{
|
||||||
|
var headersFilter = new HeadersFilter { { "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) } };
|
||||||
|
// 检查 Header 是否符合条件
|
||||||
|
if (!headersFilter.Match(result.Message.Headers))
|
||||||
|
{
|
||||||
|
await Task.Delay(500, cts.Token);
|
||||||
|
//consumer.Commit(result); // 提交偏移量
|
||||||
|
// 跳过消息
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool sucess = await messageHandler(result.Message.Value);
|
||||||
|
if (sucess)
|
||||||
|
consumer.Commit(result); // 手动提交
|
||||||
|
else
|
||||||
|
consumer.StoreOffset(result);
|
||||||
|
}
|
||||||
|
catch (ConsumeException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"{string.Join("、", topics)}消息消费失败: {ex.Error.Reason}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"Kafka消费异常: {ex.Message}");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量订阅消息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey">消息Key类型</typeparam>
|
||||||
|
/// <typeparam name="TValue">消息Value类型</typeparam>
|
||||||
|
/// <param name="topic">主题</param>
|
||||||
|
/// <param name="messageBatchHandler">批量消息处理函数</param>
|
||||||
|
/// <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
|
||||||
|
{
|
||||||
|
await SubscribeBatchAsync<TKey, TValue>(new[] { topic }, messageBatchHandler, groupId, batchSize, batchTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量订阅消息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey">消息Key类型</typeparam>
|
||||||
|
/// <typeparam name="TValue">消息Value类型</typeparam>
|
||||||
|
/// <param name="topics">主题列表</param>
|
||||||
|
/// <param name="messageBatchHandler">批量消息处理函数</param>
|
||||||
|
/// <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
|
||||||
|
{
|
||||||
|
var consumerKey = typeof(KafkaConsumer<TKey, TValue>);
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var consumer = _consumerStore.GetOrAdd(consumerKey, _ =>
|
||||||
|
(
|
||||||
|
CreateConsumer<TKey, TValue>(groupId),
|
||||||
|
cts
|
||||||
|
)).Consumer as IConsumer<TKey, TValue>;
|
||||||
|
|
||||||
|
consumer!.Subscribe(topics);
|
||||||
|
|
||||||
|
var timeout = batchTimeout ?? TimeSpan.FromSeconds(5); // 默认超时时间调整为5秒
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var messages = new List<(TValue Value, TopicPartitionOffset Offset)>();
|
||||||
|
var startTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
while (!cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 非阻塞快速累积消息
|
||||||
|
while (messages.Count < batchSize && (DateTime.UtcNow - startTime) < timeout)
|
||||||
|
{
|
||||||
|
var result = consumer.Consume(TimeSpan.Zero); // 非阻塞调用
|
||||||
|
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
if (result.IsPartitionEOF)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Kafka消费: {Topic} 分区 {Partition} 已消费完", result.Topic, result.Partition);
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1), cts.Token);
|
||||||
|
}
|
||||||
|
else if (result.Message.Value != null)
|
||||||
|
{
|
||||||
|
if (_kafkaOptionConfig.EnableFilter)
|
||||||
|
{
|
||||||
|
var headersFilter = new HeadersFilter { { "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) } };
|
||||||
|
// 检查 Header 是否符合条件
|
||||||
|
if (!headersFilter.Match(result.Message.Headers))
|
||||||
|
{
|
||||||
|
//consumer.Commit(result); // 提交偏移量
|
||||||
|
// 跳过消息
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messages.Add((result.Message.Value, result.TopicPartitionOffset));
|
||||||
|
//messages.Add(result.Message.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 无消息时短暂等待
|
||||||
|
await Task.Delay(10, cts.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理批次
|
||||||
|
if (messages.Count > 0)
|
||||||
|
{
|
||||||
|
bool success = await messageBatchHandler(messages.Select(m => m.Value));
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
var offsetsByPartition = new Dictionary<TopicPartition, long>();
|
||||||
|
foreach (var msg in messages)
|
||||||
|
{
|
||||||
|
var tp = msg.Offset.TopicPartition;
|
||||||
|
var offset = msg.Offset.Offset;
|
||||||
|
if (!offsetsByPartition.TryGetValue(tp, out var currentMax) || offset > currentMax)
|
||||||
|
{
|
||||||
|
offsetsByPartition[tp] = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var offsetsToCommit = offsetsByPartition
|
||||||
|
.Select(kv => new TopicPartitionOffset(kv.Key, new Offset(kv.Value + 1)))
|
||||||
|
.ToList();
|
||||||
|
consumer.Commit(offsetsToCommit);
|
||||||
|
}
|
||||||
|
messages.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
catch (ConsumeException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"{string.Join("、", topics)} 消息消费失败: {ex.Error.Reason}");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// 任务取消,正常退出
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "处理批量消息时发生未知错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, cts.Token);
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量订阅消息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TValue">消息Value类型</typeparam>
|
||||||
|
/// <param name="topic">主题列表</param>
|
||||||
|
/// <param name="messageBatchHandler">批量消息处理函数</param>
|
||||||
|
/// <param name="groupId">消费组ID</param>
|
||||||
|
/// <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
|
||||||
|
{
|
||||||
|
await SubscribeBatchAsync<TValue>(new[] { topic }, messageBatchHandler, groupId, batchSize, batchTimeout, consumeTimeout);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量订阅消息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TValue">消息Value类型</typeparam>
|
||||||
|
/// <param name="topics">主题列表</param>
|
||||||
|
/// <param name="messageBatchHandler">批量消息处理函数</param>
|
||||||
|
/// <param name="groupId">消费组ID</param>
|
||||||
|
/// <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
|
||||||
|
{
|
||||||
|
var consumerKey = typeof(KafkaConsumer<string, TValue>);
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var consumer = _consumerStore.GetOrAdd(consumerKey, _ =>
|
||||||
|
(
|
||||||
|
CreateConsumer<string, TValue>(groupId),
|
||||||
|
cts
|
||||||
|
)).Consumer as IConsumer<string, TValue>;
|
||||||
|
|
||||||
|
consumer!.Subscribe(topics);
|
||||||
|
|
||||||
|
var timeout = batchTimeout ?? TimeSpan.FromSeconds(5); // 默认超时时间调整为5秒
|
||||||
|
|
||||||
|
_ = 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)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 非阻塞快速累积消息
|
||||||
|
while (messages.Count < batchSize && (DateTime.UtcNow - startTime) < timeout)
|
||||||
|
{
|
||||||
|
var result = consumer.Consume(TimeSpan.Zero); // 非阻塞调用
|
||||||
|
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
if (result.IsPartitionEOF)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Kafka消费: {Topic} 分区 {Partition} 已消费完", result.Topic, result.Partition);
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1), cts.Token);
|
||||||
|
}
|
||||||
|
else if (result.Message.Value != null)
|
||||||
|
{
|
||||||
|
if (_kafkaOptionConfig.EnableFilter)
|
||||||
|
{
|
||||||
|
var headersFilter = new HeadersFilter { { "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) } };
|
||||||
|
// 检查 Header 是否符合条件
|
||||||
|
if (!headersFilter.Match(result.Message.Headers))
|
||||||
|
{
|
||||||
|
//consumer.Commit(result); // 提交偏移量
|
||||||
|
// 跳过消息
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
messages.Add((result.Message.Value, result.TopicPartitionOffset));
|
||||||
|
//messages.Add(result.Message.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 无消息时短暂等待
|
||||||
|
await Task.Delay(10, cts.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理批次
|
||||||
|
if (messages.Count > 0)
|
||||||
|
{
|
||||||
|
bool success = await messageBatchHandler(messages.Select(m => m.Value));
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
var offsetsByPartition = new Dictionary<TopicPartition, long>();
|
||||||
|
foreach (var msg in messages)
|
||||||
|
{
|
||||||
|
var tp = msg.Offset.TopicPartition;
|
||||||
|
var offset = msg.Offset.Offset;
|
||||||
|
if (!offsetsByPartition.TryGetValue(tp, out var currentMax) || offset > currentMax)
|
||||||
|
{
|
||||||
|
offsetsByPartition[tp] = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var offsetsToCommit = offsetsByPartition
|
||||||
|
.Select(kv => new TopicPartitionOffset(kv.Key, new Offset(kv.Value + 1)))
|
||||||
|
.ToList();
|
||||||
|
consumer.Commit(offsetsToCommit);
|
||||||
|
}
|
||||||
|
messages.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
catch (ConsumeException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"消息消费失败: {ex.Error.Reason}");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// 任务取消,正常退出
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "处理批量消息时发生未知错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, cts.Token);
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消消息订阅
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey"></typeparam>
|
||||||
|
/// <typeparam name="TValue"></typeparam>
|
||||||
|
public void Unsubscribe<TKey, TValue>() where TKey : notnull where TValue : class
|
||||||
|
{
|
||||||
|
var consumerKey = typeof((TKey, TValue));
|
||||||
|
if (_consumerStore.TryRemove(consumerKey, out var entry))
|
||||||
|
{
|
||||||
|
entry.CTS.Cancel();
|
||||||
|
(entry.Consumer as IDisposable)?.Dispose();
|
||||||
|
entry.CTS.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放资源
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var entry in _consumerStore.Values)
|
||||||
|
{
|
||||||
|
entry.CTS.Cancel();
|
||||||
|
(entry.Consumer as IDisposable)?.Dispose();
|
||||||
|
entry.CTS.Dispose();
|
||||||
|
}
|
||||||
|
_consumerStore.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
modules/JiShe.CollectBus.Kafka/Consumer/IConsumerService.cs
Normal file
46
modules/JiShe.CollectBus.Kafka/Consumer/IConsumerService.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
using Confluent.Kafka;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅消息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TValue"></typeparam>
|
||||||
|
/// <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<TKey, TValue>(string[] topics, Func<TKey, TValue, Task<bool>> messageHandler, string? groupId) where TKey : notnull where TValue : class;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订阅消息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey"></typeparam>
|
||||||
|
/// <typeparam name="TValue"></typeparam>
|
||||||
|
/// <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 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 topic, Func<IEnumerable<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[] topics, Func<IEnumerable<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
modules/JiShe.CollectBus.Kafka/HeadersFilter.cs
Normal file
30
modules/JiShe.CollectBus.Kafka/HeadersFilter.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
modules/JiShe.CollectBus.Kafka/HostedService.cs
Normal file
43
modules/JiShe.CollectBus.Kafka/HostedService.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Kafka
|
||||||
|
{
|
||||||
|
public class HostedService : IHostedService, IDisposable
|
||||||
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly IServiceProvider _provider;
|
||||||
|
public HostedService(ILogger<HostedService> logger, IServiceProvider provider)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("程序启动");
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
_provider.UseKafkaSubscribe();
|
||||||
|
});
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("结束");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
modules/JiShe.CollectBus.Kafka/IKafkaSubscribe.cs
Normal file
18
modules/JiShe.CollectBus.Kafka/IKafkaSubscribe.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
21
modules/JiShe.CollectBus.Kafka/ISubscribeAck.cs
Normal file
21
modules/JiShe.CollectBus.Kafka/ISubscribeAck.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
19
modules/JiShe.CollectBus.Kafka/JiShe.CollectBus.Kafka.csproj
Normal file
19
modules/JiShe.CollectBus.Kafka/JiShe.CollectBus.Kafka.csproj
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Confluent.Kafka" Version="2.9.0" />
|
||||||
|
<PackageReference Include="Volo.Abp.AspNetCore" Version="8.3.3" />
|
||||||
|
<PackageReference Include="Volo.Abp.Core" Version="8.3.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\shared\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
88
modules/JiShe.CollectBus.Kafka/JsonSerializer.cs
Normal file
88
modules/JiShe.CollectBus.Kafka/JsonSerializer.cs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Confluent.Kafka;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Kafka
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JSON 序列化器(支持泛型)
|
||||||
|
/// </summary>
|
||||||
|
public class JsonSerializer<T> : ISerializer<T>, IDeserializer<T>
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions _options = 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() } // 注册你的自定义转换器,
|
||||||
|
};
|
||||||
|
|
||||||
|
public byte[] Serialize(T data, SerializationContext context)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.SerializeToUtf8Bytes(data, _options);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Kafka序列化失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Deserialize(ReadOnlySpan<byte> data, bool isNull, SerializationContext context)
|
||||||
|
{
|
||||||
|
if (isNull)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<T>(data, _options);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Kafka反序列化失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class DateTimeJsonConverter : JsonConverter<DateTime>
|
||||||
|
{
|
||||||
|
private readonly string _dateFormatString;
|
||||||
|
public DateTimeJsonConverter()
|
||||||
|
{
|
||||||
|
_dateFormatString = "yyyy-MM-dd HH:mm:ss";
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTimeJsonConverter(string dateFormatString)
|
||||||
|
{
|
||||||
|
_dateFormatString = dateFormatString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return DateTime.Parse(reader.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value.ToString(_dateFormatString));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
modules/JiShe.CollectBus.Kafka/KafkaOptionConfig.cs
Normal file
63
modules/JiShe.CollectBus.Kafka/KafkaOptionConfig.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
266
modules/JiShe.CollectBus.Kafka/KafkaSubcribesExtensions.cs
Normal file
266
modules/JiShe.CollectBus.Kafka/KafkaSubcribesExtensions.cs
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
using Confluent.Kafka;
|
||||||
|
using JiShe.CollectBus.Common.Consts;
|
||||||
|
using JiShe.CollectBus.Common.Extensions;
|
||||||
|
using JiShe.CollectBus.Common.Helpers;
|
||||||
|
using JiShe.CollectBus.Kafka.AdminClient;
|
||||||
|
using JiShe.CollectBus.Kafka.Attributes;
|
||||||
|
using JiShe.CollectBus.Kafka.Consumer;
|
||||||
|
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.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Kafka
|
||||||
|
{
|
||||||
|
public static class KafkaSubcribesExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 添加Kafka订阅
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="app"></param>
|
||||||
|
/// <param name="assembly"></param>
|
||||||
|
public static void UseKafkaSubscribe(this IServiceProvider provider)
|
||||||
|
{
|
||||||
|
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>>();
|
||||||
|
int threadCount = 0;
|
||||||
|
int topicCount = 0;
|
||||||
|
var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
|
||||||
|
if (string.IsNullOrWhiteSpace(assemblyPath))
|
||||||
|
{
|
||||||
|
logger.LogInformation($"kafka订阅未能找到程序路径");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var dllFiles = Directory.GetFiles(assemblyPath, "*.dll");
|
||||||
|
foreach (var file in dllFiles)
|
||||||
|
{
|
||||||
|
// 跳过已加载的程序集
|
||||||
|
var assemblyName = AssemblyName.GetAssemblyName(file);
|
||||||
|
var existingAssembly = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.FirstOrDefault(a => a.GetName().FullName == assemblyName.FullName);
|
||||||
|
var assembly = existingAssembly ?? Assembly.LoadFrom(file);
|
||||||
|
// 实现IKafkaSubscribe接口
|
||||||
|
var subscribeTypes = assembly.GetTypes().Where(type =>
|
||||||
|
typeof(IKafkaSubscribe).IsAssignableFrom(type) &&
|
||||||
|
!type.IsAbstract && !type.IsInterface).ToList(); ;
|
||||||
|
if (subscribeTypes.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var subscribeType in subscribeTypes)
|
||||||
|
{
|
||||||
|
var subscribes = provider.GetServices(subscribeType).ToList();
|
||||||
|
subscribes.ForEach(subscribe =>
|
||||||
|
{
|
||||||
|
if (subscribe != null)
|
||||||
|
{
|
||||||
|
Tuple<int, int> tuple = BuildKafkaSubscribe(subscribe, provider, logger, kafkaOptions.Value);
|
||||||
|
threadCount += tuple.Item1;
|
||||||
|
topicCount += tuple.Item2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.LogInformation($"kafka订阅主题:{topicCount}数,共启动:{threadCount}线程");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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>>();
|
||||||
|
int threadCount = 0;
|
||||||
|
int topicCount = 0;
|
||||||
|
var subscribeTypes = assembly.GetTypes()
|
||||||
|
.Where(t => typeof(IKafkaSubscribe).IsAssignableFrom(t))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (subscribeTypes.Count == 0) return;
|
||||||
|
foreach (var subscribeType in subscribeTypes)
|
||||||
|
{
|
||||||
|
var subscribes = provider.GetServices(subscribeType).ToList();
|
||||||
|
subscribes.ForEach(subscribe =>
|
||||||
|
{
|
||||||
|
|
||||||
|
if (subscribe != null)
|
||||||
|
{
|
||||||
|
Tuple<int, int> tuple = BuildKafkaSubscribe(subscribe, provider, logger, kafkaOptions.Value);
|
||||||
|
threadCount += tuple.Item1;
|
||||||
|
topicCount += tuple.Item2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
logger.LogInformation($"kafka订阅主题:{topicCount}数,共启动:{threadCount}线程");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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()
|
||||||
|
.Select(m => new { Method = m, Attribute = m.GetCustomAttribute<KafkaSubscribeAttribute>() })
|
||||||
|
.Where(x => x.Attribute != null)
|
||||||
|
.ToArray();
|
||||||
|
//var configuration = provider.GetRequiredService<IConfiguration>();
|
||||||
|
int threadCount = 0;
|
||||||
|
|
||||||
|
foreach (var sub in subscribedMethods)
|
||||||
|
{
|
||||||
|
int partitionCount = 3;// kafkaOptionConfig.NumPartitions;
|
||||||
|
#if DEBUG
|
||||||
|
var adminClientService = provider.GetRequiredService<IAdminClientService>();
|
||||||
|
int topicCount = adminClientService.GetTopicPartitionsNum(sub.Attribute!.Topic);
|
||||||
|
partitionCount= partitionCount> topicCount ? topicCount: partitionCount;
|
||||||
|
#endif
|
||||||
|
//int partitionCount = sub.Attribute!.TaskCount==-1?adminClientService.GetTopicPartitionsNum(sub.Attribute!.Topic) : sub.Attribute!.TaskCount;
|
||||||
|
if (partitionCount <= 0)
|
||||||
|
partitionCount = 1;
|
||||||
|
for (int i = 0; i < partitionCount; i++)
|
||||||
|
{
|
||||||
|
//if (sub.Attribute!.Topic == ProtocolConst.SubscriberLoginReceivedEventName)
|
||||||
|
Task.Run(() => StartConsumerAsync(provider, sub.Attribute!, sub.Method, subscribe, logger));
|
||||||
|
threadCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Tuple.Create(threadCount, subscribedMethods.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
logger.LogInformation($"kafka批量消费消息:{message}");
|
||||||
|
#endif
|
||||||
|
// 处理消息
|
||||||
|
return await ProcessMessageAsync(message.ToList(), method, subscribe);
|
||||||
|
}
|
||||||
|
catch (ConsumeException ex)
|
||||||
|
{
|
||||||
|
// 处理消费错误
|
||||||
|
logger.LogError($"kafka批量消费异常:{ex.Message}");
|
||||||
|
}
|
||||||
|
return await Task.FromResult(false);
|
||||||
|
}, attr.GroupId, attr.BatchSize, attr.BatchTimeout);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await consumerService.SubscribeAsync<object>(attr.Topic, async (message) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
logger.LogInformation($"kafka消费消息:{message}");
|
||||||
|
#endif
|
||||||
|
// 处理消息
|
||||||
|
return await ProcessMessageAsync(new List<object>() { message }, method, subscribe);
|
||||||
|
}
|
||||||
|
catch (ConsumeException ex)
|
||||||
|
{
|
||||||
|
// 处理消费错误
|
||||||
|
logger.LogError($"kafka消费异常:{ex.Message}");
|
||||||
|
}
|
||||||
|
return await Task.FromResult(false);
|
||||||
|
}, attr.GroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
var parameters = method.GetParameters();
|
||||||
|
bool isGenericTask = method.ReturnType.IsGenericType
|
||||||
|
&& method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>);
|
||||||
|
bool existParameters = parameters.Length > 0;
|
||||||
|
List<object>? messageObj = null;
|
||||||
|
if (existParameters)
|
||||||
|
{
|
||||||
|
messageObj = new List<object>();
|
||||||
|
var paramType = parameters[0].ParameterType;
|
||||||
|
foreach (var msg in messages)
|
||||||
|
{
|
||||||
|
var data = paramType != typeof(string) ? msg?.ToString()?.Deserialize(paramType) : msg;
|
||||||
|
if (data != null)
|
||||||
|
messageObj.Add(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = method.Invoke(subscribe, messageObj?.ToArray());
|
||||||
|
if (result is Task<ISubscribeAck> genericTask)
|
||||||
|
{
|
||||||
|
await genericTask.ConfigureAwait(false);
|
||||||
|
return genericTask.Result.Ack;
|
||||||
|
}
|
||||||
|
else if (result is Task nonGenericTask)
|
||||||
|
{
|
||||||
|
await nonGenericTask.ConfigureAwait(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (result is ISubscribeAck ackResult)
|
||||||
|
{
|
||||||
|
return ackResult.Ack;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
20
modules/JiShe.CollectBus.Kafka/Producer/IProducerService.cs
Normal file
20
modules/JiShe.CollectBus.Kafka/Producer/IProducerService.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using Confluent.Kafka;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Kafka.Producer
|
||||||
|
{
|
||||||
|
public interface IProducerService
|
||||||
|
{
|
||||||
|
Task ProduceAsync<TKey, TValue>(string topic, TKey key, TValue value) where TKey : notnull where TValue : class;
|
||||||
|
|
||||||
|
Task ProduceAsync<TValue>(string topic, TValue value) where TValue : class;
|
||||||
|
|
||||||
|
Task ProduceAsync<TKey, TValue>(string topic, TKey key, TValue value, int? partition, Action<DeliveryReport<TKey, TValue>>? deliveryHandler = null) where TKey : notnull where TValue : class;
|
||||||
|
|
||||||
|
Task ProduceAsync<TValue>(string topic, TValue value, int? partition = null, Action<DeliveryReport<Null, TValue>>? deliveryHandler = null) where TValue : class;
|
||||||
|
}
|
||||||
|
}
|
||||||
220
modules/JiShe.CollectBus.Kafka/Producer/ProducerService.cs
Normal file
220
modules/JiShe.CollectBus.Kafka/Producer/ProducerService.cs
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Confluent.Kafka;
|
||||||
|
using JiShe.CollectBus.Kafka.Consumer;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Volo.Abp.DependencyInjection;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Kafka.Producer
|
||||||
|
{
|
||||||
|
public class ProducerService: IProducerService, IDisposable
|
||||||
|
{
|
||||||
|
private readonly ILogger<ProducerService> _logger;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
_kafkaOptionConfig = kafkaOptionConfig.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region private 私有方法
|
||||||
|
/// <summary>
|
||||||
|
/// 创建生产者实例
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey"></typeparam>
|
||||||
|
/// <typeparam name="TValue"></typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
private IProducer<TKey, TValue> GetProducer<TKey, TValue>(Type typeKey)
|
||||||
|
{
|
||||||
|
return (IProducer<TKey, TValue>)_producerCache.GetOrAdd(typeKey, _ =>
|
||||||
|
{
|
||||||
|
var config = BuildProducerConfig();
|
||||||
|
return new ProducerBuilder<TKey, TValue>(config)
|
||||||
|
.SetValueSerializer(new JsonSerializer<TValue>()) // Value 使用自定义 JSON 序列化
|
||||||
|
.SetLogHandler((_, msg) => _logger.Log(ConvertLogLevel(msg.Level), msg.Message))
|
||||||
|
.Build();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配置
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
private ProducerConfig BuildProducerConfig()
|
||||||
|
{
|
||||||
|
var config = new ProducerConfig
|
||||||
|
{
|
||||||
|
BootstrapServers = _kafkaOptionConfig.BootstrapServers,
|
||||||
|
AllowAutoCreateTopics = true,
|
||||||
|
QueueBufferingMaxKbytes = 2_097_151, // 修改缓冲区最大为2GB,默认为1GB
|
||||||
|
CompressionType = CompressionType.Lz4, // 配置使用压缩算法LZ4,其他:gzip/snappy/zstd
|
||||||
|
BatchSize = 32_768, // 修改批次大小为32K
|
||||||
|
LingerMs = 20, // 修改等待时间为20ms
|
||||||
|
Acks = Acks.All, // 表明只有所有副本Broker都收到消息才算提交成功, 可以 Acks.Leader
|
||||||
|
MessageSendMaxRetries = 50, // 消息发送失败最大重试50次
|
||||||
|
MessageTimeoutMs = 120000, // 消息发送超时时间为2分钟,设置值MessageTimeoutMs > LingerMs
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_kafkaOptionConfig.EnableAuthorization)
|
||||||
|
{
|
||||||
|
config.SecurityProtocol = _kafkaOptionConfig.SecurityProtocol;
|
||||||
|
config.SaslMechanism = _kafkaOptionConfig.SaslMechanism;
|
||||||
|
config.SaslUsername = _kafkaOptionConfig.SaslUserName;
|
||||||
|
config.SaslPassword = _kafkaOptionConfig.SaslPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LogLevel ConvertLogLevel(SyslogLevel level) => level switch
|
||||||
|
{
|
||||||
|
SyslogLevel.Emergency => LogLevel.Critical,
|
||||||
|
SyslogLevel.Alert => LogLevel.Critical,
|
||||||
|
SyslogLevel.Critical => LogLevel.Critical,
|
||||||
|
SyslogLevel.Error => LogLevel.Error,
|
||||||
|
SyslogLevel.Warning => LogLevel.Warning,
|
||||||
|
SyslogLevel.Notice => LogLevel.Information,
|
||||||
|
SyslogLevel.Info => LogLevel.Information,
|
||||||
|
SyslogLevel.Debug => LogLevel.Debug,
|
||||||
|
_ => LogLevel.None
|
||||||
|
};
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发布消息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey"></typeparam>
|
||||||
|
/// <typeparam name="TValue"></typeparam>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task ProduceAsync<TKey, TValue>(string topic, TKey key, TValue value)where TKey : notnull where TValue : class
|
||||||
|
{
|
||||||
|
var typeKey = typeof(KafkaProducer<TKey, TValue>);
|
||||||
|
var producer = GetProducer<TKey, TValue>(typeKey);
|
||||||
|
var message = new Message<TKey, TValue>
|
||||||
|
{
|
||||||
|
Key = key,
|
||||||
|
Value = value,
|
||||||
|
Headers = new Headers{
|
||||||
|
{ "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await producer.ProduceAsync(topic, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发布消息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TValue"></typeparam>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task ProduceAsync<TValue>(string topic, TValue value) where TValue : class
|
||||||
|
{
|
||||||
|
var typeKey = typeof(KafkaProducer<string, TValue>);
|
||||||
|
var producer = GetProducer<Null, TValue>(typeKey);
|
||||||
|
var message = new Message<Null, TValue>
|
||||||
|
{
|
||||||
|
//Key= _kafkaOptionConfig.ServerTagName,
|
||||||
|
Value = value,
|
||||||
|
Headers = new Headers{
|
||||||
|
{ "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await producer.ProduceAsync(topic, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发布消息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TKey"></typeparam>
|
||||||
|
/// <typeparam name="TValue"></typeparam>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <param name="partition"></param>
|
||||||
|
/// <param name="deliveryHandler"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task ProduceAsync<TKey, TValue>(string topic,TKey key,TValue value,int? partition=null, Action<DeliveryReport<TKey, TValue>>? deliveryHandler = null)where TKey : notnull where TValue : class
|
||||||
|
{
|
||||||
|
var message = new Message<TKey, TValue>
|
||||||
|
{
|
||||||
|
Key = key,
|
||||||
|
Value = value,
|
||||||
|
Headers = new Headers{
|
||||||
|
{ "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var typeKey = typeof(KafkaProducer<TKey, TValue>);
|
||||||
|
var producer = GetProducer<TKey, TValue>(typeKey);
|
||||||
|
if (partition.HasValue)
|
||||||
|
{
|
||||||
|
var topicPartition = new TopicPartition(topic, partition.Value);
|
||||||
|
producer.Produce(topicPartition, message, deliveryHandler);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
producer.Produce(topic, message, deliveryHandler);
|
||||||
|
}
|
||||||
|
await Task.CompletedTask;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发布消息
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TValue"></typeparam>
|
||||||
|
/// <param name="topic"></param>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <param name="partition"></param>
|
||||||
|
/// <param name="deliveryHandler"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task ProduceAsync<TValue>(string topic, TValue value, int? partition=null, Action<DeliveryReport<Null, TValue>>? deliveryHandler = null) where TValue : class
|
||||||
|
{
|
||||||
|
var message = new Message<Null, TValue>
|
||||||
|
{
|
||||||
|
//Key = _kafkaOptionConfig.ServerTagName,
|
||||||
|
Value = value,
|
||||||
|
Headers = new Headers{
|
||||||
|
{ "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var typeKey = typeof(KafkaProducer<Null, TValue>);
|
||||||
|
var producer = GetProducer<Null, TValue>(typeKey);
|
||||||
|
if (partition.HasValue)
|
||||||
|
{
|
||||||
|
var topicPartition = new TopicPartition(topic, partition.Value);
|
||||||
|
producer.Produce(topicPartition, message, deliveryHandler);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
producer.Produce(topic, message, deliveryHandler);
|
||||||
|
}
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var producer in _producerCache.Values.OfType<IDisposable>())
|
||||||
|
{
|
||||||
|
producer.Dispose();
|
||||||
|
}
|
||||||
|
_producerCache.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
modules/JiShe.CollectBus.Kafka/SubscribeResult.cs
Normal file
75
modules/JiShe.CollectBus.Kafka/SubscribeResult.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -17,7 +17,7 @@
|
|||||||
<PackageReference Include="Volo.Abp.MongoDB" Version="8.3.3" />
|
<PackageReference Include="Volo.Abp.MongoDB" Version="8.3.3" />
|
||||||
<PackageReference Include="Volo.Abp.BackgroundJobs.MongoDB" Version="8.3.3" />
|
<PackageReference Include="Volo.Abp.BackgroundJobs.MongoDB" Version="8.3.3" />
|
||||||
<PackageReference Include="Volo.Abp.AuditLogging.MongoDB" Version="8.3.3" />
|
<PackageReference Include="Volo.Abp.AuditLogging.MongoDB" Version="8.3.3" />
|
||||||
<ProjectReference Include="..\JiShe.CollectBus.Domain\JiShe.CollectBus.Domain.csproj" />
|
<ProjectReference Include="..\..\services\JiShe.CollectBus.Domain\JiShe.CollectBus.Domain.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
using JiShe.CollectBus.IotSystems.Devices;
|
||||||
|
using JiShe.CollectBus.IotSystems.MessageReceiveds;
|
||||||
|
using JiShe.CollectBus.IotSystems.MeterReadingRecords;
|
||||||
|
using JiShe.CollectBus.IotSystems.Protocols;
|
||||||
|
using JiShe.CollectBus.ShardingStrategy;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using JiShe.CollectBus.IotSystems.MessageIssueds;
|
||||||
|
using Volo.Abp.Data;
|
||||||
|
using Volo.Abp.MongoDB;
|
||||||
|
using Volo.Abp.MultiTenancy;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.MongoDB;
|
||||||
|
|
||||||
|
[IgnoreMultiTenancy]
|
||||||
|
[ConnectionStringName(CollectBusDbProperties.MongoDbConnectionStringName)]
|
||||||
|
public class CollectBusMongoDbContext : AbpMongoDbContext, ICollectBusMongoDbContext
|
||||||
|
{
|
||||||
|
/* Add mongo collections here. Example:
|
||||||
|
* public IMongoCollection<Question> Questions => Collection<Question>();
|
||||||
|
*/
|
||||||
|
|
||||||
|
public IMongoCollection<MessageReceived> MessageReceiveds => Collection<MessageReceived>();
|
||||||
|
public IMongoCollection<MessageReceivedLogin> MessageReceivedLogins => Collection<MessageReceivedLogin>();
|
||||||
|
public IMongoCollection<MessageReceivedHeartbeat> MessageReceivedHeartbeats => Collection<MessageReceivedHeartbeat>();
|
||||||
|
public IMongoCollection<Device> Devices => Collection<Device>();
|
||||||
|
public IMongoCollection<ProtocolInfo> ProtocolInfos => Collection<ProtocolInfo>();
|
||||||
|
|
||||||
|
public IMongoCollection<MessageIssued> MessageIssueds => Collection<MessageIssued>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
protected override void CreateModel(IMongoModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
//modelBuilder.Entity<MeterReadingRecords>(builder =>
|
||||||
|
//{
|
||||||
|
// builder.CreateCollectionOptions.Collation = new Collation(locale: "en_US", strength: CollationStrength.Secondary);
|
||||||
|
// builder.ConfigureIndexes(indexes =>
|
||||||
|
// {
|
||||||
|
// indexes.CreateOne(
|
||||||
|
// new CreateIndexModel<BsonDocument>(
|
||||||
|
// Builders<BsonDocument>.IndexKeys.Ascending("MyProperty"),
|
||||||
|
// new CreateIndexOptions { Unique = true }
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// //// 创建索引
|
||||||
|
// //builder.ConfigureIndexes(index =>
|
||||||
|
// //{
|
||||||
|
|
||||||
|
|
||||||
|
// // //List<CreateIndexModel<BsonDocument>> createIndexModels = new List<CreateIndexModel<BsonDocument>>();
|
||||||
|
// // //createIndexModels.Add(new CreateIndexModel<BsonDocument>(
|
||||||
|
// // // Builders<BsonDocument>.IndexKeys.Ascending(nameof(MeterReadingRecords)),
|
||||||
|
// // // new CreateIndexOptions
|
||||||
|
// // // {
|
||||||
|
// // // Unique = true
|
||||||
|
// // // }
|
||||||
|
// // // ));
|
||||||
|
|
||||||
|
|
||||||
|
// // //var indexKeys = Builders<BsonDocument>.IndexKeys
|
||||||
|
// // //.Ascending("CreationTime")
|
||||||
|
// // //.Ascending("OrderNumber");
|
||||||
|
|
||||||
|
// // //var indexOptions = new CreateIndexOptions
|
||||||
|
// // //{
|
||||||
|
// // // Background = true,
|
||||||
|
// // // Name = "IX_CreationTime_OrderNumber"
|
||||||
|
// // //};
|
||||||
|
// // //index.CreateOne(
|
||||||
|
// // //new CreateIndexModel<BsonDocument>(indexKeys, indexOptions));
|
||||||
|
|
||||||
|
// // //index.CreateOne(new CreateIndexModel<BsonDocument>(
|
||||||
|
// // // Builders<BsonDocument>.IndexKeys.Ascending(nameof(MeterReadingRecords)),
|
||||||
|
// // // new CreateIndexOptions
|
||||||
|
// // // {
|
||||||
|
// // // Unique = true
|
||||||
|
// // // }
|
||||||
|
// // // ));
|
||||||
|
// //});
|
||||||
|
|
||||||
|
//});
|
||||||
|
|
||||||
|
base.CreateModel(modelBuilder);
|
||||||
|
modelBuilder.ConfigureCollectBus();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,15 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using JiShe.CollectBus.IotSystems.MeterReadingRecords;
|
||||||
|
using JiShe.CollectBus.Repository;
|
||||||
|
using JiShe.CollectBus.Repository.MeterReadingRecord;
|
||||||
|
using JiShe.CollectBus.ShardingStrategy;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using System;
|
||||||
|
using Volo.Abp;
|
||||||
using Volo.Abp.AuditLogging.MongoDB;
|
using Volo.Abp.AuditLogging.MongoDB;
|
||||||
using Volo.Abp.BackgroundJobs.MongoDB;
|
using Volo.Abp.BackgroundJobs.MongoDB;
|
||||||
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
using Volo.Abp.Domain.Repositories.MongoDB;
|
||||||
using Volo.Abp.Modularity;
|
using Volo.Abp.Modularity;
|
||||||
using Volo.Abp.MongoDB;
|
using Volo.Abp.MongoDB;
|
||||||
using Volo.Abp.Uow;
|
using Volo.Abp.Uow;
|
||||||
@ -19,7 +28,15 @@ public class CollectBusMongoDbModule : AbpModule
|
|||||||
{
|
{
|
||||||
context.Services.AddMongoDbContext<CollectBusMongoDbContext>(options =>
|
context.Services.AddMongoDbContext<CollectBusMongoDbContext>(options =>
|
||||||
{
|
{
|
||||||
options.AddDefaultRepositories();
|
options.AddDefaultRepositories(includeAllEntities: true);
|
||||||
|
|
||||||
|
// 注册分表策略
|
||||||
|
context.Services.AddTransient(
|
||||||
|
typeof(IShardingStrategy<>),
|
||||||
|
typeof(DayShardingStrategy<>));
|
||||||
|
|
||||||
|
//// 分表策略仓储 替换默认仓储
|
||||||
|
//options.AddRepository<MeterReadingRecords, MeterReadingRecordRepository>();
|
||||||
});
|
});
|
||||||
|
|
||||||
context.Services.AddAlwaysDisableUnitOfWorkTransaction();
|
context.Services.AddAlwaysDisableUnitOfWorkTransaction();
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
using JiShe.CollectBus.IotSystems.MeterReadingRecords;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Repository.MeterReadingRecord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 抄读仓储接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IMeterReadingRecordRepository : IRepository<MeterReadingRecords, Guid>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 批量插入
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entities"></param>
|
||||||
|
/// <param name="dateTime"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task InsertManyAsync(List<MeterReadingRecords> entities,
|
||||||
|
DateTime? dateTime);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单个插入
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity"></param>
|
||||||
|
/// <param name="dateTime"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<MeterReadingRecords> InsertAsync(MeterReadingRecords entity, DateTime? dateTime);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单条更新
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">过滤条件,示例:Builders<MeterReadingRecords>.Filter.Eq(x => x.Id, filter.Id)</param>
|
||||||
|
/// <param name="update">包含待更新的内容,示例:Builders<MeterReadingRecords>.Update.Set(x => x.Processed, true).Set(x => x.ProcessedTime, Clock.Now)</param>
|
||||||
|
/// <param name="entity">数据实体,用于获取对应的分片库</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<MeterReadingRecords> UpdateOneAsync(FilterDefinition<MeterReadingRecords> filter, UpdateDefinition<MeterReadingRecords> update, MeterReadingRecords entity);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单个获取
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity"></param>
|
||||||
|
/// <param name="dateTime"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<MeterReadingRecords> FirOrDefaultAsync(MeterReadingRecords entity, DateTime? dateTime);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 多集合数据查询
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startTime"></param>
|
||||||
|
/// <param name="endTime"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<List<MeterReadingRecords>> ParallelQueryAsync(DateTime startTime, DateTime endTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,173 @@
|
|||||||
|
using JiShe.CollectBus.IotSystems.MeterReadingRecords;
|
||||||
|
using JiShe.CollectBus.MongoDB;
|
||||||
|
using JiShe.CollectBus.ShardingStrategy;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Volo.Abp.Domain.Entities;
|
||||||
|
using Volo.Abp.Domain.Repositories.MongoDB;
|
||||||
|
using Volo.Abp.MongoDB;
|
||||||
|
using Volo.Abp.MongoDB.DistributedEvents;
|
||||||
|
using Volo.Abp.Timing;
|
||||||
|
using static System.Net.Mime.MediaTypeNames;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Repository.MeterReadingRecord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 抄读记录仓储
|
||||||
|
/// </summary>
|
||||||
|
public class MeterReadingRecordRepository : MongoDbRepository<CollectBusMongoDbContext, MeterReadingRecords, Guid>, IMeterReadingRecordRepository
|
||||||
|
{
|
||||||
|
|
||||||
|
private readonly IShardingStrategy<MeterReadingRecords> _shardingStrategy;
|
||||||
|
private readonly IMongoDbContextProvider<CollectBusMongoDbContext> _dbContextProvider;
|
||||||
|
|
||||||
|
public MeterReadingRecordRepository(
|
||||||
|
IMongoDbContextProvider<CollectBusMongoDbContext> dbContextProvider,
|
||||||
|
IShardingStrategy<MeterReadingRecords> shardingStrategy
|
||||||
|
)
|
||||||
|
: base(dbContextProvider)
|
||||||
|
{
|
||||||
|
_dbContextProvider = dbContextProvider;
|
||||||
|
_shardingStrategy = shardingStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量插入
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entities"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public override async Task<IEnumerable<MeterReadingRecords>> InsertManyAsync(IEnumerable<MeterReadingRecords> entities, bool autoSave = false, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
var collection = await GetShardedCollection(DateTime.Now);
|
||||||
|
await collection.InsertManyAsync(entities);
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量插入
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entities"></param>
|
||||||
|
/// <param name="dateTime"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task InsertManyAsync(List<MeterReadingRecords> entities, DateTime? dateTime)
|
||||||
|
{
|
||||||
|
var collection = await GetShardedCollection(dateTime);
|
||||||
|
await collection.InsertManyAsync(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单条插入
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public override async Task<MeterReadingRecords> InsertAsync(MeterReadingRecords entity, bool autoSave = false, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
var collection = await GetShardedCollection(DateTime.Now);
|
||||||
|
await collection.InsertOneAsync(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单条插入
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity"></param>
|
||||||
|
/// <param name="dateTime"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<MeterReadingRecords> InsertAsync(MeterReadingRecords entity, DateTime? dateTime)
|
||||||
|
{
|
||||||
|
var collection = await GetShardedCollection(dateTime);
|
||||||
|
await collection.InsertOneAsync(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单条更新
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">过滤条件,示例:Builders<MeterReadingRecords>.Filter.Eq(x => x.Id, filter.Id)</param>
|
||||||
|
/// <param name="update">包含待更新的内容,示例:Builders<MeterReadingRecords>.Update.Set(x => x.Processed, true).Set(x => x.ProcessedTime, Clock.Now)</param>
|
||||||
|
/// <param name="entity">数据实体,用于获取对应的分片库</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<MeterReadingRecords> UpdateOneAsync(FilterDefinition<MeterReadingRecords> filter, UpdateDefinition<MeterReadingRecords> update, MeterReadingRecords entity)
|
||||||
|
{
|
||||||
|
var collection = await GetShardedCollection(entity.CreationTime);
|
||||||
|
|
||||||
|
await collection.UpdateOneAsync(filter, update);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单个获取
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity"></param>
|
||||||
|
/// <param name="dateTime"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="NotImplementedException"></exception>
|
||||||
|
public async Task<MeterReadingRecords> FirOrDefaultAsync(MeterReadingRecords entity, DateTime? dateTime)
|
||||||
|
{
|
||||||
|
var collection = await GetShardedCollection(dateTime);
|
||||||
|
var query = await collection.FindAsync(d => d.CreationTime == dateTime.Value && d.AFN == entity.AFN && d.Fn == entity.Fn && d.FocusAddress == entity.FocusAddress);
|
||||||
|
return await query.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 多集合数据查询
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startTime"></param>
|
||||||
|
/// <param name="endTime"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<List<MeterReadingRecords>> ParallelQueryAsync(DateTime startTime, DateTime endTime)
|
||||||
|
{
|
||||||
|
var collectionNames = _shardingStrategy.GetQueryCollectionNames(startTime, endTime);
|
||||||
|
var database = await GetDatabaseAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var tasks = collectionNames.Select(async name =>
|
||||||
|
{
|
||||||
|
var collection = database.GetCollection<MeterReadingRecords>(name);
|
||||||
|
var filter = Builders<MeterReadingRecords>.Filter.And(
|
||||||
|
Builders<MeterReadingRecords>.Filter.Gte(x => x.CreationTime, startTime),
|
||||||
|
Builders<MeterReadingRecords>.Filter.Lte(x => x.CreationTime, endTime)
|
||||||
|
);
|
||||||
|
return await collection.Find(filter).ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
var results = await Task.WhenAll(tasks);
|
||||||
|
return results.SelectMany(r => r).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获得分片集合
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
private async Task<IMongoCollection<MeterReadingRecords>> GetShardedCollection(DateTime? dateTime)
|
||||||
|
{
|
||||||
|
var database = await GetDatabaseAsync();
|
||||||
|
string collectionName = string.Empty;
|
||||||
|
|
||||||
|
if (dateTime != null)
|
||||||
|
{
|
||||||
|
collectionName = _shardingStrategy.GetCollectionName(dateTime.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
collectionName = _shardingStrategy.GetCurrentCollectionName();
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.GetCollection<MeterReadingRecords>(collectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
using JiShe.CollectBus.Common.Extensions;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Volo.Abp.DependencyInjection;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.ShardingStrategy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 按天分表策略
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEntity"></typeparam>
|
||||||
|
public class DayShardingStrategy<TEntity> : IShardingStrategy<TEntity>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定时间对应的集合名
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dateTime"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string GetCollectionName(DateTime dateTime)
|
||||||
|
{
|
||||||
|
var baseName = typeof(TEntity).Name;
|
||||||
|
return $"{baseName}_{dateTime.GetDataTableShardingStrategy()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前时间对应的集合名
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public string GetCurrentCollectionName()
|
||||||
|
{
|
||||||
|
var baseName = typeof(TEntity).Name;
|
||||||
|
return $"{baseName}_{DateTime.Now.GetDataTableShardingStrategy()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用于查询时确定目标集合
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startTime"></param>
|
||||||
|
/// <param name="endTime"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public IEnumerable<string> GetQueryCollectionNames(DateTime? startTime, DateTime? endTime)
|
||||||
|
{
|
||||||
|
var months = new List<string>();
|
||||||
|
var current = startTime ?? DateTime.MinValue;
|
||||||
|
var end = endTime ?? DateTime.MaxValue;
|
||||||
|
var baseName = typeof(TEntity).Name;
|
||||||
|
|
||||||
|
while (current <= end)
|
||||||
|
{
|
||||||
|
months.Add($"{baseName}_{current.GetDataTableShardingStrategy()}");
|
||||||
|
current = current.AddMonths(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return months.Distinct();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.ShardingStrategy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据存储分片策略
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEntity"></typeparam>
|
||||||
|
public interface IShardingStrategy<TEntity>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定时间对应的集合名
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
string GetCollectionName(DateTime dateTime);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前时间对应的集合名
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
string GetCurrentCollectionName();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用于查询时确定目标集合
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startTime"></param>
|
||||||
|
/// <param name="endTime"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
IEnumerable<string> GetQueryCollectionNames(DateTime? startTime = null,
|
||||||
|
DateTime? endTime = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,23 +1,26 @@
|
|||||||
using System.Globalization;
|
using JiShe.CollectBus.Common.Enums;
|
||||||
using DotNetCore.CAP;
|
|
||||||
using JiShe.CollectBus.Common.Enums;
|
|
||||||
using JiShe.CollectBus.Common.Extensions;
|
using JiShe.CollectBus.Common.Extensions;
|
||||||
using JiShe.CollectBus.Common.Models;
|
using JiShe.CollectBus.Common.Models;
|
||||||
using JiShe.CollectBus.MessageReceiveds;
|
|
||||||
using JiShe.CollectBus.Protocol.Contracts.Interfaces;
|
using JiShe.CollectBus.Protocol.Contracts.Interfaces;
|
||||||
using JiShe.CollectBus.Protocols;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using JiShe.CollectBus.Protocol.Contracts.Models;
|
using JiShe.CollectBus.Protocol.Contracts.Models;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
using JiShe.CollectBus.Common.BuildSendDatas;
|
using JiShe.CollectBus.Common.BuildSendDatas;
|
||||||
using JiShe.CollectBus.Protocol.Contracts.AnalysisData;
|
using JiShe.CollectBus.Protocol.Contracts.AnalysisData;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using JiShe.CollectBus.IotSystems.MessageReceiveds;
|
||||||
|
using JiShe.CollectBus.IotSystems.Protocols;
|
||||||
|
using MassTransit;
|
||||||
|
using DotNetCore.CAP;
|
||||||
|
using JiShe.CollectBus.Kafka.Producer;
|
||||||
|
using JiShe.CollectBus.Common.Consts;
|
||||||
|
|
||||||
namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
||||||
{
|
{
|
||||||
public abstract class BaseProtocolPlugin : IProtocolPlugin
|
public abstract class BaseProtocolPlugin : IProtocolPlugin
|
||||||
{
|
{
|
||||||
private readonly ICapPublisher _capBus;
|
private readonly ICapPublisher _producerBus;
|
||||||
|
private readonly IProducerService _producerService;
|
||||||
private readonly ILogger<BaseProtocolPlugin> _logger;
|
private readonly ILogger<BaseProtocolPlugin> _logger;
|
||||||
private readonly IRepository<ProtocolInfo, Guid> _protocolInfoRepository;
|
private readonly IRepository<ProtocolInfo, Guid> _protocolInfoRepository;
|
||||||
|
|
||||||
@ -37,7 +40,8 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
|
|
||||||
_logger = serviceProvider.GetRequiredService<ILogger<BaseProtocolPlugin>>();
|
_logger = serviceProvider.GetRequiredService<ILogger<BaseProtocolPlugin>>();
|
||||||
_protocolInfoRepository = serviceProvider.GetRequiredService<IRepository<ProtocolInfo, Guid>>();
|
_protocolInfoRepository = serviceProvider.GetRequiredService<IRepository<ProtocolInfo, Guid>>();
|
||||||
_capBus = serviceProvider.GetRequiredService<ICapPublisher>();
|
_producerService = serviceProvider.GetRequiredService<IProducerService>();
|
||||||
|
_producerBus = serviceProvider.GetRequiredService<ICapPublisher>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract ProtocolInfo Info { get; }
|
public abstract ProtocolInfo Info { get; }
|
||||||
@ -56,7 +60,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
//await _protocolInfoCache.Get()
|
//await _protocolInfoCache.Get()
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Task AnalyzeAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null);
|
public abstract Task<T> AnalyzeAsync<T>(MessageReceived messageReceived, Action<byte[]>? sendAction = null) where T : TB3761;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 登录帧解析
|
/// 登录帧解析
|
||||||
@ -86,8 +90,10 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
Fn = 1
|
Fn = 1
|
||||||
};
|
};
|
||||||
var bytes = Build3761SendData.BuildSendCommandBytes(reqParam);
|
var bytes = Build3761SendData.BuildSendCommandBytes(reqParam);
|
||||||
|
//await _producerBus.PublishAsync(ProtocolConst.SubscriberLoginIssuedEventName, new IssuedEventMessage { ClientId = messageReceived.ClientId, DeviceNo = messageReceived.DeviceNo, Message = bytes, Type = IssuedEventType.Login, MessageId = messageReceived.MessageId });
|
||||||
|
|
||||||
await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage { ClientId = messageReceived.ClientId, DeviceNo = messageReceived.DeviceNo, Message = bytes, Type = IssuedEventType.Login, MessageId = messageReceived.MessageId });
|
await _producerService.ProduceAsync(ProtocolConst.SubscriberLoginIssuedEventName, new IssuedEventMessage { ClientId = messageReceived.ClientId, DeviceNo = messageReceived.DeviceNo, Message = bytes, Type = IssuedEventType.Login, MessageId = messageReceived.MessageId });
|
||||||
|
//await _producerBus.Publish(new IssuedEventMessage { ClientId = messageReceived.ClientId, DeviceNo = messageReceived.DeviceNo, Message = bytes, Type = IssuedEventType.Login, MessageId = messageReceived.MessageId });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -125,7 +131,11 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
Fn = 1
|
Fn = 1
|
||||||
};
|
};
|
||||||
var bytes = Build3761SendData.BuildSendCommandBytes(reqParam);
|
var bytes = Build3761SendData.BuildSendCommandBytes(reqParam);
|
||||||
await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage { ClientId = messageReceived.ClientId, DeviceNo = messageReceived.DeviceNo, Message = bytes, Type = IssuedEventType.Heartbeat, MessageId = messageReceived.MessageId });
|
//await _producerBus.PublishAsync(ProtocolConst.SubscriberHeartbeatIssuedEventName, new IssuedEventMessage { ClientId = messageReceived.ClientId, DeviceNo = messageReceived.DeviceNo, Message = bytes, Type = IssuedEventType.Heartbeat, MessageId = messageReceived.MessageId });
|
||||||
|
|
||||||
|
await _producerService.ProduceAsync(ProtocolConst.SubscriberHeartbeatIssuedEventName, new IssuedEventMessage { ClientId = messageReceived.ClientId, DeviceNo = messageReceived.DeviceNo, Message = bytes, Type = IssuedEventType.Heartbeat, MessageId = messageReceived.MessageId });
|
||||||
|
|
||||||
|
//await _producerBus.Publish(new IssuedEventMessage { ClientId = messageReceived.ClientId, DeviceNo = messageReceived.DeviceNo, Message = bytes, Type = IssuedEventType.Heartbeat, MessageId = messageReceived.MessageId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,55 +163,55 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual List<AmmeterParameter> AnalyzeAmmeterParameterReadingDataAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
public virtual List<AmmeterParameter> AnalyzeAmmeterParameterReadingDataAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
||||||
{
|
{
|
||||||
var hexDatas = GetHexDatas(messageReceived.MessageHexString);
|
var hexData = GetHexData(messageReceived.MessageHexString);
|
||||||
|
|
||||||
var meterList = new List<AmmeterParameter>();
|
var meterList = new List<AmmeterParameter>();
|
||||||
var count = (hexDatas[1] + hexDatas[0]).HexToDec();
|
var count = (hexData[1] + hexData[0]).HexToDec();
|
||||||
//if (2 + count * 27 != hexDatas.Count - pWLen - tPLen - 2)
|
//if (2 + count * 27 != hexDatas.Count - pWLen - tPLen - 2)
|
||||||
// return;
|
// return;
|
||||||
var index = 2;//数量
|
var index = 2;//数量
|
||||||
for (int i = 1; i <= count; i++)
|
for (int i = 1; i <= count; i++)
|
||||||
{
|
{
|
||||||
var meterNumber = $"{hexDatas[index + 1]}{hexDatas[index]}".HexToDec();
|
var meterNumber = $"{hexData[index + 1]}{hexData[index]}".HexToDec();
|
||||||
index += 2;
|
index += 2;
|
||||||
|
|
||||||
var pn = $"{hexDatas[index + 1]}{hexDatas[index]}".HexToDec();
|
var pn = $"{hexData[index + 1]}{hexData[index]}".HexToDec();
|
||||||
index += 2;
|
index += 2;
|
||||||
|
|
||||||
var baudRateAndPortBin = hexDatas[index].HexToBin().PadLeft(8, '0');
|
var baudRateAndPortBin = hexData[index].HexToBin().PadLeft(8, '0');
|
||||||
var baudRate = baudRateAndPortBin.Substring(0, 3).BinToDec();
|
var baudRate = baudRateAndPortBin.Substring(0, 3).BinToDec();
|
||||||
var port = baudRateAndPortBin.Substring(3, 5).BinToDec();
|
var port = baudRateAndPortBin.Substring(3, 5).BinToDec();
|
||||||
index += 1;
|
index += 1;
|
||||||
|
|
||||||
var protocolType = (CommunicationProtocolType)hexDatas[index].HexToDec();
|
var protocolType = (CommunicationProtocolType)hexData[index].HexToDec();
|
||||||
index += 1;
|
index += 1;
|
||||||
|
|
||||||
var addressHexList = hexDatas.Skip(index).Take(6).ToList();
|
var addressHexList = hexData.Skip(index).Take(6).ToList();
|
||||||
addressHexList.Reverse();
|
addressHexList.Reverse();
|
||||||
var address = string.Join("", addressHexList);
|
var address = string.Join("", addressHexList);
|
||||||
index += 6;
|
index += 6;
|
||||||
|
|
||||||
var pwdHexList = hexDatas.Skip(index).Take(6).ToList();
|
var pwdHexList = hexData.Skip(index).Take(6).ToList();
|
||||||
pwdHexList.Reverse();
|
pwdHexList.Reverse();
|
||||||
var password = string.Join("", pwdHexList.Take(3).ToList());
|
var password = string.Join("", pwdHexList.Take(3).ToList());
|
||||||
index += 6;
|
index += 6;
|
||||||
|
|
||||||
var rateNumberBin = hexDatas[index].HexToBin().PadLeft(8, '0');
|
var rateNumberBin = hexData[index].HexToBin().PadLeft(8, '0');
|
||||||
var rateNumber = rateNumberBin.Substring(4).BinToDec();
|
var rateNumber = rateNumberBin.Substring(4).BinToDec();
|
||||||
index += 1;
|
index += 1;
|
||||||
|
|
||||||
var intBitAndDecBitNumberBin = hexDatas[index].HexToBin().PadLeft(8, '0');
|
var intBitAndDecBitNumberBin = hexData[index].HexToBin().PadLeft(8, '0');
|
||||||
var intBitNumber = intBitAndDecBitNumberBin.Substring(4, 2).BinToDec() + 4;
|
var intBitNumber = intBitAndDecBitNumberBin.Substring(4, 2).BinToDec() + 4;
|
||||||
var decBitNumber = intBitAndDecBitNumberBin.Substring(6, 2).BinToDec() + 1;
|
var decBitNumber = intBitAndDecBitNumberBin.Substring(6, 2).BinToDec() + 1;
|
||||||
index += 1;
|
index += 1;
|
||||||
|
|
||||||
// hexDatas.GetRange()
|
// hexDatas.GetRange()
|
||||||
var collectorAddressHexList = hexDatas.Skip(index).Take(6).ToList();
|
var collectorAddressHexList = hexData.Skip(index).Take(6).ToList();
|
||||||
collectorAddressHexList.Reverse();
|
collectorAddressHexList.Reverse();
|
||||||
var collectorAddress = string.Join("", collectorAddressHexList);
|
var collectorAddress = string.Join("", collectorAddressHexList);
|
||||||
index += 6;
|
index += 6;
|
||||||
|
|
||||||
var userClassNumberBin = hexDatas[index].HexToBin().PadLeft(8, '0');
|
var userClassNumberBin = hexData[index].HexToBin().PadLeft(8, '0');
|
||||||
var userClass = userClassNumberBin.Substring(0, 4).BinToDec();
|
var userClass = userClassNumberBin.Substring(0, 4).BinToDec();
|
||||||
var userSubClass = userClassNumberBin.Substring(4, 4).BinToDec();
|
var userSubClass = userClassNumberBin.Substring(4, 4).BinToDec();
|
||||||
index += 1;
|
index += 1;
|
||||||
@ -234,24 +244,24 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual CurrentPositiveActiveEnergyAnalyze AnalyzeActivePowerIndicationReadingDataAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
public virtual CurrentPositiveActiveEnergyAnalyze AnalyzeActivePowerIndicationReadingDataAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
||||||
{
|
{
|
||||||
var hexDatas = GetHexDatas(messageReceived.MessageHexString);
|
var hexData = GetHexData(messageReceived.MessageHexString);
|
||||||
|
|
||||||
var minute = Convert.ToInt32(hexDatas[0]); // 获取当前分钟数
|
var minute = Convert.ToInt32(hexData[0]); // 获取当前分钟数
|
||||||
var hour = Convert.ToInt32(hexDatas[1]); // 获取当前小时数
|
var hour = Convert.ToInt32(hexData[1]); // 获取当前小时数
|
||||||
var day = Convert.ToInt32(hexDatas[2]); // 获取当前日期的日数
|
var day = Convert.ToInt32(hexData[2]); // 获取当前日期的日数
|
||||||
var month = Convert.ToInt32(hexDatas[3]); // 获取当前月份
|
var month = Convert.ToInt32(hexData[3]); // 获取当前月份
|
||||||
var year = Convert.ToInt32(hexDatas[4]); // 获取当前日期的年份
|
var year = Convert.ToInt32(hexData[4]); // 获取当前日期的年份
|
||||||
var dateTime = new DateTime(year, month, day, hour, minute, 0);
|
var dateTime = new DateTime(year, month, day, hour, minute, 0);
|
||||||
// 转换为本地时间
|
// 转换为本地时间
|
||||||
var localDateTime = dateTime.ToLocalTime();
|
var localDateTime = dateTime.ToLocalTime();
|
||||||
|
|
||||||
var rateNumber = Convert.ToInt32(hexDatas[5]);
|
var rateNumber = Convert.ToInt32(hexData[5]);
|
||||||
var kwhTotal = hexDatas.Skip(5).Take(5).ToList();
|
var kwhTotal = hexData.Skip(5).Take(5).ToList();
|
||||||
var kwhList = new List<PositiveActiveEnergyItem>();
|
var kwhList = new List<PositiveActiveEnergyItem>();
|
||||||
var index = 11;
|
var index = 11;
|
||||||
for (int i = 0; i < rateNumber; i++)
|
for (int i = 0; i < rateNumber; i++)
|
||||||
{
|
{
|
||||||
var kwhHexList = hexDatas.Skip(index).Take(5).ToList();
|
var kwhHexList = hexData.Skip(index).Take(5).ToList();
|
||||||
kwhHexList.Reverse();
|
kwhHexList.Reverse();
|
||||||
var integerStr = $"{kwhHexList.Take(0)}{kwhHexList.Take(1)}{kwhHexList.Take(2)}";
|
var integerStr = $"{kwhHexList.Take(0)}{kwhHexList.Take(1)}{kwhHexList.Take(2)}";
|
||||||
var decimalValStr = $"{kwhHexList[3]}{kwhHexList[4]}";
|
var decimalValStr = $"{kwhHexList[3]}{kwhHexList[4]}";
|
||||||
@ -280,19 +290,19 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual void AnalyzeDailyFrozenReadingDataAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
public virtual void AnalyzeDailyFrozenReadingDataAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
||||||
{
|
{
|
||||||
var hexDatas = GetHexDatas(messageReceived.MessageHexString);
|
var hexData = GetHexData(messageReceived.MessageHexString);
|
||||||
//附录A.20 日月年
|
//附录A.20 日月年
|
||||||
var td_dHex = hexDatas.Take(3).ToList();
|
var td_dHex = hexData.Take(3).ToList();
|
||||||
//附录A.15 分时日月年
|
//附录A.15 分时日月年
|
||||||
var readingTimeHex = hexDatas.Skip(3).Take(5).ToList();
|
var readingTimeHex = hexData.Skip(3).Take(5).ToList();
|
||||||
var rateNumberHex = hexDatas.Skip(8).Take(1).FirstOrDefault().HexToDec();
|
var rateNumberHex = hexData.Skip(8).Take(1).FirstOrDefault().HexToDec();
|
||||||
|
|
||||||
var datas = new List<decimal>();
|
var datas = new List<decimal>();
|
||||||
//附录A.14 kWh 5字节
|
//附录A.14 kWh 5字节
|
||||||
for (int i = 0; i < rateNumberHex; i++)
|
for (int i = 0; i < rateNumberHex; i++)
|
||||||
{
|
{
|
||||||
var skipCount = 9 + i * 5;
|
var skipCount = 9 + i * 5;
|
||||||
var dataHexs = hexDatas.Skip(skipCount).Take(5).ToList();
|
var dataHexs = hexData.Skip(skipCount).Take(5).ToList();
|
||||||
var data = AnalyzeDataAccordingToA14(dataHexs[0], dataHexs[1], dataHexs[2], dataHexs[3], dataHexs[4]);
|
var data = AnalyzeDataAccordingToA14(dataHexs[0], dataHexs[1], dataHexs[2], dataHexs[3], dataHexs[4]);
|
||||||
datas.Add(data);
|
datas.Add(data);
|
||||||
}
|
}
|
||||||
@ -344,83 +354,83 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
/// <returns></returns>//F25ReadingAnalyze
|
/// <returns></returns>//F25ReadingAnalyze
|
||||||
public virtual Analyze3761Data AnalyzeF25ReadingDataAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
public virtual Analyze3761Data AnalyzeF25ReadingDataAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
||||||
{
|
{
|
||||||
var hexDatas = GetHexDatas(messageReceived.MessageHexString);
|
var hexData = GetHexData(messageReceived.MessageHexString);
|
||||||
//A.15 分时日月年
|
//A.15 分时日月年
|
||||||
var readingTimeHex = hexDatas.Take(5).ToList();
|
var readingTimeHex = hexData.Take(5).ToList();
|
||||||
var readingTime = AnalyzeDataAccordingToA15(readingTimeHex[0], readingTimeHex[1], readingTimeHex[2], readingTimeHex[3], readingTimeHex[4]);
|
var readingTime = AnalyzeDataAccordingToA15(readingTimeHex[0], readingTimeHex[1], readingTimeHex[2], readingTimeHex[3], readingTimeHex[4]);
|
||||||
|
|
||||||
//A.9 kW
|
//A.9 kW
|
||||||
var crntTotalActivePowerHexs = hexDatas.Skip((int)F25DataItemEnum.CrntTotalActivePower).Take(3).ToList();
|
var crntTotalActivePowerHexs = hexData.Skip((int)F25DataItemEnum.CrntTotalActivePower).Take(3).ToList();
|
||||||
var crntTotalActivePower = AnalyzeDataAccordingToA09(crntTotalActivePowerHexs[0], crntTotalActivePowerHexs[1], crntTotalActivePowerHexs[2]);
|
var crntTotalActivePower = AnalyzeDataAccordingToA09(crntTotalActivePowerHexs[0], crntTotalActivePowerHexs[1], crntTotalActivePowerHexs[2]);
|
||||||
|
|
||||||
var crntActivePowerOfAHexs = hexDatas.Skip((int)F25DataItemEnum.CrntActivePowerOfA).Take(3).ToList();
|
var crntActivePowerOfAHexs = hexData.Skip((int)F25DataItemEnum.CrntActivePowerOfA).Take(3).ToList();
|
||||||
var crntActivePowerOfA = AnalyzeDataAccordingToA09(crntActivePowerOfAHexs[0], crntActivePowerOfAHexs[1], crntActivePowerOfAHexs[2]);
|
var crntActivePowerOfA = AnalyzeDataAccordingToA09(crntActivePowerOfAHexs[0], crntActivePowerOfAHexs[1], crntActivePowerOfAHexs[2]);
|
||||||
|
|
||||||
var crntActivePowerOfBHexs = hexDatas.Skip((int)F25DataItemEnum.CrntActivePowerOfB).Take(3).ToList();
|
var crntActivePowerOfBHexs = hexData.Skip((int)F25DataItemEnum.CrntActivePowerOfB).Take(3).ToList();
|
||||||
var crntActivePowerOfB = AnalyzeDataAccordingToA09(crntActivePowerOfBHexs[0], crntActivePowerOfBHexs[1], crntActivePowerOfBHexs[2]);
|
var crntActivePowerOfB = AnalyzeDataAccordingToA09(crntActivePowerOfBHexs[0], crntActivePowerOfBHexs[1], crntActivePowerOfBHexs[2]);
|
||||||
|
|
||||||
var crntActivePowerOfCHexs = hexDatas.Skip((int)F25DataItemEnum.CrntActivePowerOfC).Take(3).ToList();
|
var crntActivePowerOfCHexs = hexData.Skip((int)F25DataItemEnum.CrntActivePowerOfC).Take(3).ToList();
|
||||||
var crntActivePowerOfC = AnalyzeDataAccordingToA09(crntActivePowerOfCHexs[0], crntActivePowerOfCHexs[1], crntActivePowerOfCHexs[2]);
|
var crntActivePowerOfC = AnalyzeDataAccordingToA09(crntActivePowerOfCHexs[0], crntActivePowerOfCHexs[1], crntActivePowerOfCHexs[2]);
|
||||||
|
|
||||||
var crntTotalReactivePowerHexs = hexDatas.Skip((int)F25DataItemEnum.CrntTotalReactivePower).Take(3).ToList();
|
var crntTotalReactivePowerHexs = hexData.Skip((int)F25DataItemEnum.CrntTotalReactivePower).Take(3).ToList();
|
||||||
var crntTotalReactivePower = AnalyzeDataAccordingToA09(crntTotalReactivePowerHexs[0], crntTotalReactivePowerHexs[1], crntTotalReactivePowerHexs[2]);
|
var crntTotalReactivePower = AnalyzeDataAccordingToA09(crntTotalReactivePowerHexs[0], crntTotalReactivePowerHexs[1], crntTotalReactivePowerHexs[2]);
|
||||||
|
|
||||||
var crntReactivePowerOfAHexs = hexDatas.Skip((int)F25DataItemEnum.CrntReactivePowerOfA).Take(3).ToList();
|
var crntReactivePowerOfAHexs = hexData.Skip((int)F25DataItemEnum.CrntReactivePowerOfA).Take(3).ToList();
|
||||||
var crntReactivePowerOfA = AnalyzeDataAccordingToA09(crntReactivePowerOfAHexs[0], crntReactivePowerOfAHexs[1], crntReactivePowerOfAHexs[2]);
|
var crntReactivePowerOfA = AnalyzeDataAccordingToA09(crntReactivePowerOfAHexs[0], crntReactivePowerOfAHexs[1], crntReactivePowerOfAHexs[2]);
|
||||||
|
|
||||||
var crntReactivePowerOfBHexs = hexDatas.Skip((int)F25DataItemEnum.CrntReactivePowerOfB).Take(3).ToList();
|
var crntReactivePowerOfBHexs = hexData.Skip((int)F25DataItemEnum.CrntReactivePowerOfB).Take(3).ToList();
|
||||||
var crntReactivePowerOfB = AnalyzeDataAccordingToA09(crntReactivePowerOfBHexs[0], crntReactivePowerOfBHexs[1], crntReactivePowerOfBHexs[2]);
|
var crntReactivePowerOfB = AnalyzeDataAccordingToA09(crntReactivePowerOfBHexs[0], crntReactivePowerOfBHexs[1], crntReactivePowerOfBHexs[2]);
|
||||||
|
|
||||||
var crntReactivePowerOfCHexs = hexDatas.Skip((int)F25DataItemEnum.CrntReactivePowerOfC).Take(2).ToList();
|
var crntReactivePowerOfCHexs = hexData.Skip((int)F25DataItemEnum.CrntReactivePowerOfC).Take(2).ToList();
|
||||||
var crntReactivePowerOfC = AnalyzeDataAccordingToA09(crntReactivePowerOfCHexs[0], crntReactivePowerOfCHexs[1], crntReactivePowerOfCHexs[2]);
|
var crntReactivePowerOfC = AnalyzeDataAccordingToA09(crntReactivePowerOfCHexs[0], crntReactivePowerOfCHexs[1], crntReactivePowerOfCHexs[2]);
|
||||||
|
|
||||||
//A.5 %
|
//A.5 %
|
||||||
var crntTotalPowerFactorHexs = hexDatas.Skip((int)F25DataItemEnum.CrntTotalPowerFactor).Take(2).ToList();
|
var crntTotalPowerFactorHexs = hexData.Skip((int)F25DataItemEnum.CrntTotalPowerFactor).Take(2).ToList();
|
||||||
var crntTotalPowerFactor = AnalyzeDataAccordingToA05(crntTotalPowerFactorHexs[0], crntTotalPowerFactorHexs[1]);
|
var crntTotalPowerFactor = AnalyzeDataAccordingToA05(crntTotalPowerFactorHexs[0], crntTotalPowerFactorHexs[1]);
|
||||||
|
|
||||||
var crntPowerFactorOfAHexs = hexDatas.Skip((int)F25DataItemEnum.CrntPowerFactorOfA).Take(2).ToList();
|
var crntPowerFactorOfAHexs = hexData.Skip((int)F25DataItemEnum.CrntPowerFactorOfA).Take(2).ToList();
|
||||||
var crntPowerFactorOfA = AnalyzeDataAccordingToA05(crntPowerFactorOfAHexs[0], crntPowerFactorOfAHexs[1]);
|
var crntPowerFactorOfA = AnalyzeDataAccordingToA05(crntPowerFactorOfAHexs[0], crntPowerFactorOfAHexs[1]);
|
||||||
|
|
||||||
var crntPowerFactorOfBHexs = hexDatas.Skip((int)F25DataItemEnum.CrntPowerFactorOfB).Take(2).ToList();
|
var crntPowerFactorOfBHexs = hexData.Skip((int)F25DataItemEnum.CrntPowerFactorOfB).Take(2).ToList();
|
||||||
var crntPowerFactorOfB = AnalyzeDataAccordingToA05(crntPowerFactorOfBHexs[0], crntPowerFactorOfBHexs[1]);
|
var crntPowerFactorOfB = AnalyzeDataAccordingToA05(crntPowerFactorOfBHexs[0], crntPowerFactorOfBHexs[1]);
|
||||||
|
|
||||||
var crntPowerFactorOfCHexs = hexDatas.Skip((int)F25DataItemEnum.CrntPowerFactorOfC).Take(2).ToList();
|
var crntPowerFactorOfCHexs = hexData.Skip((int)F25DataItemEnum.CrntPowerFactorOfC).Take(2).ToList();
|
||||||
var crntPowerFactorOfC = AnalyzeDataAccordingToA05(crntPowerFactorOfCHexs[0], crntPowerFactorOfCHexs[1]);
|
var crntPowerFactorOfC = AnalyzeDataAccordingToA05(crntPowerFactorOfCHexs[0], crntPowerFactorOfCHexs[1]);
|
||||||
|
|
||||||
//A.7 V
|
//A.7 V
|
||||||
var crntVoltageOfAHexs = hexDatas.Skip((int)F25DataItemEnum.CrntVoltageOfA).Take(2).ToList();
|
var crntVoltageOfAHexs = hexData.Skip((int)F25DataItemEnum.CrntVoltageOfA).Take(2).ToList();
|
||||||
var crntVoltageOfA = AnalyzeDataAccordingToA07(crntVoltageOfAHexs[0], crntVoltageOfAHexs[1]);
|
var crntVoltageOfA = AnalyzeDataAccordingToA07(crntVoltageOfAHexs[0], crntVoltageOfAHexs[1]);
|
||||||
|
|
||||||
var crntVoltageOfBHexs = hexDatas.Skip((int)F25DataItemEnum.CrntVoltageOfB).Take(2).ToList();
|
var crntVoltageOfBHexs = hexData.Skip((int)F25DataItemEnum.CrntVoltageOfB).Take(2).ToList();
|
||||||
var crntVoltageOfB = AnalyzeDataAccordingToA07(crntVoltageOfBHexs[0], crntVoltageOfBHexs[1]);
|
var crntVoltageOfB = AnalyzeDataAccordingToA07(crntVoltageOfBHexs[0], crntVoltageOfBHexs[1]);
|
||||||
|
|
||||||
var crntVoltageOfCHexs = hexDatas.Skip((int)F25DataItemEnum.CrntVoltageOfC).Take(2).ToList();
|
var crntVoltageOfCHexs = hexData.Skip((int)F25DataItemEnum.CrntVoltageOfC).Take(2).ToList();
|
||||||
var crntVoltageOfC = AnalyzeDataAccordingToA07(crntVoltageOfCHexs[0], crntVoltageOfCHexs[1]);
|
var crntVoltageOfC = AnalyzeDataAccordingToA07(crntVoltageOfCHexs[0], crntVoltageOfCHexs[1]);
|
||||||
|
|
||||||
//A.25 A
|
//A.25 A
|
||||||
var crntCurrentOfAHexs = hexDatas.Skip((int)F25DataItemEnum.CrntCurrentOfA).Take(3).ToList();
|
var crntCurrentOfAHexs = hexData.Skip((int)F25DataItemEnum.CrntCurrentOfA).Take(3).ToList();
|
||||||
var crntCurrentOfA = AnalyzeDataAccordingToA25(crntCurrentOfAHexs[0], crntCurrentOfAHexs[1], crntCurrentOfAHexs[2]);
|
var crntCurrentOfA = AnalyzeDataAccordingToA25(crntCurrentOfAHexs[0], crntCurrentOfAHexs[1], crntCurrentOfAHexs[2]);
|
||||||
|
|
||||||
var crntCurrentOfBHexs = hexDatas.Skip((int)F25DataItemEnum.CrntCurrentOfB).Take(3).ToList();
|
var crntCurrentOfBHexs = hexData.Skip((int)F25DataItemEnum.CrntCurrentOfB).Take(3).ToList();
|
||||||
var crntCurrentOfB = AnalyzeDataAccordingToA25(crntCurrentOfBHexs[0], crntCurrentOfBHexs[1], crntCurrentOfBHexs[2]);
|
var crntCurrentOfB = AnalyzeDataAccordingToA25(crntCurrentOfBHexs[0], crntCurrentOfBHexs[1], crntCurrentOfBHexs[2]);
|
||||||
|
|
||||||
var crntCurrentOfCHexs = hexDatas.Skip((int)F25DataItemEnum.CrntCurrentOfC).Take(3).ToList();
|
var crntCurrentOfCHexs = hexData.Skip((int)F25DataItemEnum.CrntCurrentOfC).Take(3).ToList();
|
||||||
var crntCurrentOfC = AnalyzeDataAccordingToA25(crntCurrentOfCHexs[0], crntCurrentOfCHexs[1], crntCurrentOfCHexs[2]);
|
var crntCurrentOfC = AnalyzeDataAccordingToA25(crntCurrentOfCHexs[0], crntCurrentOfCHexs[1], crntCurrentOfCHexs[2]);
|
||||||
|
|
||||||
var crntZeroSequenceCurrentHexs = hexDatas.Skip((int)F25DataItemEnum.CrntZeroSequenceCurrent).Take(3).ToList();
|
var crntZeroSequenceCurrentHexs = hexData.Skip((int)F25DataItemEnum.CrntZeroSequenceCurrent).Take(3).ToList();
|
||||||
var crntZeroSequenceCurrent = AnalyzeDataAccordingToA25(crntZeroSequenceCurrentHexs[0], crntZeroSequenceCurrentHexs[1], crntZeroSequenceCurrentHexs[2]);
|
var crntZeroSequenceCurrent = AnalyzeDataAccordingToA25(crntZeroSequenceCurrentHexs[0], crntZeroSequenceCurrentHexs[1], crntZeroSequenceCurrentHexs[2]);
|
||||||
|
|
||||||
//A.9 kVA
|
//A.9 kVA
|
||||||
var crntTotalApparentPowerHexs = hexDatas.Skip((int)F25DataItemEnum.CrntTotalApparentPower).Take(3).ToList();
|
var crntTotalApparentPowerHexs = hexData.Skip((int)F25DataItemEnum.CrntTotalApparentPower).Take(3).ToList();
|
||||||
var crntTotalApparentPower = AnalyzeDataAccordingToA09(crntTotalApparentPowerHexs[0], crntTotalApparentPowerHexs[1], crntTotalApparentPowerHexs[2]);
|
var crntTotalApparentPower = AnalyzeDataAccordingToA09(crntTotalApparentPowerHexs[0], crntTotalApparentPowerHexs[1], crntTotalApparentPowerHexs[2]);
|
||||||
|
|
||||||
var crntApparentPowerOfAHexs = hexDatas.Skip((int)F25DataItemEnum.CrntApparentPowerOfA).Take(3).ToList();
|
var crntApparentPowerOfAHexs = hexData.Skip((int)F25DataItemEnum.CrntApparentPowerOfA).Take(3).ToList();
|
||||||
var crntApparentPowerOfA = AnalyzeDataAccordingToA09(crntApparentPowerOfAHexs[0], crntApparentPowerOfAHexs[1], crntApparentPowerOfAHexs[2]);
|
var crntApparentPowerOfA = AnalyzeDataAccordingToA09(crntApparentPowerOfAHexs[0], crntApparentPowerOfAHexs[1], crntApparentPowerOfAHexs[2]);
|
||||||
|
|
||||||
var crntApparentPowerOfBHexs = hexDatas.Skip((int)F25DataItemEnum.CrntApparentPowerOfB).Take(3).ToList();
|
var crntApparentPowerOfBHexs = hexData.Skip((int)F25DataItemEnum.CrntApparentPowerOfB).Take(3).ToList();
|
||||||
var crntApparentPowerOfB = AnalyzeDataAccordingToA09(crntApparentPowerOfBHexs[0], crntApparentPowerOfBHexs[1], crntApparentPowerOfBHexs[2]);
|
var crntApparentPowerOfB = AnalyzeDataAccordingToA09(crntApparentPowerOfBHexs[0], crntApparentPowerOfBHexs[1], crntApparentPowerOfBHexs[2]);
|
||||||
|
|
||||||
var crntApparentPowerOfCHexs = hexDatas.Skip((int)F25DataItemEnum.CrntApparentPowerOfC).Take(3).ToList();
|
var crntApparentPowerOfCHexs = hexData.Skip((int)F25DataItemEnum.CrntApparentPowerOfC).Take(3).ToList();
|
||||||
var crntApparentPowerOfC = AnalyzeDataAccordingToA09(crntApparentPowerOfCHexs[0], crntApparentPowerOfCHexs[1], crntApparentPowerOfCHexs[2]);
|
var crntApparentPowerOfC = AnalyzeDataAccordingToA09(crntApparentPowerOfCHexs[0], crntApparentPowerOfCHexs[1], crntApparentPowerOfCHexs[2]);
|
||||||
|
|
||||||
return new Analyze3761Data()
|
return new Analyze3761Data()
|
||||||
@ -497,17 +507,17 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
/// <returns></returns>//TerminalVersionInfoAnalyze
|
/// <returns></returns>//TerminalVersionInfoAnalyze
|
||||||
public virtual Analyze3761Data AnalyzeTerminalVersionInfoReadingDataAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
public virtual Analyze3761Data AnalyzeTerminalVersionInfoReadingDataAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
||||||
{
|
{
|
||||||
var hexDatas = GetHexDatas(messageReceived.MessageHexString);
|
var hexData = GetHexData(messageReceived.MessageHexString);
|
||||||
|
|
||||||
var makerNo = string.Join("",hexDatas.Take(4).Select(s => (char)s.HexToDec()));//厂商代码
|
var makerNo = string.Join("",hexData.Take(4).Select(s => (char)s.HexToDec()));//厂商代码
|
||||||
var deviceNo = string.Join("", hexDatas.Skip((int)TerminalVersionInfoEnum.DeviceNo).Take(8).Select(s => (char)s.HexToDec()));//设备编号
|
var deviceNo = string.Join("", hexData.Skip((int)TerminalVersionInfoEnum.DeviceNo).Take(8).Select(s => (char)s.HexToDec()));//设备编号
|
||||||
var softwareVersionNo = string.Join("", hexDatas.Skip((int)TerminalVersionInfoEnum.SoftwareVersionNo).Take(4).Select(s => (char)s.HexToDec()));//软件版本号
|
var softwareVersionNo = string.Join("", hexData.Skip((int)TerminalVersionInfoEnum.SoftwareVersionNo).Take(4).Select(s => (char)s.HexToDec()));//软件版本号
|
||||||
var softwareReleaseDateList = hexDatas.Skip((int)TerminalVersionInfoEnum.SoftwareReleaseDate).Take(3).ToList();
|
var softwareReleaseDateList = hexData.Skip((int)TerminalVersionInfoEnum.SoftwareReleaseDate).Take(3).ToList();
|
||||||
var softwareReleaseDate = $"20{AnalyzeDataAccordingToA20(softwareReleaseDateList[0], softwareReleaseDateList[1], softwareReleaseDateList[2])}";//软件发布日期
|
var softwareReleaseDate = $"20{AnalyzeDataAccordingToA20(softwareReleaseDateList[0], softwareReleaseDateList[1], softwareReleaseDateList[2])}";//软件发布日期
|
||||||
var capacityInformationCode = string.Join("", hexDatas.Skip((int)TerminalVersionInfoEnum.CapacityInformationCode).Take(11).Select(s => (char)s.HexToDec()));//容量信息码
|
var capacityInformationCode = string.Join("", hexData.Skip((int)TerminalVersionInfoEnum.CapacityInformationCode).Take(11).Select(s => (char)s.HexToDec()));//容量信息码
|
||||||
var protocolVersionNo = string.Join("", hexDatas.Skip((int)TerminalVersionInfoEnum.ProtocolVersionNo).Take(4).Select(s => (char)s.HexToDec()));//通信协议编号
|
var protocolVersionNo = string.Join("", hexData.Skip((int)TerminalVersionInfoEnum.ProtocolVersionNo).Take(4).Select(s => (char)s.HexToDec()));//通信协议编号
|
||||||
var hardwareVersionNo = string.Join("", hexDatas.Skip((int)TerminalVersionInfoEnum.HardwareVersionNo).Take(4).Select(s => (char)s.HexToDec()));//硬件版本号
|
var hardwareVersionNo = string.Join("", hexData.Skip((int)TerminalVersionInfoEnum.HardwareVersionNo).Take(4).Select(s => (char)s.HexToDec()));//硬件版本号
|
||||||
var hardwareReleaseDateList = hexDatas.Skip((int)TerminalVersionInfoEnum.HardwareReleaseDate).Take(3).ToList();
|
var hardwareReleaseDateList = hexData.Skip((int)TerminalVersionInfoEnum.HardwareReleaseDate).Take(3).ToList();
|
||||||
var hardwareReleaseDate = $"20{AnalyzeDataAccordingToA20(hardwareReleaseDateList[0], hardwareReleaseDateList[1], hardwareReleaseDateList[2])}";//软件发布日期
|
var hardwareReleaseDate = $"20{AnalyzeDataAccordingToA20(hardwareReleaseDateList[0], hardwareReleaseDateList[1], hardwareReleaseDateList[2])}";//软件发布日期
|
||||||
|
|
||||||
return new Analyze3761Data()
|
return new Analyze3761Data()
|
||||||
@ -548,24 +558,24 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual Analyze3761Data AnalyzeATypeOfDataItems49ReadingDataAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
public virtual Analyze3761Data AnalyzeATypeOfDataItems49ReadingDataAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
||||||
{
|
{
|
||||||
var hexDatas = GetHexDatas(messageReceived.MessageHexString);
|
var hexData = GetHexData(messageReceived.MessageHexString);
|
||||||
|
|
||||||
var uabUaList = hexDatas.Take(2).ToList();
|
var uabUaList = hexData.Take(2).ToList();
|
||||||
var uabUa = AnalyzeDataAccordingToA05(uabUaList[0], uabUaList[1]); //单位 度
|
var uabUa = AnalyzeDataAccordingToA05(uabUaList[0], uabUaList[1]); //单位 度
|
||||||
|
|
||||||
var ubList = hexDatas.Skip((int)ATypeOfDataItems49.Ub).Take(2).ToList();
|
var ubList = hexData.Skip((int)ATypeOfDataItems49.Ub).Take(2).ToList();
|
||||||
var ub = AnalyzeDataAccordingToA05(ubList[0], ubList[1]);
|
var ub = AnalyzeDataAccordingToA05(ubList[0], ubList[1]);
|
||||||
|
|
||||||
var ucbUcList = hexDatas.Skip((int)ATypeOfDataItems49.UcbUc).Take(2).ToList();
|
var ucbUcList = hexData.Skip((int)ATypeOfDataItems49.UcbUc).Take(2).ToList();
|
||||||
var ucbUc = AnalyzeDataAccordingToA05(ucbUcList[0], ucbUcList[1]);
|
var ucbUc = AnalyzeDataAccordingToA05(ucbUcList[0], ucbUcList[1]);
|
||||||
|
|
||||||
var iaList = hexDatas.Skip((int)ATypeOfDataItems49.Ia).Take(2).ToList();
|
var iaList = hexData.Skip((int)ATypeOfDataItems49.Ia).Take(2).ToList();
|
||||||
var ia = AnalyzeDataAccordingToA05(iaList[0], iaList[1]);
|
var ia = AnalyzeDataAccordingToA05(iaList[0], iaList[1]);
|
||||||
|
|
||||||
var ibList = hexDatas.Skip((int)ATypeOfDataItems49.Ib).Take(2).ToList();
|
var ibList = hexData.Skip((int)ATypeOfDataItems49.Ib).Take(2).ToList();
|
||||||
var ib = AnalyzeDataAccordingToA05(ibList[0], ibList[1]);
|
var ib = AnalyzeDataAccordingToA05(ibList[0], ibList[1]);
|
||||||
|
|
||||||
var icList = hexDatas.Skip((int)ATypeOfDataItems49.Ic).Take(2).ToList();
|
var icList = hexData.Skip((int)ATypeOfDataItems49.Ic).Take(2).ToList();
|
||||||
var ic = AnalyzeDataAccordingToA05(icList[0], icList[1]);
|
var ic = AnalyzeDataAccordingToA05(icList[0], icList[1]);
|
||||||
|
|
||||||
return new Analyze3761Data()
|
return new Analyze3761Data()
|
||||||
@ -573,15 +583,15 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
AFN = 12,
|
AFN = 12,
|
||||||
FN = 49,
|
FN = 49,
|
||||||
Text = "相位角",
|
Text = "相位角",
|
||||||
DataUpChilds = new List<Analyze3761DataUpChild>()
|
DataUpChilds =
|
||||||
{
|
[
|
||||||
new Analyze3761DataUpChild(1,"UabUa相位角",uabUa.ToString(),1),
|
new Analyze3761DataUpChild(1, "UabUa相位角", uabUa.ToString(), 1),
|
||||||
new Analyze3761DataUpChild(2,"Ub相位角",ub.ToString(),2),
|
new Analyze3761DataUpChild(2, "Ub相位角", ub.ToString(), 2),
|
||||||
new Analyze3761DataUpChild(3,"UcbUc相位角",ucbUc.ToString(),3),
|
new Analyze3761DataUpChild(3, "UcbUc相位角", ucbUc.ToString(), 3),
|
||||||
new Analyze3761DataUpChild(4,"Ia相位角",ia.ToString(),4),
|
new Analyze3761DataUpChild(4, "Ia相位角", ia.ToString(), 4),
|
||||||
new Analyze3761DataUpChild(5,"Ib相位角",ib.ToString(),5),
|
new Analyze3761DataUpChild(5, "Ib相位角", ib.ToString(), 5),
|
||||||
new Analyze3761DataUpChild(6,"Ic相位角",ic.ToString(),6),
|
new Analyze3761DataUpChild(6, "Ic相位角", ic.ToString(), 6)
|
||||||
}
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,7 +603,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
|
|
||||||
public virtual void AnalyzeTerminalTimeReadingDataAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
public virtual void AnalyzeTerminalTimeReadingDataAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
||||||
{
|
{
|
||||||
var hexDatas = GetHexDatas(messageReceived.MessageHexString);
|
var hexDatas = GetHexData(messageReceived.MessageHexString);
|
||||||
var time = Appendix.Appendix_A1(hexDatas.Take(6).ToList());
|
var time = Appendix.Appendix_A1(hexDatas.Take(6).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -603,7 +613,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
/// <param name="messageReceived"></param>
|
/// <param name="messageReceived"></param>
|
||||||
/// <param name="sendAction"></param>
|
/// <param name="sendAction"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual TB3761FN AnalyzeReadingDataAsync(MessageReceived messageReceived,
|
public virtual TB3761 AnalyzeReadingDataAsync(MessageReceived messageReceived,
|
||||||
Action<byte[]>? sendAction = null)
|
Action<byte[]>? sendAction = null)
|
||||||
{
|
{
|
||||||
var hexStringList = messageReceived.MessageHexString.StringToPairs();
|
var hexStringList = messageReceived.MessageHexString.StringToPairs();
|
||||||
@ -616,14 +626,14 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
var tb3761Fn = tb3761.FnList.FirstOrDefault(it => it.Fn == fn);
|
var tb3761Fn = tb3761.FnList.FirstOrDefault(it => it.Fn == fn);
|
||||||
if (tb3761Fn == null) return null;
|
if (tb3761Fn == null) return null;
|
||||||
|
|
||||||
var hexDatas = (List<string>)hexStringList.GetAnalyzeValue(CommandChunkEnum.Data);
|
var analyzeValue = (List<string>)hexStringList.GetAnalyzeValue(CommandChunkEnum.Data);
|
||||||
|
|
||||||
var m = 0;
|
var m = 0;
|
||||||
var rateNumberUpSort = -1;
|
var rateNumberUpSort = -1;
|
||||||
var rateNumberUp = tb3761Fn.UpList.FirstOrDefault(it => it.Name.Contains("费率数M"));
|
var rateNumberUp = tb3761Fn.UpList.FirstOrDefault(it => it.Name.Contains("费率数M"));
|
||||||
if (rateNumberUp != null)
|
if (rateNumberUp != null)
|
||||||
{
|
{
|
||||||
var rateNumber = hexDatas.Skip(rateNumberUp.DataIndex).Take(rateNumberUp.DataCount).FirstOrDefault();
|
var rateNumber = analyzeValue.Skip(rateNumberUp.DataIndex).Take(rateNumberUp.DataCount).FirstOrDefault();
|
||||||
m = Convert.ToInt32(rateNumber);
|
m = Convert.ToInt32(rateNumber);
|
||||||
rateNumberUpSort = rateNumberUp.Sort;
|
rateNumberUpSort = rateNumberUp.Sort;
|
||||||
}
|
}
|
||||||
@ -640,7 +650,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
dataIndex = sum1 + sum2;
|
dataIndex = sum1 + sum2;
|
||||||
}
|
}
|
||||||
|
|
||||||
var value = AnalyzeDataAccordingDataType(hexDatas, dataIndex, up.DataCount, up.DataType);
|
var value = AnalyzeDataAccordingDataType(analyzeValue, dataIndex, up.DataCount, up.DataType);
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
up.Value = value.ToString();
|
up.Value = value.ToString();
|
||||||
@ -652,7 +662,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
{
|
{
|
||||||
for (var j = 0; j < repeatCount; j++)
|
for (var j = 0; j < repeatCount; j++)
|
||||||
{
|
{
|
||||||
var val = AnalyzeDataAccordingDataType(hexDatas, dataIndex, upChild.DataCount, upChild.DataType);
|
var val = AnalyzeDataAccordingDataType(analyzeValue, dataIndex, upChild.DataCount, upChild.DataType);
|
||||||
if (val != null)
|
if (val != null)
|
||||||
{
|
{
|
||||||
upChild.Name = string.Format(upChild.Name, j + 1);
|
upChild.Name = string.Format(upChild.Name, j + 1);
|
||||||
@ -665,7 +675,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tb3761Fn;
|
return tb3761;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -674,7 +684,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
/// <param name="messageReceived"></param>
|
/// <param name="messageReceived"></param>
|
||||||
/// <param name="sendAction"></param>
|
/// <param name="sendAction"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual TB3761FN AnalyzeReadingTdcDataAsync(MessageReceived messageReceived,
|
public virtual TB3761 AnalyzeReadingTdcDataAsync(MessageReceived messageReceived,
|
||||||
Action<byte[]>? sendAction = null)
|
Action<byte[]>? sendAction = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
@ -688,11 +698,11 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
var tb3761Fn = tb3761.FnList.FirstOrDefault(it => it.Fn == fn);
|
var tb3761Fn = tb3761.FnList.FirstOrDefault(it => it.Fn == fn);
|
||||||
if (tb3761Fn == null) return null;
|
if (tb3761Fn == null) return null;
|
||||||
|
|
||||||
var hexDatas = (List<string>)hexStringList.GetAnalyzeValue(CommandChunkEnum.Data);
|
var analyzeValue = (List<string>)hexStringList.GetAnalyzeValue(CommandChunkEnum.Data);
|
||||||
|
|
||||||
foreach (var up in tb3761Fn.UpList)
|
foreach (var up in tb3761Fn.UpList)
|
||||||
{
|
{
|
||||||
var value = AnalyzeDataAccordingDataType(hexDatas, up.DataIndex, up.DataCount, up.DataType);
|
var value = AnalyzeDataAccordingDataType(analyzeValue, up.DataIndex, up.DataCount, up.DataType);
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
up.Value = value.ToString();
|
up.Value = value.ToString();
|
||||||
@ -705,7 +715,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
{
|
{
|
||||||
for (var j = 0; j < repeatCount; j++)
|
for (var j = 0; j < repeatCount; j++)
|
||||||
{
|
{
|
||||||
var val = AnalyzeDataAccordingDataType(hexDatas, dataIndex, upChild.DataCount, upChild.DataType);
|
var val = AnalyzeDataAccordingDataType(analyzeValue, dataIndex, upChild.DataCount, upChild.DataType);
|
||||||
if (val != null)
|
if (val != null)
|
||||||
{
|
{
|
||||||
upChild.Value = val.ToString();
|
upChild.Value = val.ToString();
|
||||||
@ -718,7 +728,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tb3761Fn;
|
return tb3761;
|
||||||
//var freezeDensity = (FreezeDensity)Convert.ToInt32(hexDatas.Skip(5).Take(1));
|
//var freezeDensity = (FreezeDensity)Convert.ToInt32(hexDatas.Skip(5).Take(1));
|
||||||
//var addMinute = 0;
|
//var addMinute = 0;
|
||||||
//switch (freezeDensity)
|
//switch (freezeDensity)
|
||||||
@ -741,9 +751,9 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private object? AnalyzeDataAccordingDataType(List<string> hexDatas, int dataIndex,int dataCount,string dataType)
|
private object? AnalyzeDataAccordingDataType(List<string> analyzeValue, int dataIndex,int dataCount,string dataType)
|
||||||
{
|
{
|
||||||
var valueList = hexDatas.Skip(dataIndex).Take(dataCount).ToList();
|
var valueList = analyzeValue.Skip(dataIndex).Take(dataCount).ToList();
|
||||||
object? value = null;
|
object? value = null;
|
||||||
switch (dataType)
|
switch (dataType)
|
||||||
{
|
{
|
||||||
@ -771,11 +781,11 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
case "A15":
|
case "A15":
|
||||||
if (valueList.Count == 5)
|
if (valueList.Count == 5)
|
||||||
{
|
{
|
||||||
//var minutes = Convert.ToInt32(hexDatas[0]); // 获取当前分钟数
|
//var minutes = Convert.ToInt32(analyzeValue[0]); // 获取当前分钟数
|
||||||
//var hours = Convert.ToInt32(hexDatas[1]); // 获取当前小时数
|
//var hours = Convert.ToInt32(analyzeValue[1]); // 获取当前小时数
|
||||||
//var day = Convert.ToInt32(hexDatas[2]); // 获取当前日期的日数
|
//var day = Convert.ToInt32(analyzeValue[2]); // 获取当前日期的日数
|
||||||
//var month = Convert.ToInt32(hexDatas[3]); // 获取当前月份
|
//var month = Convert.ToInt32(analyzeValue[3]); // 获取当前月份
|
||||||
//var year = Convert.ToInt32(hexDatas[4]); // 获取当前日期的年份
|
//var year = Convert.ToInt32(analyzeValue[4]); // 获取当前日期的年份
|
||||||
value = AnalyzeDataAccordingToA15(valueList[0], valueList[1], valueList[2], valueList[3], valueList[4]);
|
value = AnalyzeDataAccordingToA15(valueList[0], valueList[1], valueList[2], valueList[3], valueList[4]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -801,7 +811,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task AnalyzeTransparentForwardingAnswerAsync(MessageReceived messageReceivedEvent, Action<byte[]>? sendAction = null)
|
public virtual async Task AnalyzeTransparentForwardingAnswerAsync(MessageReceived messageReceivedEvent, Action<byte[]>? sendAction = null)
|
||||||
{
|
{
|
||||||
var hexDatas = GetHexDatas(messageReceivedEvent.MessageHexString);
|
var hexDatas = GetHexData(messageReceivedEvent.MessageHexString);
|
||||||
|
|
||||||
var port = hexDatas[0].HexToDec();
|
var port = hexDatas[0].HexToDec();
|
||||||
|
|
||||||
@ -828,7 +838,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task AnalyzeTransparentForwardingAnswerResultAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
public virtual async Task AnalyzeTransparentForwardingAnswerResultAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
||||||
{
|
{
|
||||||
var hexDatas = GetHexDatas(messageReceived.MessageHexString);
|
var hexDatas = GetHexData(messageReceived.MessageHexString);
|
||||||
|
|
||||||
var port = hexDatas[0].HexToDec();
|
var port = hexDatas[0].HexToDec();
|
||||||
|
|
||||||
@ -844,25 +854,25 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="messageHexString"></param>
|
/// <param name="messageHexString"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static List<string> GetHexDatas(string messageHexString)
|
public static List<string> GetHexData(string messageHexString)
|
||||||
{
|
{
|
||||||
var hexStringList = messageHexString.StringToPairs();
|
var hexStringList = messageHexString.StringToPairs();
|
||||||
var hexDatas = (List<string>)hexStringList.GetAnalyzeValue(CommandChunkEnum.Data);
|
var analyzeValue = (List<string>)hexStringList.GetAnalyzeValue(CommandChunkEnum.Data);
|
||||||
return hexDatas;
|
return analyzeValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析时间标签
|
/// 解析时间标签
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hexDatas"></param>
|
/// <param name="hexData"></param>
|
||||||
public void AnalysisTp(List<string> hexDatas)
|
public void AnalysisTp(List<string> hexData)
|
||||||
{
|
{
|
||||||
var pFC = hexDatas[0].HexToDec();//启动帧帧序号计数器
|
var pFC = hexData[0].HexToDec();//启动帧帧序号计数器
|
||||||
var seconds = Convert.ToInt32(hexDatas[1]); // 获取当前秒数
|
var seconds = Convert.ToInt32(hexData[1]); // 获取当前秒数
|
||||||
var minutes = Convert.ToInt32(hexDatas[2]); // 获取当前分钟数
|
var minutes = Convert.ToInt32(hexData[2]); // 获取当前分钟数
|
||||||
var hours = Convert.ToInt32(hexDatas[3]); // 获取当前小时数
|
var hours = Convert.ToInt32(hexData[3]); // 获取当前小时数
|
||||||
var day = Convert.ToInt32(hexDatas[4]); // 获取当前日期的日数
|
var day = Convert.ToInt32(hexData[4]); // 获取当前日期的日数
|
||||||
var delayTime = hexDatas[5].HexToDec();//延迟时间 min
|
var delayTime = hexData[5].HexToDec();//延迟时间 min
|
||||||
}
|
}
|
||||||
|
|
||||||
#region 报文指定的数据格式
|
#region 报文指定的数据格式
|
||||||
@ -1,9 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using JiShe.CollectBus.Common.Models;
|
using JiShe.CollectBus.Common.Models;
|
||||||
using JiShe.CollectBus.MessageReceiveds;
|
using JiShe.CollectBus.IotSystems.MessageReceiveds;
|
||||||
|
using JiShe.CollectBus.IotSystems.Protocols;
|
||||||
using JiShe.CollectBus.Protocol.Contracts.Models;
|
using JiShe.CollectBus.Protocol.Contracts.Models;
|
||||||
using JiShe.CollectBus.Protocols;
|
|
||||||
using TouchSocket.Sockets;
|
using TouchSocket.Sockets;
|
||||||
|
|
||||||
namespace JiShe.CollectBus.Protocol.Contracts.Interfaces
|
namespace JiShe.CollectBus.Protocol.Contracts.Interfaces
|
||||||
@ -14,7 +14,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Interfaces
|
|||||||
|
|
||||||
Task AddAsync();
|
Task AddAsync();
|
||||||
|
|
||||||
Task AnalyzeAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null);
|
Task<T> AnalyzeAsync<T>(MessageReceived messageReceived, Action<byte[]>? sendAction = null) where T : TB3761;
|
||||||
|
|
||||||
Task LoginAsync(MessageReceivedLogin messageReceived);
|
Task LoginAsync(MessageReceivedLogin messageReceived);
|
||||||
|
|
||||||
@ -6,6 +6,12 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="Extensions\**" />
|
||||||
|
<EmbeddedResource Remove="Extensions\**" />
|
||||||
|
<None Remove="Extensions\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DotNetCore.CAP" Version="8.3.1" />
|
<PackageReference Include="DotNetCore.CAP" Version="8.3.1" />
|
||||||
<PackageReference Include="MassTransit.Abstractions" Version="8.3.0" />
|
<PackageReference Include="MassTransit.Abstractions" Version="8.3.0" />
|
||||||
@ -15,8 +21,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj" />
|
<ProjectReference Include="..\..\modules\JiShe.CollectBus.Kafka\JiShe.CollectBus.Kafka.csproj" />
|
||||||
<ProjectReference Include="..\JiShe.CollectBus.Domain\JiShe.CollectBus.Domain.csproj" />
|
<ProjectReference Include="..\..\services\JiShe.CollectBus.Domain\JiShe.CollectBus.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\..\shared\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@ -16,13 +16,13 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj" />
|
<ProjectReference Include="..\..\services\JiShe.CollectBus.Domain\JiShe.CollectBus.Domain.csproj" />
|
||||||
<ProjectReference Include="..\JiShe.CollectBus.Domain\JiShe.CollectBus.Domain.csproj" />
|
<ProjectReference Include="..\..\shared\JiShe.CollectBus.Common\JiShe.CollectBus.Common.csproj" />
|
||||||
<ProjectReference Include="..\JiShe.CollectBus.Protocol.Contracts\JiShe.CollectBus.Protocol.Contracts.csproj" />
|
<ProjectReference Include="..\JiShe.CollectBus.Protocol.Contracts\JiShe.CollectBus.Protocol.Contracts.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||||
<Exec Command="copy $(TargetDir)JiShe.CollectBus.Protocol.dll $(ProjectDir)..\JiShe.CollectBus.Host\Plugins\" />
|
<Exec Command="copy $(TargetDir)JiShe.CollectBus.Protocol.Test.dll $(ProjectDir)..\..\web\JiShe.CollectBus.Host\Plugins\" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
using JiShe.CollectBus.Protocol.Contracts.Interfaces;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Volo.Abp;
|
||||||
|
using Volo.Abp.Modularity;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Protocol.Test
|
||||||
|
{
|
||||||
|
public class JiSheCollectBusProtocolModule : AbpModule
|
||||||
|
{
|
||||||
|
public override void ConfigureServices(ServiceConfigurationContext context)
|
||||||
|
{
|
||||||
|
context.Services.AddKeyedSingleton<IProtocolPlugin, TestProtocolPlugin>(nameof(TestProtocolPlugin));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnApplicationInitialization(ApplicationInitializationContext context)
|
||||||
|
{
|
||||||
|
var protocol = context.ServiceProvider.GetRequiredKeyedService<IProtocolPlugin>(nameof(TestProtocolPlugin));
|
||||||
|
protocol.AddAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
155
protocols/JiShe.CollectBus.Protocol.Test/TestProtocolPlugin.cs
Normal file
155
protocols/JiShe.CollectBus.Protocol.Test/TestProtocolPlugin.cs
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
using JiShe.CollectBus.Common.Enums;
|
||||||
|
using JiShe.CollectBus.Common.Extensions;
|
||||||
|
using JiShe.CollectBus.Common.Models;
|
||||||
|
using JiShe.CollectBus.IotSystems.MessageReceiveds;
|
||||||
|
using JiShe.CollectBus.IotSystems.Protocols;
|
||||||
|
using JiShe.CollectBus.Protocol.Contracts.Abstracts;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.Protocol.Test
|
||||||
|
{
|
||||||
|
public class TestProtocolPlugin : BaseProtocolPlugin
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TestProtocolPlugin"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider">The service provider.</param>
|
||||||
|
public TestProtocolPlugin(IServiceProvider serviceProvider) : base(serviceProvider)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed override ProtocolInfo Info => new(nameof(TestProtocolPlugin), "Test", "TCP", "Test协议", "DTS1980-Test");
|
||||||
|
|
||||||
|
public override async Task<T> AnalyzeAsync<T>(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region 上行命令
|
||||||
|
|
||||||
|
//68
|
||||||
|
//32 00
|
||||||
|
//32 00
|
||||||
|
//68
|
||||||
|
//C9 1100'1001. 控制域C。
|
||||||
|
// D7=1, (终端发送)上行方向。
|
||||||
|
// D6=1, 此帧来自启动站。
|
||||||
|
// D5=0, (上行方向)要求访问位。表示终端无事件数据等待访问。
|
||||||
|
// D4=0, 保留
|
||||||
|
// D3~D0=9, 功能码。链路测试
|
||||||
|
|
||||||
|
//20 32 行政区划码
|
||||||
|
//90 26 终端地址
|
||||||
|
//00 主站地址和组地址标志。终端为单地址。 //3220 09 87 2
|
||||||
|
// 终端启动的发送帧的 MSA 应为 0, 其主站响应帧的 MSA 也应为 0.
|
||||||
|
//02 应用层功能码。AFN=2, 链路接口检测
|
||||||
|
//70 0111'0000. 帧序列域。无时间标签、单帧、需要确认。
|
||||||
|
//00 00 信息点。DA1和DA2全为“0”时,表示终端信息点。
|
||||||
|
//01 00 信息类。F1, 登录。
|
||||||
|
//44 帧尾,包含用户区数据校验和
|
||||||
|
//16 帧结束标志
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析上行命令
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cmd"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public CommandReulst? AnalysisCmd(string cmd)
|
||||||
|
{
|
||||||
|
CommandReulst? commandReulst = null;
|
||||||
|
var hexStringList = cmd.StringToPairs();
|
||||||
|
|
||||||
|
if (hexStringList.Count < hearderLen)
|
||||||
|
{
|
||||||
|
return commandReulst;
|
||||||
|
}
|
||||||
|
//验证起始字符
|
||||||
|
if (!hexStringList[0].IsStartStr() || !hexStringList[5].IsStartStr())
|
||||||
|
{
|
||||||
|
return commandReulst;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lenHexStr = $"{hexStringList[2]}{hexStringList[1]}";
|
||||||
|
var lenBin = lenHexStr.HexToBin();
|
||||||
|
var len = lenBin.Remove(lenBin.Length - 2).BinToDec();
|
||||||
|
//验证长度
|
||||||
|
if (hexStringList.Count - 2 != hearderLen + len)
|
||||||
|
return commandReulst;
|
||||||
|
|
||||||
|
var userDataIndex = hearderLen;
|
||||||
|
var c = hexStringList[userDataIndex];//控制域 1字节
|
||||||
|
userDataIndex += 1;
|
||||||
|
|
||||||
|
var aHexList = hexStringList.Skip(userDataIndex).Take(5).ToList();//地址域 5字节
|
||||||
|
var a = AnalysisA(aHexList);
|
||||||
|
var a3Bin = aHexList[4].HexToBin().PadLeft(8, '0');
|
||||||
|
var mSA = a3Bin.Substring(0, 7).BinToDec();
|
||||||
|
userDataIndex += 5;
|
||||||
|
|
||||||
|
var aFN = (AFN)hexStringList[userDataIndex].HexToDec();//1字节
|
||||||
|
userDataIndex += 1;
|
||||||
|
|
||||||
|
var seq = hexStringList[userDataIndex].HexToBin().PadLeft(8, '0');
|
||||||
|
var tpV = (TpV)Convert.ToInt32(seq.Substring(0, 1));
|
||||||
|
var fIRFIN = (FIRFIN)Convert.ToInt32(seq.Substring(1, 2));
|
||||||
|
var cON = (CON)Convert.ToInt32(seq.Substring(3, 1));
|
||||||
|
var prseqBin = seq.Substring(4, 4);
|
||||||
|
userDataIndex += 1;
|
||||||
|
|
||||||
|
// (DA2 - 1) * 8 + DA1 = pn
|
||||||
|
var da1Bin = hexStringList[userDataIndex].HexToBin();
|
||||||
|
var da1 = da1Bin == "0" ? 0 : da1Bin.Length;
|
||||||
|
userDataIndex += 1;
|
||||||
|
var da2 = hexStringList[userDataIndex].HexToDec();
|
||||||
|
var pn = da2 == 0 ? 0 : (da2 - 1) * 8 + da1;
|
||||||
|
userDataIndex += 1;
|
||||||
|
//(DT2*8)+DT1=fn
|
||||||
|
var dt1Bin = hexStringList[userDataIndex].HexToBin();
|
||||||
|
var dt1 = dt1Bin != "0" ? dt1Bin.Length : 0;
|
||||||
|
userDataIndex += 1;
|
||||||
|
var dt2 = hexStringList[userDataIndex].HexToDec();
|
||||||
|
var fn = dt2 * 8 + dt1;
|
||||||
|
userDataIndex += 1;
|
||||||
|
|
||||||
|
//数据单元
|
||||||
|
var datas = hexStringList.Skip(userDataIndex).Take(len + hearderLen - userDataIndex).ToList();
|
||||||
|
|
||||||
|
//EC
|
||||||
|
//Tp
|
||||||
|
commandReulst = new CommandReulst()
|
||||||
|
{
|
||||||
|
A = a,
|
||||||
|
MSA = mSA,
|
||||||
|
AFN = aFN,
|
||||||
|
Seq = new Seq()
|
||||||
|
{
|
||||||
|
TpV = tpV,
|
||||||
|
FIRFIN = fIRFIN,
|
||||||
|
CON = cON,
|
||||||
|
PRSEQ = prseqBin.BinToDec(),
|
||||||
|
},
|
||||||
|
CmdLength = len,
|
||||||
|
Pn = pn,
|
||||||
|
Fn = fn,
|
||||||
|
HexDatas = datas
|
||||||
|
};
|
||||||
|
|
||||||
|
return commandReulst;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析地址
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="aHexList"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private string AnalysisA(List<string> aHexList)
|
||||||
|
{
|
||||||
|
var a1 = aHexList[1] + aHexList[0];
|
||||||
|
var a2 = aHexList[3] + aHexList[2];
|
||||||
|
var a2Dec = a2.HexToDec();
|
||||||
|
var a3 = aHexList[4];
|
||||||
|
var a = $"{a1}{a2Dec.ToString().PadLeft(5, '0')}";
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<BaseOutputPath></BaseOutputPath>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DotNetCore.CAP" Version="8.3.1" />
|
||||||
|
<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="..\..\protocols\JiShe.CollectBus.Protocol.Contracts\JiShe.CollectBus.Protocol.Contracts.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||||
|
<Exec Command="copy $(TargetDir)JiShe.CollectBus.Protocol.dll $(ProjectDir)..\..\web\JiShe.CollectBus.Host\Plugins\" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@ -12,10 +12,10 @@ namespace JiShe.CollectBus.Protocol
|
|||||||
context.Services.AddKeyedSingleton<IProtocolPlugin, StandardProtocolPlugin>(nameof(StandardProtocolPlugin));
|
context.Services.AddKeyedSingleton<IProtocolPlugin, StandardProtocolPlugin>(nameof(StandardProtocolPlugin));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnApplicationInitialization(ApplicationInitializationContext context)
|
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
|
||||||
{
|
{
|
||||||
var standardProtocol = context.ServiceProvider.GetRequiredKeyedService<IProtocolPlugin>(nameof(StandardProtocolPlugin));
|
var standardProtocol = context.ServiceProvider.GetRequiredKeyedService<IProtocolPlugin>(nameof(StandardProtocolPlugin));
|
||||||
standardProtocol.AddAsync();
|
await standardProtocol.AddAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,15 +1,15 @@
|
|||||||
using JiShe.CollectBus.Common.Enums;
|
using JiShe.CollectBus.Common.Enums;
|
||||||
using JiShe.CollectBus.Common.Extensions;
|
using JiShe.CollectBus.Common.Extensions;
|
||||||
using JiShe.CollectBus.Common.Models;
|
using JiShe.CollectBus.Common.Models;
|
||||||
using JiShe.CollectBus.MessageReceiveds;
|
using JiShe.CollectBus.IotSystems.MessageReceiveds;
|
||||||
|
using JiShe.CollectBus.IotSystems.Protocols;
|
||||||
using JiShe.CollectBus.Protocol.Contracts.Abstracts;
|
using JiShe.CollectBus.Protocol.Contracts.Abstracts;
|
||||||
using JiShe.CollectBus.Protocol.Contracts.Models;
|
using JiShe.CollectBus.Protocol.Contracts.Models;
|
||||||
using JiShe.CollectBus.Protocols;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace JiShe.CollectBus.Protocol
|
namespace JiShe.CollectBus.Protocol
|
||||||
{
|
{
|
||||||
public class StandardProtocolPlugin: BaseProtocolPlugin
|
public class StandardProtocolPlugin : BaseProtocolPlugin
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="StandardProtocolPlugin"/> class.
|
/// Initializes a new instance of the <see cref="StandardProtocolPlugin"/> class.
|
||||||
@ -21,28 +21,40 @@ namespace JiShe.CollectBus.Protocol
|
|||||||
|
|
||||||
public sealed override ProtocolInfo Info => new(nameof(StandardProtocolPlugin), "376.1", "TCP", "376.1协议", "DTS1980");
|
public sealed override ProtocolInfo Info => new(nameof(StandardProtocolPlugin), "376.1", "TCP", "376.1协议", "DTS1980");
|
||||||
|
|
||||||
public override Task AnalyzeAsync(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
public override async Task<T> AnalyzeAsync<T>(MessageReceived messageReceived, Action<byte[]>? sendAction = null)
|
||||||
{
|
{
|
||||||
var hexStringList = messageReceived.MessageHexString.StringToPairs();
|
var hexStringList = messageReceived.MessageHexString.StringToPairs();
|
||||||
var aTuple = (Tuple<string, int>)hexStringList.GetAnalyzeValue(CommandChunkEnum.A);
|
var aTuple = (Tuple<string, int>)hexStringList.GetAnalyzeValue(CommandChunkEnum.A);
|
||||||
var afn = (int)hexStringList.GetAnalyzeValue(CommandChunkEnum.AFN);
|
var afn = (int)hexStringList.GetAnalyzeValue(CommandChunkEnum.AFN);
|
||||||
var fn = (int)hexStringList.GetAnalyzeValue(CommandChunkEnum.FN);
|
var fn = (int)hexStringList.GetAnalyzeValue(CommandChunkEnum.FN);
|
||||||
if (afn == (int)AFN.请求实时数据)
|
|
||||||
|
T analyze = default;
|
||||||
|
|
||||||
|
switch ((AFN)afn)
|
||||||
{
|
{
|
||||||
if (Enum.IsDefined(typeof(ATypeOfDataItems), fn)) //Enum.TryParse(afn.ToString(), out ATypeOfDataItems parseResult)
|
case AFN.确认或否认:
|
||||||
{
|
AnalyzeAnswerDataAsync(messageReceived, sendAction);
|
||||||
AnalyzeReadingDataAsync(messageReceived, sendAction);
|
break;
|
||||||
}
|
case AFN.设置参数: break;
|
||||||
}
|
case AFN.查询参数: break;
|
||||||
else if(afn == (int)AFN.请求历史数据)
|
case AFN.请求实时数据:
|
||||||
{
|
if (Enum.IsDefined(typeof(ATypeOfDataItems), fn))
|
||||||
if (Enum.IsDefined(typeof(IIdataTypeItems), fn))
|
{
|
||||||
{
|
analyze = (T?)AnalyzeReadingDataAsync(messageReceived, sendAction);
|
||||||
AnalyzeReadingTdcDataAsync(messageReceived, sendAction);
|
}
|
||||||
}
|
break;
|
||||||
|
case AFN.请求历史数据:
|
||||||
|
if (Enum.IsDefined(typeof(IIdataTypeItems), fn))
|
||||||
|
{
|
||||||
|
analyze = (T?)AnalyzeReadingTdcDataAsync(messageReceived, sendAction);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AFN.数据转发:
|
||||||
|
AnalyzeTransparentForwardingAnswerAsync(messageReceived, sendAction);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NotImplementedException();
|
return await Task.FromResult(analyze);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region 上行命令
|
#region 上行命令
|
||||||
@ -17,7 +17,7 @@
|
|||||||
- V4 → V5核心升级:
|
- V4 → V5核心升级:
|
||||||
- 微服务化架构改造
|
- 微服务化架构改造
|
||||||
- 统一配置管理中心
|
- 统一配置管理中心
|
||||||
- 支持Kafka/RabbitMQ双引擎
|
- 支持Kafka引擎
|
||||||
- 新增边缘计算能力
|
- 新增边缘计算能力
|
||||||
- 资源利用率提升40%
|
- 资源利用率提升40%
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ Body:
|
|||||||
|----------------|------------|--------------|
|
|----------------|------------|--------------|
|
||||||
| 最大连接数 | 10,000 | 线性扩展 |
|
| 最大连接数 | 10,000 | 线性扩展 |
|
||||||
| 数据处理延迟 | <50ms(p99) | - |
|
| 数据处理延迟 | <50ms(p99) | - |
|
||||||
| 吞吐量 | 20,000 TPS | 百万级TPS |
|
| 吞吐量 | 20,000 TPS | 十万级TPS |
|
||||||
| CPU利用率 | ≤70%@峰值 | 自动负载均衡 |
|
| CPU利用率 | ≤70%@峰值 | 自动负载均衡 |
|
||||||
|
|
||||||
## 6. 高可用设计
|
## 6. 高可用设计
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JiShe.CollectBus.DataMigration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据迁移服务
|
||||||
|
/// </summary>
|
||||||
|
public interface IDataMigrationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 开始迁移
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task StartMigrationAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
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