杨雷 преди 5 месеца
ревизия
06939b5133
променени са 55 файла, в които са добавени 4210 реда и са изтрити 0 реда
  1. +30
    -0
      .dockerignore
  2. +340
    -0
      .gitignore
  3. +460
    -0
      TelpoPush.Fence.Worker/Common/HttpHelperAsync.cs
  4. +137
    -0
      TelpoPush.Fence.Worker/Common/LimitedConcurrencyLevelTaskScheduler.cs
  5. +33
    -0
      TelpoPush.Fence.Worker/Common/TimeHelper.cs
  6. +40
    -0
      TelpoPush.Fence.Worker/Common/Utils.cs
  7. +30
    -0
      TelpoPush.Fence.Worker/Dockerfile
  8. +537
    -0
      TelpoPush.Fence.Worker/Handlers/FenceProcess.cs
  9. +59
    -0
      TelpoPush.Fence.Worker/Handlers/KafkaSubscribe.cs
  10. +73
    -0
      TelpoPush.Fence.Worker/Models/CacheTemplates/DeviceFenceModel.cs
  11. +19
    -0
      TelpoPush.Fence.Worker/Models/CacheTemplates/DeviceInfoModel.cs
  12. +15
    -0
      TelpoPush.Fence.Worker/Models/Config/FenceConfig.cs
  13. +94
    -0
      TelpoPush.Fence.Worker/Models/Config/RedisConfig.cs
  14. +25
    -0
      TelpoPush.Fence.Worker/Models/Config/ServiceConfig.cs
  15. +34
    -0
      TelpoPush.Fence.Worker/Models/Enum/AlarmType.cs
  16. +18
    -0
      TelpoPush.Fence.Worker/Models/Enum/GeofenceType.cs
  17. +16
    -0
      TelpoPush.Fence.Worker/Models/Enum/HeadersDto.cs
  18. +19
    -0
      TelpoPush.Fence.Worker/Models/Enum/LocationType.cs
  19. +19
    -0
      TelpoPush.Fence.Worker/Models/Enum/MqDataTopic.cs
  20. +156
    -0
      TelpoPush.Fence.Worker/Models/Enum/MqDataType.cs
  21. +26
    -0
      TelpoPush.Fence.Worker/Models/Enum/MqHeader.cs
  22. +94
    -0
      TelpoPush.Fence.Worker/Models/Fence/Algorithms/CirclePolygonAlgorithm.cs
  23. +36
    -0
      TelpoPush.Fence.Worker/Models/Fence/Algorithms/RayCastingAlgorithm.cs
  24. +81
    -0
      TelpoPush.Fence.Worker/Models/Fence/ChinaPolygon.cs
  25. +18
    -0
      TelpoPush.Fence.Worker/Models/Fence/Circle.cs
  26. +199
    -0
      TelpoPush.Fence.Worker/Models/Fence/GeoConvert.cs
  27. +178
    -0
      TelpoPush.Fence.Worker/Models/Fence/GeoUtils.cs
  28. +144
    -0
      TelpoPush.Fence.Worker/Models/Fence/GeofenceHelper.cs
  29. +31
    -0
      TelpoPush.Fence.Worker/Models/Fence/GpsFencePoint.cs
  30. +23
    -0
      TelpoPush.Fence.Worker/Models/Fence/Point.cs
  31. +42
    -0
      TelpoPush.Fence.Worker/Models/MqTemplates/BaseModel.cs
  32. +18
    -0
      TelpoPush.Fence.Worker/Models/PushTemplates/PushWxTemplate.cs
  33. +45
    -0
      TelpoPush.Fence.Worker/Models/PushTemplates/PushXiaoAnPushTemplate.cs
  34. +103
    -0
      TelpoPush.Fence.Worker/Program.cs
  35. +15
    -0
      TelpoPush.Fence.Worker/Properties/launchSettings.json
  36. +82
    -0
      TelpoPush.Fence.Worker/Service/Cache/MemoryCacheUtil.cs
  37. +191
    -0
      TelpoPush.Fence.Worker/Service/Cache/RedisUtil.cs
  38. +65
    -0
      TelpoPush.Fence.Worker/Service/Cache/SqlMapper.cs
  39. +9
    -0
      TelpoPush.Fence.Worker/Service/Mq/IKafkaService.cs
  40. +15
    -0
      TelpoPush.Fence.Worker/Service/Mq/KafkaHeader.cs
  41. +157
    -0
      TelpoPush.Fence.Worker/Service/Mq/KafkaService.cs
  42. +77
    -0
      TelpoPush.Fence.Worker/Service/Mq/MessageProducer.cs
  43. +81
    -0
      TelpoPush.Fence.Worker/Service/Mq/MqProcessMessage.cs
  44. +15
    -0
      TelpoPush.Fence.Worker/Service/Mq/TopicModel.cs
  45. +31
    -0
      TelpoPush.Fence.Worker/TelpoPush.Fence.Worker.csproj
  46. +25
    -0
      TelpoPush.Fence.Worker/Worker.cs
  47. +33
    -0
      TelpoPush.Fence.Worker/appsettings.Development.json
  48. +70
    -0
      TelpoPush.Fence.Worker/appsettings.json
  49. +29
    -0
      TelpoPush.Fence.Worker/appsettings.production.json
  50. +29
    -0
      TelpoPush.Fence.Worker/appsettings.test.json
  51. +25
    -0
      TelpoPushFence.sln
  52. +7
    -0
      nuget.config
  53. +18
    -0
      setup_production.sh
  54. +17
    -0
      setup_test.sh
  55. +27
    -0
      telpo_push_fence_run.sh

+ 30
- 0
.dockerignore Целия файл

