first commit
This commit is contained in:
63
.gitattributes
vendored
Normal file
63
.gitattributes
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
# This is need for earlier builds of msysgit that does not have it on by
|
||||
# default for csharp files.
|
||||
# Note: This is only used by command line
|
||||
###############################################################################
|
||||
#*.cs diff=csharp
|
||||
|
||||
###############################################################################
|
||||
# Set the merge driver for project and solution files
|
||||
#
|
||||
# Merging from the command prompt will add diff markers to the files if there
|
||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
||||
# the diff markers are never inserted). Diff markers may cause the following
|
||||
# file extensions to fail to load in VS. An alternative would be to treat
|
||||
# these files as binary and thus will always conflict and require user
|
||||
# intervention with every merge. To do so, just uncomment the entries below
|
||||
###############################################################################
|
||||
#*.sln merge=binary
|
||||
#*.csproj merge=binary
|
||||
#*.vbproj merge=binary
|
||||
#*.vcxproj merge=binary
|
||||
#*.vcproj merge=binary
|
||||
#*.dbproj merge=binary
|
||||
#*.fsproj merge=binary
|
||||
#*.lsproj merge=binary
|
||||
#*.wixproj merge=binary
|
||||
#*.modelproj merge=binary
|
||||
#*.sqlproj merge=binary
|
||||
#*.wwaproj merge=binary
|
||||
|
||||
###############################################################################
|
||||
# behavior for image files
|
||||
#
|
||||
# image files are treated as binary by default.
|
||||
###############################################################################
|
||||
#*.jpg binary
|
||||
#*.png binary
|
||||
#*.gif binary
|
||||
|
||||
###############################################################################
|
||||
# diff behavior for common document formats
|
||||
#
|
||||
# Convert binary document formats to text before diffing them. This feature
|
||||
# is only available from the command line. Turn it on by uncommenting the
|
||||
# entries below.
|
||||
###############################################################################
|
||||
#*.doc diff=astextplain
|
||||
#*.DOC diff=astextplain
|
||||
#*.docx diff=astextplain
|
||||
#*.DOCX diff=astextplain
|
||||
#*.dot diff=astextplain
|
||||
#*.DOT diff=astextplain
|
||||
#*.pdf diff=astextplain
|
||||
#*.PDF diff=astextplain
|
||||
#*.rtf diff=astextplain
|
||||
#*.RTF diff=astextplain
|
||||
375
.gitignore
vendored
Normal file
375
.gitignore
vendored
Normal file
@@ -0,0 +1,375 @@
|
||||
## 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
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Oo]bj**/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
.vscode/
|
||||
.amazonq/
|
||||
# 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
|
||||
nunit-*.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/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# 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
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# 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
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# 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
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# 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
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).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/
|
||||
|
||||
# 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
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
*vite.config.ts.timestamp*.mjs
|
||||
src/docker-compose.yaml
|
||||
|
||||
.windsurfrules
|
||||
*.env
|
||||
*.env.*
|
||||
scripts/*
|
||||
todo.txt
|
||||
.github/*
|
||||
161
.gitlab-ci.yml
Normal file
161
.gitlab-ci.yml
Normal file
@@ -0,0 +1,161 @@
|
||||
include:
|
||||
- local: ".gitlab/ci/templates/build-test.yml"
|
||||
- local: ".gitlab/ci/templates/build-push.yml"
|
||||
- local: ".gitlab/ci/templates/deploy.yml"
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
|
||||
# Джобы для сборки и тестирования при создании MR
|
||||
monitoring_backend_test:
|
||||
extends: .build_test_template
|
||||
variables:
|
||||
IMAGE_NAME: "monitoring.backend"
|
||||
MODULE_DIR: "src"
|
||||
CI_DOCKERFILE: "Monitoring/Monitoring.Api/Dockerfile"
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^(release|test)$/'
|
||||
changes:
|
||||
- "src/Monitoring/Monitoring.Api/**/*"
|
||||
- "src/Monitoring/Monitoring.Infrastructure/**/*"
|
||||
- "src/Monitoring/Monitoring.Common/**/*"
|
||||
- "src/Monitoring/Monitoring.Services/**/*"
|
||||
- "src/Monitoring/Monitoring.ServiceDefaults/**/*"
|
||||
when: on_success
|
||||
- when: never
|
||||
|
||||
monitoring_notifications_test:
|
||||
extends: .build_test_template
|
||||
variables:
|
||||
IMAGE_NAME: "monitoring.notifications"
|
||||
MODULE_DIR: "src"
|
||||
CI_DOCKERFILE: "Monitoring/Monitoring.Notifications/Dockerfile"
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^(release|test)$/'
|
||||
changes:
|
||||
- "src/Monitoring/Monitoring.Notifications/**/*"
|
||||
- "src/Monitoring/Monitoring.Infrastructure/**/*"
|
||||
- "src/Monitoring/Monitoring.Common/**/*"
|
||||
- "src/Monitoring/Monitoring.Services/**/*"
|
||||
- "src/Monitoring/Monitoring.ServiceDefaults/**/*"
|
||||
when: on_success
|
||||
- when: never
|
||||
|
||||
monitoring_frontend_test:
|
||||
extends: .build_test_template
|
||||
variables:
|
||||
IMAGE_NAME: "monitoring.frontend"
|
||||
MODULE_DIR: "src/Monitoring/Monitoring.Web"
|
||||
CI_DOCKERFILE: "Dockerfile"
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^(release|test)$/'
|
||||
changes:
|
||||
- "src/Monitoring/Monitoring.Web/**/*"
|
||||
when: on_success
|
||||
- when: never
|
||||
|
||||
ping_test:
|
||||
extends: .build_test_template
|
||||
variables:
|
||||
IMAGE_NAME: "monitoring.ping"
|
||||
MODULE_DIR: "src"
|
||||
CI_DOCKERFILE: "Ping/Dockerfile"
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^(release|test)$/'
|
||||
changes:
|
||||
- "src/Ping/**/*"
|
||||
- "src/Monitoring/Monitoring.Infrastructure/**/*"
|
||||
- "src/Monitoring/Monitoring.Common/**/*"
|
||||
- "src/Monitoring/Monitoring.Services/**/*"
|
||||
- "src/Monitoring/Monitoring.ServiceDefaults/**/*"
|
||||
- "src/Monitoring/Monitoring.Notifications.Client/**/*"
|
||||
when: on_success
|
||||
- when: never
|
||||
|
||||
# Джобы для сборки с пушем в реестр при коммите/слиянии в test/release
|
||||
monitoring_backend_build:
|
||||
extends: .build_push_template
|
||||
variables:
|
||||
IMAGE_NAME: "monitoring.backend"
|
||||
MODULE_DIR: "src"
|
||||
CI_DOCKERFILE: "Monitoring/Monitoring.Api/Dockerfile"
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH =~ /^(release|test)$/'
|
||||
changes:
|
||||
- "src/Monitoring/Monitoring.Api/**/*"
|
||||
- "src/Monitoring/Monitoring.Infrastructure/**/*"
|
||||
- "src/Monitoring/Monitoring.Common/**/*"
|
||||
- "src/Monitoring/Monitoring.Services/**/*"
|
||||
- "src/Monitoring/Monitoring.ServiceDefaults/**/*"
|
||||
when: on_success
|
||||
- when: never
|
||||
|
||||
monitoring_notifications_build:
|
||||
extends: .build_push_template
|
||||
variables:
|
||||
IMAGE_NAME: "monitoring.notifications"
|
||||
MODULE_DIR: "src"
|
||||
CI_DOCKERFILE: "Monitoring/Monitoring.Notifications/Dockerfile"
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH =~ /^(release|test)$/'
|
||||
changes:
|
||||
- "src/Monitoring/Monitoring.Notifications/**/*"
|
||||
- "src/Monitoring/Monitoring.Infrastructure/**/*"
|
||||
- "src/Monitoring/Monitoring.Common/**/*"
|
||||
- "src/Monitoring/Monitoring.Services/**/*"
|
||||
- "src/Monitoring/Monitoring.ServiceDefaults/**/*"
|
||||
when: on_success
|
||||
- when: never
|
||||
|
||||
monitoring_frontend_build:
|
||||
extends: .build_push_template
|
||||
variables:
|
||||
IMAGE_NAME: "monitoring.frontend"
|
||||
MODULE_DIR: "src/Monitoring/Monitoring.Web"
|
||||
CI_DOCKERFILE: "Dockerfile"
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH =~ /^(release|test)$/'
|
||||
changes:
|
||||
- "src/Monitoring/Monitoring.Web/**/*"
|
||||
when: on_success
|
||||
- when: never
|
||||
|
||||
ping_build:
|
||||
extends: .build_push_template
|
||||
variables:
|
||||
IMAGE_NAME: "monitoring.ping"
|
||||
MODULE_DIR: "src"
|
||||
CI_DOCKERFILE: "Ping/Dockerfile"
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH =~ /^(release|test)$/'
|
||||
changes:
|
||||
- "src/Ping/**/*"
|
||||
- "src/Monitoring/Monitoring.Infrastructure/**/*"
|
||||
- "src/Monitoring/Monitoring.Common/**/*"
|
||||
- "src/Monitoring/Monitoring.Services/**/*"
|
||||
- "src/Monitoring/Monitoring.ServiceDefaults/**/*"
|
||||
- "src/Monitoring/Monitoring.Notifications.Client/**/*"
|
||||
when: on_success
|
||||
- when: never
|
||||
|
||||
monitoring_backend_deploy:
|
||||
extends: .deploy_template
|
||||
variables:
|
||||
IMAGE_NAME: "monitoring.backend"
|
||||
|
||||
monitoring_notifications_deploy:
|
||||
extends: .deploy_template
|
||||
variables:
|
||||
IMAGE_NAME: "monitoring.notifications"
|
||||
|
||||
monitoring_frontend_deploy:
|
||||
extends: .deploy_template
|
||||
variables:
|
||||
IMAGE_NAME: "monitoring.frontend"
|
||||
|
||||
ping_deploy:
|
||||
extends: .deploy_template
|
||||
variables:
|
||||
IMAGE_NAME: "monitoring.ping"
|
||||
17
AGENTS.md
Normal file
17
AGENTS.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Правила для Агентов
|
||||
|
||||
## Общие требования
|
||||
|
||||
- Отвечай на русском языке и кратко.
|
||||
- Не нужно создавать отчеты после выполнения задач. (.md)
|
||||
- Давай краткий ответ о проделанной работе.
|
||||
- Используй лучшие практики, чистоту кода, правильную архитектуру, SOLID, DRY, KISS, YAGNI, DI.
|
||||
- Не создавай миграции вручную, а используй dotnet ef.
|
||||
- Используй текущий существующий стиль кодирования.
|
||||
|
||||
## Monitoring.Web - фронтенд
|
||||
|
||||
### Требования
|
||||
|
||||
- При изменении бекенда, используй orval для генерации типов и запросов.
|
||||
- Используй i18n для перевода текстов.
|
||||
156
README.md
Normal file
156
README.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Развертывание в Docker Swarm
|
||||
|
||||
## Требования
|
||||
|
||||
- Docker Engine версии 20.10.0 или выше
|
||||
- Docker Swarm инициализированный на целевых серверах
|
||||
- Доступ к Docker registry
|
||||
|
||||
## Образы приложения
|
||||
|
||||
В системе используются следующие контейнеры:
|
||||
|
||||
- `76457942/monitoring.backend` - бэкенд мониторинга
|
||||
- `76457942/monitoring.frontend` - фронтенд мониторинга
|
||||
- `76457942/forwarder.backend` - бэкенд форвардера
|
||||
- `76457942/forwarder.frontend` - фронтенд форвардера
|
||||
|
||||
## Подготовка к развертыванию
|
||||
|
||||
1. Убедитесь, что Docker Swarm инициализирован:
|
||||
|
||||
```bash
|
||||
docker swarm init
|
||||
```
|
||||
|
||||
2. Присоединение рабочих узлов (при необходимости):
|
||||
|
||||
```bash
|
||||
docker swarm join --token <token> <manager-ip>:2377
|
||||
```
|
||||
|
||||
## Развертывание приложения
|
||||
|
||||
1. Создайте docker-compose.yml файл:
|
||||
|
||||
```yaml
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
monitoring-backend:
|
||||
image: 76457942/monitoring.backend
|
||||
deploy:
|
||||
replicas: 2
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
ports:
|
||||
- "5000:80"
|
||||
networks:
|
||||
- monitoring-network
|
||||
|
||||
monitoring-frontend:
|
||||
image: 76457942/monitoring.frontend
|
||||
deploy:
|
||||
replicas: 2
|
||||
ports:
|
||||
- "80:80"
|
||||
networks:
|
||||
- monitoring-network
|
||||
|
||||
forwarder-backend:
|
||||
image: 76457942/forwarder.backend
|
||||
deploy:
|
||||
replicas: 2
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
ports:
|
||||
- "5001:80"
|
||||
networks:
|
||||
- monitoring-network
|
||||
|
||||
forwarder-frontend:
|
||||
image: 76457942/forwarder.frontend
|
||||
deploy:
|
||||
replicas: 2
|
||||
ports:
|
||||
- "81:80"
|
||||
networks:
|
||||
- monitoring-network
|
||||
|
||||
networks:
|
||||
monitoring-network:
|
||||
driver: overlay
|
||||
```
|
||||
|
||||
2. Разверните стек:
|
||||
|
||||
```bash
|
||||
docker stack deploy -c docker-compose.yml monitoring-stack
|
||||
```
|
||||
|
||||
## Управление развернутым приложением
|
||||
|
||||
### Просмотр статуса сервисов
|
||||
|
||||
```bash
|
||||
docker service ls
|
||||
```
|
||||
|
||||
### Масштабирование сервиса
|
||||
|
||||
```bash
|
||||
docker service scale monitoring-stack_monitoring-backend=3
|
||||
```
|
||||
|
||||
### Обновление сервиса
|
||||
|
||||
```bash
|
||||
docker service update --image 76457942/monitoring.backend:new-version monitoring-stack_monitoring-backend
|
||||
```
|
||||
|
||||
### Просмотр логов
|
||||
|
||||
```bash
|
||||
docker service logs monitoring-stack_monitoring-backend
|
||||
```
|
||||
|
||||
## Удаление развернутого приложения
|
||||
|
||||
```bash
|
||||
docker stack rm monitoring-stack
|
||||
```
|
||||
|
||||
## Мониторинг
|
||||
|
||||
После развертывания сервисы доступны по следующим адресам:
|
||||
|
||||
- Мониторинг Frontend: http://localhost:80
|
||||
- Мониторинг Backend: http://localhost:5000
|
||||
- Форвардер Frontend: http://localhost:81
|
||||
- Форвардер Backend: http://localhost:5001
|
||||
|
||||
## Устранение неполадок
|
||||
|
||||
1. Проверка статуса узлов:
|
||||
|
||||
```bash
|
||||
docker node ls
|
||||
```
|
||||
|
||||
2. Проверка задач сервиса:
|
||||
|
||||
```bash
|
||||
docker service ps monitoring-stack_monitoring-backend
|
||||
```
|
||||
|
||||
3. Проверка сетей:
|
||||
|
||||
```bash
|
||||
docker network ls
|
||||
```
|
||||
74
deploy/monitoring.frontend.yml
Normal file
74
deploy/monitoring.frontend.yml
Normal file
@@ -0,0 +1,74 @@
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- test
|
||||
- release
|
||||
paths:
|
||||
include:
|
||||
- src/Monitoring/Monitoring.Web/*
|
||||
|
||||
variables:
|
||||
- ${{ if eq(variables['Build.SourceBranchName'], 'test') }}:
|
||||
- group: Test
|
||||
- name: imageTag
|
||||
value: "test"
|
||||
- name: deployEnvironment
|
||||
value: "Test"
|
||||
- name: agentPool
|
||||
value: "Fire Service Pool Agent"
|
||||
- ${{ if eq(variables['Build.SourceBranchName'], 'release') }}:
|
||||
- group: Prod
|
||||
- name: imageTag
|
||||
value: "release"
|
||||
- name: deployEnvironment
|
||||
value: "Prod"
|
||||
- name: agentPool
|
||||
value: "IBTests"
|
||||
- name: versionTag
|
||||
value: "1.0.$(Build.BuildId)"
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: "Build frontend"
|
||||
pool:
|
||||
name: $(agentPool)
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: "Build and push Docker images"
|
||||
steps:
|
||||
- task: Docker@2
|
||||
displayName: "Build and push frontend image"
|
||||
inputs:
|
||||
command: "buildAndPush"
|
||||
Dockerfile: "src/Monitoring/Monitoring.Web/Dockerfile"
|
||||
repository: "$(REGISTRY_USERNAME)/$(MONITORING_FRONTEND_IMAGE_NAME)"
|
||||
tags: |
|
||||
$(imageTag)
|
||||
$(versionTag)
|
||||
buildContext: "src/Monitoring/Monitoring.Web"
|
||||
containerRegistry: $(REGISTRY_NAME)
|
||||
|
||||
- stage: Deploy
|
||||
displayName: "Deploy"
|
||||
dependsOn:
|
||||
- Build
|
||||
pool:
|
||||
name: $(agentPool)
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
displayName: "Deploy"
|
||||
environment: $(deployEnvironment)
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- script: |
|
||||
# Pull latest images
|
||||
docker pull $(REGISTRY_ADDRESS)/$(REGISTRY_USERNAME)/$(MONITORING_FRONTEND_IMAGE_NAME):$(imageTag)
|
||||
|
||||
# Update services with new images
|
||||
docker service update \
|
||||
--image $(REGISTRY_ADDRESS)/$(REGISTRY_USERNAME)/$(MONITORING_FRONTEND_IMAGE_NAME):$(imageTag) \
|
||||
--force \
|
||||
$(MONITORING_FRONTEND_SERVICE_NAME)
|
||||
displayName: "Update Docker services"
|
||||
33
src/.dockerignore
Normal file
33
src/.dockerignore
Normal file
@@ -0,0 +1,33 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/.continue
|
||||
**/.windsurf
|
||||
**/design
|
||||
**/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/**
|
||||
289
src/.editorconfig
Normal file
289
src/.editorconfig
Normal file
@@ -0,0 +1,289 @@
|
||||
[*]
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
|
||||
[*.yaml]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Файлы C#
|
||||
[*.cs]
|
||||
|
||||
#### Основные параметры EditorConfig ####
|
||||
|
||||
# Отступы и интервалы
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
tab_width = 4
|
||||
|
||||
# Предпочтения для новых строк
|
||||
end_of_line = crlf
|
||||
insert_final_newline = false
|
||||
|
||||
#### Рекомендации по написанию кода .NET ####
|
||||
|
||||
# Упорядочение Using
|
||||
dotnet_separate_import_directive_groups = true
|
||||
dotnet_sort_system_directives_first = true
|
||||
file_header_template = unset
|
||||
|
||||
# Предпочтения для this. и Me.
|
||||
dotnet_style_qualification_for_event = false
|
||||
dotnet_style_qualification_for_field = false
|
||||
dotnet_style_qualification_for_method = false
|
||||
dotnet_style_qualification_for_property = false
|
||||
|
||||
# Параметры использования ключевых слов языка и типов BCL
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true
|
||||
dotnet_style_predefined_type_for_member_access = true
|
||||
|
||||
# Предпочтения для скобок
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary
|
||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
|
||||
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
|
||||
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
|
||||
|
||||
# Предпочтения модификатора
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members
|
||||
|
||||
# Выражения уровень предпочтения
|
||||
dotnet_style_coalesce_expression = true
|
||||
dotnet_style_collection_initializer = true
|
||||
dotnet_style_explicit_tuple_names = true
|
||||
dotnet_style_namespace_match_folder = true
|
||||
dotnet_style_null_propagation = true
|
||||
dotnet_style_object_initializer = true
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
dotnet_style_prefer_auto_properties = true
|
||||
dotnet_style_prefer_collection_expression = when_types_loosely_match
|
||||
dotnet_style_prefer_compound_assignment = true
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true
|
||||
dotnet_style_prefer_conditional_expression_over_return = true
|
||||
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true
|
||||
dotnet_style_prefer_inferred_tuple_names = true
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true
|
||||
dotnet_style_prefer_simplified_interpolation = true
|
||||
|
||||
# Предпочтения для полей
|
||||
dotnet_style_readonly_field = true
|
||||
|
||||
# Настройки параметров
|
||||
dotnet_code_quality_unused_parameters = all
|
||||
|
||||
# Параметры подавления
|
||||
dotnet_remove_unnecessary_suppression_exclusions = none
|
||||
|
||||
# Предпочтения для новых строк
|
||||
dotnet_style_allow_multiple_blank_lines_experimental = false
|
||||
dotnet_style_allow_statement_immediately_after_block_experimental = false
|
||||
|
||||
#### Рекомендации по написанию кода C# ####
|
||||
|
||||
# Предпочтения var
|
||||
csharp_style_var_elsewhere = true:warning
|
||||
csharp_style_var_for_built_in_types = true:warning
|
||||
csharp_style_var_when_type_is_apparent = true:warning
|
||||
|
||||
# Члены, заданные выражениями
|
||||
csharp_style_expression_bodied_accessors = true
|
||||
csharp_style_expression_bodied_constructors = false
|
||||
csharp_style_expression_bodied_indexers = true
|
||||
csharp_style_expression_bodied_lambdas = true
|
||||
csharp_style_expression_bodied_local_functions = false
|
||||
csharp_style_expression_bodied_methods = false
|
||||
csharp_style_expression_bodied_operators = false
|
||||
csharp_style_expression_bodied_properties = true
|
||||
|
||||
# Настройки соответствия шаблонов
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_prefer_extended_property_pattern = true:suggestion
|
||||
csharp_style_prefer_not_pattern = true:suggestion
|
||||
csharp_style_prefer_pattern_matching = true:silent
|
||||
csharp_style_prefer_switch_expression = true:suggestion
|
||||
|
||||
# Настройки проверки на null
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# Предпочтения модификатора
|
||||
csharp_prefer_static_anonymous_function = true
|
||||
csharp_prefer_static_local_function = true
|
||||
csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
|
||||
csharp_style_prefer_readonly_struct = true
|
||||
csharp_style_prefer_readonly_struct_member = true
|
||||
|
||||
# Предпочтения для блоков кода
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_style_namespace_declarations = file_scoped:silent
|
||||
csharp_style_prefer_method_group_conversion = true:silent
|
||||
csharp_style_prefer_primary_constructors = true:suggestion
|
||||
csharp_style_prefer_top_level_statements = true:silent
|
||||
|
||||
# Выражения уровень предпочтения
|
||||
csharp_prefer_simple_default_expression = true:suggestion
|
||||
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_prefer_index_operator = true:suggestion
|
||||
csharp_style_prefer_local_over_anonymous_function = true:suggestion
|
||||
csharp_style_prefer_null_check_over_type_check = true:suggestion
|
||||
csharp_style_prefer_range_operator = true:suggestion
|
||||
csharp_style_prefer_tuple_swap = true:suggestion
|
||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
|
||||
|
||||
# предпочтения для директивы using
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
|
||||
# Предпочтения для новых строк
|
||||
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false
|
||||
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
|
||||
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
|
||||
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
|
||||
csharp_style_allow_embedded_statements_on_same_line_experimental = false:silent
|
||||
|
||||
#### Правила форматирования C# ####
|
||||
|
||||
# Предпочтения для новых строк
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_between_query_expression_clauses = true
|
||||
|
||||
# Предпочтения для отступов
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_case_contents_when_block = false
|
||||
csharp_indent_labels = one_less_than_current
|
||||
csharp_indent_switch_labels = true
|
||||
|
||||
# Предпочтения для интервалов
|
||||
csharp_space_after_cast = false
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
csharp_space_after_comma = true
|
||||
csharp_space_after_dot = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_after_semicolon_in_for_statement = true
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_around_declaration_statements = false
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
csharp_space_before_comma = false
|
||||
csharp_space_before_dot = false
|
||||
csharp_space_before_open_square_brackets = false
|
||||
csharp_space_before_semicolon_in_for_statement = false
|
||||
csharp_space_between_empty_square_brackets = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = false
|
||||
csharp_space_between_square_brackets = false
|
||||
|
||||
# Предпочтения переноса
|
||||
csharp_preserve_single_line_blocks = true
|
||||
csharp_preserve_single_line_statements = false
|
||||
|
||||
#### Стили именования ####
|
||||
|
||||
# Правила именования
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = error
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = error
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.private_or_internal_static_field_should_be_pascal_case.severity = error
|
||||
dotnet_naming_rule.private_or_internal_static_field_should_be_pascal_case.symbols = private_or_internal_static_field
|
||||
dotnet_naming_rule.private_or_internal_static_field_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.private_or_internal_field_should_be___start.severity = error
|
||||
dotnet_naming_rule.private_or_internal_field_should_be___start.symbols = private_or_internal_field
|
||||
dotnet_naming_rule.private_or_internal_field_should_be___start.style = __start
|
||||
|
||||
# Спецификации символов
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
|
||||
dotnet_naming_symbols.private_or_internal_field.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.private_or_internal_static_field.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_or_internal_static_field.applicable_accessibilities = internal, private, private_protected
|
||||
dotnet_naming_symbols.private_or_internal_static_field.required_modifiers = static
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# Стили именования
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.__start.required_prefix = _
|
||||
dotnet_naming_style.__start.required_suffix =
|
||||
dotnet_naming_style.__start.word_separator =
|
||||
dotnet_naming_style.__start.capitalization = camel_case
|
||||
|
||||
[*.{cs,vb}]
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||
dotnet_style_prefer_auto_properties = true:silent
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_prefer_simplified_boolean_expressions = false:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = false:silent
|
||||
dotnet_style_prefer_conditional_expression_over_return = false:silent
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_compound_assignment = true:suggestion
|
||||
dotnet_style_prefer_simplified_interpolation = true:suggestion
|
||||
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
|
||||
dotnet_style_namespace_match_folder = true:suggestion
|
||||
end_of_line = crlf
|
||||
dotnet_code_quality_unused_parameters = all:suggestion
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
|
||||
dotnet_style_predefined_type_for_member_access = true:silent
|
||||
dotnet_style_qualification_for_field = false:silent
|
||||
dotnet_style_qualification_for_property = false:silent
|
||||
dotnet_style_qualification_for_method = false:silent
|
||||
dotnet_style_qualification_for_event = false:silent
|
||||
dotnet_style_allow_multiple_blank_lines_experimental = false:silent
|
||||
dotnet_style_allow_statement_immediately_after_block_experimental = false:silent
|
||||
1
src/.gitattributes
vendored
Normal file
1
src/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
azure-pipelines.yml merge=ours
|
||||
365
src/.gitignore
vendored
Normal file
365
src/.gitignore
vendored
Normal file
@@ -0,0 +1,365 @@
|
||||
## 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
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
.vscode/
|
||||
# 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
|
||||
nunit-*.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/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# 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
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# 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
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# 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
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# 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
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).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/
|
||||
|
||||
# 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
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
mover.stack.env
|
||||
stack.env
|
||||
13
src/.idea/.idea.Monitoring/.idea/.gitignore
generated
vendored
Normal file
13
src/.idea/.idea.Monitoring/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Rider ignored files
|
||||
/.idea.Monitoring.iml
|
||||
/modules.xml
|
||||
/contentModel.xml
|
||||
/projectSettingsUpdater.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
1
src/.idea/.idea.Monitoring/.idea/.name
generated
Normal file
1
src/.idea/.idea.Monitoring/.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
||||
Monitoring
|
||||
4
src/.idea/.idea.Monitoring/.idea/encodings.xml
generated
Normal file
4
src/.idea/.idea.Monitoring/.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
||||
10
src/.idea/.idea.Monitoring/.idea/indexLayout.xml
generated
Normal file
10
src/.idea/.idea.Monitoring/.idea/indexLayout.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders>
|
||||
<Path>../../monitoringall</Path>
|
||||
</attachedFolders>
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
||||
7
src/.idea/.idea.Monitoring/.idea/vcs.xml
generated
Normal file
7
src/.idea/.idea.Monitoring/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
3
src/.prettierrc
Normal file
3
src/.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"singleQuote": false
|
||||
}
|
||||
9
src/BolidIntegrator/BolidIntegrator.csproj
Normal file
9
src/BolidIntegrator/BolidIntegrator.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
6
src/BolidIntegrator/Class1.cs
Normal file
6
src/BolidIntegrator/Class1.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace BolidIntegrator;
|
||||
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
14
src/Common.Api/Common.Api.csproj
Normal file
14
src/Common.Api/Common.Api.csproj
Normal file
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Elastic.Serilog.Sinks" Version="9.*" />
|
||||
<PackageReference Include="FastEndpoints" Version="7.*" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
23
src/Common.Api/DependencyInjectionExtensions.cs
Normal file
23
src/Common.Api/DependencyInjectionExtensions.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using FastEndpoints;
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Common.Api;
|
||||
|
||||
public static class DependencyInjectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddEndpoints(this IServiceCollection services)
|
||||
{
|
||||
_ = services.AddFastEndpoints();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UseEndpoints(this IApplicationBuilder app)
|
||||
{
|
||||
_ = app.UseFastEndpoints();
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
10
src/Common.Postgres/Common.Postgres.csproj
Normal file
10
src/Common.Postgres/Common.Postgres.csproj
Normal file
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.*" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
22
src/Common.Postgres/DependencyInjectionExtensions.cs
Normal file
22
src/Common.Postgres/DependencyInjectionExtensions.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Common.Postgres;
|
||||
public static class DependencyInjectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddPostgres<T>(
|
||||
this IServiceCollection services,
|
||||
string connectionString)
|
||||
where T : DbContext
|
||||
{
|
||||
_ = services.AddDbContext<T>(options =>
|
||||
{
|
||||
_ = options.UseNpgsql(connectionString)
|
||||
.LogTo(Console.WriteLine, LogLevel.Warning)
|
||||
.EnableSensitiveDataLogging(false);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
6
src/Common/Common.Endpoints/Class1.cs
Normal file
6
src/Common/Common.Endpoints/Class1.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Common.Endpoints;
|
||||
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
9
src/Common/Common.Endpoints/Common.Endpoints.csproj
Normal file
9
src/Common/Common.Endpoints/Common.Endpoints.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
19
src/Forwarder.Data/Configurations/AssetMapConfiguration.cs
Normal file
19
src/Forwarder.Data/Configurations/AssetMapConfiguration.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
using Forwarder.Domain;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Forwarder.Data.Configurations;
|
||||
/// <summary>
|
||||
/// Конфигурация сообщений.
|
||||
/// </summary>
|
||||
public class AssetMapConfiguration : IEntityTypeConfiguration<AssetMap>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Configure(EntityTypeBuilder<AssetMap> builder)
|
||||
{
|
||||
_ = builder
|
||||
.HasIndex(x => x.Abonent).IsUnique();
|
||||
}
|
||||
}
|
||||
24
src/Forwarder.Data/Configurations/MessageConfiguration.cs
Normal file
24
src/Forwarder.Data/Configurations/MessageConfiguration.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
using Forwarder.Domain;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Forwarder.Data.Configurations;
|
||||
/// <summary>
|
||||
/// Конфигурация сообщений.
|
||||
/// </summary>
|
||||
public class MessageConfiguration : IEntityTypeConfiguration<Message>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Configure(EntityTypeBuilder<Message> builder)
|
||||
{
|
||||
_ = builder
|
||||
.Property(x => x.Abonent)
|
||||
.IsRequired()
|
||||
.HasMaxLength(32);
|
||||
|
||||
_ = builder
|
||||
.HasIndex(x => x.CreatedAt);
|
||||
}
|
||||
}
|
||||
23
src/Forwarder.Data/DataContext.cs
Normal file
23
src/Forwarder.Data/DataContext.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Forwarder.Data.Configurations;
|
||||
using Forwarder.Domain;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Forwarder.Data;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class DataContext(DbContextOptions<DataContext> options)
|
||||
: DbContext(options), IDataContext
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public required DbSet<AssetMap> Maps { get; set; }
|
||||
/// <inheritdoc />
|
||||
public required DbSet<Message> Messages { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
_ = modelBuilder.ApplyConfiguration(new AssetMapConfiguration());
|
||||
_ = modelBuilder.ApplyConfiguration(new MessageConfiguration());
|
||||
}
|
||||
}
|
||||
19
src/Forwarder.Data/Forwarder.Data.csproj
Normal file
19
src/Forwarder.Data/Forwarder.Data.csproj
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.*" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.*" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Forwarder.Domain\Forwarder.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
28
src/Forwarder.Data/IDataContext.cs
Normal file
28
src/Forwarder.Data/IDataContext.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Forwarder.Domain;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Forwarder.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Интерфейс контекста данных.
|
||||
/// </summary>
|
||||
public interface IDataContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Маппинг идентификаторов объектов.
|
||||
/// </summary>
|
||||
DbSet<AssetMap> Maps { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Сообщения.
|
||||
/// </summary>
|
||||
DbSet<Message> Messages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Сохранение изменений.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
/// <returns>Число записанных с базу данных изменений.</returns>
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
90
src/Forwarder.Data/Migrations/20240803210721_Initial.Designer.cs
generated
Normal file
90
src/Forwarder.Data/Migrations/20240803210721_Initial.Designer.cs
generated
Normal file
@@ -0,0 +1,90 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Forwarder.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Forwarder.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20240803210721_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Forwarder.Domain.AssetMap", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Abonent")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Abonent")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Maps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Forwarder.Domain.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Abonent")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<int>("Code")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Line")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Receiver")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Sector")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Zone")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.ToTable("Messages");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/Forwarder.Data/Migrations/20240803210721_Initial.cs
Normal file
66
src/Forwarder.Data/Migrations/20240803210721_Initial.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Forwarder.Data.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
_ = migrationBuilder.CreateTable(
|
||||
name: "Maps",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Abonent = table.Column<string>(type: "text", nullable: false),
|
||||
AssetId = table.Column<Guid>(type: "uuid", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
_ = table.PrimaryKey("PK_Maps", x => x.Id);
|
||||
});
|
||||
|
||||
_ = migrationBuilder.CreateTable(
|
||||
name: "Messages",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Receiver = table.Column<int>(type: "integer", nullable: false),
|
||||
Line = table.Column<int>(type: "integer", nullable: false),
|
||||
Abonent = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Type = table.Column<int>(type: "integer", nullable: false),
|
||||
Code = table.Column<int>(type: "integer", nullable: false),
|
||||
Sector = table.Column<int>(type: "integer", nullable: false),
|
||||
Zone = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
_ = table.PrimaryKey("PK_Messages", x => x.Id);
|
||||
});
|
||||
|
||||
_ = migrationBuilder.CreateIndex(
|
||||
name: "IX_Maps_Abonent",
|
||||
table: "Maps",
|
||||
column: "Abonent",
|
||||
unique: true);
|
||||
|
||||
_ = migrationBuilder.CreateIndex(
|
||||
name: "IX_Messages_CreatedAt",
|
||||
table: "Messages",
|
||||
column: "CreatedAt");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
_ = migrationBuilder.DropTable(
|
||||
name: "Maps");
|
||||
|
||||
_ = migrationBuilder.DropTable(
|
||||
name: "Messages");
|
||||
}
|
||||
}
|
||||
93
src/Forwarder.Data/Migrations/20250620221220_AddedIsOnlineToMaps.Designer.cs
generated
Normal file
93
src/Forwarder.Data/Migrations/20250620221220_AddedIsOnlineToMaps.Designer.cs
generated
Normal file
@@ -0,0 +1,93 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Forwarder.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Forwarder.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20250620221220_AddedIsOnlineToMaps")]
|
||||
partial class AddedIsOnlineToMaps
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Forwarder.Domain.AssetMap", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Abonent")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool?>("IsOnline")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Abonent")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Maps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Forwarder.Domain.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Abonent")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<int>("Code")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Line")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Receiver")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Sector")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Zone")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.ToTable("Messages");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Forwarder.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddedIsOnlineToMaps : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsOnline",
|
||||
table: "Maps",
|
||||
type: "boolean",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsOnline",
|
||||
table: "Maps");
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/Forwarder.Data/Migrations/DataContextModelSnapshot.cs
Normal file
90
src/Forwarder.Data/Migrations/DataContextModelSnapshot.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Forwarder.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Forwarder.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
partial class DataContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Forwarder.Domain.AssetMap", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Abonent")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool?>("IsOnline")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Abonent")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Maps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Forwarder.Domain.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Abonent")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<int>("Code")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Line")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Receiver")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Sector")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Zone")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.ToTable("Messages");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/Forwarder.Domain/AssetMap.cs
Normal file
27
src/Forwarder.Domain/AssetMap.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace Forwarder.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Маппинг внешних идентификаторов объектов на внутренние
|
||||
/// </summary>
|
||||
public class AssetMap
|
||||
{
|
||||
/// <summary>
|
||||
/// Идентификатор
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Внешний идентификатор
|
||||
/// </summary>
|
||||
public string Abonent { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Внутренний идентификатор
|
||||
/// </summary>
|
||||
public Guid? AssetId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Статус подключения объекта к серверу
|
||||
/// </summary>
|
||||
public bool? IsOnline { get; set; }
|
||||
}
|
||||
9
src/Forwarder.Domain/Forwarder.Domain.csproj
Normal file
9
src/Forwarder.Domain/Forwarder.Domain.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
16
src/Forwarder.Domain/Message.cs
Normal file
16
src/Forwarder.Domain/Message.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Forwarder.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Сообщение
|
||||
/// </summary>
|
||||
public record Message(
|
||||
Guid Id,
|
||||
int Receiver,
|
||||
int Line,
|
||||
string Abonent,
|
||||
MessageType Type,
|
||||
int Code,
|
||||
int Sector,
|
||||
int Zone,
|
||||
DateTime CreatedAt
|
||||
);
|
||||
27
src/Forwarder.Domain/MessageType.cs
Normal file
27
src/Forwarder.Domain/MessageType.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace Forwarder.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Тип сообщения
|
||||
/// </summary>
|
||||
public enum MessageType
|
||||
{
|
||||
/// <summary>
|
||||
/// Неизвестное
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// Тревога или снятие с охраны
|
||||
/// </summary>
|
||||
AlarmDisarming,
|
||||
|
||||
/// <summary>
|
||||
/// Восстановление или взятие на охрану
|
||||
/// </summary>
|
||||
RecoveryArming,
|
||||
|
||||
/// <summary>
|
||||
/// Тестовое сообщение
|
||||
/// </summary>
|
||||
StatePing,
|
||||
}
|
||||
11
src/Forwarder.Tests/Forwarder.Tests.csproj
Normal file
11
src/Forwarder.Tests/Forwarder.Tests.csproj
Normal file
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="MSTest.Sdk/3.6.4">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseVSTest>true</UseVSTest>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
1
src/Forwarder.Tests/MSTestSettings.cs
Normal file
1
src/Forwarder.Tests/MSTestSettings.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
|
||||
46
src/Forwarder.Tests/Test1.cs
Normal file
46
src/Forwarder.Tests/Test1.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
namespace Forwarder.Tests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class Test1
|
||||
{
|
||||
[AssemblyInitialize]
|
||||
public static void AssemblyInit(TestContext context)
|
||||
{
|
||||
// This method is called once for the test assembly, before any tests are run.
|
||||
}
|
||||
|
||||
[AssemblyCleanup]
|
||||
public static void AssemblyCleanup()
|
||||
{
|
||||
// This method is called once for the test assembly, after all tests are run.
|
||||
}
|
||||
|
||||
[ClassInitialize]
|
||||
public static void ClassInit(TestContext context)
|
||||
{
|
||||
// This method is called once for the test class, before any tests of the class are run.
|
||||
}
|
||||
|
||||
[ClassCleanup]
|
||||
public static void ClassCleanup()
|
||||
{
|
||||
// This method is called once for the test class, after all tests of the class are run.
|
||||
}
|
||||
|
||||
[TestInitialize]
|
||||
public void TestInit()
|
||||
{
|
||||
// This method is called before each test method.
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void TestCleanup()
|
||||
{
|
||||
// This method is called after each test method.
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestMethod1()
|
||||
{
|
||||
}
|
||||
}
|
||||
14
src/Forwarder/Forwarder/Dockerfile
Normal file
14
src/Forwarder/Forwarder/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN dotnet restore "Forwarder/Forwarder/Forwarder.csproj" && \
|
||||
dotnet publish "Forwarder/Forwarder/Forwarder.csproj" -c $BUILD_CONFIGURATION -o /app/publish --no-restore
|
||||
|
||||
# Финальная стадия
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Forwarder.dll"]
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Forwarder.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Исключение происходит при отсутствии переменной окружения
|
||||
/// </summary>
|
||||
/// <param name="name">Имя переменной окружения</param>
|
||||
public class EnvironmentVariableNotFoundException(string name)
|
||||
: Exception($"Environment variable {name} not found")
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Forwarder.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Исключение, требующее пересоздания
|
||||
/// </summary>
|
||||
public class NeedRecreateException()
|
||||
: Exception("Требуется пересоздание")
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using Forwarder.Features.AssetMapManagement.CreateAssetMap;
|
||||
using Forwarder.Repositories;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Forwarder.Features.AssetMapManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Контроллер для работы с маппингами.
|
||||
/// </summary>
|
||||
[Route("api/asset-maps")]
|
||||
[ApiController]
|
||||
public class AssetMapsController(
|
||||
IAssetMapRepository assetMapRepository
|
||||
) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает список маппингов.
|
||||
/// </summary>
|
||||
/// <param name="request">Запрос</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
/// <returns>Список маппингов.</returns>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetList(
|
||||
[FromQuery] GetMapListRequest request,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var query = assetMapRepository
|
||||
.GetQuery();
|
||||
|
||||
if (request.UseOnlyEmptyAssetId == true)
|
||||
{
|
||||
query = query
|
||||
.Where(x => !x.AssetId.HasValue);
|
||||
}
|
||||
|
||||
var result = await assetMapRepository
|
||||
.GetTable(query, request, cancellationToken);
|
||||
if (result.IsFail)
|
||||
{
|
||||
return BadRequest(result.Error);
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает маппинг по идентификатору.
|
||||
/// </summary>
|
||||
/// <param name="id">Идентификатор.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
/// <returns>Маппинг.</returns>
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> Get(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await assetMapRepository.Get(id, cancellationToken);
|
||||
if (result.IsFail)
|
||||
{
|
||||
return BadRequest(result.Error);
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create(
|
||||
[FromBody] CreateAssetMapRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await assetMapRepository.AddAsync(request.Abonent, request.AssetId, cancellationToken);
|
||||
if (result.IsFail)
|
||||
{
|
||||
return BadRequest(result.Error);
|
||||
}
|
||||
|
||||
return Created($"/api/asset-maps/{result.Value.Id}", result.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обновляет маппинг.
|
||||
/// </summary>
|
||||
/// <param name="id">Идентификатор.</param>
|
||||
/// <param name="request">Запрос.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
/// <returns></returns>
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<IActionResult> Update(Guid id,
|
||||
[FromBody] UpdateAssetMapRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await assetMapRepository.UpdateAsync(id, request.AssetId, cancellationToken);
|
||||
if (result.IsFail)
|
||||
{
|
||||
return BadRequest(result.Error);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет маппинг.
|
||||
/// </summary>
|
||||
/// <param name="id">Идентификатор.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
/// <returns></returns>
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await assetMapRepository.DeleteAsync(id, cancellationToken);
|
||||
if (result.IsFail)
|
||||
{
|
||||
return BadRequest(result.Error);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Forwarder.Features.AssetMapManagement.CreateAssetMap;
|
||||
|
||||
/// <summary>
|
||||
/// Запрос на создание маппинга между объектом и абонентом.
|
||||
/// </summary>
|
||||
/// <param name="Abonent">Абонент.</param>
|
||||
/// <param name="AssetId">Идентификатор объекта.</param>
|
||||
public record CreateAssetMapRequest(
|
||||
string Abonent,
|
||||
Guid? AssetId
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Forwarder.Features.AssetMapManagement.CreateAssetMap;
|
||||
|
||||
/// <summary>
|
||||
/// Ответ на создание маппинга.
|
||||
/// </summary>
|
||||
/// <param name="Id">Идентификатор маппинга.</param>
|
||||
public record CreateAssetMapResponse(
|
||||
Guid Id
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace Forwarder.Features.AssetMapManagement.CreateAssetMap;
|
||||
|
||||
public class CreateAssetMapValidator : AbstractValidator<CreateAssetMapRequest>
|
||||
{
|
||||
public CreateAssetMapValidator()
|
||||
{
|
||||
RuleFor(x => x.Abonent)
|
||||
.NotNull()
|
||||
.NotEmpty()
|
||||
.Matches(@"^\d{8}$").WithMessage("Abonent must be 8 digits.");
|
||||
|
||||
RuleFor(x => x.AssetId)
|
||||
.NotEmpty().When(x => x.AssetId.HasValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Forwarder.Models;
|
||||
|
||||
namespace Forwarder.Features.AssetMapManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Запрос для получения списка маппингов.
|
||||
/// </summary>
|
||||
/// <param name="Skip">Смещение.</param>
|
||||
/// <param name="Take">Кол-во.</param>
|
||||
/// <param name="UseOnlyEmptyAssetId">Выбрать только маппинги с пустым assetId.</param>
|
||||
public record GetMapListRequest(
|
||||
int? Skip,
|
||||
int? Take,
|
||||
bool? UseOnlyEmptyAssetId
|
||||
) : ITableStateRequest;
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Forwarder.Features.AssetMapManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Запрос для обновления маппинга.
|
||||
/// </summary>
|
||||
/// <param name="AssetId">Идентификатор объекта.</param>
|
||||
public record UpdateAssetMapRequest(
|
||||
Guid? AssetId
|
||||
);
|
||||
45
src/Forwarder/Forwarder/Forwarder.csproj
Normal file
45
src/Forwarder/Forwarder/Forwarder.csproj
Normal file
@@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.5.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.*" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.*" />
|
||||
<PackageReference Include="NSwag.MSBuild" Version="14.4.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Polly" Version="8.6.0" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.4.13" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.*" />
|
||||
<PackageReference Include="Serilog.Enrichers.CallerInfo" Version="1.0.6" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.11.0.117924">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.*">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.7" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.15.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Common.Api\Common.Api.csproj" />
|
||||
<ProjectReference Include="..\..\Common.Postgres\Common.Postgres.csproj" />
|
||||
<ProjectReference Include="..\..\Forwarder.Data\Forwarder.Data.csproj" />
|
||||
<ProjectReference Include="..\..\Monitoring\Monitoring.Common\Monitoring.Common.csproj" />
|
||||
<ProjectReference Include="..\..\Monitoring\Monitoring.Contracts\Monitoring.Contracts.csproj" />
|
||||
<ProjectReference Include="..\..\Monitoring\Monitoring.ServiceDefaults\Monitoring.ServiceDefaults.csproj" />
|
||||
<ProjectReference Include="..\..\Telegram\TelegramSender\TelegramSender.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
17
src/Forwarder/Forwarder/HttpClients.cs
Normal file
17
src/Forwarder/Forwarder/HttpClients.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Forwarder;
|
||||
|
||||
/// <summary>
|
||||
/// Клиенты HTTP.
|
||||
/// </summary>
|
||||
public static class HttpClients
|
||||
{
|
||||
/// <summary>
|
||||
/// Клиент API.
|
||||
/// </summary>
|
||||
public const string Api = "api";
|
||||
|
||||
/// <summary>
|
||||
/// Клиент Geo RITM API.
|
||||
/// </summary>
|
||||
public const string GeoRitm = "geo-ritm";
|
||||
}
|
||||
31
src/Forwarder/Forwarder/Models/ForwarderOptions.cs
Normal file
31
src/Forwarder/Forwarder/Models/ForwarderOptions.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace Forwarder.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Настройки передатчика.
|
||||
/// </summary>
|
||||
public sealed class ForwarderOptions
|
||||
{
|
||||
[ConfigurationKeyName("TCP_PORT")]
|
||||
public ushort TcpPort { get; init; }
|
||||
|
||||
[ConfigurationKeyName("UDP_PORT")]
|
||||
public ushort UdpPort { get; init; }
|
||||
|
||||
[ConfigurationKeyName("MESSAGE_BULK_SIZE")]
|
||||
public int MessageBulkSize { get; init; }
|
||||
|
||||
[ConfigurationKeyName("API_TOKEN")]
|
||||
public string ApiToken { get; init; } = string.Empty;
|
||||
|
||||
[ConfigurationKeyName("API_URL")]
|
||||
public string ApiUrl { get; init; } = string.Empty;
|
||||
|
||||
[ConfigurationKeyName("SENDING_INTERVAL_IN_SECONDS")]
|
||||
public int SendingIntervalInSeconds { get; init; }
|
||||
|
||||
[ConfigurationKeyName("ENABLE_TEST_MESSAGE_SENDING")]
|
||||
public bool EnableTestMessageSending { get; init; } = true;
|
||||
|
||||
[ConfigurationKeyName("TEST_MESSAGE_EVENT_CODE")]
|
||||
public int TestMessageEventCode { get; init; } = 6011;
|
||||
}
|
||||
25
src/Forwarder/Forwarder/Models/GeoRitmConnectionOptions.cs
Normal file
25
src/Forwarder/Forwarder/Models/GeoRitmConnectionOptions.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace Forwarder.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Настройки подключения к geo-ritm.
|
||||
/// </summary>
|
||||
public sealed class GeoRitmConnectionOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// URL для подключения к geo-ritm.
|
||||
/// </summary>
|
||||
[ConfigurationKeyName("GEO_RITM_API_URL")]
|
||||
public string Url { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Имя пользователя для подключения к geo-ritm.
|
||||
/// </summary>
|
||||
[ConfigurationKeyName("GEO_RITM_USERNAME")]
|
||||
public string Username { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Пароль для подключения к geo-ritm.
|
||||
/// </summary>
|
||||
[ConfigurationKeyName("GEO_RITM_PASSWORD")]
|
||||
public string Password { get; init; }
|
||||
}
|
||||
134
src/Forwarder/Forwarder/Models/GetObjectListResponse.cs
Normal file
134
src/Forwarder/Forwarder/Models/GetObjectListResponse.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
namespace Forwarder.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Модель объекта.
|
||||
/// </summary>
|
||||
public class GetObjectListResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Страна, в которой находится объект
|
||||
/// </summary>
|
||||
public string Country { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Ревизия объекта (может быть null)
|
||||
/// </summary>
|
||||
public object? Rev { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Состояние объекта
|
||||
/// </summary>
|
||||
public ObjectState ObjectState { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Флаг, указывающий, что объект из IDP
|
||||
/// </summary>
|
||||
public bool IsFromIdp { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Список устройств объекта
|
||||
/// </summary>
|
||||
public List<Device> Devices { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Город, в котором находится объект
|
||||
/// </summary>
|
||||
public string City { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Флаг онлайн-статуса GSM
|
||||
/// </summary>
|
||||
public bool IsGsmOnline { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Долгота местоположения объекта
|
||||
/// </summary>
|
||||
public double Lon { get; set; } = 0.0;
|
||||
|
||||
/// <summary>
|
||||
/// Статус объекта
|
||||
/// </summary>
|
||||
public int ObjStatus { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Наименование объекта
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// IMEI устройства
|
||||
/// </summary>
|
||||
public string Imei { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Краткий адрес объекта
|
||||
/// </summary>
|
||||
public string AddressShort { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Идентификатор объекта
|
||||
/// </summary>
|
||||
public int Id { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Внешний идентификатор объекта
|
||||
/// </summary>
|
||||
public int ExtId { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Регион, в котором находится объект
|
||||
/// </summary>
|
||||
public string Region { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Тип объекта
|
||||
/// </summary>
|
||||
public string ObjType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Широта местоположения объекта
|
||||
/// </summary>
|
||||
public double Lat { get; set; } = 0.0;
|
||||
|
||||
/// <summary>
|
||||
/// Флаг онлайн-статуса
|
||||
/// </summary>
|
||||
public bool IsOnline => ObjectState.IsOnline == 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Состояние объекта.
|
||||
/// </summary>
|
||||
public class ObjectState
|
||||
{
|
||||
/// <summary>
|
||||
/// Флаг наличия неисправности (0 - нет, 1 - есть)
|
||||
/// </summary>
|
||||
public int HasFault { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Флаг онлайн-статуса (0 - оффлайн, 1 - онлайн)
|
||||
/// </summary>
|
||||
public int IsOnline { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Флаг охраны (0 - не охраняется, 1 - охраняется)
|
||||
/// </summary>
|
||||
public int IsGuarded { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Флаг тревоги (0 - нет тревоги, 1 - тревога)
|
||||
/// </summary>
|
||||
public int HasAlarm { get; set; } = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Устройство объекта.
|
||||
/// </summary>
|
||||
public class Device
|
||||
{
|
||||
/// <summary>
|
||||
/// IMEI номер устройства
|
||||
/// </summary>
|
||||
public string Imei { get; set; } = string.Empty;
|
||||
}
|
||||
18
src/Forwarder/Forwarder/Models/ITableStateResponse.cs
Normal file
18
src/Forwarder/Forwarder/Models/ITableStateResponse.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Forwarder.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Табличные данные.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Тип данных.</typeparam>
|
||||
public interface ITableStateResponse<out T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Список элементов.
|
||||
/// </summary>
|
||||
IEnumerable<T> Items { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Общее кол-во элементов.
|
||||
/// </summary>
|
||||
int Total { get; }
|
||||
}
|
||||
151
src/Forwarder/Forwarder/Models/Result.cs
Normal file
151
src/Forwarder/Forwarder/Models/Result.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
namespace Forwarder.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Результат выполнения операции.
|
||||
/// </summary>
|
||||
public readonly struct Result
|
||||
{
|
||||
/// <summary>
|
||||
/// Выполнена ли операция неуспешно.
|
||||
/// </summary>
|
||||
public bool IsFail { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Выполнена ли операция успешно.
|
||||
/// </summary>
|
||||
public bool IsSuccess => !IsFail;
|
||||
|
||||
/// <summary>
|
||||
/// Ошибка.
|
||||
/// </summary>
|
||||
public string Error { get; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Создает результат с ошибкой.
|
||||
/// </summary>
|
||||
/// <param name="error">Ошибка.</param>
|
||||
/// <returns>Результат.</returns>
|
||||
public static Result Fail(string error)
|
||||
{
|
||||
return new Result(error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает успешный результат.
|
||||
/// </summary>
|
||||
public static Result Success()
|
||||
{
|
||||
return new Result();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает неуспешный результат с ошибкой.
|
||||
/// </summary>
|
||||
/// <param name="error">Ошибка.</param>
|
||||
public Result(string error)
|
||||
{
|
||||
Error = error;
|
||||
IsFail = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает успешный результат.
|
||||
/// </summary>
|
||||
public Result()
|
||||
{
|
||||
IsFail = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает неуспешный результат с ошибкой.
|
||||
/// </summary>
|
||||
/// <param name="error">Ошибка.</param>
|
||||
public static implicit operator Result(string error)
|
||||
{
|
||||
return Fail(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Результат выполнения операции с данными.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Тип данных.</typeparam>
|
||||
public readonly struct Result<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Выполнена ли операция неуспешно.
|
||||
/// </summary>
|
||||
public bool IsFail { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Выполнена ли операция успешно.
|
||||
/// </summary>
|
||||
public bool IsSuccess => !IsFail;
|
||||
|
||||
/// <summary>
|
||||
/// Ошибка.
|
||||
/// </summary>
|
||||
public string Error { get; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Создает результат с ошибкой.
|
||||
/// </summary>
|
||||
/// <param name="error">Ошибка.</param>
|
||||
/// <returns>Результат.</returns>
|
||||
public static Result<T> Fail(string error)
|
||||
{
|
||||
return new Result<T>(error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Значение.
|
||||
/// </summary>
|
||||
public T Value { get; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Создает неуспешный результат с ошибкой.
|
||||
/// </summary>
|
||||
/// <param name="error">Ошибка.</param>
|
||||
public Result(string error)
|
||||
{
|
||||
Error = error;
|
||||
IsFail = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает успешный результат.
|
||||
/// </summary>
|
||||
/// <param name="value">Значение.</param>
|
||||
private Result(T value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает успешный результат.
|
||||
/// </summary>
|
||||
/// <param name="value">Значение.</param>
|
||||
/// <returns>Результат.</returns>
|
||||
public static Result<T> Success(T value)
|
||||
{
|
||||
return new Result<T>(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает неуспешный результат с ошибкой.
|
||||
/// </summary>
|
||||
/// <param name="error">Ошибка.</param>
|
||||
public static implicit operator Result<T>(string error)
|
||||
{
|
||||
return Fail(error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает успешный результат.
|
||||
/// </summary>
|
||||
/// <param name="value">Ошибка.</param>
|
||||
public static implicit operator Result<T>(T value)
|
||||
{
|
||||
return Success(value);
|
||||
}
|
||||
}
|
||||
17
src/Forwarder/Forwarder/Models/TableStateRequest.cs
Normal file
17
src/Forwarder/Forwarder/Models/TableStateRequest.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Forwarder.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Запрос табличных данных.
|
||||
/// </summary>
|
||||
public interface ITableStateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Кол-во записей для отображения.
|
||||
/// </summary>
|
||||
int? Take { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Кол-во записей для пропуска.
|
||||
/// </summary>
|
||||
int? Skip { get; }
|
||||
}
|
||||
6
src/Forwarder/Forwarder/Models/TableStateResponse.cs
Normal file
6
src/Forwarder/Forwarder/Models/TableStateResponse.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Forwarder.Models;
|
||||
|
||||
public record TableStateResponse<T>(
|
||||
IEnumerable<T> Items,
|
||||
int Total
|
||||
) : ITableStateResponse<T>;
|
||||
16
src/Forwarder/Forwarder/Models/TelegramSendOptions.cs
Normal file
16
src/Forwarder/Forwarder/Models/TelegramSendOptions.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Forwarder.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Настройки Telegram.
|
||||
/// </summary>
|
||||
public class TelegramSendOptions
|
||||
{
|
||||
[ConfigurationKeyName("TELEGRAM_BOT_TOKEN")]
|
||||
public string Token { get; set; } = string.Empty;
|
||||
|
||||
[ConfigurationKeyName("TELEGRAM_BATCH_SIZE")]
|
||||
public uint BatchSize { get; set; } = 100;
|
||||
|
||||
[ConfigurationKeyName("TELEGRAM_CHAT_ID")]
|
||||
public string ChatId { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using CSharpFunctionalExtensions;
|
||||
using Forwarder.Domain;
|
||||
using Serilog;
|
||||
using Serilog.Filters;
|
||||
|
||||
namespace Forwarder.Modules.Ademco;
|
||||
|
||||
internal partial class Ademco685MessageParser
|
||||
{
|
||||
public record Output(bool IsTest, Message? Message);
|
||||
|
||||
private readonly Regex _messageTemplate = RealMessagePattern();
|
||||
private readonly Regex _testTemplate = TestMessagePattern();
|
||||
|
||||
public Result<Output> Parse(string input)
|
||||
{
|
||||
// [LF]RLsAAAAs18sQXYZsGGsUCCC[CR]
|
||||
if (_testTemplate.IsMatch(input))
|
||||
{
|
||||
return Result.Success(new Output(true, null));
|
||||
}
|
||||
|
||||
var matching = _messageTemplate.Match(input);
|
||||
if (!matching.Success)
|
||||
{
|
||||
return Result.Failure<Output>("Not matched");
|
||||
}
|
||||
|
||||
return Result.Success(
|
||||
new Output(
|
||||
false,
|
||||
new Message(
|
||||
Guid.CreateVersion7(),
|
||||
int.Parse(matching.Groups["receiver"].Value),
|
||||
int.Parse(matching.Groups["line"].Value),
|
||||
ConvertAbonent(matching.Groups["abonent"].Value),
|
||||
//$"1{matching.Groups["abonent"].Value:D15}",
|
||||
GetMessageType(matching.Groups["type"].Value),
|
||||
int.Parse(matching.Groups["code"].Value),
|
||||
int.Parse(matching.Groups["area"].Value),
|
||||
int.Parse(matching.Groups["zone"].Value),
|
||||
DateTime.UtcNow
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static string ConvertAbonent(string value)
|
||||
{
|
||||
var hex = int.Parse(value, NumberStyles.HexNumber);
|
||||
Log.Information("Abonent: 0x{Hex} ({Decimal})", hex, value);
|
||||
return $"{hex:D8}";
|
||||
}
|
||||
|
||||
private static MessageType GetMessageType(string s)
|
||||
{
|
||||
return s switch
|
||||
{
|
||||
"E" => MessageType.AlarmDisarming,
|
||||
"R" => MessageType.RecoveryArming,
|
||||
"P" => MessageType.StatePing,
|
||||
_ => MessageType.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
[GeneratedRegex(
|
||||
@"(?<receiver>\d{1})(?<line>\d{1})\s{1}(?<abonent>\d{4})\s{1}18\s{1}(?<type>\w{1})(?<code>\d{3})\s{1}(?<area>\d{2})\s{1}(\w{1})(?<zone>\d{3})"
|
||||
)]
|
||||
private static partial Regex RealMessagePattern();
|
||||
|
||||
[GeneratedRegex(@"(\d{2})\s{1}OKAY\s{1}@")]
|
||||
private static partial Regex TestMessagePattern();
|
||||
}
|
||||
55
src/Forwarder/Forwarder/Modules/Ademco/AdemcoHost.cs
Normal file
55
src/Forwarder/Forwarder/Modules/Ademco/AdemcoHost.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Forwarder.Modules.Ademco;
|
||||
|
||||
/// <summary>
|
||||
/// Менеджер слушателей.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">Сервис-провайдер.</param>
|
||||
internal sealed class AdemcoHost(
|
||||
IServiceProvider serviceProvider)
|
||||
: BackgroundService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
|
||||
var options = scope.ServiceProvider.GetRequiredService<IOptions<AdemcoOptions>>().Value;
|
||||
|
||||
var taskList = new List<Task>();
|
||||
|
||||
var posts = options.Ports.Split(',')
|
||||
.Select(x => ushort.Parse(x.Trim()));
|
||||
|
||||
foreach (var port in posts)
|
||||
{
|
||||
taskList.Add(this.RunListener(port, stoppingToken));
|
||||
}
|
||||
|
||||
await Task.WhenAll(taskList);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunListener(ushort port, CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
|
||||
Log.Information("Create listener for port {Port}", port);
|
||||
|
||||
var listener = scope.ServiceProvider.GetRequiredService<UdpPortListener>();
|
||||
await listener.Start(port, stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Manager execution error when listener running");
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Forwarder/Forwarder/Modules/Ademco/AdemcoOptions.cs
Normal file
13
src/Forwarder/Forwarder/Modules/Ademco/AdemcoOptions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Forwarder.Modules.Ademco;
|
||||
|
||||
/// <summary>
|
||||
/// Параметры запуска для Ademco.
|
||||
/// </summary>
|
||||
public class AdemcoOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Порты, через запятую.
|
||||
/// </summary>
|
||||
[ConfigurationKeyName("ADEMCO_PORTS")]
|
||||
public string Ports { get; set; } = string.Empty;
|
||||
}
|
||||
7
src/Forwarder/Forwarder/Modules/Ademco/DataReadResult.cs
Normal file
7
src/Forwarder/Forwarder/Modules/Ademco/DataReadResult.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using System.Net;
|
||||
|
||||
namespace Forwarder.Modules.Ademco;
|
||||
internal sealed record DataReadResult(
|
||||
IPEndPoint EndPoint,
|
||||
byte[] Data
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Forwarder.Modules.Ademco;
|
||||
|
||||
public static class DependencyInjectionExtensions
|
||||
{
|
||||
public static IHostApplicationBuilder AddAdemcoModule(this IHostApplicationBuilder builder)
|
||||
{
|
||||
builder.Services
|
||||
.Configure<AdemcoOptions>(builder.Configuration)
|
||||
.AddScoped<Ademco685MessageParser>()
|
||||
.AddScoped<UdpPortListener>()
|
||||
.AddHostedService<AdemcoHost>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
116
src/Forwarder/Forwarder/Modules/Ademco/UdpPortListener.cs
Normal file
116
src/Forwarder/Forwarder/Modules/Ademco/UdpPortListener.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
// <copyright file="UdpPortListener.cs" company="IGORock Software">
|
||||
// Copyright (c) IGORock Software. All rights reserved.
|
||||
// </copyright>
|
||||
// <author>Igor Kozlov</author>
|
||||
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using Forwarder.Data;
|
||||
using Forwarder.Repositories;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Forwarder.Modules.Ademco;
|
||||
|
||||
/// <summary>
|
||||
/// Слушает UDP порт.
|
||||
/// </summary>
|
||||
internal sealed class UdpPortListener(
|
||||
ILogger<UdpPortListener> logger,
|
||||
Ademco685MessageParser parser,
|
||||
IMessageRepository messageRepository)
|
||||
{
|
||||
/// <summary>
|
||||
/// Начинает слушать UDP порт.
|
||||
/// </summary>
|
||||
/// <param name="port">Порт.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
/// <returns>Задача.</returns>
|
||||
public async Task Start(ushort port, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation("Start listener for port {Port}", port);
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var udpTransfer = new UdpReader(port);
|
||||
|
||||
await Listen(udpTransfer, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error in Listener.Start()");
|
||||
}
|
||||
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Listen(UdpReader udpTransfer, CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var result = await udpTransfer.Read(cancellationToken);
|
||||
if (result.IsFailure)
|
||||
{
|
||||
Log.Error("Read UDP data is failure: {Error}", result.Error);
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
await OnDataReadAsync(result.Value, udpTransfer, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnDataReadAsync(
|
||||
DataReadResult input,
|
||||
UdpReader udpReader,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var data = input.Data;
|
||||
|
||||
var sourceString = Encoding.ASCII.GetString(data, 0, data.Length);
|
||||
Log.Information(
|
||||
"Ademco [{Port}]: {Address} -> {Message}",
|
||||
udpReader.Port,
|
||||
input.EndPoint,
|
||||
sourceString.Replace("\n", "[LF]").Replace("\r", "[CR]")
|
||||
);
|
||||
|
||||
udpReader.Write([0x06], input.EndPoint);
|
||||
Log.Information("Reply the 0x06 answer");
|
||||
|
||||
var parsed = parser.Parse(sourceString);
|
||||
|
||||
// если сообщение тестовое - возвращаем ответ
|
||||
if (parsed.IsFailure)
|
||||
{
|
||||
Log.Information("Ademco: Message fail to parse.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.Value.IsTest)
|
||||
{
|
||||
Log.Information("Ademco: Test message received.");
|
||||
return;
|
||||
}
|
||||
|
||||
// если сообщение нормальное - пересылаем в тсп
|
||||
var message = parsed.Value.Message
|
||||
?? throw new InvalidOperationException("Parsed message is null");
|
||||
|
||||
Log.Information("Message: {Message}", JsonSerializer.Serialize(message));
|
||||
var addMessageResult = await messageRepository.AddAsync(message, cancellationToken);
|
||||
if (addMessageResult.IsFail)
|
||||
{
|
||||
Log.Error("Error by message add: {Error}", addMessageResult.Error);
|
||||
}
|
||||
|
||||
Log.Information("The message: {Message} saved in database", sourceString);
|
||||
}
|
||||
}
|
||||
73
src/Forwarder/Forwarder/Modules/Ademco/UdpReader.cs
Normal file
73
src/Forwarder/Forwarder/Modules/Ademco/UdpReader.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
// <copyright file="UdpReader.cs" company="IGORock Software">
|
||||
// Copyright (c) IGORock Software. All rights reserved.
|
||||
// </copyright>
|
||||
// <author>Igor Kozlov</author>
|
||||
|
||||
namespace Forwarder.Modules.Ademco;
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
using CSharpFunctionalExtensions;
|
||||
|
||||
using Serilog;
|
||||
|
||||
/// <summary>
|
||||
/// Читает сообщения из UDP порта.
|
||||
/// </summary>
|
||||
internal sealed class UdpReader : IDisposable
|
||||
{
|
||||
private readonly UdpClient _client;
|
||||
|
||||
/// <summary>
|
||||
/// Прослушиваемый порт.
|
||||
/// </summary>
|
||||
public ushort Port { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UdpReader"/> class.
|
||||
/// </summary>
|
||||
/// <param name="port">Порт прослушивания.</param>
|
||||
public UdpReader(ushort port)
|
||||
{
|
||||
_client = new UdpClient(port);
|
||||
_client.Client.ReceiveTimeout = 10_000;
|
||||
_client.Client.SendTimeout = 10_000;
|
||||
Port = port;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Читает сообщение из UDP порта.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
/// <returns>Результат чтения.</returns>
|
||||
public async Task<Result<DataReadResult>> Read(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = await _client.ReceiveAsync(cancellationToken);
|
||||
return Result.Success(new DataReadResult(data.RemoteEndPoint, data.Buffer));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "UdpReader reading error");
|
||||
return Result.Failure<DataReadResult>(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отправляет сообщение в UDP порт.
|
||||
/// </summary>
|
||||
/// <param name="data">Данные.</param>
|
||||
/// <param name="endPoint">Конечная точка.</param>
|
||||
public void Write(byte[] data, IPEndPoint endPoint)
|
||||
{
|
||||
_ = _client.Send(data, data.Length, endPoint);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_client?.Dispose();
|
||||
}
|
||||
}
|
||||
187
src/Forwarder/Forwarder/Program.cs
Normal file
187
src/Forwarder/Forwarder/Program.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
using FluentValidation;
|
||||
|
||||
using Forwarder;
|
||||
using Forwarder.Data;
|
||||
using Forwarder.Exceptions;
|
||||
using Forwarder.Models;
|
||||
using Forwarder.Modules.Ademco;
|
||||
using Forwarder.Repositories;
|
||||
using Forwarder.Repositories.Implementations;
|
||||
using Forwarder.Services;
|
||||
using Forwarder.Services.Implementations;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Monitoring.Common;
|
||||
|
||||
using Polly;
|
||||
|
||||
using Serilog;
|
||||
using Serilog.Enrichers.CallerInfo;
|
||||
|
||||
using TelegramSender;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddControllers();
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithCallerInfo(
|
||||
includeFileInfo: true,
|
||||
assemblyPrefix: "forwarder_")
|
||||
.MinimumLevel.Information()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
builder.Host.UseSerilog();
|
||||
|
||||
builder.Logging.SetMinimumLevel(LogLevel.Debug);
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(builder =>
|
||||
{
|
||||
_ = builder.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
if (EnvironmentVariables.Has("DATABASE_ASPIRE_NAME"))
|
||||
{
|
||||
var connectionName = EnvironmentVariables.Get("DATABASE_ASPIRE_NAME");
|
||||
builder.AddNpgsqlDbContext<DataContext>(connectionName: connectionName);
|
||||
builder.Services.AddScoped<IDataContext, DataContext>(serviceProvider => serviceProvider.GetRequiredService<DataContext>());
|
||||
}
|
||||
else
|
||||
{
|
||||
var connectionString = EnvironmentVariables.Get("DATABASE_CONNECTION_STRING");
|
||||
builder.Services.AddDbContext<IDataContext, DataContext>(options =>
|
||||
{
|
||||
_ = options.UseNpgsql(connectionString)
|
||||
.UseLoggerFactory(LoggerFactory.Create(x => x.SetMinimumLevel(LogLevel.Warning)));
|
||||
_ = options.EnableSensitiveDataLogging(false);
|
||||
});
|
||||
}
|
||||
|
||||
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
|
||||
|
||||
var pollyPolicy = GetRetryPolicy();
|
||||
|
||||
builder.Services.AddHttpClient(HttpClients.Api, (serviceProvider, client) =>
|
||||
{
|
||||
var options = serviceProvider.GetRequiredService<IOptions<ForwarderOptions>>().Value;
|
||||
client.BaseAddress = new Uri(options.ApiUrl);
|
||||
client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory");
|
||||
})
|
||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
|
||||
.AddPolicyHandler(pollyPolicy);
|
||||
|
||||
builder.Services.Configure<GeoRitmConnectionOptions>(builder.Configuration);
|
||||
builder.Services.AddHttpClient(HttpClients.GeoRitm, (sp, client) =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<GeoRitmConnectionOptions>>().Value;
|
||||
client.BaseAddress = new Uri(options.Url);
|
||||
client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory");
|
||||
var scope = sp.CreateScope();
|
||||
var basicAuthService = scope.ServiceProvider.GetRequiredService<IBasicAuthService>();
|
||||
client.DefaultRequestHeaders.Add(
|
||||
"Authorization",
|
||||
basicAuthService.GetBasicAuth(options.Username, options.Password));
|
||||
})
|
||||
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
|
||||
.AddPolicyHandler(pollyPolicy);
|
||||
|
||||
builder.Services.Configure<ForwarderOptions>(builder.Configuration);
|
||||
|
||||
builder.Services.AddHostedService<TcpWatcher>();
|
||||
builder.Services.AddHostedService<UdpWatcher>();
|
||||
builder.Services.AddHostedService<SendJob>();
|
||||
// builder.Services.AddHostedService<OnlineStatusJob>();
|
||||
|
||||
// Репозитории
|
||||
builder.Services.AddScoped<IMessageRepository, MessageRepository>();
|
||||
builder.Services.AddScoped<IAssetMapRepository, AssetMapRepository>();
|
||||
builder.Services.AddScoped<ICommonRepository, CommonRepository>();
|
||||
|
||||
// Отправители
|
||||
builder.Services.AddScoped<ISendService, SendService>();
|
||||
builder.Services.AddScoped<IGeoRitmClient, GeoRitmClient>();
|
||||
builder.Services.AddScoped<IBasicAuthService, BasicAuthService>();
|
||||
|
||||
// Протоколы
|
||||
builder.Services.AddScoped<IMessageConverterService, MessageConverterService>();
|
||||
|
||||
// Уведомления для пользователей
|
||||
builder.Services.Configure<TelegramSendOptions>(builder.Configuration);
|
||||
if (EnvironmentVariables.Has("TELEGRAM_BOT_TOKEN"))
|
||||
{
|
||||
builder.Services.AddTelegramSender(
|
||||
GetEnvironmentVariable("TELEGRAM_BOT_TOKEN"),
|
||||
int.Parse(GetEnvironmentVariable("TELEGRAM_BATCH_SIZE")),
|
||||
long.Parse(GetEnvironmentVariable("TELEGRAM_CHAT_ID")));
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Services.AddTelegramSender(string.Empty, default, default);
|
||||
}
|
||||
// Ademco
|
||||
builder.AddAdemcoModule();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseSerilogRequestLogging();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
_ = app.MapOpenApi();
|
||||
//_ = app.MapScalarApiReference()
|
||||
// .WithName("Forwarder API");
|
||||
}
|
||||
|
||||
app.UseRouting();
|
||||
app.UseCors();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.MapDefaultEndpoints();
|
||||
|
||||
await ApplyMigrations(app);
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
static async Task ApplyMigrations(WebApplication app)
|
||||
{
|
||||
var context = app.Services.CreateScope().ServiceProvider.GetRequiredService<DataContext>();
|
||||
await context.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
|
||||
{
|
||||
return Policy
|
||||
.Handle<HttpRequestException>()
|
||||
.WaitAndRetryAsync(
|
||||
retryCount: 10,
|
||||
sleepDurationProvider: _ => TimeSpan.FromSeconds(1),
|
||||
onRetry: (exception, timeSpan, attempt, context) =>
|
||||
{
|
||||
Log.Warning("Request failed #{Attempt}. Waiting {TimeSpan} before next retry. Retry due to: {Exception}.", attempt, timeSpan, exception.Message);
|
||||
})
|
||||
.AsAsyncPolicy<HttpResponseMessage>();
|
||||
}
|
||||
|
||||
static string GetEnvironmentVariable(string name)
|
||||
{
|
||||
return Environment.GetEnvironmentVariable(name)
|
||||
?? throw new EnvironmentVariableNotFoundException(name);
|
||||
}
|
||||
22
src/Forwarder/Forwarder/Properties/launchSettings.json
Normal file
22
src/Forwarder/Forwarder/Properties/launchSettings.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "scalar/v1",
|
||||
"applicationUrl": "http://127.0.0.1:5010",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DATABASE_CONNECTION_STRING": "***",
|
||||
"API_URL": "http://localhost:5001",
|
||||
"API_TOKEN": "***",
|
||||
"TCP_PORT": "10000",
|
||||
"UDP_PORT": "10001",
|
||||
"MESSAGE_BULK_SIZE": "100",
|
||||
"SENDING_INTERVAL_IN_SECONDS": "10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/Forwarder/Forwarder/Repositories/IAssetMapRepository.cs
Normal file
82
src/Forwarder/Forwarder/Repositories/IAssetMapRepository.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Models;
|
||||
|
||||
namespace Forwarder.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Интерфейс репозитория для работы с маппингом идентификаторов объектов.
|
||||
/// </summary>
|
||||
public interface IAssetMapRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Добавляет маппинг в репозиторий.
|
||||
/// </summary>
|
||||
/// <param name="abonent"> Абонент. </param>
|
||||
/// <param name="assetId"> Идентификатор объекта. </param>
|
||||
/// <param name="cancellationToken"> Токен отмены. </param>
|
||||
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
|
||||
Task<Result<AssetMap>> AddAsync(string abonent, Guid? assetId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Получает маппинги со обязательными заполненными идентификаторами объектов по списку абонентов.
|
||||
/// Если маппинги не существуют, то создает их.
|
||||
/// </summary>
|
||||
/// <param name="abonentList">Список абонентов.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
Task<Result<List<AssetMap>>> GetRequiredMapsAndCreateIsNewAsync(IEnumerable<string> abonentList, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает страницу таблицы с маппингами.
|
||||
/// </summary>
|
||||
/// <param name="request">Запрос.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
Task<Result<ITableStateResponse<AssetMap>>> GetTable(ITableStateRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает страницу таблицы с маппингами.
|
||||
/// </summary>
|
||||
/// <param name="query">Запрос на выборку.</param>
|
||||
/// <param name="request">Запрос.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
Task<Result<ITableStateResponse<AssetMap>>> GetTable(IQueryable<AssetMap> query, ITableStateRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Обновляет маппинг.
|
||||
/// </summary>
|
||||
/// <param name="id">Идентификатор.</param>
|
||||
/// <param name="assetId">Идентификатор объекта.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
Task<Result> UpdateAsync(Guid id, Guid? assetId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Обновляет статус онлайн.
|
||||
/// </summary>
|
||||
/// <param name="id">Идентификатор.</param>
|
||||
/// <param name="isOnline">Статус онлайн.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
Task<Result> UpdateIsOnlineStatusAsync(
|
||||
Guid id,
|
||||
bool isOnline,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает маппинг по идентификатору.
|
||||
/// </summary>
|
||||
/// <param name="id">Идентификатор.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
Task<Result<AssetMap>> Get(Guid id, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает запрос на получение маппингов.
|
||||
/// </summary>
|
||||
IQueryable<AssetMap> GetQuery();
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет маппинг.
|
||||
/// </summary>
|
||||
/// <param name="id">Идентификатор.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
/// <returns>Результат удаления.</returns>
|
||||
/// <remarks>Если удаление прошло успешно, возвращает Result без значения.</remarks>
|
||||
Task<Result> DeleteAsync(Guid id, CancellationToken cancellationToken);
|
||||
}
|
||||
13
src/Forwarder/Forwarder/Repositories/ICommonRepository.cs
Normal file
13
src/Forwarder/Forwarder/Repositories/ICommonRepository.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Models;
|
||||
|
||||
namespace Forwarder.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Интерфейс репозитория для работы с маппингом идентификаторов объектов и с сообщениями.
|
||||
/// </summary>
|
||||
public interface ICommonRepository
|
||||
{
|
||||
Task<Result<IEnumerable<Message>>> GetFirstListWhereMapReady(
|
||||
int takeCount, CancellationToken cancellationToken);
|
||||
}
|
||||
39
src/Forwarder/Forwarder/Repositories/IMessageRepository.cs
Normal file
39
src/Forwarder/Forwarder/Repositories/IMessageRepository.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Models;
|
||||
|
||||
namespace Forwarder.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Интерфейс репозитория для работы с сообщениями.
|
||||
/// </summary>
|
||||
public interface IMessageRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Добавляет сообщение в репозиторий.
|
||||
/// </summary>
|
||||
/// <param name="message"> Сообщение. </param>
|
||||
/// <param name="cancellationToken"> Токен отмены. </param>
|
||||
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
|
||||
Task<Result> AddAsync(Message message, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Получает первые <paramref name="takeCount"/> сообщений из репозитория.
|
||||
/// </summary>
|
||||
/// <param name="takeCount">Размер пачки сообщений.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
Task<List<Message>> GetFirstListAsync(int takeCount, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет сообщение из репозитория.
|
||||
/// </summary>
|
||||
/// <param name="message">Сообщение.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
Task<Result> RemoveAsync(Message message, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Удаляет сообщения из репозитория.
|
||||
/// </summary>
|
||||
/// <param name="messageIdList">Список ID сообщений.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
Task<Result> RemoveBatch(List<Guid> messageIdList, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
using Forwarder.Data;
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Models;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Forwarder.Repositories.Implementations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class AssetMapRepository(
|
||||
IDataContext dataContext
|
||||
) : IAssetMapRepository
|
||||
{
|
||||
private const string NotFoundMessage = "Маппинг не найден";
|
||||
private const string AlreadyExistsMessage = "Маппинг с таким ИД объекта уже существует";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<AssetMap>> AddAsync(
|
||||
string abonent,
|
||||
Guid? assetId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var map = await dataContext.Maps
|
||||
.FirstOrDefaultAsync(x => x.Abonent == abonent, cancellationToken);
|
||||
if (map is not null)
|
||||
{
|
||||
return $"Абонент уже существует в маппинге. ИД маппнига: {map.Id}";
|
||||
}
|
||||
|
||||
if (assetId.HasValue)
|
||||
{
|
||||
map = await dataContext.Maps
|
||||
.FirstOrDefaultAsync(x => x.AssetId == assetId.Value, cancellationToken);
|
||||
if (map is not null)
|
||||
{
|
||||
return $"ИД объекта уже существует в маппинге. ИД маппнига: {map.Id}";
|
||||
}
|
||||
}
|
||||
|
||||
map = new AssetMap()
|
||||
{
|
||||
Abonent = abonent,
|
||||
AssetId = assetId,
|
||||
};
|
||||
|
||||
_ = await dataContext.Maps.AddAsync(map, cancellationToken);
|
||||
_ = await dataContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return map;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteAsync(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assetMap = await dataContext.Maps
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (assetMap is null)
|
||||
{
|
||||
return Result.Fail("Маппинг не найден");
|
||||
}
|
||||
|
||||
dataContext.Maps.Remove(assetMap);
|
||||
_ = await dataContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<AssetMap>> Get(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var assetMap = await dataContext.Maps
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (assetMap is null)
|
||||
{
|
||||
return Result<AssetMap>.Fail("Маппинг не найден");
|
||||
}
|
||||
|
||||
return Result<AssetMap>.Success(assetMap);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IQueryable<AssetMap> GetQuery()
|
||||
{
|
||||
return dataContext.Maps
|
||||
.AsNoTracking();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<List<AssetMap>>> GetRequiredMapsAndCreateIsNewAsync(IEnumerable<string> abonentList, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var maps = await dataContext.Maps
|
||||
.Where(x => abonentList.Contains(x.Abonent))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var mapsWithAssetIds = maps.Where(x => x.AssetId.HasValue);
|
||||
if (mapsWithAssetIds.Count() == abonentList.Count())
|
||||
{
|
||||
return Result<List<AssetMap>>.Success(maps);
|
||||
}
|
||||
|
||||
var newMaps = abonentList
|
||||
.Where(x => !maps.Exists(y => y.Abonent == x))
|
||||
.Select(x => new AssetMap
|
||||
{
|
||||
Abonent = x
|
||||
})
|
||||
.ToList();
|
||||
|
||||
await dataContext.Maps.AddRangeAsync(newMaps, cancellationToken);
|
||||
_ = await dataContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result<List<AssetMap>>.Success([.. maps, .. newMaps]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<AssetMap>>.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<ITableStateResponse<AssetMap>>> GetTable(ITableStateRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = GetQuery();
|
||||
|
||||
return await GetTable(query, request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<ITableStateResponse<AssetMap>>> GetTable(IQueryable<AssetMap> query, ITableStateRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var count = await query.CountAsync(cancellationToken);
|
||||
|
||||
if (request.Skip.HasValue)
|
||||
{
|
||||
query = query.Skip(request.Skip.Value);
|
||||
}
|
||||
|
||||
if (request.Take.HasValue)
|
||||
{
|
||||
query = query.Take(request.Take.Value);
|
||||
}
|
||||
|
||||
var items = await query.ToListAsync(cancellationToken);
|
||||
|
||||
return new TableStateResponse<AssetMap>(items, count);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result> UpdateAsync(Guid id, Guid? assetId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assetMap = await dataContext.Maps
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (assetMap is null)
|
||||
{
|
||||
Log.Information(NotFoundMessage);
|
||||
return Result.Fail(NotFoundMessage);
|
||||
}
|
||||
|
||||
var sameAssetId = await dataContext.Maps
|
||||
.FirstOrDefaultAsync(x => x.Id != id && x.AssetId == assetId, cancellationToken);
|
||||
if (sameAssetId is not null)
|
||||
{
|
||||
Log.Information(AlreadyExistsMessage);
|
||||
return Result.Fail(AlreadyExistsMessage);
|
||||
}
|
||||
|
||||
assetMap.AssetId = assetId;
|
||||
_ = await dataContext.SaveChangesAsync(cancellationToken);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result> UpdateIsOnlineStatusAsync(
|
||||
Guid id,
|
||||
bool isOnline,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assetMap = await dataContext.Maps
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (assetMap is null)
|
||||
{
|
||||
Log.Information(NotFoundMessage);
|
||||
return Result.Fail(NotFoundMessage);
|
||||
}
|
||||
|
||||
assetMap.IsOnline = isOnline;
|
||||
|
||||
_ = await dataContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
using Forwarder.Data;
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Models;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Forwarder.Repositories.Implementations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class CommonRepository(
|
||||
DataContext dataContext
|
||||
) : ICommonRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<Result<IEnumerable<Message>>> GetFirstListWhereMapReady(
|
||||
int takeCount,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await dataContext.Messages
|
||||
.Join(dataContext.Maps,
|
||||
m => m.Abonent,
|
||||
a => a.Abonent,
|
||||
(m, a) => new { Message = m, Map = a })
|
||||
.Where(x => x.Map.AssetId.HasValue)
|
||||
.Select(x => x.Message)
|
||||
.OrderBy(x => x.CreatedAt)
|
||||
.Take(takeCount)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<IEnumerable<Message>>.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using Forwarder.Data;
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Models;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Forwarder.Repositories.Implementations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class MessageRepository(
|
||||
DataContext dataContext
|
||||
) : IMessageRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<Result> AddAsync(Message message, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = await dataContext.Messages.AddAsync(message, cancellationToken);
|
||||
_ = await dataContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<List<Message>> GetFirstListAsync(int takeCount, CancellationToken cancellationToken)
|
||||
{
|
||||
return dataContext.Messages
|
||||
.Take(takeCount)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result> RemoveAsync(Message message, CancellationToken cancellationToken)
|
||||
{
|
||||
var existingMessage = await dataContext.Messages
|
||||
.FirstOrDefaultAsync(x => x.Id == message.Id, cancellationToken);
|
||||
if (existingMessage is null)
|
||||
{
|
||||
return Result.Fail("Сообщение не найдено");
|
||||
}
|
||||
|
||||
_ = dataContext.Messages.Remove(existingMessage);
|
||||
_ = await dataContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result> RemoveBatch(List<Guid> messageIdList, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = await dataContext.Messages
|
||||
.Where(x => messageIdList.Contains(x.Id))
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
10
src/Forwarder/Forwarder/Scripts/add-migration.ps1
Normal file
10
src/Forwarder/Forwarder/Scripts/add-migration.ps1
Normal file
@@ -0,0 +1,10 @@
|
||||
$env:DATABASE_CONNECTION_STRING="Server=localhost;Port=5432;Database=Forwarder;User ID=postgres;Password=sql;"
|
||||
$env:API_URL="http://localhost:6001"
|
||||
$env:API_TOKEN="ejrwerhwqkerqkjwehrkjw"
|
||||
$env:TCP_PORT="9000"
|
||||
$env:UDP_PORT="9000"
|
||||
$env:MESSAGE_BULK_SIZE="1"
|
||||
|
||||
dotnet ef migrations add $args[0] -o Data\Migrations
|
||||
|
||||
exit
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
$env:JWT_ISSUER="qwe"
|
||||
$env:JWT_AUDIENCE="qwe"
|
||||
$env:JWT_KEY="qwe"
|
||||
$env:DATABASE_CONNECTION_STRING="Server=localhost;Port=5432;Database=Monolith;User ID=postgres;Password=sql;"
|
||||
$env:FILE_CONNECTION_STRING="mongodb://mongo:mongo@localhost:27017/monitoring"
|
||||
|
||||
dotnet ef migrations remove
|
||||
8
src/Forwarder/Forwarder/Scripts/update-database.ps1
Normal file
8
src/Forwarder/Forwarder/Scripts/update-database.ps1
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
$env:JWT_ISSUER="qwe"
|
||||
$env:JWT_AUDIENCE="qwe"
|
||||
$env:JWT_KEY="qwe"
|
||||
$env:DATABASE_CONNECTION_STRING="Server=localhost;Port=5432;Database=Monolith;User ID=postgres;Password=sql;"
|
||||
$env:FILE_CONNECTION_STRING="mongodb://mongo:mongo@localhost:27017/monitoring"
|
||||
|
||||
dotnet ef database update
|
||||
15
src/Forwarder/Forwarder/Services/IBasicAuthService.cs
Normal file
15
src/Forwarder/Forwarder/Services/IBasicAuthService.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Forwarder.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Сервис для генерации заголовка авторизации.
|
||||
/// </summary>
|
||||
public interface IBasicAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// Генерирует заголовок авторизации.
|
||||
/// </summary>
|
||||
/// <param name="username">Имя пользователя.</param>
|
||||
/// <param name="password">Пароль.</param>
|
||||
/// <returns>Заголовок авторизации.</returns>
|
||||
string GetBasicAuth(string username, string password);
|
||||
}
|
||||
16
src/Forwarder/Forwarder/Services/IGeoRitmClient.cs
Normal file
16
src/Forwarder/Forwarder/Services/IGeoRitmClient.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Forwarder.Models;
|
||||
|
||||
namespace Forwarder.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Клиент для работы с API GeoRitm.
|
||||
/// </summary>
|
||||
public interface IGeoRitmClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Получает список объектов с информацией об их состоянии.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
/// <returns>Список объектов.</returns>
|
||||
Task<Result<GetObjectListResponse[]>> GetObjectList(CancellationToken cancellationToken);
|
||||
}
|
||||
43
src/Forwarder/Forwarder/Services/IMessageConverter.cs
Normal file
43
src/Forwarder/Forwarder/Services/IMessageConverter.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Models;
|
||||
|
||||
namespace Forwarder.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Конвертер сообщений.
|
||||
/// </summary>
|
||||
public interface IMessageConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Конвертирует сообщение в текст.
|
||||
/// </summary>
|
||||
/// <param name="message">Сообщение.</param>
|
||||
/// <returns>Текст сообщения.</returns>
|
||||
Result<string> ToText(Message message);
|
||||
|
||||
/// <summary>
|
||||
/// Конвертирует текст в сообщение.
|
||||
/// </summary>
|
||||
/// <param name="source">Текст.</param>
|
||||
/// <returns>Сообщение.</returns>
|
||||
Result<Message> Parse(string source);
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет, может ли конвертер обработать сообщение.
|
||||
/// </summary>
|
||||
/// <param name="messageText">Текс сообщения.</param>
|
||||
/// <returns> <see langword="true"/> - если конвертер может обработать сообщение. <see langword="false"/> - в противном случае. </returns>
|
||||
bool CanHandle(string messageText);
|
||||
|
||||
/// <summary>
|
||||
/// Отправляет ответ.
|
||||
/// </summary>
|
||||
/// <param name="networkSender">Отправитель сообщений по сети.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
Task<Result> SendAnswer(INetworkSender networkSender, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Имя конвертера.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
}
|
||||
22
src/Forwarder/Forwarder/Services/IMessageConverterService.cs
Normal file
22
src/Forwarder/Forwarder/Services/IMessageConverterService.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Forwarder.Models;
|
||||
|
||||
namespace Forwarder.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Работает с конвертерами.
|
||||
/// </summary>
|
||||
public interface IMessageConverterService
|
||||
{
|
||||
/// <summary>
|
||||
/// Находит конвертер для данного сообщения.
|
||||
/// </summary>
|
||||
/// <returns>Конвертер.</returns>
|
||||
Result<IMessageConverter> FindMessageConverter(string messageText);
|
||||
|
||||
/// <summary>
|
||||
/// Возвращает отформатированное сообщение.
|
||||
/// </summary>
|
||||
/// <param name="input">Исходное сообщение.</param>
|
||||
/// <returns>Отформатированное сообщение.</returns>
|
||||
string FormatMessage(string input);
|
||||
}
|
||||
14
src/Forwarder/Forwarder/Services/INetworkSender.cs
Normal file
14
src/Forwarder/Forwarder/Services/INetworkSender.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Forwarder.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Отправитель сообщений по сети.
|
||||
/// </summary>
|
||||
public interface INetworkSender
|
||||
{
|
||||
/// <summary>
|
||||
/// Отправляет сообщение по сети.
|
||||
/// </summary>
|
||||
/// <param name="message">Сообщение.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
Task SendAsync(byte[] message, CancellationToken cancellationToken);
|
||||
}
|
||||
31
src/Forwarder/Forwarder/Services/ISendService.cs
Normal file
31
src/Forwarder/Forwarder/Services/ISendService.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Models;
|
||||
|
||||
namespace Forwarder.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Сервиса отправки.
|
||||
/// </summary>
|
||||
public interface ISendService
|
||||
{
|
||||
/// <summary>
|
||||
/// Отправляет сообщение.
|
||||
/// </summary>
|
||||
/// <param name="message">Сообщение.</param>
|
||||
/// <param name="assetId">Идентификатор ассета.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
/// <returns>Результат выполнения запроса.</returns>
|
||||
Task<Result> Send(Message message, Guid assetId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Отправляет статус подключения объекта.
|
||||
/// </summary>
|
||||
/// <param name="isOnline">Статус подключения.</param>
|
||||
/// <param name="assetId">Идентификатор ассета.</param>
|
||||
/// <param name="cancellationToken">Токен отмены.</param>
|
||||
/// <returns>Результат выполнения запроса.</returns>
|
||||
Task<Result> SendOnlineStatus(bool isOnline, Guid assetId, CancellationToken cancellationToken);
|
||||
|
||||
|
||||
Task<Result> SendTestMessage(Message message, Guid assetId, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Forwarder.Services.Implementations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class BasicAuthService : IBasicAuthService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string GetBasicAuth(string username, string password)
|
||||
{
|
||||
return $"Basic {Convert.ToBase64String(
|
||||
Encoding.UTF8.GetBytes($"{username}:{password}"))}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Forwarder.Models;
|
||||
|
||||
namespace Forwarder.Services.Implementations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class GeoRitmClient(
|
||||
IHttpClientFactory httpClientFactory
|
||||
) : IGeoRitmClient
|
||||
{
|
||||
public async Task<Result<GetObjectListResponse[]>> GetObjectList(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = httpClientFactory.CreateClient(HttpClients.GeoRitm);
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"/restapi/objects/obj-search/",
|
||||
new object(),
|
||||
cancellationToken);
|
||||
_ = response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<GetObjectListResponse[]>(cancellationToken);
|
||||
return Result<GetObjectListResponse[]>.Success(result!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<GetObjectListResponse[]>.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Forwarder.Models;
|
||||
using Forwarder.Services.Implementations.MessageConverters;
|
||||
|
||||
namespace Forwarder.Services.Implementations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class MessageConverterService : IMessageConverterService
|
||||
{
|
||||
private readonly IEnumerable<IMessageConverter> _converters = [
|
||||
new SurGardVersion4MessageConverter(),
|
||||
new Ademco685MessageConverter(),
|
||||
];
|
||||
|
||||
// Словарь для сопоставления символов с их именами
|
||||
private static readonly Dictionary<char, string> ControlCharNames = new()
|
||||
{
|
||||
{ '\u0000', "" },
|
||||
{ '\u0001', "[SOH]" },
|
||||
{ '\u0002', "[STX]" },
|
||||
{ '\u0003', "[ETX]" },
|
||||
{ '\u0004', "[EOT]" },
|
||||
{ '\u0005', "[ENQ]" },
|
||||
{ '\u0006', "[ACK]" },
|
||||
{ '\u0007', "[BEL]" },
|
||||
{ '\u0008', "[BS]" },
|
||||
{ '\u0009', "[TAB]" },
|
||||
{ '\u000A', "[LF]" },
|
||||
{ '\u000B', "[VT]" },
|
||||
{ '\u000C', "[FF]" },
|
||||
{ '\u000D', "[CR]" },
|
||||
{ '\u000E', "[SO]" },
|
||||
{ '\u000F', "[SI]" },
|
||||
{ '\u0010', "[DLE]" },
|
||||
{ '\u0011', "[DC1]" },
|
||||
{ '\u0012', "[DC2]" },
|
||||
{ '\u0013', "[DC3]" },
|
||||
{ '\u0014', "[DC4]" },
|
||||
{ '\u0015', "[NAK]" },
|
||||
{ '\u0016', "[SYN]" },
|
||||
{ '\u0017', "[ETB]" },
|
||||
{ '\u0018', "[CAN]" },
|
||||
{ '\u0019', "[EM]" },
|
||||
{ '\u001A', "[SUB]" },
|
||||
{ '\u001B', "[ESC]" },
|
||||
{ '\u001C', "[FS]" },
|
||||
{ '\u001D', "[GS]" },
|
||||
{ '\u001E', "[RS]" },
|
||||
{ '\u001F', "[US]" }
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public Result<IMessageConverter> FindMessageConverter(string messageText)
|
||||
{
|
||||
var converter = _converters.FirstOrDefault(x => x.CanHandle(messageText));
|
||||
if (converter is null)
|
||||
{
|
||||
return "Converter is not found";
|
||||
}
|
||||
|
||||
return Result<IMessageConverter>.Success(converter);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string FormatMessage(string input)
|
||||
{
|
||||
// Используем регулярное выражение для поиска и замены
|
||||
return AsciiSymbols().Replace(input, match =>
|
||||
{
|
||||
var controlChar = match.Value[0];
|
||||
return ControlCharNames.TryGetValue(controlChar, out var value) ? value : match.Value;
|
||||
});
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"[\x00-\x1F]")]
|
||||
private static partial Regex AsciiSymbols();
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Models;
|
||||
using Serilog;
|
||||
|
||||
namespace Forwarder.Services.Implementations.MessageConverters;
|
||||
|
||||
internal class Ademco685MessageConverter : IMessageConverter
|
||||
{
|
||||
private static readonly Regex Regex = new(
|
||||
$@"{(char)10}(\d{{1}})(\d{{1}})\s{{1}}(\d{{4}})\s{{1}}18\s{{1}}(\w{{1}})(\d{{3}})\s{{1}}(\d{{2}})\s{{1}}(\w{{1}})(\d{{3}})\s{{1}}{(char)13}"
|
||||
);
|
||||
private static readonly Regex RegexTest = new(
|
||||
$@"{(char)10}(\d{{2}})\s{{1}}OKAY\s{{1}}@{(char)13}"
|
||||
);
|
||||
private static readonly IEnumerable<Regex> AllRegex = [Regex, RegexTest];
|
||||
|
||||
private const byte AcknowledgeSurGardResponse = 0x06;
|
||||
private static readonly byte[] AcknowledgeSurGardResponseData = [AcknowledgeSurGardResponse];
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Ademco 685";
|
||||
|
||||
private static string GetMessageTypeSymbol(MessageType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MessageType.AlarmDisarming => "E",
|
||||
MessageType.RecoveryArming => "R",
|
||||
MessageType.StatePing => "P",
|
||||
_ => "",
|
||||
};
|
||||
}
|
||||
|
||||
private static MessageType GetMessageType(string s)
|
||||
{
|
||||
return s switch
|
||||
{
|
||||
"E" => MessageType.AlarmDisarming,
|
||||
"R" => MessageType.RecoveryArming,
|
||||
"P" => MessageType.StatePing,
|
||||
_ => MessageType.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
public Result<string> ToText(Message message)
|
||||
{
|
||||
// [LF]RLsAAAAs18sQXYZsGGsUCCC[CR]
|
||||
return Result<string>.Success(
|
||||
$"{(char)10}{message.Receiver:0}{message.Line:0} {message.Abonent:0000} 18 {GetMessageTypeSymbol(message.Type):P}{message.Code:000} {message.Sector:00} C{message.Zone:000}{(char)13}"
|
||||
);
|
||||
}
|
||||
|
||||
public Result<Message> Parse(string source)
|
||||
{
|
||||
var match = RegexTest.Match(source);
|
||||
if (match.Success)
|
||||
{
|
||||
return new Message(
|
||||
Guid.NewGuid(),
|
||||
int.Parse(match.Groups[1].Value),
|
||||
int.Parse(match.Groups[2].Value),
|
||||
"",
|
||||
MessageType.StatePing,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
DateTime.UtcNow
|
||||
);
|
||||
}
|
||||
|
||||
match = Regex.Match(source);
|
||||
if (!match.Success)
|
||||
{
|
||||
return "Invalid message format";
|
||||
}
|
||||
|
||||
// [LF]RLsAAAAs18sQXYZsGGsUCCC[CR]
|
||||
return new Message(
|
||||
Guid.NewGuid(),
|
||||
int.Parse(match.Groups[1].Value),
|
||||
int.Parse(match.Groups[2].Value),
|
||||
ConvertAbonent(match.Groups[3].Value),
|
||||
GetMessageType(match.Groups[4].Value),
|
||||
int.Parse(match.Groups[5].Value),
|
||||
int.Parse(match.Groups[6].Value),
|
||||
int.Parse(match.Groups[8].Value),
|
||||
DateTime.UtcNow
|
||||
);
|
||||
}
|
||||
|
||||
private static string ConvertAbonent(string value)
|
||||
{
|
||||
var hex = int.Parse(value, NumberStyles.HexNumber);
|
||||
Log.Warning("Parsed: {Hex}", hex);
|
||||
return hex.ToString();
|
||||
}
|
||||
|
||||
public bool CanHandle(string messageText)
|
||||
{
|
||||
return AllRegex.Any(x => x.IsMatch(messageText));
|
||||
}
|
||||
|
||||
public async Task<Result> SendAnswer(
|
||||
INetworkSender networkSender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
await networkSender.SendAsync(AcknowledgeSurGardResponseData, cancellationToken);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Models;
|
||||
|
||||
namespace Forwarder.Services.Implementations.MessageConverters;
|
||||
|
||||
/// <summary>
|
||||
/// SurGard Version 4 message converter.
|
||||
/// </summary>
|
||||
internal class SurGardVersion4MessageConverter : IMessageConverter
|
||||
{
|
||||
private static readonly Regex Regex = new(@"5(\d{2})(\d{1})\s{1}18(\d{4,16})(\w{1})(\d{3})(\d{2})(\d{3})" + (char)20);
|
||||
private static readonly Regex RegexTest = new($@"1011(\s+)@" + (char)20);
|
||||
private static readonly IEnumerable<Regex> AllRegex = [Regex, RegexTest];
|
||||
private static readonly byte[] AcknowledgeSurGardResponseData = [0x06]; // Encoding.ASCII.GetBytes(AcknowledgeSurGardResponse);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Sur-Gard Version 4";
|
||||
|
||||
public Result<string> ToText(Message message)
|
||||
{
|
||||
// 5RRLs18AAAAQCCCSSZZZ[DC4]
|
||||
// 5000 1800000281R37300001
|
||||
|
||||
if (message.Type is MessageType.StatePing or MessageType.Unknown)
|
||||
{
|
||||
return Result<string>.Success($"1011 {(char)20}");
|
||||
}
|
||||
|
||||
return Result<string>.Success(
|
||||
$"5{message.Receiver:00}{message.Line:0} 18{message.Abonent:0000000000000000}{GetMessageTypeSymbol(message.Type):P}{message.Code:000}{message.Sector:00}{message.Zone:000}{(char)20}");
|
||||
}
|
||||
|
||||
private static string GetMessageTypeSymbol(MessageType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MessageType.AlarmDisarming => "E",
|
||||
MessageType.RecoveryArming => "R",
|
||||
MessageType.StatePing => "P",
|
||||
_ => "",
|
||||
};
|
||||
}
|
||||
|
||||
public Result<Message> Parse(string source)
|
||||
{
|
||||
// 012345678901234567890
|
||||
// 5RRLs18AAAAaaaaQXYZGGCCC[DC4]
|
||||
var match = RegexTest.Match(source);
|
||||
if (match.Success)
|
||||
{
|
||||
return new Message(
|
||||
Guid.NewGuid(),
|
||||
int.Parse(match.Groups[1].Value),
|
||||
int.Parse(match.Groups[2].Value),
|
||||
"",
|
||||
MessageType.StatePing,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
DateTime.UtcNow
|
||||
);
|
||||
}
|
||||
|
||||
match = Regex.Match(source);
|
||||
if (!match.Success)
|
||||
{
|
||||
return "Invalid message format";
|
||||
}
|
||||
|
||||
return new Message(
|
||||
Guid.NewGuid(),
|
||||
int.Parse(match.Groups[1].Value),
|
||||
int.Parse(match.Groups[2].Value),
|
||||
match.Groups[3].Value,
|
||||
GetMessageType(match.Groups[4].Value),
|
||||
int.Parse(match.Groups[5].Value),
|
||||
int.Parse(match.Groups[6].Value),
|
||||
int.Parse(match.Groups[7].Value),
|
||||
DateTime.UtcNow
|
||||
);
|
||||
}
|
||||
|
||||
private static MessageType GetMessageType(string s)
|
||||
{
|
||||
return s switch
|
||||
{
|
||||
"E" => MessageType.AlarmDisarming,
|
||||
"R" => MessageType.RecoveryArming,
|
||||
"P" => MessageType.StatePing,
|
||||
_ => MessageType.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Result> SendAnswer(INetworkSender networkSender, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await networkSender.SendAsync(AcknowledgeSurGardResponseData, cancellationToken);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanHandle(string messageText)
|
||||
{
|
||||
return AllRegex.Any(x => x.IsMatch(messageText));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Forwarder.Services.Implementations.NetworkSenders;
|
||||
|
||||
/// <summary>
|
||||
/// Отправляет сообщения по TCP.
|
||||
/// </summary>
|
||||
/// <param name="networkStream">UDP-клиент.</param>
|
||||
internal class TcpNetworkSender(
|
||||
NetworkStream networkStream
|
||||
) : INetworkSender
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task SendAsync(byte[] message, CancellationToken cancellationToken)
|
||||
{
|
||||
await networkStream.WriteAsync(
|
||||
message,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Forwarder.Services.Implementations.NetworkSenders;
|
||||
|
||||
/// <summary>
|
||||
/// Отправляет сообщения по UDP.
|
||||
/// </summary>
|
||||
/// <param name="client">UDP-клиент.</param>
|
||||
/// <param name="endPoint">Получатель.</param>
|
||||
internal class UdpNetworkSender(
|
||||
UdpClient client,
|
||||
IPEndPoint endPoint
|
||||
) : INetworkSender
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task SendAsync(byte[] message, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = await client.SendAsync(
|
||||
message,
|
||||
endPoint,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
171
src/Forwarder/Forwarder/Services/Implementations/SendService.cs
Normal file
171
src/Forwarder/Forwarder/Services/Implementations/SendService.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Models;
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Monitoring.Contracts;
|
||||
|
||||
using Serilog;
|
||||
|
||||
using TelegramSender;
|
||||
|
||||
namespace Forwarder.Services.Implementations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class SendService(
|
||||
IOptions<ForwarderOptions> options,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
INotificationBroker notificationBroker
|
||||
) : ISendService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<Result> Send(Message message, Guid assetId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = httpClientFactory.CreateClient(HttpClients.Api);
|
||||
var code = GetCode(message);
|
||||
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"/api/events",
|
||||
new CreateEventRequest(
|
||||
options.Value.ApiToken,
|
||||
assetId,
|
||||
code,
|
||||
message.Sector,
|
||||
message.Zone,
|
||||
message.CreatedAt,
|
||||
new AutoAssetCreating(message.Abonent)),
|
||||
cancellationToken);
|
||||
|
||||
_ = response.EnsureSuccessStatusCode();
|
||||
|
||||
var eventId = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
Log.Information("Получен ответ от API, EventId: {EventId}", eventId);
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
// Ловим HttpRequestException
|
||||
Log.Error(ex, "Request failed: {Message}", ex.Message);
|
||||
|
||||
// Если есть статусный код в ответе, можем его получить
|
||||
if (ex.StatusCode.HasValue)
|
||||
{
|
||||
Log.Error("Status code: {Code}: {Desc}", (int)ex.StatusCode, ex.StatusCode);
|
||||
}
|
||||
|
||||
// Получаем весь ответ, если он есть (нужно расширять в .NET 5 и выше)
|
||||
if (ex.Data["HttpResponseMessage"] is HttpResponseMessage innerResponse)
|
||||
{
|
||||
var errorContent = await innerResponse.Content.ReadAsStringAsync(cancellationToken);
|
||||
Log.Error("Error response: {ErrorContent}", errorContent);
|
||||
}
|
||||
|
||||
await notificationBroker.Enqueue("Forwarder cannot send message: " + ex.Message);
|
||||
|
||||
return Result.Fail(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Не удалось отправить сообщение: {Type}: {Message}", ex.GetType(), ex.Message);
|
||||
return Result.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Result> SendOnlineStatus(
|
||||
bool isOnline,
|
||||
Guid assetId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = httpClientFactory.CreateClient(HttpClients.Api);
|
||||
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"/api/assets/online-status",
|
||||
new UpdateAssetOnlineStatusRequest(
|
||||
options.Value.ApiToken,
|
||||
assetId,
|
||||
isOnline),
|
||||
cancellationToken);
|
||||
|
||||
_ = response.EnsureSuccessStatusCode();
|
||||
|
||||
Log.Information("Статус успешно обновлен");
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
// Ловим HttpRequestException
|
||||
Log.Error(ex, "Request failed: {Message}", ex.Message);
|
||||
|
||||
// Если есть статусный код в ответе, можем его получить
|
||||
if (ex.StatusCode.HasValue)
|
||||
{
|
||||
Log.Error("Status code: {Code}: {Desc}", (int)ex.StatusCode, ex.StatusCode);
|
||||
}
|
||||
|
||||
// Получаем весь ответ, если он есть (нужно расширять в .NET 5 и выше)
|
||||
if (ex.Data["HttpResponseMessage"] is HttpResponseMessage innerResponse)
|
||||
{
|
||||
var errorContent = await innerResponse.Content.ReadAsStringAsync(cancellationToken);
|
||||
Log.Error("Error response: {ErrorContent}", errorContent);
|
||||
}
|
||||
|
||||
await notificationBroker.Enqueue("Forwarder cannot send message: " + ex.Message);
|
||||
|
||||
return Result.Fail(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Не удалось отправить сообщение: {Type}: {Message}", ex.GetType(), ex.Message);
|
||||
return Result.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
public async Task<Result> SendTestMessage(Message message, Guid assetId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = httpClientFactory.CreateClient(HttpClients.Api);
|
||||
|
||||
var response = await client.PostAsJsonAsync(
|
||||
"/api/events",
|
||||
new CreateEventRequest(
|
||||
options.Value.ApiToken,
|
||||
assetId,
|
||||
options.Value.TestMessageEventCode,
|
||||
message.Sector,
|
||||
message.Zone,
|
||||
message.CreatedAt,
|
||||
new AutoAssetCreating(message.Abonent)),
|
||||
cancellationToken);
|
||||
|
||||
_ = response.EnsureSuccessStatusCode();
|
||||
|
||||
Log.Information("Тестовое сообщение (код {Code}) отправлено, AssetId: {AssetId}", options.Value.TestMessageEventCode, assetId);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Не удалось отправить тестовое сообщение (код {Code})", options.Value.TestMessageEventCode);
|
||||
return Result.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static int GetCode(Message message)
|
||||
{
|
||||
var typePostfix = message.Type switch
|
||||
{
|
||||
MessageType.AlarmDisarming => 1,
|
||||
_ => 3,
|
||||
};
|
||||
|
||||
return message.Code * 10 + typePostfix;
|
||||
}
|
||||
}
|
||||
176
src/Forwarder/Forwarder/Services/OnlineStatusJob.cs
Normal file
176
src/Forwarder/Forwarder/Services/OnlineStatusJob.cs
Normal file
@@ -0,0 +1,176 @@
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Models;
|
||||
using Forwarder.Repositories;
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Forwarder.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Обновляет статусы объектов.
|
||||
/// </summary>
|
||||
/// <param name="options">Опции.</param>
|
||||
/// <param name="geoRitmOptions">Опции подключения к GeoRitm.</param>
|
||||
/// <param name="serviceProvider">Сервис-провайдер DI контейнер.</param>
|
||||
internal class OnlineStatusJob(
|
||||
IOptions<ForwarderOptions> options,
|
||||
IOptions<GeoRitmConnectionOptions> geoRitmOptions,
|
||||
IServiceProvider serviceProvider
|
||||
) : BackgroundService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(geoRitmOptions.Value.Url) ||
|
||||
string.IsNullOrEmpty(geoRitmOptions.Value.Username) ||
|
||||
string.IsNullOrEmpty(geoRitmOptions.Value.Password))
|
||||
{
|
||||
Log.Warning("GeoRitm connection options are not set. OnlineStatusJob will not run.");
|
||||
return;
|
||||
}
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ExecuteBatch(stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error in ExecuteBatch");
|
||||
}
|
||||
|
||||
await Task.Delay(options.Value.SendingIntervalInSeconds * 1000, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteBatch(CancellationToken stoppingToken)
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var provider = scope.ServiceProvider;
|
||||
var sendService = provider.GetRequiredService<ISendService>();
|
||||
var assetMapRepository = provider.GetRequiredService<IAssetMapRepository>();
|
||||
var geoRitmClient = provider.GetRequiredService<IGeoRitmClient>();
|
||||
|
||||
var assetMapList = await ReadAssetMaps(assetMapRepository, stoppingToken);
|
||||
|
||||
var objectInformationList = await GetObjectInformationList(
|
||||
geoRitmClient,
|
||||
stoppingToken
|
||||
);
|
||||
|
||||
foreach (var newObjectStatus in objectInformationList)
|
||||
{
|
||||
foreach (var oldObjectStatus in assetMapList)
|
||||
{
|
||||
if (!CompareAbonents(newObjectStatus.Imei, oldObjectStatus.Abonent))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CompareOnlineStatuses(newObjectStatus, oldObjectStatus))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await UpdateObjectStatus(
|
||||
assetMapRepository,
|
||||
sendService,
|
||||
oldObjectStatus,
|
||||
newObjectStatus,
|
||||
stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task UpdateObjectStatus(
|
||||
IAssetMapRepository assetMapRepository,
|
||||
ISendService sendService,
|
||||
AssetMap oldObjectStatus,
|
||||
GetObjectListResponse newObjectStatus,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Information(
|
||||
"Update object status: {Id}: {OldStatus} -> {NewStatus}",
|
||||
oldObjectStatus.Id,
|
||||
oldObjectStatus.IsOnline,
|
||||
newObjectStatus.IsOnline);
|
||||
|
||||
await assetMapRepository.UpdateIsOnlineStatusAsync(
|
||||
oldObjectStatus.Id,
|
||||
newObjectStatus.IsOnline,
|
||||
cancellationToken);
|
||||
|
||||
await sendService.SendOnlineStatus(
|
||||
newObjectStatus.IsOnline,
|
||||
oldObjectStatus.AssetId!.Value,
|
||||
cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error in UpdateObjectStatus");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<AssetMap>> ReadAssetMaps(
|
||||
IAssetMapRepository assetMapRepository,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await assetMapRepository.GetTable(
|
||||
new AssetTableStateRequest(null, null),
|
||||
cancellationToken);
|
||||
if (result.IsFail)
|
||||
{
|
||||
Log.Error("Get asset map table is failed: {Error}", result.Error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.Value.Items;
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<GetObjectListResponse>> GetObjectInformationList(
|
||||
IGeoRitmClient geoRitmClient,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await geoRitmClient.GetObjectList(cancellationToken);
|
||||
if (response.IsFail)
|
||||
{
|
||||
Log.Error("Get object list is failed: {Error}", response.Error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return response.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Get object list is failed");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record AssetTableStateRequest(int? Skip, int? Take) : ITableStateRequest;
|
||||
|
||||
private static bool CompareOnlineStatuses(GetObjectListResponse newObjectStatus, AssetMap oldObjectStatus)
|
||||
{
|
||||
return newObjectStatus.IsOnline == (oldObjectStatus.IsOnline ?? false);
|
||||
}
|
||||
|
||||
private static bool CompareAbonents(string abonent1, string abonent2)
|
||||
{
|
||||
return GetLastAbonentSymbols(abonent1) == GetLastAbonentSymbols(abonent2);
|
||||
}
|
||||
|
||||
private static string GetLastAbonentSymbols(string abonent)
|
||||
{
|
||||
const int abonentLength = 8;
|
||||
return abonent.Length > abonentLength
|
||||
? abonent[^abonentLength..]
|
||||
: abonent;
|
||||
}
|
||||
}
|
||||
160
src/Forwarder/Forwarder/Services/SendJob.cs
Normal file
160
src/Forwarder/Forwarder/Services/SendJob.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Models;
|
||||
using Forwarder.Repositories;
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Forwarder.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Отправляет сообщения.
|
||||
/// </summary>
|
||||
/// <param name="options">Опции.</param>
|
||||
internal class SendJob(
|
||||
IOptions<ForwarderOptions> options,
|
||||
IServiceProvider serviceProvider
|
||||
) : BackgroundService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ExecuteBatch(stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error in ExecuteBatch");
|
||||
}
|
||||
|
||||
await Task.Delay(options.Value.SendingIntervalInSeconds * 1000, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteBatch(CancellationToken stoppingToken)
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var provider = scope.ServiceProvider;
|
||||
|
||||
var sendService = provider.GetRequiredService<ISendService>();
|
||||
var commonRepository = provider.GetRequiredService<ICommonRepository>();
|
||||
var messageRepository = provider.GetRequiredService<IMessageRepository>();
|
||||
var assetMapRepository = provider.GetRequiredService<IAssetMapRepository>();
|
||||
|
||||
var messages = await ReadMessages(commonRepository, stoppingToken);
|
||||
var maps = await GetMapsByAbonentList(
|
||||
assetMapRepository,
|
||||
[.. messages.Select(x => x.Abonent).Distinct()],
|
||||
stoppingToken);
|
||||
|
||||
if (maps.IsFail)
|
||||
{
|
||||
Log.Error("GetMapsByAbonentList failed: {Error}", maps.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var tasks = new List<Task<Result>>();
|
||||
var messageIdListForRemoving = new List<Guid>();
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (message.Type == MessageType.StatePing)
|
||||
{
|
||||
var map = maps.Value.FirstOrDefault(x => x.Abonent == message.Abonent);
|
||||
if (map?.AssetId is not null)
|
||||
{
|
||||
var task = sendService.SendTestMessage(message, map.AssetId.Value, stoppingToken);
|
||||
tasks.Add(task);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Существующая логика для других типов сообщений
|
||||
var map = maps.Value.FirstOrDefault(x => x.Abonent == message.Abonent);
|
||||
if (map?.AssetId is not null)
|
||||
{
|
||||
var task = sendService.Send(message, map.AssetId.Value, stoppingToken);
|
||||
tasks.Add(task);
|
||||
}
|
||||
}
|
||||
|
||||
messageIdListForRemoving.Add(message.Id);
|
||||
|
||||
var mapResult = GetMapForMessage(message, maps.Value);
|
||||
if (mapResult.IsFail)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
tasks.Add(Task.Run(async () =>
|
||||
{
|
||||
var sendResult = await TrySendMessage(sendService, message, mapResult.Value, stoppingToken);
|
||||
if (sendResult.IsFail)
|
||||
{
|
||||
return Result.Fail(sendResult.Error);
|
||||
}
|
||||
|
||||
messageIdListForRemoving.Add(message.Id);
|
||||
|
||||
return Result.Success();
|
||||
}));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
if (tasks.Count != 0)
|
||||
{
|
||||
_ = await messageRepository.RemoveBatch(messageIdListForRemoving, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private static Result<AssetMap> GetMapForMessage(Message message, List<AssetMap> maps)
|
||||
{
|
||||
var map = maps.Find(x => x.Abonent == message.Abonent);
|
||||
return map != null
|
||||
? Result<AssetMap>.Success(map)
|
||||
: Result<AssetMap>.Fail($"Не удалось найти маппинг для абонента {message.Abonent}");
|
||||
}
|
||||
|
||||
private async Task<Result<List<AssetMap>>> GetMapsByAbonentList(IAssetMapRepository assetMapRepository, List<string> abonentList, CancellationToken cancellationToken)
|
||||
{
|
||||
return await assetMapRepository.GetRequiredMapsAndCreateIsNewAsync(abonentList, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> TrySendMessage(ISendService sendService, Message message, AssetMap map, CancellationToken cancellationToken)
|
||||
{
|
||||
Log.Information("Попытка отправки сообщения: {Message}", message);
|
||||
|
||||
if (!map.AssetId.HasValue)
|
||||
{
|
||||
Log.Information("AssetId не определен. Пропуск.");
|
||||
return Result.Fail("AssetId is null");
|
||||
}
|
||||
|
||||
var sendResult = await sendService.Send(message, map.AssetId.Value, cancellationToken);
|
||||
if (sendResult.IsFail)
|
||||
{
|
||||
Log.Error(sendResult.Error);
|
||||
}
|
||||
|
||||
return sendResult;
|
||||
}
|
||||
|
||||
private async Task<List<Message>> ReadMessages(ICommonRepository commonRepository, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await commonRepository.GetFirstListWhereMapReady(
|
||||
options.Value.MessageBulkSize,
|
||||
cancellationToken);
|
||||
if (result.IsFail)
|
||||
{
|
||||
Log.Error("GetFirstListWhereMapReady failed: {Error}", result.Error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return [.. result.Value];
|
||||
}
|
||||
}
|
||||
184
src/Forwarder/Forwarder/Services/TcpWatcher.cs
Normal file
184
src/Forwarder/Forwarder/Services/TcpWatcher.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Exceptions;
|
||||
using Forwarder.Models;
|
||||
using Forwarder.Repositories;
|
||||
using Forwarder.Services.Implementations.NetworkSenders;
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Forwarder.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Слушатель TCP-соединений.
|
||||
/// </summary>
|
||||
/// <param name="options">Опции.</param>
|
||||
/// <param name="serviceProvider">Сервис-провайдер.</param>
|
||||
internal class TcpWatcher(
|
||||
IOptions<ForwarderOptions> options,
|
||||
IServiceProvider serviceProvider
|
||||
) : IHostedService
|
||||
{
|
||||
private bool _enabled;
|
||||
private Task? _task;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_enabled = true;
|
||||
_task = Task.Run(() => Listening(cancellationToken), cancellationToken);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Listening(CancellationToken cancellationToken)
|
||||
{
|
||||
while (_enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Start(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error listening");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Log.CloseAndFlushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Start(CancellationToken cancellationToken)
|
||||
{
|
||||
using var tcpListener = new TcpListener(IPAddress.Any, options.Value.TcpPort);
|
||||
tcpListener.Start();
|
||||
Log.Information("TCP-server is running. Port: {Port}", options.Value.TcpPort);
|
||||
|
||||
while (_enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Information("Ожидание соединения...");
|
||||
|
||||
var client = await tcpListener.AcceptTcpClientAsync(cancellationToken);
|
||||
_ = HandleClientAsync(client, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Ошибка при обработке клиента");
|
||||
throw new NeedRecreateException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
|
||||
{
|
||||
Log.Information("Соединение с клиентом установлено.");
|
||||
|
||||
using (client)
|
||||
{
|
||||
using var stream = client.GetStream();
|
||||
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var messageResult = await ReadData(stream, cancellationToken);
|
||||
if (messageResult.IsFail)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var messageConverterService = serviceProvider
|
||||
.CreateScope()
|
||||
.ServiceProvider
|
||||
.GetRequiredService<IMessageConverterService>();
|
||||
|
||||
Log.Information("Получено сообщение по TCP: {Message}",
|
||||
messageConverterService.FormatMessage(messageResult.Value));
|
||||
|
||||
var converter = messageConverterService.FindMessageConverter(messageResult.Value);
|
||||
if (converter.IsFail)
|
||||
{
|
||||
Log.Error(converter.Error);
|
||||
break;
|
||||
}
|
||||
|
||||
Log.Information("Найден конвертер сообщения: {Converter}", converter.Value.Name);
|
||||
|
||||
var message = converter.Value.Parse(messageResult.Value);
|
||||
if (message.IsFail)
|
||||
{
|
||||
Log.Error(message.Error);
|
||||
break;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message.Value.Abonent))
|
||||
{
|
||||
Log.Information("Обнаружен пустой абонент. Пинг.");
|
||||
break;
|
||||
}
|
||||
|
||||
await WriteMessage(message.Value, cancellationToken);
|
||||
|
||||
_ = await converter.Value.SendAnswer(
|
||||
new TcpNetworkSender(stream),
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
Log.Information("Сообщение обработано.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Ошибка при обработке клиента");
|
||||
}
|
||||
finally
|
||||
{
|
||||
stream.Close();
|
||||
client.Close();
|
||||
Log.Information("Соединение с клиентом закрыто.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Result<string>> ReadData(NetworkStream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var buffer = new Memory<byte>(new byte[1024]);
|
||||
|
||||
var bytesRead = await stream.ReadAsync(buffer, cancellationToken);
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
Log.Information("Клиент отключился.");
|
||||
return Result<string>.Fail("Клиент отключился.");
|
||||
}
|
||||
|
||||
var message = Encoding.ASCII.GetString(buffer.Span);
|
||||
return Result<string>.Success(message);
|
||||
}
|
||||
|
||||
private async Task WriteMessage(Message message, CancellationToken cancellationToken)
|
||||
{
|
||||
Log.Information("TCP-сообщение: {Message}", message);
|
||||
|
||||
var messageRepository = serviceProvider
|
||||
.CreateScope()
|
||||
.ServiceProvider
|
||||
.GetRequiredService<IMessageRepository>();
|
||||
|
||||
_ = await messageRepository.AddAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_enabled = false;
|
||||
return _task ?? Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
145
src/Forwarder/Forwarder/Services/UdpWatcher.cs
Normal file
145
src/Forwarder/Forwarder/Services/UdpWatcher.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using Forwarder.Domain;
|
||||
using Forwarder.Exceptions;
|
||||
using Forwarder.Models;
|
||||
using Forwarder.Repositories;
|
||||
using Forwarder.Services.Implementations.NetworkSenders;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Serilog;
|
||||
|
||||
namespace Forwarder.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Слушатель UCP-соединений.
|
||||
/// </summary>
|
||||
/// <param name="options">Опции.</param>
|
||||
/// <param name="serviceProvider">Сервис-провайдер.</param>
|
||||
internal class UdpWatcher(IOptions<ForwarderOptions> options, IServiceProvider serviceProvider)
|
||||
: IHostedService
|
||||
{
|
||||
private bool _enabled;
|
||||
private Task? _task;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_enabled = true;
|
||||
_task = Task.Run(() => Listening(cancellationToken), cancellationToken);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Listening(CancellationToken cancellationToken)
|
||||
{
|
||||
while (_enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Start(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error listening");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Log.CloseAndFlushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Start(CancellationToken cancellationToken)
|
||||
{
|
||||
Log.Information("UDP-server is running. Port: {Port}", options.Value.UdpPort);
|
||||
using var udpClient = new UdpClient(options.Value.UdpPort);
|
||||
|
||||
while (_enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = await HandleClientAsync(udpClient, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Information("Ошибка при обработке сообщения: {Error}", ex.Message);
|
||||
Log.Error(ex, "Ошибка при обработке сообщения.");
|
||||
throw new NeedRecreateException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Result> HandleClientAsync(
|
||||
UdpClient udpClient,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var receiveResult = await udpClient.ReceiveAsync(cancellationToken);
|
||||
var messageText = Encoding.ASCII.GetString(receiveResult.Buffer);
|
||||
|
||||
var messageConverterService = serviceProvider
|
||||
.CreateScope()
|
||||
.ServiceProvider.GetRequiredService<IMessageConverterService>();
|
||||
|
||||
Log.Information(
|
||||
"Получено сообщение по UDP: {Message}.",
|
||||
messageConverterService.FormatMessage(messageText)
|
||||
);
|
||||
|
||||
var converter = messageConverterService.FindMessageConverter(messageText);
|
||||
if (converter.IsFail)
|
||||
{
|
||||
Log.Error(converter.Error);
|
||||
return converter.Error;
|
||||
}
|
||||
|
||||
Log.Information("Найден конвертер сообщения: {Converter}", converter.Value.Name);
|
||||
|
||||
var message = converter.Value.Parse(messageText);
|
||||
Log.Information(
|
||||
"Результат распознавания: {Message}",
|
||||
message.IsFail ? message.Error : message.Value.ToString()
|
||||
);
|
||||
|
||||
if (message.IsFail)
|
||||
{
|
||||
Log.Error(message.Error);
|
||||
return message.Error;
|
||||
}
|
||||
|
||||
Log.Information("Сообщение распознано.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message.Value.Abonent))
|
||||
{
|
||||
Log.Information("Обнаружен пустой абонент. Пинг.");
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
await WriteMessage(message.Value, cancellationToken);
|
||||
|
||||
_ = await converter.Value.SendAnswer(
|
||||
new UdpNetworkSender(udpClient, receiveResult.RemoteEndPoint),
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
Log.Information("Сообщение обработано.");
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
private async Task WriteMessage(Message message, CancellationToken cancellationToken)
|
||||
{
|
||||
Log.Information("UDP-сообщение: {Message}", message);
|
||||
|
||||
var messageRepository = serviceProvider
|
||||
.CreateScope()
|
||||
.ServiceProvider.GetRequiredService<IMessageRepository>();
|
||||
_ = await messageRepository.AddAsync(message, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_enabled = false;
|
||||
return _task ?? Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
8
src/Forwarder/Forwarder/appsettings.Development.json
Normal file
8
src/Forwarder/Forwarder/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/Forwarder/Forwarder/appsettings.json
Normal file
9
src/Forwarder/Forwarder/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
127
src/Forwarder/Forwarder/flow.drawio
Normal file
127
src/Forwarder/Forwarder/flow.drawio
Normal file
@@ -0,0 +1,127 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="VUB8qhCUUltujLIevwC5" name="Page-1">
|
||||
<mxGraphModel dx="1382" dy="916" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="4" value="" style="edgeStyle=none;rounded=1;html=1;noEdgeStyle=1;orthogonal=1;" edge="1" parent="1" source="2" target="3">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="82.5" y="182"/>
|
||||
<mxPoint x="82.5" y="258"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="2" value="External Source" style="html=1;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="27.5" y="130" width="110" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" value="" style="edgeStyle=none;rounded=1;html=1;noEdgeStyle=1;orthogonal=1;" edge="1" parent="1" source="3" target="5">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="82.5" y="322"/>
|
||||
<mxPoint x="82.5" y="398"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="string of protocol" style="html=1;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="27.5" y="270" width="110" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="" style="edgeStyle=none;rounded=1;html=1;noEdgeStyle=1;orthogonal=1;" edge="1" parent="1" source="5" target="7">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="82.5" y="482"/>
|
||||
<mxPoint x="82.5" y="558"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="Parse input string" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry y="410" width="165" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="11" value="" style="edgeStyle=none;rounded=1;html=1;noEdgeStyle=1;orthogonal=1;" edge="1" parent="1" source="7" target="10">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="82.5" y="642"/>
|
||||
<mxPoint x="82.5" y="718"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="Event" style="whiteSpace=wrap;html=1;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="22.5" y="570" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="Save to DB" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="22.5" y="730" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="" style="edgeStyle=none;rounded=1;html=1;noEdgeStyle=1;orthogonal=1;" edge="1" parent="1" source="15" target="19">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="310" y="270" as="sourcePoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="307.5" y="202"/>
|
||||
<mxPoint x="307.5" y="278"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="Get events from DB" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="225" y="130" width="165" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="17" value="Send to API" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="225" y="630" width="165" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="21" value="" style="edgeStyle=none;rounded=1;html=1;noEdgeStyle=1;orthogonal=1;" edge="1" parent="1" source="19" target="22">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="740" y="160" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="307.5" y="362"/>
|
||||
<mxPoint x="307.5" y="438"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="Get mappings" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fixedSize=1;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="225" y="290" width="165" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="24" value="Yes" style="edgeStyle=none;rounded=1;html=1;noEdgeStyle=1;orthogonal=1;" edge="1" parent="1" source="22" target="17">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="307.5" y="542"/>
|
||||
<mxPoint x="307.5" y="618"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="29" value="No" style="edgeStyle=none;rounded=1;html=1;" edge="1" parent="1" source="22" target="30">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="470" y="490" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="map is existing" style="rhombus;whiteSpace=wrap;html=1;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="267.5" y="450" width="80" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="Input streams" style="ellipse;html=1;shape=startState;fillColor=#000000;strokeColor=#ff0000;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="67.5" width="30" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="26" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#ff0000;rounded=1;noEdgeStyle=1;orthogonal=1;" edge="1" source="25" parent="1" target="2">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="405" y="210" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="82.5" y="42"/>
|
||||
<mxPoint x="82.5" y="118"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="27" value="Schedulling" style="ellipse;html=1;shape=startState;fillColor=#000000;strokeColor=#ff0000;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="292.5" width="30" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="28" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#ff0000;rounded=1;noEdgeStyle=1;orthogonal=1;" edge="1" source="27" parent="1" target="15">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="435" y="140" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="307.5" y="42"/>
|
||||
<mxPoint x="307.5" y="118"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="30" value="" style="ellipse;html=1;shape=endState;fillColor=#000000;strokeColor=#ff0000;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="410" y="475" width="30" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
25
src/Forwarder/Forwarder/nswag.json
Normal file
25
src/Forwarder/Forwarder/nswag.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"runtime": "Net90",
|
||||
"documentGenerator": {
|
||||
"openApiToOpenApi": {
|
||||
"url": "http://localhost:5000/openapi/v1.json",
|
||||
"output": "temp-openapi.json"
|
||||
}
|
||||
},
|
||||
"codeGenerators": {
|
||||
"openApiToCSharpClient": {
|
||||
"className": "MonitoringClient",
|
||||
"namespace": "Forwarder.Clients",
|
||||
"output": "Clients/GeneratedApiClient.cs",
|
||||
"injectHttpClient": true,
|
||||
"generateBaseUrlProperty": true,
|
||||
"generateSyncMethods": false,
|
||||
"generateOptionalParameters": true,
|
||||
"generateDtoTypes": true,
|
||||
"exceptionClass": "ApiException",
|
||||
"wrapResponses": false,
|
||||
"useBaseUrl": false,
|
||||
"typeAccessModifier": "public"
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user