diff --git a/.gitignore b/.gitignore index daafdc8..d639003 100644 --- a/.gitignore +++ b/.gitignore @@ -400,4 +400,6 @@ FodyWeavers.xsd # ABP Studio **/.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 diff --git a/JiShe.CollectBus.sln b/JiShe.CollectBus.sln index 5a41cab..d4e6591 100644 --- a/JiShe.CollectBus.sln +++ b/JiShe.CollectBus.sln @@ -3,31 +3,49 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.9.34728.123 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JiShe.CollectBus.Domain.Shared", "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 -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 -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 -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 -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 -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 -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 -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 -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 -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 -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 -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 -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 Global 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}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE0457D9-4038-4A17-8808-DCAD06CFC0A0}.Release|Any CPU.Build.0 = Release|Any CPU + {C06C4082-638F-2996-5FED-7784475766C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C06C4082-638F-2996-5FED-7784475766C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C06C4082-638F-2996-5FED-7784475766C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C06C4082-638F-2996-5FED-7784475766C1}.Release|Any CPU.Build.0 = Release|Any CPU + {F0288175-F0EC-48BD-945F-CF1512850943}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0288175-F0EC-48BD-945F-CF1512850943}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0288175-F0EC-48BD-945F-CF1512850943}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0288175-F0EC-48BD-945F-CF1512850943}.Release|Any CPU.Build.0 = Release|Any CPU + {A3F3C092-0A25-450B-BF6A-5983163CBEF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3F3C092-0A25-450B-BF6A-5983163CBEF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3F3C092-0A25-450B-BF6A-5983163CBEF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3F3C092-0A25-450B-BF6A-5983163CBEF5}.Release|Any CPU.Build.0 = Release|Any CPU + {A377955E-7EA1-6F29-8CF7-774569E93925}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A377955E-7EA1-6F29-8CF7-774569E93925}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A377955E-7EA1-6F29-8CF7-774569E93925}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A377955E-7EA1-6F29-8CF7-774569E93925}.Release|Any CPU.Build.0 = Release|Any CPU + {443B4549-0AC0-4493-8F3E-49C83225DD76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {443B4549-0AC0-4493-8F3E-49C83225DD76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {443B4549-0AC0-4493-8F3E-49C83225DD76}.Release|Any CPU.ActiveCfg = Release|Any CPU + {443B4549-0AC0-4493-8F3E-49C83225DD76}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {D64C1577-4929-4B60-939E-96DE1534891A} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545} - {F2840BC7-0188-4606-9126-DADD0F5ABF7A} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545} - {BD65D04F-08D5-40C1-8C24-77CA0BACB877} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545} - {78040F9E-3501-4A40-82DF-00A597710F35} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545} - {F1C58097-4C08-4D88-8976-6B3389391481} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545} - {077AA5F8-8B61-420C-A6B5-0150A66FDB34} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545} - {35829A15-4127-4F69-8BDE-9405DEAACA9A} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545} - {AD2F1928-4411-4511-B564-5FB996EC08B9} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545} - {C62EFF95-5C32-435F-BD78-6977E828F894} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545} - {38C1808B-009A-418B-B17B-AB3626341B5D} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545} - {8BA01C3D-297D-42DF-BD63-EF07202A0A67} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545} - {FE0457D9-4038-4A17-8808-DCAD06CFC0A0} = {649A3FFA-182F-4E56-9717-E6A9A2BEC545} + {D64C1577-4929-4B60-939E-96DE1534891A} = {EBF7C01F-9B4F-48E6-8418-2CBFDA51EB0B} + {F2840BC7-0188-4606-9126-DADD0F5ABF7A} = {BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23} + {BD65D04F-08D5-40C1-8C24-77CA0BACB877} = {BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23} + {78040F9E-3501-4A40-82DF-00A597710F35} = {BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23} + {F1C58097-4C08-4D88-8976-6B3389391481} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59} + {077AA5F8-8B61-420C-A6B5-0150A66FDB34} = {A02F7D8A-04DC-44D6-94D4-3F65712D6B94} + {35829A15-4127-4F69-8BDE-9405DEAACA9A} = {A02F7D8A-04DC-44D6-94D4-3F65712D6B94} + {AD2F1928-4411-4511-B564-5FB996EC08B9} = {EBF7C01F-9B4F-48E6-8418-2CBFDA51EB0B} + {C62EFF95-5C32-435F-BD78-6977E828F894} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC} + {38C1808B-009A-418B-B17B-AB3626341B5D} = {3C3F9DB2-EC97-4464-B49F-BF1A0C2B46DC} + {8BA01C3D-297D-42DF-BD63-EF07202A0A67} = {BA4DA3E7-9AD0-47AD-A0E6-A0BB6700DA23} + {FE0457D9-4038-4A17-8808-DCAD06CFC0A0} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59} + {C06C4082-638F-2996-5FED-7784475766C1} = {2E0FE301-34C3-4561-9CAE-C7A9E65AEE59} + {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 GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4324B3B4-B60B-4E3C-91D8-59576B4E26DD} diff --git a/modules/JiShe.CollectBus.Cassandra/CassandraConfig.cs b/modules/JiShe.CollectBus.Cassandra/CassandraConfig.cs new file mode 100644 index 0000000..c14171e --- /dev/null +++ b/modules/JiShe.CollectBus.Cassandra/CassandraConfig.cs @@ -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; } + } + +} diff --git a/modules/JiShe.CollectBus.Cassandra/CassandraProvider.cs b/modules/JiShe.CollectBus.Cassandra/CassandraProvider.cs new file mode 100644 index 0000000..dc3e0ee --- /dev/null +++ b/modules/JiShe.CollectBus.Cassandra/CassandraProvider.cs @@ -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 _logger; + + public Cluster Instance { get; set; } + + public ISession Session { get; set; } + + public CassandraConfig CassandraConfig { get; set; } + /// + /// + /// + /// + /// + public CassandraProvider( + IOptions options, + ILogger logger) + { + CassandraConfig = options.Value; + _logger = logger; + } + + public Task InitClusterAndSessionAsync() + { + InitClusterAndSession(); + return Task.CompletedTask; + } + + public void InitClusterAndSession() + { + GetCluster((keyspace) => + { + GetSession(keyspace); + }); + } + + public Cluster GetCluster(Action? 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 GetReplicationStrategy() + { + var strategy = CassandraConfig.ReplicationStrategy.Class; + var dataCenters = CassandraConfig.ReplicationStrategy.DataCenters; + + switch (strategy) + { + case "NetworkTopologyStrategy": + var networkDic = new Dictionary { { "class", "NetworkTopologyStrategy" } }; + foreach (var dataCenter in dataCenters) + { + networkDic.Add(dataCenter.Name, dataCenter.ReplicationFactor.ToString()); + } + return networkDic; + case "SimpleStrategy": + var dic = new Dictionary { { "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(); + } + } +} \ No newline at end of file diff --git a/modules/JiShe.CollectBus.Cassandra/CassandraQueryOptimizer.cs b/modules/JiShe.CollectBus.Cassandra/CassandraQueryOptimizer.cs new file mode 100644 index 0000000..0ea1b56 --- /dev/null +++ b/modules/JiShe.CollectBus.Cassandra/CassandraQueryOptimizer.cs @@ -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 _logger; + private readonly IMemoryCache _cache; + private readonly ConcurrentDictionary _preparedStatements; + private readonly int _batchSize; + private readonly TimeSpan _cacheExpiration; + + public CassandraQueryOptimizer( + ISession session, + ILogger logger, + IMemoryCache cache, + int batchSize = 100, + TimeSpan? cacheExpiration = null) + { + _session = session; + _logger = logger; + _cache = cache; + _preparedStatements = new ConcurrentDictionary(); + _batchSize = batchSize; + _cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(5); + } + + public async Task GetOrPrepareStatementAsync(string cql) + { + return _preparedStatements.GetOrAdd(cql, key => + { + try + { + var statement = _session.Prepare(key); + _logger.LogDebug($"Prepared statement for CQL: {key}"); + return statement; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed to prepare statement for CQL: {key}"); + throw; + } + }); + } + + public async Task ExecuteBatchAsync(IEnumerable statements) + { + var batch = new BatchStatement(); + var count = 0; + + foreach (var statement in statements) + { + batch.Add(statement); + count++; + + if (count >= _batchSize) + { + await ExecuteBatchAsync(batch); + batch = new BatchStatement(); + count = 0; + } + } + + if (count > 0) + { + await ExecuteBatchAsync(batch); + } + } + + private async Task ExecuteBatchAsync(BatchStatement batch) + { + try + { + await _session.ExecuteAsync(batch); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to execute batch statement"); + throw; + } + } + + public async Task GetOrSetFromCacheAsync(string cacheKey, Func> getData) + { + if (_cache.TryGetValue(cacheKey, out T cachedValue)) + { + _logger.LogDebug($"Cache hit for key: {cacheKey}"); + return cachedValue; + } + + var data = await getData(); + _cache.Set(cacheKey, data, _cacheExpiration); + _logger.LogDebug($"Cache miss for key: {cacheKey}, data cached"); + return data; + } + + public async Task> ExecutePagedQueryAsync( + string cql, + object[] parameters, + int pageSize = 100, + string pagingState = null) where T : class + { + var statement = await GetOrPrepareStatementAsync(cql); + var boundStatement = statement.Bind(parameters); + + if (!string.IsNullOrEmpty(pagingState)) + { + boundStatement.SetPagingState(Convert.FromBase64String(pagingState)); + } + + boundStatement.SetPageSize(pageSize); + + try + { + var result = await _session.ExecuteAsync(boundStatement); + //TODO: RETURN OBJECT + throw new NotImplementedException(); + //result.GetRows() + //return result.Select(row => row); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed to execute paged query: {cql}"); + throw; + } + } + + public async Task BulkInsertAsync(IEnumerable items, string tableName) + { + var mapper = new Mapper(_session); + var batch = new List(); + var cql = $"INSERT INTO {tableName} ({{0}}) VALUES ({{1}})"; + + foreach (var chunk in items.Chunk(_batchSize)) + { + var statements = chunk.Select(item => + { + var props = typeof(T).GetProperties(); + var columns = string.Join(", ", props.Select(p => p.Name)); + var values = string.Join(", ", props.Select(p => "?")); + var statement = _session.Prepare(string.Format(cql, columns, values)); + return statement.Bind(props.Select(p => p.GetValue(item)).ToArray()); + }); + + batch.AddRange(statements); + } + + await ExecuteBatchAsync(batch); + } + } +} \ No newline at end of file diff --git a/modules/JiShe.CollectBus.Cassandra/CassandraRepository.cs b/modules/JiShe.CollectBus.Cassandra/CassandraRepository.cs new file mode 100644 index 0000000..25a51e3 --- /dev/null +++ b/modules/JiShe.CollectBus.Cassandra/CassandraRepository.cs @@ -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 + : ICassandraRepository + where TEntity : class + { + private readonly ICassandraProvider _cassandraProvider; + public CassandraRepository(ICassandraProvider cassandraProvider, MappingConfiguration mappingConfig) + { + _cassandraProvider = cassandraProvider; + Mapper = new Mapper(cassandraProvider.Session, mappingConfig); + cassandraProvider.Session.CreateTable(cassandraProvider.CassandraConfig.Keyspace); + } + + public readonly IMapper Mapper; + + public virtual async Task GetAsync(TKey id) + { + return await Mapper.SingleOrDefaultAsync("WHERE id = ?", id); + } + + public virtual async Task> GetListAsync() + { + return (await Mapper.FetchAsync()).ToList(); + } + + public virtual async Task InsertAsync(TEntity entity) + { + await Mapper.InsertAsync(entity); + return entity; + } + + public virtual async Task 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("WHERE id = ?", id); + } + + public virtual async Task> 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(cql)).ToList(); + } + } +} \ No newline at end of file diff --git a/modules/JiShe.CollectBus.Cassandra/CollectBusCassandraModule.cs b/modules/JiShe.CollectBus.Cassandra/CollectBusCassandraModule.cs new file mode 100644 index 0000000..b5274f7 --- /dev/null +++ b/modules/JiShe.CollectBus.Cassandra/CollectBusCassandraModule.cs @@ -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(context.Services.GetConfiguration().GetSection("Cassandra")); + context.AddCassandra(); + return Task.CompletedTask; + } + + public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context) + { + await context.UseCassandra(); + } + } +} diff --git a/modules/JiShe.CollectBus.Cassandra/Extensions/ServiceCollectionExtensions.cs b/modules/JiShe.CollectBus.Cassandra/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..fe69268 --- /dev/null +++ b/modules/JiShe.CollectBus.Cassandra/Extensions/ServiceCollectionExtensions.cs @@ -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(); + 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())); + } + } +} diff --git a/modules/JiShe.CollectBus.Cassandra/Extensions/SessionExtension.cs b/modules/JiShe.CollectBus.Cassandra/Extensions/SessionExtension.cs new file mode 100644 index 0000000..c313d0c --- /dev/null +++ b/modules/JiShe.CollectBus.Cassandra/Extensions/SessionExtension.cs @@ -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(this ISession session,string? defaultKeyspace=null) where TEntity : class + { + var type = typeof(TEntity); + var tableAttribute = type.GetCustomAttribute(); + var tableName = tableAttribute?.Name ?? type.Name.ToLower(); + //var tableKeyspace = tableAttribute?.Keyspace ?? defaultKeyspace; + var tableKeyspace = session.Keyspace; + + var properties = type.GetProperties(); + var primaryKey = properties.FirstOrDefault(p => p.GetCustomAttribute() != null); + + 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(); + 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}"); + } + + + } +} diff --git a/modules/JiShe.CollectBus.Cassandra/ICassandraProvider.cs b/modules/JiShe.CollectBus.Cassandra/ICassandraProvider.cs new file mode 100644 index 0000000..8b1f87a --- /dev/null +++ b/modules/JiShe.CollectBus.Cassandra/ICassandraProvider.cs @@ -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? callback = null); + + void InitClusterAndSession(); + + Task InitClusterAndSessionAsync(); + } +} diff --git a/modules/JiShe.CollectBus.Cassandra/ICassandraRepository.cs b/modules/JiShe.CollectBus.Cassandra/ICassandraRepository.cs new file mode 100644 index 0000000..6dd474f --- /dev/null +++ b/modules/JiShe.CollectBus.Cassandra/ICassandraRepository.cs @@ -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 where TEntity : class + { + Task GetAsync(TKey id); + Task> GetListAsync(); + Task InsertAsync(TEntity entity); + Task UpdateAsync(TEntity entity); + Task DeleteAsync(TEntity entity); + Task DeleteAsync(TKey id); + Task> GetPagedListAsync(int skipCount, int maxResultCount, string sorting); + } +} diff --git a/modules/JiShe.CollectBus.Cassandra/JiShe.CollectBus.Cassandra.csproj b/modules/JiShe.CollectBus.Cassandra/JiShe.CollectBus.Cassandra.csproj new file mode 100644 index 0000000..edc303c --- /dev/null +++ b/modules/JiShe.CollectBus.Cassandra/JiShe.CollectBus.Cassandra.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/modules/JiShe.CollectBus.Cassandra/Mappers/CollectBusMapping.cs b/modules/JiShe.CollectBus.Cassandra/Mappers/CollectBusMapping.cs new file mode 100644 index 0000000..d07f8ea --- /dev/null +++ b/modules/JiShe.CollectBus.Cassandra/Mappers/CollectBusMapping.cs @@ -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() + .Column(e => e.Type, cm => cm.WithName("type").WithDbType()); + } + } +} diff --git a/modules/JiShe.CollectBus.FreeRedis/CollectBusFreeRedisModule.cs b/modules/JiShe.CollectBus.FreeRedis/CollectBusFreeRedisModule.cs new file mode 100644 index 0000000..fb86fdd --- /dev/null +++ b/modules/JiShe.CollectBus.FreeRedis/CollectBusFreeRedisModule.cs @@ -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(options => + { + configuration.GetSection("Redis").Bind(options); + }); + + + } + } +} + + + diff --git a/modules/JiShe.CollectBus.FreeRedis/FreeRedisProvider.cs b/modules/JiShe.CollectBus.FreeRedis/FreeRedisProvider.cs new file mode 100644 index 0000000..4c81d02 --- /dev/null +++ b/modules/JiShe.CollectBus.FreeRedis/FreeRedisProvider.cs @@ -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; + + /// + /// FreeRedis + /// + public FreeRedisProvider(IOptions options) + { + _option = options.Value; + GetInstance(); + } + + public RedisClient Instance { get; set; } = new(string.Empty); + + /// + /// 获取 FreeRedis 客户端 + /// + /// + 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; + } + + ///// + ///// 单个添加数据 + ///// + ///// + ///// 主数据存储Hash缓存Key + ///// 集中器索引Set缓存Key + ///// 集中器排序索引ZSET缓存Key + ///// 集中器采集频率分组全局索引ZSet缓存Key + ///// 表计信息 + ///// 可选时间戳 + ///// + //public async Task AddMeterCacheData( + //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; + //} + + ///// + ///// 批量添加数据 + ///// + ///// + ///// 主数据存储Hash缓存Key + ///// 集中器索引Set缓存Key + ///// 集中器排序索引ZSET缓存Key + ///// 集中器采集频率分组全局索引ZSet缓存Key + ///// 数据集合 + ///// 可选时间戳 + ///// + //public async Task BatchAddMeterData( + //string redisCacheKey, + //string redisCacheFocusIndexKey, + //string redisCacheScoresIndexKey, + //string redisCacheGlobalIndexKey, + //IEnumerable 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; + //} + + ///// + ///// 删除指定redis缓存key的缓存数据 + ///// + ///// + ///// 主数据存储Hash缓存Key + ///// 集中器索引Set缓存Key + ///// 集中器排序索引ZSET缓存Key + ///// 集中器采集频率分组全局索引ZSet缓存Key + ///// 表计信息 + ///// + //public async Task RemoveMeterData( + //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("指定数据不存在"); + //} + + ///// + ///// 修改表计缓存信息 + ///// + ///// + ///// 主数据存储Hash缓存Key + ///// 旧集中器索引Set缓存Key + ///// 新集中器索引Set缓存Key + ///// 集中器排序索引ZSET缓存Key + ///// 集中器采集频率分组全局索引ZSet缓存Key + ///// 表计信息 + ///// 可选时间戳 + ///// + //public async Task UpdateMeterData( + //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> SingleGetMeterPagedData( + //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(redisCacheKey, m)).ToArray(); + // await Task.WhenAll(dataTasks); + + // // 总数统计优化 + // var total = await Instance.ZCountAsync( + // redisCacheScoresIndexKey, + // minScore, + // maxScore); + + // return new BusPagedResult + // { + // Items = dataTasks.Select(t => t.Result).ToList(), + // TotalCount = total, + // PageIndex = pageIndex, + // PageSize = pageSize + // }; + //} + + + //public async Task> GetFocusPagedData( + //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(redisCacheKey, members.CurrentItems); + + // return new BusPagedResult + // { + // 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 GetTotalCount(string zsetKey, long min, long max) + //{ + // // 缓存计数优化 + // var cacheKey = $"{zsetKey}_count_{min}_{max}"; + // var cached = await Instance.GetAsync(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>> BatchGetMeterPagedData( + //string redisCacheKey, + //string redisCacheScoresIndexKey, + //IEnumerable focusIds, + //int pageSizePerFocus = 10) where T : DeviceCacheBasicModel + //{ + // var results = new ConcurrentDictionary>(); + // var parallelOptions = new ParallelOptions + // { + // MaxDegreeOfParallelism = Environment.ProcessorCount * 2 + // }; + + // await Parallel.ForEachAsync(focusIds, parallelOptions, async (focusId, _) => + // { + // var data = await SingleGetMeterPagedData( + // redisCacheKey, + // redisCacheScoresIndexKey, + // focusId, + // pageSizePerFocus); + + // results.TryAdd(focusId, data); + // }); + + // return new Dictionary>(results); + //} + + + + } +} \ No newline at end of file diff --git a/modules/JiShe.CollectBus.FreeRedis/IFreeRedisProvider.cs b/modules/JiShe.CollectBus.FreeRedis/IFreeRedisProvider.cs new file mode 100644 index 0000000..e9e8a40 --- /dev/null +++ b/modules/JiShe.CollectBus.FreeRedis/IFreeRedisProvider.cs @@ -0,0 +1,14 @@ +using FreeRedis; + +namespace JiShe.CollectBus.FreeRedis +{ + public interface IFreeRedisProvider + { + /// + /// 获取客户端 + /// + /// + RedisClient Instance { get; set; } + } +} + diff --git a/modules/JiShe.CollectBus.FreeRedis/JiShe.CollectBus.FreeRedis.csproj b/modules/JiShe.CollectBus.FreeRedis/JiShe.CollectBus.FreeRedis.csproj new file mode 100644 index 0000000..92b3223 --- /dev/null +++ b/modules/JiShe.CollectBus.FreeRedis/JiShe.CollectBus.FreeRedis.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/modules/JiShe.CollectBus.FreeRedis/Options/FreeRedisOptions.cs b/modules/JiShe.CollectBus.FreeRedis/Options/FreeRedisOptions.cs new file mode 100644 index 0000000..331da88 --- /dev/null +++ b/modules/JiShe.CollectBus.FreeRedis/Options/FreeRedisOptions.cs @@ -0,0 +1,26 @@ +namespace JiShe.CollectBus.FreeRedis.Options +{ + public class FreeRedisOptions + { + /// + /// 连接字符串 + /// + public string? Configuration { get; set; } + + /// + /// 最大连接数 + /// + public string? MaxPoolSize { get; set; } + + /// + /// 默认数据库 + /// + public string? DefaultDB { get; set; } + + /// + /// HangfireDB + /// + public string? HangfireDB { get; set; } + } +} + diff --git a/src/JiShe.CollectBus.FreeSql/CollectBusFreeSqlModule.cs b/modules/JiShe.CollectBus.FreeSql/CollectBusFreeSqlModule.cs similarity index 100% rename from src/JiShe.CollectBus.FreeSql/CollectBusFreeSqlModule.cs rename to modules/JiShe.CollectBus.FreeSql/CollectBusFreeSqlModule.cs diff --git a/src/JiShe.CollectBus.FreeSql/DbEnum.cs b/modules/JiShe.CollectBus.FreeSql/DbEnum.cs similarity index 100% rename from src/JiShe.CollectBus.FreeSql/DbEnum.cs rename to modules/JiShe.CollectBus.FreeSql/DbEnum.cs diff --git a/src/JiShe.CollectBus.FreeSql/FreeSqlProvider.cs b/modules/JiShe.CollectBus.FreeSql/FreeSqlProvider.cs similarity index 100% rename from src/JiShe.CollectBus.FreeSql/FreeSqlProvider.cs rename to modules/JiShe.CollectBus.FreeSql/FreeSqlProvider.cs diff --git a/src/JiShe.CollectBus.FreeSql/IFreeSqlProvider.cs b/modules/JiShe.CollectBus.FreeSql/IFreeSqlProvider.cs similarity index 100% rename from src/JiShe.CollectBus.FreeSql/IFreeSqlProvider.cs rename to modules/JiShe.CollectBus.FreeSql/IFreeSqlProvider.cs diff --git a/src/JiShe.CollectBus.FreeSql/JiShe.CollectBus.FreeSql.csproj b/modules/JiShe.CollectBus.FreeSql/JiShe.CollectBus.FreeSql.csproj similarity index 100% rename from src/JiShe.CollectBus.FreeSql/JiShe.CollectBus.FreeSql.csproj rename to modules/JiShe.CollectBus.FreeSql/JiShe.CollectBus.FreeSql.csproj diff --git a/modules/JiShe.CollectBus.IoTDB/Attribute/ATTRIBUTEColumnAttribute.cs b/modules/JiShe.CollectBus.IoTDB/Attribute/ATTRIBUTEColumnAttribute.cs new file mode 100644 index 0000000..d188c36 --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Attribute/ATTRIBUTEColumnAttribute.cs @@ -0,0 +1,10 @@ +namespace JiShe.CollectBus.IoTDB.Attribute +{ + /// + /// Column分类标记特性(ATTRIBUTE字段),也就是属性字段 + /// + [AttributeUsage(AttributeTargets.Property)] + public class ATTRIBUTEColumnAttribute : System.Attribute + { + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Attribute/FIELDColumnAttribute.cs b/modules/JiShe.CollectBus.IoTDB/Attribute/FIELDColumnAttribute.cs new file mode 100644 index 0000000..7cabdf4 --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Attribute/FIELDColumnAttribute.cs @@ -0,0 +1,10 @@ +namespace JiShe.CollectBus.IoTDB.Attribute +{ + /// + /// Column分类标记特性(FIELD字段),数据列字段 + /// + [AttributeUsage(AttributeTargets.Property)] + public class FIELDColumnAttribute : System.Attribute + { + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Attribute/SingleMeasuringAttribute.cs b/modules/JiShe.CollectBus.IoTDB/Attribute/SingleMeasuringAttribute.cs new file mode 100644 index 0000000..cebb85a --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Attribute/SingleMeasuringAttribute.cs @@ -0,0 +1,16 @@ +namespace JiShe.CollectBus.IoTDB.Attribute +{ + /// + /// 用于标识当前实体为单侧点模式,单侧点模式只有一个Filed标识字段,类型是Tuple,Item1=>测点名称,Item2=>测点值,泛型 + /// + [AttributeUsage(AttributeTargets.Property)] + public class SingleMeasuringAttribute : System.Attribute + { + public string FieldName { get; set;} + + public SingleMeasuringAttribute(string fieldName) + { + FieldName = fieldName; + } + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Attribute/TAGColumnAttribute.cs b/modules/JiShe.CollectBus.IoTDB/Attribute/TAGColumnAttribute.cs new file mode 100644 index 0000000..6f40a47 --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Attribute/TAGColumnAttribute.cs @@ -0,0 +1,10 @@ +namespace JiShe.CollectBus.IoTDB.Attribute +{ + /// + /// Column分类标记特性(TAG字段),标签字段 + /// + [AttributeUsage(AttributeTargets.Property)] + public class TAGColumnAttribute : System.Attribute + { + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/CollectBusIoTDBModule.cs b/modules/JiShe.CollectBus.IoTDB/CollectBusIoTDBModule.cs new file mode 100644 index 0000000..93bbbfe --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/CollectBusIoTDBModule.cs @@ -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(options => + { + configuration.GetSection(nameof(IoTDBOptions)).Bind(options); + }); + + // 注册上下文为Scoped + context.Services.AddScoped(); + + // 注册Session工厂 + context.Services.AddSingleton(); + + // 注册Provider + context.Services.AddScoped(); + + } + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Context/IoTDBRuntimeContext.cs b/modules/JiShe.CollectBus.IoTDB/Context/IoTDBRuntimeContext.cs new file mode 100644 index 0000000..cd99b00 --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Context/IoTDBRuntimeContext.cs @@ -0,0 +1,32 @@ +using JiShe.CollectBus.IoTDB.Options; +using Microsoft.Extensions.Options; + +namespace JiShe.CollectBus.IoTDB.Context +{ + /// + /// IoTDB SessionPool 运行时上下文 + /// + public class IoTDBRuntimeContext + { + private readonly bool _defaultValue; + + public IoTDBRuntimeContext(IOptions options) + { + _defaultValue = options.Value.UseTableSessionPoolByDefault; + UseTableSessionPool = _defaultValue; + } + + /// + /// 是否使用表模型存储, 默认false,使用tree模型存储 + /// + public bool UseTableSessionPool { get; set; } + + /// + /// 重置为默认值 + /// + public void ResetToDefault() + { + UseTableSessionPool = _defaultValue; + } + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBProvider.cs b/modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBProvider.cs new file mode 100644 index 0000000..bb47841 --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBProvider.cs @@ -0,0 +1,51 @@ +using JiShe.CollectBus.Common.Models; +using JiShe.CollectBus.IoTDB.Options; +using JiShe.CollectBus.IoTDB.Provider; + +namespace JiShe.CollectBus.IoTDB.Interface +{ + /// + /// IoTDB数据源,数据库能同时存多个时序模型,但数据是完全隔离的,不能跨时序模型查询,通过连接字符串配置 + /// + public interface IIoTDBProvider + { + ///// + ///// 切换 SessionPool + ///// + ///// 是否使用表模型 + //void SwitchSessionPool(bool useTableSession); + + /// + /// 插入数据 + /// + /// + /// + /// + Task InsertAsync(T entity) where T : IoTEntity; + + /// + /// 批量插入数据 + /// + /// + /// + /// + Task BatchInsertAsync(IEnumerable entities) where T : IoTEntity; + + + /// + /// 删除数据 + /// + /// + /// + /// + Task DeleteAsync(QueryOptions options) where T : IoTEntity; + + /// + /// 查询数据 + /// + /// + /// + /// + Task> QueryAsync(QueryOptions options) where T : IoTEntity, new(); + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBSessionFactory.cs b/modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBSessionFactory.cs new file mode 100644 index 0000000..03cd4a6 --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBSessionFactory.cs @@ -0,0 +1,10 @@ +namespace JiShe.CollectBus.IoTDB.Interface +{ + /// + /// Session 工厂接口 + /// + public interface IIoTDBSessionFactory:IDisposable + { + IIoTDBSessionPool GetSessionPool(bool useTableSession); + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBSessionPool.cs b/modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBSessionPool.cs new file mode 100644 index 0000000..026a83a --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Interface/IIoTDBSessionPool.cs @@ -0,0 +1,30 @@ +using Apache.IoTDB.DataStructure; + +namespace JiShe.CollectBus.IoTDB.Interface +{ + /// + /// Session 连接池 + /// + public interface IIoTDBSessionPool : IDisposable + { + /// + /// 打开连接池 + /// + /// + Task OpenAsync(); + + /// + /// 插入数据 + /// + /// + /// + Task InsertAsync(Tablet tablet); + + /// + /// 查询数据 + /// + /// + /// + Task ExecuteQueryStatementAsync(string sql); + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/JiShe.CollectBus.IoTDB.csproj b/modules/JiShe.CollectBus.IoTDB/JiShe.CollectBus.IoTDB.csproj new file mode 100644 index 0000000..dc8f2fb --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/JiShe.CollectBus.IoTDB.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/modules/JiShe.CollectBus.IoTDB/Options/IoTDBOptions.cs b/modules/JiShe.CollectBus.IoTDB/Options/IoTDBOptions.cs new file mode 100644 index 0000000..c9c0610 --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Options/IoTDBOptions.cs @@ -0,0 +1,46 @@ +namespace JiShe.CollectBus.IoTDB.Options +{ + /// + /// IOTDB配置 + /// + public class IoTDBOptions + { + /// + /// 数据库名称,表模型才有,树模型为空 + /// + public string DataBaseName { get; set; } + + /// + /// 集群列表 + /// + public List ClusterList { get; set; } + /// + /// 用户名 + /// + public string UserName { get; set; } + /// + /// 密码 + /// + public string Password { get; set; } + + /// + /// 连接池大小 + /// + public int PoolSize { get; set; } = 2; + + /// + /// 查询时,每次查询的数据量,默认1024 + /// + public int FetchSize { get; set; } = 1024; + + /// + /// 是否开启调试模式,生产环境请关闭,因为底层的实现方式,可能会导致内存持续增长。 + /// + public bool OpenDebugMode { get; set;} + + /// + /// 是否使用表模型存储, 默认false,使用tree模型存储 + /// + public bool UseTableSessionPoolByDefault { get; set; } = false; + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Options/QueryCondition.cs b/modules/JiShe.CollectBus.IoTDB/Options/QueryCondition.cs new file mode 100644 index 0000000..cf6d3a9 --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Options/QueryCondition.cs @@ -0,0 +1,21 @@ +namespace JiShe.CollectBus.IoTDB.Options +{ + /// + /// 查询条件 + /// + public class QueryCondition + { + /// + /// 字段 + /// + public string Field { get; set; } + /// + /// 操作符 + /// + public string Operator { get; set; } + /// + /// 值 + /// + public object Value { get; set; } + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Options/QueryOptions.cs b/modules/JiShe.CollectBus.IoTDB/Options/QueryOptions.cs new file mode 100644 index 0000000..90e44b2 --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Options/QueryOptions.cs @@ -0,0 +1,28 @@ +namespace JiShe.CollectBus.IoTDB.Options +{ + /// + /// 查询条件 + /// + public class QueryOptions + { + /// + /// 表模型的表名称或者树模型的设备路径 + /// + public required string TableNameOrTreePath { get; set; } + + /// + /// 分页 + /// + public int Page { get; set; } + + /// + /// 分页大小 + /// + public int PageSize { get; set; } + + /// + /// 查询条件 + /// + public List Conditions { get; } = new(); + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Provider/DeviceMetadata.cs b/modules/JiShe.CollectBus.IoTDB/Provider/DeviceMetadata.cs new file mode 100644 index 0000000..447f6ce --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Provider/DeviceMetadata.cs @@ -0,0 +1,30 @@ +using Apache.IoTDB; + +namespace JiShe.CollectBus.IoTDB.Provider +{ + /// + /// 设备元数据 + /// + public class DeviceMetadata + { + /// + /// 是否有单测量值 + /// + public bool IsSingleMeasuring { get; set; } + + /// + /// 测量值集合,用于构建Table的测量值,也就是columnNames参数 + /// + public List ColumnNames { get; set; } = new(); + + /// + /// 列类型集合,用于构建Table的列类型,也就是columnCategories参数 + /// + public List ColumnCategories { get; } = new(); + + /// + /// 值类型集合,用于构建Table的值类型,也就是dataTypes参数 + /// + public List DataTypes { get; } = new(); + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Provider/DevicePathBuilder.cs b/modules/JiShe.CollectBus.IoTDB/Provider/DevicePathBuilder.cs new file mode 100644 index 0000000..46ce091 --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Provider/DevicePathBuilder.cs @@ -0,0 +1,33 @@ +namespace JiShe.CollectBus.IoTDB.Provider +{ + /// + /// 设备路径构建器 + /// + public static class DevicePathBuilder + { + /// + /// 构建设备路径,由于路径的层级约束规范不能是纯数字字符,所以需要做特殊处理。 + /// + /// + /// + /// + public static string GetDevicePath(T entity) where T : IoTEntity + { + return $"root.{entity.SystemName.ToLower()}.`{entity.ProjectCode}`.`{entity.DeviceId}`"; + } + + + /// + /// 获取表名称 + /// + /// + /// + /// + public static string GetTableName() where T : IoTEntity + { + var type = typeof(T); + return $"{type.Name.ToLower()}"; + } + } + +} diff --git a/modules/JiShe.CollectBus.IoTDB/Provider/IoTDBProvider.cs b/modules/JiShe.CollectBus.IoTDB/Provider/IoTDBProvider.cs new file mode 100644 index 0000000..9e18ac8 --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Provider/IoTDBProvider.cs @@ -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 +{ + /// + /// IoTDB数据源 + /// + public class IoTDBProvider : IIoTDBProvider + { + private static readonly ConcurrentDictionary _metadataCache = new(); + private readonly ILogger _logger; + private readonly IIoTDBSessionFactory _sessionFactory; + private readonly IoTDBRuntimeContext _runtimeContext; + + private IIoTDBSessionPool CurrentSession => + _sessionFactory.GetSessionPool(_runtimeContext.UseTableSessionPool); + + public IoTDBProvider( + ILogger logger, + IIoTDBSessionFactory sessionFactory, + IoTDBRuntimeContext runtimeContext) + { + _logger = logger; + _sessionFactory = sessionFactory; + _runtimeContext = runtimeContext; + + } + + /// + /// 插入数据 + /// + /// + /// + /// + public async Task InsertAsync(T entity) where T : IoTEntity + { + var metadata = GetMetadata(); + + var tablet = BuildTablet(new[] { entity }, metadata); + + await CurrentSession.InsertAsync(tablet); + + //int result = await _currentSession.InsertAsync(tablet); + //if (result <= 0) + //{ + // _logger.LogError($"{typeof(T).Name}插入数据没有成功"); + //} + } + + /// + /// 批量插入数据 + /// + /// + /// + public async Task BatchInsertAsync(IEnumerable entities) where T : IoTEntity + { + var metadata = GetMetadata(); + + 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}批次。"); + //} + } + } + + + /// + /// 删除数据 + /// + /// + /// + /// + public async Task DeleteAsync(QueryOptions options) where T : IoTEntity + { + var query = BuildDeleteSQL(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]; + } + + /// + /// 查询数据 + /// + /// + /// + /// + public async Task> QueryAsync(QueryOptions options) where T : IoTEntity, new() + { + var query = BuildQuerySQL(options); + var sessionDataSet = await CurrentSession.ExecuteQueryStatementAsync(query); + + var result = new BusPagedResult + { + TotalCount = await GetTotalCount(options), + Items = ParseResults(sessionDataSet, options.PageSize) + }; + + return result; + } + + /// + /// 构建Tablet + /// + /// + /// 表实体 + /// 设备元数据 + /// + private Tablet BuildTablet(IEnumerable entities, DeviceMetadata metadata) where T : IoTEntity + { + var timestamps = new List(); + var values = new List>(); + var devicePaths = new HashSet(); + List tempColumnNames = new List(); + tempColumnNames.AddRange(metadata.ColumnNames); + + foreach (var entity in entities) + { + timestamps.Add(entity.Timestamps); + var rowValues = new List(); + 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()); + } + } + + 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); + } + + /// + /// 构建tree模型的Tablet + /// + /// + /// + /// + /// + /// + private Tablet BuildSessionTablet(DeviceMetadata metadata, string devicePath, + List> values, List timestamps) + { + return new Tablet( + devicePath, + metadata.ColumnNames, + metadata.DataTypes, + values, + timestamps + ); + } + + /// + /// 构建表模型的Tablet + /// + /// + /// + /// + /// + /// + private Tablet BuildTableSessionTablet(DeviceMetadata metadata, string devicePath, + List> values, List timestamps) + { + var tablet = new Tablet( + devicePath, + metadata.ColumnNames, + metadata.ColumnCategories, + metadata.DataTypes, + values, + timestamps + ); + + return tablet; + } + + /// + /// 构建查询语句 + /// + /// + /// + /// + private string BuildQuerySQL(QueryOptions options) where T : IoTEntity + { + var metadata = GetMetadata(); + 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(); + } + + /// + /// 构建删除语句 + /// + /// + /// + /// + private string BuildDeleteSQL(QueryOptions options) where T : IoTEntity + { + var metadata = GetMetadata(); + 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(); + } + + /// + /// 将查询条件转换为SQL语句 + /// + /// + /// + /// + 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") + }; + } + + /// + /// 获取查询条件的总数量 + /// + /// + /// + /// + private async Task GetTotalCount(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; + } + + /// + /// 解析查询结果 + /// + /// + /// + /// + /// + private IEnumerable ParseResults(SessionDataSet dataSet, int pageSize) where T : IoTEntity, new() + { + var results = new List(); + var metadata = GetMetadata(); + + 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; + } + + /// + /// 获取设备元数据 + /// + /// + /// + private DeviceMetadata GetMetadata() 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; + //}); + } + + /// + /// 获取设备元数据的列 + /// + /// + /// + private List CollectColumnMetadata(Type type) + { + var columns = new List(); + + foreach (var prop in type.GetProperties()) + { + //先获取Tag标签和属性标签 + ColumnInfo? column = prop.GetCustomAttribute() is not null ? new ColumnInfo( + name: prop.Name, + category: ColumnCategory.TAG, + dataType: GetDataTypeFromTypeName(prop.PropertyType.Name), + false + ) : prop.GetCustomAttribute() is not null ? new ColumnInfo( + prop.Name, + ColumnCategory.ATTRIBUTE, + GetDataTypeFromTypeName(prop.PropertyType.Name), + false + ) : prop.GetCustomAttribute() is not null ? new ColumnInfo( + prop.Name, + ColumnCategory.FIELD, + GetDataTypeFromTypeName(prop.PropertyType.Name), + false) + : null; + + //最先检查是不是单侧点模式 + SingleMeasuringAttribute singleMeasuringAttribute = prop.GetCustomAttribute(); + + if (singleMeasuringAttribute != null && column == null) + { + //warning: 单侧点模式注意事项 + //Entity实体 字段类型是 Tuple,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; + } + + /// + /// 构建设备元数据 + /// + /// + /// + private DeviceMetadata BuildDeviceMetadata(List 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; + } + + /// + /// 处理不同列类型的逻辑 + /// + /// + /// + /// + private void ProcessCategory(IReadOnlyDictionary> 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)); + } + } + + /// + /// 数据列结构 + /// + private readonly struct ColumnInfo + { + /// + /// 列名 + /// + public string Name { get; } + + /// + /// 是否是单测点 + /// + public bool IsSingleMeasuring { get; } + + /// + /// 列类型 + /// + public ColumnCategory Category { get; } + + /// + /// 数据类型 + /// + public TSDataType DataType { get; } + + public ColumnInfo(string name, ColumnCategory category, TSDataType dataType, bool isSingleMeasuring) + { + Name = name; + Category = category; + DataType = dataType; + IsSingleMeasuring = isSingleMeasuring; + } + } + + /// + /// 根据类型名称获取对应的 IoTDB 数据类型 + /// + /// 类型名称(不区分大小写) + /// 对应的 TSDataType,默认返回 TSDataType.STRING + private TSDataType GetDataTypeFromTypeName(string typeName) + { + if (string.IsNullOrWhiteSpace(typeName)) + return TSDataType.STRING; + + return DataTypeMap.TryGetValue(typeName.Trim(), out var dataType) + ? dataType + : TSDataType.STRING; + } + + /// + /// 根据类型名称获取 IoTDB 数据类型 + /// + private readonly IReadOnlyDictionary DataTypeMap = + new Dictionary(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 + }; + + /// + /// 根据类型名称获取 IoTDB 数据默认值 + /// + private readonly IReadOnlyDictionary DataTypeDefaultValueMap = + new Dictionary(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 + }; + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Provider/IoTDBSessionFactory.cs b/modules/JiShe.CollectBus.IoTDB/Provider/IoTDBSessionFactory.cs new file mode 100644 index 0000000..572e93d --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Provider/IoTDBSessionFactory.cs @@ -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 +{ + + /// + /// 实现带缓存的Session工厂 + /// + public class IoTDBSessionFactory : IIoTDBSessionFactory + { + private readonly IoTDBOptions _options; + private readonly ConcurrentDictionary _pools = new(); + private bool _disposed; + + public IoTDBSessionFactory(IOptions 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; + } + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Provider/IoTEntity.cs b/modules/JiShe.CollectBus.IoTDB/Provider/IoTEntity.cs new file mode 100644 index 0000000..39a584d --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Provider/IoTEntity.cs @@ -0,0 +1,39 @@ +using JiShe.CollectBus.IoTDB.Attribute; + +namespace JiShe.CollectBus.IoTDB.Provider +{ + /// + /// IoT实体基类 + /// + public abstract class IoTEntity + { + /// + /// 系统名称 + /// + [TAGColumn] + public string SystemName { get; set; } + + /// + /// 项目编码 + /// + [TAGColumn] + public string ProjectCode { get; set; } + + /// + /// 设备类型集中器、电表、水表、流量计、传感器等 + /// + [TAGColumn] + public string DeviceType { get; set; } + + /// + /// 设备ID + /// + [TAGColumn] + public string DeviceId { get; set; } + + /// + /// 当前时间戳,单位毫秒 + /// + public long Timestamps { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Provider/SessionPoolAdapter.cs b/modules/JiShe.CollectBus.IoTDB/Provider/SessionPoolAdapter.cs new file mode 100644 index 0000000..44692bd --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Provider/SessionPoolAdapter.cs @@ -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 +{ + /// + /// 树模型连接池 + /// + 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(); + } + + /// + /// 打开连接池 + /// + /// + public async Task OpenAsync() + { + await _sessionPool.Open(false); + if (_options.OpenDebugMode) + { + _sessionPool.OpenDebugMode(builder => + { + builder.AddConsole(); + }); + } + } + + /// + /// 批量插入对齐时间序列数据 + /// + /// + /// + public async Task InsertAsync(Tablet tablet) + { + var result = await _sessionPool.InsertAlignedTabletAsync(tablet); + if (result != 0) + { + throw new Exception($"{nameof(TableSessionPoolAdapter)} "); + } + + return result; + } + + /// + /// 查询数据 + /// + /// + /// + public async Task ExecuteQueryStatementAsync(string sql) + { + return await _sessionPool.ExecuteQueryStatementAsync(sql); + } + + public void Dispose() + { + _sessionPool?.Close().ConfigureAwait(false).GetAwaiter().GetResult(); + } + } +} diff --git a/modules/JiShe.CollectBus.IoTDB/Provider/TableSessionPoolAdapter.cs b/modules/JiShe.CollectBus.IoTDB/Provider/TableSessionPoolAdapter.cs new file mode 100644 index 0000000..1efd04f --- /dev/null +++ b/modules/JiShe.CollectBus.IoTDB/Provider/TableSessionPoolAdapter.cs @@ -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 +{ + /// + /// 表模型Session连接池 + /// + 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(); + } + + /// + /// 打开连接池 + /// + /// + public async Task OpenAsync() + { + await _sessionPool.Open(false); + if (_options.OpenDebugMode) + { + _sessionPool.OpenDebugMode(builder => builder.AddConsole()); + } + } + + /// + /// 批量插入 + /// + /// + /// + public async Task InsertAsync(Tablet tablet) + { + var result = await _sessionPool.InsertAsync(tablet); + if (result != 0) + { + throw new Exception($"{nameof(TableSessionPoolAdapter)} "); + } + + return result; + } + + /// + /// 查询数据 + /// + /// + /// + public async Task ExecuteQueryStatementAsync(string sql) + { + return await _sessionPool.ExecuteQueryStatementAsync(sql); + } + + public void Dispose() + { + _sessionPool?.Close().ConfigureAwait(false).GetAwaiter().GetResult(); + } + } +} diff --git a/modules/JiShe.CollectBus.Kafka.Test/ConsoleApplicationBuilder.cs b/modules/JiShe.CollectBus.Kafka.Test/ConsoleApplicationBuilder.cs new file mode 100644 index 0000000..f6b0891 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka.Test/ConsoleApplicationBuilder.cs @@ -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 Properties { get; set; } = new Dictionary(); + + public IFeatureCollection ServerFeatures => throw new NotImplementedException(); + + private readonly List> _middlewares = new(); + + public IApplicationBuilder Use(Func 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(this.Properties) + }; + } + } + + + public static class HostBuilderExtensions + { + public static IHostBuilder ConfigureConsoleAppBuilder( + this IHostBuilder hostBuilder, + Action configure) + { + hostBuilder.ConfigureServices((context, services) => + { + // 注册 ConsoleApplicationBuilder 到 DI 容器 + services.AddSingleton(provider => + { + var appBuilder = new ConsoleApplicationBuilder + { + ApplicationServices = provider // 注入服务提供者 + }; + configure(appBuilder); // 执行配置委托 + return appBuilder; + }); + }); + return hostBuilder; + } + } +} diff --git a/modules/JiShe.CollectBus.Kafka.Test/JiShe.CollectBus.Kafka.Test.csproj b/modules/JiShe.CollectBus.Kafka.Test/JiShe.CollectBus.Kafka.Test.csproj new file mode 100644 index 0000000..db97b00 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka.Test/JiShe.CollectBus.Kafka.Test.csproj @@ -0,0 +1,40 @@ + + + + Exe + net8.0 + enable + enable + + + + + Always + true + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/JiShe.CollectBus.Kafka.Test/KafkaProduceBenchmark.cs b/modules/JiShe.CollectBus.Kafka.Test/KafkaProduceBenchmark.cs new file mode 100644 index 0000000..a8b8d93 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka.Test/KafkaProduceBenchmark.cs @@ -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(config); + + // 初始化日志 + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(config) // 从 appsettings.json 读取配置 + .CreateLogger(); + + // 配置日志系统 + services.AddLogging(logging => + { + logging.ClearProviders(); + logging.AddSerilog(); + }); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // 构建ServiceProvider + _serviceProvider = services.BuildServiceProvider(); + + // 获取日志记录器工厂 + var loggerFactory = _serviceProvider.GetRequiredService(); + var logger = loggerFactory.CreateLogger(); + logger.LogInformation("程序启动"); + + var adminClientService = _serviceProvider.GetRequiredService(); + + + //await adminClientService.DeleteTopicAsync(topic); + // 创建 topic + adminClientService.CreateTopicAsync(topic, 3, 3).ConfigureAwait(false).GetAwaiter(); + + _consumerService = _serviceProvider.GetRequiredService(); + + _producerService = _serviceProvider.GetRequiredService(); + } + + [Benchmark] + public async Task UseAsync() + { + List tasks = new(); + for (int i = 0; i < N; ++i) + { + var task = _producerService.ProduceAsync(topic, i.ToString()); + tasks.Add(task); + } + await Task.WhenAll(tasks); + } + + [Benchmark] + public async Task UseLibrd() + { + List tasks = new(); + for (int i = 0; i < N; ++i) + { + var task = _producerService.ProduceAsync(topic, i.ToString(),null); + } + await Task.WhenAll(tasks); + } + } +} diff --git a/modules/JiShe.CollectBus.Kafka.Test/KafkaSubscribeTest.cs b/modules/JiShe.CollectBus.Kafka.Test/KafkaSubscribeTest.cs new file mode 100644 index 0000000..4c06e22 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka.Test/KafkaSubscribeTest.cs @@ -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 KafkaSubscribeAsync(object obj) + { + Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(obj)}"); + return SubscribeAck.Success(); + } + + + [KafkaSubscribe(ProtocolConst.SubscriberLoginIssuedEventName)] + //[CapSubscribe(ProtocolConst.SubscriberLoginIssuedEventName)] + public async Task LoginIssuedEvent(IssuedEventMessage issuedEventMessage) + { + Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(issuedEventMessage)}"); + return SubscribeAck.Success(); + } + + [KafkaSubscribe(ProtocolConst.SubscriberHeartbeatIssuedEventName)] + //[CapSubscribe(ProtocolConst.SubscriberHeartbeatIssuedEventName)] + public async Task HeartbeatIssuedEvent(IssuedEventMessage issuedEventMessage) + { + Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(issuedEventMessage)}"); + return SubscribeAck.Success(); + } + + [KafkaSubscribe(ProtocolConst.SubscriberReceivedEventName)] + //[CapSubscribe(ProtocolConst.SubscriberReceivedEventName)] + public async Task ReceivedEvent(MessageReceived receivedMessage) + { + Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(receivedMessage)}"); + return SubscribeAck.Success(); + } + + [KafkaSubscribe(ProtocolConst.SubscriberHeartbeatReceivedEventName)] + //[CapSubscribe(ProtocolConst.SubscriberHeartbeatReceivedEventName)] + public async Task ReceivedHeartbeatEvent(MessageReceivedHeartbeat receivedHeartbeatMessage) + { + Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(receivedHeartbeatMessage)}"); + return SubscribeAck.Success(); + } + + [KafkaSubscribe(ProtocolConst.SubscriberLoginReceivedEventName)] + //[CapSubscribe(ProtocolConst.SubscriberLoginReceivedEventName)] + public async Task ReceivedLoginEvent(MessageReceivedLogin receivedLoginMessage) + { + Console.WriteLine($"收到订阅消息: {JsonSerializer.Serialize(receivedLoginMessage)}"); + return SubscribeAck.Success(); + } + } +} diff --git a/modules/JiShe.CollectBus.Kafka.Test/Lib/JiShe.CollectBus.Kafka.dll b/modules/JiShe.CollectBus.Kafka.Test/Lib/JiShe.CollectBus.Kafka.dll new file mode 100644 index 0000000..7ca63d6 Binary files /dev/null and b/modules/JiShe.CollectBus.Kafka.Test/Lib/JiShe.CollectBus.Kafka.dll differ diff --git a/modules/JiShe.CollectBus.Kafka.Test/Program.cs b/modules/JiShe.CollectBus.Kafka.Test/Program.cs new file mode 100644 index 0000000..a359e14 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka.Test/Program.cs @@ -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(); +//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(config); + + // 初始化日志 + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(config) // 从 appsettings.json 读取配置 + .CreateLogger(); + + // 配置日志系统 + services.AddLogging(logging => + { + logging.ClearProviders(); + logging.AddSerilog(); + }); + services.Configure(config.GetSection("Kafka")); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + + }) + .ConfigureConsoleAppBuilder(appBuilder => + { + + }) + .Build(); + + + await host.StartAsync(); + var appBuilder = host.Services.GetRequiredService(); + appBuilder.ApplicationServices.UseKafkaSubscribe(); + + +// 构建ServiceProvider +//var serviceProvider = services.BuildServiceProvider(); + +// 获取日志记录器工厂 +var loggerFactory = host.Services.GetRequiredService(); +var logger = loggerFactory.CreateLogger(); +logger.LogInformation("程序启动"); +var adminClientService = host.Services.GetRequiredService(); +var configuration = host.Services.GetRequiredService(); +string topic = "test-topic"; +//await adminClientService.DeleteTopicAsync(topic); +// 创建 topic +//await adminClientService.CreateTopicAsync(topic, configuration.GetValue(CommonConst.NumPartitions), 3); + +var consumerService = host.Services.GetRequiredService(); +//var kafkaOptions = host.Services.GetRequiredService>(); +//await consumerService.SubscribeAsync(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(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(); +//int num = 840; +//while (num <= 900) +//{ +// //await producerService.ProduceAsync(topic, new TestTopic { Topic = topic, Val = i }); +// await producerService.ProduceAsync(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(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; } +} \ No newline at end of file diff --git a/modules/JiShe.CollectBus.Kafka.Test/appsettings.json b/modules/JiShe.CollectBus.Kafka.Test/appsettings.json new file mode 100644 index 0000000..b2579c6 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka.Test/appsettings.json @@ -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 + } + } +} \ No newline at end of file diff --git a/modules/JiShe.CollectBus.Kafka/AdminClient/AdminClientService.cs b/modules/JiShe.CollectBus.Kafka/AdminClient/AdminClientService.cs new file mode 100644 index 0000000..59e34fa --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/AdminClient/AdminClientService.cs @@ -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 _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The logger. + public AdminClientService(IConfiguration configuration, ILogger logger) + { + _logger = logger; + GetInstance(configuration); + } + + /// + /// Gets or sets the instance. + /// + /// + /// The instance. + /// + public IAdminClient Instance { get; set; } = default; + + /// + /// Gets the instance. + /// + /// The configuration. + /// + 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; + } + + /// + /// Checks the topic asynchronous. + /// + /// The topic. + /// + public async Task CheckTopicAsync(string topic) + { + var metadata = Instance.GetMetadata(TimeSpan.FromSeconds(5)); + return await Task.FromResult(metadata.Topics.Exists(a => a.Topic == topic)); + } + + /// + /// 判断Kafka主题是否存在 + /// + /// 主题名称 + /// 副本数量,不能高于Brokers数量 + /// + public async Task 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)); + } + + //// + /// 创建Kafka主题 + /// + /// 主题名称 + /// 主题分区数量 + /// 副本数量,不能高于Brokers数量 + /// + 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; + } + } + } + + /// + /// 删除Kafka主题 + /// + /// + /// + public async Task DeleteTopicAsync(string topic) + { + await Instance.DeleteTopicsAsync(new[] { topic }); + } + + /// + /// 获取Kafka主题列表 + /// + /// + public async Task> ListTopicsAsync() + { + var metadata = Instance.GetMetadata(TimeSpan.FromSeconds(10)); + return new List(metadata.Topics.Select(t => t.Topic)); + } + + /// + /// 判断Kafka主题是否存在 + /// + /// + /// + public async Task TopicExistsAsync(string topic) + { + var metadata = Instance.GetMetadata(TimeSpan.FromSeconds(10)); + return metadata.Topics.Any(t => t.Topic == topic); + } + + /// + /// 检测分区是否存在 + /// + /// + /// + /// + public Dictionary CheckPartitionsExists(string topic, int[] partitions) + { + var result = new Dictionary(); + 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)); + } + + /// + /// 检测分区是否存在 + /// + /// + /// + /// + 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); + } + + /// + /// 获取主题的分区数量 + /// + /// + /// + 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(); + } + } +} diff --git a/modules/JiShe.CollectBus.Kafka/AdminClient/IAdminClientService.cs b/modules/JiShe.CollectBus.Kafka/AdminClient/IAdminClientService.cs new file mode 100644 index 0000000..92121c5 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/AdminClient/IAdminClientService.cs @@ -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 + { + /// + /// 创建Kafka主题 + /// + /// 主题名称 + /// 主题分区数量 + /// 副本数量,不能高于Brokers数量 + /// + Task CreateTopicAsync(string topic, int numPartitions, short replicationFactor); + + /// + /// 删除Kafka主题 + /// + /// + /// + Task DeleteTopicAsync(string topic); + + /// + /// 获取Kafka主题列表 + /// + /// + Task> ListTopicsAsync(); + + /// + /// 判断Kafka主题是否存在 + /// + /// + /// + Task TopicExistsAsync(string topic); + + /// + /// 检测分区是否存在 + /// + /// + /// + /// + Dictionary CheckPartitionsExists(string topic, int[] partitions); + + /// + /// 检测分区是否存在 + /// + /// + /// + /// + bool CheckPartitionsExist(string topic, int targetPartition); + + /// + /// 获取主题的分区数量 + /// + /// + /// + int GetTopicPartitionsNum(string topic); + } +} diff --git a/modules/JiShe.CollectBus.Kafka/Attributes/KafkaSubscribeAttribute.cs b/modules/JiShe.CollectBus.Kafka/Attributes/KafkaSubscribeAttribute.cs new file mode 100644 index 0000000..c74aa2e --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/Attributes/KafkaSubscribeAttribute.cs @@ -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 + { + /// + /// 订阅的主题 + /// + public string Topic { get; set; } = null!; + + /// + /// 分区 + /// + public int Partition { get; set; } = -1; + + /// + /// 消费者组 + /// + public string GroupId { get; set; } = "default"; + + /// + /// 任务数(默认是多少个分区多少个任务) + /// 如设置订阅指定Partition则任务数始终为1 + /// + public int TaskCount { get; set; } = -1; + + /// + /// 批量处理数量 + /// + public int BatchSize { get; set; } = 100; + + /// + /// 是否启用批量处理 + /// + public bool EnableBatch { get; set; } = false; + + /// + /// 批次超时时间 + /// 格式:("00:05:00") + /// + public TimeSpan? BatchTimeout { get; set; }=null; + + + /// + /// 订阅主题 + /// + /// + public KafkaSubscribeAttribute(string topic) + { + this.Topic = topic; + } + + /// + /// 订阅主题 + /// + public KafkaSubscribeAttribute(string topic, int partition) + { + this.Topic = topic; + this.Partition = partition; + } + } +} diff --git a/modules/JiShe.CollectBus.Kafka/Attributes/TopicAttribute.cs b/modules/JiShe.CollectBus.Kafka/Attributes/TopicAttribute.cs new file mode 100644 index 0000000..4cb2fff --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/Attributes/TopicAttribute.cs @@ -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 + { + /// + /// Initializes a new instance of the class. + /// + /// The name. + public TopicAttribute(string name = "Default") + { + Name = name; + } + + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// + public string Name { get; set; } + } +} diff --git a/modules/JiShe.CollectBus.Kafka/CollectBusKafkaModule.cs b/modules/JiShe.CollectBus.Kafka/CollectBusKafkaModule.cs new file mode 100644 index 0000000..b467162 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/CollectBusKafkaModule.cs @@ -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(context.Services.GetConfiguration().GetSection(CommonConst.Kafka)); + + Configure(options => + { + configuration.GetSection(CommonConst.Kafka).Bind(options); + }); + + + // 注册Producer + context.Services.AddSingleton(); + // 注册Consumer + context.Services.AddSingleton(); + + //context.Services.AddHostedService(); + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var app = context.GetApplicationBuilder(); + + // 注册Subscriber + app.ApplicationServices.UseKafkaSubscribe(); + + // 获取程序集 + //app.UseKafkaSubscribers(Assembly.Load("JiShe.CollectBus.Application")); + } + } +} diff --git a/modules/JiShe.CollectBus.Kafka/Consumer/ConsumerService.cs b/modules/JiShe.CollectBus.Kafka/Consumer/ConsumerService.cs new file mode 100644 index 0000000..0ec5bd0 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/Consumer/ConsumerService.cs @@ -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 _logger; + private readonly IConfiguration _configuration; + private readonly ConcurrentDictionary + _consumerStore = new(); + private readonly KafkaOptionConfig _kafkaOptionConfig; + private class KafkaConsumer where TKey : notnull where TValue : class { } + + public ConsumerService(IConfiguration configuration, ILogger logger, IOptions kafkaOptionConfig) + { + _configuration = configuration; + _logger = logger; + _kafkaOptionConfig = kafkaOptionConfig.Value; + } + + #region private 私有方法 + + /// + /// 创建消费者 + /// + /// + /// + /// + private IConsumer CreateConsumer(string? groupId = null) where TKey : notnull where TValue : class + { + var config = BuildConsumerConfig(groupId); + return new ConsumerBuilder(config) + .SetValueDeserializer(new JsonSerializer()) + .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 + + /// + /// 订阅消息 + /// + /// + /// + /// + /// + /// + public async Task SubscribeAsync(string topic, Func> messageHandler, string? groupId = null) where TKey : notnull where TValue : class + { + await SubscribeAsync(new[] { topic }, messageHandler, groupId); + } + + + /// + /// 订阅消息 + /// + /// + /// + /// + /// + public async Task SubscribeAsync(string topic, Func> messageHandler, string? groupId = null) where TValue : class + { + await SubscribeAsync(new[] { topic }, messageHandler,groupId); + } + + /// + /// 订阅消息 + /// + /// + /// + /// + /// + /// + public async Task SubscribeAsync(string[] topics, Func> messageHandler, string? groupId = null) where TKey : notnull where TValue : class + { + var consumerKey = typeof(KafkaConsumer); + var cts = new CancellationTokenSource(); + + //var consumer = _consumerStore.GetOrAdd(consumerKey, _ => + //( + // CreateConsumer(groupId), + // cts + //)).Consumer as IConsumer; + var consumer = CreateConsumer(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; + } + + + + /// + /// 订阅消息 + /// + /// + /// + /// + /// + /// + public async Task SubscribeAsync(string[] topics, Func> messageHandler, string? groupId) where TValue : class + { + try { + var consumerKey = typeof(KafkaConsumer); + var cts = new CancellationTokenSource(); + //if (topics.Contains(ProtocolConst.SubscriberLoginReceivedEventName)) + //{ + // string ssss = ""; + //} + //var consumer = _consumerStore.GetOrAdd(consumerKey, _ => + //( + // CreateConsumer(groupId), + // cts + //)).Consumer as IConsumer; + + var consumer = CreateConsumer(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; + } + + + /// + /// 批量订阅消息 + /// + /// 消息Key类型 + /// 消息Value类型 + /// 主题 + /// 批量消息处理函数 + /// 消费组ID + /// 批次大小 + /// 批次超时时间 + public async Task SubscribeBatchAsync(string topic, Func, Task> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null) where TKey : notnull where TValue : class + { + await SubscribeBatchAsync(new[] { topic }, messageBatchHandler, groupId, batchSize, batchTimeout); + } + + /// + /// 批量订阅消息 + /// + /// 消息Key类型 + /// 消息Value类型 + /// 主题列表 + /// 批量消息处理函数 + /// 消费组ID + /// 批次大小 + /// 批次超时时间 + public async Task SubscribeBatchAsync(string[] topics,Func, Task> messageBatchHandler, string? groupId = null,int batchSize = 100, TimeSpan? batchTimeout = null) where TKey : notnull where TValue : class + { + var consumerKey = typeof(KafkaConsumer); + var cts = new CancellationTokenSource(); + + var consumer = _consumerStore.GetOrAdd(consumerKey, _ => + ( + CreateConsumer(groupId), + cts + )).Consumer as IConsumer; + + 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(); + 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; + } + + + /// + /// 批量订阅消息 + /// + /// 消息Value类型 + /// 主题列表 + /// 批量消息处理函数 + /// 消费组ID + /// 批次大小 + /// 批次超时时间 + /// 消费等待时间 + public async Task SubscribeBatchAsync(string topic, Func, Task> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null, TimeSpan? consumeTimeout = null) where TValue : class + { + await SubscribeBatchAsync(new[] { topic }, messageBatchHandler, groupId, batchSize, batchTimeout, consumeTimeout); + + } + + + /// + /// 批量订阅消息 + /// + /// 消息Value类型 + /// 主题列表 + /// 批量消息处理函数 + /// 消费组ID + /// 批次大小 + /// 批次超时时间 + /// 消费等待时间 + public async Task SubscribeBatchAsync(string[] topics,Func, Task> messageBatchHandler, string? groupId = null, int batchSize = 100,TimeSpan? batchTimeout = null,TimeSpan? consumeTimeout = null)where TValue : class + { + var consumerKey = typeof(KafkaConsumer); + var cts = new CancellationTokenSource(); + + var consumer = _consumerStore.GetOrAdd(consumerKey, _ => + ( + CreateConsumer(groupId), + cts + )).Consumer as IConsumer; + + 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>(); + 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(); + 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; + } + + + /// + /// 取消消息订阅 + /// + /// + /// + public void Unsubscribe() 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(); + } + } + + /// + /// 释放资源 + /// + public void Dispose() + { + foreach (var entry in _consumerStore.Values) + { + entry.CTS.Cancel(); + (entry.Consumer as IDisposable)?.Dispose(); + entry.CTS.Dispose(); + } + _consumerStore.Clear(); + } + } +} diff --git a/modules/JiShe.CollectBus.Kafka/Consumer/IConsumerService.cs b/modules/JiShe.CollectBus.Kafka/Consumer/IConsumerService.cs new file mode 100644 index 0000000..d86dba8 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/Consumer/IConsumerService.cs @@ -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(string topic, Func> messageHandler, string? groupId=null) where TKey : notnull where TValue : class; + + /// + /// 订阅消息 + /// + /// + /// + /// + /// + Task SubscribeAsync(string topic, Func> messageHandler, string? groupId = null) where TValue : class; + + Task SubscribeAsync(string[] topics, Func> messageHandler, string? groupId) where TKey : notnull where TValue : class; + + + /// + /// 订阅消息 + /// + /// + /// + /// + /// + /// + Task SubscribeAsync(string[] topics, Func> messageHandler, string? groupId = null) where TValue : class; + + Task SubscribeBatchAsync(string[] topics, Func, Task> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null) where TKey : notnull where TValue : class; + + Task SubscribeBatchAsync(string topic, Func, Task> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null) where TKey : notnull where TValue : class; + + Task SubscribeBatchAsync(string topic, Func, Task> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null, TimeSpan? consumeTimeout = null) where TValue : class; + + Task SubscribeBatchAsync(string[] topics, Func, Task> messageBatchHandler, string? groupId = null, int batchSize = 100, TimeSpan? batchTimeout = null, TimeSpan? consumeTimeout = null) where TValue : class; + + void Unsubscribe() where TKey : notnull where TValue : class; + } +} diff --git a/modules/JiShe.CollectBus.Kafka/HeadersFilter.cs b/modules/JiShe.CollectBus.Kafka/HeadersFilter.cs new file mode 100644 index 0000000..0790f9f --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/HeadersFilter.cs @@ -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 +{ + /// + /// 消息头过滤器 + /// + public class HeadersFilter : Dictionary + { + /// + /// 判断Headers是否匹配 + /// + /// + /// + 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; + } + } +} diff --git a/modules/JiShe.CollectBus.Kafka/HostedService.cs b/modules/JiShe.CollectBus.Kafka/HostedService.cs new file mode 100644 index 0000000..c2e672c --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/HostedService.cs @@ -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 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() + { + + } + } +} diff --git a/modules/JiShe.CollectBus.Kafka/IKafkaSubscribe.cs b/modules/JiShe.CollectBus.Kafka/IKafkaSubscribe.cs new file mode 100644 index 0000000..39e5789 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/IKafkaSubscribe.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Kafka +{ + /// + /// Kafka订阅者 + /// + /// 订阅者需要继承此接口并需要依赖注入,并使用标记 + /// + /// + public interface IKafkaSubscribe + { + } +} diff --git a/modules/JiShe.CollectBus.Kafka/ISubscribeAck.cs b/modules/JiShe.CollectBus.Kafka/ISubscribeAck.cs new file mode 100644 index 0000000..ffb30ef --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/ISubscribeAck.cs @@ -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 + { + /// + /// 是否成功标记 + /// + bool Ack { get; set; } + + /// + /// 消息 + /// + string? Msg { get; set; } + } +} diff --git a/modules/JiShe.CollectBus.Kafka/JiShe.CollectBus.Kafka.csproj b/modules/JiShe.CollectBus.Kafka/JiShe.CollectBus.Kafka.csproj new file mode 100644 index 0000000..ce31120 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/JiShe.CollectBus.Kafka.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/modules/JiShe.CollectBus.Kafka/JsonSerializer.cs b/modules/JiShe.CollectBus.Kafka/JsonSerializer.cs new file mode 100644 index 0000000..83f58a3 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/JsonSerializer.cs @@ -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 +{ + /// + /// JSON 序列化器(支持泛型) + /// + public class JsonSerializer : ISerializer, IDeserializer + { + 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 data, bool isNull, SerializationContext context) + { + if (isNull) + return default; + + try + { + return JsonSerializer.Deserialize(data, _options); + } + catch (Exception ex) + { + throw new InvalidOperationException("Kafka反序列化失败", ex); + } + } + } + + + public class DateTimeJsonConverter : JsonConverter + { + 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)); + } + } +} diff --git a/modules/JiShe.CollectBus.Kafka/KafkaOptionConfig.cs b/modules/JiShe.CollectBus.Kafka/KafkaOptionConfig.cs new file mode 100644 index 0000000..e592ea2 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/KafkaOptionConfig.cs @@ -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 + { + /// + /// kafka地址 + /// + public string BootstrapServers { get; set; } = null!; + + /// + /// 服务器标识 + /// + public string ServerTagName { get; set; }= "KafkaFilterKey"; + + /// + /// kafka主题副本数量 + /// + public short KafkaReplicationFactor { get; set; } + + /// + /// kafka主题分区数量 + /// + public int NumPartitions { get; set; } + + /// + /// 是否开启过滤器 + /// + public bool EnableFilter { get; set; }= true; + + /// + /// 是否开启认证 + /// + public bool EnableAuthorization { get; set; } = false; + + /// + /// 安全协议 + /// + public SecurityProtocol SecurityProtocol { get; set; } = SecurityProtocol.SaslPlaintext; + + /// + /// 认证方式 + /// + public SaslMechanism SaslMechanism { get; set; }= SaslMechanism.Plain; + + /// + /// 用户名 + /// + public string? SaslUserName { get; set; } + + /// + /// 密码 + /// + public string? SaslPassword { get; set; } + + } +} diff --git a/modules/JiShe.CollectBus.Kafka/KafkaSubcribesExtensions.cs b/modules/JiShe.CollectBus.Kafka/KafkaSubcribesExtensions.cs new file mode 100644 index 0000000..ff60130 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/KafkaSubcribesExtensions.cs @@ -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 + { + /// + /// 添加Kafka订阅 + /// + /// + /// + public static void UseKafkaSubscribe(this IServiceProvider provider) + { + var lifetime = provider.GetRequiredService(); + + //初始化主题信息 + var kafkaAdminClient = provider.GetRequiredService(); + var kafkaOptions = provider.GetRequiredService>(); + + List 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>(); + 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 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(); + //初始化主题信息 + var kafkaAdminClient = provider.GetRequiredService(); + var kafkaOptions = provider.GetRequiredService>(); + + List 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>(); + 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 tuple = BuildKafkaSubscribe(subscribe, provider, logger, kafkaOptions.Value); + threadCount += tuple.Item1; + topicCount += tuple.Item2; + } + }); + } + logger.LogInformation($"kafka订阅主题:{topicCount}数,共启动:{threadCount}线程"); + }); + } + + /// + /// 构建Kafka订阅 + /// + /// + /// + private static Tuple BuildKafkaSubscribe(object subscribe, IServiceProvider provider,ILogger logger, KafkaOptionConfig kafkaOptionConfig) + { + var subscribedMethods = subscribe.GetType().GetMethods() + .Select(m => new { Method = m, Attribute = m.GetCustomAttribute() }) + .Where(x => x.Attribute != null) + .ToArray(); + //var configuration = provider.GetRequiredService(); + int threadCount = 0; + + foreach (var sub in subscribedMethods) + { + int partitionCount = 3;// kafkaOptionConfig.NumPartitions; +#if DEBUG + var adminClientService = provider.GetRequiredService(); + 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); + } + + /// + /// 启动后台消费线程 + /// + /// + /// + /// + /// + /// + private static async Task StartConsumerAsync(IServiceProvider provider, KafkaSubscribeAttribute attr,MethodInfo method, object subscribe, ILogger logger) + { + var consumerService = provider.GetRequiredService(); + + if (attr.EnableBatch) + { + await consumerService.SubscribeBatchAsync(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(attr.Topic, async (message) => + { + try + { +#if DEBUG + logger.LogInformation($"kafka消费消息:{message}"); +#endif + // 处理消息 + return await ProcessMessageAsync(new List() { message }, method, subscribe); + } + catch (ConsumeException ex) + { + // 处理消费错误 + logger.LogError($"kafka消费异常:{ex.Message}"); + } + return await Task.FromResult(false); + }, attr.GroupId); + } + + } + + + /// + /// 处理消息 + /// + /// + /// + /// + /// + private static async Task ProcessMessageAsync(List 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? messageObj = null; + if (existParameters) + { + messageObj = new List(); + 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 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; + } + + } +} diff --git a/modules/JiShe.CollectBus.Kafka/Producer/IProducerService.cs b/modules/JiShe.CollectBus.Kafka/Producer/IProducerService.cs new file mode 100644 index 0000000..becea90 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/Producer/IProducerService.cs @@ -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(string topic, TKey key, TValue value) where TKey : notnull where TValue : class; + + Task ProduceAsync(string topic, TValue value) where TValue : class; + + Task ProduceAsync(string topic, TKey key, TValue value, int? partition, Action>? deliveryHandler = null) where TKey : notnull where TValue : class; + + Task ProduceAsync(string topic, TValue value, int? partition = null, Action>? deliveryHandler = null) where TValue : class; + } +} diff --git a/modules/JiShe.CollectBus.Kafka/Producer/ProducerService.cs b/modules/JiShe.CollectBus.Kafka/Producer/ProducerService.cs new file mode 100644 index 0000000..529d293 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/Producer/ProducerService.cs @@ -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 _logger; + private readonly IConfiguration _configuration; + private readonly ConcurrentDictionary _producerCache = new(); + private class KafkaProducer where TKey : notnull where TValue : class { } + private readonly KafkaOptionConfig _kafkaOptionConfig; + public ProducerService(IConfiguration configuration,ILogger logger, IOptions kafkaOptionConfig) + { + _configuration = configuration; + _logger = logger; + _kafkaOptionConfig = kafkaOptionConfig.Value; + } + + #region private 私有方法 + /// + /// 创建生产者实例 + /// + /// + /// + /// + private IProducer GetProducer(Type typeKey) + { + return (IProducer)_producerCache.GetOrAdd(typeKey, _ => + { + var config = BuildProducerConfig(); + return new ProducerBuilder(config) + .SetValueSerializer(new JsonSerializer()) // Value 使用自定义 JSON 序列化 + .SetLogHandler((_, msg) => _logger.Log(ConvertLogLevel(msg.Level), msg.Message)) + .Build(); + }); + } + + /// + /// 配置 + /// + /// + 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 + + /// + /// 发布消息 + /// + /// + /// + /// + /// + /// + /// + public async Task ProduceAsync(string topic, TKey key, TValue value)where TKey : notnull where TValue : class + { + var typeKey = typeof(KafkaProducer); + var producer = GetProducer(typeKey); + var message = new Message + { + Key = key, + Value = value, + Headers = new Headers{ + { "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) } + } + }; + await producer.ProduceAsync(topic, message); + } + + /// + /// 发布消息 + /// + /// + /// + /// + /// + public async Task ProduceAsync(string topic, TValue value) where TValue : class + { + var typeKey = typeof(KafkaProducer); + var producer = GetProducer(typeKey); + var message = new Message + { + //Key= _kafkaOptionConfig.ServerTagName, + Value = value, + Headers = new Headers{ + { "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) } + } + }; + await producer.ProduceAsync(topic, message); + } + + /// + /// 发布消息 + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task ProduceAsync(string topic,TKey key,TValue value,int? partition=null, Action>? deliveryHandler = null)where TKey : notnull where TValue : class + { + var message = new Message + { + Key = key, + Value = value, + Headers = new Headers{ + { "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) } + } + }; + var typeKey = typeof(KafkaProducer); + var producer = GetProducer(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 async Task ProduceAsync(string topic, TValue value, int? partition=null, Action>? deliveryHandler = null) where TValue : class + { + var message = new Message + { + //Key = _kafkaOptionConfig.ServerTagName, + Value = value, + Headers = new Headers{ + { "route-key", Encoding.UTF8.GetBytes(_kafkaOptionConfig.ServerTagName) } + } + }; + var typeKey = typeof(KafkaProducer); + var producer = GetProducer(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()) + { + producer.Dispose(); + } + _producerCache.Clear(); + } + } +} diff --git a/modules/JiShe.CollectBus.Kafka/SubscribeResult.cs b/modules/JiShe.CollectBus.Kafka/SubscribeResult.cs new file mode 100644 index 0000000..83eaa49 --- /dev/null +++ b/modules/JiShe.CollectBus.Kafka/SubscribeResult.cs @@ -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 + { + /// + /// 是否成功 + /// + public bool Ack { get; set; } + + /// + /// 消息 + /// + public string? Msg { get; set; } + + + /// + /// 成功 + /// + /// 消息 + public SubscribeResult Success(string? msg = null) + { + Ack = true; + Msg = msg; + return this; + } + + /// + /// 失败 + /// + /// + /// + /// + /// + public SubscribeResult Fail(string? msg = null) + { + Msg = msg; + Ack = false; + return this; + } + } + + public static partial class SubscribeAck + { + + /// + /// 成功 + /// + /// 消息 + /// + public static ISubscribeAck Success(string? msg = null) + { + return new SubscribeResult().Success(msg); + } + + + /// + /// 失败 + /// + /// 消息 + /// + public static ISubscribeAck Fail(string? msg = null) + { + return new SubscribeResult().Fail(msg); + } + } + +} diff --git a/src/JiShe.CollectBus.Application.Contracts/FodyWeavers.xml b/modules/JiShe.CollectBus.MongoDB/FodyWeavers.xml similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/FodyWeavers.xml rename to modules/JiShe.CollectBus.MongoDB/FodyWeavers.xml diff --git a/src/JiShe.CollectBus.MongoDB/JiShe.CollectBus.MongoDB.abppkg b/modules/JiShe.CollectBus.MongoDB/JiShe.CollectBus.MongoDB.abppkg similarity index 100% rename from src/JiShe.CollectBus.MongoDB/JiShe.CollectBus.MongoDB.abppkg rename to modules/JiShe.CollectBus.MongoDB/JiShe.CollectBus.MongoDB.abppkg diff --git a/src/JiShe.CollectBus.MongoDB/JiShe.CollectBus.MongoDB.csproj b/modules/JiShe.CollectBus.MongoDB/JiShe.CollectBus.MongoDB.csproj similarity index 85% rename from src/JiShe.CollectBus.MongoDB/JiShe.CollectBus.MongoDB.csproj rename to modules/JiShe.CollectBus.MongoDB/JiShe.CollectBus.MongoDB.csproj index fcb97fa..2987d33 100644 --- a/src/JiShe.CollectBus.MongoDB/JiShe.CollectBus.MongoDB.csproj +++ b/modules/JiShe.CollectBus.MongoDB/JiShe.CollectBus.MongoDB.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/JiShe.CollectBus.MongoDB/MongoDB/CollectBusDbSchemaMigrator.cs b/modules/JiShe.CollectBus.MongoDB/MongoDB/CollectBusDbSchemaMigrator.cs similarity index 100% rename from src/JiShe.CollectBus.MongoDB/MongoDB/CollectBusDbSchemaMigrator.cs rename to modules/JiShe.CollectBus.MongoDB/MongoDB/CollectBusDbSchemaMigrator.cs diff --git a/modules/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbContext.cs b/modules/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbContext.cs new file mode 100644 index 0000000..ebc5ad1 --- /dev/null +++ b/modules/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbContext.cs @@ -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 Questions => Collection(); + */ + + public IMongoCollection MessageReceiveds => Collection(); + public IMongoCollection MessageReceivedLogins => Collection(); + public IMongoCollection MessageReceivedHeartbeats => Collection(); + public IMongoCollection Devices => Collection(); + public IMongoCollection ProtocolInfos => Collection(); + + public IMongoCollection MessageIssueds => Collection(); + + + + protected override void CreateModel(IMongoModelBuilder modelBuilder) + { + //modelBuilder.Entity(builder => + //{ + // builder.CreateCollectionOptions.Collation = new Collation(locale: "en_US", strength: CollationStrength.Secondary); + // builder.ConfigureIndexes(indexes => + // { + // indexes.CreateOne( + // new CreateIndexModel( + // Builders.IndexKeys.Ascending("MyProperty"), + // new CreateIndexOptions { Unique = true } + // ) + // ); + // } + // ); + + // //// 创建索引 + // //builder.ConfigureIndexes(index => + // //{ + + + // // //List> createIndexModels = new List>(); + // // //createIndexModels.Add(new CreateIndexModel( + // // // Builders.IndexKeys.Ascending(nameof(MeterReadingRecords)), + // // // new CreateIndexOptions + // // // { + // // // Unique = true + // // // } + // // // )); + + + // // //var indexKeys = Builders.IndexKeys + // // //.Ascending("CreationTime") + // // //.Ascending("OrderNumber"); + + // // //var indexOptions = new CreateIndexOptions + // // //{ + // // // Background = true, + // // // Name = "IX_CreationTime_OrderNumber" + // // //}; + // // //index.CreateOne( + // // //new CreateIndexModel(indexKeys, indexOptions)); + + // // //index.CreateOne(new CreateIndexModel( + // // // Builders.IndexKeys.Ascending(nameof(MeterReadingRecords)), + // // // new CreateIndexOptions + // // // { + // // // Unique = true + // // // } + // // // )); + // //}); + + //}); + + base.CreateModel(modelBuilder); + modelBuilder.ConfigureCollectBus(); + } +} diff --git a/src/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbContextExtensions.cs b/modules/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbContextExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbContextExtensions.cs rename to modules/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbContextExtensions.cs diff --git a/src/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbModule.cs b/modules/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbModule.cs similarity index 51% rename from src/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbModule.cs rename to modules/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbModule.cs index 37f9377..f427d19 100644 --- a/src/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbModule.cs +++ b/modules/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbModule.cs @@ -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.BackgroundJobs.MongoDB; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Repositories.MongoDB; using Volo.Abp.Modularity; using Volo.Abp.MongoDB; using Volo.Abp.Uow; @@ -19,7 +28,15 @@ public class CollectBusMongoDbModule : AbpModule { context.Services.AddMongoDbContext(options => { - options.AddDefaultRepositories(); + options.AddDefaultRepositories(includeAllEntities: true); + + // 注册分表策略 + context.Services.AddTransient( + typeof(IShardingStrategy<>), + typeof(DayShardingStrategy<>)); + + //// 分表策略仓储 替换默认仓储 + //options.AddRepository(); }); context.Services.AddAlwaysDisableUnitOfWorkTransaction(); diff --git a/src/JiShe.CollectBus.MongoDB/MongoDB/ICollectBusMongoDbContext.cs b/modules/JiShe.CollectBus.MongoDB/MongoDB/ICollectBusMongoDbContext.cs similarity index 100% rename from src/JiShe.CollectBus.MongoDB/MongoDB/ICollectBusMongoDbContext.cs rename to modules/JiShe.CollectBus.MongoDB/MongoDB/ICollectBusMongoDbContext.cs diff --git a/modules/JiShe.CollectBus.MongoDB/Repository/MeterReadingRecord/IMeterReadingRecordRepository.cs b/modules/JiShe.CollectBus.MongoDB/Repository/MeterReadingRecord/IMeterReadingRecordRepository.cs new file mode 100644 index 0000000..20b7809 --- /dev/null +++ b/modules/JiShe.CollectBus.MongoDB/Repository/MeterReadingRecord/IMeterReadingRecordRepository.cs @@ -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 +{ + /// + /// 抄读仓储接口 + /// + public interface IMeterReadingRecordRepository : IRepository + { + /// + /// 批量插入 + /// + /// + /// + /// + Task InsertManyAsync(List entities, + DateTime? dateTime); + + /// + /// 单个插入 + /// + /// + /// + /// + Task InsertAsync(MeterReadingRecords entity, DateTime? dateTime); + + /// + /// 单条更新 + /// + /// 过滤条件,示例:Builders.Filter.Eq(x => x.Id, filter.Id) + /// 包含待更新的内容,示例:Builders.Update.Set(x => x.Processed, true).Set(x => x.ProcessedTime, Clock.Now) + /// 数据实体,用于获取对应的分片库 + /// + Task UpdateOneAsync(FilterDefinition filter, UpdateDefinition update, MeterReadingRecords entity); + + /// + /// 单个获取 + /// + /// + /// + /// + Task FirOrDefaultAsync(MeterReadingRecords entity, DateTime? dateTime); + + /// + /// 多集合数据查询 + /// + /// + /// + /// + Task> ParallelQueryAsync(DateTime startTime, DateTime endTime); + } +} diff --git a/modules/JiShe.CollectBus.MongoDB/Repository/MeterReadingRecord/MeterReadingRecordRepository.cs b/modules/JiShe.CollectBus.MongoDB/Repository/MeterReadingRecord/MeterReadingRecordRepository.cs new file mode 100644 index 0000000..cb35b5c --- /dev/null +++ b/modules/JiShe.CollectBus.MongoDB/Repository/MeterReadingRecord/MeterReadingRecordRepository.cs @@ -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 +{ + /// + /// 抄读记录仓储 + /// + public class MeterReadingRecordRepository : MongoDbRepository, IMeterReadingRecordRepository + { + + private readonly IShardingStrategy _shardingStrategy; + private readonly IMongoDbContextProvider _dbContextProvider; + + public MeterReadingRecordRepository( + IMongoDbContextProvider dbContextProvider, + IShardingStrategy shardingStrategy + ) + : base(dbContextProvider) + { + _dbContextProvider = dbContextProvider; + _shardingStrategy = shardingStrategy; + } + + /// + /// 批量插入 + /// + /// + /// + /// + public override async Task> InsertManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default(CancellationToken)) + { + var collection = await GetShardedCollection(DateTime.Now); + await collection.InsertManyAsync(entities); + + return entities; + } + + /// + /// 批量插入 + /// + /// + /// + /// + public async Task InsertManyAsync(List entities, DateTime? dateTime) + { + var collection = await GetShardedCollection(dateTime); + await collection.InsertManyAsync(entities); + } + + + /// + /// 单条插入 + /// + /// + /// + /// + public override async Task InsertAsync(MeterReadingRecords entity, bool autoSave = false, CancellationToken cancellationToken = default(CancellationToken)) + { + var collection = await GetShardedCollection(DateTime.Now); + await collection.InsertOneAsync(entity); + return entity; + } + + + /// + /// 单条插入 + /// + /// + /// + /// + public async Task InsertAsync(MeterReadingRecords entity, DateTime? dateTime) + { + var collection = await GetShardedCollection(dateTime); + await collection.InsertOneAsync(entity); + return entity; + } + + /// + /// 单条更新 + /// + /// 过滤条件,示例:Builders.Filter.Eq(x => x.Id, filter.Id) + /// 包含待更新的内容,示例:Builders.Update.Set(x => x.Processed, true).Set(x => x.ProcessedTime, Clock.Now) + /// 数据实体,用于获取对应的分片库 + /// + public async Task UpdateOneAsync(FilterDefinition filter, UpdateDefinition update, MeterReadingRecords entity) + { + var collection = await GetShardedCollection(entity.CreationTime); + + await collection.UpdateOneAsync(filter, update); + return entity; + } + + + /// + /// 单个获取 + /// + /// + /// + /// + /// + public async Task 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(); + } + + /// + /// 多集合数据查询 + /// + /// + /// + /// + public async Task> 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(name); + var filter = Builders.Filter.And( + Builders.Filter.Gte(x => x.CreationTime, startTime), + Builders.Filter.Lte(x => x.CreationTime, endTime) + ); + return await collection.Find(filter).ToListAsync(); + }); + + var results = await Task.WhenAll(tasks); + return results.SelectMany(r => r).ToList(); + } + + /// + /// 获得分片集合 + /// + /// + private async Task> 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(collectionName); + } + + } +} diff --git a/modules/JiShe.CollectBus.MongoDB/ShardingStrategy/DayShardingStrategy.cs b/modules/JiShe.CollectBus.MongoDB/ShardingStrategy/DayShardingStrategy.cs new file mode 100644 index 0000000..f26136d --- /dev/null +++ b/modules/JiShe.CollectBus.MongoDB/ShardingStrategy/DayShardingStrategy.cs @@ -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 +{ + /// + /// 按天分表策略 + /// + /// + public class DayShardingStrategy : IShardingStrategy + { + /// + /// 获取指定时间对应的集合名 + /// + /// + /// + public string GetCollectionName(DateTime dateTime) + { + var baseName = typeof(TEntity).Name; + return $"{baseName}_{dateTime.GetDataTableShardingStrategy()}"; + } + + /// + /// 获取当前时间对应的集合名 + /// + /// + public string GetCurrentCollectionName() + { + var baseName = typeof(TEntity).Name; + return $"{baseName}_{DateTime.Now.GetDataTableShardingStrategy()}"; + } + + /// + /// 用于查询时确定目标集合 + /// + /// + /// + /// + public IEnumerable GetQueryCollectionNames(DateTime? startTime, DateTime? endTime) + { + var months = new List(); + 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(); + } + } +} diff --git a/modules/JiShe.CollectBus.MongoDB/ShardingStrategy/IShardingStrategy.cs b/modules/JiShe.CollectBus.MongoDB/ShardingStrategy/IShardingStrategy.cs new file mode 100644 index 0000000..151d5df --- /dev/null +++ b/modules/JiShe.CollectBus.MongoDB/ShardingStrategy/IShardingStrategy.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.ShardingStrategy +{ + /// + /// 数据存储分片策略 + /// + /// + public interface IShardingStrategy + { + /// + /// 获取指定时间对应的集合名 + /// + /// + string GetCollectionName(DateTime dateTime); + + /// + /// 获取当前时间对应的集合名 + /// + /// + string GetCurrentCollectionName(); + + /// + /// 用于查询时确定目标集合 + /// + /// + /// + /// + IEnumerable GetQueryCollectionNames(DateTime? startTime = null, + DateTime? endTime = null); + } +} diff --git a/src/JiShe.CollectBus.Protocol.Contracts/Abstracts/BaseProtocolPlugin.cs b/protocols/JiShe.CollectBus.Protocol.Contracts/Abstracts/BaseProtocolPlugin.cs similarity index 80% rename from src/JiShe.CollectBus.Protocol.Contracts/Abstracts/BaseProtocolPlugin.cs rename to protocols/JiShe.CollectBus.Protocol.Contracts/Abstracts/BaseProtocolPlugin.cs index 6f504f5..bc066fe 100644 --- a/src/JiShe.CollectBus.Protocol.Contracts/Abstracts/BaseProtocolPlugin.cs +++ b/protocols/JiShe.CollectBus.Protocol.Contracts/Abstracts/BaseProtocolPlugin.cs @@ -1,23 +1,26 @@ -using System.Globalization; -using DotNetCore.CAP; -using JiShe.CollectBus.Common.Enums; +using JiShe.CollectBus.Common.Enums; using JiShe.CollectBus.Common.Extensions; using JiShe.CollectBus.Common.Models; -using JiShe.CollectBus.MessageReceiveds; using JiShe.CollectBus.Protocol.Contracts.Interfaces; -using JiShe.CollectBus.Protocols; using Microsoft.Extensions.Logging; using JiShe.CollectBus.Protocol.Contracts.Models; using Volo.Abp.Domain.Repositories; using JiShe.CollectBus.Common.BuildSendDatas; using JiShe.CollectBus.Protocol.Contracts.AnalysisData; 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 { public abstract class BaseProtocolPlugin : IProtocolPlugin { - private readonly ICapPublisher _capBus; + private readonly ICapPublisher _producerBus; + private readonly IProducerService _producerService; private readonly ILogger _logger; private readonly IRepository _protocolInfoRepository; @@ -37,7 +40,8 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts _logger = serviceProvider.GetRequiredService>(); _protocolInfoRepository = serviceProvider.GetRequiredService>(); - _capBus = serviceProvider.GetRequiredService(); + _producerService = serviceProvider.GetRequiredService(); + _producerBus = serviceProvider.GetRequiredService(); } public abstract ProtocolInfo Info { get; } @@ -56,7 +60,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts //await _protocolInfoCache.Get() } - public abstract Task AnalyzeAsync(MessageReceived messageReceived, Action? sendAction = null); + public abstract Task AnalyzeAsync(MessageReceived messageReceived, Action? sendAction = null) where T : TB3761; /// /// 登录帧解析 @@ -86,8 +90,10 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts Fn = 1 }; 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 }); } /// @@ -125,7 +131,11 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts Fn = 1 }; 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 /// public virtual List AnalyzeAmmeterParameterReadingDataAsync(MessageReceived messageReceived, Action? sendAction = null) { - var hexDatas = GetHexDatas(messageReceived.MessageHexString); + var hexData = GetHexData(messageReceived.MessageHexString); var meterList = new List(); - var count = (hexDatas[1] + hexDatas[0]).HexToDec(); + var count = (hexData[1] + hexData[0]).HexToDec(); //if (2 + count * 27 != hexDatas.Count - pWLen - tPLen - 2) // return; var index = 2;//数量 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; - var pn = $"{hexDatas[index + 1]}{hexDatas[index]}".HexToDec(); + var pn = $"{hexData[index + 1]}{hexData[index]}".HexToDec(); 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 port = baudRateAndPortBin.Substring(3, 5).BinToDec(); index += 1; - var protocolType = (CommunicationProtocolType)hexDatas[index].HexToDec(); + var protocolType = (CommunicationProtocolType)hexData[index].HexToDec(); index += 1; - var addressHexList = hexDatas.Skip(index).Take(6).ToList(); + var addressHexList = hexData.Skip(index).Take(6).ToList(); addressHexList.Reverse(); var address = string.Join("", addressHexList); index += 6; - var pwdHexList = hexDatas.Skip(index).Take(6).ToList(); + var pwdHexList = hexData.Skip(index).Take(6).ToList(); pwdHexList.Reverse(); var password = string.Join("", pwdHexList.Take(3).ToList()); index += 6; - var rateNumberBin = hexDatas[index].HexToBin().PadLeft(8, '0'); + var rateNumberBin = hexData[index].HexToBin().PadLeft(8, '0'); var rateNumber = rateNumberBin.Substring(4).BinToDec(); 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 decBitNumber = intBitAndDecBitNumberBin.Substring(6, 2).BinToDec() + 1; index += 1; // hexDatas.GetRange() - var collectorAddressHexList = hexDatas.Skip(index).Take(6).ToList(); + var collectorAddressHexList = hexData.Skip(index).Take(6).ToList(); collectorAddressHexList.Reverse(); var collectorAddress = string.Join("", collectorAddressHexList); 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 userSubClass = userClassNumberBin.Substring(4, 4).BinToDec(); index += 1; @@ -234,24 +244,24 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts /// public virtual CurrentPositiveActiveEnergyAnalyze AnalyzeActivePowerIndicationReadingDataAsync(MessageReceived messageReceived, Action? sendAction = null) { - var hexDatas = GetHexDatas(messageReceived.MessageHexString); + var hexData = GetHexData(messageReceived.MessageHexString); - var minute = Convert.ToInt32(hexDatas[0]); // 获取当前分钟数 - var hour = Convert.ToInt32(hexDatas[1]); // 获取当前小时数 - var day = Convert.ToInt32(hexDatas[2]); // 获取当前日期的日数 - var month = Convert.ToInt32(hexDatas[3]); // 获取当前月份 - var year = Convert.ToInt32(hexDatas[4]); // 获取当前日期的年份 + var minute = Convert.ToInt32(hexData[0]); // 获取当前分钟数 + var hour = Convert.ToInt32(hexData[1]); // 获取当前小时数 + var day = Convert.ToInt32(hexData[2]); // 获取当前日期的日数 + var month = Convert.ToInt32(hexData[3]); // 获取当前月份 + var year = Convert.ToInt32(hexData[4]); // 获取当前日期的年份 var dateTime = new DateTime(year, month, day, hour, minute, 0); // 转换为本地时间 var localDateTime = dateTime.ToLocalTime(); - var rateNumber = Convert.ToInt32(hexDatas[5]); - var kwhTotal = hexDatas.Skip(5).Take(5).ToList(); + var rateNumber = Convert.ToInt32(hexData[5]); + var kwhTotal = hexData.Skip(5).Take(5).ToList(); var kwhList = new List(); var index = 11; 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(); var integerStr = $"{kwhHexList.Take(0)}{kwhHexList.Take(1)}{kwhHexList.Take(2)}"; var decimalValStr = $"{kwhHexList[3]}{kwhHexList[4]}"; @@ -280,19 +290,19 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts /// public virtual void AnalyzeDailyFrozenReadingDataAsync(MessageReceived messageReceived, Action? sendAction = null) { - var hexDatas = GetHexDatas(messageReceived.MessageHexString); + var hexData = GetHexData(messageReceived.MessageHexString); //附录A.20 日月年 - var td_dHex = hexDatas.Take(3).ToList(); + var td_dHex = hexData.Take(3).ToList(); //附录A.15 分时日月年 - var readingTimeHex = hexDatas.Skip(3).Take(5).ToList(); - var rateNumberHex = hexDatas.Skip(8).Take(1).FirstOrDefault().HexToDec(); + var readingTimeHex = hexData.Skip(3).Take(5).ToList(); + var rateNumberHex = hexData.Skip(8).Take(1).FirstOrDefault().HexToDec(); var datas = new List(); //附录A.14 kWh 5字节 for (int i = 0; i < rateNumberHex; i++) { 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]); datas.Add(data); } @@ -344,83 +354,83 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts /// //F25ReadingAnalyze public virtual Analyze3761Data AnalyzeF25ReadingDataAsync(MessageReceived messageReceived, Action? sendAction = null) { - var hexDatas = GetHexDatas(messageReceived.MessageHexString); + var hexData = GetHexData(messageReceived.MessageHexString); //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]); //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 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 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 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 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 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 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 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]); //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 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 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 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]); //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 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 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]); //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 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 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 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]); //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 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 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 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]); return new Analyze3761Data() @@ -497,17 +507,17 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts /// //TerminalVersionInfoAnalyze public virtual Analyze3761Data AnalyzeTerminalVersionInfoReadingDataAsync(MessageReceived messageReceived, Action? 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 deviceNo = string.Join("", hexDatas.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 softwareReleaseDateList = hexDatas.Skip((int)TerminalVersionInfoEnum.SoftwareReleaseDate).Take(3).ToList(); + var makerNo = string.Join("",hexData.Take(4).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("", hexData.Skip((int)TerminalVersionInfoEnum.SoftwareVersionNo).Take(4).Select(s => (char)s.HexToDec()));//软件版本号 + var softwareReleaseDateList = hexData.Skip((int)TerminalVersionInfoEnum.SoftwareReleaseDate).Take(3).ToList(); 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 protocolVersionNo = string.Join("", hexDatas.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 hardwareReleaseDateList = hexDatas.Skip((int)TerminalVersionInfoEnum.HardwareReleaseDate).Take(3).ToList(); + var capacityInformationCode = string.Join("", hexData.Skip((int)TerminalVersionInfoEnum.CapacityInformationCode).Take(11).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("", hexData.Skip((int)TerminalVersionInfoEnum.HardwareVersionNo).Take(4).Select(s => (char)s.HexToDec()));//硬件版本号 + var hardwareReleaseDateList = hexData.Skip((int)TerminalVersionInfoEnum.HardwareReleaseDate).Take(3).ToList(); var hardwareReleaseDate = $"20{AnalyzeDataAccordingToA20(hardwareReleaseDateList[0], hardwareReleaseDateList[1], hardwareReleaseDateList[2])}";//软件发布日期 return new Analyze3761Data() @@ -548,24 +558,24 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts /// public virtual Analyze3761Data AnalyzeATypeOfDataItems49ReadingDataAsync(MessageReceived messageReceived, Action? 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 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 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 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 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 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]); return new Analyze3761Data() @@ -573,15 +583,15 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts AFN = 12, FN = 49, Text = "相位角", - DataUpChilds = new List() - { - new Analyze3761DataUpChild(1,"UabUa相位角",uabUa.ToString(),1), - new Analyze3761DataUpChild(2,"Ub相位角",ub.ToString(),2), - new Analyze3761DataUpChild(3,"UcbUc相位角",ucbUc.ToString(),3), - new Analyze3761DataUpChild(4,"Ia相位角",ia.ToString(),4), - new Analyze3761DataUpChild(5,"Ib相位角",ib.ToString(),5), - new Analyze3761DataUpChild(6,"Ic相位角",ic.ToString(),6), - } + DataUpChilds = + [ + new Analyze3761DataUpChild(1, "UabUa相位角", uabUa.ToString(), 1), + new Analyze3761DataUpChild(2, "Ub相位角", ub.ToString(), 2), + new Analyze3761DataUpChild(3, "UcbUc相位角", ucbUc.ToString(), 3), + new Analyze3761DataUpChild(4, "Ia相位角", ia.ToString(), 4), + new Analyze3761DataUpChild(5, "Ib相位角", ib.ToString(), 5), + new Analyze3761DataUpChild(6, "Ic相位角", ic.ToString(), 6) + ] }; } @@ -593,7 +603,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts public virtual void AnalyzeTerminalTimeReadingDataAsync(MessageReceived messageReceived, Action? sendAction = null) { - var hexDatas = GetHexDatas(messageReceived.MessageHexString); + var hexDatas = GetHexData(messageReceived.MessageHexString); var time = Appendix.Appendix_A1(hexDatas.Take(6).ToList()); } @@ -603,7 +613,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts /// /// /// - public virtual TB3761FN AnalyzeReadingDataAsync(MessageReceived messageReceived, + public virtual TB3761 AnalyzeReadingDataAsync(MessageReceived messageReceived, Action? sendAction = null) { var hexStringList = messageReceived.MessageHexString.StringToPairs(); @@ -616,14 +626,14 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts var tb3761Fn = tb3761.FnList.FirstOrDefault(it => it.Fn == fn); if (tb3761Fn == null) return null; - var hexDatas = (List)hexStringList.GetAnalyzeValue(CommandChunkEnum.Data); + var analyzeValue = (List)hexStringList.GetAnalyzeValue(CommandChunkEnum.Data); var m = 0; var rateNumberUpSort = -1; var rateNumberUp = tb3761Fn.UpList.FirstOrDefault(it => it.Name.Contains("费率数M")); 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); rateNumberUpSort = rateNumberUp.Sort; } @@ -640,7 +650,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts dataIndex = sum1 + sum2; } - var value = AnalyzeDataAccordingDataType(hexDatas, dataIndex, up.DataCount, up.DataType); + var value = AnalyzeDataAccordingDataType(analyzeValue, dataIndex, up.DataCount, up.DataType); if (value != null) { up.Value = value.ToString(); @@ -652,7 +662,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts { 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) { upChild.Name = string.Format(upChild.Name, j + 1); @@ -665,7 +675,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts } } - return tb3761Fn; + return tb3761; } /// @@ -674,7 +684,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts /// /// /// - public virtual TB3761FN AnalyzeReadingTdcDataAsync(MessageReceived messageReceived, + public virtual TB3761 AnalyzeReadingTdcDataAsync(MessageReceived messageReceived, Action? sendAction = null) { @@ -688,11 +698,11 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts var tb3761Fn = tb3761.FnList.FirstOrDefault(it => it.Fn == fn); if (tb3761Fn == null) return null; - var hexDatas = (List)hexStringList.GetAnalyzeValue(CommandChunkEnum.Data); + var analyzeValue = (List)hexStringList.GetAnalyzeValue(CommandChunkEnum.Data); 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) { up.Value = value.ToString(); @@ -705,7 +715,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts { 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) { 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 addMinute = 0; //switch (freezeDensity) @@ -741,9 +751,9 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts // } } - private object? AnalyzeDataAccordingDataType(List hexDatas, int dataIndex,int dataCount,string dataType) + private object? AnalyzeDataAccordingDataType(List 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; switch (dataType) { @@ -771,11 +781,11 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts case "A15": if (valueList.Count == 5) { - //var minutes = Convert.ToInt32(hexDatas[0]); // 获取当前分钟数 - //var hours = Convert.ToInt32(hexDatas[1]); // 获取当前小时数 - //var day = Convert.ToInt32(hexDatas[2]); // 获取当前日期的日数 - //var month = Convert.ToInt32(hexDatas[3]); // 获取当前月份 - //var year = Convert.ToInt32(hexDatas[4]); // 获取当前日期的年份 + //var minutes = Convert.ToInt32(analyzeValue[0]); // 获取当前分钟数 + //var hours = Convert.ToInt32(analyzeValue[1]); // 获取当前小时数 + //var day = Convert.ToInt32(analyzeValue[2]); // 获取当前日期的日数 + //var month = Convert.ToInt32(analyzeValue[3]); // 获取当前月份 + //var year = Convert.ToInt32(analyzeValue[4]); // 获取当前日期的年份 value = AnalyzeDataAccordingToA15(valueList[0], valueList[1], valueList[2], valueList[3], valueList[4]); } break; @@ -801,7 +811,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts /// public virtual async Task AnalyzeTransparentForwardingAnswerAsync(MessageReceived messageReceivedEvent, Action? sendAction = null) { - var hexDatas = GetHexDatas(messageReceivedEvent.MessageHexString); + var hexDatas = GetHexData(messageReceivedEvent.MessageHexString); var port = hexDatas[0].HexToDec(); @@ -828,7 +838,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts /// public virtual async Task AnalyzeTransparentForwardingAnswerResultAsync(MessageReceived messageReceived, Action? sendAction = null) { - var hexDatas = GetHexDatas(messageReceived.MessageHexString); + var hexDatas = GetHexData(messageReceived.MessageHexString); var port = hexDatas[0].HexToDec(); @@ -844,25 +854,25 @@ namespace JiShe.CollectBus.Protocol.Contracts.Abstracts /// /// /// - public static List GetHexDatas(string messageHexString) + public static List GetHexData(string messageHexString) { var hexStringList = messageHexString.StringToPairs(); - var hexDatas = (List)hexStringList.GetAnalyzeValue(CommandChunkEnum.Data); - return hexDatas; + var analyzeValue = (List)hexStringList.GetAnalyzeValue(CommandChunkEnum.Data); + return analyzeValue; } /// /// 解析时间标签 /// - /// - public void AnalysisTp(List hexDatas) + /// + public void AnalysisTp(List hexData) { - var pFC = hexDatas[0].HexToDec();//启动帧帧序号计数器 - var seconds = Convert.ToInt32(hexDatas[1]); // 获取当前秒数 - var minutes = Convert.ToInt32(hexDatas[2]); // 获取当前分钟数 - var hours = Convert.ToInt32(hexDatas[3]); // 获取当前小时数 - var day = Convert.ToInt32(hexDatas[4]); // 获取当前日期的日数 - var delayTime = hexDatas[5].HexToDec();//延迟时间 min + var pFC = hexData[0].HexToDec();//启动帧帧序号计数器 + var seconds = Convert.ToInt32(hexData[1]); // 获取当前秒数 + var minutes = Convert.ToInt32(hexData[2]); // 获取当前分钟数 + var hours = Convert.ToInt32(hexData[3]); // 获取当前小时数 + var day = Convert.ToInt32(hexData[4]); // 获取当前日期的日数 + var delayTime = hexData[5].HexToDec();//延迟时间 min } #region 报文指定的数据格式 diff --git a/src/JiShe.CollectBus.Protocol.Contracts/Adapters/StandardFixedHeaderDataHandlingAdapter.cs b/protocols/JiShe.CollectBus.Protocol.Contracts/Adapters/StandardFixedHeaderDataHandlingAdapter.cs similarity index 100% rename from src/JiShe.CollectBus.Protocol.Contracts/Adapters/StandardFixedHeaderDataHandlingAdapter.cs rename to protocols/JiShe.CollectBus.Protocol.Contracts/Adapters/StandardFixedHeaderDataHandlingAdapter.cs diff --git a/src/JiShe.CollectBus.Protocol.Contracts/AnalysisData/Appendix.cs b/protocols/JiShe.CollectBus.Protocol.Contracts/AnalysisData/Appendix.cs similarity index 100% rename from src/JiShe.CollectBus.Protocol.Contracts/AnalysisData/Appendix.cs rename to protocols/JiShe.CollectBus.Protocol.Contracts/AnalysisData/Appendix.cs diff --git a/src/JiShe.CollectBus.Protocol.Contracts/Attributes/ProtocolNameAttribute.cs b/protocols/JiShe.CollectBus.Protocol.Contracts/Attributes/ProtocolNameAttribute.cs similarity index 100% rename from src/JiShe.CollectBus.Protocol.Contracts/Attributes/ProtocolNameAttribute.cs rename to protocols/JiShe.CollectBus.Protocol.Contracts/Attributes/ProtocolNameAttribute.cs diff --git a/src/JiShe.CollectBus.Protocol.Contracts/Interfaces/IProtocolPlugin.cs b/protocols/JiShe.CollectBus.Protocol.Contracts/Interfaces/IProtocolPlugin.cs similarity index 68% rename from src/JiShe.CollectBus.Protocol.Contracts/Interfaces/IProtocolPlugin.cs rename to protocols/JiShe.CollectBus.Protocol.Contracts/Interfaces/IProtocolPlugin.cs index 1ccc45b..2f48cd2 100644 --- a/src/JiShe.CollectBus.Protocol.Contracts/Interfaces/IProtocolPlugin.cs +++ b/protocols/JiShe.CollectBus.Protocol.Contracts/Interfaces/IProtocolPlugin.cs @@ -1,9 +1,9 @@ using System; using System.Threading.Tasks; 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.Protocols; using TouchSocket.Sockets; namespace JiShe.CollectBus.Protocol.Contracts.Interfaces @@ -14,7 +14,7 @@ namespace JiShe.CollectBus.Protocol.Contracts.Interfaces Task AddAsync(); - Task AnalyzeAsync(MessageReceived messageReceived, Action? sendAction = null); + Task AnalyzeAsync(MessageReceived messageReceived, Action? sendAction = null) where T : TB3761; Task LoginAsync(MessageReceivedLogin messageReceived); diff --git a/src/JiShe.CollectBus.Protocol.Contracts/JiShe.CollectBus.Protocol.Contracts.csproj b/protocols/JiShe.CollectBus.Protocol.Contracts/JiShe.CollectBus.Protocol.Contracts.csproj similarity index 54% rename from src/JiShe.CollectBus.Protocol.Contracts/JiShe.CollectBus.Protocol.Contracts.csproj rename to protocols/JiShe.CollectBus.Protocol.Contracts/JiShe.CollectBus.Protocol.Contracts.csproj index aa5bd73..cb60dbd 100644 --- a/src/JiShe.CollectBus.Protocol.Contracts/JiShe.CollectBus.Protocol.Contracts.csproj +++ b/protocols/JiShe.CollectBus.Protocol.Contracts/JiShe.CollectBus.Protocol.Contracts.csproj @@ -6,6 +6,12 @@ enable + + + + + + @@ -15,8 +21,9 @@ - - + + + diff --git a/src/JiShe.CollectBus.Protocol.Contracts/Models/CustomFixedHeaderRequestInfo.cs b/protocols/JiShe.CollectBus.Protocol.Contracts/Models/CustomFixedHeaderRequestInfo.cs similarity index 100% rename from src/JiShe.CollectBus.Protocol.Contracts/Models/CustomFixedHeaderRequestInfo.cs rename to protocols/JiShe.CollectBus.Protocol.Contracts/Models/CustomFixedHeaderRequestInfo.cs diff --git a/src/JiShe.CollectBus.Protocol.Contracts/Models/TB3761.cs b/protocols/JiShe.CollectBus.Protocol.Contracts/Models/TB3761.cs similarity index 100% rename from src/JiShe.CollectBus.Protocol.Contracts/Models/TB3761.cs rename to protocols/JiShe.CollectBus.Protocol.Contracts/Models/TB3761.cs diff --git a/src/JiShe.CollectBus.Protocol.Contracts/QGDW3761Config.cs b/protocols/JiShe.CollectBus.Protocol.Contracts/QGDW3761Config.cs similarity index 100% rename from src/JiShe.CollectBus.Protocol.Contracts/QGDW3761Config.cs rename to protocols/JiShe.CollectBus.Protocol.Contracts/QGDW3761Config.cs diff --git a/src/JiShe.CollectBus.Protocol/JiShe.CollectBus.Protocol.csproj b/protocols/JiShe.CollectBus.Protocol.Test/JiShe.CollectBus.Protocol.Test.csproj similarity index 69% rename from src/JiShe.CollectBus.Protocol/JiShe.CollectBus.Protocol.csproj rename to protocols/JiShe.CollectBus.Protocol.Test/JiShe.CollectBus.Protocol.Test.csproj index 0ee8a46..899ebad 100644 --- a/src/JiShe.CollectBus.Protocol/JiShe.CollectBus.Protocol.csproj +++ b/protocols/JiShe.CollectBus.Protocol.Test/JiShe.CollectBus.Protocol.Test.csproj @@ -16,13 +16,13 @@ - - + + - + diff --git a/protocols/JiShe.CollectBus.Protocol.Test/JiSheCollectBusProtocolModule.cs b/protocols/JiShe.CollectBus.Protocol.Test/JiSheCollectBusProtocolModule.cs new file mode 100644 index 0000000..4abc95b --- /dev/null +++ b/protocols/JiShe.CollectBus.Protocol.Test/JiSheCollectBusProtocolModule.cs @@ -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(nameof(TestProtocolPlugin)); + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var protocol = context.ServiceProvider.GetRequiredKeyedService(nameof(TestProtocolPlugin)); + protocol.AddAsync(); + } + } +} diff --git a/protocols/JiShe.CollectBus.Protocol.Test/TestProtocolPlugin.cs b/protocols/JiShe.CollectBus.Protocol.Test/TestProtocolPlugin.cs new file mode 100644 index 0000000..2573ab7 --- /dev/null +++ b/protocols/JiShe.CollectBus.Protocol.Test/TestProtocolPlugin.cs @@ -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 + { + /// + /// Initializes a new instance of the class. + /// + /// The service provider. + public TestProtocolPlugin(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public sealed override ProtocolInfo Info => new(nameof(TestProtocolPlugin), "Test", "TCP", "Test协议", "DTS1980-Test"); + + public override async Task AnalyzeAsync(MessageReceived messageReceived, Action? 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 帧结束标志 + + /// + /// 解析上行命令 + /// + /// + /// + 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; + } + + /// + /// 解析地址 + /// + /// + /// + private string AnalysisA(List 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 + } +} diff --git a/protocols/JiShe.CollectBus.Protocol/JiShe.CollectBus.Protocol.csproj b/protocols/JiShe.CollectBus.Protocol/JiShe.CollectBus.Protocol.csproj new file mode 100644 index 0000000..1497183 --- /dev/null +++ b/protocols/JiShe.CollectBus.Protocol/JiShe.CollectBus.Protocol.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + preview + + + + + + + + + + + + + + + + + + + + diff --git a/src/JiShe.CollectBus.Protocol/JiSheCollectBusProtocolModule.cs b/protocols/JiShe.CollectBus.Protocol/JiSheCollectBusProtocolModule.cs similarity index 80% rename from src/JiShe.CollectBus.Protocol/JiSheCollectBusProtocolModule.cs rename to protocols/JiShe.CollectBus.Protocol/JiSheCollectBusProtocolModule.cs index 64661d1..5cda0c8 100644 --- a/src/JiShe.CollectBus.Protocol/JiSheCollectBusProtocolModule.cs +++ b/protocols/JiShe.CollectBus.Protocol/JiSheCollectBusProtocolModule.cs @@ -12,10 +12,10 @@ namespace JiShe.CollectBus.Protocol context.Services.AddKeyedSingleton(nameof(StandardProtocolPlugin)); } - public override void OnApplicationInitialization(ApplicationInitializationContext context) + public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context) { var standardProtocol = context.ServiceProvider.GetRequiredKeyedService(nameof(StandardProtocolPlugin)); - standardProtocol.AddAsync(); + await standardProtocol.AddAsync(); } } } diff --git a/src/JiShe.CollectBus.Protocol/StandardProtocolPlugin.cs b/protocols/JiShe.CollectBus.Protocol/StandardProtocolPlugin.cs similarity index 81% rename from src/JiShe.CollectBus.Protocol/StandardProtocolPlugin.cs rename to protocols/JiShe.CollectBus.Protocol/StandardProtocolPlugin.cs index 8233b99..a28cd2d 100644 --- a/src/JiShe.CollectBus.Protocol/StandardProtocolPlugin.cs +++ b/protocols/JiShe.CollectBus.Protocol/StandardProtocolPlugin.cs @@ -1,15 +1,15 @@ using JiShe.CollectBus.Common.Enums; using JiShe.CollectBus.Common.Extensions; 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.Models; -using JiShe.CollectBus.Protocols; using Newtonsoft.Json.Linq; namespace JiShe.CollectBus.Protocol -{ - public class StandardProtocolPlugin: BaseProtocolPlugin +{ + public class StandardProtocolPlugin : BaseProtocolPlugin { /// /// Initializes a new instance of the class. @@ -21,28 +21,40 @@ namespace JiShe.CollectBus.Protocol public sealed override ProtocolInfo Info => new(nameof(StandardProtocolPlugin), "376.1", "TCP", "376.1协议", "DTS1980"); - public override Task AnalyzeAsync(MessageReceived messageReceived, Action? sendAction = null) + public override async Task AnalyzeAsync(MessageReceived messageReceived, Action? sendAction = null) { var hexStringList = messageReceived.MessageHexString.StringToPairs(); var aTuple = (Tuple)hexStringList.GetAnalyzeValue(CommandChunkEnum.A); var afn = (int)hexStringList.GetAnalyzeValue(CommandChunkEnum.AFN); 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) - { - AnalyzeReadingDataAsync(messageReceived, sendAction); - } - } - else if(afn == (int)AFN.请求历史数据) - { - if (Enum.IsDefined(typeof(IIdataTypeItems), fn)) - { - AnalyzeReadingTdcDataAsync(messageReceived, sendAction); - } + case AFN.确认或否认: + AnalyzeAnswerDataAsync(messageReceived, sendAction); + break; + case AFN.设置参数: break; + case AFN.查询参数: break; + case AFN.请求实时数据: + if (Enum.IsDefined(typeof(ATypeOfDataItems), fn)) + { + analyze = (T?)AnalyzeReadingDataAsync(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 上行命令 diff --git a/readme.md b/readme.md index 28a0678..57cce20 100644 --- a/readme.md +++ b/readme.md @@ -17,7 +17,7 @@ - V4 → V5核心升级: - 微服务化架构改造 - 统一配置管理中心 - - 支持Kafka/RabbitMQ双引擎 + - 支持Kafka引擎 - 新增边缘计算能力 - 资源利用率提升40% @@ -82,7 +82,7 @@ Body: |----------------|------------|--------------| | 最大连接数 | 10,000 | 线性扩展 | | 数据处理延迟 | <50ms(p99) | - | -| 吞吐量 | 20,000 TPS | 百万级TPS | +| 吞吐量 | 20,000 TPS | 十万级TPS | | CPU利用率 | ≤70%@峰值 | 自动负载均衡 | ## 6. 高可用设计 diff --git a/src/JiShe.CollectBus.Application.Contracts/BaseResultDto.cs b/services/JiShe.CollectBus.Application.Contracts/BaseResultDto.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/BaseResultDto.cs rename to services/JiShe.CollectBus.Application.Contracts/BaseResultDto.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/CollectBusApplicationContractsModule.cs b/services/JiShe.CollectBus.Application.Contracts/CollectBusApplicationContractsModule.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/CollectBusApplicationContractsModule.cs rename to services/JiShe.CollectBus.Application.Contracts/CollectBusApplicationContractsModule.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/CollectBusRemoteServiceConsts.cs b/services/JiShe.CollectBus.Application.Contracts/CollectBusRemoteServiceConsts.cs similarity index 72% rename from src/JiShe.CollectBus.Application.Contracts/CollectBusRemoteServiceConsts.cs rename to services/JiShe.CollectBus.Application.Contracts/CollectBusRemoteServiceConsts.cs index 396140d..2a7170e 100644 --- a/src/JiShe.CollectBus.Application.Contracts/CollectBusRemoteServiceConsts.cs +++ b/services/JiShe.CollectBus.Application.Contracts/CollectBusRemoteServiceConsts.cs @@ -4,5 +4,5 @@ public class CollectBusRemoteServiceConsts { public const string RemoteServiceName = "CollectBus"; - public const string ModuleName = "collectBus"; + public const string ModuleName = "collectBus"; } diff --git a/services/JiShe.CollectBus.Application.Contracts/DataMigration/IDataMigrationService.cs b/services/JiShe.CollectBus.Application.Contracts/DataMigration/IDataMigrationService.cs new file mode 100644 index 0000000..bbfa581 --- /dev/null +++ b/services/JiShe.CollectBus.Application.Contracts/DataMigration/IDataMigrationService.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.DataMigration +{ + /// + /// 数据迁移服务 + /// + public interface IDataMigrationService + { + /// + /// 开始迁移 + /// + /// + Task StartMigrationAsync(); + } +} diff --git a/services/JiShe.CollectBus.Application.Contracts/DataMigration/Options/DataMigrationOptions.cs b/services/JiShe.CollectBus.Application.Contracts/DataMigration/Options/DataMigrationOptions.cs new file mode 100644 index 0000000..7174cad --- /dev/null +++ b/services/JiShe.CollectBus.Application.Contracts/DataMigration/Options/DataMigrationOptions.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.DataMigration.Options +{ + /// + /// 数据迁移配置 + /// + public class DataMigrationOptions + { + /// + /// MongoDb每批处理量 + /// + public int MongoDbDataBatchSize { get; set; } = 1000; + + /// + /// 批量处理通道容量 + /// + public int ChannelCapacity { get; set; } = 100; + + /// + /// 数据库 每批处理量 + /// + public int SqlBulkBatchSize { get; set; } = 1000; + + /// + /// 数据库 每批处理超时时间 + /// + public int SqlBulkTimeout { get; set; } = 60; + + /// + /// 处理器数量 + /// + public int ProcessorsCount { get; set; } = 4; + } +} diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AddConrOnlineRecordInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AddConrOnlineRecordInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AddConrOnlineRecordInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AddConrOnlineRecordInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AddFocusLogInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AddFocusLogInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AddFocusLogInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AddFocusLogInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AddSignalStrengthInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AddSignalStrengthInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AddSignalStrengthInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AddSignalStrengthInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AdjustMeterTimingInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AdjustMeterTimingInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AdjustMeterTimingInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AdjustMeterTimingInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AmmeterArchivesDownInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AmmeterArchivesDownInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AmmeterArchivesDownInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AmmeterArchivesDownInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AmmeterArchivesDownOutput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AmmeterArchivesDownOutput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AmmeterArchivesDownOutput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AmmeterArchivesDownOutput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AmmeterArchivesMatchInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AmmeterArchivesMatchInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AmmeterArchivesMatchInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AmmeterArchivesMatchInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AutoReportCollectionItemsSetInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AutoReportCollectionItemsSetInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AutoReportCollectionItemsSetInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AutoReportCollectionItemsSetInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AutoReportSetInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AutoReportSetInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AutoReportSetInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/AutoReportSetInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/BaseInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/BaseInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/BaseInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/BaseInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/BatchReadVersionInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/BatchReadVersionInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/BatchReadVersionInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/BatchReadVersionInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/BatchReadVersionOutput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/BatchReadVersionOutput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/BatchReadVersionOutput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/BatchReadVersionOutput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/CallTimeTestingInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/CallTimeTestingInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/CallTimeTestingInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/CallTimeTestingInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/CommunicationParametersSetInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/CommunicationParametersSetInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/CommunicationParametersSetInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/CommunicationParametersSetInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/EquitDubgInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/EquitDubgInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/EquitDubgInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/EquitDubgInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/QueryAutoReportOpenStatusInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/QueryAutoReportOpenStatusInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/QueryAutoReportOpenStatusInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/QueryAutoReportOpenStatusInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/QueryRecordLogInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/QueryRecordLogInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/QueryRecordLogInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/QueryRecordLogInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/QueryRecordLogOutput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/QueryRecordLogOutput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/QueryRecordLogOutput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/QueryRecordLogOutput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadMeterNumInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadMeterNumInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadMeterNumInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadMeterNumInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadMeterNumOutput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadMeterNumOutput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadMeterNumOutput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadMeterNumOutput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadTimeInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadTimeInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadTimeInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadTimeInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadTimeOutput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadTimeOutput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadTimeOutput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadTimeOutput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadingInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadingInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadingInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadingInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadingOutput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadingOutput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadingOutput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ReadingOutput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TerminalRestartInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TerminalRestartInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TerminalRestartInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TerminalRestartInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TimeAdjustInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TimeAdjustInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TimeAdjustInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TimeAdjustInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TimeSetInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TimeSetInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TimeSetInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TimeSetInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TimeSetOutput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TimeSetOutput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TimeSetOutput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/TimeSetOutput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ValveControlInput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ValveControlInput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ValveControlInput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ValveControlInput.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ValveControlOutput.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ValveControlOutput.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ValveControlOutput.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/Dto/ValveControlOutput.cs diff --git a/services/JiShe.CollectBus.Application.Contracts/EnergySystem/ICacheAppService.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/ICacheAppService.cs new file mode 100644 index 0000000..629733d --- /dev/null +++ b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/ICacheAppService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace JiShe.CollectBus.EnergySystem +{ + public interface ICacheAppService : IApplicationService + { + } +} diff --git a/src/JiShe.CollectBus.Application.Contracts/EnergySystem/IEnergySystemAppService.cs b/services/JiShe.CollectBus.Application.Contracts/EnergySystem/IEnergySystemAppService.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/EnergySystem/IEnergySystemAppService.cs rename to services/JiShe.CollectBus.Application.Contracts/EnergySystem/IEnergySystemAppService.cs diff --git a/src/JiShe.CollectBus.Application/FodyWeavers.xml b/services/JiShe.CollectBus.Application.Contracts/FodyWeavers.xml similarity index 100% rename from src/JiShe.CollectBus.Application/FodyWeavers.xml rename to services/JiShe.CollectBus.Application.Contracts/FodyWeavers.xml diff --git a/src/JiShe.CollectBus.Application.Contracts/ICollectWorker.cs b/services/JiShe.CollectBus.Application.Contracts/ICollectWorker.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/ICollectWorker.cs rename to services/JiShe.CollectBus.Application.Contracts/ICollectWorker.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/JiShe - Backup.CollectBus.Application.Contracts.csproj b/services/JiShe.CollectBus.Application.Contracts/JiShe - Backup.CollectBus.Application.Contracts.csproj similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/JiShe - Backup.CollectBus.Application.Contracts.csproj rename to services/JiShe.CollectBus.Application.Contracts/JiShe - Backup.CollectBus.Application.Contracts.csproj diff --git a/src/JiShe.CollectBus.Application.Contracts/JiShe.CollectBus.Application.Contracts.abppkg b/services/JiShe.CollectBus.Application.Contracts/JiShe.CollectBus.Application.Contracts.abppkg similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/JiShe.CollectBus.Application.Contracts.abppkg rename to services/JiShe.CollectBus.Application.Contracts/JiShe.CollectBus.Application.Contracts.abppkg diff --git a/src/JiShe.CollectBus.Application.Contracts/JiShe.CollectBus.Application.Contracts.csproj b/services/JiShe.CollectBus.Application.Contracts/JiShe.CollectBus.Application.Contracts.csproj similarity index 61% rename from src/JiShe.CollectBus.Application.Contracts/JiShe.CollectBus.Application.Contracts.csproj rename to services/JiShe.CollectBus.Application.Contracts/JiShe.CollectBus.Application.Contracts.csproj index 0a28849..fedbafd 100644 --- a/src/JiShe.CollectBus.Application.Contracts/JiShe.CollectBus.Application.Contracts.csproj +++ b/services/JiShe.CollectBus.Application.Contracts/JiShe.CollectBus.Application.Contracts.csproj @@ -9,6 +9,12 @@ True + + + + + + @@ -17,9 +23,10 @@ - - + + + diff --git a/src/JiShe.CollectBus.Application.Contracts/Permissions/CollectBusPermissionDefinitionProvider.cs b/services/JiShe.CollectBus.Application.Contracts/Permissions/CollectBusPermissionDefinitionProvider.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/Permissions/CollectBusPermissionDefinitionProvider.cs rename to services/JiShe.CollectBus.Application.Contracts/Permissions/CollectBusPermissionDefinitionProvider.cs diff --git a/src/JiShe.CollectBus.Application.Contracts/Permissions/CollectBusPermissions.cs b/services/JiShe.CollectBus.Application.Contracts/Permissions/CollectBusPermissions.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/Permissions/CollectBusPermissions.cs rename to services/JiShe.CollectBus.Application.Contracts/Permissions/CollectBusPermissions.cs diff --git a/services/JiShe.CollectBus.Application.Contracts/RedisDataCache/IRedisDataCacheService.cs b/services/JiShe.CollectBus.Application.Contracts/RedisDataCache/IRedisDataCacheService.cs new file mode 100644 index 0000000..eb400c7 --- /dev/null +++ b/services/JiShe.CollectBus.Application.Contracts/RedisDataCache/IRedisDataCacheService.cs @@ -0,0 +1,176 @@ +using JiShe.CollectBus.Common.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Application.Contracts +{ + /// + /// 数据缓存服务接口 + /// + public interface IRedisDataCacheService + { + /// + /// 单个添加数据 + /// + /// + /// 主数据存储Hash缓存Key + /// Set索引缓存Key + /// ZSET索引缓存Key + /// 待缓存数据 + /// + Task InsertDataAsync( + string redisHashCacheKey, + string redisSetIndexCacheKey, + string redisZSetScoresIndexCacheKey, + T data) where T : DeviceCacheBasicModel; + + /// + /// 批量添加数据 + /// + /// + /// 主数据存储Hash缓存Key + /// Set索引缓存Key + /// ZSET索引缓存Key + /// 待缓存数据集合 + /// + Task BatchInsertDataAsync( + string redisHashCacheKey, + string redisSetIndexCacheKey, + string redisZSetScoresIndexCacheKey, + IEnumerable items) where T : DeviceCacheBasicModel; + + /// + /// 删除缓存信息 + /// + /// + /// 主数据存储Hash缓存Key + /// Set索引缓存Key + /// ZSET索引缓存Key + /// 已缓存数据 + /// + Task RemoveCacheDataAsync( + string redisHashCacheKey, + string redisSetIndexCacheKey, + string redisZSetScoresIndexCacheKey, + T data) where T : DeviceCacheBasicModel; + + /// + /// 修改缓存信息,映射关系未发生改变 + /// + /// + /// 主数据存储Hash缓存Key + /// Set索引缓存Key + /// ZSET索引缓存Key + /// 待修改缓存数据 + /// + Task ModifyDataAsync( + string redisHashCacheKey, + string redisSetIndexCacheKey, + string redisZSetScoresIndexCacheKey, + T newData) where T : DeviceCacheBasicModel; + + + /// + /// 修改缓存信息,映射关系已改变 + /// + /// + /// 主数据存储Hash缓存Key + /// Set索引缓存Key + /// 旧的映射关系 + /// ZSET索引缓存Key + /// 待修改缓存数据 + /// + Task ModifyDataAsync( + string redisHashCacheKey, + string redisSetIndexCacheKey, + string oldMemberId, + string redisZSetScoresIndexCacheKey, + T newData) where T : DeviceCacheBasicModel; + + ///// + ///// 通过集中器与表计信息排序索引获取数据 + ///// + ///// + ///// 主数据存储Hash缓存Key + ///// ZSET索引缓存Key + ///// 分页尺寸 + ///// 最后一个索引 + ///// 最后一个唯一标识 + ///// 排序方式 + ///// + //Task> GetPagedData( + //string redisHashCacheKey, + //string redisZSetScoresIndexCacheKey, + //IEnumerable focusIds, + //int pageSize = 10, + //decimal? lastScore = null, + //string lastMember = null, + //bool descending = true) + //where T : DeviceCacheBasicModel; + + + /// + /// 通过ZSET索引获取数据,支持10万级别数据处理,控制在13秒以内。 + /// + /// + /// 主数据存储Hash缓存Key + /// ZSET索引缓存Key + /// 分页尺寸 + /// 最后一个索引 + /// 最后一个唯一标识 + /// 排序方式 + /// + Task> GetAllPagedData( + string redisHashCacheKey, + string redisZSetScoresIndexCacheKey, + int pageSize = 1000, + decimal? lastScore = null, + string lastMember = null, + bool descending = true) + where T : DeviceCacheBasicModel; + + + ///// + ///// 游标分页查询 + ///// + ///// 排序索引ZSET缓存Key + ///// 分页数量 + ///// 开始索引 + ///// 开始唯一标识 + ///// 排序方式 + ///// + //Task<(List Members, bool HasNext)> GetPagedMembers( + // string redisZSetScoresIndexCacheKey, + // int pageSize, + // decimal? startScore, + // string excludeMember, + // bool descending); + + ///// + ///// 批量获取指定分页的数据 + ///// + ///// + ///// Hash表缓存key + ///// Hash表字段集合 + ///// + //Task> BatchGetData( + // string redisHashCacheKey, + // IEnumerable members) + // where T : DeviceCacheBasicModel; + + ///// + ///// 获取下一页游标 + ///// + ///// 排序索引ZSET缓存Key + ///// 最后一个唯一标识 + ///// 排序方式 + ///// + //Task GetNextScore( + // string redisZSetScoresIndexCacheKey, + // string lastMember, + // bool descending); + } +} diff --git a/src/JiShe.CollectBus.Application.Contracts/Samples/ISampleAppService.cs b/services/JiShe.CollectBus.Application.Contracts/Samples/ISampleAppService.cs similarity index 100% rename from src/JiShe.CollectBus.Application.Contracts/Samples/ISampleAppService.cs rename to services/JiShe.CollectBus.Application.Contracts/Samples/ISampleAppService.cs diff --git a/services/JiShe.CollectBus.Application.Contracts/Samples/SampleDto.cs b/services/JiShe.CollectBus.Application.Contracts/Samples/SampleDto.cs new file mode 100644 index 0000000..6211273 --- /dev/null +++ b/services/JiShe.CollectBus.Application.Contracts/Samples/SampleDto.cs @@ -0,0 +1,18 @@ +using JiShe.CollectBus.Common.Attributes; +using Volo.Abp.EventBus; + +namespace JiShe.CollectBus.Samples; + +[EventName("Sample.Kafka.Test")] +[TopicName("Test1")] +public class SampleDto +{ + public int Value { get; set; } +} + +[EventName("Sample.Kafka.Test2")] +[TopicName("Test2")] +public class SampleDto2 +{ + public int Value { get; set; } +} diff --git a/services/JiShe.CollectBus.Application.Contracts/ScheduledMeterReading/IScheduledMeterReadingService.cs b/services/JiShe.CollectBus.Application.Contracts/ScheduledMeterReading/IScheduledMeterReadingService.cs new file mode 100644 index 0000000..0f04005 --- /dev/null +++ b/services/JiShe.CollectBus.Application.Contracts/ScheduledMeterReading/IScheduledMeterReadingService.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JiShe.CollectBus.Ammeters; +using JiShe.CollectBus.GatherItem; +using JiShe.CollectBus.IotSystems.Watermeter; +using Volo.Abp.Application.Services; + +namespace JiShe.CollectBus.ScheduledMeterReading +{ + /// + /// 定时任务基础约束 + /// + public interface IScheduledMeterReadingService : IApplicationService + { + + /// + /// 获取采集项列表 + /// + /// + Task> GetGatherItemByDataTypes(); + + /// + /// 构建待处理的下发指令任务处理 + /// + /// + Task CreateToBeIssueTasks(); + + #region 电表采集处理 + /// + /// 获取电表信息 + /// + /// 采集端Code + /// + Task> GetAmmeterInfoList(string gatherCode = ""); + + /// + /// 初始化电表缓存数据 + /// + /// 采集端Code + /// + Task InitAmmeterCacheData(string gatherCode = ""); + + /// + /// 1分钟采集电表数据,只获取任务数据下发,不构建任务 + /// + /// + Task AmmeterScheduledMeterOneMinuteReading(); + + /// + /// 5分钟采集电表数据,只获取任务数据下发,不构建任务 + /// + /// + Task AmmeterScheduledMeterFiveMinuteReading(); + + /// + /// 15分钟采集电表数据,只获取任务数据下发,不构建任务 + /// + /// + Task AmmeterScheduledMeterFifteenMinuteReading(); + + #endregion + + + #region 水表采集处理 + /// + /// 获取水表信息 + /// + /// 采集端Code + /// + Task> GetWatermeterInfoList(string gatherCode = ""); + + /// + /// 初始化水表缓存数据,只获取任务数据下发,不构建任务 + /// + /// 采集端Code + /// + Task InitWatermeterCacheData(string gatherCode = ""); + + /// + /// 水表数据采集 + /// + /// + Task WatermeterScheduledMeterAutoReading(); + + #endregion + + + } +} diff --git a/services/JiShe.CollectBus.Application.Contracts/Subscribers/ISubscriberAppService.cs b/services/JiShe.CollectBus.Application.Contracts/Subscribers/ISubscriberAppService.cs new file mode 100644 index 0000000..658ff29 --- /dev/null +++ b/services/JiShe.CollectBus.Application.Contracts/Subscribers/ISubscriberAppService.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using JiShe.CollectBus.Common.Models; +using JiShe.CollectBus.IotSystems.MessageReceiveds; +using JiShe.CollectBus.Kafka; +using Volo.Abp.Application.Services; + +namespace JiShe.CollectBus.Subscribers +{ + public interface ISubscriberAppService : IApplicationService + { + Task LoginIssuedEvent(IssuedEventMessage issuedEventMessage); + Task HeartbeatIssuedEvent(IssuedEventMessage issuedEventMessage); + Task ReceivedEvent(MessageReceived receivedMessage); + Task ReceivedHeartbeatEvent(MessageReceivedHeartbeat receivedHeartbeatMessage); + Task ReceivedLoginEvent(MessageReceivedLogin receivedLoginMessage); + } +} diff --git a/services/JiShe.CollectBus.Application.Contracts/Subscribers/IWorkerSubscriberAppService.cs b/services/JiShe.CollectBus.Application.Contracts/Subscribers/IWorkerSubscriberAppService.cs new file mode 100644 index 0000000..9a37167 --- /dev/null +++ b/services/JiShe.CollectBus.Application.Contracts/Subscribers/IWorkerSubscriberAppService.cs @@ -0,0 +1,47 @@ +using JiShe.CollectBus.IotSystems.MessageIssueds; +using JiShe.CollectBus.IotSystems.MessageReceiveds; +using JiShe.CollectBus.IotSystems.MeterReadingRecords; +using JiShe.CollectBus.Kafka; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace JiShe.CollectBus.Subscribers +{ + /// + /// 定时抄读任务消息订阅 + /// + public interface IWorkerSubscriberAppService : IApplicationService + { + + #region 电表消息采集 + + /// + /// 1分钟采集电表数据下行消息消费订阅 + /// + /// + Task AmmeterScheduledMeterOneMinuteReadingIssuedEvent(ScheduledMeterReadingIssuedEventMessage issuedEventMessage); + + /// + /// 5分钟采集电表数据下行消息消费订阅 + /// + /// + Task AmmeterScheduledMeterFiveMinuteReadingIssuedEvent(ScheduledMeterReadingIssuedEventMessage issuedEventMessage); + + /// + /// 15分钟采集电表数据下行消息消费订阅 + /// + /// + Task AmmeterScheduledMeterFifteenMinuteReadingIssuedEvent(ScheduledMeterReadingIssuedEventMessage issuedEventMessage); + #endregion + + #region 水表消息采集 + /// + /// 1分钟采集水表数据下行消息消费订阅 + /// + /// + Task WatermeterSubscriberWorkerAutoReadingIssuedEvent(ScheduledMeterReadingIssuedEventMessage issuedEventMessage); + + #endregion + } +} diff --git a/services/JiShe.CollectBus.Application/CollectBusAppService.cs b/services/JiShe.CollectBus.Application/CollectBusAppService.cs new file mode 100644 index 0000000..e155c65 --- /dev/null +++ b/services/JiShe.CollectBus.Application/CollectBusAppService.cs @@ -0,0 +1,224 @@ +using JiShe.CollectBus.Common.Consts; +using JiShe.CollectBus.Common.Enums; +using JiShe.CollectBus.Common.Extensions; +using JiShe.CollectBus.Common.Helpers; +using JiShe.CollectBus.FreeSql; +using JiShe.CollectBus.Localization; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using JiShe.CollectBus.FreeRedis; +using Volo.Abp.Application.Services; + +namespace JiShe.CollectBus; + +[ApiExplorerSettings(GroupName = CollectBusDomainSharedConsts.Business)] +public abstract class CollectBusAppService : ApplicationService +{ + public IFreeSqlProvider SqlProvider => LazyServiceProvider.LazyGetRequiredService(); + protected IFreeRedisProvider FreeRedisProvider => LazyServiceProvider.LazyGetService()!; + + + protected CollectBusAppService() + { + LocalizationResource = typeof(CollectBusResource); + ObjectMapperContext = typeof(CollectBusApplicationModule); + } + + /// + /// Lua脚本批量获取缓存的表计信息 + /// + /// 表信息数据对象 + /// 采集频率对应的缓存Key集合 + /// 系统类型 + /// 服务器标识 + /// 采集频率,1分钟、5分钟、15分钟 + /// 表计类型 + /// + protected async Task>> GetMeterRedisCacheDictionaryData(string[] redisKeys, string systemType, string serverTagName, string timeDensity, MeterTypeEnum meterType) where T : class + { + if (redisKeys == null || redisKeys.Length <= 0 || string.IsNullOrWhiteSpace(systemType) || string.IsNullOrWhiteSpace(serverTagName) || string.IsNullOrWhiteSpace(timeDensity)) + { + throw new Exception($"{nameof(GetMeterRedisCacheDictionaryData)} 获取缓存的表计信息失败,参数异常,-101"); + } + + var meterInfos = new Dictionary>(); + var luaScript = @" + local results = {} + for i, key in ipairs(KEYS) do + local data = redis.call('HGETALL', key) + results[i] = {key, data} + end + return results"; + + // 分页参数:每页处理10000个键 + int pageSize = 10000; + int totalPages = (int)Math.Ceiling(redisKeys.Length / (double)pageSize); + + for (int page = 0; page < totalPages; page++) + { + // 分页获取当前批次的键 + var batchKeys = redisKeys + .Skip(page * pageSize) + .Take(pageSize) + .ToArray(); + + // 执行Lua脚本获取当前批次数据 + var merterResult = await FreeRedisProvider.Instance.EvalAsync(luaScript, batchKeys); + if (merterResult == null) + { + throw new Exception($"{nameof(GetMeterRedisCacheDictionaryData)} 获取缓存的表计信息失败,第 {page + 1} 页数据未返回,-102"); + } + + // 解析当前批次的结果 + if (merterResult is object[] arr) + { + foreach (object[] item in arr) + { + string key = (string)item[0]; + object[] fieldsAndValues = (object[])item[1]; + var redisCacheKey = $"{string.Format(RedisConst.CacheMeterInfoHashKey, systemType, serverTagName, meterType, timeDensity)}"; + string focusAddress = key.Replace(redisCacheKey, ""); + + var meterHashs = new Dictionary(); + for (int i = 0; i < fieldsAndValues.Length; i += 2) + { + string meterId = (string)fieldsAndValues[i]; + string meterStr = (string)fieldsAndValues[i + 1]; + + T meterInfo = default!; + if (!string.IsNullOrWhiteSpace(meterStr)) + { + meterInfo = meterStr.Deserialize()!; + } + if (meterInfo != null) + { + meterHashs[meterId] = meterInfo; + } + else + { + throw new Exception($"{nameof(GetMeterRedisCacheDictionaryData)} 缓存表计数据异常,集中器 {key} 的表计 {meterId} 解析失败,-103"); + } + } + + // 合并到总结果,若存在重复key则覆盖 + if (meterInfos.ContainsKey(focusAddress)) + { + foreach (var kvp in meterHashs) + { + meterInfos[focusAddress][kvp.Key] = kvp.Value; + } + } + else + { + meterInfos[focusAddress] = meterHashs; + } + } + } + else + { + throw new Exception($"{nameof(GetMeterRedisCacheDictionaryData)} 第 {page + 1} 页数据解析失败,返回类型不符,-104"); + } + } + + return meterInfos; + } + + /// + /// Lua脚本批量获取缓存的表计信息 + /// + /// 表信息数据对象 + /// 采集频率对应的缓存Key集合 + /// 系统类型 + /// 服务器标识 + /// 采集频率,1分钟、5分钟、15分钟 + /// 表计类型 + /// + protected async Task> GetMeterRedisCacheListData(string[] redisKeys, string systemType, string serverTagName, string timeDensity, MeterTypeEnum meterType) where T : class + { + if (redisKeys == null || redisKeys.Length <= 0 || + string.IsNullOrWhiteSpace(systemType) || + string.IsNullOrWhiteSpace(serverTagName) || + string.IsNullOrWhiteSpace(timeDensity)) + { + throw new Exception($"{nameof(GetMeterRedisCacheListData)} 参数异常,-101"); + } + + var meterInfos = new List(); + var luaScript = @" + local results = {} + for i, key in ipairs(KEYS) do + local data = redis.call('HGETALL', key) + results[i] = {key, data} + end + return results"; + + // 分页参数:每页10000个键 + int pageSize = 10000; + int totalPages = (int)Math.Ceiling(redisKeys.Length / (double)pageSize); + + for (int page = 0; page < totalPages; page++) + { + // 分页获取当前批次键 + var batchKeys = redisKeys + .Skip(page * pageSize) + .Take(pageSize) + .ToArray(); + + // 执行Lua脚本获取当前页数据 + var merterResult = await FreeRedisProvider.Instance.EvalAsync(luaScript, batchKeys); + if (merterResult == null) + { + throw new Exception($"{nameof(GetMeterRedisCacheListData)} 第 {page + 1} 页数据未返回,-102"); + } + + // 解析当前页结果 + if (merterResult is object[] arr) + { + foreach (object[] item in arr) + { + string key = (string)item[0]; + object[] fieldsAndValues = (object[])item[1]; + var redisCacheKey = string.Format( + RedisConst.CacheMeterInfoHashKey, + systemType, + serverTagName, + meterType, + timeDensity + ); + string focusAddress = key.Replace(redisCacheKey, ""); + + for (int i = 0; i < fieldsAndValues.Length; i += 2) + { + string meterId = (string)fieldsAndValues[i]; + string meterStr = (string)fieldsAndValues[i + 1]; + + T meterInfo = default!; + if (!string.IsNullOrWhiteSpace(meterStr)) + { + meterInfo = meterStr.Deserialize()!; + } + if (meterInfo != null) + { + meterInfos.Add(meterInfo); + } + else + { + throw new Exception( + $"{nameof(GetMeterRedisCacheListData)} 表计 {meterId} 解析失败(页 {page + 1}),-103" + ); + } + } + } + } + else + { + throw new Exception($"{nameof(GetMeterRedisCacheListData)} 第 {page + 1} 页数据格式错误,-104"); + } + } + + return meterInfos; + } +} diff --git a/src/JiShe.CollectBus.Application/CollectBusApplicationAutoMapperProfile.cs b/services/JiShe.CollectBus.Application/CollectBusApplicationAutoMapperProfile.cs similarity index 100% rename from src/JiShe.CollectBus.Application/CollectBusApplicationAutoMapperProfile.cs rename to services/JiShe.CollectBus.Application/CollectBusApplicationAutoMapperProfile.cs diff --git a/services/JiShe.CollectBus.Application/CollectBusApplicationModule.cs b/services/JiShe.CollectBus.Application/CollectBusApplicationModule.cs new file mode 100644 index 0000000..e5077cc --- /dev/null +++ b/services/JiShe.CollectBus.Application/CollectBusApplicationModule.cs @@ -0,0 +1,84 @@ +using JiShe.CollectBus.Common.Consts; +using JiShe.CollectBus.Common.Extensions; +using JiShe.CollectBus.FreeSql; +using JiShe.CollectBus.Kafka; +using JiShe.CollectBus.Kafka.AdminClient; +using JiShe.CollectBus.Protocol.Contracts; +using JiShe.CollectBus.ScheduledMeterReading; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using JiShe.CollectBus.Cassandra; +using JiShe.CollectBus.FreeRedis; +using JiShe.CollectBus.IoTDB; +using Volo.Abp; +using Volo.Abp.Application; +using Volo.Abp.Autofac; +using Volo.Abp.AutoMapper; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.BackgroundWorkers.Hangfire; +using Volo.Abp.EventBus; +using Volo.Abp.Modularity; +using Microsoft.Extensions.Options; + +namespace JiShe.CollectBus; + +[DependsOn( + typeof(CollectBusDomainModule), + typeof(CollectBusApplicationContractsModule), + typeof(AbpDddApplicationModule), + typeof(AbpAutoMapperModule), + typeof(AbpAutofacModule), + typeof(AbpBackgroundWorkersHangfireModule), + typeof(CollectBusFreeRedisModule), + typeof(CollectBusFreeSqlModule), + typeof(CollectBusKafkaModule), + typeof(CollectBusIoTDBModule), + typeof(CollectBusCassandraModule) + )] +public class CollectBusApplicationModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + context.Services.AddAutoMapperObjectMapper(); + Configure(options => + { + options.AddMaps(validate: true); + }); + } + + public override async Task OnApplicationInitializationAsync( + ApplicationInitializationContext context) + { + var assembly = Assembly.GetExecutingAssembly(); + var types = assembly.GetTypes().Where(t => typeof(ICollectWorker).IsAssignableFrom(t) && !t.IsInterface).ToList(); + foreach (var type in types) + { + await context.AddBackgroundWorkerAsync(type); + } + + //默认初始化表计信息 + var dbContext = context.ServiceProvider.GetRequiredService(); + //await dbContext.InitAmmeterCacheData(); + //await dbContext.InitWatermeterCacheData(); + + //初始化主题信息 + var kafkaAdminClient = context.ServiceProvider.GetRequiredService(); + var configuration = context.ServiceProvider.GetRequiredService(); + var kafkaOptions = context.ServiceProvider.GetRequiredService>(); + + List topics = ProtocolConstExtensions.GetAllTopicNamesByIssued(); + topics.AddRange(ProtocolConstExtensions.GetAllTopicNamesByReceived()); + + foreach (var item in topics) + { + await kafkaAdminClient.CreateTopicAsync(item, kafkaOptions.Value.NumPartitions, kafkaOptions.Value.KafkaReplicationFactor); + } + } + +} diff --git a/src/JiShe.CollectBus.Application/Consumers/IssuedConsumer.cs b/services/JiShe.CollectBus.Application/Consumers/IssuedConsumer.cs similarity index 92% rename from src/JiShe.CollectBus.Application/Consumers/IssuedConsumer.cs rename to services/JiShe.CollectBus.Application/Consumers/IssuedConsumer.cs index 710a268..b1041f2 100644 --- a/src/JiShe.CollectBus.Application/Consumers/IssuedConsumer.cs +++ b/services/JiShe.CollectBus.Application/Consumers/IssuedConsumer.cs @@ -1,8 +1,8 @@ using System; using System.Threading.Tasks; using JiShe.CollectBus.Common.Enums; -using JiShe.CollectBus.MessageIssueds; -using JiShe.CollectBus.MessageReceiveds; +using JiShe.CollectBus.IotSystems.MessageIssueds; +using JiShe.CollectBus.IotSystems.MessageReceiveds; using MassTransit; using Microsoft.Extensions.Logging; using TouchSocket.Sockets; @@ -12,7 +12,7 @@ namespace JiShe.CollectBus.Consumers { public class IssuedConsumer: IConsumer { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ITcpService _tcpService; private readonly IRepository _messageReceivedLoginEventRepository; private readonly IRepository _messageReceivedHeartbeatEventRepository; @@ -24,7 +24,7 @@ namespace JiShe.CollectBus.Consumers /// /// /// - public IssuedConsumer(ILogger logger, + public IssuedConsumer(ILogger logger, ITcpService tcpService, IRepository messageReceivedLoginEventRepository, IRepository messageReceivedHeartbeatEventRepository) diff --git a/src/JiShe.CollectBus.Application/Consumers/IssuedFaultConsumer.cs b/services/JiShe.CollectBus.Application/Consumers/IssuedFaultConsumer.cs similarity index 86% rename from src/JiShe.CollectBus.Application/Consumers/IssuedFaultConsumer.cs rename to services/JiShe.CollectBus.Application/Consumers/IssuedFaultConsumer.cs index 903beac..9bc9983 100644 --- a/src/JiShe.CollectBus.Application/Consumers/IssuedFaultConsumer.cs +++ b/services/JiShe.CollectBus.Application/Consumers/IssuedFaultConsumer.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using JiShe.CollectBus.MessageIssueds; +using JiShe.CollectBus.IotSystems.MessageIssueds; using MassTransit; namespace JiShe.CollectBus.Consumers diff --git a/src/JiShe.CollectBus.Application/Consumers/ReceivedConsumer.cs b/services/JiShe.CollectBus.Application/Consumers/ReceivedConsumer.cs similarity index 91% rename from src/JiShe.CollectBus.Application/Consumers/ReceivedConsumer.cs rename to services/JiShe.CollectBus.Application/Consumers/ReceivedConsumer.cs index 3dc22e3..4f7b5eb 100644 --- a/src/JiShe.CollectBus.Application/Consumers/ReceivedConsumer.cs +++ b/services/JiShe.CollectBus.Application/Consumers/ReceivedConsumer.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using JiShe.CollectBus.MessageReceiveds; +using JiShe.CollectBus.IotSystems.MessageReceiveds; using JiShe.CollectBus.Protocol.Contracts.Interfaces; +using JiShe.CollectBus.Protocol.Contracts.Models; using MassTransit; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -48,7 +49,7 @@ namespace JiShe.CollectBus.Consumers var list = new List(); foreach (var contextItem in context.Message) { - await protocolPlugin.AnalyzeAsync(contextItem.Message); + await protocolPlugin.AnalyzeAsync(contextItem.Message); list.Add(contextItem.Message); } await _messageReceivedEventRepository.InsertManyAsync(list); diff --git a/src/JiShe.CollectBus.Application/Consumers/ReceivedFaultConsumer.cs b/services/JiShe.CollectBus.Application/Consumers/ReceivedFaultConsumer.cs similarity index 87% rename from src/JiShe.CollectBus.Application/Consumers/ReceivedFaultConsumer.cs rename to services/JiShe.CollectBus.Application/Consumers/ReceivedFaultConsumer.cs index e569769..60bbdfc 100644 --- a/src/JiShe.CollectBus.Application/Consumers/ReceivedFaultConsumer.cs +++ b/services/JiShe.CollectBus.Application/Consumers/ReceivedFaultConsumer.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using JiShe.CollectBus.MessageReceiveds; +using JiShe.CollectBus.IotSystems.MessageReceiveds; using MassTransit; namespace JiShe.CollectBus.Consumers diff --git a/src/JiShe.CollectBus.Application/Consumers/ReceivedHeartbeatConsumer.cs b/services/JiShe.CollectBus.Application/Consumers/ReceivedHeartbeatConsumer.cs similarity index 96% rename from src/JiShe.CollectBus.Application/Consumers/ReceivedHeartbeatConsumer.cs rename to services/JiShe.CollectBus.Application/Consumers/ReceivedHeartbeatConsumer.cs index ce5233c..c2317c6 100644 --- a/src/JiShe.CollectBus.Application/Consumers/ReceivedHeartbeatConsumer.cs +++ b/services/JiShe.CollectBus.Application/Consumers/ReceivedHeartbeatConsumer.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using JiShe.CollectBus.Common.Models; -using JiShe.CollectBus.MessageReceiveds; +using JiShe.CollectBus.IotSystems.MessageReceiveds; using JiShe.CollectBus.Protocol.Contracts.Interfaces; using MassTransit; using Microsoft.Extensions.DependencyInjection; diff --git a/src/JiShe.CollectBus.Application/Consumers/ReceivedLoginConsumer.cs b/services/JiShe.CollectBus.Application/Consumers/ReceivedLoginConsumer.cs similarity index 96% rename from src/JiShe.CollectBus.Application/Consumers/ReceivedLoginConsumer.cs rename to services/JiShe.CollectBus.Application/Consumers/ReceivedLoginConsumer.cs index f17fe8e..ce67886 100644 --- a/src/JiShe.CollectBus.Application/Consumers/ReceivedLoginConsumer.cs +++ b/services/JiShe.CollectBus.Application/Consumers/ReceivedLoginConsumer.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using JiShe.CollectBus.Common.Models; -using JiShe.CollectBus.MessageReceiveds; +using JiShe.CollectBus.IotSystems.MessageReceiveds; using JiShe.CollectBus.Protocol.Contracts.Interfaces; using MassTransit; using Microsoft.Extensions.DependencyInjection; diff --git a/services/JiShe.CollectBus.Application/Consumers/ScheduledMeterReadingConsumer.cs b/services/JiShe.CollectBus.Application/Consumers/ScheduledMeterReadingConsumer.cs new file mode 100644 index 0000000..cdc731a --- /dev/null +++ b/services/JiShe.CollectBus.Application/Consumers/ScheduledMeterReadingConsumer.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using JiShe.CollectBus.Common.Enums; +using JiShe.CollectBus.Common.Models; +using JiShe.CollectBus.IotSystems.MessageIssueds; +using MassTransit; +using Microsoft.Extensions.Logging; +using TouchSocket.Sockets; +using Volo.Abp.Domain.Repositories; + +namespace JiShe.CollectBus.Consumers +{ + /// + /// 定时抄读任务消费者 + /// + public class ScheduledMeterReadingConsumer : IConsumer + { + private readonly ILogger _logger; + private readonly ITcpService _tcpService; + + /// + /// WorkerConsumer + /// + /// + /// + public ScheduledMeterReadingConsumer(ILogger logger, + ITcpService tcpService) + { + _logger = logger; + _tcpService = tcpService; + } + + + public async Task Consume(ConsumeContext context) + { + _logger.LogError($"{nameof(ScheduledMeterReadingConsumer)} 集中器的消息消费{context.Message.FocusAddress}"); + await _tcpService.SendAsync(context.Message.FocusAddress, context.Message.MessageHexString); + } + } +} diff --git a/services/JiShe.CollectBus.Application/DataMigration/DataMigrationService.cs b/services/JiShe.CollectBus.Application/DataMigration/DataMigrationService.cs new file mode 100644 index 0000000..f1f0c9b --- /dev/null +++ b/services/JiShe.CollectBus.Application/DataMigration/DataMigrationService.cs @@ -0,0 +1,153 @@ +using JiShe.CollectBus.DataMigration.Options; +using JiShe.CollectBus.IotSystems.MeterReadingRecords; +using LiteDB; +using Microsoft.Extensions.Options; +using System; +using System.Data; +using System.Linq; +using System.Threading.Channels; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace JiShe.CollectBus.DataMigration +{ + /// + /// 数据迁移服务 + /// + public class DataMigrationService: CollectBusAppService, IDataMigrationService + { + private readonly IRepository _meterReadingRecordsRepository; + private readonly DataMigrationOptions _options; + + + public DataMigrationService(IOptions options, + IRepository meterReadingRecordsRepository) + { + _options = options.Value; + _meterReadingRecordsRepository = meterReadingRecordsRepository; + } + + /// + /// 开始迁移 + /// + /// + public async Task StartMigrationAsync() + { + var rawDataChannel = Channel.CreateBounded(new BoundedChannelOptions(_options.ChannelCapacity) + { + SingleWriter = false, + SingleReader = false, + FullMode = BoundedChannelFullMode.Wait + }); + + var cleanDataChannel = Channel.CreateBounded(new BoundedChannelOptions(_options.ChannelCapacity) + { + SingleWriter = false, + SingleReader = false, + FullMode = BoundedChannelFullMode.Wait + }); + + // 启动生产者和消费者 + var producer = Task.Run(() => ProduceDataAsync(rawDataChannel.Writer)); + + var processors = Enumerable.Range(0, _options.ProcessorsCount) + .Select(_ => Task.Run(() => ProcessDataAsync(rawDataChannel.Reader, cleanDataChannel.Writer))) + .ToArray(); + + var consumer = Task.Run(() => ConsumeDataAsync(cleanDataChannel.Reader)); + + await Task.WhenAll(new[] { producer }.Union(processors).Union(new[] { consumer })); + } + + /// + /// 生产者,生产数据,主要是从MongoDB中读取数据 + /// + /// + /// + private async Task ProduceDataAsync(ChannelWriter writer) + { + //while (true) + //{ + // var queryable = await _meterReadingRecordsRepository.GetQueryableAsync(); + // var batchRecords = queryable.Where(d => d.MigrationStatus == Common.Enums.RecordsDataMigrationStatusEnum.NotStarted) + // .Take(_options.MongoDbDataBatchSize) + // .ToArray(); + + // if (batchRecords == null || batchRecords.Length == 0) + // { + // writer.Complete(); + // break; + // } + + // await writer.WriteAsync(batchRecords); + //} + } + + /// + /// 清洗数据 + /// + /// + /// + /// + private async Task ProcessDataAsync(ChannelReader reader, ChannelWriter writer) + { + await foreach (var batch in reader.ReadAllAsync()) + { + //var dataTable = new DataTable(); + //dataTable.Columns.Add("Id", typeof(string)); + //dataTable.Columns.Add("CleanName", typeof(string)); + //dataTable.Columns.Add("ProcessedTime", typeof(DateTime)); + + //foreach (var doc in batch) + //{ + // // 业务清洗逻辑 + // var cleanName = doc["name"].AsString.Trim().ToUpper(); + // dataTable.Rows.Add( + // doc["_id"].ToString(), + // cleanName, + // DateTime.UtcNow); + //} + + //await writer.WriteAsync(dataTable); + + // 批量更新标记 + //var ids = batch.Select(d => d.Id).ToArray(); + //foreach (var item in batch) + //{ + // item.MigrationStatus = Common.Enums.RecordsDataMigrationStatusEnum.InProgress; + // item.MigrationTime = DateTime.Now; + //} + + //await _meterReadingRecordsRepository.UpdateManyAsync(batch); + } + writer.Complete(); + } + + /// + /// 消费清洗后的数据入库 + /// + /// + /// + private async Task ConsumeDataAsync(ChannelReader reader) + { + //await using var connection = new SqlConnection(_sqlConnectionString); + //await connection.OpenAsync(); + + //await foreach (var dataTable in reader.ReadAllAsync()) + //{ + // using var bulkCopy = new SqlBulkCopy(connection) + // { + // DestinationTableName = "CleanData", + // BatchSize = 5000, + // BulkCopyTimeout = 300 + // }; + + // bulkCopy.ColumnMappings.Add("Id", "Id"); + // bulkCopy.ColumnMappings.Add("CleanName", "CleanName"); + // bulkCopy.ColumnMappings.Add("ProcessedTime", "ProcessedTime"); + + // await bulkCopy.WriteToServerAsync(dataTable); + //} + } + } +} diff --git a/services/JiShe.CollectBus.Application/EnergySystem/CacheAppService.cs b/services/JiShe.CollectBus.Application/EnergySystem/CacheAppService.cs new file mode 100644 index 0000000..d4ace87 --- /dev/null +++ b/services/JiShe.CollectBus.Application/EnergySystem/CacheAppService.cs @@ -0,0 +1,33 @@ +using JiShe.CollectBus.IotSystems.Records; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JiShe.CollectBus.Common.Consts; +using JiShe.CollectBus.EnergySystems.TableViews; +using JiShe.CollectBus.FreeSql; +using Volo.Abp.Domain.Repositories; +using JiShe.CollectBus.IotSystems.PrepayModel; +using JiShe.CollectBus.Ammeters; +using JiShe.CollectBus.Common.BuildSendDatas; +using JiShe.CollectBus.Common.Enums; +using JiShe.CollectBus.Common.Helpers; + +namespace JiShe.CollectBus.EnergySystem +{ + public class CacheAppService : CollectBusAppService, ICacheAppService + { + public async Task SetHashByKey(string key) + { + var data = await SqlProvider.Instance.Change(DbEnum.EnergyDB).Select().ToListAsync(); + + var groupData = data.GroupBy(a => $"{a.FocusAreaCode}{a.FocusAddress}").ToList(); + + foreach (var group in groupData) + { + await FreeRedisProvider.Instance.HSetAsync($"{RedisConst.CacheAmmeterFocusKey}:{group.Key}", group.ToDictionary(a => $"{a.ID}_{a.Address}", b => b)); + } + } + } +} diff --git a/src/JiShe.CollectBus.Application/EnergySystem/EnergySystemAppService.cs b/services/JiShe.CollectBus.Application/EnergySystem/EnergySystemAppService.cs similarity index 75% rename from src/JiShe.CollectBus.Application/EnergySystem/EnergySystemAppService.cs rename to services/JiShe.CollectBus.Application/EnergySystem/EnergySystemAppService.cs index 9c4b653..9fc8dd4 100644 --- a/src/JiShe.CollectBus.Application/EnergySystem/EnergySystemAppService.cs +++ b/services/JiShe.CollectBus.Application/EnergySystem/EnergySystemAppService.cs @@ -7,14 +7,17 @@ using System.Threading.Tasks; using DeviceDetectorNET.Class.Device; using DotNetCore.CAP; using JiShe.CollectBus.Common.BuildSendDatas; +using JiShe.CollectBus.Common.Consts; using JiShe.CollectBus.Common.Enums; using JiShe.CollectBus.Common.Extensions; +using JiShe.CollectBus.Common.Helpers; using JiShe.CollectBus.Common.Models; using JiShe.CollectBus.EnergySystem.Dto; using JiShe.CollectBus.FreeSql; -using JiShe.CollectBus.PrepayModel; +using JiShe.CollectBus.IotSystems.PrepayModel; +using JiShe.CollectBus.IotSystems.Records; +using JiShe.CollectBus.Kafka.Producer; using JiShe.CollectBus.Protocol.Contracts; -using JiShe.CollectBus.Records; using MassTransit; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; @@ -28,14 +31,16 @@ namespace JiShe.CollectBus.EnergySystem private readonly IRepository _focusRecordRepository; private readonly IRepository _csqRecordRepository; private readonly IRepository _conrOnlineRecordRepository; + private readonly IProducerService _producerService; private readonly ICapPublisher _capBus; public EnergySystemAppService(IRepository focusRecordRepository, IRepository csqRecordRepository, - IRepository conrOnlineRecordRepository, ICapPublisher capBus) + IRepository conrOnlineRecordRepository, IProducerService producerService, ICapPublisher capBus) { _focusRecordRepository = focusRecordRepository; _csqRecordRepository = csqRecordRepository; _conrOnlineRecordRepository = conrOnlineRecordRepository; + _producerService = producerService; _capBus = capBus; } @@ -70,8 +75,16 @@ namespace JiShe.CollectBus.EnergySystem if (bytes == null) return result; - - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -108,7 +121,16 @@ namespace JiShe.CollectBus.EnergySystem foreach (var bytes in bytesList) { - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -116,6 +138,7 @@ namespace JiShe.CollectBus.EnergySystem Type = IssuedEventType.Data, MessageId = NewId.NextGuid().ToString() }); + } return result; @@ -149,7 +172,15 @@ namespace JiShe.CollectBus.EnergySystem }).ToList(); var bytes = Build3761SendData.BuildAmmeterParameterSetSendCmd(address, meterParameters); - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -178,7 +209,16 @@ namespace JiShe.CollectBus.EnergySystem { var dataUnit = Build645SendData.BuildReadMeterAddressSendDataUnit(detail.MeterAddress); var bytes =Build3761SendData.BuildTransparentForwardingSendCmd(address, detail.Port, detail.BaudRate.ToString(), dataUnit, StopBit.Stop1, Parity.None); - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -261,7 +301,16 @@ namespace JiShe.CollectBus.EnergySystem if (bytes != null) { - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -320,7 +369,16 @@ namespace JiShe.CollectBus.EnergySystem var bytes = Build3761SendData.BuildCommunicationParametersSetSendCmd(address, masterIP, materPort, backupIP, backupPort, input.Data.APN); - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -347,7 +405,16 @@ namespace JiShe.CollectBus.EnergySystem var address = $"{input.AreaCode}{input.Address}"; var bytes = Build3761SendData.BuildTerminalCalendarClockSendCmd(address); - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -375,7 +442,15 @@ namespace JiShe.CollectBus.EnergySystem bool isManual = !input.AreaCode.Equals("5110");//低功耗集中器不是长连接,在连接的那一刻再发送 var bytes = Build3761SendData.BuildConrCheckTimeSendCmd(address,DateTime.Now, isManual); - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -402,7 +477,15 @@ namespace JiShe.CollectBus.EnergySystem var address = $"{input.AreaCode}{input.Address}"; var bytes = Build3761SendData.BuildConrRebootSendCmd(address); - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -430,7 +513,15 @@ namespace JiShe.CollectBus.EnergySystem var address = $"{input.AreaCode}{input.Address}"; var pnList = input.Data.Split(',').Select(it => int.Parse(it)).ToList(); var bytes = Build3761SendData.BuildAmmeterParameterReadingSendCmd(address, pnList); - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -479,7 +570,16 @@ namespace JiShe.CollectBus.EnergySystem foreach (var bytes in bytesList) { - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -487,7 +587,7 @@ namespace JiShe.CollectBus.EnergySystem Type = IssuedEventType.Data, MessageId = NewId.NextGuid().ToString() }); - + } result.Status = true; result.Msg = "操作成功"; @@ -548,7 +648,15 @@ namespace JiShe.CollectBus.EnergySystem foreach (var bytes in bytesList) { - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -577,7 +685,15 @@ namespace JiShe.CollectBus.EnergySystem var address = $"{code.AreaCode}{code.Address}"; var bytes = Build3761SendData.BuildAmmeterReportCollectionItemsSetSendCmd(address,input.Detail.Pn, input.Detail.Unit,input.Detail.Cycle,input.Detail.BaseTime, input.Detail.CurveRatio,input.Detail.Details.Select(it => new PnFn(it.Pn,it.Fn)).ToList()); - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -605,7 +721,15 @@ namespace JiShe.CollectBus.EnergySystem { var address = $"{code.AreaCode}{code.Address}"; var bytes = Build3761SendData.BuildAmmeterAutoUpSwitchSetSendCmd(address, input.Detail.Pn,input.Detail.IsOpen); - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -631,7 +755,15 @@ namespace JiShe.CollectBus.EnergySystem var result = new BaseResultDto(); var address = $"{input.AreaCode}{input.Address}"; var bytes = Build3761SendData.BuildAmmeterReadAutoUpSwitchSendCmd(address, input.Detail.Pn); - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -658,7 +790,15 @@ namespace JiShe.CollectBus.EnergySystem { var address = $"{data.AreaCode}{data.Address}"; var bytes = Build3761SendData.BuildTerminalVersionInfoReadingSendCmd(address); - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, @@ -713,7 +853,15 @@ namespace JiShe.CollectBus.EnergySystem foreach (var bytes in bytesList) { - await _capBus.PublishAsync(ProtocolConst.SubscriberIssuedEventName, new IssuedEventMessage + //await _capBus.PublishAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage + //{ + // //ClientId = messageReceived.ClientId, + // DeviceNo = address, + // Message = bytes, + // Type = IssuedEventType.Data, + // MessageId = NewId.NextGuid().ToString() + //}); + await _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerManualValveReadingIssuedEventName, new IssuedEventMessage { //ClientId = messageReceived.ClientId, DeviceNo = address, diff --git a/src/JiShe.CollectBus.Application/Exceptions/CloseException.cs b/services/JiShe.CollectBus.Application/Exceptions/CloseException.cs similarity index 100% rename from src/JiShe.CollectBus.Application/Exceptions/CloseException.cs rename to services/JiShe.CollectBus.Application/Exceptions/CloseException.cs diff --git a/src/JiShe.CollectBus.Domain.Shared/FodyWeavers.xml b/services/JiShe.CollectBus.Application/FodyWeavers.xml similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/FodyWeavers.xml rename to services/JiShe.CollectBus.Application/FodyWeavers.xml diff --git a/services/JiShe.CollectBus.Application/Handlers/SampleHandler.cs b/services/JiShe.CollectBus.Application/Handlers/SampleHandler.cs new file mode 100644 index 0000000..292c8d6 --- /dev/null +++ b/services/JiShe.CollectBus.Application/Handlers/SampleHandler.cs @@ -0,0 +1,46 @@ +using JiShe.CollectBus.Samples; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Distributed; + +namespace JiShe.CollectBus.Handlers +{ + public class SampleHandler : IDistributedEventHandler, + ITransientDependency + { + + private readonly ILogger _logger; + + public SampleHandler(ILogger logger) + { + _logger = logger; + } + + public async Task HandleEventAsync(SampleDto eventData) + { + _logger.LogWarning($"topic Test1 message: {eventData.Value.ToString()}"); + } + } + + public class SampleHandler2 : IDistributedEventHandler, + ITransientDependency + { + + private readonly ILogger _logger; + + public SampleHandler2(ILogger logger) + { + _logger = logger; + } + + public async Task HandleEventAsync(SampleDto2 eventData) + { + _logger.LogWarning($"topic Test2 message: {eventData.Value.ToString()}"); + } + } +} diff --git a/src/JiShe.CollectBus.Application/JiShe - Backup.CollectBus.Application.csproj b/services/JiShe.CollectBus.Application/JiShe - Backup.CollectBus.Application.csproj similarity index 100% rename from src/JiShe.CollectBus.Application/JiShe - Backup.CollectBus.Application.csproj rename to services/JiShe.CollectBus.Application/JiShe - Backup.CollectBus.Application.csproj diff --git a/src/JiShe.CollectBus.Application/JiShe.CollectBus.Application.abppkg b/services/JiShe.CollectBus.Application/JiShe.CollectBus.Application.abppkg similarity index 100% rename from src/JiShe.CollectBus.Application/JiShe.CollectBus.Application.abppkg rename to services/JiShe.CollectBus.Application/JiShe.CollectBus.Application.abppkg diff --git a/src/JiShe.CollectBus.Application/JiShe.CollectBus.Application.csproj b/services/JiShe.CollectBus.Application/JiShe.CollectBus.Application.csproj similarity index 50% rename from src/JiShe.CollectBus.Application/JiShe.CollectBus.Application.csproj rename to services/JiShe.CollectBus.Application/JiShe.CollectBus.Application.csproj index 8bd63d9..b473dea 100644 --- a/src/JiShe.CollectBus.Application/JiShe.CollectBus.Application.csproj +++ b/services/JiShe.CollectBus.Application/JiShe.CollectBus.Application.csproj @@ -15,20 +15,24 @@ - + + + - - - + + + + - - - - + + + + + diff --git a/src/JiShe.CollectBus.Application/Plugins/CloseMonitor.cs b/services/JiShe.CollectBus.Application/Plugins/CloseMonitor.cs similarity index 73% rename from src/JiShe.CollectBus.Application/Plugins/CloseMonitor.cs rename to services/JiShe.CollectBus.Application/Plugins/CloseMonitor.cs index 19b66d8..f8651c7 100644 --- a/src/JiShe.CollectBus.Application/Plugins/CloseMonitor.cs +++ b/services/JiShe.CollectBus.Application/Plugins/CloseMonitor.cs @@ -7,10 +7,10 @@ using TouchSocket.Sockets; namespace JiShe.CollectBus.Plugins { - public partial class TcpCloseMonitor(ILogger logger) : PluginBase + public partial class TcpCloseMonitor(ILogger logger) : PluginBase, ITcpReceivedPlugin { - [GeneratorPlugin(typeof(ITcpReceivedPlugin))] - public async Task OnTcpReceived(ITcpSessionClient client, ReceivedDataEventArgs e) + + public async Task OnTcpReceived(ITcpSession client, ReceivedDataEventArgs e) { try { @@ -19,21 +19,21 @@ namespace JiShe.CollectBus.Plugins catch (CloseException ex) { logger.LogInformation("拦截到CloseException"); - client.Close(ex.Message); + await client.CloseAsync(ex.Message); } catch (Exception exx) { - // ignored + } finally { + } } } - public partial class UdpCloseMonitor(ILogger logger) : PluginBase + public partial class UdpCloseMonitor(ILogger logger) : PluginBase, IUdpReceivedPlugin { - [GeneratorPlugin(typeof(IUdpReceivedPlugin))] public Task OnUdpReceived(IUdpSessionBase client, UdpReceivedDataEventArgs e) { throw new NotImplementedException(); diff --git a/src/JiShe.CollectBus.Application/Plugins/ServerMonitor.cs b/services/JiShe.CollectBus.Application/Plugins/ServerMonitor.cs similarity index 90% rename from src/JiShe.CollectBus.Application/Plugins/ServerMonitor.cs rename to services/JiShe.CollectBus.Application/Plugins/ServerMonitor.cs index 39174db..2b3c15a 100644 --- a/src/JiShe.CollectBus.Application/Plugins/ServerMonitor.cs +++ b/services/JiShe.CollectBus.Application/Plugins/ServerMonitor.cs @@ -5,9 +5,8 @@ using TouchSocket.Sockets; namespace JiShe.CollectBus.Plugins { - public partial class ServerMonitor(ILogger logger) : PluginBase + public partial class ServerMonitor(ILogger logger) : PluginBase, IServerStartedPlugin, IServerStopedPlugin { - [GeneratorPlugin(typeof(IServerStartedPlugin))] public Task OnServerStarted(IServiceBase sender, ServiceStateEventArgs e) { switch (sender) @@ -32,7 +31,6 @@ namespace JiShe.CollectBus.Plugins return e.InvokeNext(); } - [GeneratorPlugin(typeof(IServerStopedPlugin))] public Task OnServerStoped(IServiceBase sender,ServiceStateEventArgs e) { logger.LogInformation("服务已停止"); diff --git a/services/JiShe.CollectBus.Application/Plugins/TcpMonitor.cs b/services/JiShe.CollectBus.Application/Plugins/TcpMonitor.cs new file mode 100644 index 0000000..c2bd026 --- /dev/null +++ b/services/JiShe.CollectBus.Application/Plugins/TcpMonitor.cs @@ -0,0 +1,278 @@ +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using DeviceDetectorNET.Parser.Device; +using DotNetCore.CAP; +using JiShe.CollectBus.Ammeters; +using JiShe.CollectBus.Common.Consts; +using JiShe.CollectBus.Common.Enums; +using JiShe.CollectBus.Common.Extensions; +using JiShe.CollectBus.Common.Helpers; +using JiShe.CollectBus.Enums; +using JiShe.CollectBus.Interceptors; +using JiShe.CollectBus.IotSystems.Devices; +using JiShe.CollectBus.IotSystems.MessageReceiveds; +using JiShe.CollectBus.Kafka.Producer; +using JiShe.CollectBus.Protocol.Contracts; +using MassTransit; +using Microsoft.Extensions.Logging; +using TouchSocket.Core; +using TouchSocket.Sockets; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Repositories; +using static FreeSql.Internal.GlobalFilter; + +namespace JiShe.CollectBus.Plugins +{ + public partial class TcpMonitor : PluginBase, ITransientDependency, ITcpReceivedPlugin, ITcpConnectingPlugin, ITcpConnectedPlugin, ITcpClosedPlugin + { + private readonly ICapPublisher _producerBus; + private readonly IProducerService _producerService; + private readonly ILogger _logger; + private readonly IRepository _deviceRepository; + private readonly IDistributedCache _ammeterInfoCache; + + /// + /// + /// + /// + /// + /// + /// + public TcpMonitor(ICapPublisher producerBus, IProducerService producerService, + ILogger logger, + IRepository deviceRepository, + IDistributedCache ammeterInfoCache) + { + _producerBus = producerBus; + _producerService = producerService; + _logger = logger; + _deviceRepository = deviceRepository; + _ammeterInfoCache = ammeterInfoCache; + } + + public async Task OnTcpReceived(ITcpSession client, ReceivedDataEventArgs e) + { + var messageHexString = Convert.ToHexString(e.ByteBlock.Span); + var hexStringList = messageHexString.StringToPairs(); + var aFn = (int?)hexStringList.GetAnalyzeValue(CommandChunkEnum.AFN); + var fn = (int?)hexStringList.GetAnalyzeValue(CommandChunkEnum.FN); + var aTuple = (Tuple)hexStringList.GetAnalyzeValue(CommandChunkEnum.A); + if (aFn.HasValue && fn.HasValue && aTuple != null && !string.IsNullOrWhiteSpace(aTuple.Item1)) + { + var tcpSessionClient = (ITcpSessionClient)client; + + if ((AFN)aFn == AFN.链路接口检测) + { + switch (fn) + { + case 1: + await OnTcpLoginReceived(tcpSessionClient, messageHexString, aTuple.Item1); + break; + case 3: + //心跳帧有两种情况: + //1. 集中器先有登录帧,再有心跳帧 + //2. 集中器没有登录帧,只有心跳帧 + await OnTcpHeartbeatReceived(tcpSessionClient, messageHexString, aTuple.Item1); + break; + default: + _logger.LogError($"指令初步解析失败,指令内容:{messageHexString}"); + break; + } + } + else + { + await OnTcpNormalReceived(tcpSessionClient, messageHexString, aTuple.Item1,aFn.ToString()!.PadLeft(2,'0')); + } + } + else + { + _logger.LogError($"指令初步解析失败,指令内容:{messageHexString}"); + } + + await e.InvokeNext(); + } + + //[GeneratorPlugin(typeof(ITcpConnectingPlugin))] + public async Task OnTcpConnecting(ITcpSession client, ConnectingEventArgs e) + { + var tcpSessionClient = (ITcpSessionClient)client; + + _logger.LogInformation($"[TCP] ID:{tcpSessionClient.Id} IP:{client.GetIPPort()}正在连接中..."); + await e.InvokeNext(); + } + + //[GeneratorPlugin(typeof(ITcpConnectedPlugin))] + public async Task OnTcpConnected(ITcpSession client, ConnectedEventArgs e) + { + var tcpSessionClient = (ITcpSessionClient)client; + + + _logger.LogInformation($"[TCP] ID:{tcpSessionClient.Id} IP:{client.GetIPPort()}已连接"); + await e.InvokeNext(); + } + + //[GeneratorPlugin(typeof(ITcpClosedPlugin))]//ITcpSessionClient + public async Task OnTcpClosed(ITcpSession client, ClosedEventArgs e) + { + + var tcpSessionClient = (ITcpSessionClient)client; + var entity = await _deviceRepository.FindAsync(a => a.ClientId == tcpSessionClient.Id); + if (entity != null) + { + entity.UpdateByOnClosed(); + await _deviceRepository.UpdateAsync(entity); + } + else + { + _logger.LogWarning($"[TCP] ID:{tcpSessionClient.Id} IP:{client.GetIPPort()}已关闭连接,但采集程序检索失败"); + } + + await e.InvokeNext(); + } + + /// + /// 登录帧处理 + /// + /// + /// + /// 集中器编号 + /// + private async Task OnTcpLoginReceived(ITcpSessionClient client, string messageHexString, string deviceNo) + { + string oldClientId = $"{client.Id}"; + + await client.ResetIdAsync(deviceNo); + + var deviceInfoList= await _deviceRepository.GetListAsync(a => a.Number == deviceNo); + if (deviceInfoList != null && deviceInfoList.Count > 1) + { + //todo 推送集中器编号重复预警 + _logger.LogError($"集中器编号:{deviceNo},存在多个集中器,请检查集中器编号是否重复"); + return; + } + + var entity = deviceInfoList?.FirstOrDefault(a => a.Number == deviceNo); + if (entity == null) + { + await _deviceRepository.InsertAsync(new Device(deviceNo, oldClientId, DateTime.Now, DateTime.Now, DeviceStatus.Online)); + } + else + { + entity.UpdateByLoginAndHeartbeat(oldClientId); + await _deviceRepository.UpdateAsync(entity); + } + + var messageReceivedLoginEvent = new MessageReceivedLogin + { + ClientId = deviceNo, + ClientIp = client.IP, + ClientPort = client.Port, + MessageHexString = messageHexString, + DeviceNo = deviceNo, + MessageId = NewId.NextGuid().ToString() + }; + + //await _producerBus.PublishAsync(ProtocolConst.SubscriberLoginReceivedEventName, messageReceivedLoginEvent); + + + await _producerService.ProduceAsync(ProtocolConst.SubscriberLoginReceivedEventName, messageReceivedLoginEvent); + + //await _producerBus.Publish( messageReceivedLoginEvent); + } + + private async Task OnTcpHeartbeatReceived(ITcpSessionClient client, string messageHexString, string deviceNo) + { + string clientId = deviceNo; + string oldClientId = $"{client.Id}"; + + var deviceInfoList = await _deviceRepository.GetListAsync(a => a.Number == deviceNo); + if (deviceInfoList != null && deviceInfoList.Count > 1) + { + //todo 推送集中器编号重复预警 + _logger.LogError($"集中器编号:{deviceNo},存在多个集中器,请检查集中器编号是否重复"); + return; + } + + var entity = deviceInfoList?.FirstOrDefault(a => a.Number == deviceNo); + if (entity == null) //没有登录帧的设备,只有心跳帧 + { + await client.ResetIdAsync(clientId); + await _deviceRepository.InsertAsync(new Device(deviceNo, oldClientId, DateTime.Now, DateTime.Now, DeviceStatus.Online)); + } + else + { + if (clientId != oldClientId) + { + entity.UpdateByLoginAndHeartbeat(oldClientId); + } + else + { + entity.UpdateByLoginAndHeartbeat(); + } + + await _deviceRepository.UpdateAsync(entity); + } + + var messageReceivedHeartbeatEvent = new MessageReceivedHeartbeat + { + ClientId = clientId, + ClientIp = client.IP, + ClientPort = client.Port, + MessageHexString = messageHexString, + DeviceNo = deviceNo, + MessageId = NewId.NextGuid().ToString() + }; + //await _producerBus.PublishAsync(ProtocolConst.SubscriberHeartbeatReceivedEventName, messageReceivedHeartbeatEvent); + + await _producerService.ProduceAsync(ProtocolConst.SubscriberHeartbeatReceivedEventName, messageReceivedHeartbeatEvent); + //await _producerBus.Publish(messageReceivedHeartbeatEvent); + } + + /// + /// 正常帧处理,将不同的AFN进行分发 + /// + /// + /// + /// + /// + /// + private async Task OnTcpNormalReceived(ITcpSessionClient client, string messageHexString, string deviceNo,string aFn) + { + //await _producerBus.Publish(new MessageReceived + //{ + // ClientId = client.Id, + // ClientIp = client.IP, + // ClientPort = client.Port, + // MessageHexString = messageHexString, + // DeviceNo = deviceNo, + // MessageId = NewId.NextGuid().ToString() + //}); + + + //string topicName = string.Format(ProtocolConst.AFNTopicNameFormat, aFn); + //todo 如何确定时标?目前集中器的采集频率,都是固定,数据上报的时候,根据当前时间,往后推测出应当采集的时间点作为时标。但是如果由于网络问题,数据一直没上报的情况改怎么计算? + //await _producerBus.PublishAsync(ProtocolConst.SubscriberReceivedEventName, new MessageReceived + //{ + // ClientId = client.Id, + // ClientIp = client.IP, + // ClientPort = client.Port, + // MessageHexString = messageHexString, + // DeviceNo = deviceNo, + // MessageId = NewId.NextGuid().ToString() + //}); + await _producerService.ProduceAsync(ProtocolConst.SubscriberReceivedEventName, new MessageReceived + { + ClientId = client.Id, + ClientIp = client.IP, + ClientPort = client.Port, + MessageHexString = messageHexString, + DeviceNo = deviceNo, + MessageId = NewId.NextGuid().ToString() + }); + } + } +} diff --git a/src/JiShe.CollectBus.Application/Plugins/UdpMonitor.cs b/services/JiShe.CollectBus.Application/Plugins/UdpMonitor.cs similarity index 83% rename from src/JiShe.CollectBus.Application/Plugins/UdpMonitor.cs rename to services/JiShe.CollectBus.Application/Plugins/UdpMonitor.cs index f975a03..b6a576c 100644 --- a/src/JiShe.CollectBus.Application/Plugins/UdpMonitor.cs +++ b/services/JiShe.CollectBus.Application/Plugins/UdpMonitor.cs @@ -5,9 +5,8 @@ using TouchSocket.Sockets; namespace JiShe.CollectBus.Plugins { - public partial class UdpMonitor : PluginBase + public partial class UdpMonitor : PluginBase, IUdpReceivedPlugin { - [GeneratorPlugin(typeof(IUdpReceivedPlugin))] public async Task OnUdpReceived(IUdpSessionBase client, UdpReceivedDataEventArgs e) { var udpSession = client as UdpSession; diff --git a/services/JiShe.CollectBus.Application/RedisDataCache/RedisDataCacheService.cs b/services/JiShe.CollectBus.Application/RedisDataCache/RedisDataCacheService.cs new file mode 100644 index 0000000..1a056a7 --- /dev/null +++ b/services/JiShe.CollectBus.Application/RedisDataCache/RedisDataCacheService.cs @@ -0,0 +1,762 @@ +using Confluent.Kafka; +using FreeRedis; +using JiShe.CollectBus.Application.Contracts; +using JiShe.CollectBus.Common.Extensions; +using JiShe.CollectBus.Common.Helpers; +using JiShe.CollectBus.Common.Models; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JiShe.CollectBus.FreeRedis; +using Volo.Abp.DependencyInjection; +using static FreeSql.Internal.GlobalFilter; +using static System.Runtime.InteropServices.JavaScript.JSType; +using static Volo.Abp.UI.Navigation.DefaultMenuNames.Application; + +namespace JiShe.CollectBus.RedisDataCache +{ + /// + /// 数据缓存服务接口 + /// + public class RedisDataCacheService : IRedisDataCacheService, ITransientDependency + { + private readonly IFreeRedisProvider _freeRedisProvider; + private readonly ILogger _logger; + private RedisClient Instance { get; set; } + + /// + /// 数据缓存服务接口 + /// + /// + /// + public RedisDataCacheService(IFreeRedisProvider freeRedisProvider, + ILogger logger) + { + this._freeRedisProvider = freeRedisProvider; + this._logger = logger; + + Instance = _freeRedisProvider.Instance; + } + + /// + /// 单个添加数据 + /// + /// + /// 主数据存储Hash缓存Key + /// Set索引缓存Key + /// ZSET索引缓存Key + /// 待缓存数据 + /// + public async Task InsertDataAsync( + string redisHashCacheKey, + string redisSetIndexCacheKey, + string redisZSetScoresIndexCacheKey, + T data) where T : DeviceCacheBasicModel + { + // 参数校验增强 + if (data == null || string.IsNullOrWhiteSpace(redisHashCacheKey) + || string.IsNullOrWhiteSpace(redisSetIndexCacheKey) + || string.IsNullOrWhiteSpace(redisZSetScoresIndexCacheKey)) + { + _logger.LogError($"{nameof(InsertDataAsync)} 参数异常,-101"); + return; + } + + // 使用事务保证原子性 + using (var trans = Instance.Multi()) + { + // 主数据存储Hash + trans.HSet(redisHashCacheKey, data.MemberId, data.Serialize()); + + // 集中器号分组索引Set缓存 + trans.SAdd(redisSetIndexCacheKey, data.MemberId); + + // 集中器与表计信息排序索引ZSET缓存Key + trans.ZAdd(redisZSetScoresIndexCacheKey, data.ScoreValue, data.MemberId); + + var results = trans.Exec(); + + if (results == null || results.Length <= 0) + { + _logger.LogError($"{nameof(InsertDataAsync)} 添加事务提交失败,-102"); + } + } + + await Task.CompletedTask; + } + + /// + /// 批量添加数据 + /// + /// + /// 主数据存储Hash缓存Key + /// Set索引缓存Key + /// ZSET索引缓存Key + /// 待缓存数据集合 + /// + public async Task BatchInsertDataAsync( + string redisHashCacheKey, + string redisSetIndexCacheKey, + string redisZSetScoresIndexCacheKey, + IEnumerable items) where T : DeviceCacheBasicModel + { + if (items == null + || items.Count() <= 0 + || string.IsNullOrWhiteSpace(redisHashCacheKey) + || string.IsNullOrWhiteSpace(redisSetIndexCacheKey) + || string.IsNullOrWhiteSpace(redisZSetScoresIndexCacheKey)) + { + _logger.LogError($"{nameof(BatchInsertDataAsync)} 参数异常,-101"); + return; + } + + 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) + { + // 主数据存储Hash + pipe.HSet(redisHashCacheKey, item.MemberId, item.Serialize()); + + // Set索引缓存 + pipe.SAdd(redisSetIndexCacheKey, item.MemberId); + + // ZSET索引缓存Key + pipe.ZAdd(redisZSetScoresIndexCacheKey, item.ScoreValue, item.MemberId); + } + pipe.EndPipe(); + } + semaphore.Release(); + }); + } + + await Task.CompletedTask; + } + + /// + /// 删除缓存信息 + /// + /// + /// 主数据存储Hash缓存Key + /// Set索引缓存Key + /// ZSET索引缓存Key + /// 已缓存数据 + /// + public async Task RemoveCacheDataAsync( + string redisHashCacheKey, + string redisSetIndexCacheKey, + string redisZSetScoresIndexCacheKey, + T data) where T : DeviceCacheBasicModel + { + if (data == null + || string.IsNullOrWhiteSpace(redisHashCacheKey) + || string.IsNullOrWhiteSpace(redisSetIndexCacheKey) + || string.IsNullOrWhiteSpace(redisZSetScoresIndexCacheKey)) + { + _logger.LogError($"{nameof(RemoveCacheDataAsync)} 参数异常,-101"); + return; + } + + const string luaScript = @" + local hashCacheKey = KEYS[1] + local setIndexCacheKey = KEYS[2] + local zsetScoresIndexCacheKey = KEYS[3] + local member = ARGV[1] + + local deleted = 0 + if redis.call('HDEL', hashCacheKey, member) > 0 then + deleted = 1 + end + + redis.call('SREM', setIndexCacheKey, member) + redis.call('ZREM', zsetScoresIndexCacheKey, member) + return deleted + "; + + var keys = new[] + { + redisHashCacheKey, + redisSetIndexCacheKey, + redisZSetScoresIndexCacheKey + }; + + var result = await Instance.EvalAsync(luaScript, keys, new[] { data.MemberId }); + + if ((int)result == 0) + { + _logger.LogError($"{nameof(RemoveCacheDataAsync)} 删除指定Key{redisHashCacheKey}的{data.MemberId}数据失败,-102"); + } + } + + /// + /// 修改缓存信息,映射关系未发生改变 + /// + /// + /// 主数据存储Hash缓存Key + /// Set索引缓存Key + /// ZSET索引缓存Key + /// 待修改缓存数据 + /// + public async Task ModifyDataAsync( + string redisHashCacheKey, + string redisSetIndexCacheKey, + string redisZSetScoresIndexCacheKey, + T newData) where T : DeviceCacheBasicModel + { + if (newData == null + || string.IsNullOrWhiteSpace(redisHashCacheKey) + || string.IsNullOrWhiteSpace(redisSetIndexCacheKey) + || string.IsNullOrWhiteSpace(redisZSetScoresIndexCacheKey)) + { + _logger.LogError($"{nameof(ModifyDataAsync)} 参数异常,-101"); + return; + } + + var luaScript = @" + local hashCacheKey = KEYS[1] + local member = ARGV[1] + local newData = ARGV[2] + + -- 校验存在性 + if redis.call('HEXISTS', hashCacheKey, member) == 0 then + return 0 + end + + -- 更新主数据 + redis.call('HSET', hashCacheKey, member, newData) + + return 1 + "; + + + var result = await Instance.EvalAsync(luaScript, + new[] + { + redisHashCacheKey + }, + new object[] + { + newData.MemberId, + newData.Serialize() + }); + + if ((int)result == 0) + { + _logger.LogError($"{nameof(ModifyDataAsync)} 更新指定Key{redisHashCacheKey}的{newData.MemberId}数据失败,-102"); + } + } + + /// + /// 修改缓存信息,映射关系已经改变 + /// + /// + /// 主数据存储Hash缓存Key + /// Set索引缓存Key + /// 旧的映射关系 + /// ZSET索引缓存Key + /// 待修改缓存数据 + /// + public async Task ModifyDataAsync( + string redisHashCacheKey, + string redisSetIndexCacheKey, + string oldMemberId, + string redisZSetScoresIndexCacheKey, + T newData) where T : DeviceCacheBasicModel + { + if (newData == null + || string.IsNullOrWhiteSpace(redisHashCacheKey) + || string.IsNullOrWhiteSpace(redisSetIndexCacheKey) + || string.IsNullOrWhiteSpace(oldMemberId) + || string.IsNullOrWhiteSpace(redisZSetScoresIndexCacheKey)) + { + _logger.LogError($"{nameof(ModifyDataAsync)} 参数异常,-101"); + return; + } + + var luaScript = @" + local hashCacheKey = KEYS[1] + local setIndexCacheKey = KEYS[2] + local zsetScoresIndexCacheKey = KEYS[3] + local member = ARGV[1] + local oldMember = ARGV[2] + local newData = ARGV[3] + local newScore = ARGV[4] + + -- 校验存在性 + if redis.call('HEXISTS', hashCacheKey, oldMember) == 0 then + return 0 + end + + -- 删除旧数据 + redis.call('HDEL', hashCacheKey, oldMember) + + -- 插入新主数据 + redis.call('HSET', hashCacheKey, member, newData) + + -- 处理变更 + if newScore ~= '' then + -- 删除旧索引 + redis.call('SREM', setIndexCacheKey, oldMember) + redis.call('ZREM', zsetScoresIndexCacheKey, oldMember) + + -- 添加新索引 + redis.call('SADD', setIndexCacheKey, member) + redis.call('ZADD', zsetScoresIndexCacheKey, newScore, member) + end + + return 1 + "; + + var result = await Instance.EvalAsync(luaScript, + new[] + { + redisHashCacheKey, + redisSetIndexCacheKey, + redisZSetScoresIndexCacheKey + }, + new object[] + { + newData.MemberId, + oldMemberId, + newData.Serialize(), + newData.ScoreValue.ToString() ?? "", + }); + + if ((int)result == 0) + { + _logger.LogError($"{nameof(ModifyDataAsync)} 更新指定Key{redisHashCacheKey}的{newData.MemberId}数据失败,-102"); + } + } + + + /// + /// 通过集中器与表计信息排序索引获取指定集中器号集合数据 + /// + /// + /// 主数据存储Hash缓存Key + /// 集中器与表计信息排序索引ZSET缓存Key + /// 集中器Id + /// 分页尺寸 + /// 最后一个索引 + /// 最后一个唯一标识 + /// 排序方式 + /// + public async Task> GetPagedData( + string redisCacheKey, + string redisCacheFocusScoresIndexKey, + IEnumerable focusIds, + int pageSize = 10, + decimal? lastScore = null, + string lastMember = null, + bool descending = true) + where T : DeviceCacheBasicModel + { + throw new Exception(); + } + + /// + /// 通过ZSET索引获取数据,支持10万级别数据处理,控制在13秒以内。 + /// + /// + /// 主数据存储Hash缓存Key + /// ZSET索引缓存Key + /// 分页尺寸 + /// 最后一个索引 + /// 最后一个唯一标识 + /// 排序方式 + /// + public async Task> GetAllPagedData( + string redisHashCacheKey, + string redisZSetScoresIndexCacheKey, + int pageSize = 1000, + decimal? lastScore = null, + string lastMember = null, + bool descending = true) + where T : DeviceCacheBasicModel + { + // 参数校验增强 + if (string.IsNullOrWhiteSpace(redisHashCacheKey) || + string.IsNullOrWhiteSpace(redisZSetScoresIndexCacheKey)) + { + _logger.LogError($"{nameof(GetAllPagedData)} 参数异常,-101"); + return new BusCacheGlobalPagedResult { Items = new List() }; + } + + pageSize = Math.Clamp(pageSize, 1, 10000); + + var luaScript = @" + local command = ARGV[1] + local range_start = ARGV[2] + local range_end = ARGV[3] + local limit = tonumber(ARGV[4]) + local last_score = ARGV[5] + local last_member = ARGV[6] + + -- 获取原始数据 + local members + if command == 'ZRANGEBYSCORE' then + members = redis.call(command, KEYS[1], range_start, range_end, 'WITHSCORES', 'LIMIT', 0, limit * 2) + else + members = redis.call('ZREVRANGEBYSCORE', KEYS[1], range_start, range_end, 'WITHSCORES', 'LIMIT', 0, limit * 2) + end + + -- 过滤数据 + local filtered_members = {} + local count = 0 + for i = 1, #members, 2 do + local member = members[i] + local score = members[i+1] + local include = true + if last_score ~= '' and last_member ~= '' then + if command == 'ZRANGEBYSCORE' then + -- 升序:score > last_score 或 (score == last_score 且 member > last_member) + if score == last_score then + include = member > last_member + else + include = tonumber(score) > tonumber(last_score) + end + else + -- 降序:score < last_score 或 (score == last_score 且 member < last_member) + if score == last_score then + include = member < last_member + else + include = tonumber(score) < tonumber(last_score) + end + end + end + if include then + table.insert(filtered_members, member) + table.insert(filtered_members, score) + count = count + 1 + if count >= limit then + break + end + end + end + + -- 提取有效数据 + local result_members, result_scores = {}, {} + for i=1,#filtered_members,2 do + table.insert(result_members, filtered_members[i]) + table.insert(result_scores, filtered_members[i+1]) + end + + if #result_members == 0 then return {0,{},{},{}} end + + -- 获取Hash数据 + local hash_data = redis.call('HMGET', KEYS[2], unpack(result_members)) + return {#result_members, result_members, result_scores, hash_data}"; + + // 调整范围构造逻辑(移除排他符号) + string rangeStart, rangeEnd; + if (descending) + { + rangeStart = lastScore.HasValue ? lastScore.Value.ToString() : "+inf"; + rangeEnd = "-inf"; + } + else + { + rangeStart = lastScore.HasValue ? lastScore.Value.ToString() : "-inf"; + rangeEnd = "+inf"; + } + + var scriptResult = (object[])await Instance.EvalAsync(luaScript, + new[] { redisZSetScoresIndexCacheKey, redisHashCacheKey }, + new object[] + { + descending ? "ZREVRANGEBYSCORE" : "ZRANGEBYSCORE", + rangeStart, + rangeEnd, + (pageSize + 1).ToString(), // 获取pageSize+1条以判断是否有下一页 + lastScore?.ToString() ?? "", + lastMember ?? "" + }); + + if ((long)scriptResult[0] == 0) + return new BusCacheGlobalPagedResult { Items = new List() }; + + // 处理结果集 + var members = ((object[])scriptResult[1]).Cast().ToList(); + var scores = ((object[])scriptResult[2]).Cast().Select(decimal.Parse).ToList(); + var hashData = ((object[])scriptResult[3]).Cast().ToList(); + + var validItems = members.AsParallel() + .Select((m, i) => + { + try { return BusJsonSerializer.Deserialize(hashData[i]); } + catch { return null; } + }) + .Where(x => x != null) + .ToList(); + + var hasNext = validItems.Count > pageSize; + var actualItems = hasNext ? validItems.Take(pageSize) : validItems; + + //分页锚点索引 + decimal? nextScore = null; + string nextMember = null; + if (hasNext && actualItems.Any()) + { + var lastIndex = actualItems.Count() - 1; // 使用actualItems的最后一个索引 + nextScore = scores[lastIndex]; + nextMember = members[lastIndex]; + } + + return new BusCacheGlobalPagedResult + { + Items = actualItems.ToList(), + HasNext = hasNext, + NextScore = nextScore, + NextMember = nextMember, + TotalCount = await GetTotalCount(redisZSetScoresIndexCacheKey), + PageSize = pageSize, + }; + } + + ///// + ///// 通过集中器与表计信息排序索引获取数据 + ///// + ///// + ///// 主数据存储Hash缓存Key + ///// ZSET索引缓存Key + ///// 分页尺寸 + ///// 最后一个索引 + ///// 最后一个唯一标识 + ///// 排序方式 + ///// + //public async Task> GetAllPagedData( + //string redisHashCacheKey, + //string redisZSetScoresIndexCacheKey, + //int pageSize = 1000, + //decimal? lastScore = null, + //string lastMember = null, + //bool descending = true) + //where T : DeviceCacheBasicModel + //{ + // // 参数校验增强 + // if (string.IsNullOrWhiteSpace(redisHashCacheKey) || string.IsNullOrWhiteSpace(redisZSetScoresIndexCacheKey)) + // { + // _logger.LogError($"{nameof(GetAllPagedData)} 参数异常,-101"); + // return null; + // } + + // if (pageSize < 1 || pageSize > 10000) + // { + // _logger.LogError($"{nameof(GetAllPagedData)} 分页大小应在1-10000之间,-102"); + // return null; + // } + + // //// 分页参数解析 + // //var (startScore, excludeMember) = descending + // // ? (lastScore ?? decimal.MaxValue, lastMember) + // // : (lastScore ?? 0, lastMember); + + // //执行分页查询(整合游标处理) + // var pageResult = await GetPagedMembers( + // redisZSetScoresIndexCacheKey, + // pageSize, + // lastScore, + // lastMember, + // descending); + + // // 批量获取数据(优化内存分配) + // var dataDict = await BatchGetData(redisHashCacheKey, pageResult.Members); + + // return new BusCacheGlobalPagedResult + // { + // Items = pageResult.Members.Select(m => dataDict.TryGetValue(m, out var v) ? v : default) + // .Where(x => x != null).ToList(), + // HasNext = pageResult.HasNext, + // NextScore = pageResult.NextScore, + // NextMember = pageResult.NextMember, + // TotalCount = await GetTotalCount(redisZSetScoresIndexCacheKey), + // PageSize = pageSize, + // }; + //} + + /// + /// 游标分页查询 + /// + /// 排序索引ZSET缓存Key + /// 分页数量 + /// 上一个索引 + /// 上一个标识 + /// 排序方式 + /// + private async Task<(List Members, bool HasNext, decimal? NextScore, string NextMember)> GetPagedMembers( + string redisZSetScoresIndexCacheKey, + int pageSize, + decimal? lastScore, + string lastMember, + bool descending) + { + // 根据排序方向初始化参数 + long initialScore = descending ? long.MaxValue : 0; + decimal? currentScore = lastScore ?? initialScore; + string currentMember = lastMember; + var members = new List(pageSize + 1); + + // 使用游标分页查询 + while (members.Count < pageSize + 1 && currentScore.HasValue) + { + var (batch, hasMore) = await GetNextBatch( + redisZSetScoresIndexCacheKey, + pageSize + 1 - members.Count, + currentScore.Value, + currentMember, + descending); + + if (!batch.Any()) break; + + members.AddRange(batch); + + // 更新游标 + currentMember = batch.LastOrDefault(); + currentScore = await GetNextScore(redisZSetScoresIndexCacheKey, currentMember, descending); + } + + // 处理分页结果 + bool hasNext = members.Count > pageSize; + var resultMembers = members.Take(pageSize).ToList(); + + return ( + resultMembers, + hasNext, + currentScore, + currentMember + ); + } + + /// + /// 批量获取指定分页的数据 + /// + /// + /// Hash表缓存key + /// Hash表字段集合 + /// + private async Task> BatchGetData( + string redisHashCacheKey, + IEnumerable members) + where T : DeviceCacheBasicModel + { + using var pipe = Instance.StartPipe(); + + foreach (var member in members) + { + pipe.HGet(redisHashCacheKey, member); + } + + var results = pipe.EndPipe(); + return await Task.FromResult(members.Zip(results, (k, v) => new { k, v }) + .ToDictionary(x => x.k, x => (T)x.v)); + } + + /// + /// 处理下一个分页数据 + /// + /// + /// + /// + /// + /// + /// + private async Task<(string[] Batch, bool HasMore)> GetNextBatch( + string zsetKey, + int limit, + decimal score, + string excludeMember, + bool descending) + { + var query = descending + ? await Instance.ZRevRangeByScoreAsync( + zsetKey, + max: score, + min: 0, + offset: 0, + count: limit) + : await Instance.ZRangeByScoreAsync( + zsetKey, + min: score, + max: long.MaxValue, + offset: 0, + count: limit); + + return (query, query.Length >= limit); + } + + /// + /// 获取下一页游标 + /// + /// 排序索引ZSET缓存Key + /// 最后一个唯一标识 + /// 排序方式 + /// + private async Task GetNextScore( + string redisZSetScoresIndexCacheKey, + string lastMember, + bool descending) + { + if (string.IsNullOrEmpty(lastMember)) return null; + + var score = await Instance.ZScoreAsync(redisZSetScoresIndexCacheKey, lastMember); + if (!score.HasValue) return null; + + // 根据排序方向调整score + return descending + ? score.Value - 1 // 降序时下页查询小于当前score + : score.Value + 1; // 升序时下页查询大于当前score + } + + /// + /// 获取指定ZSET区间内的总数量 + /// + /// + /// + /// + /// + public async Task GetCount(string zsetKey, long min, long max) + { + // 缓存计数优化 + var cacheKey = $"{zsetKey}_count_{min}_{max}"; + var cached = await Instance.GetAsync(cacheKey); + + if (cached.HasValue) + return cached.Value; + + var count = await Instance.ZCountAsync(zsetKey, min, max); + await Instance.SetExAsync(cacheKey, 60, count); // 缓存60秒 + return count; + } + + /// + /// 获取指定ZSET的总数量 + /// + /// + /// + private async Task GetTotalCount(string redisZSetScoresIndexCacheKey) + { + // 缓存计数优化 + var cacheKey = $"{redisZSetScoresIndexCacheKey}_total_count"; + var cached = await Instance.GetAsync(cacheKey); + + if (cached.HasValue) + return cached.Value; + + var count = await Instance.ZCountAsync(redisZSetScoresIndexCacheKey, 0, decimal.MaxValue); + await Instance.SetExAsync(cacheKey, 30, count); // 缓存30秒 + return count; + } + } +} diff --git a/services/JiShe.CollectBus.Application/Samples/SampleAppService.cs b/services/JiShe.CollectBus.Application/Samples/SampleAppService.cs new file mode 100644 index 0000000..f0aa541 --- /dev/null +++ b/services/JiShe.CollectBus.Application/Samples/SampleAppService.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Apache.IoTDB.DataStructure; +using Apache.IoTDB; +using Confluent.Kafka; +using JiShe.CollectBus.Ammeters; +using JiShe.CollectBus.FreeSql; +using JiShe.CollectBus.IotSystems.PrepayModel; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging; +using JiShe.CollectBus.IotSystems.AFNEntity; +using JiShe.CollectBus.Protocol.Contracts.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using JiShe.CollectBus.Common.Consts; +using JiShe.CollectBus.Common.Enums; +using System.Diagnostics.Metrics; +using JiShe.CollectBus.Common.DeviceBalanceControl; +using JiShe.CollectBus.Kafka.Attributes; +using System.Text.Json; +using JiShe.CollectBus.Kafka; +using JiShe.CollectBus.Application.Contracts; +using JiShe.CollectBus.Common.Models; +using System.Diagnostics; +using JiShe.CollectBus.IoTDB.Context; +using JiShe.CollectBus.IoTDB.Interface; +using JiShe.CollectBus.IoTDB.Options; + +namespace JiShe.CollectBus.Samples; + +public class SampleAppService : CollectBusAppService, ISampleAppService, IKafkaSubscribe +{ + private readonly ILogger _logger; + private readonly IIoTDBProvider _iotDBProvider; + private readonly IoTDBRuntimeContext _dbContext; + private readonly IoTDBOptions _options; + private readonly IRedisDataCacheService _redisDataCacheService; + + public SampleAppService(IIoTDBProvider iotDBProvider, IOptions options, + IoTDBRuntimeContext dbContext, ILogger logger, IRedisDataCacheService redisDataCacheService) + { + _iotDBProvider = iotDBProvider; + _options = options.Value; + _dbContext = dbContext; + _logger = logger; + _redisDataCacheService = redisDataCacheService; + } + + /// + /// 测试 UseSessionPool + /// + /// + /// + [HttpGet] + public async Task UseSessionPool(long timestamps) + { + string? messageHexString = null; + if (timestamps == 0) + { + timestamps = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + _logger.LogError($"timestamps_{timestamps}"); + } + else + { + messageHexString = messageHexString + timestamps; + } + + ElectricityMeter meter = new ElectricityMeter() + { + SystemName = "energy", + DeviceId = "402440506", + DeviceType = "Ammeter", + Current = 10, + MeterModel = "DDZY-1980", + ProjectCode = "10059", + Voltage = 10, + IssuedMessageHexString = messageHexString, + Timestamps = timestamps, + }; + await _iotDBProvider.InsertAsync(meter); + } + + /// + /// 测试Session切换 + /// + /// + [HttpGet] + public async Task UseTableSessionPool() + { + ElectricityMeter meter2 = new ElectricityMeter() + { + SystemName = "energy", + DeviceId = "402440506", + DeviceType = "Ammeter", + Current = 10, + MeterModel = "DDZY-1980", + ProjectCode = "10059", + Voltage = 10, + Timestamps = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + }; + + await _iotDBProvider.InsertAsync(meter2); + + _dbContext.UseTableSessionPool = true; + + ElectricityMeter meter = new ElectricityMeter() + { + SystemName = "energy", + DeviceId = "402440506", + DeviceType = "Ammeter", + Current = 10, + MeterModel = "DDZY-1980", + ProjectCode = "10059", + Voltage = 10, + Timestamps = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + }; + await _iotDBProvider.InsertAsync(meter); + } + + /// + /// 测试设备分组均衡控制算法 + /// + /// + /// + [HttpGet] + public async Task TestDeviceGroupBalanceControl(int deviceCount = 200000) + { + //var deviceList = new List(); + //for (int i = 0; i < deviceCount; i++) + //{ + // deviceList.Add($"Device_{Guid.NewGuid()}"); + //} + + //// 初始化缓存 + //DeviceGroupBalanceControl.InitializeCache(deviceList); + + var timeDensity = "15"; + //获取缓存中的电表信息 + var redisKeyList = $"{string.Format(RedisConst.CacheMeterInfoHashKey, "Energy", "JiSheCollectBus", MeterTypeEnum.Ammeter.ToString(), timeDensity)}*"; + + var oneMinutekeyList = await FreeRedisProvider.Instance.KeysAsync(redisKeyList); + var meterInfos = await GetMeterRedisCacheListData(oneMinutekeyList, "Energy", "JiSheCollectBus", timeDensity, MeterTypeEnum.Ammeter); + List focusAddressDataLista = new List(); + foreach (var item in meterInfos) + { + focusAddressDataLista.Add(item.FocusAddress); + } + + DeviceGroupBalanceControl.InitializeCache(focusAddressDataLista); + + // 打印分布统计 + DeviceGroupBalanceControl.PrintDistributionStats(); + + await Task.CompletedTask; + } + + /// + /// 测试设备分组均衡控制算法获取分组Id + /// + /// + /// + [HttpGet] + public async Task TestGetDeviceGroupBalanceControl(string deviceAddress) + { + var groupId = DeviceGroupBalanceControl.GetDeviceGroupId(deviceAddress); + Console.WriteLine(groupId); + + await Task.CompletedTask; + } + + + /// + /// 测试单个测点数据项 + /// + /// + /// + [HttpGet] + public async Task TestSingleMeasuringAFNData(string measuring, string value) + { + var meter = new SingleMeasuringAFNDataEntity() + { + SystemName = "energy", + DeviceId = "402440506", + DeviceType = "Ammeter", + ProjectCode = "10059", + Timestamps = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + SingleMeasuring = new Tuple(measuring, value) + }; + await _iotDBProvider.InsertAsync(meter); + } + + /// + /// 测试Redis批量读取10万条数据性能 + /// + /// + [HttpGet] + public async Task TestRedisCacheGetAllPagedData() + { + var timeDensity = "15"; + string SystemType = "Energy"; + string ServerTagName = "JiSheCollectBus2"; + var redisCacheMeterInfoHashKeyTemp = $"{string.Format(RedisConst.CacheMeterInfoHashKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, timeDensity)}"; + var redisCacheMeterInfoSetIndexKeyTemp = $"{string.Format(RedisConst.CacheMeterInfoSetIndexKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, timeDensity)}"; + var redisCacheMeterInfoZSetScoresIndexKeyTemp = $"{string.Format(RedisConst.CacheMeterInfoZSetScoresIndexKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, timeDensity)}"; + + var timer1 = Stopwatch.StartNew(); + decimal? cursor = null; + string member = null; + bool hasNext; + List meterInfos = new List(); + do + { + var page = await _redisDataCacheService.GetAllPagedData( + redisCacheMeterInfoHashKeyTemp, + redisCacheMeterInfoZSetScoresIndexKeyTemp, + pageSize: 1000, + lastScore: cursor, + lastMember: member); + + meterInfos.AddRange(page.Items); + cursor = page.HasNext ? page.NextScore : null; + member = page.HasNext ? page.NextMember : null; + hasNext = page.HasNext; + } while (hasNext); + + timer1.Stop(); + _logger.LogError($"读取数据更花费时间{timer1.ElapsedMilliseconds}毫秒"); + + List focusAddressDataLista = new List(); + foreach (var item in meterInfos) + { + focusAddressDataLista.Add(item.FocusAddress); + } + + DeviceGroupBalanceControl.InitializeCache(focusAddressDataLista); + + // 打印分布统计 + DeviceGroupBalanceControl.PrintDistributionStats(); + + await Task.CompletedTask; + } + + + public Task GetAsync() + { + return Task.FromResult( + new SampleDto + { + Value = 42 + } + ); + } + + [Authorize] + public Task GetAuthorizedAsync() + { + return Task.FromResult( + new SampleDto + { + Value = 42 + } + ); + } + + [AllowAnonymous] + public async Task> Test() + { + + var ammeterList = await SqlProvider.Instance.Change(DbEnum.PrepayDB).Select().Where(d => d.TB_CustomerID == 5).Take(10).ToListAsync(); + return ammeterList; + } + + [AllowAnonymous] + public bool GetTestProtocol() + { + var aa = LazyServiceProvider.GetKeyedService("TestProtocolPlugin"); + return aa == null; + } + + [KafkaSubscribe(ProtocolConst.TESTTOPIC)] + + public async Task KafkaSubscribeAsync(object obj) + { + _logger.LogWarning($"收到订阅消息: {obj}"); + return SubscribeAck.Success(); + } +} + diff --git a/services/JiShe.CollectBus.Application/Samples/TestAppService.cs b/services/JiShe.CollectBus.Application/Samples/TestAppService.cs new file mode 100644 index 0000000..2de458f --- /dev/null +++ b/services/JiShe.CollectBus.Application/Samples/TestAppService.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Apache.IoTDB.DataStructure; +using Apache.IoTDB; +using Confluent.Kafka; +using JiShe.CollectBus.Ammeters; +using JiShe.CollectBus.FreeSql; +using JiShe.CollectBus.IotSystems.PrepayModel; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging; +using JiShe.CollectBus.Common.Helpers; +using JiShe.CollectBus.IotSystems.AFNEntity; +using JiShe.CollectBus.Protocol.Contracts.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using JiShe.CollectBus.Cassandra; +using JiShe.CollectBus.Common.Enums; +using JiShe.CollectBus.Common.Extensions; +using JiShe.CollectBus.IotSystems.MessageIssueds; +using Volo.Abp.Application.Services; +using JiShe.CollectBus.IotSystems.MessageReceiveds; +using Volo.Abp.Domain.Repositories; +using System.Diagnostics; +using System.Linq; +using Cassandra; + +namespace JiShe.CollectBus.Samples; + +[AllowAnonymous] +public class TestAppService : CollectBusAppService +{ + private readonly ILogger _logger; + private readonly ICassandraRepository _messageReceivedCassandraRepository; + private readonly ICassandraProvider _cassandraProvider; + + + + public TestAppService( + ILogger logger, + ICassandraRepository messageReceivedCassandraRepository, ICassandraProvider cassandraProvider) + { + _logger = logger; + _messageReceivedCassandraRepository = messageReceivedCassandraRepository; + _cassandraProvider = cassandraProvider; + } + public async Task AddMessageOfCassandra() + { + var stopwatch = Stopwatch.StartNew(); + for (int i = 1; i <= 10000; i++) + { + var str = Guid.NewGuid().ToString(); + await _messageReceivedCassandraRepository.InsertAsync(new MessageIssued + { + ClientId = str, + DeviceNo = i.ToString(), + MessageId = str, + Type = IssuedEventType.Data, + Id = str, + Message = str.GetBytes() + }); + } + stopwatch.Stop(); + _logger.LogWarning($"插入 {10000} 条记录完成,耗时: {stopwatch.ElapsedMilliseconds} 毫秒"); + } + + public async Task AddMessageOfBulkInsertCassandra() + { + var records = new List(); + var prepared = await _cassandraProvider.Session.PrepareAsync( + $"INSERT INTO {_cassandraProvider.CassandraConfig.Keyspace}.{nameof(MessageIssued)} (id, clientid, message, deviceno,type,messageid) VALUES (?, ?, ?, ?, ?, ?)"); + + for (int i = 1; i <= 100000; i++) + { + var str = Guid.NewGuid().ToString(); + records.Add(new MessageIssued + { + ClientId = str, + DeviceNo = i.ToString(), + MessageId = str, + Type = IssuedEventType.Data, + Id = str, + Message = str.GetBytes() + }); + } + var stopwatch = Stopwatch.StartNew(); + await BulkInsertAsync(_cassandraProvider.Session, prepared, records); + stopwatch.Stop(); + _logger.LogWarning($"插入 {100000} 条记录完成,耗时: {stopwatch.ElapsedMilliseconds} 毫秒"); + } + + private static async Task BulkInsertAsync(ISession session, PreparedStatement prepared, List records) + { + var tasks = new List(); + var batch = new BatchStatement(); + + for (int i = 0; i < records.Count; i++) + { + var record = records[i]; + var boundStatement = prepared.Bind( + record.Id, + record.ClientId, + record.Message, + record.DeviceNo, + (int)record.Type, + record.MessageId); + + // 设置一致性级别为ONE以提高性能 + boundStatement.SetConsistencyLevel(ConsistencyLevel.One); + + batch.Add(boundStatement); + + // 当达到批处理大小时执行 + if (batch.Statements.Count() >= 1000 || i == records.Count - 1) + { + tasks.Add(session.ExecuteAsync(batch)); + batch = new BatchStatement(); + } + } + + // 等待所有批处理完成 + await Task.WhenAll(tasks); + } +} diff --git a/services/JiShe.CollectBus.Application/ScheduledMeterReading/BasicScheduledMeterReadingService.cs b/services/JiShe.CollectBus.Application/ScheduledMeterReading/BasicScheduledMeterReadingService.cs new file mode 100644 index 0000000..42f0f1c --- /dev/null +++ b/services/JiShe.CollectBus.Application/ScheduledMeterReading/BasicScheduledMeterReadingService.cs @@ -0,0 +1,1182 @@ +using DotNetCore.CAP; +using JiShe.CollectBus.Ammeters; +using JiShe.CollectBus.Application.Contracts; +using JiShe.CollectBus.Common.BuildSendDatas; +using JiShe.CollectBus.Common.Consts; +using JiShe.CollectBus.Common.DeviceBalanceControl; +using JiShe.CollectBus.Common.Enums; +using JiShe.CollectBus.Common.Extensions; +using JiShe.CollectBus.Common.Helpers; +using JiShe.CollectBus.Common.Models; +using JiShe.CollectBus.GatherItem; +using JiShe.CollectBus.IotSystems.MessageIssueds; +using JiShe.CollectBus.IotSystems.MeterReadingRecords; +using JiShe.CollectBus.IotSystems.Watermeter; +using JiShe.CollectBus.Kafka; +using JiShe.CollectBus.Kafka.Producer; +using JiShe.CollectBus.Protocol.Contracts; +using JiShe.CollectBus.RedisDataCache; +using JiShe.CollectBus.Repository.MeterReadingRecord; +using Mapster; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JiShe.CollectBus.IoTDB.Interface; +using static FreeSql.Internal.GlobalFilter; + +namespace JiShe.CollectBus.ScheduledMeterReading +{ + /// + /// 定时采集服务 + /// + public abstract class BasicScheduledMeterReadingService : CollectBusAppService, IScheduledMeterReadingService + { + private readonly ILogger _logger; + private readonly IIoTDBProvider _dbProvider; + private readonly IMeterReadingRecordRepository _meterReadingRecordRepository; + private readonly IProducerService _producerService; + private readonly IRedisDataCacheService _redisDataCacheService; + private readonly KafkaOptionConfig _kafkaOptions; + + public BasicScheduledMeterReadingService( + ILogger logger, + IMeterReadingRecordRepository meterReadingRecordRepository, + IProducerService producerService, + IRedisDataCacheService redisDataCacheService, + IIoTDBProvider dbProvider, + IOptions kafkaOptions) + { + _logger = logger; + _dbProvider = dbProvider; + _meterReadingRecordRepository = meterReadingRecordRepository; + _producerService = producerService; + _redisDataCacheService = redisDataCacheService; + _kafkaOptions = kafkaOptions.Value; + } + + /// + /// 系统类型 + /// + public abstract string SystemType { get; } + + /// + /// 应用服务器部署标记 + /// + public abstract string ServerTagName { get; } + + /// + ///电表日冻结采集项 + /// + protected List DayFreezeCodes = new List() { "0D_3", "0D_4", "0D_161", "0D_162", "0D_163", "0D_164", "0D_165", "0D_166", "0D_167", "0D_168", "0C_149", }; + + /// + /// 电表月冻结采集项 + /// + protected List MonthFreezeCodes = new List() { "0D_177", "0D_178", "0D_179", "0D_180", "0D_181", "0D_182", "0D_183", "0D_184", "0D_193", "0D_195", }; + + /// + /// 获取采集项列表 + /// + /// + public virtual Task> GetGatherItemByDataTypes() + { + throw new NotImplementedException($"{nameof(GetGatherItemByDataTypes)}请根据不同系统类型进行实现"); + } + + /// + /// 构建待处理的下发指令任务处理 + /// + /// + public virtual async Task CreateToBeIssueTasks() + { + var redisCacheKey = $"{RedisConst.CacheBasicDirectoryKey}{SystemType}:{ServerTagName}:TaskInfo:*"; + var taskInfos = await FreeRedisProvider.Instance.KeysAsync(redisCacheKey); + if (taskInfos == null || taskInfos.Length <= 0) + { + _logger.LogWarning($"{nameof(CreateToBeIssueTasks)} 构建待处理的下发指令任务处理时没有缓存数据,-101"); + return; + } + + foreach (var item in taskInfos) + { + var tasksToBeIssueModel = await FreeRedisProvider.Instance.GetAsync(item); + if (tasksToBeIssueModel == null) + { + _logger.LogWarning($"{nameof(CreateToBeIssueTasks)} 构建待处理的下发指令任务处理时Key=>{item}没有缓存数据,102"); + continue; + } + + //item 为 CacheTasksToBeIssuedKey 对应的缓存待下发的指令生产任务数据Redis Key tempArryay[0]=>CollectBus,tempArryay[1]=>SystemTypeConst,tempArryay[2]=>TaskInfo,tempArryay[3]=>表计类别,tempArryay[4]=>采集频率 + var tempArryay = item.Split(":"); + string meteryType = tempArryay[4];//表计类别 + int timeDensity = Convert.ToInt32(tempArryay[5]);//采集频率 + if (timeDensity > 15) + { + timeDensity = 15; + } + + //检查任务时间节点,由于定时任务10秒钟运行一次,需要判定当前时间是否在任务时间节点内,不在则跳过 + if (!IsTaskTime(tasksToBeIssueModel.NextTaskTime, timeDensity)) + { + _logger.LogInformation($"{nameof(CreateToBeIssueTasks)} 构建待处理的下发指令任务处理时Key=>{item}时间节点不在当前时间范围内,103"); + continue; + } + + var meterTypes = EnumExtensions.ToEnumDictionary(); + + if (meteryType == MeterTypeEnum.Ammeter.ToString()) + { + var timer = Stopwatch.StartNew(); + + //获取对应频率中的所有电表信息 + var redisCacheMeterInfoHashKeyTemp = $"{string.Format(RedisConst.CacheMeterInfoHashKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, timeDensity)}"; + var redisCacheMeterInfoSetIndexKeyTemp = $"{string.Format(RedisConst.CacheMeterInfoSetIndexKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, timeDensity)}"; + var redisCacheMeterInfoZSetScoresIndexKeyTemp = $"{string.Format(RedisConst.CacheMeterInfoZSetScoresIndexKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, timeDensity)}"; + + List meterInfos = new List(); + decimal? cursor = null; + string member = null; + bool hasNext; + do + { + var page = await _redisDataCacheService.GetAllPagedData( + redisCacheMeterInfoHashKeyTemp, + redisCacheMeterInfoZSetScoresIndexKeyTemp, + pageSize: 1000, + lastScore: cursor, + lastMember: member); + + meterInfos.AddRange(page.Items); + cursor = page.HasNext ? page.NextScore : null; + member = page.HasNext ? page.NextMember : null; + hasNext = page.HasNext; + } while (hasNext); + + if (meterInfos == null || meterInfos.Count <= 0) + { + timer.Stop(); + _logger.LogError($"{nameof(CreateToBeIssueTasks)} {timeDensity}分钟采集待下发任务创建失败,没有获取到缓存信息,-105"); + return; + } + //await AmmerterScheduledMeterReadingIssued(timeDensity, meterInfos); + + + //处理数据 + //await DeviceGroupBalanceControl.ProcessGenericListAsync( + // items: meterInfos, + // deviceIdSelector: data => data.FocusAddress, + // processor: (data, groupIndex) => + // { + // _ = AmmerterCreatePublishTask(timeDensity, data, groupIndex, tasksToBeIssueModel.NextTaskTime.ToString("yyyyMMddHHmmss")); + // } + //); + + + + await DeviceGroupBalanceControl.ProcessWithThrottleAsync( + items: meterInfos, + deviceIdSelector: data => data.FocusAddress, + processor: (data, groupIndex) => + { + AmmerterCreatePublishTask(timeDensity, data, groupIndex, tasksToBeIssueModel.NextTaskTime.ToString("yyyyMMddHHmmss")); + } + ); + + timer.Stop(); + _logger.LogInformation($"{nameof(CreateToBeIssueTasks)} {timeDensity}分钟采集待下发任务创建完成,{timer.ElapsedMilliseconds},总共{meterInfos.Count}表计信息"); + + } + else if (meteryType == MeterTypeEnum.WaterMeter.ToString()) + { + //todo 水表任务创建待处理 + //await WatermeterScheduledMeterReadingIssued(timeDensity, meterInfos); + } + else + { + _logger.LogError($"{nameof(CreateToBeIssueTasks)} {timeDensity}分钟采集待下发任务创建失败,没有获取到缓存信息,-106"); + } + + _logger.LogInformation($"{nameof(CreateToBeIssueTasks)} {timeDensity}分钟采集待下发任务创建完成"); + + + + //根据当前的采集频率和类型,重新更新下一个任务点,把任务的创建源固定在当前逻辑,避免任务处理的逻辑异常导致任务创建失败。 + tasksToBeIssueModel.NextTaskTime = tasksToBeIssueModel.NextTaskTime.AddMinutes(timeDensity); + await FreeRedisProvider.Instance.SetAsync(item, tasksToBeIssueModel); + } + } + + #region 电表采集处理 + + /// + /// 获取电表信息 + /// + /// 采集端Code + /// + public virtual Task> GetAmmeterInfoList(string gatherCode = "") + { + throw new NotImplementedException($"{nameof(GetAmmeterInfoList)}请根据不同系统类型进行实现"); + } + + /// + /// 初始化电表缓存数据 + /// + /// 采集端Code + /// + public virtual async Task InitAmmeterCacheData(string gatherCode = "") + { +#if DEBUG + //var timeDensity = "15"; + //string tempCacheMeterInfoKey = $"CollectBus:{"{0}:{1}"}:MeterInfo:{"{2}"}:{"{3}"}"; + ////获取缓存中的电表信息 + //var redisKeyList = $"{string.Format(tempCacheMeterInfoKey, SystemType, "JiSheCollectBus", MeterTypeEnum.Ammeter, timeDensity)}*"; + + //var oneMinutekeyList = await FreeRedisProvider.Instance.KeysAsync(redisKeyList); + //var tempMeterInfos = await GetMeterRedisCacheListData(oneMinutekeyList, SystemType, ServerTagName, timeDensity, MeterTypeEnum.Ammeter); + ////List focusAddressDataLista = new List(); + //List meterInfos = new List(); + //foreach (var item in tempMeterInfos) + //{ + // var tempData = item.Adapt(); + // tempData.FocusId = item.FocusID; + // tempData.MeterId = item.Id; + // meterInfos.Add(tempData); + // //focusAddressDataLista.Add(item.FocusAddress); + //} + + + + var timeDensity = "15"; + var redisCacheMeterInfoHashKeyTemp = $"{string.Format(RedisConst.CacheMeterInfoHashKey, SystemType, "JiSheCollectBus2", MeterTypeEnum.Ammeter, timeDensity)}"; + var redisCacheMeterInfoSetIndexKeyTemp = $"{string.Format(RedisConst.CacheMeterInfoSetIndexKey, SystemType, "JiSheCollectBus2", MeterTypeEnum.Ammeter, timeDensity)}"; + var redisCacheMeterInfoZSetScoresIndexKeyTemp = $"{string.Format(RedisConst.CacheMeterInfoZSetScoresIndexKey, SystemType, "JiSheCollectBus2", MeterTypeEnum.Ammeter, timeDensity)}"; + + List meterInfos = new List(); + List focusAddressDataLista = new List(); + var timer1 = Stopwatch.StartNew(); + //decimal? cursor = null; + //string member = null; + //bool hasNext; + //do + //{ + // var page = await _redisDataCacheService.GetAllPagedDataOptimized( + // redisCacheMeterInfoHashKeyTemp, + // redisCacheMeterInfoZSetScoresIndexKeyTemp, + // pageSize: 1000, + // lastScore: cursor, + // lastMember: member); + + // meterInfos.AddRange(page.Items); + // cursor = page.HasNext ? page.NextScore : null; + // member = page.HasNext ? page.NextMember : null; + // hasNext = page.HasNext; + //} while (hasNext); + + var allIds = new HashSet(); + decimal? score = null; + string member = null; + + while (true) + { + var page = await _redisDataCacheService.GetAllPagedData( + redisCacheMeterInfoHashKeyTemp, + redisCacheMeterInfoZSetScoresIndexKeyTemp, + pageSize: 1000, + lastScore: score, + lastMember: member); + + meterInfos.AddRange(page.Items); + focusAddressDataLista.AddRange(page.Items.Select(d => d.FocusAddress)); + foreach (var item in page.Items) + { + if (!allIds.Add(item.MemberId)) + { + _logger.LogError($"{item.MemberId}Duplicate data found!"); + } + } + if (!page.HasNext) break; + score = page.NextScore; + member = page.NextMember; + } + + + timer1.Stop(); + _logger.LogError($"读取数据更花费时间{timer1.ElapsedMilliseconds}毫秒"); + DeviceGroupBalanceControl.InitializeCache(focusAddressDataLista, _kafkaOptions.NumPartitions); + return; +#else + var meterInfos = await GetAmmeterInfoList(gatherCode); +#endif + + if (meterInfos == null || meterInfos.Count <= 0) + { + throw new NullReferenceException($"{nameof(InitAmmeterCacheData)} 初始化电表缓存数据时,电表数据为空"); + } + + //获取采集项类型数据 + var gatherItemInfos = await GetGatherItemByDataTypes(); + if (gatherItemInfos == null || gatherItemInfos.Count <= 0) + { + throw new NullReferenceException($"{nameof(InitAmmeterCacheData)} 初始化电表缓存数据时,采集项类型数据为空"); + } + var timer = Stopwatch.StartNew(); + + List focusAddressDataList = new List();//用于处理Kafka主题分区数据的分发和处理。 + + //根据采集频率分组,获得采集频率分组 + var meterInfoGroupByTimeDensity = meterInfos.GroupBy(d => d.TimeDensity); + foreach (var itemTimeDensity in meterInfoGroupByTimeDensity) + { + var redisCacheMeterInfoHashKey = $"{string.Format(RedisConst.CacheMeterInfoHashKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, itemTimeDensity.Key)}"; + var redisCacheMeterInfoSetIndexKey = $"{string.Format(RedisConst.CacheMeterInfoSetIndexKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, itemTimeDensity.Key)}"; + var redisCacheMeterInfoZSetScoresIndexKey = $"{string.Format(RedisConst.CacheMeterInfoZSetScoresIndexKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, itemTimeDensity.Key)}"; + + List ammeterInfos = new List(); + //将表计信息根据集中器分组,获得集中器号 + var meterInfoGroup = itemTimeDensity.GroupBy(x => x.FocusAddress).ToList(); + foreach (var item in meterInfoGroup) + { + if (string.IsNullOrWhiteSpace(item.Key))//集中器号为空,跳过 + { + continue; + } + + focusAddressDataList.Add(item.Key); + + // var redisCacheKey = $"{string.Format(RedisConst.CacheMeterInfoKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, itemTimeDensity.Key)}{item.Key}"; + +#if DEBUG + //每次缓存时,删除缓存,避免缓存数据有不准确的问题 + //await FreeRedisProvider.Instance.DelAsync(redisCacheKey); +#else + //每次缓存时,删除缓存,避免缓存数据有不准确的问题 + //await FreeRedisProvider.Instance.DelAsync(redisCacheKey); +#endif + + //Dictionary keyValuePairs = new Dictionary(); + foreach (var ammeter in item) + { + //处理ItemCode + if (string.IsNullOrWhiteSpace(ammeter.ItemCodes) && !string.IsNullOrWhiteSpace(ammeter.DataTypes)) + { + var itemArr = ammeter.DataTypes.Split(',').ToList(); + + #region 拼接采集项 + List itemCodeList = new List(); + foreach (var dataType in itemArr) + { + var excludeItemCode = "10_98,10_94";//TODO 排除透明转发:尖峰平谷时段、跳合闸,特殊电表 + var gatherItem = gatherItemInfos.FirstOrDefault(f => f.DataType.Equals(dataType)); + if (gatherItem != null) + { + if (!excludeItemCode.Contains(gatherItem.ItemCode)) + { + itemCodeList.Add(gatherItem.ItemCode); + } + } + + #region 特殊电表采集项编号处理 + if (itemArr.Exists(e => e.Equals("95"))) //德力西DTS + { + itemCodeList.Add("10_95"); + } + if (itemArr.Exists(e => e.Equals("109")))//WAVE_109 + { + itemCodeList.Add("10_109"); + } + #endregion + } + #endregion + + + + ammeter.ItemCodes = itemCodeList.Serialize();//转换成JSON字符串 + + if (!string.IsNullOrWhiteSpace(ammeter.ItemCodes)) + { + ammeter.ItemCodes = ammeter.ItemCodes.Replace("WAVE_109", "10_109"); + } + } + + ammeterInfos.Add(ammeter); + //keyValuePairs.TryAdd($"{ammeter.MeterId}", ammeter); + } + //await FreeRedisProvider.Instance.HSetAsync(redisCacheKey, keyValuePairs); + } + + await _redisDataCacheService.BatchInsertDataAsync( + redisCacheMeterInfoHashKey, + redisCacheMeterInfoSetIndexKey, + redisCacheMeterInfoZSetScoresIndexKey, ammeterInfos); + + //在缓存表信息数据的时候,新增下一个时间的自动处理任务,1分钟后执行所有的采集频率任务 + TasksToBeIssueModel nextTask = new TasksToBeIssueModel() + { + TimeDensity = itemTimeDensity.Key, + NextTaskTime = DateTime.Now.AddMinutes(1) + }; + + var taskRedisCacheKey = string.Format(RedisConst.CacheTasksToBeIssuedKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, itemTimeDensity.Key); + await FreeRedisProvider.Instance.SetAsync(taskRedisCacheKey, nextTask); + } + + //初始化设备组负载控制 + if (focusAddressDataList == null || focusAddressDataList.Count <= 0) + { + _logger.LogError($"{nameof(InitAmmeterCacheData)} 初始化设备组负载控制失败,没有找到对应的设备信息"); + + } + else + { + DeviceGroupBalanceControl.InitializeCache(focusAddressDataList, _kafkaOptions.NumPartitions); + } + + timer.Stop(); + + _logger.LogInformation($"{nameof(InitAmmeterCacheData)} 初始化电表缓存数据完成,耗时{timer.ElapsedMilliseconds}毫秒"); + } + + /// + /// 1分钟采集电表数据,只获取任务数据下发,不构建任务 + /// + /// + public virtual async Task AmmeterScheduledMeterOneMinuteReading() + { + //获取缓存中的电表信息 + int timeDensity = 1; + var currentTime = DateTime.Now; + + var redisKeyList = GetTelemetryPacketCacheKeyPrefix(timeDensity, MeterTypeEnum.Ammeter); + var oneMinutekeyList = await FreeRedisProvider.Instance.KeysAsync(redisKeyList); + if (oneMinutekeyList == null || oneMinutekeyList.Length <= 0) + { + _logger.LogError($"{nameof(AmmeterScheduledMeterOneMinuteReading)} {timeDensity}分钟采集电表数据处理时没有获取到缓存信息,-101"); + return; + } + + //获取下发任务缓存数据 + Dictionary> meterTaskInfos = await GetMeterRedisCacheDictionaryData(oneMinutekeyList, SystemType, ServerTagName, timeDensity.ToString(), MeterTypeEnum.Ammeter); + if (meterTaskInfos == null || meterTaskInfos.Count <= 0) + { + _logger.LogError($"{nameof(AmmeterScheduledMeterOneMinuteReading)} {timeDensity}分钟采集电表数据处理时没有获取到缓存信息,-102"); + return; + } + + List meterTaskInfosList = new List(); + + //将取出的缓存任务数据发送到Kafka消息队列中 + foreach (var focusItem in meterTaskInfos) + { + foreach (var ammerterItem in focusItem.Value) + { + var tempMsg = new ScheduledMeterReadingIssuedEventMessage() + { + MessageHexString = ammerterItem.Value.IssuedMessageHexString, + MessageId = ammerterItem.Value.IssuedMessageId, + FocusAddress = ammerterItem.Value.FocusAddress, + TimeDensity = timeDensity.ToString(), + }; + //_ = _producerBus.PublishDelayAsync(TimeSpan.FromMicroseconds(500), ProtocolConst.AmmeterSubscriberWorkerOneMinuteIssuedEventName, tempMsg); + + _ = _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerOneMinuteIssuedEventName, tempMsg); + //_= _producerBus.Publish(tempMsg); + + + meterTaskInfosList.Add(ammerterItem.Value); + } + } + if (meterTaskInfosList != null && meterTaskInfosList.Count > 0) + { + //_dbProvider.SwitchSessionPool(true); + //await _dbProvider.InsertAsync(meterTaskInfosList); + + await _meterReadingRecordRepository.InsertManyAsync(meterTaskInfosList, currentTime); + } + + ////删除任务数据 + //await FreeRedisProvider.Instance.DelAsync(oneMinutekeyList); + //await CacheNextTaskData(timeDensity, MeterTypeEnum.Ammeter); + + + _logger.LogInformation($"{nameof(AmmeterScheduledMeterOneMinuteReading)} {timeDensity}分钟采集电表数据处理完成"); + + } + + /// + /// 5分钟采集电表数据 + /// + /// + public virtual async Task AmmeterScheduledMeterFiveMinuteReading() + { + //获取缓存中的电表信息 + int timeDensity = 5; + var currentTime = DateTime.Now; + + var redisKeyList = GetTelemetryPacketCacheKeyPrefix(timeDensity, MeterTypeEnum.Ammeter); + var fiveMinutekeyList = await FreeRedisProvider.Instance.KeysAsync(redisKeyList); + if (fiveMinutekeyList == null || fiveMinutekeyList.Length <= 0) + { + _logger.LogError($"{nameof(AmmeterScheduledMeterOneMinuteReading)} {timeDensity}分钟采集电表数据处理时没有获取到缓存信息,-101"); + return; + } + + //获取下发任务缓存数据 + Dictionary> meterTaskInfos = await GetMeterRedisCacheDictionaryData(fiveMinutekeyList, SystemType, ServerTagName, timeDensity.ToString(), MeterTypeEnum.Ammeter); + if (meterTaskInfos == null || meterTaskInfos.Count <= 0) + { + _logger.LogError($"{nameof(AmmeterScheduledMeterOneMinuteReading)} {timeDensity}分钟采集电表数据处理时没有获取到缓存信息,-102"); + return; + } + + List meterTaskInfosList = new List(); + + //将取出的缓存任务数据发送到Kafka消息队列中 + foreach (var focusItem in meterTaskInfos) + { + foreach (var ammerterItem in focusItem.Value) + { + var tempMsg = new ScheduledMeterReadingIssuedEventMessage() + { + MessageHexString = ammerterItem.Value.IssuedMessageHexString, + MessageId = ammerterItem.Value.IssuedMessageId, + FocusAddress = ammerterItem.Value.FocusAddress, + TimeDensity = timeDensity.ToString(), + }; + //_ = _producerBus.PublishDelayAsync(TimeSpan.FromMicroseconds(500), ProtocolConst.AmmeterSubscriberWorkerFiveMinuteIssuedEventName, tempMsg); + + _ = _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerFiveMinuteIssuedEventName, tempMsg); + + //_ = _producerBus.Publish(tempMsg); + + meterTaskInfosList.Add(ammerterItem.Value); + } + } + if (meterTaskInfosList != null && meterTaskInfosList.Count > 0) + { + await _meterReadingRecordRepository.InsertManyAsync(meterTaskInfosList, currentTime); + } + + ////删除任务数据 + //await FreeRedisProvider.Instance.DelAsync(fiveMinutekeyList); + + ////缓存下一个时间的任务 + //await CacheNextTaskData(timeDensity, MeterTypeEnum.Ammeter); + + _logger.LogInformation($"{nameof(AmmeterScheduledMeterFiveMinuteReading)} {timeDensity}分钟采集电表数据处理完成"); + } + + /// + /// 15分钟采集电表数据 + /// + /// + public virtual async Task AmmeterScheduledMeterFifteenMinuteReading() + { + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + + //获取缓存中的电表信息 + int timeDensity = 15; + var currentDateTime = DateTime.Now; + + // 自动计算最佳并发度 + int recommendedThreads = DeviceGroupBalanceControl.CalculateOptimalThreadCount(); + + var options = new ParallelOptions + { + MaxDegreeOfParallelism = recommendedThreads, + }; + string taskBatch = "20250417155016"; + Parallel.For(0, _kafkaOptions.NumPartitions, options, async groupIndex => + { + Console.WriteLine($"15分钟采集电表数据:{groupIndex}"); + var redisCacheTelemetryPacketInfoHashKey = $"{string.Format(RedisConst.CacheTelemetryPacketInfoHashKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, timeDensity, groupIndex, taskBatch)}"; + var redisCacheTelemetryPacketInfoSetIndexKey = $"{string.Format(RedisConst.CacheTelemetryPacketInfoSetIndexKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, timeDensity, groupIndex, taskBatch)}"; + var redisCacheTelemetryPacketInfoZSetScoresIndexKey = $"{string.Format(RedisConst.CacheTelemetryPacketInfoZSetScoresIndexKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, timeDensity, groupIndex, taskBatch)}"; + + List meterInfos = new List(); + decimal? cursor = null; + string member = null; + bool hasNext; + do + { + var page = await _redisDataCacheService.GetAllPagedData( + redisCacheTelemetryPacketInfoHashKey, + redisCacheTelemetryPacketInfoZSetScoresIndexKey, + pageSize: 1000, + lastScore: cursor, + lastMember: member); + + meterInfos.AddRange(page.Items); + cursor = page.HasNext ? page.NextScore : null; + member = page.HasNext ? page.NextMember : null; + hasNext = page.HasNext; + + await DeviceGroupBalanceControl.ProcessWithThrottleAsync( + items: meterInfos, + deviceIdSelector: data => data.FocusAddress, + processor: (data, groupIndex) => + { + _= KafkaProducerIssuedMessage(ProtocolConst.AmmeterSubscriberWorkerFifteenMinuteIssuedEventName,data, groupIndex); + } + ); + + } while (hasNext); + }); + + + + //var redisKeyList = GetTelemetryPacketCacheKeyPrefix(timeDensity, MeterTypeEnum.Ammeter); + //var fifteenMinutekeyList = await FreeRedisProvider.Instance.KeysAsync(redisKeyList); + //if (fifteenMinutekeyList == null || fifteenMinutekeyList.Length <= 0) + //{ + // _logger.LogError($"{nameof(AmmeterScheduledMeterOneMinuteReading)} {timeDensity}分钟采集电表数据处理时没有获取到缓存信息,-101"); + // return; + //} + + ////获取下发任务缓存数据 + //Dictionary> meterTaskInfos = await GetMeterRedisCacheDictionaryData(fifteenMinutekeyList, SystemType, ServerTagName, timeDensity.ToString(), MeterTypeEnum.Ammeter); + //if (meterTaskInfos == null || meterTaskInfos.Count <= 0) + //{ + // _logger.LogError($"{nameof(AmmeterScheduledMeterOneMinuteReading)} {timeDensity}分钟采集电表数据处理时没有获取到缓存信息,-102"); + // return; + //} + + //List meterTaskInfosList = new List(); + + ////将取出的缓存任务数据发送到Kafka消息队列中 + //foreach (var focusItem in meterTaskInfos) + //{ + // foreach (var ammerterItem in focusItem.Value) + // { + // var tempMsg = new ScheduledMeterReadingIssuedEventMessage() + // { + // MessageHexString = ammerterItem.Value.IssuedMessageHexString, + // MessageId = ammerterItem.Value.IssuedMessageId, + // FocusAddress = ammerterItem.Value.FocusAddress, + // TimeDensity = timeDensity.ToString(), + // }; + // //_ = _producerBus.PublishDelayAsync(TimeSpan.FromMicroseconds(500), ProtocolConst.AmmeterSubscriberWorkerFifteenMinuteIssuedEventName, tempMsg); + + // _ = _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerFifteenMinuteIssuedEventName, tempMsg); + + // //_ = _producerBus.Publish(tempMsg); + + // meterTaskInfosList.Add(ammerterItem.Value); + // } + //} + //if (meterTaskInfosList != null && meterTaskInfosList.Count > 0) + //{ + // await _meterReadingRecordRepository.InsertManyAsync(meterTaskInfosList, currentDateTime); + //} + + + //stopwatch.Stop(); + + //_logger.LogError($"{nameof(AmmeterScheduledMeterFifteenMinuteReading)} {timeDensity}分钟采集电表数据处理完成,共消耗{stopwatch.ElapsedMilliseconds}毫秒。"); + } + + + + /// + /// 电表创建发布任务 + /// + /// 采集频率 + /// 集中器号hash分组的集中器集合数据 + /// 集中器所在分组 + /// 时间格式的任务批次名称 + /// + private void AmmerterCreatePublishTask(int timeDensity + , AmmeterInfo ammeterInfo, int groupIndex, string taskBatch) + { + var handlerPacketBuilder = TelemetryPacketBuilder.AFNHandlersDictionary; + //todo 检查需要待补抄的电表的时间点信息,保存到需要待补抄的缓存中。如果此线程异常,该如何补偿? + + var currentTime = DateTime.Now; + var pendingCopyReadTime = currentTime.AddMinutes(timeDensity); + + var redisCacheTelemetryPacketInfoHashKey = $"{string.Format(RedisConst.CacheTelemetryPacketInfoHashKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, timeDensity, groupIndex, taskBatch)}"; + var redisCacheTelemetryPacketInfoSetIndexKey = $"{string.Format(RedisConst.CacheTelemetryPacketInfoSetIndexKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, timeDensity, groupIndex, taskBatch)}"; + var redisCacheTelemetryPacketInfoZSetScoresIndexKey = $"{string.Format(RedisConst.CacheTelemetryPacketInfoZSetScoresIndexKey, SystemType, ServerTagName, MeterTypeEnum.Ammeter, timeDensity, groupIndex, taskBatch)}"; + + if (string.IsNullOrWhiteSpace(ammeterInfo.ItemCodes)) + { + // _logger.LogError($"{nameof(AmmerterCreatePublishTask)} 集中器{ammeterInfo.FocusAddress}的电表{ammeterInfo.Name}数据采集指令生成失败,采集项为空,-101"); + return; + } + + //载波的不处理 + if (ammeterInfo.MeteringPort == (int)MeterLinkProtocolEnum.Carrierwave) + { + //_logger.LogError($"{nameof(AmmerterCreatePublishTask)} 集中器{ammeterInfo.FocusAddress}的电表{ammeterInfo.Name}数据采集指令生成失败,载波不处理,-102"); + return; + } + + if (ammeterInfo.State.Equals(2)) + { + //_logger.LogWarning($"{nameof(AmmerterCreatePublishTask)} {ammeterInfo.Name} 集中器{ammeterInfo.FocusAddress}的电表{ammeterInfo.Name}状态为禁用,不处理"); + return; + } + + ////排除1天未在线的集中器生成指令 或 排除集中器配置为自动上报的集中器 + //if (!IsGennerateCmd(ammeter.LastTime, -1)) + //{ + // _logger.LogInformation($"{nameof(CreatePublishTask)} 集中器{ammeter.FocusAddress}的电表{ammeter.Name},采集时间:{ammeter.LastTime},已超过1天未在线,不生成指令"); + // continue; + //} + + if (string.IsNullOrWhiteSpace(ammeterInfo.AreaCode)) + { + // _logger.LogError($"{nameof(AmmerterCreatePublishTask)} 表ID:{ammeterInfo.ID},集中器通信区号为空"); + return; + } + if (string.IsNullOrWhiteSpace(ammeterInfo.Address)) + { + //_logger.LogError($"{nameof(AmmerterCreatePublishTask)} 表ID:{ammeterInfo.ID},集中器通信地址为空"); + return; + } + if (Convert.ToInt32(ammeterInfo.Address) > 65535) + { + //_logger.LogError($"{nameof(AmmerterCreatePublishTask)} 表ID:{ammeterInfo.ID},集中器通信地址无效,确保大于65535"); + return; + } + if (ammeterInfo.MeteringCode <= 0 || ammeterInfo.MeteringCode > 33) + { + //_logger.LogError($"{nameof(AmmerterCreatePublishTask)} 表ID:{ammeterInfo.ID},非有效测量点号({ammeterInfo.MeteringCode})"); + return; + } + + List tempCodes = ammeterInfo.ItemCodes.Deserialize>()!; + + //TODO:自动上报数据只主动采集1类数据。 + if (ammeterInfo.AutomaticReport.Equals(1)) + { + var tempSubCodes = new List(); + if (tempCodes.Contains("0C_49")) + { + tempSubCodes.Add("0C_49"); + } + + if (tempSubCodes.Contains("0C_149")) + { + tempSubCodes.Add("0C_149"); + } + + if (ammeterInfo.ItemCodes.Contains("10_97")) + { + tempSubCodes.Add("10_97"); + } + + if (tempSubCodes == null || tempSubCodes.Count <= 0) + { + //_logger.LogInformation($"{nameof(AmmerterCreatePublishTask)} 集中器{ammeterInfo.FocusAddress}的电表{ammeterInfo.Name}自动上报数据主动采集1类数据时数据类型为空"); + return; + } + else + { + tempCodes = tempSubCodes; + } + } + + //Dictionary keyValuePairs = new Dictionary(); + List taskList = new List(); + + foreach (var tempItem in tempCodes) + { + //排除已发送日冻结和月冻结采集项配置 + if (DayFreezeCodes.Contains(tempItem)) + { + continue; + } + + if (MonthFreezeCodes.Contains(tempItem)) + { + continue; + } + + var itemCodeArr = tempItem.Split('_'); + var aFNStr = itemCodeArr[0]; + var aFN = (AFN)aFNStr.HexToDec(); + var fn = int.Parse(itemCodeArr[1]); + byte[] dataInfos = null; + if (ammeterInfo.AutomaticReport.Equals(1) && aFN == AFN.请求实时数据) + { + //实时数据 + dataInfos = Build3761SendData.BuildAmmeterReadRealTimeDataSendCmd(ammeterInfo.FocusAddress, ammeterInfo.MeteringCode, (ATypeOfDataItems)fn); + } + else + { + string methonCode = $"AFN{aFNStr}_Fn_Send"; + //特殊表暂不处理 + if (handlerPacketBuilder != null && handlerPacketBuilder.TryGetValue(methonCode + , out var handler)) + { + dataInfos = handler(new TelemetryPacketRequest() + { + FocusAddress = ammeterInfo.FocusAddress, + Fn = fn, + Pn = ammeterInfo.MeteringCode + }); + } + else + { + //_logger.LogWarning($"{nameof(AmmerterCreatePublishTask)} 集中器{ammeterInfo.FocusAddress}的电表{ammeterInfo.Name}采集项{tempItem}无效编码。"); + continue; + } + } + //TODO:特殊表 + + if (dataInfos == null || dataInfos.Length <= 0) + { + //_logger.LogWarning($"{nameof(AmmerterCreatePublishTask)} 集中器{ammeterInfo.FocusAddress}的电表{ammeterInfo.Name}采集项{tempItem}未能正确获取报文。"); + continue; + } + + + + var meterReadingRecords = new MeterReadingTelemetryPacketInfo() + { + ProjectID = ammeterInfo.ProjectID, + DatabaseBusiID = ammeterInfo.DatabaseBusiID, + PendingCopyReadTime = pendingCopyReadTime, + CreationTime = currentTime, + MeterAddress = ammeterInfo.AmmerterAddress, + MeterId = ammeterInfo.MeterId, + MeterType = MeterTypeEnum.Ammeter, + FocusAddress = ammeterInfo.FocusAddress, + FocusId = ammeterInfo.FocusId, + AFN = aFN, + Fn = fn, + ItemCode = tempItem, + TaskMark = CommonHelper.GetTaskMark((int)aFN, fn, ammeterInfo.MeteringCode), + ManualOrNot = false, + Pn = ammeterInfo.MeteringCode, + IssuedMessageId = GuidGenerator.Create().ToString(), + IssuedMessageHexString = Convert.ToHexString(dataInfos), + }; + + //meterReadingRecords.CreateDataId(GuidGenerator.Create()); + + taskList.Add(meterReadingRecords); + } + //TimeSpan timeSpan = TimeSpan.FromMicroseconds(5); + //await Task.Delay(timeSpan); + + //return keyValuePairs; + // await FreeRedisProvider.Instance.HSetAsync(redisCacheKey, keyValuePairs); + + //using (var pipe = FreeRedisProvider.Instance.StartPipe()) + //{ + // pipe.HSet(redisCacheKey, keyValuePairs); + // object[] ret = pipe.EndPipe(); + //} + if (taskList == null + || taskList.Count() <= 0 + || string.IsNullOrWhiteSpace(redisCacheTelemetryPacketInfoHashKey) + || string.IsNullOrWhiteSpace(redisCacheTelemetryPacketInfoSetIndexKey) + || string.IsNullOrWhiteSpace(redisCacheTelemetryPacketInfoZSetScoresIndexKey)) + { + _logger.LogError($"{nameof(AmmerterCreatePublishTask)} {ammeterInfo.Name}的写入参数异常,{redisCacheTelemetryPacketInfoHashKey}:{redisCacheTelemetryPacketInfoSetIndexKey}:{redisCacheTelemetryPacketInfoZSetScoresIndexKey},-101"); + return; + } + + using (var pipe = FreeRedisProvider.Instance.StartPipe()) + { + foreach (var item in taskList) + { + // 主数据存储Hash + pipe.HSet(redisCacheTelemetryPacketInfoHashKey, item.MemberId, item.Serialize()); + + // Set索引缓存 + pipe.SAdd(redisCacheTelemetryPacketInfoSetIndexKey, item.MemberId); + + // ZSET索引缓存Key + pipe.ZAdd(redisCacheTelemetryPacketInfoZSetScoresIndexKey, item.ScoreValue, item.MemberId); + } + pipe.EndPipe(); + } + + //await _redisDataCacheService.BatchInsertDataAsync( + // redisCacheTelemetryPacketInfoHashKey, + // redisCacheTelemetryPacketInfoSetIndexKey, + // redisCacheTelemetryPacketInfoZSetScoresIndexKey, + // taskList); + } + + /// + /// Kafka 推送消息 + /// + /// 主题名称 + /// 任务记录 + /// 对应分区,也就是集中器号所在的分组序号 + /// + private async Task KafkaProducerIssuedMessage(string topicName, + MeterReadingTelemetryPacketInfo taskRecord,int partition) + { + if (string.IsNullOrWhiteSpace(topicName) || taskRecord == null) + { + throw new Exception($"{nameof(KafkaProducerIssuedMessage)} 推送消息失败,参数异常,-101"); + } + + await _producerService.ProduceAsync(topicName, partition, taskRecord); + } + + private async Task AmmerterCreatePublishTask(int timeDensity, MeterTypeEnum meterType) + { + var currentDateTime = DateTime.Now; + + var redisKeyList = GetTelemetryPacketCacheKeyPrefix(timeDensity, meterType); + + //FreeRedisProvider.Instance.key() + + var fifteenMinutekeyList = await FreeRedisProvider.Instance.KeysAsync(redisKeyList); + if (fifteenMinutekeyList == null || fifteenMinutekeyList.Length <= 0) + { + _logger.LogError($"{nameof(AmmeterScheduledMeterOneMinuteReading)} {timeDensity}分钟采集电表数据处理时没有获取到缓存信息,-101"); + return; + } + + //获取下发任务缓存数据 + Dictionary> meterTaskInfos = await GetMeterRedisCacheDictionaryData(fifteenMinutekeyList, SystemType, ServerTagName, timeDensity.ToString(), meterType); + if (meterTaskInfos == null || meterTaskInfos.Count <= 0) + { + _logger.LogError($"{nameof(AmmeterScheduledMeterOneMinuteReading)} {timeDensity}分钟采集电表数据处理时没有获取到缓存信息,-102"); + return; + } + + List meterTaskInfosList = new List(); + + //将取出的缓存任务数据发送到Kafka消息队列中 + foreach (var focusItem in meterTaskInfos) + { + foreach (var ammerterItem in focusItem.Value) + { + var tempMsg = new ScheduledMeterReadingIssuedEventMessage() + { + MessageHexString = ammerterItem.Value.IssuedMessageHexString, + MessageId = ammerterItem.Value.IssuedMessageId, + FocusAddress = ammerterItem.Value.FocusAddress, + TimeDensity = timeDensity.ToString(), + }; + //_ = _producerBus.PublishDelayAsync(TimeSpan.FromMicroseconds(500), ProtocolConst.AmmeterSubscriberWorkerFifteenMinuteIssuedEventName, tempMsg); + + _ = _producerService.ProduceAsync(ProtocolConst.AmmeterSubscriberWorkerFifteenMinuteIssuedEventName, tempMsg); + + //_ = _producerBus.Publish(tempMsg); + + meterTaskInfosList.Add(ammerterItem.Value); + } + } + if (meterTaskInfosList != null && meterTaskInfosList.Count > 0) + { + await _meterReadingRecordRepository.InsertManyAsync(meterTaskInfosList, currentDateTime); + } + } + + #endregion + + + #region 水表采集处理 + + /// + /// 获取水表信息 + /// + /// 采集端Code + /// + public virtual Task> GetWatermeterInfoList(string gatherCode = "") + { + throw new NotImplementedException($"{nameof(GetWatermeterInfoList)}请根据不同系统类型进行实现"); + } + + /// + /// 初始化水表缓存数据 + /// + /// 采集端Code + /// + public virtual async Task InitWatermeterCacheData(string gatherCode = "") + { + var meterInfos = await GetWatermeterInfoList(gatherCode); + if (meterInfos == null || meterInfos.Count <= 0) + { + throw new NullReferenceException($"{nameof(InitWatermeterCacheData)} 初始化水表缓存数据时,水表数据为空"); + } + + //获取采集项类型数据 + var gatherItemInfos = await GetGatherItemByDataTypes(); + if (gatherItemInfos == null || gatherItemInfos.Count <= 0) + { + throw new NullReferenceException($"{nameof(InitAmmeterCacheData)} 初始化水表缓存数据时,采集项类型数据为空"); + } + + //根据采集频率分组,获得采集频率分组 + var meterInfoGroupByTimeDensity = meterInfos.GroupBy(d => d.TimeDensity); + foreach (var itemTimeDensity in meterInfoGroupByTimeDensity) + { + //将表计信息根据集中器分组,获得集中器号 + var meterInfoGroup = itemTimeDensity.GroupBy(x => x.FocusAddress).ToList(); + foreach (var item in meterInfoGroup) + { + if (string.IsNullOrWhiteSpace(item.Key)) + { + continue; + } + + var redisCacheKey = $"{string.Format(RedisConst.CacheMeterInfoHashKey, SystemType, ServerTagName, MeterTypeEnum.WaterMeter, itemTimeDensity.Key)}{item.Key}"; + Dictionary keyValuePairs = new Dictionary(); + foreach (var subItem in item) + { + + keyValuePairs.TryAdd($"{subItem.MeterId}", subItem); + } + await FreeRedisProvider.Instance.HSetAsync(redisCacheKey, keyValuePairs); + } + + //在缓存表信息数据的时候,新增下一个时间的自动处理任务,1分钟后执行 + TasksToBeIssueModel nextTask = new TasksToBeIssueModel() + { + TimeDensity = itemTimeDensity.Key, + NextTaskTime = DateTime.Now.AddMinutes(1) + }; + + var taskRedisCacheKey = string.Format(RedisConst.CacheTasksToBeIssuedKey, SystemType, ServerTagName, MeterTypeEnum.WaterMeter, itemTimeDensity.Key); + await FreeRedisProvider.Instance.SetAsync(taskRedisCacheKey, nextTask); + } + _logger.LogInformation($"{nameof(InitAmmeterCacheData)} 初始化水表缓存数据完成"); + } + + /// + /// 水表数据采集 + /// + /// + public virtual async Task WatermeterScheduledMeterAutoReading() + { + //获取缓存中的水表信息 + int timeDensity = 1; + var redisKeyList = GetTelemetryPacketCacheKeyPrefix(timeDensity, MeterTypeEnum.WaterMeter); + var oneMinutekeyList = await FreeRedisProvider.Instance.KeysAsync(redisKeyList); + if (oneMinutekeyList == null || oneMinutekeyList.Length <= 0) + { + _logger.LogError($"{nameof(WatermeterScheduledMeterAutoReading)} {timeDensity}分钟采集水表数据处理时没有获取到缓存信息,-101"); + return; + } + + //获取下发任务缓存数据 + Dictionary> meterTaskInfos = await GetMeterRedisCacheDictionaryData(oneMinutekeyList, SystemType, ServerTagName, timeDensity.ToString(), MeterTypeEnum.WaterMeter); + if (meterTaskInfos == null || meterTaskInfos.Count <= 0) + { + _logger.LogError($"{nameof(WatermeterScheduledMeterAutoReading)} {timeDensity}分钟采集水表数据处理时没有获取到缓存信息,-102"); + return; + } + + List meterTaskInfosList = new List(); + + //将取出的缓存任务数据发送到Kafka消息队列中 + foreach (var focusItem in meterTaskInfos) + { + foreach (var ammerterItem in focusItem.Value) + { + var tempMsg = new ScheduledMeterReadingIssuedEventMessage() + { + MessageHexString = ammerterItem.Value.IssuedMessageHexString, + MessageId = ammerterItem.Value.IssuedMessageId, + FocusAddress = ammerterItem.Value.FocusAddress, + TimeDensity = timeDensity.ToString(), + }; + + //await _producerBus.PublishAsync(ProtocolConst.WatermeterSubscriberWorkerAutoReadingIssuedEventName, tempMsg); + + //_ = _producerBus.Publish(tempMsg); + + + meterTaskInfosList.Add(ammerterItem.Value); + } + } + if (meterTaskInfosList != null && meterTaskInfosList.Count > 0) + { + await _meterReadingRecordRepository.InsertManyAsync(meterTaskInfosList); + } + + ////删除任务数据 + //await FreeRedisProvider.Instance.DelAsync(oneMinutekeyList); + + ////缓存下一个时间的任务 + //await CacheNextTaskData(timeDensity, MeterTypeEnum.WaterMeter); + + + _logger.LogInformation($"{nameof(WatermeterScheduledMeterAutoReading)} {timeDensity}分钟采集水表数据处理完成"); + } + + #endregion + + + #region 公共处理方法 + + + /// + /// 判断是否需要生成采集指令 + /// + /// + /// + /// + private bool IsTaskTime(DateTime nextTaskTime, int timeDensity = 0) + { + if (DateTime.Now.AddMinutes(timeDensity) >= nextTaskTime) + { + return true; + } + + return false; + } + + ///// + ///// 指定时间对比当前时间 + ///// + ///// + ///// + ///// + //private bool IsGennerateCmd(DateTime lastTime, int subtrahend = 0) + //{ + // if (DateTime.Now.AddDays(subtrahend) >= lastTime)//当前时间减去一天,大于等于最后在线时间,不再生成该集中器下表生成采集指令 + // return false; + // return true; + //} + + ///// + ///// 缓存下一个时间的任务 + ///// + ///// 采集频率 + ///// 表类型 + ///// + //private async Task CacheNextTaskData(int timeDensity, MeterTypeEnum meterType) + //{ + // //缓存下一个时间的任务 + // TasksToBeIssueModel nextTask = new TasksToBeIssueModel() + // { + // TimeDensity = timeDensity, + // NextTask = DateTime.Now.AddMinutes(timeDensity) + // }; + + // var redisCacheKey = string.Format(RedisConst.CacheTasksToBeIssuedKey, SystemType, ServerTagName, meterType, timeDensity); + // await FreeRedisProvider.Instance.SetAsync(redisCacheKey, nextTask); + //} + + + /// + /// 获取缓存表计下发指令缓存key前缀 + /// + /// + /// + /// + private string GetTelemetryPacketCacheKeyPrefix(int timeDensity, MeterTypeEnum meterType) + { + return $"{string.Format(RedisConst.CacheTelemetryPacketInfoHashKey, SystemType, ServerTagName, meterType, timeDensity)}*"; + } + + #endregion + + } +} diff --git a/services/JiShe.CollectBus.Application/ScheduledMeterReading/EnergySystemScheduledMeterReadingService.cs b/services/JiShe.CollectBus.Application/ScheduledMeterReading/EnergySystemScheduledMeterReadingService.cs new file mode 100644 index 0000000..733f14a --- /dev/null +++ b/services/JiShe.CollectBus.Application/ScheduledMeterReading/EnergySystemScheduledMeterReadingService.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Confluent.Kafka; +using DotNetCore.CAP; +using JiShe.CollectBus.Ammeters; +using JiShe.CollectBus.Application.Contracts; +using JiShe.CollectBus.Common.Consts; +using JiShe.CollectBus.Common.DeviceBalanceControl; +using JiShe.CollectBus.Common.Helpers; +using JiShe.CollectBus.FreeSql; +using JiShe.CollectBus.GatherItem; +using JiShe.CollectBus.IoTDB.Interface; +using JiShe.CollectBus.IotSystems.Devices; +using JiShe.CollectBus.IotSystems.MessageIssueds; +using JiShe.CollectBus.IotSystems.MeterReadingRecords; +using JiShe.CollectBus.IotSystems.Watermeter; +using JiShe.CollectBus.Kafka; +using JiShe.CollectBus.Kafka.Producer; +using JiShe.CollectBus.Repository; +using JiShe.CollectBus.Repository.MeterReadingRecord; +using MassTransit; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Uow; + +namespace JiShe.CollectBus.ScheduledMeterReading +{ + /// + /// 能耗系统定时采集服务 + /// + [AllowAnonymous] + //[Route($"/energy/app/scheduled")] + public class EnergySystemScheduledMeterReadingService : BasicScheduledMeterReadingService + { + string serverTagName = string.Empty; + public EnergySystemScheduledMeterReadingService( + ILogger logger, + IIoTDBProvider dbProvider, + IMeterReadingRecordRepository meterReadingRecordRepository, + IOptions kafkaOptions, + IProducerService producerService, + IRedisDataCacheService redisDataCacheService) + : base(logger, + meterReadingRecordRepository, + producerService, + redisDataCacheService, + dbProvider, + kafkaOptions) + { + serverTagName = kafkaOptions.Value.ServerTagName; + } + + public sealed override string SystemType => SystemTypeConst.Energy; + + public sealed override string ServerTagName => serverTagName; + + /// + /// 获取采集项列表 + /// + /// + public override async Task> GetGatherItemByDataTypes() + { + try + { + string sql = $"SELECT DataType,ItemCode FROM TB_GatherItem(NOLOCK) WHERE [State]=0"; + return await SqlProvider.Instance.Change(DbEnum.EnergyDB) + .Ado + .QueryAsync(sql, null); + } + catch + { + return null; + } + } + + /// + /// 获取电表信息 + /// + /// 采集端Code + /// + //[HttpGet] + //[Route($"ammeter/list")] + public override async Task> GetAmmeterInfoList(string gatherCode = "V4-Gather-8890") + { + + //List ammeterInfos = new List(); + //ammeterInfos.Add(new AmmeterInfo() + //{ + // Baudrate = 2400, + // FocusAddress = "402440506", + // Name = "张家祠工务(三相电表)", + // FocusId = 95780, + // DatabaseBusiID = 1, + // MeteringCode = 1, + // AmmerterAddress = "402410040506", + // MeterId = 127035, + // TypeName = 3, + // DataTypes = "449,503,581,582,583,584,585,586,587,588,589,590,591,592,593,594,597,598,599,600,601,602,603,604,605,606,607,608,661,663,677,679", + // TimeDensity = 15, + //}); + //ammeterInfos.Add(new AmmeterInfo() + //{ + // Baudrate = 2400, + // FocusAddress = "542400504", + // Name = "五号配(长芦二所四排)(单相电表)", + // FocusId = 69280, + // DatabaseBusiID = 1, + // MeteringCode = 2, + // AmmerterAddress = "542410000504", + // MeterId = 95594, + // TypeName = 1, + // DataTypes = "581,589,592,597,601", + // TimeDensity = 15, + //}); + + //return ammeterInfos; + + string sql = $@"SELECT C.ID as MeterId,C.Name,C.FocusID as FocusId,C.SingleRate,C.MeteringCode,C.Code AS BrandType,C.Baudrate,C.Password,C.MeteringPort,C.[Address] AS AmmerterAddress,C.TypeName,C.Protocol,C.TripState,C.[State],B.[Address],B.AreaCode,B.AutomaticReport,D.DataTypes,B.TimeDensity,A.GatherCode,C.Special,C.[ProjectID],B.AbnormalState,B.LastTime,CONCAT(B.AreaCode, B.[Address]) AS FocusAddress,(select top 1 DatabaseBusiID from TB_Project where ID = B.ProjectID) AS DatabaseBusiID + FROM TB_GatherInfo(NOLOCK) AS A + INNER JOIN TB_FocusInfo(NOLOCK) AS B ON A.ID = B.GatherInfoID AND B.RemoveState >= 0 AND B.State>=0 + INNER JOIN TB_AmmeterInfo(NOLOCK) AS C ON B.ID = C.FocusID AND C.State>= 0 AND C.State<100 + INNER JOIN TB_AmmeterGatherItem(NOLOCK) AS D ON C.ID = D.AmmeterID AND D.State>=0 + WHERE 1=1 and C.Special = 0 "; + //TODO 记得移除特殊表过滤 + + //if (!string.IsNullOrWhiteSpace(gatherCode)) + //{ + // sql = $@"{sql} AND A.GatherCode = '{gatherCode}'"; + //} + return await SqlProvider.Instance.Change(DbEnum.EnergyDB) + .Ado + .QueryAsync(sql); + } + + /// + /// 获取水表信息 + /// + /// 采集端Code + /// + //[HttpGet] + //[Route($"ammeter/list")] + public override async Task> GetWatermeterInfoList(string gatherCode = "V4-Gather-8890") + { + string sql = $@"SELECT + A.ID as MeterId, + A.Name, + A.FocusID as FocusId, + A.MeteringCode, + A.Baudrate, + A.MeteringPort, + A.[Address] AS MeterAddress, + A.[Password], + A.TypeName, + A.Protocol, + A.Code, + A.LinkType, + A.HaveValve, + A.MeterType AS MeterTypeName, + A.MeterBrand, + A.TimesRate, + A.TimeDensity, + A.TripState, + B.[Address], + B.AreaCode, + B.AutomaticReport, + A.[State], + C.GatherCode, + A.[ProjectID], + B.AbnormalState, + B.LastTime, + CONCAT(B.AreaCode, B.[Address]) AS FocusAddress, + (select top 1 DatabaseBusiID from TB_Project where ID = b.ProjectID) AS DatabaseBusiID + FROM [dbo].[TB_WatermeterInfo](NOLOCK) AS A + INNER JOIN [dbo].[TB_FocusInfo](NOLOCK) AS B ON A.FocusID=B.ID AND B.RemoveState >= 0 AND B.State>=0 + INNER JOIN [dbo].[TB_GatherInfo](NOLOCK) AS C ON B.GatherInfoID=C.ID + WHERE A.State>=0 AND A.State<100 "; + + if (!string.IsNullOrWhiteSpace(gatherCode)) + { + sql = $@"{sql} AND C.GatherCode= '{gatherCode}'"; + } + return await SqlProvider.Instance.Change(DbEnum.EnergyDB) + .Ado + .QueryAsync(sql); + } + + + /// + /// 测试设备分组均衡控制算法 + /// + /// + /// + [HttpGet] + public async Task TestDeviceGroupBalanceControl(int deviceCount = 200000) + { + var deviceList = new List(); + for (int i = 0; i < deviceCount; i++) + { + deviceList.Add($"Device_{Guid.NewGuid()}"); + } + + // 初始化缓存 + DeviceGroupBalanceControl.InitializeCache(deviceList); + + // 打印分布统计 + DeviceGroupBalanceControl.PrintDistributionStats(); + + await Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/services/JiShe.CollectBus.Application/Subscribers/SubscriberAppService.cs b/services/JiShe.CollectBus.Application/Subscribers/SubscriberAppService.cs new file mode 100644 index 0000000..eb3abd9 --- /dev/null +++ b/services/JiShe.CollectBus.Application/Subscribers/SubscriberAppService.cs @@ -0,0 +1,224 @@ +using DotNetCore.CAP; +using JiShe.CollectBus.Common.Consts; +using JiShe.CollectBus.Common.Enums; +using JiShe.CollectBus.Common.Helpers; +using JiShe.CollectBus.Common.Models; +using JiShe.CollectBus.IotSystems.Devices; +using JiShe.CollectBus.IotSystems.MessageReceiveds; +using JiShe.CollectBus.IotSystems.MeterReadingRecords; +using JiShe.CollectBus.Kafka; +using JiShe.CollectBus.Kafka.Attributes; +using JiShe.CollectBus.Protocol.Contracts; +using JiShe.CollectBus.Protocol.Contracts.Interfaces; +using JiShe.CollectBus.Protocol.Contracts.Models; +using JiShe.CollectBus.Repository.MeterReadingRecord; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; +using JiShe.CollectBus.IoTDB.Interface; +using TouchSocket.Sockets; +using Volo.Abp.Domain.Repositories; + +namespace JiShe.CollectBus.Subscribers +{ + public class SubscriberAppService : CollectBusAppService, ISubscriberAppService, ICapSubscribe, IKafkaSubscribe + { + private readonly ILogger _logger; + private readonly ITcpService _tcpService; + private readonly IServiceProvider _serviceProvider; + private readonly IRepository _messageReceivedLoginEventRepository; + private readonly IRepository _messageReceivedHeartbeatEventRepository; + private readonly IRepository _messageReceivedEventRepository; + private readonly IRepository _deviceRepository; + private readonly IMeterReadingRecordRepository _meterReadingRecordsRepository; + private readonly IIoTDBProvider _dbProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The TCP service. + /// The service provider. + /// The message received login event repository. + /// The message received heartbeat event repository. + /// The message received event repository. + /// The device repository. + /// The device repository. + public SubscriberAppService(ILogger logger, + ITcpService tcpService, IServiceProvider serviceProvider, + IRepository messageReceivedLoginEventRepository, + IRepository messageReceivedHeartbeatEventRepository, + IRepository messageReceivedEventRepository, + IRepository deviceRepository, + IIoTDBProvider dbProvider, + IMeterReadingRecordRepository meterReadingRecordsRepository) + { + _logger = logger; + _tcpService = tcpService; + _serviceProvider = serviceProvider; + _messageReceivedLoginEventRepository = messageReceivedLoginEventRepository; + _messageReceivedHeartbeatEventRepository = messageReceivedHeartbeatEventRepository; + _messageReceivedEventRepository = messageReceivedEventRepository; + _deviceRepository = deviceRepository; + _meterReadingRecordsRepository = meterReadingRecordsRepository; + _dbProvider = dbProvider; + } + + [KafkaSubscribe(ProtocolConst.SubscriberLoginIssuedEventName)] + //[CapSubscribe(ProtocolConst.SubscriberLoginIssuedEventName)] + public async Task LoginIssuedEvent(IssuedEventMessage issuedEventMessage) + { + bool isAck = false; + switch (issuedEventMessage.Type) + { + case IssuedEventType.Heartbeat: + break; + case IssuedEventType.Login: + _logger.LogWarning($"集中器地址{issuedEventMessage.ClientId} 登录回复下发内容:{issuedEventMessage.Serialize()}"); + var loginEntity = await _messageReceivedLoginEventRepository.GetAsync(a => a.MessageId == issuedEventMessage.MessageId); + loginEntity.AckTime = Clock.Now; + loginEntity.IsAck = true; + await _messageReceivedLoginEventRepository.UpdateAsync(loginEntity); + isAck = true; + break; + case IssuedEventType.Data: + break; + default: + throw new ArgumentOutOfRangeException(); + } + + //var device = await _deviceRepository.FindAsync(a => a.Number == issuedEventMessage.DeviceNo); + //if (device != null) + //{ + // await _tcpService.SendAsync(device.ClientId, issuedEventMessage.Message); + //} + + await _tcpService.SendAsync(issuedEventMessage.ClientId, issuedEventMessage.Message); + return isAck? SubscribeAck.Success(): SubscribeAck.Fail(); + } + + [KafkaSubscribe(ProtocolConst.SubscriberHeartbeatIssuedEventName)] + //[CapSubscribe(ProtocolConst.SubscriberHeartbeatIssuedEventName)] + public async Task HeartbeatIssuedEvent(IssuedEventMessage issuedEventMessage) + { + bool isAck = false; + switch (issuedEventMessage.Type) + { + case IssuedEventType.Heartbeat: + _logger.LogWarning($"集中器地址{issuedEventMessage.ClientId} 心跳回复下发内容:{issuedEventMessage.Serialize()}"); + var heartbeatEntity = await _messageReceivedHeartbeatEventRepository.GetAsync(a => a.MessageId == issuedEventMessage.MessageId); + heartbeatEntity.AckTime = Clock.Now; + heartbeatEntity.IsAck = true; + await _messageReceivedHeartbeatEventRepository.UpdateAsync(heartbeatEntity); + isAck = true; + break; + case IssuedEventType.Data: + break; + default: + throw new ArgumentOutOfRangeException(); + } + + //var device = await _deviceRepository.FindAsync(a => a.Number == issuedEventMessage.DeviceNo); + //if (device != null) + //{ + // await _tcpService.SendAsync(device.ClientId, issuedEventMessage.Message); + //} + + await _tcpService.SendAsync(issuedEventMessage.ClientId, issuedEventMessage.Message); + return isAck ? SubscribeAck.Success() : SubscribeAck.Fail(); + } + + [KafkaSubscribe(ProtocolConst.SubscriberReceivedEventName)] + //[CapSubscribe(ProtocolConst.SubscriberReceivedEventName)] + public async Task ReceivedEvent(MessageReceived receivedMessage) + { + var currentTime = Clock.Now; + + var protocolPlugin = _serviceProvider.GetKeyedService("StandardProtocolPlugin"); + if (protocolPlugin == null) + { + _logger.LogError("协议不存在!"); + } + else + { + + //todo 会根据不同的协议进行解析,然后做业务处理 + TB3761 fN = await protocolPlugin.AnalyzeAsync(receivedMessage); + if(fN == null) + { + Logger.LogError($"数据解析失败:{receivedMessage.Serialize()}"); + return SubscribeAck.Success(); + } + var tb3761FN = fN.FnList.FirstOrDefault(); + if (tb3761FN == null) + { + Logger.LogError($"数据解析失败:{receivedMessage.Serialize()}"); + return SubscribeAck.Success(); + } + + //报文入库 + var entity = new MeterReadingRecords() + { + ReceivedMessageHexString = receivedMessage.MessageHexString, + AFN = fN.Afn, + Fn = tb3761FN.Fn, + Pn = 0, + FocusAddress = "", + MeterAddress = "", + }; + + //如果没数据,则插入,有数据则更新 + var updateEntity = await _meterReadingRecordsRepository.FirOrDefaultAsync(entity, currentTime); + if (updateEntity == null) + { + await _meterReadingRecordsRepository.InsertAsync(entity, currentTime); + } + + + //_dbProvider.InsertAsync(); + //todo 查找是否有下发任务 + + //await _messageReceivedEventRepository.InsertAsync(receivedMessage); + + + } + return SubscribeAck.Success(); + } + + [KafkaSubscribe(ProtocolConst.SubscriberHeartbeatReceivedEventName)] + //[CapSubscribe(ProtocolConst.SubscriberHeartbeatReceivedEventName)] + public async Task ReceivedHeartbeatEvent(MessageReceivedHeartbeat receivedHeartbeatMessage) + { + var protocolPlugin = _serviceProvider.GetKeyedService("StandardProtocolPlugin"); + if (protocolPlugin == null) + { + _logger.LogError("协议不存在!"); + } + else + { + await protocolPlugin.HeartbeatAsync(receivedHeartbeatMessage); + await _messageReceivedHeartbeatEventRepository.InsertAsync(receivedHeartbeatMessage); + } + return SubscribeAck.Success(); + } + + [KafkaSubscribe(ProtocolConst.SubscriberLoginReceivedEventName)] + //[CapSubscribe(ProtocolConst.SubscriberLoginReceivedEventName)] + public async Task ReceivedLoginEvent(MessageReceivedLogin receivedLoginMessage) + { + var protocolPlugin = _serviceProvider.GetKeyedService("StandardProtocolPlugin"); + if (protocolPlugin == null) + { + _logger.LogError("协议不存在!"); + } + else + { + await protocolPlugin.LoginAsync(receivedLoginMessage); + await _messageReceivedLoginEventRepository.InsertAsync(receivedLoginMessage); + } + return SubscribeAck.Success(); + } + } +} diff --git a/services/JiShe.CollectBus.Application/Subscribers/WorkerSubscriberAppService.cs b/services/JiShe.CollectBus.Application/Subscribers/WorkerSubscriberAppService.cs new file mode 100644 index 0000000..65ecc01 --- /dev/null +++ b/services/JiShe.CollectBus.Application/Subscribers/WorkerSubscriberAppService.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using DeviceDetectorNET.Parser.Device; +using DotNetCore.CAP; +using JiShe.CollectBus.Common.Consts; +using JiShe.CollectBus.Common.Enums; +using JiShe.CollectBus.IotSystems.Devices; +using JiShe.CollectBus.IotSystems.MessageIssueds; +using JiShe.CollectBus.IotSystems.MessageReceiveds; +using JiShe.CollectBus.IotSystems.MeterReadingRecords; +using JiShe.CollectBus.Kafka; +using JiShe.CollectBus.Kafka.Attributes; +using JiShe.CollectBus.Protocol.Contracts; +using JiShe.CollectBus.Protocol.Contracts.Interfaces; +using JiShe.CollectBus.Repository.MeterReadingRecord; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using TouchSocket.Sockets; +using Volo.Abp.Caching; +using Volo.Abp.Domain.Repositories; + +namespace JiShe.CollectBus.Subscribers +{ + /// + /// 定时抄读任务消息消费订阅 + /// + [Route($"/worker/app/subscriber")] + public class WorkerSubscriberAppService : CollectBusAppService, IWorkerSubscriberAppService, ICapSubscribe, IKafkaSubscribe + { + private readonly ILogger _logger; + private readonly ITcpService _tcpService; + private readonly IServiceProvider _serviceProvider; + private readonly IRepository _deviceRepository; + private readonly IMeterReadingRecordRepository _meterReadingRecordsRepository; + + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The TCP service. + /// The Device pepository. + /// The service provider. + public WorkerSubscriberAppService(ILogger logger, + ITcpService tcpService, + IRepository deviceRepository, + IMeterReadingRecordRepository meterReadingRecordsRepository, + IServiceProvider serviceProvider) + { + _logger = logger; + _tcpService = tcpService; + _serviceProvider = serviceProvider; + _deviceRepository = deviceRepository; + _meterReadingRecordsRepository = meterReadingRecordsRepository; + } + + + #region 电表消息采集 + + /// + /// 一分钟定时抄读任务消息消费订阅 + /// + /// + /// + [HttpPost] + [Route("ammeter/oneminute/issued-event")] + [KafkaSubscribe(ProtocolConst.AmmeterSubscriberWorkerOneMinuteIssuedEventName)] + //[CapSubscribe(ProtocolConst.AmmeterSubscriberWorkerOneMinuteIssuedEventName)] + public async Task AmmeterScheduledMeterOneMinuteReadingIssuedEvent(ScheduledMeterReadingIssuedEventMessage receivedMessage) + { + _logger.LogInformation("1分钟采集电表数据下行消息消费队列开始处理"); + var protocolPlugin = _serviceProvider.GetKeyedService("StandardProtocolPlugin"); + if (protocolPlugin == null) + { + _logger.LogError("【1分钟采集电表数据下行消息消费队列开始处理】协议不存在!"); + } + else + { + var device = await _deviceRepository.FirstOrDefaultAsync(a => a.Number == receivedMessage.FocusAddress); + if (device != null) + { + await _tcpService.SendAsync(device.ClientId, Convert.FromHexString(receivedMessage.MessageHexString)); + + } + } + return SubscribeAck.Success(); + } + + /// + /// 5分钟采集电表数据下行消息消费订阅 + /// + /// + /// + [HttpPost] + [Route("ammeter/fiveminute/issued-event")] + [KafkaSubscribe(ProtocolConst.AmmeterSubscriberWorkerFiveMinuteIssuedEventName)] + //[CapSubscribe(ProtocolConst.AmmeterSubscriberWorkerFiveMinuteIssuedEventName)] + public async Task AmmeterScheduledMeterFiveMinuteReadingIssuedEvent(ScheduledMeterReadingIssuedEventMessage receivedMessage) + { + _logger.LogInformation("5分钟采集电表数据下行消息消费队列开始处理"); + var protocolPlugin = _serviceProvider.GetKeyedService("StandardProtocolPlugin"); + if (protocolPlugin == null) + { + _logger.LogError("【5分钟采集电表数据下行消息消费队列开始处理】协议不存在!"); + } + else + { + var device = await _deviceRepository.FirstOrDefaultAsync(a => a.Number == receivedMessage.FocusAddress); + if (device != null) + { + await _tcpService.SendAsync(device.ClientId, Convert.FromHexString(receivedMessage.MessageHexString)); + + } + } + return SubscribeAck.Success(); + } + + /// + /// 15分钟采集电表数据下行消息消费订阅 + /// + /// + /// + [HttpPost] + [Route("ammeter/fifteenminute/issued-event")] + [KafkaSubscribe(ProtocolConst.AmmeterSubscriberWorkerFifteenMinuteIssuedEventName)] + //[CapSubscribe(ProtocolConst.AmmeterSubscriberWorkerFifteenMinuteIssuedEventName)] + public async Task AmmeterScheduledMeterFifteenMinuteReadingIssuedEvent(ScheduledMeterReadingIssuedEventMessage receivedMessage) + { + _logger.LogInformation("15分钟采集电表数据下行消息消费队列开始处理"); + try + { + var protocolPlugin = _serviceProvider.GetKeyedService("StandardProtocolPlugin"); + if (protocolPlugin == null) + { + _logger.LogError("【15分钟采集电表数据下行消息消费队列开始处理】协议不存在!"); + } + else + { + var device = await _deviceRepository.FirstOrDefaultAsync(a => a.Number == receivedMessage.FocusAddress); + if (device != null) + { + await _tcpService.SendAsync(device.ClientId, Convert.FromHexString(receivedMessage.MessageHexString)); + + } + } + return SubscribeAck.Success(); + } + catch (Exception ex) + { + + throw ex; + } + } + #endregion + + #region 水表消息采集 + + /// + /// 水表数据下行消息消费订阅 + /// + /// + /// + [HttpPost] + [Route("watermeter/fifteenminute/issued-event")] + [KafkaSubscribe(ProtocolConst.WatermeterSubscriberWorkerAutoReadingIssuedEventName)] + //[CapSubscribe(ProtocolConst.WatermeterSubscriberWorkerAutoReadingIssuedEventName)] + public async Task WatermeterSubscriberWorkerAutoReadingIssuedEvent(ScheduledMeterReadingIssuedEventMessage receivedMessage) + { + _logger.LogInformation("15分钟采集水表数据下行消息消费队列开始处理"); + var protocolPlugin = _serviceProvider.GetKeyedService("StandardProtocolPlugin"); + if (protocolPlugin == null) + { + _logger.LogError("【15分钟采集水表数据下行消息消费队列开始处理】协议不存在!"); + } + else + { + var device = await _deviceRepository.FindAsync(a => a.Number == receivedMessage.FocusAddress); + if (device != null) + { + await _tcpService.SendAsync(device.ClientId, Convert.FromHexString(receivedMessage.MessageHexString)); + } + } + return SubscribeAck.Success(); + } + #endregion + } +} diff --git a/services/JiShe.CollectBus.Application/Workers/CreateToBeIssueTaskWorker.cs b/services/JiShe.CollectBus.Application/Workers/CreateToBeIssueTaskWorker.cs new file mode 100644 index 0000000..b03193a --- /dev/null +++ b/services/JiShe.CollectBus.Application/Workers/CreateToBeIssueTaskWorker.cs @@ -0,0 +1,40 @@ +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using JiShe.CollectBus.Common.Consts; +using JiShe.CollectBus.ScheduledMeterReading; +using Microsoft.Extensions.Logging; +using Volo.Abp.BackgroundWorkers.Hangfire; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; + +namespace JiShe.CollectBus.Workers +{ + /// + /// 构建待处理的下发指令任务处理 + /// + public class CreateToBeIssueTaskWorker : HangfireBackgroundWorkerBase, ITransientDependency, ICollectWorker + { + private readonly ILogger _logger; + private readonly IScheduledMeterReadingService _scheduledMeterReadingService; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// 定时任务 + public CreateToBeIssueTaskWorker(ILogger logger, IScheduledMeterReadingService scheduledMeterReadingService) + { + _logger = logger; + RecurringJobId = nameof(CreateToBeIssueTaskWorker); + CronExpression = "* 10 * * * *"; + this._scheduledMeterReadingService = scheduledMeterReadingService; + } + + + public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken()) + { + // await _scheduledMeterReadingService.CreateToBeIssueTasks(); + } + } +} diff --git a/src/JiShe.CollectBus.Application/Workers/EpiCollectWorker.cs b/services/JiShe.CollectBus.Application/Workers/EpiCollectWorker.cs similarity index 61% rename from src/JiShe.CollectBus.Application/Workers/EpiCollectWorker.cs rename to services/JiShe.CollectBus.Application/Workers/EpiCollectWorker.cs index bbbee85..12f3e37 100644 --- a/src/JiShe.CollectBus.Application/Workers/EpiCollectWorker.cs +++ b/services/JiShe.CollectBus.Application/Workers/EpiCollectWorker.cs @@ -1,11 +1,16 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; +using Hangfire; +using JiShe.CollectBus.Common.Attributes; using Microsoft.Extensions.Logging; using Volo.Abp.BackgroundWorkers.Hangfire; using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; namespace JiShe.CollectBus.Workers { + [IgnoreJob] public class EpiCollectWorker : HangfireBackgroundWorkerBase, ITransientDependency,ICollectWorker { private readonly ILogger _logger; @@ -17,13 +22,19 @@ namespace JiShe.CollectBus.Workers public EpiCollectWorker(ILogger logger) { _logger = logger; + RecurringJobId = nameof(EpiCollectWorker); + CronExpression = Cron.Daily(); + } public override Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken()) { - _logger.LogInformation("Executed MyLogWorker..!"); - return Task.CompletedTask; + using (var uow = LazyServiceProvider.LazyGetRequiredService().Begin()) + { + Logger.LogInformation("Executed MyLogWorker..!"); + return Task.CompletedTask; + } } } } diff --git a/services/JiShe.CollectBus.Application/Workers/SubscriberFifteenMinuteWorker.cs b/services/JiShe.CollectBus.Application/Workers/SubscriberFifteenMinuteWorker.cs new file mode 100644 index 0000000..441b22a --- /dev/null +++ b/services/JiShe.CollectBus.Application/Workers/SubscriberFifteenMinuteWorker.cs @@ -0,0 +1,46 @@ +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using JiShe.CollectBus.ScheduledMeterReading; +using Microsoft.Extensions.Logging; +using Volo.Abp.BackgroundWorkers.Hangfire; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; + +namespace JiShe.CollectBus.Workers +{ + /// + /// 15分钟采集数据 + /// + public class SubscriberFifteenMinuteWorker : HangfireBackgroundWorkerBase, ITransientDependency, ICollectWorker + { + private readonly ILogger _logger; + private readonly IScheduledMeterReadingService _scheduledMeterReadingService; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// 定时任务 + public SubscriberFifteenMinuteWorker(ILogger logger, IScheduledMeterReadingService scheduledMeterReadingService) + { + _logger = logger; + RecurringJobId = nameof(SubscriberFifteenMinuteWorker); + CronExpression = "* 15 * * * *"; + this._scheduledMeterReadingService = scheduledMeterReadingService; + } + + + public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken()) + { + //await _scheduledMeterReadingService.AmmeterScheduledMeterFifteenMinuteReading(); + //await _scheduledMeterReadingService.WatermeterScheduledMeterFifteenMinuteReading(); + + //using (var uow = LazyServiceProvider.LazyGetRequiredService().Begin()) + //{ + // Logger.LogInformation("Executed MyLogWorker..!"); + // return Task.CompletedTask; + //} + } + } +} diff --git a/services/JiShe.CollectBus.Application/Workers/SubscriberFiveMinuteWorker.cs b/services/JiShe.CollectBus.Application/Workers/SubscriberFiveMinuteWorker.cs new file mode 100644 index 0000000..0a61c63 --- /dev/null +++ b/services/JiShe.CollectBus.Application/Workers/SubscriberFiveMinuteWorker.cs @@ -0,0 +1,40 @@ +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using JiShe.CollectBus.ScheduledMeterReading; +using Microsoft.Extensions.Logging; +using Volo.Abp.BackgroundWorkers.Hangfire; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; + +namespace JiShe.CollectBus.Workers +{ + /// + /// 5分钟采集数据 + /// + public class SubscriberFiveMinuteWorker : HangfireBackgroundWorkerBase, ITransientDependency,ICollectWorker + { + private readonly ILogger _logger; + private readonly IScheduledMeterReadingService _scheduledMeterReadingService; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// 定时任务 + public SubscriberFiveMinuteWorker(ILogger logger, IScheduledMeterReadingService scheduledMeterReadingService) + { + _logger = logger; + RecurringJobId = nameof(SubscriberFiveMinuteWorker); + CronExpression = "* 5 * * * *"; + this._scheduledMeterReadingService = scheduledMeterReadingService; + } + + + public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken()) + { + //await _scheduledMeterReadingService.AmmeterScheduledMeterFiveMinuteReading(); + //await _scheduledMeterReadingService.WatermeterScheduledMeterFiveMinuteReading(); + } + } +} diff --git a/services/JiShe.CollectBus.Application/Workers/SubscriberOneMinuteWorker.cs b/services/JiShe.CollectBus.Application/Workers/SubscriberOneMinuteWorker.cs new file mode 100644 index 0000000..8b7cbfd --- /dev/null +++ b/services/JiShe.CollectBus.Application/Workers/SubscriberOneMinuteWorker.cs @@ -0,0 +1,42 @@ +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using JiShe.CollectBus.ScheduledMeterReading; +using Microsoft.Extensions.Logging; +using Volo.Abp.BackgroundWorkers.Hangfire; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; + +namespace JiShe.CollectBus.Workers +{ + /// + /// 1分钟采集数据 + /// + public class SubscriberOneMinuteWorker : HangfireBackgroundWorkerBase, ITransientDependency,ICollectWorker + { + private readonly ILogger _logger; + private readonly IScheduledMeterReadingService _scheduledMeterReadingService; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// 定时任务 + public SubscriberOneMinuteWorker(ILogger logger, IScheduledMeterReadingService scheduledMeterReadingService) + { + _logger = logger; + RecurringJobId = nameof(SubscriberOneMinuteWorker); + CronExpression = "* 1 * * * *"; + this._scheduledMeterReadingService = scheduledMeterReadingService; + } + + + public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken()) + { + //await _scheduledMeterReadingService.AmmeterScheduledMeterOneMinuteReading(); + + //await _scheduledMeterReadingService.WatermeterScheduledMeterOneMinuteReading(); + + } + } +} diff --git a/src/JiShe.CollectBus.Application/cmd3761Matching.txt b/services/JiShe.CollectBus.Application/cmd3761Matching.txt similarity index 100% rename from src/JiShe.CollectBus.Application/cmd3761Matching.txt rename to services/JiShe.CollectBus.Application/cmd3761Matching.txt diff --git a/src/JiShe.CollectBus.Application/cmd3761TdcMatching.txt b/services/JiShe.CollectBus.Application/cmd3761TdcMatching.txt similarity index 100% rename from src/JiShe.CollectBus.Application/cmd3761TdcMatching.txt rename to services/JiShe.CollectBus.Application/cmd3761TdcMatching.txt diff --git a/src/JiShe.CollectBus.DbMigrator/CollectBusDbMigratorModule.cs b/services/JiShe.CollectBus.DbMigrator/CollectBusDbMigratorModule.cs similarity index 100% rename from src/JiShe.CollectBus.DbMigrator/CollectBusDbMigratorModule.cs rename to services/JiShe.CollectBus.DbMigrator/CollectBusDbMigratorModule.cs diff --git a/src/JiShe.CollectBus.DbMigrator/DbMigratorHostedService.cs b/services/JiShe.CollectBus.DbMigrator/DbMigratorHostedService.cs similarity index 100% rename from src/JiShe.CollectBus.DbMigrator/DbMigratorHostedService.cs rename to services/JiShe.CollectBus.DbMigrator/DbMigratorHostedService.cs diff --git a/src/JiShe.CollectBus.DbMigrator/Dockerfile b/services/JiShe.CollectBus.DbMigrator/Dockerfile similarity index 100% rename from src/JiShe.CollectBus.DbMigrator/Dockerfile rename to services/JiShe.CollectBus.DbMigrator/Dockerfile diff --git a/src/JiShe.CollectBus.DbMigrator/FodyWeavers.xml b/services/JiShe.CollectBus.DbMigrator/FodyWeavers.xml similarity index 100% rename from src/JiShe.CollectBus.DbMigrator/FodyWeavers.xml rename to services/JiShe.CollectBus.DbMigrator/FodyWeavers.xml diff --git a/src/JiShe.CollectBus.DbMigrator/JiShe.CollectBus.DbMigrator.abppkg b/services/JiShe.CollectBus.DbMigrator/JiShe.CollectBus.DbMigrator.abppkg similarity index 100% rename from src/JiShe.CollectBus.DbMigrator/JiShe.CollectBus.DbMigrator.abppkg rename to services/JiShe.CollectBus.DbMigrator/JiShe.CollectBus.DbMigrator.abppkg diff --git a/src/JiShe.CollectBus.DbMigrator/JiShe.CollectBus.DbMigrator.csproj b/services/JiShe.CollectBus.DbMigrator/JiShe.CollectBus.DbMigrator.csproj similarity index 81% rename from src/JiShe.CollectBus.DbMigrator/JiShe.CollectBus.DbMigrator.csproj rename to services/JiShe.CollectBus.DbMigrator/JiShe.CollectBus.DbMigrator.csproj index a333f5a..f875a56 100644 --- a/src/JiShe.CollectBus.DbMigrator/JiShe.CollectBus.DbMigrator.csproj +++ b/services/JiShe.CollectBus.DbMigrator/JiShe.CollectBus.DbMigrator.csproj @@ -10,16 +10,16 @@ - - - + + + + - diff --git a/src/JiShe.CollectBus.DbMigrator/Program.cs b/services/JiShe.CollectBus.DbMigrator/Program.cs similarity index 100% rename from src/JiShe.CollectBus.DbMigrator/Program.cs rename to services/JiShe.CollectBus.DbMigrator/Program.cs diff --git a/src/JiShe.CollectBus.DbMigrator/appsettings.json b/services/JiShe.CollectBus.DbMigrator/appsettings.json similarity index 100% rename from src/JiShe.CollectBus.DbMigrator/appsettings.json rename to services/JiShe.CollectBus.DbMigrator/appsettings.json diff --git a/src/JiShe.CollectBus.DbMigrator/appsettings.secrets.json b/services/JiShe.CollectBus.DbMigrator/appsettings.secrets.json similarity index 100% rename from src/JiShe.CollectBus.DbMigrator/appsettings.secrets.json rename to services/JiShe.CollectBus.DbMigrator/appsettings.secrets.json diff --git a/src/JiShe.CollectBus.Domain/Ammeters/AmmeterInfo.cs b/services/JiShe.CollectBus.Domain/Ammeters/AmmeterInfo.cs similarity index 70% rename from src/JiShe.CollectBus.Domain/Ammeters/AmmeterInfo.cs rename to services/JiShe.CollectBus.Domain/Ammeters/AmmeterInfo.cs index e9bc4b5..8b082bb 100644 --- a/src/JiShe.CollectBus.Domain/Ammeters/AmmeterInfo.cs +++ b/services/JiShe.CollectBus.Domain/Ammeters/AmmeterInfo.cs @@ -1,4 +1,6 @@ -using System; +using FreeSql.DataAnnotations; +using JiShe.CollectBus.Common.Models; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -6,82 +8,125 @@ using System.Threading.Tasks; namespace JiShe.CollectBus.Ammeters { - public class AmmeterInfo + public class AmmeterInfo: DeviceCacheBasicModel { - public int ID { get; set; } + /// + /// 关系映射标识,用于ZSet的Member字段和Set的Value字段,具体值可以根据不同业务场景进行定义 + /// + [Column(IsIgnore = true)] + public override string MemberId => $"{FocusId}:{MeterId}"; + + /// + /// ZSet排序索引分数值,具体值可以根据不同业务场景进行定义,例如时间戳 + /// + [Column(IsIgnore = true)] + public override long ScoreValue => ((long)FocusId << 32) | (uint)DateTime.Now.Ticks; + + /// + /// 电表名称 + /// public string Name { get; set; } - public int FocusID { get; set; } + + /// + /// 集中器地址 + /// + public string FocusAddress { get; set; } + + /// + /// 集中器地址 + /// public string Address { get; set; } + + /// + /// 集中器区域代码 + /// public string AreaCode { get; set; } + /// /// 电表类别 (1单相、2三相三线、3三相四线), /// 07协议: 开合闸指令(1A开闸断电,1C单相表合闸,1B多相表合闸) 645 2007 表 /// 97协议://true(合闸);false(跳闸) 545 1997 没有单相多相 之分 "true" ? "9966" : "3355" /// public int TypeName { get; set; } + /// /// 跳合闸状态字段: 0 合闸,1 跳闸 /// 电表:TripState (0 合闸-通电, 1 断开、跳闸); /// public int TripState { get; set; } + /// /// 规约 -电表default(30) 1:97协议,30:07协议 /// public int? Protocol { get; set; } + /// /// 一个集中器下的[MeteringCode]必须唯一。 PN /// public int MeteringCode { get; set; } + /// /// 电表通信地址 /// public string AmmerterAddress { get; set; } + /// /// 波特率 default(2400) /// public int Baudrate { get; set; } + /// /// MeteringPort 端口就几个可以枚举。 /// public int MeteringPort { get; set; } + /// /// 电表密码 /// public string Password { get; set; } + /// /// 采集时间间隔(分钟,如15) /// public int TimeDensity { get; set; } + /// - /// 该电表方案下采集项,如:0D_80 + /// 该电表方案下采集项,JSON格式,如:["0D_80","0D_80"] /// public string ItemCodes { get; set; } + /// /// State表状态: /// 0新装(未下发),1运行(档案下发成功时设置状态值1), 2暂停, 100销表(销表后是否重新启用) /// 特定:State -1 已删除 /// public int State { get; set; } + /// /// 是否自动采集(0:主动采集,1:自动采集) /// public int AutomaticReport { get; set; } + /// /// 该电表方案下采集项编号 /// public string DataTypes { get; set; } + /// /// 品牌型号 /// public string BrandType { get; set; } + /// /// 采集器编号 /// public string GatherCode { get; set; } + /// - /// 是否特殊表 + /// 是否特殊表,1是特殊电表 /// public int Special { get; set; } + /// /// 费率类型,单、多 (SingleRate :单费率(单相表1),多费率(其他0) ,与TypeName字段无关) /// SingleRate ? "单" : "复" @@ -89,11 +134,22 @@ namespace JiShe.CollectBus.Ammeters ///对应 TB_PayPlan.Type: 1复费率,2单费率 /// public bool SingleRate { get; set; } + + /// + /// 项目ID + /// public int ProjectID { get; set; } + + /// + /// 数据库业务ID + /// + public int DatabaseBusiID { get; set; } + /// /// 是否异常集中器 0:正常,1异常 /// public int AbnormalState { get; set; } - public DateTime LastTime { get; set; } + + public DateTime LastTime { get; set; } } } diff --git a/src/JiShe.CollectBus.Application/Ammeters/AmmeterInfo.cs b/services/JiShe.CollectBus.Domain/Ammeters/AmmeterInfoTemp.cs similarity index 78% rename from src/JiShe.CollectBus.Application/Ammeters/AmmeterInfo.cs rename to services/JiShe.CollectBus.Domain/Ammeters/AmmeterInfoTemp.cs index e9bc4b5..fce33fa 100644 --- a/src/JiShe.CollectBus.Application/Ammeters/AmmeterInfo.cs +++ b/services/JiShe.CollectBus.Domain/Ammeters/AmmeterInfoTemp.cs @@ -1,4 +1,5 @@ -using System; +using JiShe.CollectBus.Common.Models; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -6,82 +7,123 @@ using System.Threading.Tasks; namespace JiShe.CollectBus.Ammeters { - public class AmmeterInfo + public class AmmeterInfoTemp { - public int ID { get; set; } - public string Name { get; set; } + /// + /// 集中器Id + /// public int FocusID { get; set; } + + /// + /// 表Id + /// + public int Id { get; set; } + + /// + /// 电表名称 + /// + public string Name { get; set; } + + /// + /// 集中器地址 + /// + public string FocusAddress { get; set; } + + /// + /// 集中器地址 + /// public string Address { get; set; } + + /// + /// 集中器区域代码 + /// public string AreaCode { get; set; } + /// /// 电表类别 (1单相、2三相三线、3三相四线), /// 07协议: 开合闸指令(1A开闸断电,1C单相表合闸,1B多相表合闸) 645 2007 表 /// 97协议://true(合闸);false(跳闸) 545 1997 没有单相多相 之分 "true" ? "9966" : "3355" /// public int TypeName { get; set; } + /// /// 跳合闸状态字段: 0 合闸,1 跳闸 /// 电表:TripState (0 合闸-通电, 1 断开、跳闸); /// public int TripState { get; set; } + /// /// 规约 -电表default(30) 1:97协议,30:07协议 /// public int? Protocol { get; set; } + /// /// 一个集中器下的[MeteringCode]必须唯一。 PN /// public int MeteringCode { get; set; } + /// /// 电表通信地址 /// public string AmmerterAddress { get; set; } + /// /// 波特率 default(2400) /// public int Baudrate { get; set; } + /// /// MeteringPort 端口就几个可以枚举。 /// public int MeteringPort { get; set; } + /// /// 电表密码 /// public string Password { get; set; } + /// /// 采集时间间隔(分钟,如15) /// public int TimeDensity { get; set; } + /// - /// 该电表方案下采集项,如:0D_80 + /// 该电表方案下采集项,JSON格式,如:["0D_80","0D_80"] /// public string ItemCodes { get; set; } + /// /// State表状态: /// 0新装(未下发),1运行(档案下发成功时设置状态值1), 2暂停, 100销表(销表后是否重新启用) /// 特定:State -1 已删除 /// public int State { get; set; } + /// /// 是否自动采集(0:主动采集,1:自动采集) /// public int AutomaticReport { get; set; } + /// /// 该电表方案下采集项编号 /// public string DataTypes { get; set; } + /// /// 品牌型号 /// public string BrandType { get; set; } + /// /// 采集器编号 /// public string GatherCode { get; set; } + /// - /// 是否特殊表 + /// 是否特殊表,1是特殊电表 /// public int Special { get; set; } + /// /// 费率类型,单、多 (SingleRate :单费率(单相表1),多费率(其他0) ,与TypeName字段无关) /// SingleRate ? "单" : "复" @@ -89,11 +131,22 @@ namespace JiShe.CollectBus.Ammeters ///对应 TB_PayPlan.Type: 1复费率,2单费率 /// public bool SingleRate { get; set; } + + /// + /// 项目ID + /// public int ProjectID { get; set; } + + /// + /// 数据库业务ID + /// + public int DatabaseBusiID { get; set; } + /// /// 是否异常集中器 0:正常,1异常 /// public int AbnormalState { get; set; } - public DateTime LastTime { get; set; } + + public DateTime LastTime { get; set; } } } diff --git a/services/JiShe.CollectBus.Domain/Ammeters/ElectricityMeter.cs b/services/JiShe.CollectBus.Domain/Ammeters/ElectricityMeter.cs new file mode 100644 index 0000000..2d9cc67 --- /dev/null +++ b/services/JiShe.CollectBus.Domain/Ammeters/ElectricityMeter.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JiShe.CollectBus.IoTDB.Attribute; +using JiShe.CollectBus.IoTDB.Provider; + +namespace JiShe.CollectBus.Ammeters +{ + public class ElectricityMeter : IoTEntity + { + [ATTRIBUTEColumn] + public string MeterModel { get; set; } + + /// + /// 下发消息内容 + /// + [FIELDColumn] + public string IssuedMessageHexString { get; set; } + + ///// + ///// 下发消息Id + ///// + //[FIELDColumn] + //public string IssuedMessageId { get; set; } + + [FIELDColumn] + public double Voltage { get; set; } + + [FIELDColumn] + public double Current { get; set; } + + [FIELDColumn] + public double Power => Voltage * Current; + } +} diff --git a/src/JiShe.CollectBus.Domain/Analyze3761Data.cs b/services/JiShe.CollectBus.Domain/Analyze3761Data.cs similarity index 100% rename from src/JiShe.CollectBus.Domain/Analyze3761Data.cs rename to services/JiShe.CollectBus.Domain/Analyze3761Data.cs diff --git a/src/JiShe.CollectBus.Domain/CollectBusConsts.cs b/services/JiShe.CollectBus.Domain/CollectBusConsts.cs similarity index 100% rename from src/JiShe.CollectBus.Domain/CollectBusConsts.cs rename to services/JiShe.CollectBus.Domain/CollectBusConsts.cs diff --git a/src/JiShe.CollectBus.Domain/CollectBusDbProperties.cs b/services/JiShe.CollectBus.Domain/CollectBusDbProperties.cs similarity index 100% rename from src/JiShe.CollectBus.Domain/CollectBusDbProperties.cs rename to services/JiShe.CollectBus.Domain/CollectBusDbProperties.cs diff --git a/src/JiShe.CollectBus.Domain/CollectBusDomainModule.cs b/services/JiShe.CollectBus.Domain/CollectBusDomainModule.cs similarity index 98% rename from src/JiShe.CollectBus.Domain/CollectBusDomainModule.cs rename to services/JiShe.CollectBus.Domain/CollectBusDomainModule.cs index 64519c7..24945de 100644 --- a/src/JiShe.CollectBus.Domain/CollectBusDomainModule.cs +++ b/services/JiShe.CollectBus.Domain/CollectBusDomainModule.cs @@ -12,7 +12,7 @@ namespace JiShe.CollectBus; [DependsOn( typeof(CollectBusDomainSharedModule), typeof(AbpAuditLoggingDomainModule), - typeof(AbpCachingModule), + typeof(AbpCachingModule), typeof(AbpBackgroundJobsDomainModule) )] public class CollectBusDomainModule : AbpModule diff --git a/src/JiShe.CollectBus.Domain/Data/CollectBusDbMigrationService.cs b/services/JiShe.CollectBus.Domain/Data/CollectBusDbMigrationService.cs similarity index 100% rename from src/JiShe.CollectBus.Domain/Data/CollectBusDbMigrationService.cs rename to services/JiShe.CollectBus.Domain/Data/CollectBusDbMigrationService.cs diff --git a/src/JiShe.CollectBus.Domain/Data/ICollectBusDbSchemaMigrator.cs b/services/JiShe.CollectBus.Domain/Data/ICollectBusDbSchemaMigrator.cs similarity index 100% rename from src/JiShe.CollectBus.Domain/Data/ICollectBusDbSchemaMigrator.cs rename to services/JiShe.CollectBus.Domain/Data/ICollectBusDbSchemaMigrator.cs diff --git a/src/JiShe.CollectBus.Domain/Data/NullCollectBusDbSchemaMigrator.cs b/services/JiShe.CollectBus.Domain/Data/NullCollectBusDbSchemaMigrator.cs similarity index 100% rename from src/JiShe.CollectBus.Domain/Data/NullCollectBusDbSchemaMigrator.cs rename to services/JiShe.CollectBus.Domain/Data/NullCollectBusDbSchemaMigrator.cs diff --git a/services/JiShe.CollectBus.Domain/EnergySystems/Entities/TB_AmmeterInfo.cs b/services/JiShe.CollectBus.Domain/EnergySystems/Entities/TB_AmmeterInfo.cs new file mode 100644 index 0000000..e324535 --- /dev/null +++ b/services/JiShe.CollectBus.Domain/EnergySystems/Entities/TB_AmmeterInfo.cs @@ -0,0 +1,264 @@ +using System; +using FreeSql.DataAnnotations; + +namespace JiShe.CollectBus.EnergySystems.Entities +{ + public class TB_AmmeterInfo + { + + /// + /// 电表信息 + /// + public int ID { get; set; } + + /// + /// 电表编号、电表型号 + /// + public string Code { get; set; } + + /// + /// 区域信息外键 + /// + public int AreaID { get; set; } + /// + /// 区域名 + /// + [Column(IsIgnore = true)] + public string AreaName { get; set; } + + /// + /// 电表别名 + /// + public string Name { get; set; } + + /// + /// 电表类别 (1单相、2三相三线、3三相四线) + /// + public int TypeName { get; set; } + + /// + /// 电表安装地址 + /// + public string Location { get; set; } + /// + /// 电表安装时间 + /// + public DateTime? InstallTime { get; set; } + + /// + /// 电表密码 + /// + public string Password { get; set; } + + /// + /// 电表通信地址 + /// + public string Address { get; set; } + + /// + /// 采集器地址 + /// + public string CollectorAddress { get; set; } + + /// + /// 电压变比 + /// 电压互感器(PT) + /// + public double TimesV { get; set; } + + /// + /// 电流变比 + /// 电流互感器(CT) + /// + public double TimesA { get; set; } + + /// + /// 是否总表 + /// + public int IsSum { get; set; } + + /// + /// 总表ID + /// + public int ParentID { get; set; } + + /// + /// Explain + /// + public string Explain { get; set; } + + /// + /// AddDate + /// + public DateTime AddDate { get; set; } + + /// + /// State表状态: (对应枚举 MeterStateEnum) + /// 0新装(未下发),1运行(档案下发成功时设置状态值1), 2暂停, 100销表(销表后是否重新启用); + /// 特定State: -1 已删除 + /// + public int State { get; set; } + + + /// + /// 费率类型,单、多 (SingleRate :单费率(单相表1),多费率(其他0) ,与TypeName字段无关) + /// SingleRate ? "单" : "复" + /// [SingleRate] --0 复费率 false , 1 单费率 true (与PayPlanID保持一致) + ///对应 TB_PayPlan.Type: 1复费率,2单费率 + /// + public bool SingleRate { get; set; } + + /// + /// 0 未下发 (false), 1 已下发 (true) + /// + public bool IsSend { get; set; } + + /// + /// 创建人ID + /// + public int CreateUserID { get; set; } + + /// + /// 波特率 default(2400) + /// + public int Baudrate { get; set; } + /// + /// 规约 -电表default(30) + /// + public int? Protocol { get; set; } + /// + /// 一个集中器下的[MeteringCode]必须唯一。 + /// + public int MeteringCode { get; set; } + /// + /// MeteringPort 端口就几个可以枚举。 + /// + public int MeteringPort { get; set; } + + /// + /// 对应[TB_PayPlan] + /// + public int PayPlanID { get; set; } + + + public int ProjectID { get; set; } + + public int FocusID { get; set; } + + /// + /// 集中器名称(扩展字段) + /// + [Column(IsIgnore = true)] + public string FocusName { get; set; } + + /// + /// 跳合闸状态字段: 0 合闸,1 跳闸 + /// 电表:TripState (0 合闸-通电, 1 断开、跳闸); + /// + public int TripState { get; set; } + /// + /// 最近阀控时间 + /// + public DateTime? TripTime { get; set; } + + /// + /// 排序字段 + /// + public int Sort { get; set; } + + /// + /// 电子表0 , + /// 机械表1(德力西机械电表-Code) + /// (原有数据都默认:电子电表) + /// + [Column(IsIgnore = true)] + public int MeterKind { get; set; } + + /// + /// 采集方案ID + /// + [Column(IsIgnore = true)] + public int GatherPlanID { get; set; } + + /// + /// 采集项 + /// + public string ReadClass { get; set; } + + /// + /// 修改日期 + /// + public DateTime? EditDate { get; set; } + + /// + /// 修改用ID + /// + public int? EditUserID { get; set; } + + /// + /// 删除时间 + /// + public DateTime? RemoveDate { get; set; } + + /// + /// 删除用户ID + /// + public int? RemoveUserID { get; set; } + + /// + /// 掉电状态 (未上电=1,上电掉电中=2) + /// + public int? PowerDownStatus { get; set; } + + /// + /// 电流规格 + /// + public string CurrentSpec { get; set; } + + /// + /// 电压规格 + /// + public string VoltageSpec { get; set; } + + /// + /// 通讯状态 1:在线 0:离线 + /// + public int LineState { get; set; } + + /// + /// 特殊表 1:是 0:否 + /// + public int Special { get; set; } + + /* + /// + /// 采集项总数 + /// + public int GatherTotal { get; set; } + + /// + /// 采集项 + /// + public string GatherDataTypes { get; set; } + */ + + /// + /// 复费率类型(四费率=4,八费率=8) + /// + public int? MultipleRateType { get; set; } + } + + public class VMAmmeterInfo : TB_AmmeterInfo + { + public decimal? Rate { get; set; } + public decimal? Rate1 { get; set; } + public decimal? Rate2 { get; set; } + public decimal? Rate3 { get; set; } + + public decimal? Rate4 { get; set; } + + public decimal? Rate5 { get; set; } + + public decimal? Rate6 { get; set; } + } +} diff --git a/services/JiShe.CollectBus.Domain/EnergySystems/TableViews/V_FocusAmmeter.cs b/services/JiShe.CollectBus.Domain/EnergySystems/TableViews/V_FocusAmmeter.cs new file mode 100644 index 0000000..b217972 --- /dev/null +++ b/services/JiShe.CollectBus.Domain/EnergySystems/TableViews/V_FocusAmmeter.cs @@ -0,0 +1,21 @@ +using JiShe.CollectBus.EnergySystems.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.EnergySystems.TableViews +{ + public class V_FocusAmmeter: TB_AmmeterInfo + { + public string FocusAddress { get; set; } + + public string FocusAreaCode { get; set; } + + public string FocusCode { get; set; } + + public string FocusState { get; set; } + + } +} diff --git a/src/JiShe.CollectBus.Domain/FodyWeavers.xml b/services/JiShe.CollectBus.Domain/FodyWeavers.xml similarity index 100% rename from src/JiShe.CollectBus.Domain/FodyWeavers.xml rename to services/JiShe.CollectBus.Domain/FodyWeavers.xml diff --git a/services/JiShe.CollectBus.Domain/GatherItem/GatherItemInfo.cs b/services/JiShe.CollectBus.Domain/GatherItem/GatherItemInfo.cs new file mode 100644 index 0000000..da16012 --- /dev/null +++ b/services/JiShe.CollectBus.Domain/GatherItem/GatherItemInfo.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.GatherItem +{ + public class GatherItemInfo + { + /// + /// 数据类型 + /// + public string DataType { get; set; } + + /// + /// 采集项编码 + /// + public string ItemCode { get; set; } + } +} diff --git a/services/JiShe.CollectBus.Domain/IotSystems/AFNEntity/SingleMeasuringAFNDataEntity.cs b/services/JiShe.CollectBus.Domain/IotSystems/AFNEntity/SingleMeasuringAFNDataEntity.cs new file mode 100644 index 0000000..0440c1d --- /dev/null +++ b/services/JiShe.CollectBus.Domain/IotSystems/AFNEntity/SingleMeasuringAFNDataEntity.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using JiShe.CollectBus.IoTDB.Attribute; +using JiShe.CollectBus.IoTDB.Provider; + +namespace JiShe.CollectBus.IotSystems.AFNEntity +{ + /// + /// AFN单项数据实体 + /// + public class SingleMeasuringAFNDataEntity : IoTEntity + { + /// + /// 单项数据对象 + /// + [SingleMeasuring(nameof(SingleMeasuring))] + public Tuple SingleMeasuring { get; set; } + } +} diff --git a/src/JiShe.CollectBus.Domain/Devices/Device.cs b/services/JiShe.CollectBus.Domain/IotSystems/Devices/Device.cs similarity index 59% rename from src/JiShe.CollectBus.Domain/Devices/Device.cs rename to services/JiShe.CollectBus.Domain/IotSystems/Devices/Device.cs index 8dcf886..91a5ce7 100644 --- a/src/JiShe.CollectBus.Domain/Devices/Device.cs +++ b/services/JiShe.CollectBus.Domain/IotSystems/Devices/Device.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using JiShe.CollectBus.Enums; using Volo.Abp.Domain.Entities; -namespace JiShe.CollectBus.Devices +namespace JiShe.CollectBus.IotSystems.Devices { public class Device : AggregateRoot { @@ -27,18 +27,41 @@ namespace JiShe.CollectBus.Devices Status = status; } + /// + /// 集中器编号,在集中器登录时解析获取,并会更新为当前TCP连接的最新ClientId + /// public string Number { get; set; } + /// + /// 首次上线时间 + /// public DateTime FirstOnlineTime { get; set; } + /// + /// 最后上线时间 + /// public DateTime LastOnlineTime { get; set; } + /// + /// TCP客户端首次连接ID,在登录解析成功以后会被Number集中器编号覆盖 + /// public string ClientId { get; set; } + /// + /// TCP客户端断线时间,用于计算是否断线 + /// public DateTime? LastOfflineTime { get; set; } + /// + /// 设备状态 + /// public DeviceStatus Status { get; set; } + /// + /// 设备任务超时次数,超过一定次数则发出预警。 + /// + public int TaskTimeOutCounts { get; set; } = 0; + public void UpdateByLoginAndHeartbeat(string clientId) { LastOnlineTime = DateTime.Now; @@ -46,6 +69,12 @@ namespace JiShe.CollectBus.Devices Status = DeviceStatus.Online; } + public void UpdateByLoginAndHeartbeat() + { + LastOnlineTime = DateTime.Now; + Status = DeviceStatus.Online; + } + public void UpdateByOnClosed() { LastOfflineTime = DateTime.Now; diff --git a/src/JiShe.CollectBus.Domain/MessageIssueds/MessageIssued.cs b/services/JiShe.CollectBus.Domain/IotSystems/MessageIssueds/MessageIssued.cs similarity index 50% rename from src/JiShe.CollectBus.Domain/MessageIssueds/MessageIssued.cs rename to services/JiShe.CollectBus.Domain/IotSystems/MessageIssueds/MessageIssued.cs index 45f53b1..072abcb 100644 --- a/src/JiShe.CollectBus.Domain/MessageIssueds/MessageIssued.cs +++ b/services/JiShe.CollectBus.Domain/IotSystems/MessageIssueds/MessageIssued.cs @@ -1,18 +1,29 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; +using JiShe.CollectBus.Common.Attributes; using JiShe.CollectBus.Common.Enums; +using Volo.Abp.Domain.Entities; -namespace JiShe.CollectBus.MessageIssueds +namespace JiShe.CollectBus.IotSystems.MessageIssueds { - public class MessageIssued + [CassandraTable] + public class MessageIssued:IEntity { public string ClientId { get; set; } public byte[] Message { get; set; } public string DeviceNo { get; set; } public IssuedEventType Type { get; set; } public string MessageId { get; set; } + [Key] + public string Id { get; set; } + + public object?[] GetKeys() + { + return new object[] { Id }; + } } } diff --git a/services/JiShe.CollectBus.Domain/IotSystems/MessageIssueds/ScheduledMeterReadingIssuedEventMessage.cs b/services/JiShe.CollectBus.Domain/IotSystems/MessageIssueds/ScheduledMeterReadingIssuedEventMessage.cs new file mode 100644 index 0000000..41ba7ea --- /dev/null +++ b/services/JiShe.CollectBus.Domain/IotSystems/MessageIssueds/ScheduledMeterReadingIssuedEventMessage.cs @@ -0,0 +1,38 @@ +using JiShe.CollectBus.Common.Enums; +using System; +using Volo.Abp.Domain.Entities; + +namespace JiShe.CollectBus.IotSystems.MessageIssueds +{ + /// + /// 定时抄读Kafka消息实体,1分钟、5分钟、15分钟 + /// + public class ScheduledMeterReadingIssuedEventMessage + { + /// + /// 下发消息内容 + /// + public string MessageHexString { get; set; } + + /// + /// 集中器编号 + /// + public string FocusAddress { get; set; } + + /// + /// 采集时间间隔,通过Kafka主题区分(分钟,如15) + /// + public string TimeDensity { get; set; } + + /// + /// 消息Id + /// + public string MessageId { get; set; } + + /// + /// 最后一次消息Id,用于在消费消息时检查上一个任务是否处理完。 + /// + public string LastMessageId { get; set; } + + } +} diff --git a/src/JiShe.CollectBus.Domain/MessageReceiveds/IReceived.cs b/services/JiShe.CollectBus.Domain/IotSystems/MessageReceiveds/IReceived.cs similarity index 60% rename from src/JiShe.CollectBus.Domain/MessageReceiveds/IReceived.cs rename to services/JiShe.CollectBus.Domain/IotSystems/MessageReceiveds/IReceived.cs index 40c6a68..1f4c97f 100644 --- a/src/JiShe.CollectBus.Domain/MessageReceiveds/IReceived.cs +++ b/services/JiShe.CollectBus.Domain/IotSystems/MessageReceiveds/IReceived.cs @@ -1,4 +1,4 @@ -namespace JiShe.CollectBus.MessageReceiveds +namespace JiShe.CollectBus.IotSystems.MessageReceiveds { public interface IReceived { diff --git a/src/JiShe.CollectBus.Domain/MessageReceiveds/MessageReceived.cs b/services/JiShe.CollectBus.Domain/IotSystems/MessageReceiveds/MessageReceived.cs similarity index 96% rename from src/JiShe.CollectBus.Domain/MessageReceiveds/MessageReceived.cs rename to services/JiShe.CollectBus.Domain/IotSystems/MessageReceiveds/MessageReceived.cs index 4503873..4fad589 100644 --- a/src/JiShe.CollectBus.Domain/MessageReceiveds/MessageReceived.cs +++ b/services/JiShe.CollectBus.Domain/IotSystems/MessageReceiveds/MessageReceived.cs @@ -1,7 +1,7 @@ using System; using Volo.Abp.Domain.Entities; -namespace JiShe.CollectBus.MessageReceiveds +namespace JiShe.CollectBus.IotSystems.MessageReceiveds { public class MessageReceived: AggregateRoot,IReceived { diff --git a/services/JiShe.CollectBus.Domain/IotSystems/MeterReadingRecords/MeterReadingRecords.cs b/services/JiShe.CollectBus.Domain/IotSystems/MeterReadingRecords/MeterReadingRecords.cs new file mode 100644 index 0000000..b1f1112 --- /dev/null +++ b/services/JiShe.CollectBus.Domain/IotSystems/MeterReadingRecords/MeterReadingRecords.cs @@ -0,0 +1,138 @@ +using JiShe.CollectBus.Common.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Entities.Auditing; + +namespace JiShe.CollectBus.IotSystems.MeterReadingRecords +{ + /// + /// 抄读任务数据记录 + /// + public class MeterReadingRecords : AggregateRoot + { + /// + /// 是否手动操作 + /// + public bool ManualOrNot { get; set; } + + /// + /// 任务数据唯一标记 + /// + public string TaskMark { get; set; } + + /// + /// 时间戳标记,IoTDB时间列处理,上报通过构建标记获取唯一标记匹配时间戳。 + /// + public long Timestamps { get; set; } + + /// + /// 是否超时 + /// + public bool IsTimeout { get; set; } = false; + + /// + /// 待抄读时间 + /// + public DateTime PendingCopyReadTime { get; set; } + + /// + /// 集中器ID + /// + public int FocusID { get; set; } + + /// + /// 集中器地址 + /// + public string FocusAddress { get; set; } + + /// + /// 表Id + /// + public int MeterId { get; set; } + + /// + /// 表地址 + /// + public string MeterAddress { get; set; } + + /// + /// 表类型 + /// + public MeterTypeEnum MeterType { get; set; } + + /// + /// 项目ID + /// + public int ProjectID { get; set; } + + /// + /// 数据库业务ID + /// + public int DatabaseBusiID { get; set; } + + /// + /// AFN功能码 + /// + public AFN AFN { get; set; } + + /// + /// 抄读功能码 + /// + public int Fn { get; set; } + + /// + /// 抄读计量点 + /// + public int Pn { get; set; } + + /// + /// 采集项编码 + /// + public string ItemCode { get; set;} + + + /// + /// 创建时间 + /// + public DateTime CreationTime { get; set; } + + /// + /// 下发消息内容 + /// + public string IssuedMessageHexString { get; set; } + + /// + /// 下发消息Id + /// + public string IssuedMessageId { get; set; } + + /// + /// 消息上报内容 + /// + public string? ReceivedMessageHexString { get; set; } + + /// + /// 消息上报时间 + /// + public DateTime? ReceivedTime { get; set; } + + /// + /// 上报消息Id + /// + public string ReceivedMessageId { get; set; } + + /// + /// 上报报文解析备注,异常情况下才有 + /// + public string ReceivedRemark { get; set; } + + public void CreateDataId(Guid Id) + { + this.Id = Id; + } + } +} diff --git a/services/JiShe.CollectBus.Domain/IotSystems/MeterReadingRecords/MeterReadingTelemetryPacketInfo.cs b/services/JiShe.CollectBus.Domain/IotSystems/MeterReadingRecords/MeterReadingTelemetryPacketInfo.cs new file mode 100644 index 0000000..c3f75d3 --- /dev/null +++ b/services/JiShe.CollectBus.Domain/IotSystems/MeterReadingRecords/MeterReadingTelemetryPacketInfo.cs @@ -0,0 +1,141 @@ +using JiShe.CollectBus.Common.Enums; +using JiShe.CollectBus.Common.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Entities.Auditing; + +namespace JiShe.CollectBus.IotSystems.MeterReadingRecords +{ + /// + /// 抄读任务Redis缓存数据记录 + /// + public class MeterReadingTelemetryPacketInfo : DeviceCacheBasicModel + { + /// + /// 关系映射标识,用于ZSet的Member字段和Set的Value字段,具体值可以根据不同业务场景进行定义 + /// + public override string MemberId => $"{FocusId}:{MeterId}:{ItemCode}"; + + /// + /// ZSet排序索引分数值,具体值可以根据不同业务场景进行定义,例如时间戳 + /// + public override long ScoreValue => ((long)FocusId << 32) | (uint)DateTime.Now.Ticks; + + + /// + /// 是否手动操作 + /// + public bool ManualOrNot { get; set; } + + /// + /// 任务数据唯一标记 + /// + public string TaskMark { get; set; } + + /// + /// 时间戳标记,IoTDB时间列处理,上报通过构建标记获取唯一标记匹配时间戳。 + /// + public long Timestamps { get; set; } + + /// + /// 是否超时 + /// + public bool IsTimeout { get; set; } = false; + + /// + /// 待抄读时间 + /// + public DateTime PendingCopyReadTime { get; set; } + + + /// + /// 集中器地址 + /// + public string FocusAddress { get; set; } + + /// + /// 表地址 + /// + public string MeterAddress { get; set; } + + /// + /// 表类型 + /// + public MeterTypeEnum MeterType { get; set; } + + /// + /// 项目ID + /// + public int ProjectID { get; set; } + + /// + /// 数据库业务ID + /// + public int DatabaseBusiID { get; set; } + + /// + /// AFN功能码 + /// + public AFN AFN { get; set; } + + /// + /// 抄读功能码 + /// + public int Fn { get; set; } + + /// + /// 抄读计量点 + /// + public int Pn { get; set; } + + /// + /// 采集项编码 + /// + public string ItemCode { get; set;} + + + /// + /// 创建时间 + /// + public DateTime CreationTime { get; set; } + + /// + /// 下发消息内容 + /// + public string IssuedMessageHexString { get; set; } + + /// + /// 下发消息Id + /// + public string IssuedMessageId { get; set; } + + /// + /// 消息上报内容 + /// + public string? ReceivedMessageHexString { get; set; } + + /// + /// 消息上报时间 + /// + public DateTime? ReceivedTime { get; set; } + + /// + /// 上报消息Id + /// + public string ReceivedMessageId { get; set; } + + /// + /// 上报报文解析备注,异常情况下才有 + /// + public string ReceivedRemark { get; set; } + + //public void CreateDataId(Guid Id) + //{ + // this.Id = Id; + //} + } +} diff --git a/services/JiShe.CollectBus.Domain/IotSystems/PrepayModel/Vi_BaseAmmeterInfo.cs b/services/JiShe.CollectBus.Domain/IotSystems/PrepayModel/Vi_BaseAmmeterInfo.cs new file mode 100644 index 0000000..b3ff165 --- /dev/null +++ b/services/JiShe.CollectBus.Domain/IotSystems/PrepayModel/Vi_BaseAmmeterInfo.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.IotSystems.PrepayModel +{ + /// + /// 预付费电表视图模型 + /// + public partial class Vi_BaseAmmeterInfo + { + public int ID { get; set; } + public string BarCode { get; set; } + public string Address { get; set; } + public string BaudRate { get; set; } + public string Password { get; set; } + public string Explan { get; set; } + public DateTime AddTime { get; set; } + public bool State { get; set; } + public int TB_EquipmentTypeID { get; set; } + public int BaseID { get; set; } + public string Code { get; set; } + public int? MeterCode { get; set; } + public int? PortNumber { get; set; } + public int CT { get; set; } + public bool SoftTripsLock { get; set; } + public bool HardwareTripsLock { get; set; } + public bool ValveState { get; set; } + public bool ArchivesState { get; set; } + public int? SortNumber { get; set; } + public decimal Balance { get; set; } + public bool IsAlarm { get; set; } + public string sysExplan { get; set; } + public DateTime sysAddTime { get; set; } + public bool sysState { get; set; } + public int? TB_sysConcentratorID { get; set; } + public int? TB_sysChargingSchemeID { get; set; } + public int? TB_sysChargingSchemeID1 { get; set; } + public int TB_CustomerID { get; set; } + public int? TB_sysAlarmPlanID { get; set; } + public int? TB_sysCollectorID { get; set; } + public int? TB_sysRoomID { get; set; } + public string SpecialNoCode { get; set; } + public decimal? RatedCurrent { get; set; } + public int? TripDelayTime { get; set; } + public int? ClosingTime { get; set; } + public int? Remainder { get; set; } + public decimal? RatedPower { get; set; } + public bool? ParentAmmState { get; set; } + public int? ParentAmmID { get; set; } + public DateTime? LastUpdateTime { get; set; } + public bool? IsAuthentication { get; set; } + public bool? IsShowBalance { get; set; } + public string Number { get; set; } + public DateTime? LastAuthTime { get; set; } + public bool? IsAuthenticationFunction { get; set; } + public bool? IsShowBalanceFunction { get; set; } + public int TB_ProtocolID { get; set; } + public string Operator { get; set; } + public bool? IsTimingPowerSetting { get; set; } + public string CommSchemeCode { get; set; } + public string RoomAllName { get; set; } + public bool? IsLadderPrice { get; set; } + public int? LadderNum { get; set; } + public decimal? RoomBalance { get; set; } + public decimal? OtherBalance { get; set; } + public int? LastValveEventType { get; set; } + public decimal? ReadKwh4 { get; set; } + public decimal? ReadKwh3 { get; set; } + public decimal? ReadKwh2 { get; set; } + public decimal? ReadKwh1 { get; set; } + public decimal? ReadKwh { get; set; } + public DateTime? LastEventTime { get; set; } + public DateTime? LastReadTime { get; set; } + public string RoomNumber { get; set; } + public string EquipmentName { get; set; } + public int? EquipmentId { get; set; } + public bool? IsMalignantLoad { get; set; } + public string BrandName { get; set; } + public decimal? MalignantPower { get; set; } + public bool? MalignantPowerState { get; set; } + public bool? RatedPowerState { get; set; } + public string SolveStatus { get; set; } + public int? PowerDownStatus { get; set; } + public DateTime? SolveStatusTime { get; set; } + public decimal DisableKwh { get; set; } + public int? IsDeliver { get; set; } + public int? HandleUser { get; set; } + public string HandleUserName { get; set; } + public string Solutions { get; set; } + public decimal DisableKwh1 { get; set; } + public decimal DisableKwh2 { get; set; } + public decimal DisableKwh3 { get; set; } + public decimal DisableKwh4 { get; set; } + public bool DelaySwitchOff { get; set; } + public string MeterSolveStatus { get; set; } + public DateTime? DeliverTime { get; set; } + public bool EnableState { get; set; } + public string DxNbiotDeviceId { get; set; } + public int MeterStatus { get; set; } + public string AllowTripTime { get; set; } + public int? LastCostMilliSecond { get; set; } + public string DxNbiotIMEI { get; set; } + public decimal? ReadKwh5 { get; set; } + public decimal? ReadKwh6 { get; set; } + public decimal? ReadKwh7 { get; set; } + public decimal? ReadKwh8 { get; set; } + public decimal? DisableKwh5 { get; set; } + public decimal? DisableKwh6 { get; set; } + public decimal? DisableKwh7 { get; set; } + public decimal? DisableKwh8 { get; set; } + public bool? IsLadder { get; set; } + public int? TimeSpanSetNum { get; set; } + public string ExecutePeriod { get; set; } + public string FreqInterval { get; set; } + public int? Tb_sysUseValueAlarmID { get; set; } + public bool? IsMaxDemandKwh { get; set; } + public DateTime? OnLineTime { get; set; } + public int? OperatorId { get; set; } + public int PowerCT { get; set; } + public string CommunicationsModule { get; set; } + public bool? CanBlueTooth { get; set; } + public bool? CanRemote { get; set; } + public bool? IsDownPrice { get; set; } + public int? BuyCount { get; set; } + } +} diff --git a/src/JiShe.CollectBus.Domain/Protocols/ProtocolInfo.cs b/services/JiShe.CollectBus.Domain/IotSystems/Protocols/ProtocolInfo.cs similarity index 96% rename from src/JiShe.CollectBus.Domain/Protocols/ProtocolInfo.cs rename to services/JiShe.CollectBus.Domain/IotSystems/Protocols/ProtocolInfo.cs index 4ae8572..c193535 100644 --- a/src/JiShe.CollectBus.Domain/Protocols/ProtocolInfo.cs +++ b/services/JiShe.CollectBus.Domain/IotSystems/Protocols/ProtocolInfo.cs @@ -2,7 +2,7 @@ using JiShe.CollectBus.Interfaces; using Volo.Abp.Domain.Entities; -namespace JiShe.CollectBus.Protocols +namespace JiShe.CollectBus.IotSystems.Protocols { public class ProtocolInfo : AggregateRoot, IProtocolInfo { diff --git a/src/JiShe.CollectBus.Domain/Records/ConrOnlineRecord.cs b/services/JiShe.CollectBus.Domain/IotSystems/Records/ConrOnlineRecord.cs similarity index 95% rename from src/JiShe.CollectBus.Domain/Records/ConrOnlineRecord.cs rename to services/JiShe.CollectBus.Domain/IotSystems/Records/ConrOnlineRecord.cs index e62525f..1c07b01 100644 --- a/src/JiShe.CollectBus.Domain/Records/ConrOnlineRecord.cs +++ b/services/JiShe.CollectBus.Domain/IotSystems/Records/ConrOnlineRecord.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; using Volo.Abp.Domain.Entities; -namespace JiShe.CollectBus.Records +namespace JiShe.CollectBus.IotSystems.Records { /// /// 集中器在线记录 diff --git a/src/JiShe.CollectBus.Domain/Records/CsqRecord.cs b/services/JiShe.CollectBus.Domain/IotSystems/Records/CsqRecord.cs similarity index 95% rename from src/JiShe.CollectBus.Domain/Records/CsqRecord.cs rename to services/JiShe.CollectBus.Domain/IotSystems/Records/CsqRecord.cs index e99235a..72b8638 100644 --- a/src/JiShe.CollectBus.Domain/Records/CsqRecord.cs +++ b/services/JiShe.CollectBus.Domain/IotSystems/Records/CsqRecord.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; using Volo.Abp.Domain.Entities; -namespace JiShe.CollectBus.Records +namespace JiShe.CollectBus.IotSystems.Records { /// /// 信号强度 diff --git a/src/JiShe.CollectBus.Domain/Records/FocusRecord.cs b/services/JiShe.CollectBus.Domain/IotSystems/Records/FocusRecord.cs similarity index 97% rename from src/JiShe.CollectBus.Domain/Records/FocusRecord.cs rename to services/JiShe.CollectBus.Domain/IotSystems/Records/FocusRecord.cs index 6436e33..d94e3e0 100644 --- a/src/JiShe.CollectBus.Domain/Records/FocusRecord.cs +++ b/services/JiShe.CollectBus.Domain/IotSystems/Records/FocusRecord.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; using Volo.Abp.Domain.Entities; -namespace JiShe.CollectBus.Records +namespace JiShe.CollectBus.IotSystems.Records { /// /// 集中器上下线、心跳记录 diff --git a/services/JiShe.CollectBus.Domain/IotSystems/Watermeter/WatermeterInfo.cs b/services/JiShe.CollectBus.Domain/IotSystems/Watermeter/WatermeterInfo.cs new file mode 100644 index 0000000..966192b --- /dev/null +++ b/services/JiShe.CollectBus.Domain/IotSystems/Watermeter/WatermeterInfo.cs @@ -0,0 +1,149 @@ +using JiShe.CollectBus.Common.Enums; +using JiShe.CollectBus.Common.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.IotSystems.Watermeter +{ + /// + /// 水表信息 + /// + public class WatermeterInfo: DeviceCacheBasicModel + { + /// + /// 水表名称 + /// + public string Name { get; set; } + /// + /// 表密码 + /// + public string Password { get; set; } + + /// + /// 集中器地址 + /// + public string FocusAddress { get; set; } + + /// + /// 一个集中器下的[MeteringCode]必须唯一。 PN + /// + public int MeteringCode { get; set; } + + /// + /// 波特率 default(2400) + /// + public int Baudrate { get; set; } + + /// + /// MeteringPort 端口就几个可以枚举。 + /// + public int MeteringPort { get; set; } + + /// + /// 水表通信地址 (当protocol=32时,Address为14位字符;否则12位字符) + /// + public string MeterAddress { get; set; } + + /// + /// 水表类别 (水表类型改成“公称口径”) + /// + public string TypeName { get; set; } + + /// + /// 规约 -电表default(30) 1:97协议,30:07协议,32:CJ/T 188—2018协议 + /// + public int? Protocol { get; set; } + + + public string Code { get; set; } + /// + /// 通讯方案: + /// NB-IOT常德水表、NB-IOT泽联电表、GPRS华立水表、 + /// RS-485、无线、载波 + /// + public string LinkType { get; set; } + + /// + /// HaveValve: 是否带阀 (0 不带阀, 1 带阀) + /// 注意:NULL表示未设置 + /// + public int? HaveValve { get; set; } + + /// + /// 设备类型: 水表\气表、流量计 + /// + public string MeterTypeName { get; set; } + + /// + /// 表计类型 + //// 电表= 1,水表= 2,燃气表= 3,热能表= 4,水表流量计=5,燃气表流量计=6,特殊电表=7 + /// + public MeterTypeEnum MeterType { get; set; } + /// + /// 设备品牌; + /// (当 MeterType = 水表, 如 威铭、捷先 等) + /// (当 MeterType = 流量计, 如 西恩超声波流量计、西恩电磁流量计、涡街流量计 等) + /// + public string MeterBrand { get; set; } + + /// + /// 倍率 + /// + public decimal TimesRate { get; set; } + + /// + /// 集中器地址 + /// + public string Address { get; set; } + + /// + /// 网关地址 + /// + public string GateAddress { get; set; } + /// + /// 集中器区域 + /// + public string AreaCode { get; set; } + /// + /// 通讯状态 + /// 水表:TripState (0 合闸-开阀, 1 关阀);开阀关阀 + /// + public int TripState { get; set; } + /// + /// 是否自动采集 + /// + public int AutomaticReport { get; set; } + /// + /// State表状态: + /// 0新装(未下发),1运行(档案下发成功时设置状态值1), 2暂停, 100销表(销表后是否重新启用) + /// 特定:State -1 已删除 + /// + public int State { get; set; } + /// + /// 采集时间间隔(分钟,如15) + /// + public int TimeDensity { get; set; } + /// + /// 采集器编号 + /// + public string GatherCode { get; set; } + + /// + /// 项目ID + /// + public int ProjectID { get; set; } + + /// + /// 是否异常集中器 0:正常,1异常 + /// + public int AbnormalState { get; set; } + + /// + /// 集中器最后在线时间 + /// + public DateTime LastTime { get; set; } + } +} diff --git a/src/JiShe.CollectBus.Domain/JiShe - Backup.CollectBus.Domain.csproj b/services/JiShe.CollectBus.Domain/JiShe - Backup.CollectBus.Domain.csproj similarity index 100% rename from src/JiShe.CollectBus.Domain/JiShe - Backup.CollectBus.Domain.csproj rename to services/JiShe.CollectBus.Domain/JiShe - Backup.CollectBus.Domain.csproj diff --git a/src/JiShe.CollectBus.Domain/JiShe.CollectBus.Domain.abppkg b/services/JiShe.CollectBus.Domain/JiShe.CollectBus.Domain.abppkg similarity index 100% rename from src/JiShe.CollectBus.Domain/JiShe.CollectBus.Domain.abppkg rename to services/JiShe.CollectBus.Domain/JiShe.CollectBus.Domain.abppkg diff --git a/services/JiShe.CollectBus.Domain/JiShe.CollectBus.Domain.csproj b/services/JiShe.CollectBus.Domain/JiShe.CollectBus.Domain.csproj new file mode 100644 index 0000000..a4dbc5d --- /dev/null +++ b/services/JiShe.CollectBus.Domain/JiShe.CollectBus.Domain.csproj @@ -0,0 +1,42 @@ + + + + + + net8.0 + enable + JiShe.CollectBus + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/JiShe.CollectBus.Domain/Properties/AssemblyInfo.cs b/services/JiShe.CollectBus.Domain/Properties/AssemblyInfo.cs similarity index 100% rename from src/JiShe.CollectBus.Domain/Properties/AssemblyInfo.cs rename to services/JiShe.CollectBus.Domain/Properties/AssemblyInfo.cs diff --git a/src/JiShe.CollectBus.Domain/Settings/CollectBusSettingDefinitionProvider.cs b/services/JiShe.CollectBus.Domain/Settings/CollectBusSettingDefinitionProvider.cs similarity index 100% rename from src/JiShe.CollectBus.Domain/Settings/CollectBusSettingDefinitionProvider.cs rename to services/JiShe.CollectBus.Domain/Settings/CollectBusSettingDefinitionProvider.cs diff --git a/src/JiShe.CollectBus.Domain/Settings/CollectBusSettings.cs b/services/JiShe.CollectBus.Domain/Settings/CollectBusSettings.cs similarity index 100% rename from src/JiShe.CollectBus.Domain/Settings/CollectBusSettings.cs rename to services/JiShe.CollectBus.Domain/Settings/CollectBusSettings.cs diff --git a/src/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusDbContext.cs b/services/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusDbContext.cs similarity index 100% rename from src/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusDbContext.cs rename to services/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusDbContext.cs diff --git a/src/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusDbContextFactory.cs b/services/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusDbContextFactory.cs similarity index 100% rename from src/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusDbContextFactory.cs rename to services/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusDbContextFactory.cs diff --git a/src/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusEfCoreEntityExtensionMappings.cs b/services/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusEfCoreEntityExtensionMappings.cs similarity index 100% rename from src/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusEfCoreEntityExtensionMappings.cs rename to services/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusEfCoreEntityExtensionMappings.cs diff --git a/src/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusEntityFrameworkCoreModule.cs b/services/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusEntityFrameworkCoreModule.cs similarity index 100% rename from src/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusEntityFrameworkCoreModule.cs rename to services/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/CollectBusEntityFrameworkCoreModule.cs diff --git a/src/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/EntityFrameworkCoreCollectBusDbSchemaMigrator.cs b/services/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/EntityFrameworkCoreCollectBusDbSchemaMigrator.cs similarity index 100% rename from src/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/EntityFrameworkCoreCollectBusDbSchemaMigrator.cs rename to services/JiShe.CollectBus.EntityFrameworkCore/EntityFrameworkCore/EntityFrameworkCoreCollectBusDbSchemaMigrator.cs diff --git a/src/JiShe.CollectBus.EntityFrameworkCore/FodyWeavers.xml b/services/JiShe.CollectBus.EntityFrameworkCore/FodyWeavers.xml similarity index 100% rename from src/JiShe.CollectBus.EntityFrameworkCore/FodyWeavers.xml rename to services/JiShe.CollectBus.EntityFrameworkCore/FodyWeavers.xml diff --git a/src/JiShe.CollectBus.EntityFrameworkCore/JiShe.CollectBus.EntityFrameworkCore.abppkg b/services/JiShe.CollectBus.EntityFrameworkCore/JiShe.CollectBus.EntityFrameworkCore.abppkg similarity index 100% rename from src/JiShe.CollectBus.EntityFrameworkCore/JiShe.CollectBus.EntityFrameworkCore.abppkg rename to services/JiShe.CollectBus.EntityFrameworkCore/JiShe.CollectBus.EntityFrameworkCore.abppkg diff --git a/src/JiShe.CollectBus.EntityFrameworkCore/JiShe.CollectBus.EntityFrameworkCore.csproj b/services/JiShe.CollectBus.EntityFrameworkCore/JiShe.CollectBus.EntityFrameworkCore.csproj similarity index 100% rename from src/JiShe.CollectBus.EntityFrameworkCore/JiShe.CollectBus.EntityFrameworkCore.csproj rename to services/JiShe.CollectBus.EntityFrameworkCore/JiShe.CollectBus.EntityFrameworkCore.csproj diff --git a/src/JiShe.CollectBus.EntityFrameworkCore/Properties/AssemblyInfo.cs b/services/JiShe.CollectBus.EntityFrameworkCore/Properties/AssemblyInfo.cs similarity index 100% rename from src/JiShe.CollectBus.EntityFrameworkCore/Properties/AssemblyInfo.cs rename to services/JiShe.CollectBus.EntityFrameworkCore/Properties/AssemblyInfo.cs diff --git a/shared/JiShe.CollectBus.Common/AttributeInfo/NumericalOrderAttribute.cs b/shared/JiShe.CollectBus.Common/AttributeInfo/NumericalOrderAttribute.cs new file mode 100644 index 0000000..a062f16 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/AttributeInfo/NumericalOrderAttribute.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.AttributeInfo +{ + /// + /// 排序序号 + /// + public class NumericalOrderAttribute : Attribute + { + /// + /// 序号 + /// + public int Index { get; set; } + + /// + /// 排序序号 + /// + /// + public NumericalOrderAttribute(int index) + { + Index = index; + } + } +} diff --git a/shared/JiShe.CollectBus.Common/Attributes/CassandraTableAttribute.cs b/shared/JiShe.CollectBus.Common/Attributes/CassandraTableAttribute.cs new file mode 100644 index 0000000..5ee6d8f --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Attributes/CassandraTableAttribute.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.EventBus; +using Volo.Abp; + +namespace JiShe.CollectBus.Common.Attributes +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public class CassandraTableAttribute(string? name = null) : Attribute + { + public virtual string? Name { get; } = name; + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public class CassandraIgnoreAttribute : Attribute + { + public CassandraIgnoreAttribute() + { + } + } +} diff --git a/shared/JiShe.CollectBus.Common/Attributes/IgnoreJobAttribute.cs b/shared/JiShe.CollectBus.Common/Attributes/IgnoreJobAttribute.cs new file mode 100644 index 0000000..464d1ee --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Attributes/IgnoreJobAttribute.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.Attributes +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public class IgnoreJobAttribute : Attribute + { + } +} diff --git a/shared/JiShe.CollectBus.Common/Attributes/TopicNameAttribute.cs b/shared/JiShe.CollectBus.Common/Attributes/TopicNameAttribute.cs new file mode 100644 index 0000000..7e404a5 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Attributes/TopicNameAttribute.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.EventBus; +using Volo.Abp; + +namespace JiShe.CollectBus.Common.Attributes +{ + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public class TopicNameAttribute : Attribute + { + public virtual string Name { get; } + + public TopicNameAttribute(string name) + { + this.Name = Check.NotNullOrWhiteSpace(name, nameof(name)); + } + + public string GetName(Type eventType) => this.Name; + } +} diff --git a/src/JiShe.CollectBus.Common/BuildSendDatas/Build188SendData.cs b/shared/JiShe.CollectBus.Common/BuildSendDatas/Build188SendData.cs similarity index 100% rename from src/JiShe.CollectBus.Common/BuildSendDatas/Build188SendData.cs rename to shared/JiShe.CollectBus.Common/BuildSendDatas/Build188SendData.cs diff --git a/src/JiShe.CollectBus.Common/BuildSendDatas/Build3761SendData.cs b/shared/JiShe.CollectBus.Common/BuildSendDatas/Build3761SendData.cs similarity index 99% rename from src/JiShe.CollectBus.Common/BuildSendDatas/Build3761SendData.cs rename to shared/JiShe.CollectBus.Common/BuildSendDatas/Build3761SendData.cs index 9bed5b8..f4dd11d 100644 --- a/src/JiShe.CollectBus.Common/BuildSendDatas/Build3761SendData.cs +++ b/shared/JiShe.CollectBus.Common/BuildSendDatas/Build3761SendData.cs @@ -36,6 +36,7 @@ namespace JiShe.CollectBus.Common.BuildSendDatas MSA.Add(i); } } + /// /// Gets the msa. /// @@ -1401,7 +1402,7 @@ namespace JiShe.CollectBus.Common.BuildSendDatas cmdStrList.AddRange(userDatas); cmdStrList.Add(cs); cmdStrList.Add(endStr); - Console.WriteLine(string.Join(" ", cmdStrList)); + //Console.WriteLine(string.Join(" ", cmdStrList)); var bytes = cmdStrList.Select(x => Convert.ToByte(x, 16)).ToArray(); return bytes; } diff --git a/src/JiShe.CollectBus.Common/BuildSendDatas/Build645SendData.cs b/shared/JiShe.CollectBus.Common/BuildSendDatas/Build645SendData.cs similarity index 100% rename from src/JiShe.CollectBus.Common/BuildSendDatas/Build645SendData.cs rename to shared/JiShe.CollectBus.Common/BuildSendDatas/Build645SendData.cs diff --git a/shared/JiShe.CollectBus.Common/BuildSendDatas/TasksToBeIssueModel.cs b/shared/JiShe.CollectBus.Common/BuildSendDatas/TasksToBeIssueModel.cs new file mode 100644 index 0000000..5184459 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/BuildSendDatas/TasksToBeIssueModel.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.BuildSendDatas +{ + /// + /// 待下发的指令生产任务数据 + /// + public class TasksToBeIssueModel + { + /// + /// 下个任务时间 + /// + public DateTime NextTaskTime { get; set; } + + /// + /// 采集时间间隔,1分钟,5分钟,15分钟 + /// + public int TimeDensity { get; set; } + } +} diff --git a/shared/JiShe.CollectBus.Common/BuildSendDatas/TelemetryPacketBuilder.cs b/shared/JiShe.CollectBus.Common/BuildSendDatas/TelemetryPacketBuilder.cs new file mode 100644 index 0000000..8ad2a39 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/BuildSendDatas/TelemetryPacketBuilder.cs @@ -0,0 +1,297 @@ +using JiShe.CollectBus.Common.Enums; +using JiShe.CollectBus.Common.Models; +using System; +using System.Collections.Generic; +using System.Data.SqlTypes; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.BuildSendDatas +{ + /// + /// 构建下发报文,只适用与定时抄读 + /// + public static class TelemetryPacketBuilder + { + /// + /// 构建报文的委托 + /// + /// + /// + /// + public delegate byte[] AFNDelegate(TelemetryPacketRequest request); + + /// + /// 编码与方法的映射表 + /// + public static readonly Dictionary AFNHandlersDictionary = new(); + + static TelemetryPacketBuilder() + { + // 初始化时自动注册所有符合命名规则的方法 + var methods = typeof(TelemetryPacketBuilder).GetMethods(BindingFlags.Static | BindingFlags.Public); + foreach (var method in methods) + { + if (method.Name.StartsWith("AFN") && method.Name.EndsWith("_Fn_Send")) + { + string code = method.Name; + var delegateInstance = (AFNDelegate)Delegate.CreateDelegate(typeof(AFNDelegate), method); + AFNHandlersDictionary[code] = delegateInstance; + } + } + } + + #region AFN_00H 确认∕否认 + public static byte[] AFN00_Fn_Send(TelemetryPacketRequest request) + { + var reqParameter = new ReqParameter2() + { + AFN = AFN.确认或否认, + FunCode = (int)CMasterStationFunCode.请求2级数据, + A = request.FocusAddress, + Seq = new Seq() + { + TpV = TpV.附加信息域中无时间标签, + FIRFIN = FIRFIN.单帧, + CON = CON.需要对该帧进行确认, + PRSEQ = 0, + }, + MSA = Build3761SendData.GetMSA(request.FocusAddress), + Pn = request.Pn, + Fn = request.Fn + }; + var bytes = Build3761SendData.BuildSendCommandBytes(reqParameter); + return bytes; + } + #endregion + + + #region AFN_01H 复位命令 + public static byte[] AFN01_Fn_Send(TelemetryPacketRequest request) + { + var reqParameter = new ReqParameter2() + { + AFN = AFN.复位, + FunCode = (int)CMasterStationFunCode.复位命令, + A = request.FocusAddress, + Seq = new Seq() + { + TpV = TpV.附加信息域中无时间标签, + FIRFIN = FIRFIN.单帧, + CON = CON.需要对该帧进行确认, + PRSEQ = 10, + }, + MSA = Build3761SendData.GetMSA(request.FocusAddress), + Pn = request.Pn, + Fn = request.Fn + }; + var bytes = Build3761SendData.BuildSendCommandBytes(reqParameter); + return bytes; + } + #endregion + + + #region AFN_02H 链路接口检测 + public static byte[] AFN02_Fn_Send(TelemetryPacketRequest request) + { + var reqParameter = new ReqParameter2() + { + AFN = AFN.链路接口检测, + FunCode = (int)CMasterStationFunCode.请求2级数据, + A = request.FocusAddress, + Seq = new Seq() + { + TpV = TpV.附加信息域中无时间标签, + FIRFIN = FIRFIN.单帧, + CON = CON.不需要对该帧进行确认, + PRSEQ = 0, + }, + MSA = Build3761SendData.GetMSA(request.FocusAddress), + Pn = request.Pn, + Fn = request.Fn + }; + var bytes = Build3761SendData.BuildSendCommandBytes(reqParameter); + return bytes; + } + #endregion + + #region AFN_04H 设置参数 + public static byte[] AFN04_Fn_Send(TelemetryPacketRequest request) + { + var reqParameter = new ReqParameter2() + { + AFN = AFN.设置参数, + FunCode = (int)CMasterStationFunCode.请求1级数据, + A = request.FocusAddress, + Seq = new Seq() + { + TpV = TpV.附加信息域中无时间标签, + FIRFIN = FIRFIN.单帧, + CON = CON.需要对该帧进行确认, + PRSEQ = 10, + }, + MSA = Build3761SendData.GetMSA(request.FocusAddress), + Pn = request.Pn, + Fn = request.Fn + }; + var bytes = Build3761SendData.BuildSendCommandBytes(reqParameter); + return bytes; + } + + #endregion + + #region AFN_05H 控制命令 + public static byte[] AFN05_Fn_Send(TelemetryPacketRequest request) + { + var reqParameter = new ReqParameter2() + { + AFN = AFN.控制命令, + FunCode = (int)CMasterStationFunCode.请求1级数据, + A = request.FocusAddress, + Seq = new Seq() + { + TpV = TpV.附加信息域中无时间标签, + FIRFIN = FIRFIN.单帧, + CON = CON.需要对该帧进行确认, + PRSEQ = 10, + }, + MSA = Build3761SendData.GetMSA(request.FocusAddress), + Pn = request.Pn, + Fn = request.Fn + }; + var bytes = Build3761SendData.BuildSendCommandBytes(reqParameter); + return bytes; + } + #endregion + + #region AFN_09H 请求终端配置及信息 + public static byte[] AFN09_Fn_Send(TelemetryPacketRequest request) + { + var reqParameter = new ReqParameter2() + { + AFN = AFN.请求终端配置, + FunCode = (int)CMasterStationFunCode.请求2级数据, + A = request.FocusAddress, + Seq = new Seq() + { + TpV = TpV.附加信息域中无时间标签, + FIRFIN = FIRFIN.单帧, + CON = CON.不需要对该帧进行确认, + PRSEQ = 0, + }, + MSA = Build3761SendData.GetMSA(request.FocusAddress), + Pn = request.Pn, + Fn = request.Fn + }; + var bytes = Build3761SendData.BuildSendCommandBytes(reqParameter); + return bytes; + } + + #endregion + + #region AFN_0AH 查询参数 + public static byte[] AFN0A_Fn_Send(TelemetryPacketRequest request) + { + var reqParameter = new ReqParameter2() + { + AFN = AFN.查询参数, + FunCode = (int)CMasterStationFunCode.请求2级数据, + A = request.FocusAddress, + Seq = new Seq() + { + TpV = TpV.附加信息域中无时间标签, + FIRFIN = FIRFIN.单帧, + CON = CON.不需要对该帧进行确认, + PRSEQ = 0, + }, + MSA = Build3761SendData.GetMSA(request.FocusAddress), + Pn = request.Pn, + Fn = request.Fn + }; + var bytes = Build3761SendData.BuildSendCommandBytes(reqParameter); + return bytes; + } + #endregion + + #region AFN_0CH 请求一类数据 + public static byte[] AFN0C_Fn_Send(TelemetryPacketRequest request) + { + var reqParameter = new ReqParameter2() + { + AFN = AFN.请求实时数据, + FunCode = (int)CMasterStationFunCode.请求2级数据, + A = request.FocusAddress, + Seq = new Seq() + { + TpV = TpV.附加信息域中无时间标签, + FIRFIN = FIRFIN.单帧, + CON = CON.不需要对该帧进行确认, + PRSEQ = 2, + }, + MSA = Build3761SendData.GetMSA(request.FocusAddress), + Pn = request.Pn, + Fn = request.Fn + }; + var bytes = Build3761SendData.BuildSendCommandBytes(reqParameter); + return bytes; + } + #endregion + + #region AFN_0DH 请求二类数据 + public static byte[] AFN0D_Fn_Send(TelemetryPacketRequest request) + { + var reqParameter = new ReqParameter2() + { + AFN = AFN.请求历史数据, + FunCode = (int)CMasterStationFunCode.请求2级数据, + A = request.FocusAddress, + Seq = new Seq() + { + TpV = TpV.附加信息域中无时间标签, + FIRFIN = FIRFIN.单帧, + CON = CON.不需要对该帧进行确认, + PRSEQ = 2, + }, + MSA = Build3761SendData.GetMSA(request.FocusAddress), + Pn = request.Pn, + Fn = request.Fn + }; + var bytes = Build3761SendData.BuildSendCommandBytes(reqParameter); + return bytes; + } + #endregion + + #region AFN10H 数据转发 + public static byte[] AFN10_Fn_Send(TelemetryPacketRequest request) + { + var reqParameter = new ReqParameter2() + { + AFN = AFN.数据转发, + FunCode = (int)CMasterStationFunCode.请求2级数据, + A = request.FocusAddress, + Seq = new Seq() + { + TpV = TpV.附加信息域中无时间标签, + FIRFIN = FIRFIN.单帧, + CON = CON.不需要对该帧进行确认, + PRSEQ = 0, + }, + MSA = Build3761SendData.GetMSA(request.FocusAddress), + Pn = request.Pn, + Fn = request.Fn + }; + var bytes = Build3761SendData.BuildSendCommandBytes(reqParameter,request.DataUnit); + return bytes; + } + + #region SpecialAmmeter 特殊电表转发 + //TODO 特殊电表处理 + + #endregion + + #endregion + } +} diff --git a/shared/JiShe.CollectBus.Common/BuildSendDatas/TelemetryPacketRequest.cs b/shared/JiShe.CollectBus.Common/BuildSendDatas/TelemetryPacketRequest.cs new file mode 100644 index 0000000..d22f923 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/BuildSendDatas/TelemetryPacketRequest.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.BuildSendDatas +{ + /// + /// 报文构建参数 + /// + public class TelemetryPacketRequest + { + /// + /// 集中器地址 + /// + public string FocusAddress { get; set; } + + /// + /// 抄读功能码 + /// + public int Fn { get; set; } + + /// + /// 抄读计量点,也就是终端电表对应端口 + /// + public int Pn { get; set; } + + /// + /// 透明转发单元 + /// + public List DataUnit { get; set; } + } +} diff --git a/shared/JiShe.CollectBus.Common/Consts/CommonConst.cs b/shared/JiShe.CollectBus.Common/Consts/CommonConst.cs new file mode 100644 index 0000000..471c897 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Consts/CommonConst.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.Consts +{ + /// + /// 常用常量管理 + /// + public class CommonConst + { + /// + /// 服务器标识 + /// + public const string ServerTagName = "ServerTagName"; + + /// + /// Kafka + /// + public const string Kafka = "Kafka"; + + /// + /// Kafka副本数量 + /// + public const string KafkaReplicationFactor = "KafkaReplicationFactor"; + + /// + /// Kafka主题分区数量 + /// + public const string NumPartitions = "NumPartitions"; + + } +} diff --git a/shared/JiShe.CollectBus.Common/Consts/ProtocolConst.cs b/shared/JiShe.CollectBus.Common/Consts/ProtocolConst.cs new file mode 100644 index 0000000..6b65535 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Consts/ProtocolConst.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.Consts +{ + public class ProtocolConst + { + public const string SubscriberGroup = "jishe.collectbus"; + /// + /// 心跳下行消息主题 + /// + public const string SubscriberHeartbeatIssuedEventName = "issued.heartbeat.event"; + /// + /// 登录下行消息主题 + /// + public const string SubscriberLoginIssuedEventName = "issued.login.event"; + + /// + /// 上行消息主题,测试使用 + /// + public const string SubscriberReceivedEventName = "received.event"; + + /// + /// 心跳上行消息主题 + /// + public const string SubscriberHeartbeatReceivedEventName = "received.heartbeat.event"; + /// + /// 登录上行消息主题 + /// + public const string SubscriberLoginReceivedEventName = "received.login.event"; + + #region 电表消息主题 + /// + /// 1分钟采集电表数据下行消息主题 + /// + public const string AmmeterSubscriberWorkerOneMinuteIssuedEventName = "issued.auto.one.ammeter.event"; + /// + /// 5分钟采集电表数据下行消息主题 + /// + public const string AmmeterSubscriberWorkerFiveMinuteIssuedEventName = "issued.auto.five.ammeter.event"; + /// + /// 15分钟采集电表数据下行消息主题 + /// + public const string AmmeterSubscriberWorkerFifteenMinuteIssuedEventName = "issued.auto.fifteen.ammeter.event"; + + /// + /// 其他采集数据下行消息主题,日冻结,月冻结、集中器版本号控等 + /// + public const string AmmeterSubscriberWorkerOtherIssuedEventName = "issued.auto.other.ammeter.event"; + + /// + /// 电表自动阀控 + /// + public const string AmmeterSubscriberWorkerAutoValveControlIssuedEventName = "issued.auto.control.ammeter.event"; + + /// + /// 电表手动阀控 + /// + public const string AmmeterSubscriberWorkerManualValveControlIssuedEventName = "issued.manual.control.ammeter.event"; + + /// + /// 电表手动抄读 + /// + public const string AmmeterSubscriberWorkerManualValveReadingIssuedEventName = "issued.manual.reading.ammeter.event"; + + #endregion + + #region 水表消息主题 + /// + /// 水表数据下行消息主题,由于水表采集频率不高,所以一个主题就够 + /// + public const string WatermeterSubscriberWorkerAutoReadingIssuedEventName = "issued.auto.reading.watermeter.event"; + + /// + /// 水表自动阀控 + /// + public const string WatermeterSubscriberWorkerAutoValveControlIssuedEventName = "issued.auto.control.watermeter.event"; + + /// + /// 水表手动阀控 + /// + public const string WatermeterSubscriberWorkerManualValveControlIssuedEventName = "issued.manual.control.watermeter.event"; + + /// + /// 水表手动抄读 + /// + public const string WatermeterSubscriberWorkerManualValveReadingIssuedEventName = "issued.manual.reading.watermeter.event"; + #endregion + + /// + /// AFN上行主题格式 + /// + public const string AFNTopicNameFormat = "received.afn{0}h.event"; + + /// + /// AFN00H上行主题格式 + /// + public const string SubscriberAFN00ReceivedEventNameTemp = "received.afn00h.event"; + + /// + /// AFN01H上行主题格式 + /// + public const string SubscriberAFN00HReceivedEventNameTemp = "received.afn01h.event"; + + /// + /// AFN02H上行主题格式 + /// + public const string SubscriberAFN01HReceivedEventNameTemp = "received.afn02h.event"; + + /// + /// AFN03H上行主题格式 + /// + public const string SubscriberAFN02HReceivedEventNameTemp = "received.afn03h.event"; + + /// + /// AFN04H上行主题格式 + /// + public const string SubscriberAFN04HReceivedEventNameTemp = "received.afn04h.event"; + + /// + /// AFN05H上行主题格式 + /// + public const string SubscriberAFN05HReceivedEventNameTemp = "received.afn05h.event"; + + /// + /// AFN09H上行主题格式 + /// + public const string SubscriberAFN09HReceivedEventNameTemp = "received.afn09h.event"; + + /// + /// AFN0AH上行主题格式 + /// + public const string SubscriberAFN0AHReceivedEventNameTemp = "received.afn10h.event"; + + /// + /// AFN0BH上行主题格式 + /// + public const string SubscriberAFN0BHReceivedEventNameTemp = "received.afn11h.event"; + + /// + /// AFN0CH上行主题格式 + /// + public const string SubscriberAFN0CHReceivedEventNameTemp = "received.afn12h.event"; + + /// + /// AFN0DH上行主题格式 + /// + public const string SubscriberAFN0DHReceivedEventNameTemp = "received.afn13h.event"; + + /// + /// AFN0EH上行主题格式 + /// + public const string SubscriberAFN0EHReceivedEventNameTemp = "received.afn14h.event"; + + /// + /// AFN10H上行主题格式 + /// + public const string SubscriberAFN10HReceivedEventNameTemp = "received.afn16h.event"; + + + /// + /// 测试主题格式 + /// + public const string TESTTOPIC = "test-topic"; + } +} diff --git a/shared/JiShe.CollectBus.Common/Consts/RedisConst.cs b/shared/JiShe.CollectBus.Common/Consts/RedisConst.cs new file mode 100644 index 0000000..7ac170b --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Consts/RedisConst.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.Consts +{ + public class RedisConst + { + /// + /// 缓存基础目录 + /// + public const string CacheBasicDirectoryKey = "CollectBus:"; + + /// + /// 1分钟采集间隔 + /// + public const string OneMinuteAcquisitionTimeInterval = "One"; + + /// + /// 5分钟采集间隔 + /// + public const string FiveMinuteAcquisitionTimeInterval = "Five"; + + /// + /// 15分钟采集间隔 + /// + public const string FifteenMinuteAcquisitionTimeInterval = "Fifteen"; + + public const string MeterInfo = "MeterInfo"; + /// + /// 缓存表计信息,{0}=>系统类型,{1}=>应用服务部署标记,{2}=>表计类别,{3}=>采集频率 + /// + public const string CacheMeterInfoHashKey = $"{CacheBasicDirectoryKey}{"{0}:{1}"}:{MeterInfo}:{"{2}"}:{"{3}"}"; + + /// + /// 缓存表计信息索引Set缓存Key,{0}=>系统类型,{1}=>应用服务部署标记,{2}=>表计类别,{3}=>采集频率 + /// + public const string CacheMeterInfoSetIndexKey = $"{CacheBasicDirectoryKey}{"{0}:{1}"}:{MeterInfo}:{"{2}"}:SetIndex:{"{3}"}"; + + /// + /// 缓存表计信息排序索引ZSET缓存Key,{0}=>系统类型,{1}=>应用服务部署标记,{2}=>表计类别,{3}=>采集频率 + /// + public const string CacheMeterInfoZSetScoresIndexKey = $"{CacheBasicDirectoryKey}{"{0}:{1}"}:{MeterInfo}:{"{2}"}:ZSetScoresIndex:{"{3}"}"; + + + public const string TaskInfo = "TaskInfo"; + /// + /// 缓存待下发的指令生产任务数据,{0}=>系统类型,{1}=>应用服务部署标记,{2}=>表计类别,{3}=>采集频率 + /// + public const string CacheTasksToBeIssuedKey = $"{CacheBasicDirectoryKey}{"{0}:{1}"}:{TaskInfo}:{"{2}"}:{"{3}"}"; + + public const string TelemetryPacket = "TelemetryPacket"; + /// + /// 缓存表计下发指令数据集,{0}=>系统类型,{1}=>应用服务部署标记,{2}=>表计类别,{3}=>采集频率,{4}=>集中器所在分组,{5}=>时间格式的任务批次 + /// + public const string CacheTelemetryPacketInfoHashKey = $"{CacheBasicDirectoryKey}{"{0}:{1}"}:{TelemetryPacket}:{"{2}"}:{"{3}"}:{"{4}"}:{"{5}"}"; + + /// + /// 缓存表计下发指令数据集索引Set缓存Key,{0}=>系统类型,{1}=>应用服务部署标记,{2}=>表计类别,{3}=>采集频率,{4}=>集中器所在分组,{5}=>时间格式的任务批次 + /// + public const string CacheTelemetryPacketInfoSetIndexKey = $"{CacheBasicDirectoryKey}{"{0}:{1}"}:{TelemetryPacket}:{"{2}"}:SetIndex:{"{3}"}:{"{4}"}:{"{5}"}"; + + /// + /// 缓存表计下发指令数据集排序索引ZSET缓存Key,{0}=>系统类型,{1}=>应用服务部署标记,{2}=>表计类别,{3}=>采集频率,{4}=>集中器所在分组,{5}=>时间格式的任务批次 + /// + public const string CacheTelemetryPacketInfoZSetScoresIndexKey = $"{CacheBasicDirectoryKey}{"{0}:{1}"}:{TelemetryPacket}:{"{2}"}:ZSetScoresIndex:{"{3}"}:{"{4}"}:{"{5}"}"; + + ///// + ///// 缓存设备平衡关系映射结果,{0}=>系统类型,{1}=>应用服务部署标记 + ///// + //public const string CacheDeviceBalanceRelationMapResultKey = $"{CacheBasicDirectoryKey}{"{0}:{1}"}:RelationMap"; + + public const string CacheAmmeterFocusKey = "CacheAmmeterFocusKey"; + } +} diff --git a/src/JiShe.CollectBus.Common/Consts/RegexConst.cs b/shared/JiShe.CollectBus.Common/Consts/RegexConst.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Consts/RegexConst.cs rename to shared/JiShe.CollectBus.Common/Consts/RegexConst.cs diff --git a/shared/JiShe.CollectBus.Common/Consts/SystemTypeConst.cs b/shared/JiShe.CollectBus.Common/Consts/SystemTypeConst.cs new file mode 100644 index 0000000..fd37fd9 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Consts/SystemTypeConst.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.Consts +{ + /// + /// 系统类型常量 + /// + public class SystemTypeConst + { + /// + /// 预付费系统 + /// + public const string Prepay = "Prepay"; + + /// + /// 能耗系统 + /// + public const string Energy = "Energy"; + } +} diff --git a/shared/JiShe.CollectBus.Common/DeviceBalanceControl/DeviceGroupBalanceControl.cs b/shared/JiShe.CollectBus.Common/DeviceBalanceControl/DeviceGroupBalanceControl.cs new file mode 100644 index 0000000..aba63e8 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/DeviceBalanceControl/DeviceGroupBalanceControl.cs @@ -0,0 +1,426 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace JiShe.CollectBus.Common.DeviceBalanceControl +{ + /// + /// 设备组负载控制 + /// + public class DeviceGroupBalanceControl + { + private static readonly object _syncRoot = new object(); + + private static volatile CacheState _currentCache; + + /// + /// 使用ConcurrentDictionary保证线程安全的设备分组映射 + /// + private sealed class CacheState + { + public readonly ConcurrentDictionary BalancedMapping; + public readonly List[] CachedGroups; + + public CacheState(int groupCount) + { + BalancedMapping = new ConcurrentDictionary(); + CachedGroups = new List[groupCount]; + for (int i = 0; i < groupCount; i++) + { + CachedGroups[i] = new List(); + } + } + } + + /// + /// 初始化或增量更新缓存 + /// + public static void InitializeCache(List deviceList, int groupCount = 30) + { + if (deviceList == null || deviceList.Count <= 0) + { + throw new ArgumentException($"{nameof(InitializeCache)} 设备分组初始化失败,设备数据为空"); + } + + if (groupCount > 60 || groupCount <= 0) + { + groupCount = 60; + } + + lock (_syncRoot) + { + // 首次初始化 + if (_currentCache == null) + { + var newCache = new CacheState(groupCount); + UpdateCacheWithDevices(newCache, deviceList, groupCount); + _currentCache = newCache; + } + // 后续增量更新 + else + { + if (_currentCache.CachedGroups.Length != groupCount) + { + throw new ArgumentException($"{nameof(InitializeCache)} 设备分组初始化完成以后,分组数量不能更改"); + } + + var clonedCache = CloneExistingCache(); + UpdateCacheWithDevices(clonedCache, deviceList, groupCount); + _currentCache = clonedCache; + } + } + } + + /// + /// 带锁的缓存克隆(写入时复制) + /// + private static CacheState CloneExistingCache() + { + var oldCache = _currentCache; + var newCache = new CacheState(oldCache.CachedGroups.Length); + + // 复制已有映射 + foreach (var kvp in oldCache.BalancedMapping) + { + newCache.BalancedMapping.TryAdd(kvp.Key, kvp.Value); + } + + // 复制分组数据 + for (int i = 0; i < oldCache.CachedGroups.Length; i++) + { + newCache.CachedGroups[i].AddRange(oldCache.CachedGroups[i]); + } + + return newCache; + } + + /// + /// 更新设备到缓存 + /// + private static void UpdateCacheWithDevices(CacheState cache, List deviceList, int groupCount) + { + foreach (var deviceId in deviceList) + { + // 原子操作:如果设备不存在则计算分组 + cache.BalancedMapping.GetOrAdd(deviceId, id => + { + int groupId = GetGroupId(id, groupCount); + lock (cache.CachedGroups[groupId]) + { + cache.CachedGroups[groupId].Add(id); + } + return groupId; + }); + } + } + + /// + /// 并行处理泛型数据集(支持动态线程分配) + /// + /// 已经分组的设备信息 + /// 部分或者全部的已经分组的设备集合 + /// 从泛型对象提取deviceId + /// 处理委托(参数:当前对象,线程ID) + /// 可选线程限制 + /// + /// + public static async Task ProcessGenericListAsync( + List items, Func deviceIdSelector, Action processor, int? maxThreads = null) + { + var cache = _currentCache ?? throw new InvalidOperationException("缓存未初始化"); + + // 创建分组任务队列 + var groupQueues = new ConcurrentQueue[cache.CachedGroups.Length]; + for (int i = 0; i < groupQueues.Length; i++) + { + groupQueues[i] = new ConcurrentQueue(); + } + + // 阶段1:分发数据到分组队列 + Parallel.ForEach(items, item => + { + var deviceId = deviceIdSelector(item); + if (cache.BalancedMapping.TryGetValue(deviceId, out int groupId)) + { + groupQueues[groupId].Enqueue(item); + } + }); + + if ((maxThreads.HasValue && maxThreads.Value > cache.CachedGroups.Length) || maxThreads.HasValue == false) + { + maxThreads = cache.CachedGroups.Length; + } + + // 阶段2:并行处理队列 + var options = new ParallelOptions + { + MaxDegreeOfParallelism = maxThreads.Value, + }; + + await Task.Run(() => + { + Parallel.For(0, cache.CachedGroups.Length, options, async groupId => + { + var queue = groupQueues[groupId]; + while (queue.TryDequeue(out T item)) + { + processor(item, groupId); + } + }); + }); + } + + + /// + /// 智能节流处理(CPU友好型) + /// + /// 已经分组的设备信息 + /// 部分或者全部的已经分组的设备集合 + /// 从泛型对象提取deviceId + /// 处理委托(参数:当前对象,分组ID) + /// 可选最佳并发度 + /// + /// + public static async Task ProcessWithThrottleAsync( + List items, + Func deviceIdSelector, + Action processor, + int? maxConcurrency = null) + { + var cache = _currentCache ?? throw new InvalidOperationException("缓存未初始化"); + + // 自动计算最佳并发度 + int recommendedThreads = CalculateOptimalThreadCount(); + if ((maxConcurrency.HasValue && maxConcurrency.Value > cache.CachedGroups.Length) || maxConcurrency.HasValue == false) + { + maxConcurrency = cache.CachedGroups.Length; + } + + int actualThreads = maxConcurrency ?? recommendedThreads; + + // 创建节流器 + using var throttler = new SemaphoreSlim(initialCount: actualThreads); + + // 使用LongRunning避免线程池饥饿 + var tasks = items.Select(async item => + { + await throttler.WaitAsync(); + try + { + var deviceId = deviceIdSelector(item); + if (cache.BalancedMapping.TryGetValue(deviceId, out int groupId)) + { + // 分组级处理(保持顺序性) + await ProcessItemAsync(item, processor, groupId); + } + } + finally + { + throttler.Release(); + } + }); + + await Task.WhenAll(tasks); + } + + /// + /// 自动计算最优线程数 + /// + public static int CalculateOptimalThreadCount() + { + int coreCount = Environment.ProcessorCount; + return Math.Min( + coreCount * 2, // 超线程优化 + _currentCache?.CachedGroups.Length ?? 60 + ); + } + + /// + /// 分组异步处理(带节流) + /// + private static async Task ProcessItemAsync(T item, Action processor, int groupId) + { + // 使用内存缓存降低CPU负载 + await Task.Yield(); // 立即释放当前线程 + + // 分组处理上下文 + var context = ExecutionContext.Capture(); + ThreadPool.QueueUserWorkItem(_ => + { + ExecutionContext.Run(context!, state => + { + processor(item,groupId); + }, null); + }); + } + + + /// + /// 通过 deviceId 获取所在的分组集合 + /// + public static List GetGroup(string deviceId) + { + var cache = _currentCache; + if (cache == null) + throw new InvalidOperationException("缓存未初始化"); + + return cache.CachedGroups[cache.BalancedMapping[deviceId]]; + } + + /// + /// 通过 deviceId 获取分组Id + /// + public static int GetDeviceGroupId(string deviceId) + { + var cache = _currentCache; + if (cache == null) + throw new InvalidOperationException("缓存未初始化"); + + return cache.BalancedMapping[deviceId]; + } + + + /// + /// 创建均衡映射表 + /// + /// 数据集合 + /// 分组数量 + /// 允许的最大偏差百分比 + /// + public static Dictionary CreateBalancedMapping(List deviceList, int groupCount, int maxDeviation = 5) + { + var mapping = new Dictionary(); + int targetPerGroup = deviceList.Count / groupCount; + int maxAllowed = (int)(targetPerGroup * (1 + maxDeviation / 100.0)); + + // 初始化分组计数器 + int[] groupCounters = new int[groupCount]; + + foreach (var deviceId in deviceList) + { + int preferredGroup = GetGroupId(deviceId, groupCount); + + // 如果首选分组未满,直接分配 + if (groupCounters[preferredGroup] < maxAllowed) + { + mapping[deviceId] = preferredGroup; + groupCounters[preferredGroup]++; + } + else + { + // 寻找当前最空闲的分组 + int fallbackGroup = Array.IndexOf(groupCounters, groupCounters.Min()); + mapping[deviceId] = fallbackGroup; + groupCounters[fallbackGroup]++; + } + } + + return mapping; + } + + /// + /// 分析分组分布 + /// + /// + /// + /// + public static Dictionary AnalyzeDistribution(List deviceList, int groupCount) + { + Dictionary distribution = new Dictionary(); + foreach (var deviceId in deviceList) + { + int groupId = GetGroupId(deviceId, groupCount); + distribution[groupId] = distribution.TryGetValue(groupId, out var count) ? count + 1 : 1; + } + return distribution; + } + + /// + /// 获取设备ID对应的分组ID + /// + /// + /// + /// + public static int GetGroupId(string deviceId, int groupCount) + { + int hash = Fnv1aHash(deviceId); + // 双重取模确保分布均匀 + return (hash % groupCount + groupCount) % groupCount; + } + + /// + /// FNV-1a哈希算法 + /// + /// + /// + public static int Fnv1aHash(string input) + { + const uint fnvPrime = 16777619; + const uint fnvOffsetBasis = 2166136261; + + uint hash = fnvOffsetBasis; + foreach (char c in input) + { + hash ^= (byte)c; + hash *= fnvPrime; + } + return (int)hash; + } + + /// + /// CRC16算法实现 + /// + /// + /// + public static ushort CRC16Hash(byte[] bytes) + { + ushort crc = 0xFFFF; + for (int i = 0; i < bytes.Length; i++) + { + crc ^= bytes[i]; + for (int j = 0; j < 8; j++) + { + if ((crc & 0x0001) == 1) + { + crc = (ushort)((crc >> 1) ^ 0xA001); + } + else + { + crc >>= 1; + } + } + } + return crc; + } + + /// + /// 打印分组统计数据 + /// + public static void PrintDistributionStats() + { + var cache = _currentCache; + if (cache == null) + { + Console.WriteLine("缓存未初始化"); + return; + } + + var stats = cache.CachedGroups + .Select((group, idx) => new { GroupId = idx, Count = group.Count }) + .OrderBy(x => x.GroupId); + + Console.WriteLine("分组数据量统计:"); + foreach (var stat in stats) + { + Console.WriteLine($"Group {stat.GroupId}: {stat.Count} 条数据"); + } + + Console.WriteLine($"总共: {stats.Sum(d=>d.Count)} 条数据"); + } + + } +} diff --git a/src/JiShe.CollectBus.Common/Enums/188Enums.cs b/shared/JiShe.CollectBus.Common/Enums/188Enums.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Enums/188Enums.cs rename to shared/JiShe.CollectBus.Common/Enums/188Enums.cs diff --git a/src/JiShe.CollectBus.Common/Enums/376Enums.cs b/shared/JiShe.CollectBus.Common/Enums/376Enums.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Enums/376Enums.cs rename to shared/JiShe.CollectBus.Common/Enums/376Enums.cs diff --git a/src/JiShe.CollectBus.Common/Enums/645Enums.cs b/shared/JiShe.CollectBus.Common/Enums/645Enums.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Enums/645Enums.cs rename to shared/JiShe.CollectBus.Common/Enums/645Enums.cs diff --git a/src/JiShe.CollectBus.Common/Enums/ComandCodeEnum.cs b/shared/JiShe.CollectBus.Common/Enums/ComandCodeEnum.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Enums/ComandCodeEnum.cs rename to shared/JiShe.CollectBus.Common/Enums/ComandCodeEnum.cs diff --git a/src/JiShe.CollectBus.Common/Enums/CommandChunkEnum.cs b/shared/JiShe.CollectBus.Common/Enums/CommandChunkEnum.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Enums/CommandChunkEnum.cs rename to shared/JiShe.CollectBus.Common/Enums/CommandChunkEnum.cs diff --git a/src/JiShe.CollectBus.Common/Enums/F25DataItemEnum.cs b/shared/JiShe.CollectBus.Common/Enums/F25DataItemEnum.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Enums/F25DataItemEnum.cs rename to shared/JiShe.CollectBus.Common/Enums/F25DataItemEnum.cs diff --git a/src/JiShe.CollectBus.Common/Enums/MaskTypeEnum.cs b/shared/JiShe.CollectBus.Common/Enums/MaskTypeEnum.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Enums/MaskTypeEnum.cs rename to shared/JiShe.CollectBus.Common/Enums/MaskTypeEnum.cs diff --git a/shared/JiShe.CollectBus.Common/Enums/MeterLinkProtocolEnum.cs b/shared/JiShe.CollectBus.Common/Enums/MeterLinkProtocolEnum.cs new file mode 100644 index 0000000..637acf9 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Enums/MeterLinkProtocolEnum.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.Enums +{ + /// + /// 表计连接通讯协议--表计与集中器的通讯协议 + /// + public enum MeterLinkProtocolEnum + { + /// + /// 无 + /// + None = 0, + + /// + /// DL/T 645—1997 + /// + DLT_645_1997 = 1, + + /// + /// 交流采样装置通信协议(电表) + /// + ACSamplingDevice = 2, + + /// + /// DL/T 645—2007 + /// + DLT_645_2007 = 30, + + /// + /// 载波通信 + /// + Carrierwave = 31, + + /// + /// CJ/T 188—2018协议(水表) + /// + CJT_188_2018 = 32, + + /// + /// CJ/T 188—2004协议 + /// + CJT_188_2004 = 33, + + /// + /// MODBUS-RTU + /// + MODBUS_RTU = 34, + } +} diff --git a/shared/JiShe.CollectBus.Common/Enums/MeterTypeEnum.cs b/shared/JiShe.CollectBus.Common/Enums/MeterTypeEnum.cs new file mode 100644 index 0000000..43cfaae --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Enums/MeterTypeEnum.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.Enums +{ + /// + /// 表计类型 + /// 电表= 1,水表= 2,燃气表= 3,热能表= 4,水表流量计=5,燃气表流量计=6 + /// + public enum MeterTypeEnum + { + /// + /// 电表 + /// + Ammeter = 1, + /// + /// 水表 + /// + WaterMeter = 2, + /// + /// 燃气表 + /// + Gasmeter = 3, + /// + /// 热能表 + /// + HeatMeter = 4, + /// + /// 水表流量计 + /// + WaterMeterFlowmeter = 5, + /// + /// 燃气表流量计 + /// + GasmeterFlowmeter = 6, + /// + /// 特殊电表 + /// + SpecialAmmeter = 7, + /// + /// 传感器 + /// + Sensor = 8, + + /// + /// 采集器 + /// + Collector = 9, + } +} diff --git a/shared/JiShe.CollectBus.Common/Enums/RecordsDataMigrationStatusEnum.cs b/shared/JiShe.CollectBus.Common/Enums/RecordsDataMigrationStatusEnum.cs new file mode 100644 index 0000000..544be42 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Enums/RecordsDataMigrationStatusEnum.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.Enums +{ + /// + /// 数据迁移状态 + /// + public enum RecordsDataMigrationStatusEnum + { + /// + /// 未开始 + /// + NotStarted = 0, + + /// + /// 进行中 + /// + InProgress = 1, + + /// + /// 已完成 + /// + Completed = 2, + + /// + /// 已取消 + /// + Cancelled = 3, + + /// + /// 失败 + /// + Failed = 4, + } +} diff --git a/src/JiShe.CollectBus.Common/Enums/SortEnum.cs b/shared/JiShe.CollectBus.Common/Enums/SortEnum.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Enums/SortEnum.cs rename to shared/JiShe.CollectBus.Common/Enums/SortEnum.cs diff --git a/src/JiShe.CollectBus.Common/Enums/TransparentForwardingFlagEnum.cs b/shared/JiShe.CollectBus.Common/Enums/TransparentForwardingFlagEnum.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Enums/TransparentForwardingFlagEnum.cs rename to shared/JiShe.CollectBus.Common/Enums/TransparentForwardingFlagEnum.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/CollectionExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/CollectionExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/CollectionExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/CollectionExtensions.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/ComparableExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/ComparableExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/ComparableExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/ComparableExtensions.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/DataTableExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/DataTableExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/DataTableExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/DataTableExtensions.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/DateTimeExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/DateTimeExtensions.cs similarity index 86% rename from src/JiShe.CollectBus.Common/Extensions/DateTimeExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/DateTimeExtensions.cs index b12778f..4e3fef9 100644 --- a/src/JiShe.CollectBus.Common/Extensions/DateTimeExtensions.cs +++ b/shared/JiShe.CollectBus.Common/Extensions/DateTimeExtensions.cs @@ -167,5 +167,38 @@ namespace JiShe.CollectBus.Common.Extensions ) ); } + + /// + /// 获取数据表分片策略 + /// + /// + /// + public static string GetDataTableShardingStrategy(this DateTime dateTime) + { +#if DEBUG + return $"{dateTime:yyyyMMddHHmm}"; +#else + return $"{dateTime:yyyyMMddHH}"; +#endif + } + + /// + /// 获取当前时间毫秒级时间戳 + /// + /// + public static long GetCurrentTimeMillis() + { + return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } + + /// + /// 将Unix时间戳转换为日期时间 + /// + /// + /// + public static DateTime FromUnixMillis(long millis) + { + return DateTimeOffset.FromUnixTimeMilliseconds(millis).DateTime; + } } } diff --git a/src/JiShe.CollectBus.Common/Extensions/DayOfWeekExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/DayOfWeekExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/DayOfWeekExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/DayOfWeekExtensions.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/DecimalOrIntExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/DecimalOrIntExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/DecimalOrIntExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/DecimalOrIntExtensions.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/DictionaryExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/DictionaryExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/DictionaryExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/DictionaryExtensions.cs diff --git a/shared/JiShe.CollectBus.Common/Extensions/EnumExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..f54a3a4 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Extensions/EnumExtensions.cs @@ -0,0 +1,83 @@ +using System; + +namespace JiShe.CollectBus.Common.Extensions +{ + public static class EnumExtensions + { + /// + /// 将枚举转换为字典 + /// + /// + /// + public static Dictionary ToDictionary() where TEnum : Enum + { + return Enum.GetValues(typeof(TEnum)) + .Cast() + .ToDictionary( + e => e.ToString(), + e => Convert.ToInt32(e) + ); + } + + /// + /// 将枚举转换为字典 + /// + /// + /// + public static Dictionary ToEnumDictionary() where TEnum : Enum + { + return Enum.GetValues(typeof(TEnum)) + .Cast() + .ToDictionary( + e => e.ToString(), + e => e + ); + } + + /// + /// 将枚举转换为字典 + /// + /// + /// + public static Dictionary ToValueNameDictionary() where TEnum : Enum + { + return Enum.GetValues(typeof(TEnum)) + .Cast() + .ToDictionary( + e => Convert.ToInt32(e), + e => e.ToString() + ); + } + + /// + /// 将枚举转换为字典 + /// + /// + /// + public static Dictionary ToNameValueDictionary() where TEnum : Enum + { + return Enum.GetValues(typeof(TEnum)) + .Cast() + .ToDictionary( + e => e.ToString(), + e => Convert.ToInt32(e) + ); + } + + /// + /// 将枚举转换为字典 + /// + /// + /// + public static Dictionary ToEnumNameDictionary() where TEnum : Enum + { + return Enum.GetValues(typeof(TEnum)) + .Cast() + .ToDictionary( + e => e, + e => e.ToString() + ); + } + + } +} diff --git a/src/JiShe.CollectBus.Common/Extensions/EnumerableExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/EnumerableExtensions.cs similarity index 70% rename from src/JiShe.CollectBus.Common/Extensions/EnumerableExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/EnumerableExtensions.cs index 94e4ad1..b17e9c2 100644 --- a/src/JiShe.CollectBus.Common/Extensions/EnumerableExtensions.cs +++ b/shared/JiShe.CollectBus.Common/Extensions/EnumerableExtensions.cs @@ -64,5 +64,51 @@ namespace JiShe.CollectBus.Common.Extensions ? source.Where(predicate) : source; } + + /// + /// 分批 + /// + /// + /// + /// + /// + public static IEnumerable> Batch( + this IEnumerable source, + int batchSize) + { + var buffer = new List(batchSize); + foreach (var item in source) + { + buffer.Add(item); + if (buffer.Count == batchSize) + { + yield return buffer; + buffer = new List(batchSize); + } + } + if (buffer.Count > 0) + yield return buffer; + } + + //public static IEnumerable> Batch(this IEnumerable source, int batchSize) + //{ + // if (batchSize <= 0) + // throw new ArgumentOutOfRangeException(nameof(batchSize)); + + // using var enumerator = source.GetEnumerator(); + // while (enumerator.MoveNext()) + // { + // yield return GetBatch(enumerator, batchSize); + // } + //} + + //private static IEnumerable GetBatch(IEnumerator enumerator, int batchSize) + //{ + // do + // { + // yield return enumerator.Current; + // batchSize--; + // } while (batchSize > 0 && enumerator.MoveNext()); + //} } } diff --git a/src/JiShe.CollectBus.Common/Extensions/EventHandlerExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/EventHandlerExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/EventHandlerExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/EventHandlerExtensions.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/ExceptionExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/ExceptionExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/ExceptionExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/ExceptionExtensions.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/HexStringExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/HexStringExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/HexStringExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/HexStringExtensions.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/HttpResponseExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/HttpResponseExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/HttpResponseExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/HttpResponseExtensions.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/IntExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/IntExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/IntExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/IntExtensions.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/ListExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/ListExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/ListExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/ListExtensions.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/LockExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/LockExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/LockExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/LockExtensions.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/ObjectExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/ObjectExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/ObjectExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/ObjectExtensions.cs diff --git a/shared/JiShe.CollectBus.Common/Extensions/ProtocolConstExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/ProtocolConstExtensions.cs new file mode 100644 index 0000000..1dbb301 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Extensions/ProtocolConstExtensions.cs @@ -0,0 +1,69 @@ +using JiShe.CollectBus.Common.Consts; +using JiShe.CollectBus.Common.Enums; +using JiShe.CollectBus.Common.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.Extensions +{ + public class ProtocolConstExtensions + { + /// + /// 自动获取 ProtocolConst 类中所有下行 Kafka 主题名称 + /// (通过反射筛选 public const string 且字段名以 "EventName" 结尾的常量) + /// + public static List GetAllTopicNamesByIssued() + { + List topics = typeof(ProtocolConst) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(f => + f.IsLiteral && + !f.IsInitOnly && + f.FieldType == typeof(string) && + f.Name.EndsWith("IssuedEventName")) // 通过命名规则过滤主题字段 + .Select(f => (string)f.GetRawConstantValue()!) + .ToList(); + + return topics; + } + + /// + /// 自动获取 ProtocolConst 类中所有下行 Kafka 主题名称 + /// (通过反射筛选 public const string 且字段名以 "EventName" 结尾的常量) + /// + public static List GetAllTopicNamesByReceived() + { + //固定的上报主题 + var topicList = typeof(ProtocolConst) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(f => + f.IsLiteral && + !f.IsInitOnly && + f.FieldType == typeof(string) && + f.Name.EndsWith("ReceivedEventName")) // 通过命名规则过滤主题字段 + .Select(f => (string)f.GetRawConstantValue()!) + .ToList(); + + //动态上报主题,需根据协议的AFN功能码动态获取 + var afnList = EnumExtensions.ToNameValueDictionary(); + //需要排除的AFN功能码 + var excludeItems = new List() { 6, 7, 8,15 }; + + foreach (var item in afnList) + { + if (excludeItems.Contains(item.Value)) + { + continue; + } + + topicList.Add(string.Format(ProtocolConst.AFNTopicNameFormat, item.Value.ToString().PadLeft(2, '0'))); + } + + return topicList; + } + } +} diff --git a/src/JiShe.CollectBus.Common/Extensions/StreamExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/StreamExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/StreamExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/StreamExtensions.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/StringExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/StringExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/StringExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/StringExtensions.cs diff --git a/src/JiShe.CollectBus.Common/Extensions/XmlExtensions.cs b/shared/JiShe.CollectBus.Common/Extensions/XmlExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Extensions/XmlExtensions.cs rename to shared/JiShe.CollectBus.Common/Extensions/XmlExtensions.cs diff --git a/shared/JiShe.CollectBus.Common/Helpers/BusJsonSerializer.cs b/shared/JiShe.CollectBus.Common/Helpers/BusJsonSerializer.cs new file mode 100644 index 0000000..713501e --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Helpers/BusJsonSerializer.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json.Serialization; +using System.Text.Json; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.Helpers +{ + /// + /// json帮助类 + /// + public static class BusJsonSerializer + { + /// + /// json对象转换成字符串 + /// + /// 需要序列化的对象 + /// 是否忽略实体中实体,不再序列化里面包含的实体 + /// 配置 + /// + public static string Serialize(this object obj, bool IsIgnore = false, JsonSerializerOptions jsonSerializerOptions = null) + { + if (jsonSerializerOptions == null) + { + jsonSerializerOptions = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + WriteIndented = false,// 设置格式化输出 + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,// 允许特殊字符 + IgnoreReadOnlyFields = true, + IgnoreReadOnlyProperties = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString, // 允许数字字符串 + AllowTrailingCommas = true, // 忽略尾随逗号 + ReadCommentHandling = JsonCommentHandling.Skip, // 忽略注释 + PropertyNameCaseInsensitive = true, // 属性名称大小写不敏感 + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // 属性名称使用驼峰命名规则 + Converters = { new DateTimeJsonConverter() }, // 注册你的自定义转换器, + DefaultBufferSize = 4096, + }; + } + + if (IsIgnore == true) + { + jsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; // 忽略循环引用 + + return JsonSerializer.Serialize(obj, jsonSerializerOptions); + } + else + { + return JsonSerializer.Serialize(obj, jsonSerializerOptions); + } + } + + /// + /// json字符串转换成json对象 + /// + /// + /// + /// 是否忽略实体中实体,不再序列化里面包含的实体 + /// 配置 + /// + public static T? Deserialize(this string json, bool IsIgnore = false, JsonSerializerOptions jsonSerializerOptions = null) + { + if (jsonSerializerOptions == null) + { + jsonSerializerOptions = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + WriteIndented = false,// 设置格式化输出 + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,// 允许特殊字符 + IgnoreReadOnlyFields = true, + IgnoreReadOnlyProperties = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString, // 允许数字字符串 + AllowTrailingCommas = true, // 忽略尾随逗号 + ReadCommentHandling = JsonCommentHandling.Skip, // 忽略注释 + PropertyNameCaseInsensitive = true, // 属性名称大小写不敏感 + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // 属性名称使用驼峰命名规则 + Converters = { new DateTimeJsonConverter() }, // 注册你的自定义转换器, + DefaultBufferSize = 4096, + }; + } + + if (IsIgnore == true) + { + jsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; // 忽略循环引用 + + return json == null ? default(T) : JsonSerializer.Deserialize(json, jsonSerializerOptions); + } + else + { + return json == null ? default(T) : JsonSerializer.Deserialize(json, jsonSerializerOptions); + } + } + + /// + /// json字符串转换成json对象 + /// + /// + /// + /// 是否忽略实体中实体,不再序列化里面包含的实体 + /// 配置 + /// + public static object? Deserialize(this string json, Type type, bool IsIgnore = false, JsonSerializerOptions jsonSerializerOptions = null) + { + if (jsonSerializerOptions == null) + { + jsonSerializerOptions = new JsonSerializerOptions + { + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + WriteIndented = false,// 设置格式化输出 + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,// 允许特殊字符 + IgnoreReadOnlyFields = true, + IgnoreReadOnlyProperties = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString, // 允许数字字符串 + AllowTrailingCommas = true, // 忽略尾随逗号 + ReadCommentHandling = JsonCommentHandling.Skip, // 忽略注释 + PropertyNameCaseInsensitive = true, // 属性名称大小写不敏感 + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // 属性名称使用驼峰命名规则 + Converters = { new DateTimeJsonConverter() } // 注册你的自定义转换器, + }; + } + + + + if (IsIgnore == true) + { + jsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; // 忽略循环引用 + + return json == null ? null : JsonSerializer.Deserialize(json, type, jsonSerializerOptions); + } + else + { + return json == null ? null : JsonSerializer.Deserialize(json, type, jsonSerializerOptions); + } + } + + /// + /// list json字符串转换成list + /// + /// + /// + /// + public static List? DeserializeToList(this string json) + { + return json == null ? default(List) : Deserialize>(json); + } + } + + + public class DateTimeJsonConverter : JsonConverter + { + 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)); + } + } + + /// + /// Unix格式时间格式化 + /// + public class UnixTimeConverter : JsonConverter + { + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + if (long.TryParse(reader.GetString(), out long timestamp)) + return DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime; + } + + if (reader.TokenType == JsonTokenType.Number) + { + long timestamp = reader.GetInt64(); + return DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime; + } + return reader.GetDateTime(); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + long timestamp = new DateTimeOffset(value).ToUnixTimeSeconds(); + writer.WriteStringValue(timestamp.ToString()); + } + } +} diff --git a/shared/JiShe.CollectBus.Common/Helpers/CommonHelper.cs b/shared/JiShe.CollectBus.Common/Helpers/CommonHelper.cs new file mode 100644 index 0000000..22ebef5 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Helpers/CommonHelper.cs @@ -0,0 +1,776 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using JiShe.CollectBus.Common.AttributeInfo; + +namespace JiShe.CollectBus.Common.Helpers +{ + public static class CommonHelper + { + /// + /// 获得无符号GUID + /// + /// + public static string GetGUID() + { + return Guid.NewGuid().ToString("N"); + } + + /// + /// 获取时间戳 + /// + /// 是否返回秒,false返回毫秒 + /// + public static long GetTimeStampTen(bool isSeconds) + { + if (isSeconds) + { + return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + else + { + return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } + } + + /// + /// 获取指定长度的随机数 + /// + /// + /// + public static string GetRandomNumber(int length = 8) + { + if (length <= 8) + { + length = 8; + } + + if (length > 31) + { + length = 32; + } + + var randomArray = RandomNumberGenerator.GetBytes(length); + StringBuilder stringBuilder = new StringBuilder(); + foreach (var item in randomArray) + { + stringBuilder.Append(item); + } + + return stringBuilder.ToString().Substring(0, length); + } + + /// + /// C#反射遍历对象属性获取键值对 + /// + /// 对象类型 + /// 对象 + public static Dictionary GetClassProperties(T model) + { + Type t = model.GetType(); + List propertyList = new List(); + + PropertyInfo[] tempPropertyList = t.GetProperties(); + if (tempPropertyList != null && tempPropertyList.Length > 0) + { + propertyList.AddRange(tempPropertyList); + } + + var parentPropertyInfo = t.BaseType?.GetProperties(); + if (parentPropertyInfo != null && parentPropertyInfo.Length > 0) + { + foreach (var item in parentPropertyInfo) + { + if (!propertyList.Any(d => d.Name == item.Name)) //如果子类已经包含父类的属性或者字段,跳过不处理 + { + propertyList.Add(item); + } + } + } + + Dictionary resultData = new Dictionary(); + + foreach (PropertyInfo item in propertyList) + { + resultData.Add(item.Name, item.GetValue(model, null)); + } + + return resultData; + } + + /// + /// C#反射遍历对象属性,将键值对赋值给属性 + /// + public static object SetClassProperties(string typeModel, Dictionary keyValues) + { + if (keyValues.Count <= 0) + { + return null; + } + + Type tType = Type.GetType(typeModel); + + + PropertyInfo[] PropertyList = tType.GetProperties(); + + object objModel = tType.Assembly.CreateInstance(tType.FullName); + foreach (PropertyInfo item in PropertyList) + { + if (keyValues.ContainsKey(item.Name)) + { + object objectValue = keyValues[item.Name]; + item.SetValue(objModel, objectValue); + } + } + + return objModel; + } + + /// + /// 取得某月的第一天 + /// + /// 要取得月份第一天的时间 + /// + public static DateTime FirstDayOfMonth(this DateTime datetime) + { + return datetime.AddDays(1 - datetime.Day); + } + + /// + /// 取得某月的最后一天 + /// + /// 要取得月份最后一天的时间 + /// + public static DateTime LastDayOfMonth(this DateTime datetime) + { + return datetime.AddDays(1 - datetime.Day).AddMonths(1).AddDays(-1); + } + + /// + /// 取得某月第一天0点以及最后一天的23:59:59时间范围 + /// + /// + /// + public static Tuple GetMonthDateRange(this DateTime datetime) + { + var lastDayOfMonthDate = LastDayOfMonth(datetime); + return new Tuple(datetime.FirstDayOfMonth(), new DateTime(lastDayOfMonthDate.Year, lastDayOfMonthDate.Month, lastDayOfMonthDate.Day, 23, 59, 59)); + } + + /// + /// 取得某一天0点到当月最后一天的23:59:59时间范围 + /// + /// + /// + public static Tuple GetCurrentDateToLastDayRange(this DateTime datetime) + { + var lastDayOfMonthDate = LastDayOfMonth(datetime); + return new Tuple(datetime.Date, new DateTime(lastDayOfMonthDate.Year, lastDayOfMonthDate.Month, lastDayOfMonthDate.Day, 23, 59, 59)); + } + + /// + /// 取得某一天0点到23:59:59时间范围 + /// + /// + /// + public static Tuple GetCurrentDateRange(this DateTime datetime) + { + return new Tuple(datetime.Date, new DateTime(datetime.Year, datetime.Month, datetime.Day, 23, 59, 59)); + } + + /// + /// 获取指定枚举的所有 Attribute 说明以及value组成的键值对 + /// + /// 对象类 + /// false获取字段、true获取属性 + /// + public static List GetEnumAttributeList(Type type, bool getPropertie = false) + { + if (type == null) + { + return null; + } + + List selectResults = new List(); + List memberInfos = new List(); + + if (getPropertie == false) + { + FieldInfo[] fieldArray = type.GetFields(); + if (null == fieldArray || fieldArray.Length <= 0) + { + return null; + } + + memberInfos.AddRange(fieldArray); + //获取父类的字段 + var parentFieldInfo = type.BaseType?.GetFields(); + if (parentFieldInfo != null && parentFieldInfo.Length > 0) + { + foreach (var item in parentFieldInfo) + { + if (!memberInfos.Any(d => d.Name == item.Name)) //如果子类已经包含父类的属性或者字段,跳过不处理 + { + memberInfos.Add(item); + } + } + } + } + else + { + PropertyInfo[] properties = type.GetProperties(); + if (null == properties || properties.Length <= 0) + { + return null; + } + + memberInfos.AddRange(properties); + //获取父类的属性 + var parentPropertyInfo = type.BaseType?.GetProperties(); + if (parentPropertyInfo != null && parentPropertyInfo.Length > 0) + { + foreach (var item in parentPropertyInfo) + { + if (!memberInfos.Any(d => d.Name == item.Name)) //如果子类已经包含父类的属性或者字段,跳过不处理 + { + memberInfos.Add(item); + } + } + } + } + + foreach (var item in memberInfos) + { + DescriptionAttribute[] EnumAttributes = + (DescriptionAttribute[])item.GetCustomAttributes(typeof(DescriptionAttribute), false); + + dynamic infoObject = null; + if (getPropertie == false) + { + infoObject = (FieldInfo)item; + } + else + { + infoObject = (PropertyInfo)item; + } + + if (EnumAttributes.Length > 0) + { + SelectResult selectResult = new SelectResult() + { + Key = Convert.ToInt32(infoObject.GetValue(null)).ToString(), + Value = EnumAttributes[0].Description, + }; + + selectResults.Add(selectResult); + } + + DisplayAttribute[] DisplayAttributes = + (DisplayAttribute[])item.GetCustomAttributes(typeof(DisplayAttribute), false); + if (DisplayAttributes.Length > 0) + { + SelectResult selectResult = + selectResults.FirstOrDefault(e => e.Key == Convert.ToInt32(infoObject.GetValue(null)).ToString()); + if (selectResult != null) + { + selectResult.SecondValue = DisplayAttributes[0].Name; + } + } + } + + return selectResults; + } + + /// + /// 获取指定枚举的所有 Attribute 说明以及value组成的键值对 + /// + /// 对象类 + /// 第三个标签类型 + /// 第三个标签类型取值名称 + /// false获取字段、true获取属性 + /// + public static List GetEnumAttributeListWithThirdValue(Type type, Type thirdAttributeType, string thirdAttributePropertieName, bool getPropertie = false) + { + if (type == null) + { + return null; + } + + List selectResults = new List(); + List memberInfos = new List(); + + if (getPropertie == false) + { + FieldInfo[] EnumInfo = type.GetFields(); + if (null == EnumInfo || EnumInfo.Length <= 0) + { + return null; + } + memberInfos.AddRange(EnumInfo); + var parentFieldInfo = type.BaseType?.GetFields(); + if (parentFieldInfo != null && parentFieldInfo.Length > 0) + { + memberInfos.AddRange(parentFieldInfo); + } + } + else + { + PropertyInfo[] EnumInfo = type.GetProperties(); + if (null == EnumInfo || EnumInfo.Length <= 0) + { + return null; + } + memberInfos.AddRange(EnumInfo); + var parentPropertyInfo = type.BaseType?.GetProperties(); + if (parentPropertyInfo != null && parentPropertyInfo.Length > 0) + { + memberInfos.AddRange(parentPropertyInfo); + } + } + + foreach (var item in memberInfos) + { + var thirdAttributes = item. + GetCustomAttributes(thirdAttributeType, false); + if (thirdAttributes == null || thirdAttributes.Length <= 0) + { + continue; + } + + DescriptionAttribute[] descriptionAttributes = (DescriptionAttribute[])item. + GetCustomAttributes(typeof(DescriptionAttribute), false); + + dynamic infoObject = null; + if (getPropertie == false) + { + infoObject = (FieldInfo)item; + } + else + { + infoObject = (PropertyInfo)item; + } + + if (descriptionAttributes.Length > 0) + { + SelectResult selectResult = new SelectResult() + { + Key = infoObject.Name, + Value = descriptionAttributes[0].Description, + }; + + selectResults.Add(selectResult); + } + + DisplayAttribute[] displayAttributes = (DisplayAttribute[])item. + GetCustomAttributes(typeof(DisplayAttribute), false); + if (displayAttributes.Length > 0) + { + SelectResult selectResult = selectResults.FirstOrDefault(e => e.Key == infoObject.Name); + if (selectResult != null) + { + selectResult.SecondValue = displayAttributes[0].Name; + } + } + + if (thirdAttributes.Length > 0 && !string.IsNullOrWhiteSpace(thirdAttributePropertieName)) + { + foreach (var attr in thirdAttributes) + { + // 使用反射获取特性的属性值 + var properties = thirdAttributeType.GetProperties(); + foreach (var prop in properties) + { + // 假设你要获取特性的某个属性值,例如 TypeName + if (prop.Name == thirdAttributePropertieName) + { + object value = prop.GetValue(attr); + SelectResult selectResult = selectResults.FirstOrDefault(e => e.Key == infoObject.Name); + if (selectResult != null) + { + selectResult.ThirdValue = value?.ToString(); // 将属性值赋给 ThirdValue + } + break; // 如果找到了需要的属性,可以跳出循环 + } + } + } + } + } + + return selectResults; + } + + /// + /// 获取指定类、指定常量值的Description说明 + /// 包含直接继承的父级字段 + /// + /// 对象类 + /// + /// + public static List> GetTypeDescriptionListToTuple(Type t, bool getPropertie = false) + { + if (t == null) + { + return null; + } + + List memberInfos = new List(); + + if (getPropertie == false) + { + FieldInfo[] fieldInfo = t.GetFields(); + if (null == fieldInfo || fieldInfo.Length <= 0) + { + return null; + } + + memberInfos.AddRange(fieldInfo); + var parentFieldInfo = t.BaseType?.GetFields(); + if (parentFieldInfo != null && parentFieldInfo.Length > 0) + { + foreach (var item in parentFieldInfo) + { + if (!memberInfos.Any(d => d.Name == item.Name)) //如果子类已经包含父类的属性或者字段,跳过不处理 + { + memberInfos.Add(item); + } + } + } + } + else + { + PropertyInfo[] fieldInfo = t.GetProperties(); + if (null == fieldInfo || fieldInfo.Length <= 0) + { + return null; + } + + memberInfos.AddRange(fieldInfo); + var parentPropertyInfo = t.BaseType?.GetProperties(); + if (parentPropertyInfo != null && parentPropertyInfo.Length > 0) + { + foreach (var item in parentPropertyInfo) + { + if (!memberInfos.Any(d => d.Name == item.Name)) //如果子类已经包含父类的属性或者字段,跳过不处理 + { + memberInfos.Add(item); + } + } + } + } + + List> tuples = new List>(); + + foreach (var item in memberInfos) + { + DescriptionAttribute[] descriptionAttribute = + (DescriptionAttribute[])item.GetCustomAttributes(typeof(DescriptionAttribute), false); + + NumericalOrderAttribute[] indexAttributes = + (NumericalOrderAttribute[])item.GetCustomAttributes(typeof(NumericalOrderAttribute), false); + + + if (descriptionAttribute.Length > 0 && indexAttributes.Length > 0) + { + Tuple tuple = new Tuple(item.Name, + descriptionAttribute[0].Description, indexAttributes[0].Index); + tuples.Add(tuple); + } + } + + return tuples; + } + + /// + /// 获取指定类、指定常量值的常量说明 + /// + /// 常量字段名称 + ///属性还是字段 + /// + public static string GetTypeDescriptionName(string fieldName, bool getPropertie = false) + { + if (string.IsNullOrEmpty(fieldName)) + { + return ""; + } + + MemberInfo memberInfo = null; + if (getPropertie == false) + { + memberInfo = typeof(T).GetField(fieldName); + } + else + { + memberInfo = typeof(T).GetProperty(fieldName); + } + + if (null == memberInfo) + { + return ""; + } + + DescriptionAttribute[] EnumAttributes = + (DescriptionAttribute[])memberInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); + if (EnumAttributes.Length <= 0) + { + return ""; + } + + return EnumAttributes[0].Description; + } + + /// + /// 获取指定命名空间下指定常量值的常量说明 + /// + /// 常量字段名称 + /// 命名空间,主要用来找到对应程序集 + ///属性还是字段 + /// + public static string GetTypeDescriptionName(string fieldName, string assemblyName, bool getPropertie = false) + { + if (string.IsNullOrWhiteSpace(fieldName) || string.IsNullOrWhiteSpace(assemblyName)) + { + return null; + } + + string desc = ""; + foreach (var item in GetEnumList(assemblyName)) + { + desc = GetTypeDescriptionName(item, fieldName, getPropertie); + if (!string.IsNullOrEmpty(desc)) + { + break; + } + } + + return desc; + } + + /// + /// 获取指定类、指定常量值的常量说明 + /// + /// 对象类 + /// 常量字段名称 + ///属性还是字段 + /// + public static string GetTypeDescriptionName(this Type t, string fieldName, bool getPropertie = false) + { + if (string.IsNullOrWhiteSpace(fieldName)) + { + return ""; + } + + MemberInfo memberInfo = null; + if (getPropertie == false) + { + memberInfo = t.GetField(fieldName); + } + else + { + memberInfo = t.GetProperty(fieldName); + } + + if (null != memberInfo) + { + DescriptionAttribute[] EnumAttributes = + (DescriptionAttribute[])memberInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); + if (EnumAttributes.Length > 0) + { + return EnumAttributes[0].Description; + } + } + + return ""; + } + + /// + /// 扩展方法,获得枚举值集合 + /// + ///枚举的DisplayName + public static List GetEnumList() where T : Enum + { + List enumList = new List(); + foreach (T value in Enum.GetValues(typeof(T))) + { + enumList.Add(value); + } + + return enumList; + } + + private static List GetEnumList(string assemblyName) + { + if (!String.IsNullOrEmpty(assemblyName)) + { + Assembly assembly = Assembly.Load(assemblyName); + List ts = assembly.GetTypes().Where(x => x.GetTypeInfo().IsClass).ToList(); + return ts; + } + + return new List(); + } + + /// + /// 扩展方法,获得枚举的Display值 + /// + ///枚举值 + ///当枚举值没有定义DisplayNameAttribute,是否使用枚举名代替,默认是使用 + ///属性还是字段 + ///枚举的DisplayName + public static string GetDisplayName(this Enum value, Boolean nameInstead = true, bool getPropertie = false) + { + Type type = value.GetType(); + string name = Enum.GetName(type, value); + if (name == null) + { + return null; + } + + DisplayAttribute attribute = null; + + if (getPropertie == false) + { + attribute = Attribute.GetCustomAttribute(type.GetField(name), typeof(DisplayAttribute)) as DisplayAttribute; + } + else + { + attribute = + Attribute.GetCustomAttribute(type.GetProperty(name), typeof(DisplayAttribute)) as DisplayAttribute; + } + + + if (attribute == null && nameInstead == true) + { + return name; + } + + return attribute == null ? null : attribute.Name; + } + + /// + /// 获取枚举的描述信息 + /// + /// + /// + public static string GetEnumDescription(this Enum value) + { + var name = value.ToString(); + var field = value.GetType().GetField(name); + if (field == null) return name; + + var att = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute), false); + + return att == null ? field.Name : ((DescriptionAttribute)att).Description; + } + + + + /// + /// 将传入的字符串中间部分字符替换成特殊字符 + /// + /// 需要替换的字符串 + /// 前保留长度 + /// 尾保留长度 + /// 特殊字符 + /// 被特殊字符替换的字符串 + public static string ReplaceWithSpecialChar(this string value, int startLen = 1, int endLen = 1, + char specialChar = '*') + { + if (string.IsNullOrEmpty(value)) + { + return value; + } + + try + { + + if (value.Length <= startLen + endLen) + { + var temStartVal = value.Substring(0, startLen); + return $"{temStartVal}{"".PadLeft(endLen, specialChar)}"; + } + + if (value.Length == 10 && endLen == 1) + { + endLen = 3; + } + + var startVal = value.Substring(0, startLen); + var endVal = value.Substring(value.Length - endLen); + if (value.Length == 2) + { + endVal = ""; + } + + value = $"{startVal}{endVal.PadLeft(value.Length - startLen, specialChar)}"; + } + catch (Exception) + { + throw; + } + + return value; + } + + /// + /// Linux下字体名称转换 + /// + /// + /// + public static string GetLinuxFontFamily(this string fontValue) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + if (fontValue == "楷体") + { + fontValue = "KaiTi"; + } + else if (fontValue == "隶书") + { + fontValue = "LiSu"; + } + else if (fontValue == "宋体") + { + fontValue = "SimSun"; + } + else if (fontValue == "微软雅黑") + { + fontValue = "Microsoft YaHei"; + } + else if (fontValue == "新宋体") + { + fontValue = "NSimSun"; + } + else if (fontValue == "仿宋") + { + fontValue = "FangSong"; + } + else if (fontValue == "黑体") + { + fontValue = "SimHei"; + } + + } + + return fontValue; + } + + /// + /// 获取任务标识 + /// + /// + /// + /// + /// + public static string GetTaskMark(int afn,int fn,int pn) + { + return $"{afn.ToString().PadLeft(2,'0')}{fn}{pn}"; + } + } +} diff --git a/shared/JiShe.CollectBus.Common/Helpers/SelectResult.cs b/shared/JiShe.CollectBus.Common/Helpers/SelectResult.cs new file mode 100644 index 0000000..4ccaf99 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Helpers/SelectResult.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.Helpers +{ + /// + /// 下拉框选项元素 + /// + public class SelectResult + { + /// + /// 下拉框 键 + /// + public string Key { get; set; } + + /// + /// 下拉框 值 + /// + public string Value { get; set; } + + /// + /// 下拉框 值2 + /// + public string SecondValue { get; set; } + + /// + /// 下拉框 值3 + /// + public object ThirdValue { get; set; } + + } +} diff --git a/src/JiShe.CollectBus.Common/Helpers/TypeHelper.cs b/shared/JiShe.CollectBus.Common/Helpers/TypeHelper.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Helpers/TypeHelper.cs rename to shared/JiShe.CollectBus.Common/Helpers/TypeHelper.cs diff --git a/src/JiShe.CollectBus.Common/JiShe.CollectBus.Common.csproj b/shared/JiShe.CollectBus.Common/JiShe.CollectBus.Common.csproj similarity index 86% rename from src/JiShe.CollectBus.Common/JiShe.CollectBus.Common.csproj rename to shared/JiShe.CollectBus.Common/JiShe.CollectBus.Common.csproj index 8b0b7af..05645ef 100644 --- a/src/JiShe.CollectBus.Common/JiShe.CollectBus.Common.csproj +++ b/shared/JiShe.CollectBus.Common/JiShe.CollectBus.Common.csproj @@ -8,12 +8,16 @@ + + + + @@ -22,10 +26,7 @@ - - - diff --git a/src/JiShe.CollectBus.Common/Jobs/IBaseJob.cs b/shared/JiShe.CollectBus.Common/Jobs/IBaseJob.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Jobs/IBaseJob.cs rename to shared/JiShe.CollectBus.Common/Jobs/IBaseJob.cs diff --git a/shared/JiShe.CollectBus.Common/Models/BusCacheGlobalPagedResult.cs b/shared/JiShe.CollectBus.Common/Models/BusCacheGlobalPagedResult.cs new file mode 100644 index 0000000..465bd15 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Models/BusCacheGlobalPagedResult.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.Models +{ + /// + /// 缓存全局分页结果 + /// + /// + public class BusCacheGlobalPagedResult + { + /// + /// 数据集合 + /// + public List Items { get; set; } + + /// + /// 总条数 + /// + public long TotalCount { get; set; } + + /// + /// 每页条数 + /// + public int PageSize { get; set; } + + /// + /// 总页数 + /// + public int PageCount + { + get + { + return (int)Math.Ceiling((double)TotalCount / PageSize); + } + } + + /// + /// 是否有下一页 + /// + public bool HasNext { get; set; } + + /// + /// 下一页的分页索引 + /// + public decimal? NextScore { get; set; } + + /// + /// 下一页的分页索引 + /// + public string NextMember { get; set; } + } +} diff --git a/shared/JiShe.CollectBus.Common/Models/BusPagedResult.cs b/shared/JiShe.CollectBus.Common/Models/BusPagedResult.cs new file mode 100644 index 0000000..f2bd1a3 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Models/BusPagedResult.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.Models +{ + /// + /// 查询结果 + /// + /// + public class BusPagedResult + { + /// + /// 总条数 + /// + public long TotalCount { get; set; } + + /// + /// 当前页码 + /// + public int PageIndex { get; set; } + + /// + /// 每页条数 + /// + public int PageSize { get; set; } + + /// + /// 数据集合 + /// + public IEnumerable Items { get; set; } + + /// + /// 是否有下一页 + /// + public bool HasNext { get; set; } + + /// + /// 下一页的分页索引 + /// + public decimal? NextScore { get; set; } + + /// + /// 下一页的分页索引 + /// + public string NextMember { get; set; } + } +} diff --git a/src/JiShe.CollectBus.Common/Models/CommandReuslt.cs b/shared/JiShe.CollectBus.Common/Models/CommandReuslt.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Models/CommandReuslt.cs rename to shared/JiShe.CollectBus.Common/Models/CommandReuslt.cs diff --git a/src/JiShe.CollectBus.Common/Models/CurrentPositiveActiveEnergyAnalyze.cs b/shared/JiShe.CollectBus.Common/Models/CurrentPositiveActiveEnergyAnalyze.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Models/CurrentPositiveActiveEnergyAnalyze.cs rename to shared/JiShe.CollectBus.Common/Models/CurrentPositiveActiveEnergyAnalyze.cs diff --git a/shared/JiShe.CollectBus.Common/Models/DeviceCacheBasicModel.cs b/shared/JiShe.CollectBus.Common/Models/DeviceCacheBasicModel.cs new file mode 100644 index 0000000..d397151 --- /dev/null +++ b/shared/JiShe.CollectBus.Common/Models/DeviceCacheBasicModel.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JiShe.CollectBus.Common.Models +{ + /// + /// 设备缓存基础模型 + /// + public abstract class DeviceCacheBasicModel + { + /// + /// 集中器Id + /// + public int FocusId { get; set; } + + /// + /// 表Id + /// + public int MeterId { get; set; } + + /// + /// 关系映射标识,用于ZSet的Member字段和Set的Value字段,具体值可以根据不同业务场景进行定义 + /// + public virtual string MemberId => $"{FocusId}:{MeterId}"; + + /// + /// ZSet排序索引分数值,具体值可以根据不同业务场景进行定义,例如时间戳 + /// + public virtual long ScoreValue=> ((long)FocusId << 32) | (uint)MeterId; + } +} diff --git a/src/JiShe.CollectBus.Common/Models/F25ReadingAnalyze.cs b/shared/JiShe.CollectBus.Common/Models/F25ReadingAnalyze.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Models/F25ReadingAnalyze.cs rename to shared/JiShe.CollectBus.Common/Models/F25ReadingAnalyze.cs diff --git a/src/JiShe.CollectBus.Common/Models/IssuedEventMessage.cs b/shared/JiShe.CollectBus.Common/Models/IssuedEventMessage.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Models/IssuedEventMessage.cs rename to shared/JiShe.CollectBus.Common/Models/IssuedEventMessage.cs diff --git a/src/JiShe.CollectBus.Common/Models/ReqParameter.cs b/shared/JiShe.CollectBus.Common/Models/ReqParameter.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Models/ReqParameter.cs rename to shared/JiShe.CollectBus.Common/Models/ReqParameter.cs diff --git a/src/JiShe.CollectBus.Common/Models/SetAmmeterJFPGEntity.cs b/shared/JiShe.CollectBus.Common/Models/SetAmmeterJFPGEntity.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Models/SetAmmeterJFPGEntity.cs rename to shared/JiShe.CollectBus.Common/Models/SetAmmeterJFPGEntity.cs diff --git a/src/JiShe.CollectBus.Common/Models/TerminalVersionInfoAnalyze.cs b/shared/JiShe.CollectBus.Common/Models/TerminalVersionInfoAnalyze.cs similarity index 100% rename from src/JiShe.CollectBus.Common/Models/TerminalVersionInfoAnalyze.cs rename to shared/JiShe.CollectBus.Common/Models/TerminalVersionInfoAnalyze.cs diff --git a/src/JiShe.CollectBus.Domain.Shared/CollectBusDomainSharedConsts.cs b/shared/JiShe.CollectBus.Domain.Shared/CollectBusDomainSharedConsts.cs similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/CollectBusDomainSharedConsts.cs rename to shared/JiShe.CollectBus.Domain.Shared/CollectBusDomainSharedConsts.cs diff --git a/src/JiShe.CollectBus.Domain.Shared/CollectBusDomainSharedModule.cs b/shared/JiShe.CollectBus.Domain.Shared/CollectBusDomainSharedModule.cs similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/CollectBusDomainSharedModule.cs rename to shared/JiShe.CollectBus.Domain.Shared/CollectBusDomainSharedModule.cs diff --git a/src/JiShe.CollectBus.Domain.Shared/CollectBusErrorCodes.cs b/shared/JiShe.CollectBus.Domain.Shared/CollectBusErrorCodes.cs similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/CollectBusErrorCodes.cs rename to shared/JiShe.CollectBus.Domain.Shared/CollectBusErrorCodes.cs diff --git a/src/JiShe.CollectBus.Domain.Shared/CollectBusGlobalFeatureConfigurator.cs b/shared/JiShe.CollectBus.Domain.Shared/CollectBusGlobalFeatureConfigurator.cs similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/CollectBusGlobalFeatureConfigurator.cs rename to shared/JiShe.CollectBus.Domain.Shared/CollectBusGlobalFeatureConfigurator.cs diff --git a/src/JiShe.CollectBus.Domain.Shared/CollectBusModuleExtensionConfigurator.cs b/shared/JiShe.CollectBus.Domain.Shared/CollectBusModuleExtensionConfigurator.cs similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/CollectBusModuleExtensionConfigurator.cs rename to shared/JiShe.CollectBus.Domain.Shared/CollectBusModuleExtensionConfigurator.cs diff --git a/src/JiShe.CollectBus.Domain.Shared/Enums/DeviceStatus.cs b/shared/JiShe.CollectBus.Domain.Shared/Enums/DeviceStatus.cs similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Enums/DeviceStatus.cs rename to shared/JiShe.CollectBus.Domain.Shared/Enums/DeviceStatus.cs diff --git a/src/JiShe.CollectBus.HttpApi/FodyWeavers.xml b/shared/JiShe.CollectBus.Domain.Shared/FodyWeavers.xml similarity index 100% rename from src/JiShe.CollectBus.HttpApi/FodyWeavers.xml rename to shared/JiShe.CollectBus.Domain.Shared/FodyWeavers.xml diff --git a/src/JiShe.CollectBus.Domain.Shared/Interceptors/ProtocolInspectAttribute.cs b/shared/JiShe.CollectBus.Domain.Shared/Interceptors/ProtocolInspectAttribute.cs similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Interceptors/ProtocolInspectAttribute.cs rename to shared/JiShe.CollectBus.Domain.Shared/Interceptors/ProtocolInspectAttribute.cs diff --git a/src/JiShe.CollectBus.Domain.Shared/Interceptors/ProtocolInspectInterceptor.cs b/shared/JiShe.CollectBus.Domain.Shared/Interceptors/ProtocolInspectInterceptor.cs similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Interceptors/ProtocolInspectInterceptor.cs rename to shared/JiShe.CollectBus.Domain.Shared/Interceptors/ProtocolInspectInterceptor.cs diff --git a/src/JiShe.CollectBus.Domain.Shared/Interceptors/ProtocolInspectInterceptorRegistrar.cs b/shared/JiShe.CollectBus.Domain.Shared/Interceptors/ProtocolInspectInterceptorRegistrar.cs similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Interceptors/ProtocolInspectInterceptorRegistrar.cs rename to shared/JiShe.CollectBus.Domain.Shared/Interceptors/ProtocolInspectInterceptorRegistrar.cs diff --git a/src/JiShe.CollectBus.Domain.Shared/Interfaces/IProtocolInfo.cs b/shared/JiShe.CollectBus.Domain.Shared/Interfaces/IProtocolInfo.cs similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Interfaces/IProtocolInfo.cs rename to shared/JiShe.CollectBus.Domain.Shared/Interfaces/IProtocolInfo.cs diff --git a/src/JiShe.CollectBus.Domain.Shared/JiShe.CollectBus.Domain.Shared.abppkg b/shared/JiShe.CollectBus.Domain.Shared/JiShe.CollectBus.Domain.Shared.abppkg similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/JiShe.CollectBus.Domain.Shared.abppkg rename to shared/JiShe.CollectBus.Domain.Shared/JiShe.CollectBus.Domain.Shared.abppkg diff --git a/src/JiShe.CollectBus.Domain.Shared/JiShe.CollectBus.Domain.Shared.csproj b/shared/JiShe.CollectBus.Domain.Shared/JiShe.CollectBus.Domain.Shared.csproj similarity index 89% rename from src/JiShe.CollectBus.Domain.Shared/JiShe.CollectBus.Domain.Shared.csproj rename to shared/JiShe.CollectBus.Domain.Shared/JiShe.CollectBus.Domain.Shared.csproj index 0552858..a2747f4 100644 --- a/src/JiShe.CollectBus.Domain.Shared/JiShe.CollectBus.Domain.Shared.csproj +++ b/shared/JiShe.CollectBus.Domain.Shared/JiShe.CollectBus.Domain.Shared.csproj @@ -20,7 +20,10 @@ + + + @@ -28,8 +31,4 @@ - - - - diff --git a/src/JiShe.CollectBus.Domain.Shared/Jobs/EPICollectArgs.cs b/shared/JiShe.CollectBus.Domain.Shared/Jobs/EPICollectArgs.cs similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Jobs/EPICollectArgs.cs rename to shared/JiShe.CollectBus.Domain.Shared/Jobs/EPICollectArgs.cs diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/ar.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/ar.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/ar.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/ar.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/cs.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/cs.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/cs.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/cs.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/de.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/de.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/de.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/de.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/en-GB.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/en-GB.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/en-GB.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/en-GB.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/en.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/en.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/en.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/en.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/es.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/es.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/es.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/es.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/fi.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/fi.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/fi.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/fi.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/fr.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/fr.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/fr.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/fr.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/hi.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/hi.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/hi.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/hi.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/hr.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/hr.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/hr.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/hr.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/hu.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/hu.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/hu.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/hu.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/is.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/is.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/is.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/is.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/it.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/it.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/it.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/it.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/nl.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/nl.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/nl.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/nl.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/pl-PL.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/pl-PL.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/pl-PL.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/pl-PL.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/pt-BR.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/pt-BR.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/pt-BR.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/pt-BR.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/ro-RO.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/ro-RO.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/ro-RO.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/ro-RO.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/ru.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/ru.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/ru.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/ru.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/sk.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/sk.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/sk.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/sk.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/sl.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/sl.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/sl.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/sl.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/tr.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/tr.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/tr.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/tr.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/vi.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/vi.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/vi.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/vi.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/zh-Hans.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/zh-Hans.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/zh-Hans.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/zh-Hans.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/zh-Hant.json b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/zh-Hant.json similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/zh-Hant.json rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBus/zh-Hant.json diff --git a/src/JiShe.CollectBus.Domain.Shared/Localization/CollectBusResource.cs b/shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBusResource.cs similarity index 100% rename from src/JiShe.CollectBus.Domain.Shared/Localization/CollectBusResource.cs rename to shared/JiShe.CollectBus.Domain.Shared/Localization/CollectBusResource.cs diff --git a/shared/JiShe.CollectBus.Domain.Shared/Loggers/LoggerExtensions.cs b/shared/JiShe.CollectBus.Domain.Shared/Loggers/LoggerExtensions.cs new file mode 100644 index 0000000..7fbbb41 --- /dev/null +++ b/shared/JiShe.CollectBus.Domain.Shared/Loggers/LoggerExtensions.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace JiShe.CollectBus.Loggers +{ + public static class LoggerExtensions + { + public static void LogInfoToMongo( + this ILogger logger, + EventId eventId, + Exception? exception, + string? message, + params object?[] args) + { + + } + + public static void LogInfoToMongo(this ILogger logger) + { + + } + + private static void Log( + this ILogger logger, + LogLevel logLevel, + EventId eventId, + string? message, + params object?[] args) + { + + } + } +} + \ No newline at end of file diff --git a/src/JiShe.CollectBus.Application.Contracts/Samples/SampleDto.cs b/src/JiShe.CollectBus.Application.Contracts/Samples/SampleDto.cs deleted file mode 100644 index 02a9f19..0000000 --- a/src/JiShe.CollectBus.Application.Contracts/Samples/SampleDto.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace JiShe.CollectBus.Samples; - -public class SampleDto -{ - public int Value { get; set; } -} diff --git a/src/JiShe.CollectBus.Application.Contracts/Subscribers/ISubscriberAppService.cs b/src/JiShe.CollectBus.Application.Contracts/Subscribers/ISubscriberAppService.cs deleted file mode 100644 index bf7dec1..0000000 --- a/src/JiShe.CollectBus.Application.Contracts/Subscribers/ISubscriberAppService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Threading.Tasks; -using JiShe.CollectBus.Common.Models; -using JiShe.CollectBus.MessageReceiveds; -using Volo.Abp.Application.Services; - -namespace JiShe.CollectBus.Subscribers -{ - public interface ISubscriberAppService : IApplicationService - { - Task IssuedEvent(IssuedEventMessage issuedEventMessage); - Task ReceivedEvent(MessageReceived receivedMessage); - Task ReceivedHeartbeatEvent(MessageReceivedHeartbeat receivedHeartbeatMessage); - Task ReceivedLoginEvent(MessageReceivedLogin receivedLoginMessage); - } -} diff --git a/src/JiShe.CollectBus.Application/CollectBusAppService.cs b/src/JiShe.CollectBus.Application/CollectBusAppService.cs deleted file mode 100644 index 4a45327..0000000 --- a/src/JiShe.CollectBus.Application/CollectBusAppService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using JiShe.CollectBus.FreeSql; -using JiShe.CollectBus.Localization; -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.Application.Services; - -namespace JiShe.CollectBus; - -[ApiExplorerSettings(GroupName = CollectBusDomainSharedConsts.Business)] -public abstract class CollectBusAppService : ApplicationService -{ - public IFreeSqlProvider SqlProvider => LazyServiceProvider.LazyGetRequiredService(); - - protected CollectBusAppService() - { - LocalizationResource = typeof(CollectBusResource); - ObjectMapperContext = typeof(CollectBusApplicationModule); - } -} diff --git a/src/JiShe.CollectBus.Application/CollectBusApplicationModule.cs b/src/JiShe.CollectBus.Application/CollectBusApplicationModule.cs deleted file mode 100644 index ab38baa..0000000 --- a/src/JiShe.CollectBus.Application/CollectBusApplicationModule.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.AutoMapper; -using Volo.Abp.Modularity; -using Volo.Abp.Application; -using Volo.Abp.BackgroundWorkers; -using System.Threading.Tasks; -using Volo.Abp; -using System.Reflection; -using JiShe.CollectBus.FreeSql; -using System; -using Volo.Abp.AspNetCore.Mvc.AntiForgery; - -namespace JiShe.CollectBus; - -[DependsOn( - typeof(CollectBusDomainModule), - typeof(CollectBusApplicationContractsModule), - typeof(AbpDddApplicationModule), - typeof(AbpAutoMapperModule), - typeof(AbpBackgroundWorkersModule), - typeof(CollectBusFreeSqlModule) - )] -public class CollectBusApplicationModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddMaps(validate: true); - }); - - Configure(options => - { - options.TokenCookie.Expiration = TimeSpan.FromDays(365); - options.AutoValidateIgnoredHttpMethods.Add("POST"); - }); - } - - public override async Task OnApplicationInitializationAsync( - ApplicationInitializationContext context) - { - var assembly = Assembly.GetExecutingAssembly(); - var types = assembly.GetTypes().Where(t => typeof(ICollectWorker).IsAssignableFrom(t) && !t.IsInterface).ToList(); - - foreach (var type in types) - { - await context.AddBackgroundWorkerAsync(type); - } - } - -} diff --git a/src/JiShe.CollectBus.Application/Plugins/TcpMonitor.cs b/src/JiShe.CollectBus.Application/Plugins/TcpMonitor.cs deleted file mode 100644 index b40273a..0000000 --- a/src/JiShe.CollectBus.Application/Plugins/TcpMonitor.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCore.CAP; -using JiShe.CollectBus.Ammeters; -using JiShe.CollectBus.Common.Enums; -using JiShe.CollectBus.Common.Extensions; -using JiShe.CollectBus.Devices; -using JiShe.CollectBus.Enums; -using JiShe.CollectBus.Interceptors; -using JiShe.CollectBus.MessageReceiveds; -using JiShe.CollectBus.Protocol.Contracts; -using MassTransit; -using Microsoft.Extensions.Logging; -using TouchSocket.Core; -using TouchSocket.Sockets; -using Volo.Abp.Caching; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Entities; -using Volo.Abp.Domain.Repositories; - -namespace JiShe.CollectBus.Plugins -{ - public partial class TcpMonitor : PluginBase, ITransientDependency - { - private readonly ICapPublisher _capBus; - private readonly ILogger _logger; - private readonly IRepository _deviceRepository; - private readonly IDistributedCache _ammeterInfoCache; - - /// - /// - /// - /// - /// - /// - /// - public TcpMonitor(ICapPublisher capBus, - ILogger logger, - IRepository deviceRepository, - IDistributedCache ammeterInfoCache) - { - _capBus = capBus; - _logger = logger; - _deviceRepository = deviceRepository; - _ammeterInfoCache = ammeterInfoCache; - } - - [GeneratorPlugin(typeof(ITcpReceivedPlugin))] - public async Task OnTcpReceived(ITcpSessionClient client, ReceivedDataEventArgs e) - { - var messageHexString = Convert.ToHexString(e.ByteBlock.Span); - var hexStringList = messageHexString.StringToPairs(); - var aFn = (int?)hexStringList.GetAnalyzeValue(CommandChunkEnum.AFN); - var fn = (int?)hexStringList.GetAnalyzeValue(CommandChunkEnum.FN); - var aTuple = (Tuple)hexStringList.GetAnalyzeValue(CommandChunkEnum.A); - if (aFn.HasValue && fn.HasValue) - { - if ((AFN)aFn == AFN.链路接口检测) - { - switch (fn) - { - case 1: - await OnTcpLoginReceived(client, messageHexString, aTuple.Item1); - break; - case 3: - await OnTcpHeartbeatReceived(client, messageHexString, aTuple.Item1); - break; - default: - _logger.LogError($"指令初步解析失败,指令内容:{messageHexString}"); - break; - } - } - else - { - await OnTcpNormalReceived(client, messageHexString, aTuple.Item1); - } - } - else - { - _logger.LogError($"指令初步解析失败,指令内容:{messageHexString}"); - } - - await e.InvokeNext(); - } - - [GeneratorPlugin(typeof(ITcpConnectingPlugin))] - public async Task OnTcpConnecting(ITcpSessionClient client, ConnectingEventArgs e) - { - _logger.LogInformation($"[TCP] ID:{client.Id} IP:{client.GetIPPort()}正在连接中..."); - await e.InvokeNext(); - } - - [GeneratorPlugin(typeof(ITcpConnectedPlugin))] - public async Task OnTcpConnected(ITcpSessionClient client, ConnectedEventArgs e) - { - _logger.LogInformation($"[TCP] ID:{client.Id} IP:{client.GetIPPort()}已连接"); - await e.InvokeNext(); - } - - [GeneratorPlugin(typeof(ITcpClosedPlugin))] - public async Task OnTcpClosed(ITcpSessionClient client, ClosedEventArgs e) - { - var entity = await _deviceRepository.FindAsync(a=>a.ClientId == client.Id); - if (entity != null) - { - entity.UpdateByOnClosed(); - await _deviceRepository.UpdateAsync(entity); - } - else - { - _logger.LogWarning($"[TCP] ID:{client.Id} IP:{client.GetIPPort()}已关闭连接,但采集程序检索失败"); - } - - await e.InvokeNext(); - } - private async Task OnTcpLoginReceived(ITcpSessionClient client, string messageHexString, string deviceNo) - { - var messageReceivedLoginEvent = new MessageReceivedLogin - { - ClientId = client.Id, - ClientIp = client.IP, - ClientPort = client.Port, - MessageHexString = messageHexString, - DeviceNo = deviceNo, - MessageId = NewId.NextGuid().ToString() - }; - await _capBus.PublishAsync(ProtocolConst.SubscriberReceivedLoginEventName, messageReceivedLoginEvent); - var entity = await _deviceRepository.FindAsync(a => a.Number == deviceNo); - if (entity == null) - { - await _deviceRepository.InsertAsync(new Device(deviceNo, client.Id, DateTime.Now, DateTime.Now, DeviceStatus.Online)); - } - else - { - entity.UpdateByLoginAndHeartbeat(client.Id); - await _deviceRepository.UpdateAsync(entity); - } - } - - private async Task OnTcpHeartbeatReceived(ITcpSessionClient client, string messageHexString, string deviceNo) - { - var messageReceivedHeartbeatEvent = new MessageReceivedHeartbeat - { - ClientId = client.Id, - ClientIp = client.IP, - ClientPort = client.Port, - MessageHexString = messageHexString, - DeviceNo = deviceNo, - MessageId = NewId.NextGuid().ToString() - }; - await _capBus.PublishAsync(ProtocolConst.SubscriberReceivedHeartbeatEventName, messageReceivedHeartbeatEvent); - var entity = await _deviceRepository.FindAsync(a => a.Number == deviceNo); - if (entity == null) - { - await _deviceRepository.InsertAsync(new Device(deviceNo, client.Id, DateTime.Now, DateTime.Now, DeviceStatus.Online)); - } - else - { - entity.UpdateByLoginAndHeartbeat(client.Id); - await _deviceRepository.UpdateAsync(entity); - } - } - - private async Task OnTcpNormalReceived(ITcpSessionClient client, string messageHexString, string deviceNo) - { - await _capBus.PublishAsync(ProtocolConst.SubscriberReceivedEventName, new MessageReceived - { - ClientId = client.Id, - ClientIp = client.IP, - ClientPort = client.Port, - MessageHexString = messageHexString, - DeviceNo = deviceNo, - MessageId = NewId.NextGuid().ToString() - }); - } - } -} diff --git a/src/JiShe.CollectBus.Application/Samples/SampleAppService.cs b/src/JiShe.CollectBus.Application/Samples/SampleAppService.cs deleted file mode 100644 index 5c0dae8..0000000 --- a/src/JiShe.CollectBus.Application/Samples/SampleAppService.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using JiShe.CollectBus.FreeSql; -using JiShe.CollectBus.PrepayModel; -using Microsoft.AspNetCore.Authorization; - -namespace JiShe.CollectBus.Samples; - -public class SampleAppService : CollectBusAppService, ISampleAppService -{ - public Task GetAsync() - { - return Task.FromResult( - new SampleDto - { - Value = 42 - } - ); - } - - [Authorize] - public Task GetAuthorizedAsync() - { - return Task.FromResult( - new SampleDto - { - Value = 42 - } - ); - } - - [AllowAnonymous] - public async Task> Test() - { - - var ammeterList = await SqlProvider.Instance.Change(DbEnum.PrepayDB).Select().Where(d => d.TB_CustomerID == 5).Take(10).ToListAsync(); - return ammeterList; - } -} diff --git a/src/JiShe.CollectBus.Application/Subscribers/SubscriberAppService.cs b/src/JiShe.CollectBus.Application/Subscribers/SubscriberAppService.cs deleted file mode 100644 index d5cc55e..0000000 --- a/src/JiShe.CollectBus.Application/Subscribers/SubscriberAppService.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCore.CAP; -using JiShe.CollectBus.Common.Enums; -using JiShe.CollectBus.Common.Models; -using JiShe.CollectBus.Devices; -using JiShe.CollectBus.MessageReceiveds; -using JiShe.CollectBus.Protocol.Contracts; -using JiShe.CollectBus.Protocol.Contracts.Interfaces; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using TouchSocket.Sockets; -using Volo.Abp.Domain.Repositories; - -namespace JiShe.CollectBus.Subscribers -{ - public class SubscriberAppService : CollectBusAppService, ISubscriberAppService,ICapSubscribe - { - private readonly ILogger _logger; - private readonly ITcpService _tcpService; - private readonly IServiceProvider _serviceProvider; - private readonly IRepository _messageReceivedLoginEventRepository; - private readonly IRepository _messageReceivedHeartbeatEventRepository; - private readonly IRepository _messageReceivedEventRepository; - private readonly IRepository _deviceRepository; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The TCP service. - /// The service provider. - /// The message received login event repository. - /// The message received heartbeat event repository. - /// The message received event repository. - /// The device repository. - public SubscriberAppService(ILogger logger, - ITcpService tcpService, IServiceProvider serviceProvider, - IRepository messageReceivedLoginEventRepository, - IRepository messageReceivedHeartbeatEventRepository, - IRepository messageReceivedEventRepository, - IRepository deviceRepository) - { - _logger = logger; - _tcpService = tcpService; - _serviceProvider = serviceProvider; - _messageReceivedLoginEventRepository = messageReceivedLoginEventRepository; - _messageReceivedHeartbeatEventRepository = messageReceivedHeartbeatEventRepository; - _messageReceivedEventRepository = messageReceivedEventRepository; - _deviceRepository = deviceRepository; - } - - [CapSubscribe(ProtocolConst.SubscriberIssuedEventName)] - public async Task IssuedEvent(IssuedEventMessage issuedEventMessage) - { - switch (issuedEventMessage.Type) - { - case IssuedEventType.Heartbeat: - _logger.LogInformation($"IssuedEvent:{issuedEventMessage.MessageId}"); - var heartbeatEntity = await _messageReceivedHeartbeatEventRepository.GetAsync(a => a.MessageId == issuedEventMessage.MessageId); - heartbeatEntity.AckTime = Clock.Now; - heartbeatEntity.IsAck = true; - await _messageReceivedHeartbeatEventRepository.UpdateAsync(heartbeatEntity); - break; - case IssuedEventType.Login: - var loginEntity = await _messageReceivedLoginEventRepository.GetAsync(a => a.MessageId == issuedEventMessage.MessageId); - loginEntity.AckTime = Clock.Now; - loginEntity.IsAck = true; - await _messageReceivedLoginEventRepository.UpdateAsync(loginEntity); - break; - case IssuedEventType.Data: - break; - default: - throw new ArgumentOutOfRangeException(); - } - var device = await _deviceRepository.FindAsync(a => a.Number == issuedEventMessage.DeviceNo); - if (device!=null) - { - await _tcpService.SendAsync(device.ClientId, issuedEventMessage.Message); - - } - } - - [CapSubscribe(ProtocolConst.SubscriberReceivedEventName)] - public async Task ReceivedEvent(MessageReceived receivedMessage) - { - var protocolPlugin = _serviceProvider.GetKeyedService("StandardProtocolPlugin"); - if (protocolPlugin == null) - { - _logger.LogError("协议不存在!"); - } - else - { - await protocolPlugin.AnalyzeAsync(receivedMessage); - await _messageReceivedEventRepository.InsertAsync(receivedMessage); - } - } - - [CapSubscribe(ProtocolConst.SubscriberReceivedHeartbeatEventName)] - public async Task ReceivedHeartbeatEvent(MessageReceivedHeartbeat receivedHeartbeatMessage) - { - _logger.LogInformation("心跳消费队列开始处理"); - var protocolPlugin = _serviceProvider.GetKeyedService("StandardProtocolPlugin"); - if (protocolPlugin == null) - { - _logger.LogError("【心跳消费队列开始处理】协议不存在!"); - } - else - { - await protocolPlugin.HeartbeatAsync(receivedHeartbeatMessage); - await _messageReceivedHeartbeatEventRepository.InsertAsync(receivedHeartbeatMessage); - - _logger.LogInformation($"心跳消费队列完成处理:{receivedHeartbeatMessage.MessageId}"); - } - } - - [CapSubscribe(ProtocolConst.SubscriberReceivedLoginEventName)] - public async Task ReceivedLoginEvent(MessageReceivedLogin receivedLoginMessage) - { - _logger.LogInformation("登录消费队列开始处理"); - var protocolPlugin = _serviceProvider.GetKeyedService("StandardProtocolPlugin"); - if (protocolPlugin == null) - { - _logger.LogError("【登录消费队列开始处理】协议不存在!"); - } - else - { - await protocolPlugin.LoginAsync(receivedLoginMessage); - await _messageReceivedLoginEventRepository.InsertAsync(receivedLoginMessage); - _logger.LogInformation("登录消费队列完成处理"); - } - } - } -} diff --git a/src/JiShe.CollectBus.Domain/JiShe.CollectBus.Domain.csproj b/src/JiShe.CollectBus.Domain/JiShe.CollectBus.Domain.csproj deleted file mode 100644 index ae1b120..0000000 --- a/src/JiShe.CollectBus.Domain/JiShe.CollectBus.Domain.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - net8.0 - enable - JiShe.CollectBus - - - - - - - - - - - - - - - - - - - - - diff --git a/src/JiShe.CollectBus.Domain/PrepayModel/Vi_BaseAmmeterInfo.cs b/src/JiShe.CollectBus.Domain/PrepayModel/Vi_BaseAmmeterInfo.cs deleted file mode 100644 index 2990b1e..0000000 --- a/src/JiShe.CollectBus.Domain/PrepayModel/Vi_BaseAmmeterInfo.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace JiShe.CollectBus.PrepayModel -{ - /// - /// 预付费电表视图模型 - /// - public partial class Vi_BaseAmmeterInfo - { - public int ID { get; set; } - public string BarCode { get; set; } - public string Address { get; set; } - public string BaudRate { get; set; } - public string Password { get; set; } - public string Explan { get; set; } - public System.DateTime AddTime { get; set; } - public bool State { get; set; } - public int TB_EquipmentTypeID { get; set; } - public int BaseID { get; set; } - public string Code { get; set; } - public Nullable MeterCode { get; set; } - public Nullable PortNumber { get; set; } - public int CT { get; set; } - public bool SoftTripsLock { get; set; } - public bool HardwareTripsLock { get; set; } - public bool ValveState { get; set; } - public bool ArchivesState { get; set; } - public Nullable SortNumber { get; set; } - public decimal Balance { get; set; } - public bool IsAlarm { get; set; } - public string sysExplan { get; set; } - public System.DateTime sysAddTime { get; set; } - public bool sysState { get; set; } - public Nullable TB_sysConcentratorID { get; set; } - public Nullable TB_sysChargingSchemeID { get; set; } - public Nullable TB_sysChargingSchemeID1 { get; set; } - public int TB_CustomerID { get; set; } - public Nullable TB_sysAlarmPlanID { get; set; } - public Nullable TB_sysCollectorID { get; set; } - public Nullable TB_sysRoomID { get; set; } - public string SpecialNoCode { get; set; } - public Nullable RatedCurrent { get; set; } - public Nullable TripDelayTime { get; set; } - public Nullable ClosingTime { get; set; } - public Nullable Remainder { get; set; } - public Nullable RatedPower { get; set; } - public Nullable ParentAmmState { get; set; } - public Nullable ParentAmmID { get; set; } - public Nullable LastUpdateTime { get; set; } - public Nullable IsAuthentication { get; set; } - public Nullable IsShowBalance { get; set; } - public string Number { get; set; } - public Nullable LastAuthTime { get; set; } - public Nullable IsAuthenticationFunction { get; set; } - public Nullable IsShowBalanceFunction { get; set; } - public int TB_ProtocolID { get; set; } - public string Operator { get; set; } - public Nullable IsTimingPowerSetting { get; set; } - public string CommSchemeCode { get; set; } - public string RoomAllName { get; set; } - public Nullable IsLadderPrice { get; set; } - public Nullable LadderNum { get; set; } - public Nullable RoomBalance { get; set; } - public Nullable OtherBalance { get; set; } - public Nullable LastValveEventType { get; set; } - public Nullable ReadKwh4 { get; set; } - public Nullable ReadKwh3 { get; set; } - public Nullable ReadKwh2 { get; set; } - public Nullable ReadKwh1 { get; set; } - public Nullable ReadKwh { get; set; } - public Nullable LastEventTime { get; set; } - public Nullable LastReadTime { get; set; } - public string RoomNumber { get; set; } - public string EquipmentName { get; set; } - public Nullable EquipmentId { get; set; } - public Nullable IsMalignantLoad { get; set; } - public string BrandName { get; set; } - public Nullable MalignantPower { get; set; } - public Nullable MalignantPowerState { get; set; } - public Nullable RatedPowerState { get; set; } - public string SolveStatus { get; set; } - public Nullable PowerDownStatus { get; set; } - public Nullable SolveStatusTime { get; set; } - public decimal DisableKwh { get; set; } - public Nullable IsDeliver { get; set; } - public Nullable HandleUser { get; set; } - public string HandleUserName { get; set; } - public string Solutions { get; set; } - public decimal DisableKwh1 { get; set; } - public decimal DisableKwh2 { get; set; } - public decimal DisableKwh3 { get; set; } - public decimal DisableKwh4 { get; set; } - public bool DelaySwitchOff { get; set; } - public string MeterSolveStatus { get; set; } - public Nullable DeliverTime { get; set; } - public bool EnableState { get; set; } - public string DxNbiotDeviceId { get; set; } - public int MeterStatus { get; set; } - public string AllowTripTime { get; set; } - public Nullable LastCostMilliSecond { get; set; } - public string DxNbiotIMEI { get; set; } - public Nullable ReadKwh5 { get; set; } - public Nullable ReadKwh6 { get; set; } - public Nullable ReadKwh7 { get; set; } - public Nullable ReadKwh8 { get; set; } - public Nullable DisableKwh5 { get; set; } - public Nullable DisableKwh6 { get; set; } - public Nullable DisableKwh7 { get; set; } - public Nullable DisableKwh8 { get; set; } - public Nullable IsLadder { get; set; } - public Nullable TimeSpanSetNum { get; set; } - public string ExecutePeriod { get; set; } - public string FreqInterval { get; set; } - public Nullable Tb_sysUseValueAlarmID { get; set; } - public Nullable IsMaxDemandKwh { get; set; } - public Nullable OnLineTime { get; set; } - public Nullable OperatorId { get; set; } - public int PowerCT { get; set; } - public string CommunicationsModule { get; set; } - public Nullable CanBlueTooth { get; set; } - public Nullable CanRemote { get; set; } - public Nullable IsDownPrice { get; set; } - public Nullable BuyCount { get; set; } - } -} diff --git a/src/JiShe.CollectBus.Host/CollectBusHostConst.cs b/src/JiShe.CollectBus.Host/CollectBusHostConst.cs deleted file mode 100644 index 4df4b8d..0000000 --- a/src/JiShe.CollectBus.Host/CollectBusHostConst.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace JiShe.CollectBus.Host -{ - public static class CollectBusHostConst - { - /// - /// 跨域策略名 - /// - public const string DefaultCorsPolicyName = "Default"; - - /// - /// Cookies名称 - /// - public const string DefaultCookieName = "JiShe.CollectBus.Host"; - - } -} diff --git a/src/JiShe.CollectBus.Host/Controllers/WeatherForecastController.cs b/src/JiShe.CollectBus.Host/Controllers/WeatherForecastController.cs deleted file mode 100644 index 986c33b..0000000 --- a/src/JiShe.CollectBus.Host/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace JiShe.CollectBus.Host.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/src/JiShe.CollectBus.Host/Program.cs b/src/JiShe.CollectBus.Host/Program.cs deleted file mode 100644 index 40a13cc..0000000 --- a/src/JiShe.CollectBus.Host/Program.cs +++ /dev/null @@ -1,41 +0,0 @@ -using JiShe.CollectBus.Host; -using Microsoft.AspNetCore.Hosting; -using Serilog; - -public class Program -{ - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseSerilog((context, loggerConfiguration) => - { - loggerConfiguration.ReadFrom.Configuration(context.Configuration); - }) - .UseAutofac() - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - - - - private static IHostBuilder CreateConsoleHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => { ConfigureServices(services, hostContext); }) - .UseAutofac() - .UseSerilog((context, loggerConfiguration) => - { - loggerConfiguration.ReadFrom.Configuration(context.Configuration); - }); - - - private static void ConfigureServices(IServiceCollection services, HostBuilderContext hostContext) - { - services.AddApplication(); - } -} \ No newline at end of file diff --git a/src/JiShe.CollectBus.Host/Properties/launchSettings.json b/src/JiShe.CollectBus.Host/Properties/launchSettings.json deleted file mode 100644 index cf8c7cb..0000000 --- a/src/JiShe.CollectBus.Host/Properties/launchSettings.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:38293", - "sslPort": 44329 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:5063", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:7232;http://localhost:5063", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/src/JiShe.CollectBus.Host/WeatherForecast.cs b/src/JiShe.CollectBus.Host/WeatherForecast.cs deleted file mode 100644 index 137fed8..0000000 --- a/src/JiShe.CollectBus.Host/WeatherForecast.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace JiShe.CollectBus.Host -{ - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } - } -} diff --git a/src/JiShe.CollectBus.Host/appsettings.json b/src/JiShe.CollectBus.Host/appsettings.json deleted file mode 100644 index 4a166da..0000000 --- a/src/JiShe.CollectBus.Host/appsettings.json +++ /dev/null @@ -1,84 +0,0 @@ - { - "Serilog": { - "Using": [ - "Serilog.Sinks.Console", - "Serilog.Sinks.File" - ], - "MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft": "Information", - "Volo.Abp": "Warning", - "Hangfire": "Information", - "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", - "Kafka": "121.42.242.91:29092,121.42.242.91:39092,121.42.242.91: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": "118.190.144.92:6379,syncTimeout=30000,abortConnect=false,connectTimeout=30000,allowAdmin=true", - "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 - } - } -} \ No newline at end of file diff --git a/src/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbContext.cs b/src/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbContext.cs deleted file mode 100644 index 890a29f..0000000 --- a/src/JiShe.CollectBus.MongoDB/MongoDB/CollectBusMongoDbContext.cs +++ /dev/null @@ -1,31 +0,0 @@ -using JiShe.CollectBus.Devices; -using JiShe.CollectBus.MessageReceiveds; -using JiShe.CollectBus.Protocols; -using MongoDB.Driver; -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 Questions => Collection(); - */ - - public IMongoCollection MessageReceiveds => Collection(); - public IMongoCollection MessageReceivedLogins => Collection(); - public IMongoCollection MessageReceivedHeartbeats => Collection(); - public IMongoCollection Devices => Collection(); - public IMongoCollection ProtocolInfos => Collection(); - - protected override void CreateModel(IMongoModelBuilder modelBuilder) - { - base.CreateModel(modelBuilder); - - modelBuilder.ConfigureCollectBus(); - } -} diff --git a/src/JiShe.CollectBus.Protocol.Contracts/ProtocolConst.cs b/src/JiShe.CollectBus.Protocol.Contracts/ProtocolConst.cs deleted file mode 100644 index 25b160c..0000000 --- a/src/JiShe.CollectBus.Protocol.Contracts/ProtocolConst.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace JiShe.CollectBus.Protocol.Contracts -{ - public class ProtocolConst - { - public const string SubscriberGroup = "jishe.collectbus"; - public const string SubscriberIssuedEventName = "issued.event"; - public const string SubscriberReceivedEventName = "received.event"; - public const string SubscriberReceivedHeartbeatEventName = "received.heartbeat.event"; - public const string SubscriberReceivedLoginEventName = "received.login.event"; - - } -} diff --git a/test/JiShe.CollectBus.Application.Tests/CollectBusApplicationTestBase.cs b/test/JiShe.CollectBus.Application.Tests/CollectBusApplicationTestBase.cs deleted file mode 100644 index f680698..0000000 --- a/test/JiShe.CollectBus.Application.Tests/CollectBusApplicationTestBase.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Volo.Abp.Modularity; - -namespace JiShe.CollectBus; - -/* Inherit from this class for your application layer tests. - * See SampleAppService_Tests for example. - */ -public abstract class CollectBusApplicationTestBase : CollectBusTestBase - where TStartupModule : IAbpModule -{ - -} diff --git a/test/JiShe.CollectBus.Application.Tests/CollectBusApplicationTestModule.cs b/test/JiShe.CollectBus.Application.Tests/CollectBusApplicationTestModule.cs deleted file mode 100644 index bc81f70..0000000 --- a/test/JiShe.CollectBus.Application.Tests/CollectBusApplicationTestModule.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Volo.Abp.Modularity; - -namespace JiShe.CollectBus; - -[DependsOn( - typeof(CollectBusApplicationModule), - typeof(CollectBusDomainTestModule) - )] -public class CollectBusApplicationTestModule : AbpModule -{ - -} diff --git a/test/JiShe.CollectBus.Application.Tests/FodyWeavers.xml b/test/JiShe.CollectBus.Application.Tests/FodyWeavers.xml deleted file mode 100644 index 1715698..0000000 --- a/test/JiShe.CollectBus.Application.Tests/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/test/JiShe.CollectBus.Application.Tests/JiShe.CollectBus.Application.Tests.abppkg b/test/JiShe.CollectBus.Application.Tests/JiShe.CollectBus.Application.Tests.abppkg deleted file mode 100644 index a686451..0000000 --- a/test/JiShe.CollectBus.Application.Tests/JiShe.CollectBus.Application.Tests.abppkg +++ /dev/null @@ -1,3 +0,0 @@ -{ - "role": "lib.test" -} \ No newline at end of file diff --git a/test/JiShe.CollectBus.Application.Tests/JiShe.CollectBus.Application.Tests.csproj b/test/JiShe.CollectBus.Application.Tests/JiShe.CollectBus.Application.Tests.csproj deleted file mode 100644 index 04959f7..0000000 --- a/test/JiShe.CollectBus.Application.Tests/JiShe.CollectBus.Application.Tests.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - net8.0 - enable - JiShe.CollectBus - - - - - - - - - diff --git a/test/JiShe.CollectBus.Application.Tests/Samples/SampleAppService_Tests.cs b/test/JiShe.CollectBus.Application.Tests/Samples/SampleAppService_Tests.cs deleted file mode 100644 index 9b81ebf..0000000 --- a/test/JiShe.CollectBus.Application.Tests/Samples/SampleAppService_Tests.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Modularity; -using Xunit; - -namespace JiShe.CollectBus.Samples; - -public abstract class SampleAppService_Tests : CollectBusApplicationTestBase - where TStartupModule : IAbpModule -{ - private readonly ISampleAppService _sampleAppService; - - protected SampleAppService_Tests() - { - _sampleAppService = GetRequiredService(); - } - - [Fact] - public async Task GetAsync() - { - var result = await _sampleAppService.GetAsync(); - result.Value.ShouldBe(42); - } - - [Fact] - public async Task GetAuthorizedAsync() - { - var result = await _sampleAppService.GetAuthorizedAsync(); - result.Value.ShouldBe(42); - } -} diff --git a/test/JiShe.CollectBus.Domain.Tests/CollectBusDomainTestBase.cs b/test/JiShe.CollectBus.Domain.Tests/CollectBusDomainTestBase.cs deleted file mode 100644 index 71a0f76..0000000 --- a/test/JiShe.CollectBus.Domain.Tests/CollectBusDomainTestBase.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Volo.Abp.Modularity; - -namespace JiShe.CollectBus; - -/* Inherit from this class for your domain layer tests. - * See SampleManager_Tests for example. - */ -public abstract class CollectBusDomainTestBase : CollectBusTestBase - where TStartupModule : IAbpModule -{ - -} diff --git a/test/JiShe.CollectBus.Domain.Tests/CollectBusDomainTestModule.cs b/test/JiShe.CollectBus.Domain.Tests/CollectBusDomainTestModule.cs deleted file mode 100644 index ff4d85b..0000000 --- a/test/JiShe.CollectBus.Domain.Tests/CollectBusDomainTestModule.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Volo.Abp.Modularity; - -namespace JiShe.CollectBus; - -[DependsOn( - typeof(CollectBusDomainModule), - typeof(CollectBusTestBaseModule) -)] -public class CollectBusDomainTestModule : AbpModule -{ - -} diff --git a/test/JiShe.CollectBus.Domain.Tests/FodyWeavers.xml b/test/JiShe.CollectBus.Domain.Tests/FodyWeavers.xml deleted file mode 100644 index 1715698..0000000 --- a/test/JiShe.CollectBus.Domain.Tests/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/test/JiShe.CollectBus.Domain.Tests/JiShe.CollectBus.Domain.Tests.abppkg b/test/JiShe.CollectBus.Domain.Tests/JiShe.CollectBus.Domain.Tests.abppkg deleted file mode 100644 index a686451..0000000 --- a/test/JiShe.CollectBus.Domain.Tests/JiShe.CollectBus.Domain.Tests.abppkg +++ /dev/null @@ -1,3 +0,0 @@ -{ - "role": "lib.test" -} \ No newline at end of file diff --git a/test/JiShe.CollectBus.Domain.Tests/JiShe.CollectBus.Domain.Tests.csproj b/test/JiShe.CollectBus.Domain.Tests/JiShe.CollectBus.Domain.Tests.csproj deleted file mode 100644 index e9b2d08..0000000 --- a/test/JiShe.CollectBus.Domain.Tests/JiShe.CollectBus.Domain.Tests.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - net8.0 - enable - JiShe.CollectBus - - - - - - - - - - - - - diff --git a/test/JiShe.CollectBus.Domain.Tests/Samples/SampleManager_Tests.cs b/test/JiShe.CollectBus.Domain.Tests/Samples/SampleManager_Tests.cs deleted file mode 100644 index 431484b..0000000 --- a/test/JiShe.CollectBus.Domain.Tests/Samples/SampleManager_Tests.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Threading.Tasks; -using Volo.Abp.Modularity; -using Xunit; - -namespace JiShe.CollectBus.Samples; - -public abstract class SampleManager_Tests : CollectBusDomainTestBase - where TStartupModule : IAbpModule -{ - //private readonly SampleManager _sampleManager; - - public SampleManager_Tests() - { - //_sampleManager = GetRequiredService(); - } - - [Fact] - public Task Method1Async() - { - return Task.CompletedTask; - } -} diff --git a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/Applications/EfCoreSampleAppService_Tests.cs b/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/Applications/EfCoreSampleAppService_Tests.cs deleted file mode 100644 index d63e41d..0000000 --- a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/Applications/EfCoreSampleAppService_Tests.cs +++ /dev/null @@ -1,9 +0,0 @@ -using JiShe.CollectBus.Samples; -using Xunit; - -namespace JiShe.CollectBus.EntityFrameworkCore.Applications; - -public class EfCoreSampleAppService_Tests : SampleAppService_Tests -{ - -} diff --git a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/CollectBusEntityFrameworkCoreTestBase.cs b/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/CollectBusEntityFrameworkCoreTestBase.cs deleted file mode 100644 index dbb442b..0000000 --- a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/CollectBusEntityFrameworkCoreTestBase.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace JiShe.CollectBus.EntityFrameworkCore; - -/* This class can be used as a base class for EF Core integration tests, - * while SampleRepository_Tests uses a different approach. - */ -public abstract class CollectBusEntityFrameworkCoreTestBase : CollectBusTestBase -{ - -} diff --git a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/CollectBusEntityFrameworkCoreTestModule.cs b/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/CollectBusEntityFrameworkCoreTestModule.cs deleted file mode 100644 index ec59a28..0000000 --- a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/CollectBusEntityFrameworkCoreTestModule.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage; -using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.EntityFrameworkCore.Sqlite; -using Volo.Abp.Modularity; -using Volo.Abp.Uow; - -namespace JiShe.CollectBus.EntityFrameworkCore; - -[DependsOn( - typeof(CollectBusApplicationTestModule), - typeof(CollectBusEntityFrameworkCoreModule), - typeof(AbpEntityFrameworkCoreSqliteModule) -)] -public class CollectBusEntityFrameworkCoreTestModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddAlwaysDisableUnitOfWorkTransaction(); - - var sqliteConnection = CreateDatabaseAndGetConnection(); - - Configure(options => - { - options.Configure(abpDbContextConfigurationContext => - { - abpDbContextConfigurationContext.DbContextOptions.UseSqlite(sqliteConnection); - }); - }); - } - - private static SqliteConnection CreateDatabaseAndGetConnection() - { - var connection = new SqliteConnection("Data Source=:memory:"); - connection.Open(); - - new CollectBusDbContext( - new DbContextOptionsBuilder().UseSqlite(connection).Options - ).GetService().CreateTables(); - - return connection; - } -} diff --git a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/Domains/EfCoreSampleDomain_Tests.cs b/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/Domains/EfCoreSampleDomain_Tests.cs deleted file mode 100644 index 2a115f5..0000000 --- a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/Domains/EfCoreSampleDomain_Tests.cs +++ /dev/null @@ -1,9 +0,0 @@ -using JiShe.CollectBus.Samples; -using Xunit; - -namespace JiShe.CollectBus.EntityFrameworkCore.Domains; - -public class EfCoreSampleDomain_Tests : SampleManager_Tests -{ - -} diff --git a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/Samples/SampleRepository_Tests.cs b/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/Samples/SampleRepository_Tests.cs deleted file mode 100644 index 01dd9e2..0000000 --- a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/EntityFrameworkCore/Samples/SampleRepository_Tests.cs +++ /dev/null @@ -1,11 +0,0 @@ -using JiShe.CollectBus.Samples; - -namespace JiShe.CollectBus.EntityFrameworkCore.Samples; - -public class SampleRepository_Tests : SampleRepository_Tests -{ - /* Don't write custom repository tests here, instead write to - * the base class. - * One exception can be some specific tests related to EF core. - */ -} diff --git a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/FodyWeavers.xml b/test/JiShe.CollectBus.EntityFrameworkCore.Tests/FodyWeavers.xml deleted file mode 100644 index 1715698..0000000 --- a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/JiShe.CollectBus.EntityFrameworkCore.Tests.abppkg b/test/JiShe.CollectBus.EntityFrameworkCore.Tests/JiShe.CollectBus.EntityFrameworkCore.Tests.abppkg deleted file mode 100644 index a686451..0000000 --- a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/JiShe.CollectBus.EntityFrameworkCore.Tests.abppkg +++ /dev/null @@ -1,3 +0,0 @@ -{ - "role": "lib.test" -} \ No newline at end of file diff --git a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/JiShe.CollectBus.EntityFrameworkCore.Tests.csproj b/test/JiShe.CollectBus.EntityFrameworkCore.Tests/JiShe.CollectBus.EntityFrameworkCore.Tests.csproj deleted file mode 100644 index 63c7e8e..0000000 --- a/test/JiShe.CollectBus.EntityFrameworkCore.Tests/JiShe.CollectBus.EntityFrameworkCore.Tests.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - net8.0 - enable - JiShe.CollectBus - - - - - - - - - - - diff --git a/test/JiShe.CollectBus.MongoDB.Tests/FodyWeavers.xml b/test/JiShe.CollectBus.MongoDB.Tests/FodyWeavers.xml deleted file mode 100644 index 1715698..0000000 --- a/test/JiShe.CollectBus.MongoDB.Tests/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/test/JiShe.CollectBus.MongoDB.Tests/JiShe.CollectBus.MongoDB.Tests.abppkg b/test/JiShe.CollectBus.MongoDB.Tests/JiShe.CollectBus.MongoDB.Tests.abppkg deleted file mode 100644 index a686451..0000000 --- a/test/JiShe.CollectBus.MongoDB.Tests/JiShe.CollectBus.MongoDB.Tests.abppkg +++ /dev/null @@ -1,3 +0,0 @@ -{ - "role": "lib.test" -} \ No newline at end of file diff --git a/test/JiShe.CollectBus.MongoDB.Tests/JiShe.CollectBus.MongoDB.Tests.csproj b/test/JiShe.CollectBus.MongoDB.Tests/JiShe.CollectBus.MongoDB.Tests.csproj deleted file mode 100644 index c17ce34..0000000 --- a/test/JiShe.CollectBus.MongoDB.Tests/JiShe.CollectBus.MongoDB.Tests.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - net8.0 - enable - JiShe.CollectBus - - - - - - - - - - - - - diff --git a/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/Applications/MongoDBSampleAppService_Tests.cs b/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/Applications/MongoDBSampleAppService_Tests.cs deleted file mode 100644 index f6dd5c7..0000000 --- a/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/Applications/MongoDBSampleAppService_Tests.cs +++ /dev/null @@ -1,11 +0,0 @@ -using JiShe.CollectBus.MongoDB; -using JiShe.CollectBus.Samples; -using Xunit; - -namespace JiShe.CollectBus.MongoDb.Applications; - -[Collection(MongoTestCollection.Name)] -public class MongoDBSampleAppService_Tests : SampleAppService_Tests -{ - -} diff --git a/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/CollectBusMongoDbTestBase.cs b/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/CollectBusMongoDbTestBase.cs deleted file mode 100644 index 242879a..0000000 --- a/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/CollectBusMongoDbTestBase.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace JiShe.CollectBus.MongoDB; - -/* This class can be used as a base class for MongoDB integration tests, - * while SampleRepository_Tests uses a different approach. - */ -public abstract class CollectBusMongoDbTestBase : CollectBusTestBase -{ - -} diff --git a/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/CollectBusMongoDbTestModule.cs b/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/CollectBusMongoDbTestModule.cs deleted file mode 100644 index 02028e7..0000000 --- a/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/CollectBusMongoDbTestModule.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Volo.Abp.Data; -using Volo.Abp.Modularity; -using Volo.Abp.Uow; - -namespace JiShe.CollectBus.MongoDB; - -[DependsOn( - typeof(CollectBusApplicationTestModule), - typeof(CollectBusMongoDbModule) -)] -public class CollectBusMongoDbTestModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - Configure(options => - { - options.ConnectionStrings.Default = MongoDbFixture.GetRandomConnectionString(); - }); - } -} diff --git a/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/Domains/MongoDBSampleDomain_Tests.cs b/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/Domains/MongoDBSampleDomain_Tests.cs deleted file mode 100644 index 466ae53..0000000 --- a/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/Domains/MongoDBSampleDomain_Tests.cs +++ /dev/null @@ -1,10 +0,0 @@ -using JiShe.CollectBus.Samples; -using Xunit; - -namespace JiShe.CollectBus.MongoDB.Domains; - -[Collection(MongoTestCollection.Name)] -public class MongoDBSampleDomain_Tests : SampleManager_Tests -{ - -} diff --git a/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/MongoDbFixture.cs b/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/MongoDbFixture.cs deleted file mode 100644 index 540bd3c..0000000 --- a/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/MongoDbFixture.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using EphemeralMongo; - -namespace JiShe.CollectBus.MongoDB; - -public class MongoDbFixture : IDisposable -{ - public readonly static IMongoRunner MongoDbRunner; - - static MongoDbFixture() - { - MongoDbRunner = MongoRunner.Run(new MongoRunnerOptions - { - UseSingleNodeReplicaSet = true - }); - } - - public static string GetRandomConnectionString() - { - return GetConnectionString("Db_" + Guid.NewGuid().ToString("N")); - } - - public static string GetConnectionString(string databaseName) - { - var stringArray = MongoDbRunner.ConnectionString.Split('?'); - var connectionString = stringArray[0].EnsureEndsWith('/') + databaseName + "/?" + stringArray[1]; - return connectionString; - } - - public void Dispose() - { - MongoDbRunner?.Dispose(); - } -} diff --git a/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/MongoTestCollection.cs b/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/MongoTestCollection.cs deleted file mode 100644 index 08fc3b9..0000000 --- a/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/MongoTestCollection.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Xunit; - -namespace JiShe.CollectBus.MongoDB; - -[CollectionDefinition(Name)] -public class MongoTestCollection : ICollectionFixture -{ - public const string Name = "MongoDB Collection"; -} diff --git a/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/Samples/SampleRepository_Tests.cs b/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/Samples/SampleRepository_Tests.cs deleted file mode 100644 index 2fc8587..0000000 --- a/test/JiShe.CollectBus.MongoDB.Tests/MongoDB/Samples/SampleRepository_Tests.cs +++ /dev/null @@ -1,13 +0,0 @@ -using JiShe.CollectBus.Samples; -using Xunit; - -namespace JiShe.CollectBus.MongoDB.Samples; - -[Collection(MongoTestCollection.Name)] -public class SampleRepository_Tests : SampleRepository_Tests -{ - /* Don't write custom repository tests here, instead write to - * the base class. - * One exception can be some specific tests related to MongoDB. - */ -} diff --git a/test/JiShe.CollectBus.TestBase/CollectBusDataSeedContributor.cs b/test/JiShe.CollectBus.TestBase/CollectBusDataSeedContributor.cs deleted file mode 100644 index a72a0d1..0000000 --- a/test/JiShe.CollectBus.TestBase/CollectBusDataSeedContributor.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Guids; -using Volo.Abp.MultiTenancy; - -namespace JiShe.CollectBus; - -public class CollectBusDataSeedContributor : IDataSeedContributor, ITransientDependency -{ - private readonly IGuidGenerator _guidGenerator; - private readonly ICurrentTenant _currentTenant; - - public CollectBusDataSeedContributor( - IGuidGenerator guidGenerator, ICurrentTenant currentTenant) - { - _guidGenerator = guidGenerator; - _currentTenant = currentTenant; - } - - public Task SeedAsync(DataSeedContext context) - { - /* Instead of returning the Task.CompletedTask, you can insert your test data - * at this point! - */ - - using (_currentTenant.Change(context?.TenantId)) - { - return Task.CompletedTask; - } - } -} diff --git a/test/JiShe.CollectBus.TestBase/CollectBusTestBase.cs b/test/JiShe.CollectBus.TestBase/CollectBusTestBase.cs deleted file mode 100644 index 7db1dce..0000000 --- a/test/JiShe.CollectBus.TestBase/CollectBusTestBase.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp; -using Volo.Abp.Modularity; -using Volo.Abp.Uow; -using Volo.Abp.Testing; - -namespace JiShe.CollectBus; - -/* All test classes are derived from this class, directly or indirectly. */ -public abstract class CollectBusTestBase : AbpIntegratedTest - where TStartupModule : IAbpModule -{ - protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) - { - options.UseAutofac(); - } - - protected virtual Task WithUnitOfWorkAsync(Func func) - { - return WithUnitOfWorkAsync(new AbpUnitOfWorkOptions(), func); - } - - protected virtual async Task WithUnitOfWorkAsync(AbpUnitOfWorkOptions options, Func action) - { - using (var scope = ServiceProvider.CreateScope()) - { - var uowManager = scope.ServiceProvider.GetRequiredService(); - - using (var uow = uowManager.Begin(options)) - { - await action(); - - await uow.CompleteAsync(); - } - } - } - - protected virtual Task WithUnitOfWorkAsync(Func> func) - { - return WithUnitOfWorkAsync(new AbpUnitOfWorkOptions(), func); - } - - protected virtual async Task WithUnitOfWorkAsync(AbpUnitOfWorkOptions options, Func> func) - { - using (var scope = ServiceProvider.CreateScope()) - { - var uowManager = scope.ServiceProvider.GetRequiredService(); - - using (var uow = uowManager.Begin(options)) - { - var result = await func(); - await uow.CompleteAsync(); - return result; - } - } - } -} diff --git a/test/JiShe.CollectBus.TestBase/CollectBusTestBaseModule.cs b/test/JiShe.CollectBus.TestBase/CollectBusTestBaseModule.cs deleted file mode 100644 index 89b154e..0000000 --- a/test/JiShe.CollectBus.TestBase/CollectBusTestBaseModule.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp; -using Volo.Abp.Authorization; -using Volo.Abp.Autofac; -using Volo.Abp.Data; -using Volo.Abp.Guids; -using Volo.Abp.Modularity; -using Volo.Abp.Threading; - -namespace JiShe.CollectBus; - -[DependsOn( - typeof(AbpAutofacModule), - typeof(AbpTestBaseModule), - typeof(AbpAuthorizationModule), - typeof(AbpGuidsModule) -)] -public class CollectBusTestBaseModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddAlwaysAllowAuthorization(); - } - - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - SeedTestData(context); - } - - private static void SeedTestData(ApplicationInitializationContext context) - { - AsyncHelper.RunSync(async () => - { - using (var scope = context.ServiceProvider.CreateScope()) - { - await scope.ServiceProvider - .GetRequiredService() - .SeedAsync(); - } - }); - } -} diff --git a/test/JiShe.CollectBus.TestBase/FodyWeavers.xml b/test/JiShe.CollectBus.TestBase/FodyWeavers.xml deleted file mode 100644 index 1715698..0000000 --- a/test/JiShe.CollectBus.TestBase/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/test/JiShe.CollectBus.TestBase/JiShe.CollectBus.TestBase.abppkg b/test/JiShe.CollectBus.TestBase/JiShe.CollectBus.TestBase.abppkg deleted file mode 100644 index a686451..0000000 --- a/test/JiShe.CollectBus.TestBase/JiShe.CollectBus.TestBase.abppkg +++ /dev/null @@ -1,3 +0,0 @@ -{ - "role": "lib.test" -} \ No newline at end of file diff --git a/test/JiShe.CollectBus.TestBase/JiShe.CollectBus.TestBase.csproj b/test/JiShe.CollectBus.TestBase/JiShe.CollectBus.TestBase.csproj deleted file mode 100644 index cda873b..0000000 --- a/test/JiShe.CollectBus.TestBase/JiShe.CollectBus.TestBase.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - net8.0 - enable - JiShe.CollectBus - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - - - - - diff --git a/test/JiShe.CollectBus.TestBase/Samples/SampleRepository_Tests.cs b/test/JiShe.CollectBus.TestBase/Samples/SampleRepository_Tests.cs deleted file mode 100644 index 06874fd..0000000 --- a/test/JiShe.CollectBus.TestBase/Samples/SampleRepository_Tests.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Threading.Tasks; -using Volo.Abp.Modularity; -using Xunit; - -namespace JiShe.CollectBus.Samples; - -/* Write your custom repository tests like that, in this project, as abstract classes. - * Then inherit these abstract classes from EF Core & MongoDB test projects. - * In this way, both database providers are tests with the same set tests. - */ -public abstract class SampleRepository_Tests : CollectBusTestBase - where TStartupModule : IAbpModule -{ - //private readonly ISampleRepository _sampleRepository; - - protected SampleRepository_Tests() - { - //_sampleRepository = GetRequiredService(); - } - - [Fact] - public Task Method1Async() - { - return Task.CompletedTask; - } -} diff --git a/test/JiShe.CollectBus.TestBase/Security/FakeCurrentPrincipalAccessor.cs b/test/JiShe.CollectBus.TestBase/Security/FakeCurrentPrincipalAccessor.cs deleted file mode 100644 index a976624..0000000 --- a/test/JiShe.CollectBus.TestBase/Security/FakeCurrentPrincipalAccessor.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using System.Security.Claims; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Security.Claims; - -namespace JiShe.CollectBus.Security; - -[Dependency(ReplaceServices = true)] -public class FakeCurrentPrincipalAccessor : ThreadCurrentPrincipalAccessor -{ - protected override ClaimsPrincipal GetClaimsPrincipal() - { - return GetPrincipal(); - } - - private ClaimsPrincipal GetPrincipal() - { - return new ClaimsPrincipal(new ClaimsIdentity(new List - { - new Claim(AbpClaimTypes.UserId, "2e701e62-0953-4dd3-910b-dc6cc93ccb0d"), - new Claim(AbpClaimTypes.UserName, "admin"), - new Claim(AbpClaimTypes.Email, "admin@abp.io") - } - ) - ); - } -} diff --git a/web/JiShe.CollectBus.Host/CollectBusHostConst.cs b/web/JiShe.CollectBus.Host/CollectBusHostConst.cs new file mode 100644 index 0000000..319dfed --- /dev/null +++ b/web/JiShe.CollectBus.Host/CollectBusHostConst.cs @@ -0,0 +1,31 @@ +namespace JiShe.CollectBus.Host +{ + public static class CollectBusHostConst + { + /// + /// 跨域策略名 + /// + public const string DefaultCorsPolicyName = "Default"; + + /// + /// Cookies名称 + /// + public const string DefaultCookieName = "JiShe.CollectBus.Host"; + + /// + /// SwaggerUi 端点 + /// + public const string SwaggerUiEndPoint = "/swagger"; + + /// + /// Hangfire 端点 + /// + public const string HangfireDashboardEndPoint = "/hangfire"; + + /// + /// CAP 端点 + /// + public const string CapDashboardEndPoint = "/cap"; + + } +} diff --git a/src/JiShe.CollectBus.Host/CollectBusHostModule.Configure.cs b/web/JiShe.CollectBus.Host/CollectBusHostModule.Configure.cs similarity index 84% rename from src/JiShe.CollectBus.Host/CollectBusHostModule.Configure.cs rename to web/JiShe.CollectBus.Host/CollectBusHostModule.Configure.cs index 1f306f8..82cc214 100644 --- a/src/JiShe.CollectBus.Host/CollectBusHostModule.Configure.cs +++ b/web/JiShe.CollectBus.Host/CollectBusHostModule.Configure.cs @@ -18,8 +18,13 @@ using TouchSocket.Core; using TouchSocket.Sockets; using JiShe.CollectBus.Plugins; using JiShe.CollectBus.Consumers; -using JiShe.CollectBus.MessageReceiveds; using JiShe.CollectBus.Protocol.Contracts; +using JiShe.CollectBus.IotSystems.MessageReceiveds; +using JiShe.CollectBus.IotSystems.MessageIssueds; +using Confluent.Kafka; +using MassTransit.SqlTransport.Topology; +using Confluent.Kafka.Admin; +using JiShe.CollectBus.Common.Consts; namespace JiShe.CollectBus.Host @@ -224,6 +229,7 @@ namespace JiShe.CollectBus.Host { config.SetListenIPHosts(int.Parse(configuration["TCP:ClientPort"] ?? "10500")) //.SetTcpDataHandlingAdapter(()=>new StandardFixedHeaderDataHandlingAdapter()) + //.SetGetDefaultNewId(() => Guid.NewGuid().ToString())//定义ClientId的生成策略 .ConfigurePlugins(a => { a.Add(); @@ -251,7 +257,7 @@ namespace JiShe.CollectBus.Host /// The context. /// The configuration. public void ConfigureCap(ServiceConfigurationContext context, IConfiguration configuration) - { + { context.Services.AddCap(x => { x.DefaultGroupName = ProtocolConst.SubscriberGroup; @@ -261,6 +267,11 @@ namespace JiShe.CollectBus.Host x.UseKafka(option => { option.Servers = kafka; + if (!Convert.ToBoolean(configuration["Kafka:EnableAuthorization"])) return; + option.MainConfig.Add("security.protocol", configuration["Kafka:SecurityProtocol"]); + option.MainConfig.Add("sasl.mechanism", configuration["Kafka:SaslMechanism"]); + option.MainConfig.Add("sasl.username", configuration["Kafka:SaslUserName"]); + option.MainConfig.Add("sasl.password", configuration["Kafka:SaslPassword"]); }); x.UseDashboard(); @@ -275,11 +286,18 @@ namespace JiShe.CollectBus.Host /// /// The context. /// The configuration. + /// + /// Configures the mass transit. + /// public void ConfigureMassTransit(ServiceConfigurationContext context, IConfiguration configuration) - { - context.Services.AddMassTransit(x => + { + var consumerConfig = new ConsumerConfig { GroupId = ProtocolConst.SubscriberGroup }; + var producerConfig = new ProducerConfig(); + + context.Services + .AddMassTransit(x => { - x.UsingInMemory(); + x.UsingInMemory((context, cfg) => cfg.ConfigureEndpoints(context)); x.AddConfigureEndpointsCallback((c, name, cfg) => { @@ -300,33 +318,44 @@ namespace JiShe.CollectBus.Host .SetTimeLimit(s: 1) .SetTimeLimitStart(BatchTimeLimitStart.FromLast) .SetConcurrencyLimit(10)); - }); + }); + rider.AddConsumer(); + + rider.AddProducer(ProtocolConst.SubscriberLoginReceivedEventName); + rider.AddProducer(ProtocolConst.SubscriberHeartbeatReceivedEventName); + rider.UsingKafka((c, cfg) => { cfg.Host(configuration.GetConnectionString("Kafka")); - cfg.TopicEndpoint(ProtocolConst.SubscriberReceivedHeartbeatEventName, ProtocolConst.SubscriberGroup, configurator => + cfg.TopicEndpoint(ProtocolConst.SubscriberHeartbeatReceivedEventName, consumerConfig, configurator => { + configurator.AutoOffsetReset = AutoOffsetReset.Earliest; configurator.ConfigureConsumer(c); - configurator.ConfigureConsumeTopology = false; }); - cfg.TopicEndpoint(ProtocolConst.SubscriberReceivedLoginEventName, ProtocolConst.SubscriberGroup, configurator => + cfg.TopicEndpoint(ProtocolConst.SubscriberLoginReceivedEventName, consumerConfig, configurator => { configurator.ConfigureConsumer(c); - configurator.ConfigureConsumeTopology = false; + configurator.AutoOffsetReset = AutoOffsetReset.Earliest; }); - cfg.TopicEndpoint(ProtocolConst.SubscriberReceivedEventName, ProtocolConst.SubscriberGroup, configurator => + cfg.TopicEndpoint(ProtocolConst.SubscriberReceivedEventName, consumerConfig, configurator => { configurator.ConfigureConsumer(c); - configurator.ConfigureConsumeTopology = false; + configurator.AutoOffsetReset = AutoOffsetReset.Earliest; }); - cfg.TopicEndpoint(ProtocolConst.SubscriberIssuedEventName, ProtocolConst.SubscriberGroup, configurator => + cfg.TopicEndpoint(ProtocolConst.SubscriberReceivedEventName, consumerConfig, configurator => { configurator.ConfigureConsumer(c); - configurator.ConfigureConsumeTopology = false; + configurator.AutoOffsetReset = AutoOffsetReset.Earliest; + }); + + cfg.TopicEndpoint(ProtocolConst.AmmeterSubscriberWorkerFifteenMinuteIssuedEventName, consumerConfig, configurator => + { + configurator.ConfigureConsumer(c); + configurator.AutoOffsetReset = AutoOffsetReset.Earliest; }); }); }); diff --git a/src/JiShe.CollectBus.Host/CollectBusHostModule.cs b/web/JiShe.CollectBus.Host/CollectBusHostModule.cs similarity index 93% rename from src/JiShe.CollectBus.Host/CollectBusHostModule.cs rename to web/JiShe.CollectBus.Host/CollectBusHostModule.cs index 933641b..aabc2ba 100644 --- a/src/JiShe.CollectBus.Host/CollectBusHostModule.cs +++ b/web/JiShe.CollectBus.Host/CollectBusHostModule.cs @@ -25,7 +25,7 @@ namespace JiShe.CollectBus.Host typeof(AbpAspNetCoreSerilogModule), typeof(AbpSwashbuckleModule), typeof(CollectBusApplicationModule), - typeof(CollectBusMongoDbModule), + typeof(CollectBusMongoDbModule), typeof(AbpCachingStackExchangeRedisModule), typeof(AbpBackgroundWorkersHangfireModule) )] @@ -45,6 +45,7 @@ namespace JiShe.CollectBus.Host ConfigureHangfire(context); ConfigureCap(context, configuration); //ConfigureMassTransit(context, configuration); + //ConfigureKafkaTopic(context, configuration); ConfigureAuditLog(context); ConfigureCustom(context, configuration); } @@ -81,6 +82,10 @@ namespace JiShe.CollectBus.Host app.UseAuditing(); app.UseAbpSerilogEnrichers(); app.UseUnitOfWork(); + app.UseHangfireDashboard("/hangfire", new DashboardOptions + { + IgnoreAntiforgeryToken = true + }); app.UseConfiguredEndpoints(endpoints => { endpoints.MapHealthChecks("/health", new HealthCheckOptions @@ -89,7 +94,6 @@ namespace JiShe.CollectBus.Host ResponseWriter = HealthCheckResponse.Writer }); }); - app.UseHangfireDashboard(); } } } diff --git a/web/JiShe.CollectBus.Host/Controllers/HomeController.cs b/web/JiShe.CollectBus.Host/Controllers/HomeController.cs new file mode 100644 index 0000000..0b4c594 --- /dev/null +++ b/web/JiShe.CollectBus.Host/Controllers/HomeController.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; + +namespace JiShe.CollectBus.Host.Controllers +{ + public class HomeController : AbpController + { + public ActionResult Index() + { + return Redirect("/Monitor"); + } + } +} diff --git a/src/JiShe.CollectBus.Host/Extensions/CustomApplicationBuilderExtensions.cs b/web/JiShe.CollectBus.Host/Extensions/CustomApplicationBuilderExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Host/Extensions/CustomApplicationBuilderExtensions.cs rename to web/JiShe.CollectBus.Host/Extensions/CustomApplicationBuilderExtensions.cs diff --git a/src/JiShe.CollectBus.Host/Extensions/ServiceCollections/ServiceCollectionExtensions.cs b/web/JiShe.CollectBus.Host/Extensions/ServiceCollections/ServiceCollectionExtensions.cs similarity index 100% rename from src/JiShe.CollectBus.Host/Extensions/ServiceCollections/ServiceCollectionExtensions.cs rename to web/JiShe.CollectBus.Host/Extensions/ServiceCollections/ServiceCollectionExtensions.cs diff --git a/src/JiShe.CollectBus.Host/Hangfire/JobRetryLastFilter.cs b/web/JiShe.CollectBus.Host/Hangfire/JobRetryLastFilter.cs similarity index 100% rename from src/JiShe.CollectBus.Host/Hangfire/JobRetryLastFilter.cs rename to web/JiShe.CollectBus.Host/Hangfire/JobRetryLastFilter.cs diff --git a/src/JiShe.CollectBus.Host/HealthChecks/HealthCheckResponse.cs b/web/JiShe.CollectBus.Host/HealthChecks/HealthCheckResponse.cs similarity index 100% rename from src/JiShe.CollectBus.Host/HealthChecks/HealthCheckResponse.cs rename to web/JiShe.CollectBus.Host/HealthChecks/HealthCheckResponse.cs diff --git a/src/JiShe.CollectBus.Host/JiShe.CollectBus.Host.csproj b/web/JiShe.CollectBus.Host/JiShe.CollectBus.Host.csproj similarity index 82% rename from src/JiShe.CollectBus.Host/JiShe.CollectBus.Host.csproj rename to web/JiShe.CollectBus.Host/JiShe.CollectBus.Host.csproj index 5a549e5..76dfe60 100644 --- a/src/JiShe.CollectBus.Host/JiShe.CollectBus.Host.csproj +++ b/web/JiShe.CollectBus.Host/JiShe.CollectBus.Host.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -25,8 +25,8 @@ - - + + @@ -52,10 +52,16 @@ - - + + + - + + + + + Always + diff --git a/web/JiShe.CollectBus.Host/Pages/Monitor.cshtml b/web/JiShe.CollectBus.Host/Pages/Monitor.cshtml new file mode 100644 index 0000000..aaadf3f --- /dev/null +++ b/web/JiShe.CollectBus.Host/Pages/Monitor.cshtml @@ -0,0 +1,172 @@ +@page +@using JiShe.CollectBus.Host +@model JiShe.CollectBus.Host.Pages.Monitor + + +@{ + Layout = null; +} + + + + + + + + + + 后端服务 + + + +
+ +
+
+
+ + + +
+

+ SwaggerUI +

+
+
+
+ +
+
+ + + +
+

+ Hangfire面板 +

+
+
+
+
+
+ + + +
+

+ CAP +

+
+
+
+ @*
+
+ + + +
+

+ 了解更多... +

+
+
+
*@ +
+
+ + + \ No newline at end of file diff --git a/web/JiShe.CollectBus.Host/Pages/Monitor.cshtml.cs b/web/JiShe.CollectBus.Host/Pages/Monitor.cshtml.cs new file mode 100644 index 0000000..6019db9 --- /dev/null +++ b/web/JiShe.CollectBus.Host/Pages/Monitor.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace JiShe.CollectBus.Host.Pages +{ + public class Monitor : PageModel + { + public void OnGet() + { + + } + } +} \ No newline at end of file diff --git a/src/JiShe.CollectBus.Host/Plugins/ignore.txt b/web/JiShe.CollectBus.Host/Plugins/ignore.txt similarity index 100% rename from src/JiShe.CollectBus.Host/Plugins/ignore.txt rename to web/JiShe.CollectBus.Host/Plugins/ignore.txt diff --git a/web/JiShe.CollectBus.Host/Program.cs b/web/JiShe.CollectBus.Host/Program.cs new file mode 100644 index 0000000..d75a227 --- /dev/null +++ b/web/JiShe.CollectBus.Host/Program.cs @@ -0,0 +1,65 @@ +using JiShe.CollectBus.Host; +using Microsoft.AspNetCore.Hosting; +using Serilog; +using Volo.Abp.Modularity.PlugIns; + +public class Program +{ + /// + /// + /// + /// + /// + public static async Task Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + builder.Host.UseContentRoot(Directory.GetCurrentDirectory()) + .UseSerilog((context, loggerConfiguration) => + { + loggerConfiguration.ReadFrom.Configuration(context.Configuration); + }) + .UseAutofac(); + await builder.AddApplicationAsync(options => + { + // ز̶ģʽȲ + options.PlugInSources.AddFolder(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins")); + }); + var app = builder.Build(); + await app.InitializeApplicationAsync(); + await app.RunAsync(); + + + //await CreateHostBuilder(args).Build().RunAsync(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseSerilog((context, loggerConfiguration) => + { + loggerConfiguration.ReadFrom.Configuration(context.Configuration); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }) + .UseAutofac(); + + + + + private static IHostBuilder CreateConsoleHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => { ConfigureServices(services, hostContext); }) + .UseAutofac() + .UseSerilog((context, loggerConfiguration) => + { + loggerConfiguration.ReadFrom.Configuration(context.Configuration); + }); + + + private static async Task ConfigureServices(IServiceCollection services, HostBuilderContext hostContext) + { + await services.AddApplicationAsync(); + } +} \ No newline at end of file diff --git a/web/JiShe.CollectBus.Host/Properties/launchSettings.json b/web/JiShe.CollectBus.Host/Properties/launchSettings.json new file mode 100644 index 0000000..ab22661 --- /dev/null +++ b/web/JiShe.CollectBus.Host/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "JiShe.CollectBus.Host": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:44315", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/JiShe.CollectBus.Host/Startup.cs b/web/JiShe.CollectBus.Host/Startup.cs similarity index 78% rename from src/JiShe.CollectBus.Host/Startup.cs rename to web/JiShe.CollectBus.Host/Startup.cs index d5a0e4e..32740ee 100644 --- a/src/JiShe.CollectBus.Host/Startup.cs +++ b/web/JiShe.CollectBus.Host/Startup.cs @@ -1,4 +1,5 @@ -using Volo.Abp.Modularity.PlugIns; +using TouchSocket.Core; +using Volo.Abp.Modularity.PlugIns; namespace JiShe.CollectBus.Host { @@ -38,7 +39,15 @@ namespace JiShe.CollectBus.Host /// The lifetime. public void Configure(IApplicationBuilder app, IHostApplicationLifetime lifetime) { - app.InitializeApplication(); + + app.Use(async (context, next) => + { + // 在请求处理之前调用 InitializeApplicationAsync + await app.InitializeApplicationAsync(); + + // 继续请求管道中的下一个中间件 + await next(); + }); } } } diff --git a/src/JiShe.CollectBus.Host/Swaggers/EnumSchemaFilter.cs b/web/JiShe.CollectBus.Host/Swaggers/EnumSchemaFilter.cs similarity index 100% rename from src/JiShe.CollectBus.Host/Swaggers/EnumSchemaFilter.cs rename to web/JiShe.CollectBus.Host/Swaggers/EnumSchemaFilter.cs diff --git a/src/JiShe.CollectBus.Host/Swaggers/HiddenAbpDefaultApiFilter.cs b/web/JiShe.CollectBus.Host/Swaggers/HiddenAbpDefaultApiFilter.cs similarity index 100% rename from src/JiShe.CollectBus.Host/Swaggers/HiddenAbpDefaultApiFilter.cs rename to web/JiShe.CollectBus.Host/Swaggers/HiddenAbpDefaultApiFilter.cs diff --git a/src/JiShe.CollectBus.Host/Swaggers/SwaggerConfig.cs b/web/JiShe.CollectBus.Host/Swaggers/SwaggerConfig.cs similarity index 100% rename from src/JiShe.CollectBus.Host/Swaggers/SwaggerConfig.cs rename to web/JiShe.CollectBus.Host/Swaggers/SwaggerConfig.cs diff --git a/src/JiShe.CollectBus.Host/appsettings.Development.json b/web/JiShe.CollectBus.Host/appsettings.Development.json similarity index 73% rename from src/JiShe.CollectBus.Host/appsettings.Development.json rename to web/JiShe.CollectBus.Host/appsettings.Development.json index 0c208ae..bcdd3fc 100644 --- a/src/JiShe.CollectBus.Host/appsettings.Development.json +++ b/web/JiShe.CollectBus.Host/appsettings.Development.json @@ -1,7 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Warning", "Microsoft.AspNetCore": "Warning" } } diff --git a/web/JiShe.CollectBus.Host/appsettings.json b/web/JiShe.CollectBus.Host/appsettings.json new file mode 100644 index 0000000..7cf18d7 --- /dev/null +++ b/web/JiShe.CollectBus.Host/appsettings.json @@ -0,0 +1,186 @@ +{ + "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://mongo_PmEeF3:lixiao1980@192.168.1.9:27017/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": 30, + "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" + }, + { + "Host": "192.168.1.9", + "Port": 9044, + "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 + } + } +} \ No newline at end of file diff --git a/web/JiShe.CollectBus.Host/wwwroot/images/cap.png b/web/JiShe.CollectBus.Host/wwwroot/images/cap.png new file mode 100644 index 0000000..76c667e Binary files /dev/null and b/web/JiShe.CollectBus.Host/wwwroot/images/cap.png differ diff --git a/web/JiShe.CollectBus.Host/wwwroot/images/hangfire.png b/web/JiShe.CollectBus.Host/wwwroot/images/hangfire.png new file mode 100644 index 0000000..5cdeb40 Binary files /dev/null and b/web/JiShe.CollectBus.Host/wwwroot/images/hangfire.png differ diff --git a/web/JiShe.CollectBus.Host/wwwroot/images/miniprofiler.png b/web/JiShe.CollectBus.Host/wwwroot/images/miniprofiler.png new file mode 100644 index 0000000..244c702 Binary files /dev/null and b/web/JiShe.CollectBus.Host/wwwroot/images/miniprofiler.png differ diff --git a/web/JiShe.CollectBus.Host/wwwroot/images/more.png b/web/JiShe.CollectBus.Host/wwwroot/images/more.png new file mode 100644 index 0000000..55c056f Binary files /dev/null and b/web/JiShe.CollectBus.Host/wwwroot/images/more.png differ diff --git a/web/JiShe.CollectBus.Host/wwwroot/images/swagger.png b/web/JiShe.CollectBus.Host/wwwroot/images/swagger.png new file mode 100644 index 0000000..7bcbd43 Binary files /dev/null and b/web/JiShe.CollectBus.Host/wwwroot/images/swagger.png differ diff --git a/web/JiShe.CollectBus.Host/wwwroot/libs/bootstrap/css/bootstrap.min.css b/web/JiShe.CollectBus.Host/wwwroot/libs/bootstrap/css/bootstrap.min.css new file mode 100644 index 0000000..ed3905e --- /dev/null +++ b/web/JiShe.CollectBus.Host/wwwroot/libs/bootstrap/css/bootstrap.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/src/JiShe.CollectBus.HttpApi/CollectBusController.cs b/web/JiShe.CollectBus.HttpApi/CollectBusController.cs similarity index 100% rename from src/JiShe.CollectBus.HttpApi/CollectBusController.cs rename to web/JiShe.CollectBus.HttpApi/CollectBusController.cs diff --git a/src/JiShe.CollectBus.HttpApi/CollectBusHttpApiModule.cs b/web/JiShe.CollectBus.HttpApi/CollectBusHttpApiModule.cs similarity index 100% rename from src/JiShe.CollectBus.HttpApi/CollectBusHttpApiModule.cs rename to web/JiShe.CollectBus.HttpApi/CollectBusHttpApiModule.cs diff --git a/src/JiShe.CollectBus.MongoDB/FodyWeavers.xml b/web/JiShe.CollectBus.HttpApi/FodyWeavers.xml similarity index 100% rename from src/JiShe.CollectBus.MongoDB/FodyWeavers.xml rename to web/JiShe.CollectBus.HttpApi/FodyWeavers.xml diff --git a/src/JiShe.CollectBus.HttpApi/JiShe.CollectBus.HttpApi.abppkg b/web/JiShe.CollectBus.HttpApi/JiShe.CollectBus.HttpApi.abppkg similarity index 100% rename from src/JiShe.CollectBus.HttpApi/JiShe.CollectBus.HttpApi.abppkg rename to web/JiShe.CollectBus.HttpApi/JiShe.CollectBus.HttpApi.abppkg diff --git a/src/JiShe.CollectBus.HttpApi/JiShe.CollectBus.HttpApi.csproj b/web/JiShe.CollectBus.HttpApi/JiShe.CollectBus.HttpApi.csproj similarity index 68% rename from src/JiShe.CollectBus.HttpApi/JiShe.CollectBus.HttpApi.csproj rename to web/JiShe.CollectBus.HttpApi/JiShe.CollectBus.HttpApi.csproj index d5ecd25..3ea1d32 100644 --- a/src/JiShe.CollectBus.HttpApi/JiShe.CollectBus.HttpApi.csproj +++ b/web/JiShe.CollectBus.HttpApi/JiShe.CollectBus.HttpApi.csproj @@ -16,8 +16,8 @@ - - + +
diff --git a/src/JiShe.CollectBus.HttpApi/Samples/SampleController.cs b/web/JiShe.CollectBus.HttpApi/Samples/SampleController.cs similarity index 100% rename from src/JiShe.CollectBus.HttpApi/Samples/SampleController.cs rename to web/JiShe.CollectBus.HttpApi/Samples/SampleController.cs