@@ -0,0 +1,30 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
!**/.gitignore
!.git/HEAD
!.git/config
!.git/packed-refs
!.git/refs/heads/**

+ 340
- 0
.gitignore Целия файл

@@ -0,0 +1,340 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/

# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

# Visual Studio 2017 auto generated files
Generated\ Files/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUNIT
*.VisualState.xml
TestResult.xml

# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c

# Benchmark Results
BenchmarkDotNet.Artifacts/

# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/

# StyleCop
StyleCopReport.xml

# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc

# Chutzpah Test files
_Chutzpah*

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb

# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap

# Visual Studio Trace Files
*.e2e

# TFS 2012 Local Workspace
$tf/

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user

# JustCode is a .NET coding add-in
.JustCode

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json

# Visual Studio code coverage results
*.coverage
*.coveragexml

# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*

# MightyMoose
*.mm.*
AutoTest.Net/

# Web workbench (sass)
.sass-cache/

# Installshield output folder
[Ee]xpress/

# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj

# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/

# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets

# Microsoft Azure Build Output
csx/
*.build.csdef

# Microsoft Azure Emulator
ecf/
rcf/

# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx

# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/

# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs

# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk

# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak

# SQL Server files
*.mdf
*.ldf
*.ndf

# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- Backup*.rdl

# Microsoft Fakes
FakesAssemblies/

# GhostDoc plugin setting file
*.GhostDoc.xml

# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/

# Visual Studio 6 build log
*.plg

# Visual Studio 6 workspace options file
*.opt

# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw

# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions

# Paket dependency manager
.paket/paket.exe
paket-files/

# FAKE - F# Make
.fake/

# JetBrains Rider
.idea/
*.sln.iml

# CodeRush personal settings
.cr/personal

# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc

# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config

# Tabs Studio
*.tss

# Telerik's JustMock configuration file
*.jmconfig

# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs

# OpenCover UI analysis results
OpenCover/

# Azure Stream Analytics local run output
ASALocalRun/

# MSBuild Binary and Structured Log
*.binlog

# NVidia Nsight GPU debugger configuration file
*.nvuser

# MFractors (Xamarin productivity tool) working folder
.mfractor/

# Local History for Visual Studio
.localhistory/

# BeatPulse healthcheck temp database
healthchecksdb

+ 460
- 0
TelpoPush.Fence.Worker/Common/HttpHelperAsync.cs Целия файл

@@ -0,0 +1,460 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Common
{
/// <summary>
/// HTTP帮助类
/// </summary>
public class HttpHelperAsync
{
private IHttpClientFactory _httpClientFactory;
private readonly ILogger<HttpHelperAsync> _logger;
public HttpHelperAsync(IHttpClientFactory httpClientFactory, ILogger<HttpHelperAsync> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}


#region 异步
/// <summary>
/// 发起POST异步请求表单
/// </summary>
/// <param name="url">请求地址</param>
/// <param name="body">POST提交的内容</param>
/// <param name="bodyMediaType">POST内容的媒体类型,如:application/xml、application/json</param>
/// <param name="responseContentType">HTTP响应上的content-type内容头的值,如:application/xml、application/json、application/text、application/x-www-form-urlencoded等</param>
/// <param name="headers">请求头信息</param>
/// <param name="timeOut">请求超时时间,单位秒</param>
/// <returns>返回string</returns>
public async Task<string> PostFormAsync(string url, MultipartFormDataContent content,
Dictionary<string, string> headers = null,
int timeOut = 5)
{
try
{
var hostName = GetHostName(url);
using (HttpClient client = _httpClientFactory.CreateClient(hostName))
{
client.Timeout = TimeSpan.FromSeconds(timeOut);
if (headers?.Count > 0)
{
foreach (string key in headers.Keys)
{
client.DefaultRequestHeaders.Add(key, headers[key]);
}
}
content.Headers.Add("ContentType", "multipart/form-data");//声明头部
using (HttpResponseMessage response = await client.PostAsync(url, content))
{
return JsonConvert.SerializeObject(new { response.IsSuccessStatusCode, response.StatusCode });
//if (response.IsSuccessStatusCode)
//{
// string responseString = await response.Content.ReadAsStringAsync();
// return responseString;
//}
//else
//{
// return string.Empty;
//}
}
}
}
catch (Exception ex)
{
return $"推送完成:请求响应超过{timeOut}秒,异常 {ex.Message}";
}
}



/// <summary>
/// 发起GET异步请求
/// </summary>
/// <typeparam name="T">返回类型</typeparam>
/// <param name="url">请求地址</param>
/// <param name="headers">请求头信息</param>
/// <param name="timeOut">请求超时时间,单位秒</param>
/// <returns>返回string</returns>
public async Task<string> GetAsync(string url, Dictionary<string, string> headers = null, int timeOut = 30)
{
var hostName = GetHostName(url);
using (HttpClient client = _httpClientFactory.CreateClient(hostName))
{
client.Timeout = TimeSpan.FromSeconds(timeOut);
if (headers?.Count > 0)
{
foreach (string key in headers.Keys)
{
client.DefaultRequestHeaders.Add(key, headers[key]);
}
}
using (HttpResponseMessage response = await client.GetAsync(url))
{
if (response.IsSuccessStatusCode)
{
string responseString = await response.Content.ReadAsStringAsync();
return responseString;
}
else
{
return string.Empty;
}
}
}
}


/// <summary>
/// 发起POST异步请求
/// </summary>
/// <param name="url">请求地址</param>
/// <param name="body">POST提交的内容</param>
/// <param name="bodyMediaType">POST内容的媒体类型,如:application/xml、application/json</param>
/// <param name="responseContentType">HTTP响应上的content-type内容头的值,如:application/xml、application/json、application/text、application/x-www-form-urlencoded等</param>
/// <param name="headers">请求头信息</param>
/// <param name="timeOut">请求超时时间,单位秒</param>
/// <returns>返回string</returns>
public async Task<string> PostAsync(string url, string body,
Dictionary<string, string> headers = null,
int timeOut = 30,
string bodyMediaType = "application/json",
string responseContentType = "application/json;charset=utf-8")
{

//content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

//var clientHandler = new HttpClientHandler
//{
// ServerCertificateCustomValidationCallback = (message, certificate2, arg3, arg4) => true
//};
//using (var client = new HttpClient(clientHandler))
//{
// client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

try
{
var hostName = GetHostName(url);
using (HttpClient client = _httpClientFactory.CreateClient(hostName))
{
client.Timeout = TimeSpan.FromSeconds(timeOut);
if (headers?.Count > 0)
{
foreach (string key in headers.Keys)
{
client.DefaultRequestHeaders.Add(key, headers[key]);
}
}
StringContent content = new StringContent(body, System.Text.Encoding.UTF8, mediaType: bodyMediaType);
if (!string.IsNullOrWhiteSpace(responseContentType))
{
content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(responseContentType);
}
using (HttpResponseMessage response = await client.PostAsync(url, content))
{
if (response.IsSuccessStatusCode)
{
string responseString = await response.Content.ReadAsStringAsync();
return responseString;
}
else
return $"请求异常:{response.IsSuccessStatusCode},{response.StatusCode}";
}
}
}
catch(Exception ex)
{
return $"请求异常:{ex.Message}";
}
}

/// <summary>
/// 发起POST异步请求
/// </summary>
/// <param name="url">请求地址</param>
/// <param name="body">POST提交的内容</param>
/// <param name="bodyMediaType">POST内容的媒体类型,如:application/xml、application/json</param>
/// <param name="responseContentType">HTTP响应上的content-type内容头的值,如:application/xml、application/json、application/text、application/x-www-form-urlencoded等</param>
/// <param name="headers">请求头信息</param>
/// <param name="timeOut">请求超时时间,单位秒</param>
/// <returns>返回string</returns>
public async Task<string> PutAsync(string url, string body,
string bodyMediaType = null,
string responseContentType = null,
Dictionary<string, string> headers = null,
int timeOut = 30)
{
var hostName = GetHostName(url);
using (HttpClient client = _httpClientFactory.CreateClient(hostName))
{
client.Timeout = TimeSpan.FromSeconds(timeOut);
if (headers?.Count > 0)
{
foreach (string key in headers.Keys)
{
client.DefaultRequestHeaders.Add(key, headers[key]);
}
}
StringContent content = new StringContent(body, System.Text.Encoding.UTF8, mediaType: bodyMediaType);
if (!string.IsNullOrWhiteSpace(responseContentType))
{
content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(responseContentType);
}
using (HttpResponseMessage response = await client.PutAsync(url, content))
{
if (response.IsSuccessStatusCode)
{
string responseString = await response.Content.ReadAsStringAsync();
return responseString;
}
else
{
return string.Empty;
}
}
}
}

/// <summary>
/// 发起GET异步请求
/// </summary>
/// <typeparam name="T">返回类型</typeparam>
/// <param name="url">请求地址</param>
/// <param name="headers">请求头信息</param>
/// <param name="timeOut">请求超时时间,单位秒</param>
/// <returns>返回string</returns>
public async Task<string> DeleteAsync(string url, Dictionary<string, string> headers = null, int timeOut = 30)
{
var hostName = GetHostName(url);
using (HttpClient client = _httpClientFactory.CreateClient(hostName))
{
client.Timeout = TimeSpan.FromSeconds(timeOut);
if (headers?.Count > 0)
{
foreach (string key in headers.Keys)
{
client.DefaultRequestHeaders.Add(key, headers[key]);
}
}
using (HttpResponseMessage response = await client.DeleteAsync(url))
{
if (response.IsSuccessStatusCode)
{
string responseString = await response.Content.ReadAsStringAsync();
return responseString;
}
else
{
return string.Empty;
}
}
}
}

/// <summary>
/// 发起GET异步请求
/// </summary>
/// <typeparam name="T">返回类型</typeparam>
/// <param name="url">请求地址</param>
/// <param name="headers">请求头信息</param>
/// <param name="timeOut">请求超时时间,单位秒</param>
/// <returns>返回T</returns>
public async Task<T> GetAsync<T>(string url, Dictionary<string, string> headers = null, int timeOut = 30) where T : new()
{
string responseString = await GetAsync(url, headers, timeOut);
if (!string.IsNullOrWhiteSpace(responseString))
{
return JsonConvert.DeserializeObject<T>(responseString);
}
else
{
return default(T);
}
}


/// <summary>
/// 发起POST异步请求
/// </summary>
/// <typeparam name="T">返回类型</typeparam>
/// <param name="url">请求地址</param>
/// <param name="body">POST提交的内容</param>
/// <param name="bodyMediaType">POST内容的媒体类型,如:application/xml、application/json</param>
/// <param name="responseContentType">HTTP响应上的content-type内容头的值,如:application/xml、application/json、application/text、application/x-www-form-urlencoded等</param>
/// <param name="headers">请求头信息</param>
/// <param name="timeOut">请求超时时间,单位秒</param>
/// <returns>返回T</returns>
public async Task<T> PostAsync<T>(string url, string body,
Dictionary<string, string> headers = null,
int timeOut = 30,
string bodyMediaType = "application/json",
string responseContentType = "application/json;charset=utf-8"
) where T : new()
{
string responseString = await PostAsync(url, body, headers, timeOut, bodyMediaType, responseContentType);
if (!string.IsNullOrWhiteSpace(responseString))
{
return JsonConvert.DeserializeObject<T>(responseString);
}
else
{
return default(T);
}
}

/// <summary>
/// 发起PUT异步请求
/// </summary>
/// <typeparam name="T">返回类型</typeparam>
/// <param name="url">请求地址</param>
/// <param name="body">POST提交的内容</param>
/// <param name="bodyMediaType">POST内容的媒体类型,如:application/xml、application/json</param>
/// <param name="responseContentType">HTTP响应上的content-type内容头的值,如:application/xml、application/json、application/text、application/x-www-form-urlencoded等</param>
/// <param name="headers">请求头信息</param>
/// <param name="timeOut">请求超时时间,单位秒</param>
/// <returns>返回T</returns>
public async Task<T> PutAsync<T>(string url, string body,
string bodyMediaType = null,
string responseContentType = null,
Dictionary<string, string> headers = null,
int timeOut = 30) where T : new()
{
string responseString = await PutAsync(url, body, bodyMediaType, responseContentType, headers, timeOut);
if (!string.IsNullOrWhiteSpace(responseString))
{
return JsonConvert.DeserializeObject<T>(responseString);
}
else
{
return default(T);
}
}

/// <summary>
/// 发起DELETE异步请求
/// </summary>
/// <typeparam name="T">返回类型</typeparam>
/// <param name="url">请求地址</param>
/// <param name="headers">请求头信息</param>
/// <param name="timeOut">请求超时时间,单位秒</param>
/// <returns>返回T</returns>
public async Task<T> DeleteAsync<T>(string url, Dictionary<string, string> headers = null, int timeOut = 30) where T : new()
{
string responseString = await DeleteAsync(url, headers, timeOut);
if (!string.IsNullOrWhiteSpace(responseString))
{
return JsonConvert.DeserializeObject<T>(responseString);
}
else
{
return default(T);
}
}
#region 私有函数

/// <summary>
/// 获取请求的主机名
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
private static string GetHostName(string url)
{
if (!string.IsNullOrWhiteSpace(url))
{
return url.Replace("https://", "").Replace("http://", "").Split('/')[0];
}
else
{
return "AnyHost";
}
}

#endregion


#endregion

#region 同步

/// <summary>
/// 发起GET同步请求
/// </summary>
/// <param name="url"></param>
/// <param name="headers"></param>
/// <param name="contentType"></param>
/// <returns></returns>
///


public string HttpGet(string url, Dictionary<string, string> headers = null, string contentType = "application/json;charset=utf-8")
{
if (string.IsNullOrEmpty(url)) return "";
try
{
using (HttpClient client = new HttpClient())
{
if (contentType != null)
client.DefaultRequestHeaders.Add("ContentType", contentType);
if (headers != null)
{
foreach (var header in headers)
client.DefaultRequestHeaders.Add(header.Key, header.Value);
}
HttpResponseMessage response = client.GetAsync(url).Result;
return response.Content.ReadAsStringAsync().Result;
}
}
catch (Exception ex)
{
_logger.LogDebug($"HttpGet/URL:{url},headers:{JsonConvert.SerializeObject(headers)},异常:{ex.Message}|{ex.Source}|{ex.StackTrace}");
}
return "";
}
/// <summary>
/// 发起POST同步请求
/// </summary>
/// <param name="url"></param>
/// <param name="postData"></param>
/// <param name="contentType">application/xml、application/json、application/text、application/x-www-form-urlencoded</param>
/// <param name="headers">填充消息头</param>
/// <returns></returns>
public string HttpPost(string url, string postData = null, Dictionary<string, string> headers = null, string contentType = "application/json")
{
if (string.IsNullOrEmpty(url)) return "";
try
{
postData = postData ?? "";
using (HttpClient client = new HttpClient())
{
if (headers != null)
{
foreach (var header in headers)
client.DefaultRequestHeaders.Add(header.Key, header.Value);
}
using (HttpContent httpContent = new StringContent(postData, Encoding.UTF8))
{
if (contentType != null)
httpContent.Headers.ContentType = new MediaTypeHeaderValue(contentType);

HttpResponseMessage response = client.PostAsync(url, httpContent).Result;
return response.Content.ReadAsStringAsync().Result;
}
}
}
catch (Exception ex){
_logger.LogDebug($"HttpPost/URL:{url},postStr:{postData},headers:{JsonConvert.SerializeObject(headers)},异常:{ex.Message}|{ex.Source}|{ex.StackTrace}");
}
return "";
}

#endregion
}
}

+ 137
- 0
TelpoPush.Fence.Worker/Common/LimitedConcurrencyLevelTaskScheduler.cs Целия файл

@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Common
{
/// <summary>
/// Provides a task scheduler that ensures a maximum concurrency level while
/// running on top of the ThreadPool.
/// </summary>
public class LimitedConcurrencyLevelTaskScheduler : TaskScheduler
{
/// <summary>Whether the current thread is processing work items.</summary>
[ThreadStatic]
private static bool _currentThreadIsProcessingItems;
/// <summary>The list of tasks to be executed.</summary>
private readonly LinkedList<Task> _tasks = new LinkedList<Task>(); // protected by lock(_tasks)
/// <summary>The maximum concurrency level allowed by this scheduler.</summary>
private readonly int _maxDegreeOfParallelism;
/// <summary>Whether the scheduler is currently processing work items.</summary>
private int _delegatesQueuedOrRunning = 0; // protected by lock(_tasks)

/// <summary>
/// Initializes an instance of the LimitedConcurrencyLevelTaskScheduler class with the
/// specified degree of parallelism.
/// </summary>
/// <param name="maxDegreeOfParallelism">The maximum degree of parallelism provided by this scheduler.</param>
public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism)
{
if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException("maxDegreeOfParallelism");
_maxDegreeOfParallelism = maxDegreeOfParallelism;
}

/// <summary>Queues a task to the scheduler.</summary>
/// <param name="task">The task to be queued.</param>
protected sealed override void QueueTask(Task task)
{
// Add the task to the list of tasks to be processed. If there aren't enough
// delegates currently queued or running to process tasks, schedule another.
lock (_tasks)
{
_tasks.AddLast(task);
if (_delegatesQueuedOrRunning < _maxDegreeOfParallelism)
{
++_delegatesQueuedOrRunning;
NotifyThreadPoolOfPendingWork();
}
}
}

/// <summary>
/// Informs the ThreadPool that there's work to be executed for this scheduler.
/// </summary>
private void NotifyThreadPoolOfPendingWork()
{
ThreadPool.UnsafeQueueUserWorkItem(_ =>
{
// Note that the current thread is now processing work items.
// This is necessary to enable inlining of tasks into this thread.
_currentThreadIsProcessingItems = true;
try
{
// Process all available items in the queue.
while (true)
{
Task item;
lock (_tasks)
{
// When there are no more items to be processed,
// note that we're done processing, and get out.
if (_tasks.Count == 0)
{
--_delegatesQueuedOrRunning;
break;
}

// Get the next item from the queue
item = _tasks.First.Value;
_tasks.RemoveFirst();
}

// Execute the task we pulled out of the queue
base.TryExecuteTask(item);
}
}
// We're done processing items on the current thread
finally { _currentThreadIsProcessingItems = false; }
}, null);
}

/// <summary>Attempts to execute the specified task on the current thread.</summary>
/// <param name="task">The task to be executed.</param>
/// <param name="taskWasPreviouslyQueued"></param>
/// <returns>Whether the task could be executed on the current thread.</returns>
protected sealed override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
// If this thread isn't already processing a task, we don't support inlining
if (!_currentThreadIsProcessingItems) return false;

// If the task was previously queued, remove it from the queue
if (taskWasPreviouslyQueued) TryDequeue(task);

// Try to run the task.
return base.TryExecuteTask(task);
}

/// <summary>Attempts to remove a previously scheduled task from the scheduler.</summary>
/// <param name="task">The task to be removed.</param>
/// <returns>Whether the task could be found and removed.</returns>
protected sealed override bool TryDequeue(Task task)
{
lock (_tasks) return _tasks.Remove(task);
}

/// <summary>Gets the maximum concurrency level supported by this scheduler.</summary>
public sealed override int MaximumConcurrencyLevel { get { return _maxDegreeOfParallelism; } }

/// <summary>Gets an enumerable of the tasks currently scheduled on this scheduler.</summary>
/// <returns>An enumerable of the tasks currently scheduled.</returns>
protected sealed override IEnumerable<Task> GetScheduledTasks()
{
bool lockTaken = false;
try
{
Monitor.TryEnter(_tasks, ref lockTaken);
if (lockTaken) return _tasks.ToArray();
else throw new NotSupportedException();
}
finally
{
if (lockTaken) Monitor.Exit(_tasks);
}
}
}
}

+ 33
- 0
TelpoPush.Fence.Worker/Common/TimeHelper.cs Целия файл

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Common
{
public class TimeHelper
{
/// <summary>
/// 时间戳转成时间类型
/// </summary>
/// <param name="timeStamp"></param>
/// <returns></returns>
public static DateTime ConvertToLocalDateTime(string timeStamp)
{
DateTime dtStart = TimeZoneInfo.ConvertTime(new DateTime(1970, 1, 1), TimeZoneInfo.Utc, TimeZoneInfo.Local);
if (timeStamp.Length == 13)
{
long lTime = long.Parse(timeStamp + "0000");
TimeSpan toNow = new TimeSpan(lTime);
return dtStart.Add(toNow);
}
return dtStart.AddSeconds(long.Parse(timeStamp));
}

public static string ToDateTimeStr(DateTime dt)
{
return dt.ToString("yyyy-MM-dd HH:mm:ss");
}
}
}

+ 40
- 0
TelpoPush.Fence.Worker/Common/Utils.cs Целия файл

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;

namespace TelpoPush.Fence.Worker.Common
{
public class Utils
{
public static MultipartFormDataContent GetMultipartFormDataContent(Dictionary<string, object> dic, string appId, ref Dictionary<string, object> outDic)
{
MultipartFormDataContent mfdc = new MultipartFormDataContent();
StringBuilder sb = new StringBuilder();
if (dic != null && dic.Count > 0)
{
var dicOrderBy = dic.OrderBy(z => z.Key);
foreach (KeyValuePair<string, object> kv in dicOrderBy)
{
sb.Append($"{kv.Key}={kv.Value.ToString()}&");
mfdc.Add(new StringContent(kv.Value.ToString()), kv.Key);//参数, 内容在前,参数名称在后
}
}
string signStr = $"{sb.ToString().Trim('&')}{appId}";
byte[] bytes = Encoding.UTF8.GetBytes(signStr);
byte[] hash = SHA256.Create().ComputeHash(bytes);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < hash.Length; i++)
{
builder.Append(hash[i].ToString("X2"));
}
string sign = builder.ToString().ToLower();
dic.Add("sign", sign);
mfdc.Add(new StringContent(sign), "sign");//参数, 内容在前,参数名称在后
outDic = dic;
return mfdc;
}
}
}

+ 30
- 0
TelpoPush.Fence.Worker/Dockerfile Целия файл

@@ -0,0 +1,30 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
WORKDIR /src
COPY ["TelpoPush.Fence.Worker/TelpoPush.Fence.Worker.csproj", "TelpoPush.Fence.Worker/"]

COPY ["nuget.config","."]
RUN dotnet nuget remove source nuget.org
RUN dotnet nuget add source https://repo.huaweicloud.com/repository/nuget/v3/index.json -n huaweicloud_nuget

RUN dotnet restore "TelpoPush.Fence.Worker/TelpoPush.Fence.Worker.csproj"
COPY . .
WORKDIR "/src/TelpoPush.Fence.Worker"

FROM build AS publish
RUN dotnet publish "TelpoPush.Fence.Worker.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
#COPY pem /app/pem
ENV environment=Development
RUN apk add --no-cache tzdata
ENV TimeZone=Asia/Shanghai
ENV LANG C.UTF-8
ENV ASPNETCORE_ENVIRONMENT_LOG=Development
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone
ENTRYPOINT dotnet TelpoPush.Fence.Worker.dll --environment=$environment

+ 537
- 0
TelpoPush.Fence.Worker/Handlers/FenceProcess.cs Целия файл

@@ -0,0 +1,537 @@
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System.Text;
using TelpoDataService.Util.Clients;
using TelpoDataService.Util;
using TelpoDataService.Util.Entities.GpsCard;
using TelpoDataService.Util.Entities.GpsLocationHistory;
using TelpoDataService.Util.Models;
using TelpoDataService.Util.QueryObjects;
using TelpoPush.Fence.Worker.Common;
using TelpoPush.Fence.Worker.Models.CacheTemplates;
using TelpoPush.Fence.Worker.Models.Config;
using TelpoPush.Fence.Worker.Models.Enum;
using TelpoPush.Fence.Worker.Models.Fence;
using TelpoPush.Fence.Worker.Models.MqTemplates;
using TelpoPush.Fence.Worker.Models.PushTemplates;
using TelpoPush.Fence.Worker.Service.Cache;
using TelpoPush.Fence.Worker.Service.Mq;

namespace TelpoPush.Fence.Worker.Handlers
{
public class FenceProcess
{
private readonly static object _GeofenceStatusLock = new object();
private readonly static object _GeofenceDataLock = new object();
private readonly GpsCardAccessorClient<GpsGeofenceStatus> _fenceStatusApiClient;
private readonly GpsLocationHistoryAccessorClient<HisGpsAlarm> _alarmApiClient;
private readonly IHostEnvironment _env;
private readonly ILogger<FenceProcess> _logger;
private readonly HttpHelperAsync _httpHelper;
private readonly RedisUtil _redis;
private readonly MqProcessMessage _serviceMqProcess;
private readonly FenceConfig _positionConfig;

public FenceProcess(
IHostEnvironment env,
ILogger<FenceProcess> logger,
HttpHelperAsync httpHelper,
RedisUtil redis,
GpsCardAccessorClient<GpsGeofenceStatus> fenceStatusApiClient,
GpsLocationHistoryAccessorClient<HisGpsAlarm> alarmApiClient,
MqProcessMessage serviceMqProcess,
IOptions<FenceConfig> positionConfig
)
{
_env = env;
_logger = logger;
_httpHelper = httpHelper;
_redis = redis;
_positionConfig = positionConfig.Value;
_serviceMqProcess = serviceMqProcess;
_fenceStatusApiClient = fenceStatusApiClient;
_alarmApiClient = alarmApiClient;

}

public async Task SendFence(string message)
{

_redis.ClearGpsDeviceFence();

BaseModel model = JsonConvert.DeserializeObject<BaseModel>(message);
var loc = model?.data;
_logger.LogInformation($"设备{loc.imei},获取kafka数据并解析<{JsonConvert.SerializeObject(model)}>");
if ((int)loc.locationType == 2) // 限制条件:locationType=2 LBS定位不推送
{
_logger.LogInformation($"设备{loc.imei},定位类型为LBS:<locationType={(int)loc.locationType}>不做处理");
return;
}
DateTime time = DateTime.Parse(model.time); //=LastUpdate
DateTime UtcDate = DateTime.Parse(loc.UtcDate);
bool isPush = await _redis.GetIsDateStatus(new DeviceTime
{
imei = loc.imei,
dt = time
});

isPush = true;
if (!isPush)
_logger.LogInformation($"数据未处理(历史数据):{loc.imei},{model.time}");


var fenceObj = await _redis.GetGpsDeviceFence(loc.imei);
if (fenceObj == null)
_logger.LogInformation($"设备<{loc.imei}>还未设置电子围栏!");

int radius = loc.Radius;
GpsFencePoint location = new GpsFencePoint()
{
Longitude = Convert.ToDouble(loc.gaodeLongitude),
Latitude = Convert.ToDouble(loc.gaodeLatitude)
};
var lpt = new GpsPoint(loc.originalLatitude, loc.originalLongitude);
bool isInside = false; //{false=外面,true=里面}
bool isInside_old = false; //{false=外面,true=里面}
try
{
foreach (var fence in fenceObj.fenceList)
{
if (!Convert.ToBoolean(fence.isActive)) //围栏开关 0是关 1是开
{
_logger.LogInformation($"围栏<{fence.geofenceId}:{fence.geofenceName}>未生效!");
continue;
}
//当前gaode坐标为中心点,精度(Radius)为半径画圆
double _lat, _lng;
GeoConvert.G2Gps((double)loc.gaodeLatitude, (double)loc.gaodeLongitude, out _lat, out _lng);
var _cpt = new GpsPoint(Convert.ToDecimal(_lat), Convert.ToDecimal(_lng));
var _circle = new Circle(_cpt, loc.Radius);
switch (fence.fenceType) // 1 为圆形,2 多边形
{
case 1: // 圆形
{
foreach (var item in fence.pointList)
{
//圆形围栏坐标+半径
double lat, lng;
GeoConvert.G2Gps((double)item.latitude, (double)item.longitude, out lat, out lng);
var cpt = new GpsPoint(Convert.ToDecimal(lat), Convert.ToDecimal(lng));
var circle = new Circle(cpt, item.radius);
//是否在围栏范围内
isInside = GeoUtils.IsPointInCircle(lpt, circle);
isInside_old = isInside;
if (!isInside)
{
isInside_old = isInside;
//圆和圆是否有交点
isInside = GeoUtils.IsCircle2Circle(_circle, circle);
}
}
}
break;
case 2: //多边形
List<GpsFencePoint> pointList = new List<GpsFencePoint>();
foreach (var item in fence.pointList)
{
pointList.Add(new GpsFencePoint
{
Longitude = (double)item.longitude,
Latitude = (double)item.latitude
});
}
isInside = GeofenceHelper.IsPolygonContainsPoint(pointList, new GpsFencePoint { Longitude = (double)loc.gaodeLongitude, Latitude = (double)loc.gaodeLatitude });
isInside_old = isInside;
if (!isInside)
{
//圆和多边形围栏否有交点
var degree = GeofenceHelper.CalcDisgreeFromRadius(radius);
isInside = CirclePolygonAlgorithm.IsIntersect(location, degree, pointList);
isInside = GeofenceHelper.IsPolygonIntersectCircle(pointList, location, radius);
}
break;
default:
break;
}

//围栏进处理逻辑----------------------
// [上次(历史缓存)状态]
//lastStatus.isInside => false:在里面、false:在外面;
//[当前状态]
//sInside => false:外面、true:里面
//=============================================
//进入围栏:isInside = true
// status = true
//离开围栏:isInside = false
// status = false
//进入围栏:isInside = true
// status(没有历史记录)


var lastStatus = await GetLastGeofenceStatus(loc.imei, fence.geofenceId, fence.geofenceName);
if (lastStatus != null)
{
if (isInside == lastStatus.isInside)
await DataSaveAsync(model, fence, isInside, isInside_old, lastStatus.isInside, true).ConfigureAwait(false);
}
else
{
if (isInside)
await DataSaveAsync(model, fence, isInside, true, true, false).ConfigureAwait(false);
}
}
}
catch (Exception ex)
{
_logger.LogError($"设备<{loc.imei}>进出围栏报警处理逻辑[异常] {ex.Message}\n{ex.StackTrace}\n{ex.HResult}");
}
}


/// <summary>
/// 进出记录处理
/// </summary>
/// <param name="model">当前定位数据</param>
/// <param name="fence">当前围栏信息</param>
/// <param name="isInside">当前围栏状态[面到面]:(sInside => false:外面、true:里面)</param>
/// <param name="isInside_old">当前围栏状态[点到面]:(isInside_old => false:外面、true:里面)</param>
/// <param name="status">最近(缓存)围栏状态[数据\Redis]:(false:在里面、false:在外面)</param>
/// <param name="isRecord">是否第一次触发围栏状态(进入)</param>
/// <returns></returns>
public async Task DataSaveAsync(BaseModel model, DeviceFenceList fence, bool isInside, bool isInside_old, bool status, bool isRecord)
{
lock (_GeofenceDataLock)
{
var loc = model.data;
string key = loc.imei + "_" + fence.geofenceId;
DateTime UtcDate = DateTime.Parse(loc.UtcDate);
bool? saveStatus = null;
bool isExt = false; // 是否保存警告数据,并推送进/出围栏报警
int isReuslt = 0; //最终进出结果(3=进,4=出)
string deviceName = "";//设备昵称
string alarmTypeTilte = ""; //进出描述
string alarmTypeTilteLog = ""; //日志描述
string fenceTypeStr = fence.fenceType == 1 ? "圆形围栏" : "多边形围栏"; //围栏类型描述
_logger.LogInformation($"{fenceTypeStr}:{fence.geofenceName},Id:{fence.geofenceId},当前状态:({isInside}){(isInside ? "在里面" : "在外面")},上次状态:({status}){(status ? "在外面" : "在里面")}");
//进出逻辑判断
#region 进围栏报警
if (isInside && status) // 当前在里面,上次记录为在外面,则触发进围栏报警
{
isReuslt = Convert.ToInt32(AlarmType.Entry);
alarmTypeTilte = Enum.GetName(typeof(AlarmTypeChinese), AlarmType.Entry);
saveStatus = false;
#region 保存警告数据,并推送 进围栏报警
//进围栏是否报警条件
if (fence.alarmType == Convert.ToUInt32(GeofenceType.Entry) || fence.alarmType == Convert.ToUInt32(GeofenceType.Both))
isExt = true;
#endregion
}
#endregion
#region 出围栏报警
else if (!isInside && !status) // 当前在外面,上次记录为在里面,则触发出围栏报警
{
isReuslt = Convert.ToInt32(AlarmType.Exit);
alarmTypeTilte = Enum.GetName(typeof(AlarmTypeChinese), AlarmType.Exit);
saveStatus = true;
#region 保存警告数据,并推送 出围栏报警
//出围栏是否报警条件
if (fence.alarmType == Convert.ToUInt32(GeofenceType.Exit) || fence.alarmType == Convert.ToUInt32(GeofenceType.Both))
isExt = true;
#endregion
}
#endregion
//进出数据处理
if (isReuslt > 0)
{
#region 保存状态,更新缓存
//更新最后状态到数据库
var dfstatus = new GpsGeofenceStatus
{
IsGeofenceStatus = saveStatus.Value,
Serialno = loc.imei,
GeofenceId = fence.geofenceId,
};
if (!isRecord)
{
dfstatus.Id = Guid.NewGuid().ToString();
alarmTypeTilteLog = "第一次" + alarmTypeTilte;
}
else
alarmTypeTilteLog = alarmTypeTilte;
var saveGeofenceStatus = SaveGeofenceStatus(dfstatus);
if (!saveGeofenceStatus.result)
_logger.LogError(saveGeofenceStatus.message);
else
//更新缓存
_redis.SetFenceLastStatus(key, new LastStatusInfo()
{
imei = loc.imei,
fenceId = fence.geofenceId,
fenceInfo = fence.geofenceName,
typeId = isReuslt,
typeInfo = Enum.GetName(typeof(AlarmTypeChinese), isReuslt),
isInside = saveStatus.Value,
address = loc.address,
dt = model.time
}).Wait();
#endregion
if (isExt)
{
#region 保存警告数据,并推送 进/出围栏报警
string GeofenceResult = "";
StringBuilder sb_old = new StringBuilder();
StringBuilder sb = new StringBuilder();
string url = "https://id.ssjlai.com/antpayweb/#/gaode-point-in-ring?";
if (fence.fenceType == 1)
{
foreach (var item in fence.pointList)
{
sb.Append($"新算法【出围栏({isInside})】:{url}center={loc.gaodeLongitude},{loc.gaodeLatitude},{loc.Radius}&polygon={item.longitude},{item.latitude},{fence.geofenceId},{item.radius}");
sb_old.Append($"旧算法【出围栏({isInside_old})】:{url}center={loc.gaodeLongitude},{loc.gaodeLatitude},0&polygon={item.longitude},{item.latitude},{fence.geofenceId},{item.radius}");
}
}
else
{
string pointListStr = "";
foreach (var item in fence.pointList)
{
pointListStr += $"{item.longitude}-{item.latitude},";
}
sb.Append($"新算法【出围栏({isInside})】:{url}center={loc.gaodeLongitude},{loc.gaodeLatitude},{loc.Radius}&polygon={pointListStr.Trim(',')},{fence.geofenceId},0");
sb_old.Append($"旧算法【出围栏({isInside_old})】:{url}center={loc.gaodeLongitude},{loc.gaodeLatitude},0&polygon={pointListStr.Trim(',')},{fence.geofenceId},0");
}
GeofenceResult = sb.ToString() + ";" + sb_old.ToString();
//设备昵称
var deviceInfo = _redis.GetGpsDevice(loc.imei).Result;
if (deviceInfo != null)
deviceName = deviceInfo.deviceName;
//警告保存到数据库
var alarm = new HisGpsAlarm
{
DeviceId = loc.DeviceId,
BaiduLat = loc.baiduLatitude,
BaiduLng = loc.baiduLongitude,
CreateTime = DateTime.Now,
DeviceUtcTime = UtcDate,
GeofenceId = fence.geofenceId,
Glat = loc.gaodeLatitude,
Glng = loc.gaodeLongitude,
MessageId = Guid.NewGuid().ToString("D"),
Olat = loc.originalLatitude,
Olng = loc.originalLongitude,
Serialno = loc.imei,
DeviceName = deviceName,
TypeId = isReuslt,
Address = loc.address,
Remarks = alarmTypeTilte + " " + fence.geofenceName,
GeofenceResult = GeofenceResult
};
_alarmApiClient.Add(alarm);

#region 微信推送(启用)
PushWxTemplate wxModel = new PushWxTemplate
{
deviceId = loc.DeviceId,
imei = loc.imei,
alarmTypeId = alarm.TypeId,
alarmDeviceName = alarm.DeviceName,
alarmRemarks = alarmTypeTilte + " " + fence.geofenceName,
address = loc.address,
};
_serviceMqProcess.ProcessWxAlarm(wxModel, fence.geofenceId, model.time).Wait();
#endregion
#region 第三方推送(停用)
// 进围栏报警
PushXiaoAnPushTemplate statusModel = new PushXiaoAnPushTemplate
{
Address = fence.geofenceName,
DateTime = model.time,
NickName = loc.imei,
Title = alarmTypeTilte + " 报警",
TextContent = alarmTypeTilte + " " + fence.geofenceName,
};
//PushStatusXiaoAn(loc.imei, statusModel);
//_serviceMqProcess.ProcessThirdAlarm(statusModel, MqAlarmType.EnterFenceAlarm, model.time);
// _serviceMqProcess.ProcessThirdAlarm(alarm, MqAlarmType.EnterFenceAlarm, model.time);
#endregion
#region 数据服务(java)部分推送
if (fence.appType == 2)
{
var push = new
{
BaiduLatitude = loc.baiduLatitude,
BaiduLongitude = loc.baiduLongitude,
GaodeLatitude = loc.gaodeLatitude,
GaodeLongitude = loc.gaodeLongitude,
OriginalLatitude = loc.originalLatitude,
OriginalLongitude = loc.originalLongitude,
FenceId = fence.geofenceId,
FenceName = fence.geofenceName,
Address = loc.address,
Info = alarmTypeTilte + " " + fence.geofenceName,
};

var commonInfo = new
{
Data = push,
Imei = loc.imei,
Time = model.time,
AlarmType = isReuslt
};
_serviceMqProcess.ProcessPushService(commonInfo, loc.imei).Wait();
}
#endregion
_logger.LogError($"设备{loc.imei}{alarmTypeTilteLog},loc.UtcDate: {UtcDate.ToString("yyyy-MM-dd HH:mm:ss")},loc.LastUpdate: {model.time},DateTime.Now: {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}");
#endregion
}
}
}
}

//获取最近一次围栏进出状态
public async Task<LastStatusInfo> GetLastGeofenceStatus(string imei, string fenceId, string fenceName)
{
LastStatusInfo model = null;
string key = imei + "_" + fenceId;
var lastStatus = await _redis.GetFenceLastStatus(key);
if (lastStatus != null)
return lastStatus;
else
{
// 数据服务调用
var param = new GeneralParam
{
Filters = new List<QueryFilterCondition>
{
new QueryFilterCondition
{
Key=nameof(GpsGeofenceStatus.GeofenceId),
Value=fenceId,
ValueType=QueryValueTypeEnum.String,
Operator=QueryOperatorEnum.Equal
}
}
};
var gfs = _fenceStatusApiClient.GetFirst(param);
if (gfs != null)
{
model = new LastStatusInfo()
{
imei = gfs.Serialno,
fenceId = gfs.GeofenceId,
fenceInfo = fenceName,
typeId = gfs.IsGeofenceStatus ? Convert.ToInt32(AlarmType.Exit) : Convert.ToInt32(AlarmType.Entry),
typeInfo = gfs.IsGeofenceStatus ? Enum.GetName(typeof(AlarmTypeChinese), AlarmType.Exit) : Enum.GetName(typeof(AlarmTypeChinese), AlarmType.Entry),
isInside = gfs.IsGeofenceStatus,
address = "",
dt = gfs.CreateTime.Value.ToString("yyyy-MM-dd HH:mm:ss")
};
await _redis.SetFenceLastStatus(key, model);
}
}
return model;
}

//保存围栏状态
public (bool result, string message) SaveGeofenceStatus(GpsGeofenceStatus gfs)
{
lock (_GeofenceStatusLock)
{
bool result = false;
string message = String.Empty;
try
{
var param = new GeneralParam
{
Filters = new List<QueryFilterCondition>
{
new QueryFilterCondition
{
Key=nameof(GpsGeofenceStatus.GeofenceId),
Value=gfs.GeofenceId,
ValueType=QueryValueTypeEnum.String,
Operator=QueryOperatorEnum.Equal
}
}
};

List<GpsGeofenceStatus> list = _fenceStatusApiClient.GetList(param).ToList();
if (list.Count > 0)
{
if (list.Count == 1)
{
var gfStatus = list.FirstOrDefault();
gfStatus.IsGeofenceStatus = gfs.IsGeofenceStatus;
gfStatus.CreateTime = DateTime.Now;
_fenceStatusApiClient.Update(gfStatus);
}
else
{
//只保留一条记录,多余的删除(历史遗留数据)
int loop = 0;
foreach (var item in list)
{
loop++;
if (loop == 1)
{
var gfStatus = list.FirstOrDefault();
gfStatus.IsGeofenceStatus = gfs.IsGeofenceStatus;
gfStatus.CreateTime = DateTime.Now;
_fenceStatusApiClient.Update(gfStatus);
}
else
{
_fenceStatusApiClient.Delete(item);
}
}
}
}
else
{
gfs.CreateTime = DateTime.Now;
_fenceStatusApiClient.Add(gfs);
_logger.LogInformation($"增加围栏状态:<{JsonConvert.SerializeObject(gfs)}>");
}

#region 旧代码
// GeofenceStatus gfStatus = client.GetFirst(param);
//if (gfStatus != null)
//{
// gfStatus.IsGeofenceStatus = gfs.IsGeofenceStatus;
// gfStatus.CreateTime = DateTime.Now;
// client.Update(gfStatus);

// //只保留一条记录,多余的删除(历史遗留数据)
// List<GeofenceStatus> list = client.GetList(param).ToList();
// if (list.Count > 1)
// {
// foreach (var item in list)
// {
// if (gfStatus.Id != item.Id)
// {
// client.Delete(item);
// }
// }
// }

// logger.LogInformation($"修改围栏状态:<{JsonConvert.SerializeObject(gfStatus)}>");
//}
//else
//{
// gfs.CreateTime = DateTime.Now;
// client.Add(gfs);
// logger.LogInformation($"增加围栏状态:<{JsonConvert.SerializeObject(gfs)}>");
//}
#endregion
message = $"保存围栏状态发生成功!";
result = true;
}
catch (Exception ex)
{
message = $"保存围栏状态发生异常:{ex.Message},{ex.StackTrace}!";
}
return (result, message);
}
}
}
}

+ 59
- 0
TelpoPush.Fence.Worker/Handlers/KafkaSubscribe.cs Целия файл

@@ -0,0 +1,59 @@
using Confluent.Kafka;
using TelpoPush.Fence.Worker.Common;
using TelpoPush.Fence.Worker.Service.Mq;

namespace TelpoPush.Fence.Worker.Handlers
{
public class KafkaSubscribe
{
private readonly ILogger<KafkaSubscribe> _logger;
private readonly IHostEnvironment _env;
private readonly IKafkaService _kafkaService;
private readonly FenceProcess _feneProcess;


public KafkaSubscribe(
ILogger<KafkaSubscribe> logger, IHostEnvironment env,
IKafkaService kafkaService,
FenceProcess feneProcess)
{
_logger = logger;
_env = env;
_kafkaService = kafkaService;
_feneProcess = feneProcess;
}
public async Task SubscribeAsync()
{
#if DEBUG

_logger.LogInformation("11312");


//string message = "{\"messageId\":\"1798637996003269632\",\"topic\":\"topic.push.position\",\"time\":\"2024-06-06 16:43:21\",\"data\":{\"deviceId\":\"f9f395b1-a6c3-4919-bf24-5ed963d79fad\",\"imei\":\"861281060086083\",\"wifiInfo\":\"88:86:03:df:a3:6c,-59,|88:86:03:e0:09:64,-63,|e8:84:c6:ef:cc:14,-68,|34:da:b7:69:d5:00,-68,|88:86:03:e0:09:4c,-68,|88:86:03:e0:0a:5c,-74,|88:86:03:df:b7:54,-83,|88:86:03:df:dc:18,-87,\",\"address\":\"通钦街道合作市第三小学\",\"baiduLatitude\":34.9966307394798,\"baiduLongitude\":102.918411172862,\"gaodeLatitude\":34.9908855355319,\"gaodeLongitude\":102.911835939226,\"originalLatitude\":34.9920144084504,\"originalLongitude\":102.910114012956,\"locationType\":3,\"lastUpdate\":\"2024-06-06 16:43:21\",\"utcDate\":\"2024-06-06 08:43:21\",\"radius\":50}}";
//await _feneProcess.SendFence(message);

//// await _kafkaService.SubscribeAsync(DoReceive, CancellationToken.None);

#else
LimitedConcurrencyLevelTaskScheduler lcts = new LimitedConcurrencyLevelTaskScheduler(5);
TaskFactory factory = new TaskFactory(lcts);
try
{
await factory.StartNew(async () =>
{
await _kafkaService.SubscribeAsync(DoReceive, CancellationToken.None);
});
}
catch (Exception ex)
{
_logger.LogError($"Subscribe 处理Kafka数据发生异常 {ex.Message}|{ex.Source}|{ex.StackTrace}");
}
#endif
}
async void DoReceive(string message)
{
await _feneProcess.SendFence(message);
}
}
}

+ 73
- 0
TelpoPush.Fence.Worker/Models/CacheTemplates/DeviceFenceModel.cs Целия файл

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Models.CacheTemplates
{
public class DeviceFenceModel
{
public string imei { get; set; }
public string deviceId { get; set; }
public int count { get; set; }
public List<DeviceFenceList> fenceList { get; set; }
}


public class DeviceFenceList
{
public string geofenceId { get; set; }
public string geofenceName { get; set; }
public List<string> WifiInfo { get; set; }
public int fenceType { get; set; }
public int alarmType { get; set; }
public string address { get; set; }
public List<pointList> pointList { get; set; }
public bool isActive { get; set; }
public int appType { get; set; }
}

public class DeviceFenceModels
{
public string imei { get; set; }
public string deviceId { get; set; }
public string geofenceId { get; set; }
public string geofenceName { get; set; }
public int fenceType { get; set; }
public int alarmType { get; set; }
public int radius { get; set; }
public decimal latitude { get; set; }
public decimal longitude { get; set; }
public string address { get; set; }
public bool isActive { get; set; }
public int appType { get; set; }
}

public class pointList
{
public int index { get; set; }
public int radius { get; set; }
public decimal latitude { get; set; }
public decimal longitude { get; set; }
}


public class DeviceTime
{
public string imei { get; set; }
public DateTime dt { get; set; }
}

public class LastStatusInfo
{
public string imei { get; set; }
public string fenceId { get; set; }
public string fenceInfo { get; set; }
public int typeId { get; set; }
public bool isInside { get; set; }
public string typeInfo { get; set; }
public string address { get; set; }
public string dt { get; set; }
}
}

+ 19
- 0
TelpoPush.Fence.Worker/Models/CacheTemplates/DeviceInfoModel.cs Целия файл

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Models.CacheTemplates
{
public class DeviceInfoModel
{
public string deviceId { get; set; }
public string imei { get; set; }
public string deviceName { get; set; }
public string orgId { get; set; }
public string apiUid { get; set; }
public string activeStatus { get; set; }
public string activeTime { get; set; }
}
}

+ 15
- 0
TelpoPush.Fence.Worker/Models/Config/FenceConfig.cs Целия файл

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Models.Config
{
public class FenceConfig
{
public string XiaoAnManufactorId { get; set; }
public string XiaoAnPushUrl { get; set; }

}
}

+ 94
- 0
TelpoPush.Fence.Worker/Models/Config/RedisConfig.cs Целия файл

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Models.Config
{
/// <summary>
/// Redis配置模板类
/// </summary>
public class RedisConfig
{
public string Server { get; set; }
/// <summary>
/// Redis server password
/// </summary>
public string Password { get; set; }
/// <summary>
/// Redis server database, default 0
/// </summary>
public int? DefaultDatabase { get; set; }
/// <summary>
/// The asynchronous method automatically uses pipeline, and the 10W concurrent time is 450ms(welcome to feedback)
/// </summary>
public bool? AsyncPipeline { get; set; }
/// <summary>
/// Connection pool size, default 50
/// </summary>
public int? Poolsize { get; set; }
/// <summary>
/// Idle time of elements in the connection pool(MS), suitable for connecting to remote redis server, default 20000
/// </summary>
public int? IdleTimeout { get; set; }
/// <summary>
/// Connection timeout(MS), default 5000
/// </summary>
public int? ConnectTimeout { get; set; }
/// <summary>
/// Send / receive timeout(MS), default 10000
/// </summary>
public int? SyncTimeout { get; set; }
/// <summary>
/// Preheat connections, receive values such as preheat = 5 preheat 5 connections, default 5
/// </summary>
public int? Preheat { get; set; }
/// <summary>
/// Follow system exit event to release automatically, default true
/// </summary>
public bool? AutoDispose { get; set; }
/// <summary>
/// Enable encrypted transmission, default false
/// </summary>
public bool? Ssl { get; set; }
/// <summary>
/// 是否尝试集群模式,阿里云、腾讯云集群需要设置此选项为 false, default true
/// </summary>
public bool? Testcluster { get; set; }
/// <summary>
/// Execution error, retry attempts, default 0
/// </summary>
public int? Tryit { get; set; }
/// <summary>
/// Connection name, use client list command to view
/// </summary>
public string Name { get; set; }
/// <summary>
/// key前辍,所有方法都会附带此前辍,csredis.Set(prefix + "key", 111)
/// </summary>
public string Prefix { get; set; }

public override string ToString()
{
if (string.IsNullOrWhiteSpace(Server)) throw new ArgumentNullException(nameof(Server));
var sb = new StringBuilder(Server);
if (!string.IsNullOrWhiteSpace(Password)) sb.Append($",password={Password}");
if (DefaultDatabase.HasValue) sb.Append($",defaultDatabase={DefaultDatabase}");
if (AsyncPipeline.HasValue) sb.Append($",asyncPipeline={AsyncPipeline}");
if (Poolsize.HasValue) sb.Append($",poolsize={Poolsize}");
if (IdleTimeout.HasValue) sb.Append($",idleTimeout={IdleTimeout}");
if (ConnectTimeout.HasValue) sb.Append($",connectTimeout={ConnectTimeout}");
if (SyncTimeout.HasValue) sb.Append($",syncTimeout={0}");
if (Preheat.HasValue) sb.Append($",preheat={Preheat}");
if (AutoDispose.HasValue) sb.Append($",autoDispose={AutoDispose}");
if (Ssl.HasValue) sb.Append($",ssl={Ssl}");
if (Testcluster.HasValue) sb.Append($",testcluster={Testcluster}");
if (Tryit.HasValue) sb.Append($",tryit={Tryit}");
if (!string.IsNullOrWhiteSpace(Name)) sb.Append($",name={Name}");
if (!string.IsNullOrWhiteSpace(Prefix)) sb.Append($",prefix={Prefix}");

return sb.ToString();
}
}
}

+ 25
- 0
TelpoPush.Fence.Worker/Models/Config/ServiceConfig.cs Целия файл

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Models.Config
{
public class ServiceConfig
{
/// <summary>
/// 数据服务Host Url
/// </summary>
public string TelpoDataUrl { get; set; }

/// <summary>
/// Kafka服务地址
/// </summary>
public string KafkaBootstrapServers { get; set; }
public List<string> KafkaTopics { get; set; }
public string KafkaGroupId { get; set; }
public int MaxDegreeOfParallelism { get; set; }
public int CacheDurationSeconds { get; set; }
}
}

+ 34
- 0
TelpoPush.Fence.Worker/Models/Enum/AlarmType.cs Целия файл

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Models.Enum
{
/// <summary>
/// 报警类型
/// </summary>
public enum AlarmType
{
Online = 0,
Offline = 9,
SOS = 1,//求救
LowBattery = 2,//电量
Entry = 3, //进围栏
Exit = 4, //出围栏
PowerOff = 5//关机报警
}
/// <summary>
/// </summary>
public enum AlarmTypeChinese
{
上线 = 0,
离线 = 9,
SOS求救 = 1,//求救
低电报警 = 2,//电量
进入围栏 = 3, //进围栏
离开围栏 = 4, //出围栏
关机报警 = 5//关机报警
}
}

+ 18
- 0
TelpoPush.Fence.Worker/Models/Enum/GeofenceType.cs Целия файл

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Models.Enum
{
/// <summary>
/// 围栏类型
/// </summary>
public enum GeofenceType
{
Entry = 1,
Both = 2,
Exit = 3
}
}

+ 16
- 0
TelpoPush.Fence.Worker/Models/Enum/HeadersDto.cs Целия файл

@@ -0,0 +1,16 @@
using Newtonsoft.Json;
namespace TelpoPush.Fence.Worker.Models.Enum
{
/// <summary>
/// 消息数据头
/// </summary>
public class HeadersDto
{
[JsonProperty(PropertyName = "DataType")]
public int? DataType { get; set; }
[JsonProperty(PropertyName = "AlarmType")]
public int? AlarmType { get; set; }
[JsonProperty(PropertyName = "OperType")]
public int? OperType { get; set; }
}
}

+ 19
- 0
TelpoPush.Fence.Worker/Models/Enum/LocationType.cs Целия файл

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Models.Enum
{
/// <summary>
/// 定位类型
/// </summary>
public enum LocationType
{
GPS = 1,
LBS = 2,
WIFI = 3,
WifiPlus = 4
}
}

+ 19
- 0
TelpoPush.Fence.Worker/Models/Enum/MqDataTopic.cs Целия файл

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Models.Enum
{

public enum MqDataTopic : int
{
/// <summary>
/// 中高实时心率
/// </summary>
ZkRealHRMonitorTopic = 1


}
}

+ 156
- 0
TelpoPush.Fence.Worker/Models/Enum/MqDataType.cs Целия файл

@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Models.Enum
{
/// <summary>
/// 数据类型,标识发布到kafka的消息的数据类型
/// </summary>
public enum MqDataType : int
{
/// <summary>
/// 报警消息
/// </summary>
AlarmInfo = 0,

/// <summary>
/// 温度数据信息
/// </summary>
TemperatureInfo = 1,

/// <summary>
/// 步数信息
/// </summary>
StepInfo = 2,

/// <summary>
/// 电量信息
/// </summary>
BatteryLevelInfo = 3,

/// <summary>
/// 设备配置信息
/// </summary>
DeviceConfigInfo = 4,

/// <summary>
/// 设备通话记录
/// </summary>
DeviceCallLog = 5,

/// <summary>
/// 设备短信记录
/// </summary>
DeviceSmsLog = 6,

/// <summary>
/// 位置信息
/// </summary>
PositionInfo = 7,

/// <summary>
/// 支付
/// </summary>
PayInfo = 8,

/// <summary>
/// 设备状态(offline,online)
/// </summary>
Status = 9,

/// <summary>
/// 设备激活状态(激活1,未激活0)
/// </summary>
Active = 10,

/// <summary>
/// 指令回调
/// </summary>
reply = 11,

/// <summary>
/// 天气查询
/// </summary>
Weather = 12,

/// <summary>
/// 短信阅读事件
/// </summary>
ReadMsg = 13,
/// <summary>
/// 学习能力状态上报事件
/// </summary>
StudyAINotifyStatusUpload = 14,
/// <summary>
/// 心率
/// </summary>
HeartRateInfo = 15,
/// <summary>
/// 血氧
/// </summary>
Spo2Info = 16,
/// <summary>
/// 周期性报体温数据。
/// </summary>
Temperature1Info = 17,
/// <summary>
/// 周期心率。
/// </summary>
HeartRate1Info = 18,
/// <summary>
/// 周期性血氧
/// </summary>
Spo21Info = 19,
/// <summary>
/// 溺水状态
/// </summary>
DrownReportInfo = 20,
/// <summary>
/// 手表佩戴状态
/// </summary>
WearStatusInfo = 21,
/// <summary>
/// 血压
/// </summary>
BloodPressInfo = 22,
/// <summary>
/// 周期性血压
/// </summary>
BloodPress1Info = 23,
/// <summary>
/// 心理监测
/// </summary>
PsychInfo = 24,

/// <summary>
/// AI呼叫回调结果
/// </summary>
AiCallResult = 25,

/// <summary>
/// 越界上报(围栏进出告警)
/// </summary>
CrossBorder = 26,

/// <summary>
/// 运动数据上报
/// </summary>
SportResult = 27,
/// <summary>
/// 运动数据上报
/// </summary>
BloodSugar = 28,

/// <summary>
/// 中考实时心率数据上报
/// </summary>
ZkRealHeartRate = 29,
/// <summary>
/// 绑定业务
/// </summary>
BindDevice = 100
}
}

+ 26
- 0
TelpoPush.Fence.Worker/Models/Enum/MqHeader.cs Целия файл

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Models.Enum
{
public static class MqHeader
{
/// <summary>
/// DataType
/// </summary>
public const string DataType = "DataType";

/// <summary>
/// OperType
/// </summary>
public const string OperType = "OperType";

/// <summary>
/// AlarmType
/// </summary>
public const string AlarmTypes = "AlarmType";
}
}

+ 94
- 0
TelpoPush.Fence.Worker/Models/Fence/Algorithms/CirclePolygonAlgorithm.cs Целия файл

@@ -0,0 +1,94 @@
namespace TelpoPush.Fence.Worker.Models.Fence
{
public static class CirclePolygonAlgorithm
{
/// <summary>
/// 判断圆和多边形是否相交
/// </summary>
/// <param name="circle">圆心坐标</param>
/// <param name="radius">圆半径</param>
/// <param name="polygon">多边形各个点坐标</param>
/// <returns></returns>
public static bool IsIntersect(GpsFencePoint circle, double degree, IList<GpsFencePoint> polygon)
{
double powRadius = Math.Pow(degree, 2);

//情况1:多边形任一顶点在圆内,则认为相交
foreach (var pt in polygon)
{
if (Math.Pow(circle.Longitude - pt.Longitude, 2) + Math.Pow(circle.Latitude - pt.Latitude, 2) < powRadius) return true;
}

//情况2:多边形顶点集的任一有效线段和圆相交(圆心到线段的距离小于半径)
var lines = EnumeratePolygonLines(polygon);
foreach (var seg in lines)
{
if (PointToLine(circle, seg.Item1, seg.Item2) < degree) return true;
}

return false;
}

private static IList<Tuple<GpsFencePoint, GpsFencePoint>> EnumeratePolygonLines(IList<GpsFencePoint> polygon)
{
var list = new List<Tuple<GpsFencePoint, GpsFencePoint>>();

for (int i = 0; i < polygon.Count - 1; i++)
{
list.Add(new Tuple<GpsFencePoint, GpsFencePoint>(polygon[i], polygon[i + 1]));
}
list.Add(new Tuple<GpsFencePoint, GpsFencePoint>(polygon[polygon.Count - 1], polygon[0]));

return list;
}

//计算两点之间的距离
private static double LineSpace(GpsFencePoint ptStart, GpsFencePoint ptEnd)
{
if (ptStart == ptEnd) return 0;
return Math.Sqrt((ptEnd.Longitude - ptStart.Longitude) * (ptEnd.Longitude - ptStart.Longitude) + (ptEnd.Latitude - ptStart.Latitude) * (ptEnd.Latitude - ptStart.Latitude));
}

/// <summary>
/// 点到线段的距离
/// </summary>
/// <param name="pt"></param>
/// <param name="ptStart">线段顶点1</param>
/// <param name="ptEnd">线段顶点2</param>
/// <returns></returns>
private static double PointToLine(GpsFencePoint pt, GpsFencePoint ptStart, GpsFencePoint ptEnd)
{
double distance = 0;
double a, b, c;
a = LineSpace(ptStart, ptEnd); //线段长度
b = LineSpace(pt, ptStart); //点到线段顶点1的距离
c = LineSpace(pt, ptEnd); //点到线段顶点2的距离
if (b == 0 || c == 0)
{
distance = 0;
return distance;
}
if (a == 0)
{
distance = b;
return distance;
}
if (c * c >= a * a + b * b)
{
distance = b;
return distance;
}
if (b * b >= a * a + c * c)
{
distance = c;
return distance;
}

double p = (a + b + c) / 2; //半周长
double s = Math.Sqrt(p * (p - a) * (p - b) * (p - c)); // 海伦公式求面积
distance = 2 * s / a;// 返回点到线的距离(利用三角形面积公式求高)

return distance;
}
}
}

+ 36
- 0
TelpoPush.Fence.Worker/Models/Fence/Algorithms/RayCastingAlgorithm.cs Целия файл

@@ -0,0 +1,36 @@
namespace TelpoPush.Fence.Worker.Models.Fence
{
public static class RayCastingAlgorithm
{
public static bool IsWithin(GpsFencePoint pt, IList<GpsFencePoint> polygon, bool noneZeroMode)
{
int ptNum = polygon.Count();
if (ptNum < 3)
{
return false;
}
int j = ptNum - 1;
bool oddNodes = false;
int zeroState = 0;
for (int k = 0; k < ptNum; k++)
{
var ptK = polygon[k];
var ptJ = polygon[j];
if (ptK.Latitude > pt.Latitude != ptJ.Latitude > pt.Latitude && pt.Longitude < (ptJ.Longitude - ptK.Longitude) * (pt.Latitude - ptK.Latitude) / (ptJ.Latitude - ptK.Latitude) + ptK.Longitude)
{
oddNodes = !oddNodes;
if (ptK.Latitude > ptJ.Latitude)
{
zeroState++;
}
else
{
zeroState--;
}
}
j = k;
}
return noneZeroMode ? zeroState != 0 : oddNodes;
}
}
}

+ 81
- 0
TelpoPush.Fence.Worker/Models/Fence/ChinaPolygon.cs
Файловите разлики са ограничени, защото са твърде много
Целия файл


+ 18
- 0
TelpoPush.Fence.Worker/Models/Fence/Circle.cs Целия файл

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace TelpoPush.Fence.Worker.Models.Fence
{
public class Circle
{
public GpsPoint Center { get; set; }
public decimal Radius { get; set; }

public Circle(GpsPoint center, decimal radius)
{
Center = center;
Radius = radius;
}
}
}

+ 199
- 0
TelpoPush.Fence.Worker/Models/Fence/GeoConvert.cs Целия файл

@@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace TelpoPush.Fence.Worker.Models.Fence
{
public class GeoConvert
{
const double Pi = 3.14159265358979324;
const double A = 6378245.0;
const double Ee = 0.00669342162296594323;
const double XPi = 3.14159265358979324 * 3000.0 / 180.0;
//
/// <summary>
/// 地球坐标系 ===> 火星坐标系统 (gps === > google)
/// </summary>
/// <param name="wgLat"></param>
/// <param name="wgLon"></param>
/// <param name="mgLat"></param>
/// <param name="mgLon"></param>
public static void Transform(double wgLat, double wgLon, out double mgLat, out double mgLon)
{
if (OutOfChina(wgLat, wgLon))
{
mgLat = wgLat;
mgLon = wgLon;
return;
}
var dLat = TransformLat(wgLon - 105.0, wgLat - 35.0);
var dLon = TransformLon(wgLon - 105.0, wgLat - 35.0);
var radLat = wgLat / 180.0 * Pi;
var magic = Math.Sin(radLat);
magic = 1 - Ee * magic * magic;
var sqrtMagic = Math.Sqrt(magic);
dLat = (dLat * 180.0) / ((A * (1 - Ee)) / (magic * sqrtMagic) * Pi);
dLon = (dLon * 180.0) / (A / sqrtMagic * Math.Cos(radLat) * Pi);
mgLat = wgLat + dLat;
mgLon = wgLon + dLon;
}

///// <summary>
///// 算法纠偏,无缓存
///// 0:高德纬度
///// 1:高德经度
///// 2:百度纬度
///// 3:百度经度
///// </summary>
///// <param name="lat"></param>
///// <param name="lng"></param>
///// <returns></returns>
//public static decimal[] ConvertGoogleBaiduLatLng(decimal lat, decimal lng)
//{
// //中国经度范围:73°33′E至135°05′E 纬度范围:3°51′N至53°33′N
// var LatLng = new decimal[] { lat, lng, lat, lng };
// if (lat > 4 && lat < 54 && lng > 73 && lng < 136)
// {
// try
// {
// double gLat = 0, gLng = 0, bLat = 0, bLng = 0;
// Transform(SafeType.SafeDouble(lat), SafeType.SafeDouble(lng), out gLat, out gLng);
// G2B(gLat, gLng, out bLat, out bLng);
// LatLng[0] = Convert.ToDecimal(gLat);
// LatLng[1] = Convert.ToDecimal(gLng);
// LatLng[2] = Convert.ToDecimal(bLat);
// LatLng[3] = Convert.ToDecimal(bLng);
// }
// catch (Exception ex)
// {
// LogHelper.WriteLog($"ConvertGoogleBaiduLatLng[异常] {ex.Message}");
// }
// }
// return LatLng;
//}


/// <summary>
/// google to gps
/// </summary>
/// <param name="lat"></param>
/// <param name="lng"></param>
/// <param name="olat"></param>
/// <param name="olng"></param>
public static void G2Gps(double lat, double lng, out double olat, out double olng)
{
if (OutOfChina(lat, lng))
{
olat = lat;
olng = lng;
return;
}

var dLat = TransformLat(lng - 105.0, lat - 35.0);
var dLng = TransformLon(lng - 105.0, lat - 35.0);
var radLat = lat / 180.0 * Math.PI;
var magic = Math.Sin(radLat);
magic = 1 - Ee * magic * magic;
var sqrtMagic = Math.Sqrt(magic);
dLat = (dLat * 180.0) / ((A * (1 - Ee)) / (magic * sqrtMagic) * Math.PI);
dLng = (dLng * 180.0) / (A / sqrtMagic * Math.Cos(radLat) * Math.PI);
olat = lat - dLat;
olng = lng - dLng;
}

/// <summary>
/// 判断是否在中国
/// </summary>
/// <param name="lat"></param>
/// <param name="lon"></param>
/// <returns></returns>
static bool OutOfChina(double lat, double lon)
{
return !ChinaPolygon.IsInChina(lat, lon);
}

static double TransformLat(double x, double y)
{
var ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.Sqrt(Math.Abs(x));
ret += (20.0 * Math.Sin(6.0 * x * Pi) + 20.0 * Math.Sin(2.0 * x * Pi)) * 2.0 / 3.0;
ret += (20.0 * Math.Sin(y * Pi) + 40.0 * Math.Sin(y / 3.0 * Pi)) * 2.0 / 3.0;
ret += (160.0 * Math.Sin(y / 12.0 * Pi) + 320 * Math.Sin(y * Pi / 30.0)) * 2.0 / 3.0;
return ret;
}

static double TransformLon(double x, double y)
{
var ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.Sqrt(Math.Abs(x));
ret += (20.0 * Math.Sin(6.0 * x * Pi) + 20.0 * Math.Sin(2.0 * x * Pi)) * 2.0 / 3.0;
ret += (20.0 * Math.Sin(x * Pi) + 40.0 * Math.Sin(x / 3.0 * Pi)) * 2.0 / 3.0;
ret += (150.0 * Math.Sin(x / 12.0 * Pi) + 300.0 * Math.Sin(x / 30.0 * Pi)) * 2.0 / 3.0;
return ret;
}

/// <summary>
/// Google to Baidu
/// </summary>
/// <param name="glat"></param>
/// <param name="glng"></param>
/// <param name="blat"></param>
/// <param name="blng"></param>
public static void G2B(double glat, double glng, out double blat, out double blng)
{
var x = glng;
var y = glat;
var z = Math.Sqrt(x * x + y * y) + 0.00002 * Math.Sin(y * XPi);
var theta = Math.Atan2(y, x) + 0.000003 * Math.Cos(x * XPi);
blng = z * Math.Cos(theta) + 0.0065;
blat = z * Math.Sin(theta) + 0.006;
}

/// <summary>
/// Baidu to Google
/// </summary>
/// <param name="blat"></param>
/// <param name="blng"></param>
/// <param name="glat"></param>
/// <param name="glng"></param>
public static void B2G(double blat, double blng, out double glat, out double glng)
{
var x = blng - 0.0065;
var y = blat - 0.006;
var z = Math.Sqrt(x * x + y * y) - 0.00002 * Math.Sin(y * XPi);
var theta = Math.Atan2(y, x) - 0.000003 * Math.Cos(x * XPi);
glng = z * Math.Cos(theta);
glat = z * Math.Sin(theta);
}
/// <summary>
/// 百度转原始经纬度
/// </summary>
/// <param name="blat"></param>
/// <param name="blng"></param>
/// <param name="olat"></param>
/// <param name="olng"></param>
public static void B2Gps(double blat, double blng, out double olat, out double olng)
{
if (OutOfChina(blat, blng))
{
olat = blat;
olng = blng;
return;
}
double glat, glng;
B2G(blat, blng, out glat, out glng);
G2Gps(glat, glng, out olat, out olng);
}
/// <summary>
/// 原始转百度
/// </summary>
/// <param name="olat"></param>
/// <param name="olng"></param>
/// <param name="blat"></param>
/// <param name="blng"></param>
public static void Gps2Bd(double olat, double olng, out double blat, out double blng)
{
double glat, glng;
Transform(olat, olng, out glat, out glng);
G2B(glat, glng, out blat, out blng);
}
}
}

+ 178
- 0
TelpoPush.Fence.Worker/Models/Fence/GeoUtils.cs Целия файл

@@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.Text;


namespace TelpoPush.Fence.Worker.Models.Fence
{
public class Bounds
{
public GpsPoint SW { get; set; }
public GpsPoint NE { get; set; }

public Bounds(GpsPoint sw, GpsPoint ne)
{
SW = sw;
NE = ne;
}

public GpsPoint GetCenter()
{
return new GpsPoint((SW.Lng + NE.Lng) / 2, (SW.Lat + NE.Lat) / 2);
}
}
public class GeoUtils
{
private const decimal A = 6370996.81m;

private const double EARTH_RADIUS = 6378137;//赤道半径(单位m)

/// <summary>
/// 点是否在矩形区域中
/// </summary>
/// <param name="pt"></param>
/// <param name="rect"></param>
/// <returns></returns>
public static bool IsPointInRect(GpsPoint pt, Bounds rect)
{
var e = rect.SW;
var h = rect.NE;
return (pt.Lng >= e.Lng && pt.Lng <= h.Lng && pt.Lat >= e.Lat && pt.Lat <= h.Lat);
}
/// <summary>
/// 点是否在圆形区域中
/// </summary>
/// <param name="pt"></param>
/// <param name="circle"></param>
/// <returns></returns>
public static bool IsPointInCircle(GpsPoint pt, Circle circle)
{
var i = circle.Center;
var g = circle.Radius;
var f = GetDistance(pt, i);
return f <= g;
}

/// <summary>
/// 圆和圆是否有交点
/// </summary>
/// <param name="pt"></param>
/// <param name="circle"></param>
/// <returns></returns>
public static bool IsCircle2Circle(Circle _circle, Circle circle)
{
var _r = _circle.Radius;
var r = circle.Radius;
var f = GetDistance(_circle.Center, circle.Center);
//判断两圆是否相交
if (f > _r + r)
{
return false;
}
return true;
}

/// <summary>
/// 获取形状的矩形范围
/// </summary>
/// <param name="pts"></param>
/// <returns></returns>
public static Bounds GetBound(IList<GpsPoint> pts)
{
if (pts.Count == 0)
return new Bounds(new GpsPoint(0, 0), new GpsPoint(0, 0)); ;
decimal minLat = pts[0].Lat, minLng = pts[0].Lng, maxLat = pts[0].Lat, maxLng = pts[0].Lng;
foreach (var pt in pts)
{
minLat = Math.Min(minLat, pt.Lat);
minLng = Math.Min(minLng, pt.Lng);
maxLat = Math.Max(maxLat, pt.Lat);
maxLng = Math.Max(maxLng, pt.Lng);
}

return new Bounds(new GpsPoint(minLng, minLat), new GpsPoint(maxLng, maxLat));
}



/// <summary>
/// 经纬度测距算法
/// </summary>
/// <param name="lon1"></param>
/// <param name="lat1"></param>
/// <param name="lon2"></param>
/// <param name="lat2"></param>
/// <returns>单位 米</returns>
public static double GetDistance2(double lon1, double lat1, double lon2, double lat2)
{
double radLat1 = rad(lat1);
double radLat2 = rad(lat2);
double a = radLat1 - radLat2;
double b = rad(lon1) - rad(lon2);
double s = 2 * Math.Asin(Math.Sqrt(Math.Pow(Math.Sin(a / 2), 2) + Math.Cos(radLat1) * Math.Cos(radLat2) * Math.Pow(Math.Sin(b / 2), 2)));
s = s * EARTH_RADIUS;
return s;
}

/// <summary>
/// 转化为弧度(rad)
/// </summary>
/// <param name="d">经度或纬度</param>
/// <returns>弧度</returns>
private static double rad(double d)
{
return d * Math.PI / 180.0;
}


/// <summary>
/// 计算两点的距离 单位:米
/// </summary>
/// <param name="j"></param>
/// <param name="h"></param>
/// <returns></returns>
public static decimal GetDistance(GpsPoint j, GpsPoint h)
{
j.Lng = ValidLng(j.Lng, -180, 180);
j.Lat = ValidLat(j.Lat, -74, 74);
h.Lng = ValidLng(h.Lng, -180, 180);
h.Lat = ValidLat(h.Lat, -74, 74);
decimal f, e, i, g;
f = DegreeToRad(j.Lng);
i = DegreeToRad(j.Lat);
e = DegreeToRad(h.Lng);
g = DegreeToRad(h.Lat);
return Convert.ToDecimal((double)A * Math.Acos((Math.Sin((double)i) * Math.Sin((double)g) + Math.Cos((double)i) * Math.Cos((double)g) * Math.Cos((double)e - (double)f))));
}

private static decimal DegreeToRad(decimal d)
{
return Convert.ToDecimal(Math.PI * (double)d / 180);
}

private static decimal RadToDegree(decimal d)
{
return Convert.ToDecimal((180 * (double)d) / Math.PI);
}

private static decimal ValidLng(decimal lng, decimal min, decimal max)
{
while (lng > max)
{
lng -= max - min;
}
while (lng < min)
{
lng += max - min;
}
return lng;
}

private static decimal ValidLat(decimal lat, decimal min, decimal max)
{
lat = Math.Max(lat, min);
lat = Math.Min(lat, max);
return lat;
}
}
}

+ 144
- 0
TelpoPush.Fence.Worker/Models/Fence/GeofenceHelper.cs Целия файл

@@ -0,0 +1,144 @@
using System.Drawing;

namespace TelpoPush.Fence.Worker.Models.Fence
{
public static class GeofenceHelper
{

public static double CalcDisgreeFromRadius(int radius)
{
double metersPerSecond = 30.0;
double seconds = radius / metersPerSecond;
return seconds / 3600;
}

/// <summary>
/// 计算点是否在多边形内
/// </summary>
/// <param name="polygon">多边形点集合</param>
/// <param name="pt">要判断的点</param>
/// <returns>是否在多边形内</returns>
public static bool IsPolygonContainsPoint(IList<GpsFencePoint> polygon, GpsFencePoint pt)
{
return RayCastingAlgorithm.IsWithin(pt, polygon, true);
}

/// <summary>
/// 判断圆是否和多边形相交
/// </summary>
/// <param name="polygon">多边形点集合</param>
/// <param name="ptCenter">要判断的圆的圆心点</param>
/// <param name="radius">圆半径(单位米)</param>
/// <returns></returns>
public static bool IsPolygonIntersectCircle(IList<GpsFencePoint> polygon, GpsFencePoint ptCenter,int radius)
{
var degree = CalcDisgreeFromRadius(radius);
return CirclePolygonAlgorithm.IsIntersect(ptCenter, degree, polygon);
}



//---yl---新增-- 2022.6.13----
/// <summary>
/// 判断圆和多边形是否相交
/// </summary>
/// <param name="circle">圆心坐标</param>
/// <param name="radius">圆半径</param>
/// <param name="polygon">多边形各个点坐标</param>
/// <returns></returns>
public static bool IsCircle2Polygon(Point circle, double radius, IList<Point> polygon)
{
bool res = false;

double powRadius = Math.Pow(radius, 2);

//情况1:多边形任一顶点在圆内,则认为相交
foreach (var pt in polygon)
{
if (Math.Pow(circle.X - pt.X, 2) + Math.Pow(circle.Y - pt.Y, 2) < powRadius)
{
res = true;
return res;
//return true;
}
}

//情况2:多边形顶点集的任一有效线段和圆相交(圆心到线段的距离小于半径)
var lines = EnumeratePolygonLines(polygon);
foreach (var seg in lines)
{
if (PointToLine(circle, seg.Item1, seg.Item2) < radius)
{
res = true;
return res;
//return true;
}
//return true;
}

return res;// false;
}

private static IList<Tuple<Point, Point>> EnumeratePolygonLines(IList<Point> polygon)
{
var list = new List<Tuple<Point, Point>>();

for (int i = 0; i < polygon.Count - 1; i++)
{
list.Add(new Tuple<Point, Point>(polygon[i], polygon[i + 1]));
}
list.Add(new Tuple<Point, Point>(polygon[polygon.Count - 1], polygon[0]));

return list;
}

//计算两点之间的距离
private static double LineSpace(Point ptStarts, Point ptEnds)
{
if (ptStarts == ptEnds) return 0;
return Math.Sqrt((ptEnds.X - ptStarts.X) * (ptEnds.X - ptStarts.X) + (ptEnds.Y - ptStarts.Y) * (ptEnds.Y - ptStarts.Y));
}

/// <summary>
/// 点到线段的距离
/// </summary>
/// <param name="pt"></param>
/// <param name="ptStart">线段顶点1</param>
/// <param name="ptEnd">线段顶点2</param>
/// <returns></returns>
private static double PointToLine(Point pt, Point ptStart, Point ptEnd)
{
double distance = 0;
double a, b, c;
a = LineSpace(ptStart, ptEnd); //线段长度
b = LineSpace(pt, ptStart); //点到线段顶点1的距离
c = LineSpace(pt, ptEnd); //点到线段顶点2的距离
if (b == 0 || c == 0)
{
distance = 0;
return distance;
}
if (a == 0)
{
distance = b;
return distance;
}
if (c * c >= a * a + b * b)
{
distance = b;
return distance;
}
if (b * b >= a * a + c * c)
{
distance = c;
return distance;
}

double p = (a + b + c) / 2; //半周长
double s = Math.Sqrt(p * (p - a) * (p - b) * (p - c)); // 海伦公式求面积
distance = 2 * s / a;// 返回点到线的距离(利用三角形面积公式求高)

return distance;
}
}
}

+ 31
- 0
TelpoPush.Fence.Worker/Models/Fence/GpsFencePoint.cs Целия файл

@@ -0,0 +1,31 @@
namespace TelpoPush.Fence.Worker.Models.Fence
{
/// <summary>
/// gps点坐标
/// </summary>
public class GpsFencePoint
{
public GpsFencePoint() { }

/// <summary>
///
/// </summary>
/// <param name="lng">经度</param>
/// <param name="lat">纬度</param>
public GpsFencePoint(double lng,double lat)
{
Longitude = lng;
Latitude = lat;
}

/// <summary>
/// 经度
/// </summary>
public double Longitude { get; set; }

/// <summary>
/// 纬度
/// </summary>
public double Latitude { get; set; }
}
}

+ 23
- 0
TelpoPush.Fence.Worker/Models/Fence/Point.cs Целия файл

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace TelpoPush.Fence.Worker.Models.Fence
{
public class GpsPoint
{
public decimal Lat { get; set; }
public decimal Lng { get; set; }

public GpsPoint(decimal lat, decimal lng)
{
Lat = lat;
Lng = lng;
}

public bool Equals(GpsPoint obj)
{
return Math.Abs(obj.Lat - Lat) < 0.000000001m && Math.Abs(obj.Lng - Lng) < 0.000000001m;
}
}
}

+ 42
- 0
TelpoPush.Fence.Worker/Models/MqTemplates/BaseModel.cs Целия файл

@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Models.MqTemplates
{
public class BaseModel
{

public string messageId { get; set; }
public string topic { get; set; }
public string time { get; set; }
public data data { get; set; }
}

public class data
{
public string DeviceId { get; set; }
public int Radius { get; set; }
public string imei { get; set; }
public string wifiInfo { get; set; }
public int locationType { get; set; }
public string address { get; set; }
public decimal baiduLatitude { get; set; }
public decimal baiduLongitude { get; set; }
public decimal gaodeLatitude { get; set; }
public decimal gaodeLongitude { get; set; }
public decimal originalLatitude { get; set; }
public decimal originalLongitude { get; set; }
public string LastUpdate { get; set; }
public string UtcDate { get; set; }
}

public class wifiInfo
{
public string mmac { get; set; }
public string macs { get; set; }
}

}

+ 18
- 0
TelpoPush.Fence.Worker/Models/PushTemplates/PushWxTemplate.cs Целия файл

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Models.PushTemplates
{
public class PushWxTemplate
{
public string deviceId { get; set; }
public string imei { get; set; }
public int alarmTypeId { get; set; }
public string alarmDeviceName { get; set; }
public string alarmRemarks { get; set; }
public string address { get; set; }
}
}

+ 45
- 0
TelpoPush.Fence.Worker/Models/PushTemplates/PushXiaoAnPushTemplate.cs Целия файл

@@ -0,0 +1,45 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Models.PushTemplates
{
/// <summary>
/// 进出围栏http推送(校安)
/// </summary>
public class PushXiaoAnPushTemplate
{
/// <summary>
/// 推送title
/// </summary>
[JsonProperty("title")]
public string Title { get; set; }

/// <summary>
/// 地址
/// </summary>
[JsonProperty("address")]
public string Address { get; set; }

/// <summary>
/// 推送内容
/// </summary>
[JsonProperty("textcontent")]
public string TextContent { get; set; }

/// <summary>
/// 设备imei号
/// </summary>
[JsonProperty("nickname")]
public string NickName { get; set; }

/// <summary>
/// 时间
/// </summary>
[JsonProperty("datetime")]
public string DateTime { get; set; }
}
}

+ 103
- 0
TelpoPush.Fence.Worker/Program.cs Целия файл

@@ -0,0 +1,103 @@

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Serilog;
using TelpoDataService.Util.Clients;
using TelpoPush.Fence.Worker;
using TelpoPush.Fence.Worker.Common;
using TelpoPush.Fence.Worker.Handlers;
using TelpoPush.Fence.Worker.Models.Config;
using TelpoPush.Fence.Worker.Service.Cache;
using TelpoPush.Fence.Worker.Service.Mq;

try
{
#region 日志
//using Serilog.Events;
//using Serilog;
//using Microsoft.AspNetCore.Builder;

//Log.Logger = new LoggerConfiguration()
//#if DEBUG
// .MinimumLevel.Debug()
//#else
// .MinimumLevel.Information()
//#endif
// .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
// .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
// .Enrich.FromLogContext()
// //.Filter.ByExcluding(c => c.Properties.Any(p => p.Value.ToString().Contains("Microsoft")))//过滤
// .WriteTo.Async(c => c.File("/var/telpo_pushthird_ssl2/logs/infos/info.log",
// restrictedToMinimumLevel: LogEventLevel.Information,
// rollingInterval: RollingInterval.Day,//滚动策略(天)
// //fileSizeLimitBytes: 20971520, //设置单个文件大小为3M 默认1G
// rollOnFileSizeLimit: true, //超过文件大小后创建新的
// retainedFileCountLimit: 7,//默认31,意思就是只保留最近的31个日志文件"
// outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff}[{Level:u3}] [Thread-{ThreadId}] [{SourceContext:l}] {Message:lj}{NewLine}{Exception}"
// )
// )
// .WriteTo.Async(c => c.File("/var/telpo_pushthird_ssl2/logs/errors/errors.log",
// restrictedToMinimumLevel: LogEventLevel.Error,
// rollingInterval: RollingInterval.Day,
// rollOnFileSizeLimit: true,
// retainedFileCountLimit: 7,
// outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff }[{Level:u3}] [Thread-{ThreadId}] [{SourceContext:l}] {Message:lj}{NewLine}{Exception}"
// )
// )
// .WriteTo.Async(c => c.Console())
// .CreateLogger();

//选择配置文件appsetting.json
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();

Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.WithThreadId()
.CreateLogger();
#endregion

Log.Information("Starting up");
var builder = Host.CreateApplicationBuilder(args);

var config = builder.Configuration;
builder.Services.Configure<FenceConfig>(config.GetSection("FenceConfig"));
builder.Services.Configure<ServiceConfig>(config.GetSection("ServiceConfig"));
builder.Services.Configure<RedisConfig>(config.GetSection("Redis"));

JsonSerializerSettings setting = new JsonSerializerSettings();
JsonConvert.DefaultSettings = () =>
{
setting.DateFormatString = "yyyy-MM-dd HH:mm:ss";
setting.ContractResolver = new CamelCasePropertyNamesContractResolver();
return setting;
};
builder.Services.AddTelpoDataServices(opt =>
{
opt.TelpoDataUrl = config["ServiceConfig:TelpoDataUrl"];
});

builder.Services.AddSerilog();
builder.Services.AddHttpClient();
builder.Services.AddTransient<HttpHelperAsync>();
builder.Services.AddSingleton<SqlMapper>();
builder.Services.AddSingleton<RedisUtil>();
builder.Services.AddSingleton<IKafkaService, KafkaService>();
builder.Services.AddSingleton<KafkaSubscribe>();
builder.Services.AddSingleton<MessageProducer>();
builder.Services.AddSingleton<MqProcessMessage>();
builder.Services.AddSingleton<FenceProcess>();
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application start-up failed");
}
finally
{
Log.CloseAndFlush();
}

+ 15
- 0
TelpoPush.Fence.Worker/Properties/launchSettings.json Целия файл

@@ -0,0 +1,15 @@
{
"profiles": {
"TelpoPush.Fence.Worker": {
"commandName": "Project",
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true
},
"Container (Dockerfile)": {
"commandName": "Docker"
}
},
"$schema": "http://json.schemastore.org/launchsettings.json"
}

+ 82
- 0
TelpoPush.Fence.Worker/Service/Cache/MemoryCacheUtil.cs Целия файл

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;

namespace TelpoPush.Fence.Worker.Service.Cache
{
internal class MemoryCacheUtil
{
static MemoryCache cache = new MemoryCache(new MemoryCacheOptions());
/// <summary>
/// 创建缓存项的文件
/// </summary>
/// <param name="key">缓存Key</param>
/// <param name="obj">object对象</param>
public static void Set(string key, object value)
{
if (key != null)
{
cache.Set(key, value);
}
}
/// <summary>
/// 创建缓存项过期
/// </summary>
/// <param name="key">缓存Key</param>
/// <param name="obj">object对象</param>
/// <param name="expires">过期时间(秒)</param>
public static void Set(string key, object value, int expires)
{
if (key != null)
{
cache.Set(key, value, new MemoryCacheEntryOptions()
//设置缓存时间,如果被访问重置缓存时间。设置相对过期时间x秒
.SetSlidingExpiration(TimeSpan.FromSeconds(expires)));
}
}

/// <summary>
/// 获取缓存对象
/// </summary>
/// <param name="key">缓存Key</param>
/// <returns>object对象</returns>
public static object Get(string key)
{
object val = null;
if (key != null && cache.TryGetValue(key, out val))
{

return val;
}
else
{
return default(object);
}
}

/// <summary>
/// 获取缓存对象
/// </summary>
/// <typeparam name="T">T对象</typeparam>
/// <param name="key">缓存Key</param>
/// <returns></returns>
public static T Get<T>(string key)
{
object obj = Get(key);
return obj == null ? default(T) : (T)obj;
}


/// <summary>
/// 移除缓存项的文件
/// </summary>
/// <param name="key">缓存Key</param>
public static void Remove(string key)
{
cache.Remove(key);
}
}
}

+ 191
- 0
TelpoPush.Fence.Worker/Service/Cache/RedisUtil.cs Целия файл

@@ -0,0 +1,191 @@
using Microsoft.Extensions.Options;
using TelpoPush.Fence.Worker.Models.CacheTemplates;
using TelpoPush.Fence.Worker.Models.Config;

namespace TelpoPush.Fence.Worker.Service.Cache
{
public class RedisUtil
{
private const string CACHE_HASH_KEY_TELPO_GPSDEVICE = "TELPO#GPSDEVICE_HASH";
private const string CACHE_HASH_KEY_TELPO_GPSDEVICE_FENCE = "TELPO#GPSDEVICE_FENCE_HASH";
private const string CACHE_HASH_KEY_FENCE_LAST_TIME = "TELPO_FENCE#GPSDEVICE_LAST_TIME_HASH";
private const string CACHE_HASH_KEY_FENCE_LAST_STATUS = "TELPO_FENCE#GPSDEVICE_LAST_STATUS_HASH";
private readonly ILogger<RedisUtil> _logger;
private readonly ServiceConfig _configService;
private readonly SqlMapper _sqlMapper;
public RedisUtil(ILogger<RedisUtil> logger,IOptions<RedisConfig> optConfigRedis, IOptions<ServiceConfig> configService, SqlMapper sqlMapper)
{
_configService = configService.Value;
_logger = logger;
optConfigRedis.Value.Prefix = "";
optConfigRedis.Value.DefaultDatabase = 7;
var csredis = new CSRedis.CSRedisClient(optConfigRedis.Value.ToString());
RedisHelper.Initialization(csredis);
_sqlMapper = sqlMapper;
}

public async Task<DeviceInfoModel> GetGpsDevice(string imei)
{
if (string.IsNullOrWhiteSpace(imei)) return null;
string keyCache = $"{imei}_GpsDevice";
try
{
var objCache = MemoryCacheUtil.Get<DeviceInfoModel>(keyCache);
if (objCache == null)
{
var obj = await RedisHelper.HGetAsync<DeviceInfoModel>(CACHE_HASH_KEY_TELPO_GPSDEVICE, imei);
if (obj == null)
{
var deviceInfo = _sqlMapper.DeviceInfo(imei);
if (deviceInfo != null)
{
await RedisHelper.HSetAsync(CACHE_HASH_KEY_TELPO_GPSDEVICE, imei, deviceInfo);
MemoryCacheUtil.Set(keyCache, deviceInfo, _configService.CacheDurationSeconds);
return deviceInfo;
}
}
else
{
MemoryCacheUtil.Set(keyCache, obj, _configService.CacheDurationSeconds);
return obj;
}
}
return objCache;
}
catch (Exception ex)
{
_logger.LogError($"GetGpsDevice,key={imei},缓存异常,重新获取数据,{ex.Message}|{ex.Source}|{ex.StackTrace}");
var deviceInfo = _sqlMapper.DeviceInfo(imei);
if (deviceInfo != null)
{
await RedisHelper.HSetAsync(CACHE_HASH_KEY_TELPO_GPSDEVICE, imei, deviceInfo);
MemoryCacheUtil.Set(keyCache, deviceInfo, _configService.CacheDurationSeconds);
return deviceInfo;
}
}
return null;
}

public bool ClearGpsDeviceFence() {

var fence = _sqlMapper.DeviceFenceAll();
var status = RedisHelper.HGetAll<LastStatusInfo>(CACHE_HASH_KEY_FENCE_LAST_STATUS);

var list = status.Where(s => !fence.Where(p => s.Key == p.imei + "_" + p.geofenceId).Any()).ToList();

foreach (var item in list)
{
RedisHelper.HDel(CACHE_HASH_KEY_FENCE_LAST_STATUS, item.Key);
}
//int[] oldArray = { 1, 2, 3, 4, 5 };
//int[] newArray = { 2, 4, 5, 7, 8, 9 };
//var jiaoJi = oldArray.Intersect(newArray).ToList();//2,4,5
//var oldChaJi = oldArray.Except(newArray).ToList();//1,3
//var newChaJi = newArray.Except(oldArray).ToList();//7,8,9
//var bingJi = oldArray.Union(newArray).ToList();//1,2,3,4,5,7,8,9

return true;
}


public async Task<DeviceFenceModel> GetGpsDeviceFence(string imei)
{
if (string.IsNullOrWhiteSpace(imei)) return null;
var obj = await RedisHelper.HGetAsync<DeviceFenceModel>(CACHE_HASH_KEY_TELPO_GPSDEVICE_FENCE, imei);
if (obj == null)
{
var device = GetGpsDevice(imei).Result;
if (device == null) return null;
DeviceFenceModel model = new DeviceFenceModel();
model.imei = device.imei;
model.deviceId = device.deviceId;
List<DeviceFenceList> fenceList = new List<DeviceFenceList>();
var list = _sqlMapper.DeviceFencelist(imei);
if (list.Count > 0)
{
foreach (var fence in list)
{
DeviceFenceList val = new DeviceFenceList();
val.geofenceId = fence.geofenceId;
val.geofenceName = fence.geofenceName;
val.WifiInfo = _sqlMapper.DeviceFenceWifi(imei, fence.geofenceId);
val.fenceType = fence.fenceType;
val.isActive = fence.isActive;
val.appType = fence.appType;
val.address = fence.address;
val.alarmType = fence.alarmType;
List<pointList> pointList = new List<pointList>();
if (fence.fenceType == 1)
{
pointList.Add(new pointList()
{
index = 0,
radius = fence.radius,
latitude = fence.latitude,
longitude = fence.longitude
});
}
else
{
var pointLists = _sqlMapper.DeviceFencePointlist(fence.geofenceId);
foreach (var point in pointLists)
{
pointList.Add(new pointList()
{
index = point.index,
radius = 0,
latitude = point.latitude,
longitude = point.longitude
});
}
}
val.pointList = pointList;
fenceList.Add(val);
}
model.count = fenceList.Count;
model.fenceList = fenceList;
await RedisHelper.HSetAsync(CACHE_HASH_KEY_TELPO_GPSDEVICE_FENCE, imei, model);
return model;
}
else
return null;
}
return obj;
}
public async Task<bool> GetIsDateStatus(DeviceTime model)
{
bool result = true;
try
{
string file = model.imei;
if (string.IsNullOrWhiteSpace(model.imei)) return false;
var obj = await RedisHelper.HGetAsync<DeviceTime>(CACHE_HASH_KEY_FENCE_LAST_TIME, file);
if (obj == null)
await RedisHelper.HSetAsync(CACHE_HASH_KEY_FENCE_LAST_TIME, file, model);
else
{
if (model.dt > obj.dt)
await RedisHelper.HSetAsync(CACHE_HASH_KEY_FENCE_LAST_TIME, file, model);
else
result = false;
}
}
catch (Exception ex)
{
string exs = ex.Message;
}
return result;
}
public async Task SetFenceLastStatus(string fenceKey, LastStatusInfo info)
{
await RedisHelper.HSetAsync(CACHE_HASH_KEY_FENCE_LAST_STATUS, fenceKey, info);
}
public async Task<LastStatusInfo> GetFenceLastStatus(string fenceKey)
{
if (string.IsNullOrWhiteSpace(fenceKey)) return null;
var status = await RedisHelper.HGetAsync<LastStatusInfo>(CACHE_HASH_KEY_FENCE_LAST_STATUS, fenceKey);
return status;
}

}
}

+ 65
- 0
TelpoPush.Fence.Worker/Service/Cache/SqlMapper.cs Целия файл

@@ -0,0 +1,65 @@
using Dapper;
using MySql.Data.MySqlClient;
using System.Data;
using TelpoPush.Fence.Worker.Models.CacheTemplates;

namespace TelpoPush.Fence.Worker.Service.Cache
{
public class SqlMapper
{
private readonly IConfiguration _config;
private static string gps_conn = "";
private static string telcommon_conn = "";
public SqlMapper(IConfiguration config)
{
_config = config;
gps_conn = _config["ConnectionStrings:DB_Connection_String"].ToString();
telcommon_conn = _config["ConnectionStrings:Telpo_common_ConnString"].ToString();
}

public DeviceInfoModel DeviceInfo(string imei)
{
using (IDbConnection connection = new MySqlConnection(gps_conn))
{
var sql = @"SELECT d.device_id deviceId, p.nick_name deviceName,p.api_uid apiUid,d.serialno imei, d.org_uid orgId, d.active_status activeStatus,active_time activeTime FROM gps_device as d LEFT JOIN `gps_person` as p on p.device_id=d.device_id WHERE d.serialno=@imei";
return connection.QueryFirstOrDefault<DeviceInfoModel>(sql, new { imei });
}
}

public List<string> DeviceFenceWifi(string imei, string geofence_id)
{
using (IDbConnection connection = new MySqlConnection(gps_conn))
{
var sql = @"SELECT wifi_info FROM gps_geofence_wifi WHERE imei=@imei AND geofence_id=@geofence_id";
return connection.Query<string>(sql, new { imei, geofence_id }).ToList();
}
}

public List<DeviceFenceModels> DeviceFenceAll()
{
using (IDbConnection connection = new MySqlConnection(gps_conn))
{
var sql = @"SELECT geofence_id geofenceId,geofence_name geofenceName, device_id deviceId, serialno AS imei, radius, center_lat latitude, center_lng longitude, fence_type fenceType, is_active isActive, app_type appType, alarm_type alarmType, address FROM gps_geofence";
return connection.Query<DeviceFenceModels>(sql ).ToList();
}
}

public List<DeviceFenceModels> DeviceFencelist(string imei)
{
using (IDbConnection connection = new MySqlConnection(gps_conn))
{
var sql = @"SELECT geofence_id geofenceId,geofence_name geofenceName, device_id deviceId, serialno AS imei, radius, center_lat latitude, center_lng longitude, fence_type fenceType, is_active isActive, app_type appType, alarm_type alarmType, address FROM gps_geofence where serialno=@imei";
return connection.Query<DeviceFenceModels>(sql, new { imei }).ToList();
}
}

public List<pointList> DeviceFencePointlist(string geofence_id)
{
using (IDbConnection connection = new MySqlConnection(gps_conn))
{
var sql = @"SELECT geofence_id geofenceId, radius, longitude, latitude, vertex_index AS `index` FROM gps_geofence_details where geofence_id=@geofence_id ORDER BY `index`";
return connection.Query<pointList>(sql, new { geofence_id }).ToList();
}
}
}
}

+ 9
- 0
TelpoPush.Fence.Worker/Service/Mq/IKafkaService.cs Целия файл

@@ -0,0 +1,9 @@
using Confluent.Kafka;

namespace TelpoPush.Fence.Worker.Service.Mq
{
public interface IKafkaService
{
Task SubscribeAsync(Action<string, string, Headers> messageFunc, CancellationToken cancellationToken);
}
}

+ 15
- 0
TelpoPush.Fence.Worker/Service/Mq/KafkaHeader.cs Целия файл

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Service.Mq
{
public class KafkaHeader
{
public static readonly string DataType = "DataType";
public static readonly string AlarmType = "AlarmType";
public static readonly string OperType = "OperType";
}
}

+ 157
- 0
TelpoPush.Fence.Worker/Service/Mq/KafkaService.cs Целия файл

@@ -0,0 +1,157 @@
using Confluent.Kafka;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using TelpoPush.Fence.Worker.Models.Config;

namespace TelpoPush.Fence.Worker.Service.Mq
{
public class KafkaService : IKafkaService
{
private readonly ConsumerConfig _consumerConfig;
private readonly IHostEnvironment env;
private readonly ILogger<KafkaService> logger;
private readonly ServiceConfig _configService;
public KafkaService(ILogger<KafkaService> _logger, IHostEnvironment _env, IOptions<ServiceConfig> optConfigService)
{
_configService = optConfigService.Value;
env = _env;
logger = _logger;

_consumerConfig = new ConsumerConfig
{
BootstrapServers = _configService.KafkaBootstrapServers,
GroupId = _configService.KafkaGroupId,
EnableAutoCommit = false, // 禁止AutoCommit
Acks = Acks.Leader, // 假设只需要Leader响应即可
AutoOffsetReset = AutoOffsetReset.Earliest,// 从最早的开始消费起
CancellationDelayMaxMs = 1//set CancellationDelayMaxMs
};


//_consumerConfig = new ConsumerConfig
//{
// BootstrapServers = _configService.KafkaBootstrapServers,
// GroupId = _configService.KafkaGroupId,
// EnableAutoCommit = false,
// StatisticsIntervalMs = 5000,
// SessionTimeoutMs = 6000,
// AutoOffsetReset = AutoOffsetReset.Earliest,
// EnablePartitionEof = true,
// CancellationDelayMaxMs=1
//};
}
public async Task SubscribeAsync(Action<string, string, Headers> messageFunc, CancellationToken cancellationToken)
{
List<string> topics = _configService.KafkaTopics;
using (var consumer = new ConsumerBuilder<Ignore, string>(_consumerConfig)
.SetErrorHandler((_, e) =>
{
logger.LogError($"Error: {e.Reason}");
})
.SetStatisticsHandler((_, json) =>
{
logger.LogInformation($" - {DateTime.Now:yyyy-MM-dd HH:mm:ss} > 消息监听中..");
})
.SetPartitionsAssignedHandler((c, partitions) =>
{
string partitionsStr = string.Join(", ", partitions);
logger.LogInformation($" - 分配的 kafka 分区: {partitionsStr}");
})
.SetPartitionsRevokedHandler((c, partitions) =>
{
string partitionsStr = string.Join(", ", partitions);
logger.LogInformation($" - 回收了 kafka 分区: {partitionsStr}");
})
.Build())
{

consumer.Subscribe(topics);
try
{
while (true)
{
try
{
var consumeResult = consumer.Consume(cancellationToken);
string topic = consumeResult.Topic;
string messageResult = consumeResult.Message.Value;
Headers headers = consumeResult.Message.Headers;
bool isPartitionEOF = consumeResult.IsPartitionEOF;
var partition = consumeResult.Partition;

int DataType = -1, AlarmType = -1, OperType = -1;
foreach (var item in headers)
{
if (item.Key == KafkaHeader.DataType)
DataType = BitConverter.ToInt32(item.GetValueBytes(), 0);
else if (item.Key == KafkaHeader.AlarmType)
AlarmType = BitConverter.ToInt32(item.GetValueBytes(), 0);
else if (item.Key == KafkaHeader.OperType)
OperType = BitConverter.ToInt32(item.GetValueBytes(), 0);
}
var Headers = new { DataType, AlarmType, OperType };
logger.LogInformation($"Consumed topic '{topic}' , message '{messageResult}' , headers '{JsonConvert.SerializeObject(Headers)}', at '{consumeResult?.TopicPartitionOffset}'.");
if (isPartitionEOF)
{
logger.LogInformation($" - {DateTime.Now:yyyy-MM-dd HH:mm:ss} 已经到底了:{topic}, partition {partition}, offset {consumeResult?.Offset}.");
continue;
}
if (!string.IsNullOrEmpty(messageResult))
{
messageFunc(topic, messageResult, headers);
try
{
consumer.Commit(consumeResult);
}
catch (KafkaException e)
{
logger.LogError($" - {e.Message}.");
}
}
#region 注释
//string messageResult = null;
//Headers headers = null;
//try
//{
// messageResult = consumeResult.Message.Value;
// headers = consumeResult.Message.Headers;
//}
//catch (Exception ex)
//{
// var errorMessage = $" - {DateTime.Now:yyyy-MM-dd HH:mm:ss}【Exception 消息反序列化失败,Value:{consumeResult.Message.Value}】 :{ex.StackTrace?.ToString()}";
// Console.WriteLine(errorMessage);
// logger.LogError(errorMessage);
// messageResult = null;
//}
//if (!string.IsNullOrEmpty(messageResult)/* && consumeResult.Offset % commitPeriod == 0*/)
//{
// string topic = consumeResult.Topic;
// messageFunc(topic, messageResult, headers);

// //try
// //{
// // consumer.Commit(consumeResult);
// //}
// //catch (KafkaException e)
// //{
// // Console.WriteLine(e.Message);
// //}
//}
#endregion
}
catch (ConsumeException e)
{
logger.LogError($"Consume error: {e.Error.Reason}");
}
}
}
catch (OperationCanceledException)
{
logger.LogError("Closing consumer.");
consumer.Close();
}
}
await Task.CompletedTask;
}
}
}

+ 77
- 0
TelpoPush.Fence.Worker/Service/Mq/MessageProducer.cs Целия файл

@@ -0,0 +1,77 @@
using Confluent.Kafka;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using TelpoPush.Fence.Worker.Models.Config;

namespace TelpoPush.Fence.Worker.Service.Mq
{
/// <summary>
/// 消息生产者
/// </summary>
public class MessageProducer
{
private readonly ILogger<MessageProducer> _logger;
private readonly ServiceConfig _configService;
private readonly IProducer<Null, string> _producer;

public MessageProducer(ILogger<MessageProducer> logger, IOptions<ServiceConfig> optConfigService)
{
_logger = logger;
_configService = optConfigService.Value;

var config = new ProducerConfig
{
BootstrapServers = _configService.KafkaBootstrapServers,
EnableIdempotence = true,
Acks = Acks.All,
//LingerMs=5000,
//BatchNumMessages =1000,
//BatchSize=32768,
//CompressionType= CompressionType.Lz4,
MessageSendMaxRetries = 3
};
_producer = new ProducerBuilder<Null, string>(config).Build();
}

public Headers CreateHeader(Dictionary<string, int> pair = null)
{
if (pair == null)
{
return null;
}
else
{
Headers headers = new Headers();

foreach (var item in pair)
{
headers.Add(item.Key, BitConverter.GetBytes(item.Value));
}
return headers;
}
}

public async Task ProduceAsync(List<TopicModel> topic, object message)
{
try
{
foreach (var item in topic)
{
// producer = new ProducerBuilder<Null, string>(config).Build();
await _producer.ProduceAsync(item.Topic, new Message<Null, string>
{
Headers = item.Headers,
Value = JsonConvert.SerializeObject(message)
});
}
}
catch (ProduceException<Null, string> e)
{
_logger.LogError($"推送到kafka失败,topic: {topic},\n message:{JsonConvert.SerializeObject(message)}: \n{e.Error.Reason}");
}
}



}
}

+ 81
- 0
TelpoPush.Fence.Worker/Service/Mq/MqProcessMessage.cs Целия файл

@@ -0,0 +1,81 @@
using Confluent.Kafka;
using Newtonsoft.Json;
using System.Reflection.PortableExecutable;
using TelpoPush.Fence.Worker.Models.Enum;
using TelpoPush.Fence.Worker.Models.MqTemplates;
using TelpoPush.Fence.Worker.Models.PushTemplates;

namespace TelpoPush.Fence.Worker.Service.Mq
{
public class MqProcessMessage
{
private readonly ILogger<MqProcessMessage> _logger;
private readonly MessageProducer _messageProducer;

public MqProcessMessage(ILogger<MqProcessMessage> logger, MessageProducer producer)
{
_logger = logger;
_messageProducer = producer;
}

public async Task ProcessProperty(string imei, BaseModel model, HeadersDto headers)
{
List<TopicModel> ls = new List<TopicModel>();

ls.Add(new TopicModel()
{
Topic = "topic.push.property",
Headers = _messageProducer.CreateHeader(new Dictionary<string, int>
{
{MqHeader.DataType,headers.DataType.Value }
})
});
await _messageProducer.ProduceAsync(ls, model);
_logger.LogInformation($"【成功】围栏告警推送(topic.property):IMEI<{imei}>,pushData:{JsonConvert.SerializeObject(model)}");
}


//推送服务
public async Task ProcessPushService(object model,string imei)
{
List<TopicModel> ls = new List<TopicModel>();
ls.Add(new TopicModel()
{
Topic = "topic.push",
Headers = _messageProducer.CreateHeader(new Dictionary<string, int>
{
{MqHeader.DataType,(int)MqDataType.AlarmInfo }
})
});
await _messageProducer.ProduceAsync(ls, model);

_logger.LogInformation($"【成功】围栏告警推送(topic.push):IMEI<{imei}>,pushData:{JsonConvert.SerializeObject(model)}");
}



public async Task ProcessWxAlarm(PushWxTemplate model, string fenceId, string timeString)
{

List<TopicModel> ls = new List<TopicModel>();
ls.Add(new TopicModel()
{
Topic = "topic.push.wx",
Headers = _messageProducer.CreateHeader(new Dictionary<string, int>
{
{MqHeader.DataType,(int)MqDataType.AlarmInfo },
})
});
var objData = new
{
messageId = string.Format("{0:yyyyMMddHHmmssffff}", DateTime.Now),
topic = string.Join(",", ls.Select(e => e.Topic)),
time = timeString,
data = model
};
await _messageProducer.ProduceAsync(ls, objData);
_logger.LogInformation($"【成功】围栏告警推送(topic.push.wx):IMEI<{model.imei}>,pushData:{JsonConvert.SerializeObject(model)}");
}

}
}

+ 15
- 0
TelpoPush.Fence.Worker/Service/Mq/TopicModel.cs Целия файл

@@ -0,0 +1,15 @@
using Confluent.Kafka;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TelpoPush.Fence.Worker.Service.Mq
{
public class TopicModel
{
public string Topic { get; set; }
public Headers Headers { get; set; }
}
}

+ 31
- 0
TelpoPush.Fence.Worker/TelpoPush.Fence.Worker.csproj Целия файл

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<!--<Nullable>enable</Nullable>-->
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-TelpoPush.Fence.Worker-3429e355-573c-49e4-9ea5-28d6c4563829</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<Nullable>disable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Confluent.Kafka" Version="2.4.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Exceptionless" Version="4.0.0" />
<PackageReference Include="CSRedisCore" Version="3.8.803" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="MySql.Data" Version="8.4.0" />
<PackageReference Include="TelpoDataService.Util" Version="1.6.9.28-beta1" />
</ItemGroup>

<ItemGroup>
<Folder Include="Service\Biz\" />
</ItemGroup>
</Project>

+ 25
- 0
TelpoPush.Fence.Worker/Worker.cs Целия файл

@@ -0,0 +1,25 @@
using TelpoPush.Fence.Worker.Handlers;

namespace TelpoPush.Fence.Worker
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
KafkaSubscribe _kafkaSubscribe;

public Worker(ILogger<Worker> logger, KafkaSubscribe kafkaSubscribe)
{
_logger = logger;
_kafkaSubscribe = kafkaSubscribe;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await _kafkaSubscribe.SubscribeAsync();
await Task.Delay(1000, stoppingToken);
}
}
}
}

+ 33
- 0
TelpoPush.Fence.Worker/appsettings.Development.json Целия файл

@@ -0,0 +1,33 @@
{
"ConnectionStrings": {
//"DB_Connection_String": "server=139.224.254.18;port=3305;database=gps_card;uid=root;pwd=telpo#1234;CharSet=utf8;MinimumPoolSize=10;MaximumPoolSize=1000;SslMode=none",
//"Telpo_common_ConnString": "server=139.224.254.18;port=3305;database=telpo_common;uid=root;pwd=telpo#1234;CharSet=utf8;MinimumPoolSize=10;MaximumPoolSize=1000;SslMode=none"

"DB_Connection_String": "server=rm-uf6j529mu0v6g0btpco.mysql.rds.aliyuncs.com;port=3305;database=gps_card;uid=root;pwd=telpo#1234;CharSet=utf8;MinimumPoolSize=10;MaximumPoolSize=1000;SslMode=none",
"Telpo_common_ConnString": "server=rm-uf6j529mu0v6g0btpco.mysql.rds.aliyuncs.com;port=3305;database=telpo_common;uid=linwl;pwd=linwl#1234;CharSet=utf8;MinimumPoolSize=10;MaximumPoolSize=1000;SslMode=none"
},
"FenceConfig": {
"XiaoAnManufactorId": "ad5434be-f5f2-4dee-a68d-3b2e7ac0ed7c",
"XiaoAnPushUrl": "http://182.92.204.237:5003/api/warn/send"
},
"ServiceConfig": {
"TelpoDataUrl": "http://id.ssjlai.com/data",
"KafkaBootstrapServers": "47.116.67.214:9092", // "172.19.42.53:9092",
"KafkaTopics": [ "topic.push.fence" ],
"KafkaGroupId": "telpo.fence",
"MaxDegreeOfParallelism": "3",
"CacheDurationSeconds": 1200 //20分钟
},
"Redis": {
//"Server": "47.116.142.20:8090",
"Server": "139.224.254.18:8090",
"Password": "telpo#1234",
"DefaultDatabase": 7,
"Poolsize": 150,
"Preheat": 50,
"Prefix": "_Fence_",
"ConnectTimeout": 5000,
"IdleTimeout": 20000
},
"Environment": "dev"
}

+ 70
- 0
TelpoPush.Fence.Worker/appsettings.json Целия файл

@@ -0,0 +1,70 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"System.Net.Http.HttpClient": "Warning"
}
},
"Serilog": {
"Using": [ "Serilog.Sinks.File", "Serilog.Sinks.Async", "Serilog.Sinks.Console", "Serilog.Sinks.Exceptionless" ],
"MinimumLevel": {
"Default": "Verbose",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"System.Net.Http.HttpClient": "Warning"
}
},
"WriteTo:Information": {
"Name": "Async",
"Args": {
"Configure": [
{
"Name": "File",
"Args": {
"RestrictedToMinimumLevel": "Information",
"RollingInterval": "Day",
"RollOnFileSizeLimit": "true",
"OutputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss }[{Level:u3}] [Thread-{ThreadId}] [{SourceContext:l}] {Message:lj}{NewLine}{Exception}",
"Path": "/var/telpo_pushfence/logs/infos/info.log",
"RetainedFileCountLimit": 7 // "--设置日志文件个数最大值,默认31,意思就是只保留最近的31个日志文件", "等于null时永远保留文件": null
// "FileSizeLimitBytes": 20971520, //设置单个文件大小为3M 默认1G
// "RollOnFileSizeLimit": true //超过文件大小后创建新的

}
}
]
}
},
"WriteTo:Error": {
"Name": "Async",
"Args": {
"Configure": [
{
"Name": "File",
"Args": {
"RestrictedToMinimumLevel": "Error",
"RollingInterval": "Day",
"RollOnFileSizeLimit": "true",
"OutputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss }[{Level:u3}] [Thread-{ThreadId}][{SourceContext:l}] {Message:lj}{NewLine}{Exception}",
"Path": "/var/telpo_pushfence/logs/errors/error.log",
"RetainedFileCountLimit": 7 // "--设置日志文件个数最大值,默认31,意思就是只保留最近的31个日志文件", "等于null时永远保留文件": null
// "FileSizeLimitBytes": 20971520, //设置单个文件大小为3M 默认1G
// "RollOnFileSizeLimit": true //超过文件大小后创建新的
}
}
]
}
},
"WriteTo:Console": {
"Name": "Console",
"Args": {
"restrictedToMinimumLevel": "Verbose",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss }[{Level:u3}] [Thread-{ThreadId}] [{SourceContext:l}] {Message:lj}{NewLine}{Exception}",
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console"
}
}
}
}

+ 29
- 0
TelpoPush.Fence.Worker/appsettings.production.json Целия файл

@@ -0,0 +1,29 @@
{
"ConnectionStrings": {
"DB_Connection_String": "server=rm-uf6j529mu0v6g0btp.mysql.rds.aliyuncs.com;port=3305;database=gps_card;uid=root;pwd=telpo#1234;CharSet=utf8;MinimumPoolSize=10;MaximumPoolSize=1000;SslMode=none",
"Telpo_common_ConnString": "server=rm-uf6j529mu0v6g0btp.mysql.rds.aliyuncs.com;port=3305;database=telpo_common;uid=linwl;pwd=linwl#1234;CharSet=utf8;MinimumPoolSize=10;MaximumPoolSize=1000;SslMode=none"
},
"FenceConfig": {
"XiaoAnManufactorId": "ad5434be-f5f2-4dee-a68d-3b2e7ac0ed7c",
"XiaoAnPushUrl": "http://182.92.204.237:5003/api/warn/send"
},
"ServiceConfig": {
"TelpoDataUrl": "https://ai.ssjlai.com/data",
"KafkaBootstrapServers": "172.19.42.40:9092,172.19.42.41:9092,172.19.42.48:9092",
"KafkaTopics": [ "topic.push.fence" ],
"KafkaGroupId": "telpo.fence",
"MaxDegreeOfParallelism": "3",
"CacheDurationSeconds": 1200 //20分钟
},
"Redis": {
"Server": "172.19.42.40:8090",
"Password": "telpo#1234",
"DefaultDatabase": 7,
"Poolsize": 150,
"Preheat": 50,
"Prefix": "_Fence_",
"ConnectTimeout": 5000,
"IdleTimeout": 20000
},
"Environment": "production"
}

+ 29
- 0
TelpoPush.Fence.Worker/appsettings.test.json Целия файл

@@ -0,0 +1,29 @@
{
"ConnectionStrings": {
"DB_Connection_String": "server=172.19.42.40;port=3305;database=gps_card;uid=root;pwd=telpo#1234;CharSet=utf8;MinimumPoolSize=10;MaximumPoolSize=1000;SslMode=none",
"Telpo_common_ConnString": "server=172.19.42.40;port=3305;database=telpo_common;uid=root;pwd=telpo#1234;CharSet=utf8;MinimumPoolSize=10;MaximumPoolSize=1000;SslMode=none"
},
"FenceConfig": {
"XiaoAnManufactorId": "ad5434be-f5f2-4dee-a68d-3b2e7ac0ed7c",
"XiaoAnPushUrl": "http://182.92.204.237:5003/api/warn/send"
},
"ServiceConfig": {
"TelpoDataUrl": "http://id.ssjlai.com/data",
"KafkaBootstrapServers": "172.19.42.53:9092", // "172.19.42.53:9092",
"KafkaTopics": [ "topic.push.fence" ],
"KafkaGroupId": "telpo.fence",
"MaxDegreeOfParallelism": "3",
"CacheDurationSeconds": 1200 //20分钟
},
"Redis": {
"Server": "172.19.42.44:8090",
"Password": "telpo#1234",
"DefaultDatabase": 7,
"Poolsize": 150,
"Preheat": 50,
"Prefix": "_Fence_",
"ConnectTimeout": 5000,
"IdleTimeout": 20000
},
"Environment": "test"
}

+ 25
- 0
TelpoPushFence.sln Целия файл

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34902.65
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TelpoPush.Fence.Worker", "TelpoPush.Fence.Worker\TelpoPush.Fence.Worker.csproj", "{6D80B967-98F7-43DC-8AB6-BEB2F23B35F1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6D80B967-98F7-43DC-8AB6-BEB2F23B35F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6D80B967-98F7-43DC-8AB6-BEB2F23B35F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6D80B967-98F7-43DC-8AB6-BEB2F23B35F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6D80B967-98F7-43DC-8AB6-BEB2F23B35F1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8CCD230E-0371-4487-AAE2-9174BEA2C719}
EndGlobalSection
EndGlobal

+ 7
- 0
nuget.config Целия файл

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

+ 18
- 0
setup_production.sh Целия файл

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
image_version=$version
# 删除镜像
docker rmi -f $(
docker images | grep registry.cn-shanghai.aliyuncs.com/gps_card/telpo_push_fence | awk '{print $3}'
)
# 构建telpo/mrp:$image_version镜像
docker build -f ./TelpoPush.Work.Fence/Dockerfile . -t telpo/telpo_push_fence:$image_version
#TODO:推送镜像到阿里仓库
echo '=================开始推送镜像======================='
docker login --username=rzl_wangjx@1111649216405698 --password=telpo.123 registry.cn-shanghai.aliyuncs.com
docker tag telpo/telpo_push_fence:$image_version registry.cn-shanghai.aliyuncs.com/gps_card/telpo_push_fence:$image_version
docker push registry.cn-shanghai.aliyuncs.com/gps_card/telpo_push_fence:$image_version
echo '=================推送镜像完成======================='
#删除产生的None镜像
docker rmi -f $(docker images | grep none | awk '{print $3}')
# 查看镜像列表
docker images

+ 17
- 0
setup_test.sh Целия файл

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
image_version=$version
# 删除镜像
docker rmi -f $(
docker images | grep 139.224.254.18:5000/telpo_push_fence | awk '{print $3}'
)
# 构建telpo/mrp:$image_version镜像
docker build -f ./TelpoPush.Work.Fence/Dockerfile . -t telpo/telpo_push_fence:$image_version
#TODO:推送镜像到私有仓库
echo '=================开始推送镜像======================='
docker tag telpo/telpo_push_fence:$image_version 139.224.254.18:5000/telpo_push_fence:$image_version
docker push 139.224.254.18:5000/telpo_push_fence:$image_version
echo '=================推送镜像完成======================='
#删除产生的None镜像
docker rmi -f $(docker images | grep none | awk '{print $3}')
# 查看镜像列表
docker images

+ 27
- 0
telpo_push_fence_run.sh Целия файл

@@ -0,0 +1,27 @@
#!/bin/bash
environment=$1
version=$2
echo "环境变量为${environment},版本为$version!"
if [[ ${environment} == 'production' ]]; then
echo "开始远程构建容器"
docker stop telpo_push_fence || true
docker rm telpo_push_fence || true
docker rmi -f $(docker images | grep registry.cn-shanghai.aliyuncs.com/gps_card/telpo_push_fence | awk '{print $3}')
docker login --username=rzl_wangjx@1111649216405698 --password=telpo.123 registry.cn-shanghai.aliyuncs.com
docker pull registry.cn-shanghai.aliyuncs.com/gps_card/telpo_push_fence:$version
docker run -d --network=host -e environment=production -v /home/data/telpo_push_fence/log:/var/telpo_pushfence/logs --restart=always --name telpo_push_fence registry.cn-shanghai.aliyuncs.com/gps_card/telpo_push_fence:$version;
#删除产生的None镜像
docker rmi -f $(docker images | grep none | awk '{print $3}')
docker ps -a

elif [[ ${environment} == 'test' ]]; then
echo "开始在测试环境远程构建容器"
docker stop telpo_push_fence || true
docker rm telpo_push_fence || true
docker rmi -f $(docker images | grep 139.224.254.18:5000/telpo_push_fence | awk '{print $3}')
docker pull 139.224.254.18:5000/telpo_push_fence:$version
docker run -d --network=host -e environment=${environment} -v /home/data/telpo_push_fence/log:/var/telpo_pushfence/logs --restart=always --name telpo_push_fence 139.224.254.18:5000/telpo_push_fence:$version;
#删除产生的None镜像
docker rmi -f $(docker images | grep none | awk '{print $3}')
docker ps -a
fi

Loading…
Отказ
Запис