xf 2 years ago
commit
6d2fe03426
100 changed files with 7661 additions and 0 deletions
  1. 63 0
      .gitattributes
  2. 343 0
      .gitignore
  3. 121 0
      CallCenter.sln
  4. 7 0
      NuGet.Config
  5. 25 0
      build/.dockerignore
  6. 4 0
      build/Dockerfile
  7. 27 0
      src/CallCenter.Api/CallCenter - Backup.Api.csproj
  8. 26 0
      src/CallCenter.Api/CallCenter.Api.csproj
  9. 10 0
      src/CallCenter.Api/Controllers/BaseController.cs
  10. 126 0
      src/CallCenter.Api/Controllers/CallController.cs
  11. 51 0
      src/CallCenter.Api/Controllers/HomeController.cs
  12. 211 0
      src/CallCenter.Api/Controllers/IvrController.cs
  13. 66 0
      src/CallCenter.Api/Controllers/ListsController.cs
  14. 26 0
      src/CallCenter.Api/Controllers/ReportController.cs
  15. 83 0
      src/CallCenter.Api/Controllers/SettingController.cs
  16. 445 0
      src/CallCenter.Api/Controllers/TelController.cs
  17. 118 0
      src/CallCenter.Api/Controllers/TestController.cs
  18. 794 0
      src/CallCenter.Api/Controllers/TestSdkController.cs
  19. 162 0
      src/CallCenter.Api/Controllers/UserController.cs
  20. 50 0
      src/CallCenter.Api/Filters/TempTokenFilter.cs
  21. 57 0
      src/CallCenter.Api/Filters/UnifyResponseFilter.cs
  22. 63 0
      src/CallCenter.Api/Filters/UserFriendlyExceptionFilter.cs
  23. 32 0
      src/CallCenter.Api/Program.cs
  24. 15 0
      src/CallCenter.Api/Properties/launchSettings.json
  25. 65 0
      src/CallCenter.Api/Realtimes/CallCenterHub.cs
  26. 145 0
      src/CallCenter.Api/StartupExtensions.cs
  27. 50 0
      src/CallCenter.Api/Token/DefaultSessionContext.cs
  28. 7 0
      src/CallCenter.Api/Token/Sercurity.cs
  29. 8 0
      src/CallCenter.Api/appsettings.Development.json
  30. 63 0
      src/CallCenter.Api/appsettings.json
  31. 20 0
      src/CallCenter.Application.Contracts/AppContractsStartupExtensions.cs
  32. 17 0
      src/CallCenter.Application.Contracts/CallCenter.Application.Contracts.csproj
  33. 23 0
      src/CallCenter.Application.Contracts/Mappers/MapperConfigs.cs
  34. 19 0
      src/CallCenter.Application.Contracts/Validators/AddBlacklistDtoValidator.cs
  35. 41 0
      src/CallCenter.Application.Contracts/Validators/AddIvrDtoValidator.cs
  36. 27 0
      src/CallCenter.Application.Contracts/Validators/AddTelGroupDtoValidator.cs
  37. 18 0
      src/CallCenter.Application.Contracts/Validators/GetOutCallListRequestValidator.cs
  38. 19 0
      src/CallCenter.Application.Contracts/Validators/IvrDtoValidator.cs
  39. 61 0
      src/CallCenter.Application.Contracts/Validators/TelToTelDtoValidator.cs
  40. 18 0
      src/CallCenter.Application.Contracts/Validators/UpdateUserDtoValidator.cs
  41. 10 0
      src/CallCenter.Application.Contracts/Validators/ValidatorExtensions.cs
  42. 18 0
      src/CallCenter.Application/ApplicationStartupExtensions.cs
  43. 17 0
      src/CallCenter.Application/CallCenter.Application.csproj
  44. 155 0
      src/CallCenter.Application/Handlers/BaseHandler.cs
  45. 47 0
      src/CallCenter.Application/Handlers/CallState/AlertExtToOuterNotificationHandler.cs
  46. 43 0
      src/CallCenter.Application/Handlers/CallState/AlertMenuToOuterNotificationHandler.cs
  47. 42 0
      src/CallCenter.Application/Handlers/CallState/AlertVisitorToExtNotificationHandler.cs
  48. 79 0
      src/CallCenter.Application/Handlers/CallState/DtmfNotificationHandler.cs
  49. 25 0
      src/CallCenter.Application/Handlers/CallState/FailedNotificationHandler.cs
  50. 52 0
      src/CallCenter.Application/Handlers/CallState/RingExtToOuterNotificationHandler.cs
  51. 14 0
      src/CallCenter.Application/Handlers/CallState/RingMenuToExtNotificationHandler.cs
  52. 42 0
      src/CallCenter.Application/Handlers/CallState/RingVisitorToExtNotificationHandler.cs
  53. 30 0
      src/CallCenter.Application/Handlers/ExtState/BusyNotificationHandler.cs
  54. 29 0
      src/CallCenter.Application/Handlers/ExtState/IdleNotificationHandler.cs
  55. 31 0
      src/CallCenter.Application/Handlers/ExtState/OfflineNotificationHandler.cs
  56. 33 0
      src/CallCenter.Application/Handlers/ExtState/OnlineNotificationHandler.cs
  57. 43 0
      src/CallCenter.Application/Handlers/FlowControl/AnswerExtToOuterNotificationHandler.cs
  58. 44 0
      src/CallCenter.Application/Handlers/FlowControl/AnswerViisitorToExtNotificationHandler.cs
  59. 43 0
      src/CallCenter.Application/Handlers/FlowControl/AnsweredExtToOuterNotificationHandler.cs
  60. 43 0
      src/CallCenter.Application/Handlers/FlowControl/AnsweredExtToOuterToExtNotificationHandler.cs
  61. 44 0
      src/CallCenter.Application/Handlers/FlowControl/AnsweredVisitorToExtNotificationHandler.cs
  62. 46 0
      src/CallCenter.Application/Handlers/FlowControl/ByeExtAndOuterOneNotificationHandler.cs
  63. 44 0
      src/CallCenter.Application/Handlers/FlowControl/ByeExtAndOuterTwoNotificationHandler.cs
  64. 45 0
      src/CallCenter.Application/Handlers/FlowControl/ByeOuterAndOuterNotificationHandler.cs
  65. 45 0
      src/CallCenter.Application/Handlers/FlowControl/ByeVisitorAndExtNotificationHandler.cs
  66. 25 0
      src/CallCenter.Application/Handlers/FlowControl/ByeVisitorAndOuterNotificationHandler.cs
  67. 45 0
      src/CallCenter.Application/Handlers/FlowControl/ByeVisitorOffNotificationHandler.cs
  68. 62 0
      src/CallCenter.Application/Handlers/FlowControl/CdrNotificationHandler.cs
  69. 43 0
      src/CallCenter.Application/Handlers/FlowControl/DivertVisitorToExtNotificationHandler.cs
  70. 51 0
      src/CallCenter.Application/Handlers/FlowControl/EndOfAnnOuterToMenuNotificationHandler.cs
  71. 57 0
      src/CallCenter.Application/Handlers/FlowControl/EndOfAnnVisitorToMenuNotificationHandler.cs
  72. 116 0
      src/CallCenter.Application/Handlers/FlowControl/IncomingNotificationHandler.cs
  73. 103 0
      src/CallCenter.Application/Handlers/FlowControl/InviteNotificationHandler.cs
  74. 24 0
      src/CallCenter.Application/Handlers/FlowControl/QueueVisitorToGroupBusyNotificationHandler.cs
  75. 62 0
      src/CallCenter.Application/Handlers/System/BootupNotificationHandler.cs
  76. 57 0
      src/CallCenter.Application/Handlers/Transient/TransientOuterNotificationHandler.cs
  77. 22 0
      src/CallCenter.Application/Handlers/Transient/TransinetVisitorNotificationHandler.cs
  78. 62 0
      src/CallCenter.CacheManager/BlacklistManager.cs
  79. 21 0
      src/CallCenter.CacheManager/CallCenter.CacheManager.csproj
  80. 135 0
      src/CallCenter.CacheManager/DefaultTypedCache.cs
  81. 46 0
      src/CallCenter.CacheManager/JsonCacheItem.cs
  82. 57 0
      src/CallCenter.CacheManager/StartupExtensions.cs
  83. 42 0
      src/CallCenter.CacheManager/SystemTextJsonSerializer.cs
  84. 18 0
      src/CallCenter.NewRock/CallCenter.NewRock.csproj
  85. 860 0
      src/CallCenter.NewRock/DeviceManager.cs
  86. 370 0
      src/CallCenter.NewRock/Handlers/DeviceEventHandler.cs
  87. 207 0
      src/CallCenter.NewRock/Mappers/EventConfigs.cs
  88. 21 0
      src/CallCenter.NewRock/NewRockStartupExtensions.cs
  89. 187 0
      src/CallCenter.Repository.SqlSugar/BaseRepository.cs
  90. 18 0
      src/CallCenter.Repository.SqlSugar/BlacklistRepository.cs
  91. 13 0
      src/CallCenter.Repository.SqlSugar/CallCenter.Repository.SqlSugar.csproj
  92. 7 0
      src/CallCenter.Repository.SqlSugar/CallCenterDbContext.cs
  93. 19 0
      src/CallCenter.Repository.SqlSugar/CallDetailRepository.cs
  94. 19 0
      src/CallCenter.Repository.SqlSugar/CallRecordRepository.cs
  95. 29 0
      src/CallCenter.Repository.SqlSugar/CallRepository.cs
  96. 43 0
      src/CallCenter.Repository.SqlSugar/IvrCategoryRepository.cs
  97. 23 0
      src/CallCenter.Repository.SqlSugar/IvrRepository.cs
  98. 170 0
      src/CallCenter.Repository.SqlSugar/SqlSugarStartupExtensions.cs
  99. 18 0
      src/CallCenter.Repository.SqlSugar/SystemSettingGroupRepository.cs
  100. 19 0
      src/CallCenter.Repository.SqlSugar/SystemSettingRepository.cs

+ 63 - 0
.gitattributes

@@ -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

+ 343 - 0
.gitignore

@@ -0,0 +1,343 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+*.Comments.xml
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- Backup*.rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+/src/CallCenter.Api/Document.xml

+ 121 - 0
CallCenter.sln

@@ -0,0 +1,121 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.2.32616.157
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "01_Infrastructure", "01_Infrastructure", "{61683AC9-2CA4-4E27-BA98-5D2497E21FF6}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "02_Domain", "02_Domain", "{A0348BAC-6335-4195-9FEE-E55D8A354999}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "03_Application", "03_Application", "{8F5C4C9D-81A7-4BE4-88A3-019100D400EA}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "04_Presentation", "04_Presentation", "{435C1B45-F4AD-4D1D-8A3B-D77759B15BFD}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CallCenter", "src\CallCenter\CallCenter.csproj", "{5986B988-AC98-4B20-B2C6-06D43E11AB33}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5A55ED9D-0B53-4796-8881-D1595B077557}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NewRockSdk.Tests", "test\NewRockSdk.Tests\NewRockSdk.Tests.csproj", "{CE43D0D8-442A-4217-9E01-2AABEDF807E6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CallCenter.Api", "src\CallCenter.Api\CallCenter.Api.csproj", "{46A791D4-637D-4F90-BC48-2A7696FED664}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CallCenter.Share", "src\CallCenter.Share\CallCenter.Share.csproj", "{062B46A5-21A0-443E-A21D-22FC54F0C465}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CallCenter.Application", "src\CallCenter.Application\CallCenter.Application.csproj", "{4F47CCDD-51D2-4F4E-A93D-820B48FCB0BD}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1DF943AD-82EF-4E06-8C0B-52F1F0FB2D1D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NewRock.Sdk", "src\NewRock.Sdk\NewRock.Sdk.csproj", "{0E9F1E0B-7DFF-4DE2-A2E6-92318DBBB022}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XF.Domain", "src\XF.Domain\XF.Domain.csproj", "{D43C978C-3617-449E-A923-C3E59B7633F9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XF.Domain.Repository", "src\XF.Domain.Repository\XF.Domain.Repository.csproj", "{D75BA722-82C5-4DDF-9F15-B4F53A6A8116}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CallCenter.NewRock", "src\CallCenter.NewRock\CallCenter.NewRock.csproj", "{FAD0AC30-DA50-47C2-9985-66ABA359F54F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CallCenter.Repository.SqlSugar", "src\CallCenter.Repository.SqlSugar\CallCenter.Repository.SqlSugar.csproj", "{E1762DB1-A366-495D-AE6A-079623E840C9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CallCenter.CacheManager", "src\CallCenter.CacheManager\CallCenter.CacheManager.csproj", "{C044AA74-C9FA-4754-AB86-0AFDB7032D13}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CallCenter.Application.Contracts", "src\CallCenter.Application.Contracts\CallCenter.Application.Contracts.csproj", "{47361A16-ECC5-40C5-9A58-23D46DCD2A0B}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{5986B988-AC98-4B20-B2C6-06D43E11AB33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{5986B988-AC98-4B20-B2C6-06D43E11AB33}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{5986B988-AC98-4B20-B2C6-06D43E11AB33}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{5986B988-AC98-4B20-B2C6-06D43E11AB33}.Release|Any CPU.Build.0 = Release|Any CPU
+		{CE43D0D8-442A-4217-9E01-2AABEDF807E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{CE43D0D8-442A-4217-9E01-2AABEDF807E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{CE43D0D8-442A-4217-9E01-2AABEDF807E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{CE43D0D8-442A-4217-9E01-2AABEDF807E6}.Release|Any CPU.Build.0 = Release|Any CPU
+		{46A791D4-637D-4F90-BC48-2A7696FED664}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{46A791D4-637D-4F90-BC48-2A7696FED664}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{46A791D4-637D-4F90-BC48-2A7696FED664}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{46A791D4-637D-4F90-BC48-2A7696FED664}.Release|Any CPU.Build.0 = Release|Any CPU
+		{062B46A5-21A0-443E-A21D-22FC54F0C465}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{062B46A5-21A0-443E-A21D-22FC54F0C465}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{062B46A5-21A0-443E-A21D-22FC54F0C465}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{062B46A5-21A0-443E-A21D-22FC54F0C465}.Release|Any CPU.Build.0 = Release|Any CPU
+		{4F47CCDD-51D2-4F4E-A93D-820B48FCB0BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{4F47CCDD-51D2-4F4E-A93D-820B48FCB0BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{4F47CCDD-51D2-4F4E-A93D-820B48FCB0BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{4F47CCDD-51D2-4F4E-A93D-820B48FCB0BD}.Release|Any CPU.Build.0 = Release|Any CPU
+		{0E9F1E0B-7DFF-4DE2-A2E6-92318DBBB022}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{0E9F1E0B-7DFF-4DE2-A2E6-92318DBBB022}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{0E9F1E0B-7DFF-4DE2-A2E6-92318DBBB022}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{0E9F1E0B-7DFF-4DE2-A2E6-92318DBBB022}.Release|Any CPU.Build.0 = Release|Any CPU
+		{D43C978C-3617-449E-A923-C3E59B7633F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{D43C978C-3617-449E-A923-C3E59B7633F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{D43C978C-3617-449E-A923-C3E59B7633F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{D43C978C-3617-449E-A923-C3E59B7633F9}.Release|Any CPU.Build.0 = Release|Any CPU
+		{D75BA722-82C5-4DDF-9F15-B4F53A6A8116}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{D75BA722-82C5-4DDF-9F15-B4F53A6A8116}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{D75BA722-82C5-4DDF-9F15-B4F53A6A8116}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{D75BA722-82C5-4DDF-9F15-B4F53A6A8116}.Release|Any CPU.Build.0 = Release|Any CPU
+		{FAD0AC30-DA50-47C2-9985-66ABA359F54F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{FAD0AC30-DA50-47C2-9985-66ABA359F54F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{FAD0AC30-DA50-47C2-9985-66ABA359F54F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{FAD0AC30-DA50-47C2-9985-66ABA359F54F}.Release|Any CPU.Build.0 = Release|Any CPU
+		{E1762DB1-A366-495D-AE6A-079623E840C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{E1762DB1-A366-495D-AE6A-079623E840C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{E1762DB1-A366-495D-AE6A-079623E840C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{E1762DB1-A366-495D-AE6A-079623E840C9}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C044AA74-C9FA-4754-AB86-0AFDB7032D13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{C044AA74-C9FA-4754-AB86-0AFDB7032D13}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C044AA74-C9FA-4754-AB86-0AFDB7032D13}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{C044AA74-C9FA-4754-AB86-0AFDB7032D13}.Release|Any CPU.Build.0 = Release|Any CPU
+		{47361A16-ECC5-40C5-9A58-23D46DCD2A0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{47361A16-ECC5-40C5-9A58-23D46DCD2A0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{47361A16-ECC5-40C5-9A58-23D46DCD2A0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{47361A16-ECC5-40C5-9A58-23D46DCD2A0B}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+		{61683AC9-2CA4-4E27-BA98-5D2497E21FF6} = {1DF943AD-82EF-4E06-8C0B-52F1F0FB2D1D}
+		{A0348BAC-6335-4195-9FEE-E55D8A354999} = {1DF943AD-82EF-4E06-8C0B-52F1F0FB2D1D}
+		{8F5C4C9D-81A7-4BE4-88A3-019100D400EA} = {1DF943AD-82EF-4E06-8C0B-52F1F0FB2D1D}
+		{435C1B45-F4AD-4D1D-8A3B-D77759B15BFD} = {1DF943AD-82EF-4E06-8C0B-52F1F0FB2D1D}
+		{5986B988-AC98-4B20-B2C6-06D43E11AB33} = {A0348BAC-6335-4195-9FEE-E55D8A354999}
+		{CE43D0D8-442A-4217-9E01-2AABEDF807E6} = {5A55ED9D-0B53-4796-8881-D1595B077557}
+		{46A791D4-637D-4F90-BC48-2A7696FED664} = {435C1B45-F4AD-4D1D-8A3B-D77759B15BFD}
+		{062B46A5-21A0-443E-A21D-22FC54F0C465} = {A0348BAC-6335-4195-9FEE-E55D8A354999}
+		{4F47CCDD-51D2-4F4E-A93D-820B48FCB0BD} = {8F5C4C9D-81A7-4BE4-88A3-019100D400EA}
+		{0E9F1E0B-7DFF-4DE2-A2E6-92318DBBB022} = {61683AC9-2CA4-4E27-BA98-5D2497E21FF6}
+		{D43C978C-3617-449E-A923-C3E59B7633F9} = {61683AC9-2CA4-4E27-BA98-5D2497E21FF6}
+		{D75BA722-82C5-4DDF-9F15-B4F53A6A8116} = {61683AC9-2CA4-4E27-BA98-5D2497E21FF6}
+		{FAD0AC30-DA50-47C2-9985-66ABA359F54F} = {61683AC9-2CA4-4E27-BA98-5D2497E21FF6}
+		{E1762DB1-A366-495D-AE6A-079623E840C9} = {61683AC9-2CA4-4E27-BA98-5D2497E21FF6}
+		{C044AA74-C9FA-4754-AB86-0AFDB7032D13} = {61683AC9-2CA4-4E27-BA98-5D2497E21FF6}
+		{47361A16-ECC5-40C5-9A58-23D46DCD2A0B} = {8F5C4C9D-81A7-4BE4-88A3-019100D400EA}
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {CD08520D-0F71-435D-89D5-57CC30B1AB94}
+	EndGlobalSection
+EndGlobal

+ 7 - 0
NuGet.Config

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

+ 25 - 0
build/.dockerignore

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

+ 4 - 0
build/Dockerfile

@@ -0,0 +1,4 @@
+FROM  mcr.microsoft.com/dotnet/aspnet:6.0
+WORKDIR /app
+COPY out/ .
+ENTRYPOINT ["dotnet", "CallCenter.Api.dll"]

+ 27 - 0
src/CallCenter.Api/CallCenter - Backup.Api.csproj

@@ -0,0 +1,27 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <Nullable>enable</Nullable>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <GenerateDocumentationFile>True</GenerateDocumentationFile>
+    <DocumentationFile>Document.xml</DocumentationFile>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="FluentValidation.AspNetCore" Version="11.2.1" />
+    <PackageReference Include="Mapster.DependencyInjection" Version="1.0.0" />
+    <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
+    <PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="6.0.8" />
+    <PackageReference Include="NETCore.Encrypt" Version="2.1.0" />
+    <PackageReference Include="Serilog.Enrichers.Span" Version="2.3.0" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\CallCenter.Application\CallCenter.Application.csproj" />
+  </ItemGroup>
+
+  <ProjectExtensions><VisualStudio><UserProperties appsettings_1json__JsonSchema="" properties_4launchsettings_1json__JsonSchema="" /></VisualStudio></ProjectExtensions>
+
+</Project>

+ 26 - 0
src/CallCenter.Api/CallCenter.Api.csproj

@@ -0,0 +1,26 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <Nullable>enable</Nullable>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <GenerateDocumentationFile>True</GenerateDocumentationFile>
+    <DocumentationFile>Document.xml</DocumentationFile>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="FluentValidation.AspNetCore" Version="11.2.1" />
+    <PackageReference Include="Mapster.DependencyInjection" Version="1.0.0" />
+    <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
+    <PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="6.0.8" />
+    <PackageReference Include="NETCore.Encrypt" Version="2.1.0" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\CallCenter.Application\CallCenter.Application.csproj" />
+  </ItemGroup>
+
+  <ProjectExtensions><VisualStudio><UserProperties appsettings_1json__JsonSchema="" properties_4launchsettings_1json__JsonSchema="" /></VisualStudio></ProjectExtensions>
+
+</Project>

+ 10 - 0
src/CallCenter.Api/Controllers/BaseController.cs

@@ -0,0 +1,10 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace CallCenter.Api.Controllers;
+
+[ApiController]
+[Route("api/v1/[controller]")]
+public class BaseController : ControllerBase
+{
+    
+}

+ 126 - 0
src/CallCenter.Api/Controllers/CallController.cs

@@ -0,0 +1,126 @@
+using CallCenter.Calls;
+using CallCenter.Share.Dtos;
+using CallCenter.Share.Enums;
+using CallCenter.Share.Requests;
+using MapsterMapper;
+using MessagePack.Formatters;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CallCenter.Api.Controllers
+{
+    /// <summary>
+    /// 
+    /// </summary>
+    public class CallController : BaseController
+    {
+        private readonly ICallDomainService _callDomainService;
+        private readonly ICallRepository _callRepository;
+        private readonly IMapper _mapper;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public CallController(ICallDomainService callDomainService, ICallRepository callRepository, IMapper mapper, ICallDetailRepository callDetailRepository)
+        {
+            _callDomainService = callDomainService;
+            _callRepository = callRepository;
+            _mapper = mapper;
+            _callDetailRepository = callDetailRepository;
+        }
+
+
+        #region 强拆
+
+        /// <summary>
+        /// 强拆分机
+        /// </summary>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        [HttpPost("ClearExt")]
+        public async Task ClearExt([FromBody] ClearExtRequest request)
+        {
+            await _callDomainService.ClearExtAsync(request, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 强拆来电
+        /// </summary>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        [HttpPost("ClearVisitor")]
+        public async Task ClearVisitor([FromBody] ClearVisitorRequest request)
+        {
+            await _callDomainService.ClearVisitorAsync(request, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 强拆去电
+        /// </summary>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        [HttpPost("ClearOuter")]
+        public async Task ClearOuter([FromBody] ClearOuterRequest request)
+        {
+            await _callDomainService.ClearOuterAsync(request, HttpContext.RequestAborted);
+        }
+
+        #endregion
+
+        #region 通话记录
+
+        /// <summary>
+        /// 分页通话记录列表
+        /// </summary>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        [HttpGet("paged")]
+        public async Task<PagedDto<CallDto>> GetCallList([FromQuery]GetCallListRequest request)
+        {
+            var (total, items) = await _callRepository.QueryPagedAsync(
+                x=> true,
+                x => x.OrderByDescending(d => d.CreationTime),
+                request.PageIndex,
+                request.PageSize,
+                (!string.IsNullOrEmpty(request.PhoneNum), d => d.FromNo.Contains(request.PhoneNum!))
+            );
+            return new PagedDto<CallDto>(total, _mapper.Map<IReadOnlyList<CallDto>>(items));
+        }
+
+        #endregion
+
+        #region 对外通话记录
+
+        /// <summary>
+        /// 通话记录(外部对接)
+        /// </summary>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        [HttpGet("out-calllist")]
+        public async Task<IReadOnlyList<OutCallDto>> GetOutCallList([FromQuery] GetOutCallListRequest request)
+        {
+            var list = await _callRepository.QueryPartAsync(request.time);
+            List<OutCallDto> outCallList = new List<OutCallDto>();
+            foreach (var item in list)
+            {
+                OutCallDto outCallDto = new OutCallDto();
+                outCallDto.CallId = item.Id;
+                outCallDto.InfoType = Share.Enums.EInfoType.Call;//TODO目前写死(只有电话)
+                outCallDto.Direction = item.CallDirection;
+                outCallDto.Cpn = item.FromNo??"";
+                outCallDto.Cdpn = item.ToNo ?? "";
+                outCallDto.Answered = item.CallDetails?.FirstOrDefault(x => x.EventName == "ANSWERED")?.AnswerNo ?? "";
+                outCallDto.BeginTime = item.CreationTime;
+                outCallDto.OnTime = item.CallDetails?.FirstOrDefault(x => x.EventName == "ANSWERED")?.CreationTime;//TODO 接通时间是否可以为空
+                outCallDto.ByeTime = item.CallDetails?.FirstOrDefault(x => x.EventName == "BYE")?.CreationTime;
+                outCallDto.TalkTime = item.Duration;
+                outCallDto.SoundFileName = item.CallDetails?.FirstOrDefault(x => x.EventName == "BYE")?.Recording ?? "";
+                outCallDto.EvaluateResult = "";
+                outCallDto.EndBy = item.EndBy;
+                outCallDto.OnState = item.CallDetails?.Any(x => x.EventName == "ANSWERED")==true? EOnState.On : EOnState.NoOn;
+                outCallList.Add(outCallDto);
+            }
+            return outCallList;
+        }
+
+        #endregion
+
+    }
+}

+ 51 - 0
src/CallCenter.Api/Controllers/HomeController.cs

@@ -0,0 +1,51 @@
+using CallCenter.Api.Token;
+using CallCenter.BlackLists;
+using CallCenter.Calls;
+using CallCenter.Ivrs;
+using CallCenter.Repository.SqlSugar;
+using CallCenter.Settings;
+using CallCenter.Share.Requests;
+using CallCenter.Tels;
+using CallCenter.Users;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using NETCore.Encrypt;
+using SqlSugar;
+using XF.Domain.Exceptions;
+using XF.Domain.Https;
+
+namespace CallCenter.Api.Controllers;
+
+public class HomeController : BaseController
+{
+    private readonly IUserRepository _userRepository;
+    private readonly ISugarUnitOfWork<CallCenterDbContext> _uow;
+
+    public HomeController(IUserRepository userRepository, ISugarUnitOfWork<CallCenterDbContext> uow)
+    {
+        _userRepository = userRepository;
+        _uow = uow;
+    }
+
+    [HttpPost("login")]
+    public async Task<string> Login([FromBody] LoginRequest request)
+    {
+        var user =
+            await _userRepository.GetAsync(d => !d.IsDeleted && d.PhoneNo == request.PhoneNo, HttpContext.RequestAborted);
+        if (user is null)
+            throw new UserFriendlyException("未查询到该用户");
+
+        var token = EncryptProvider.AESEncrypt(System.Text.Json.JsonSerializer.Serialize(user), Sercurity.Key);
+        return token;
+    }
+
+    [HttpGet("createdb")]
+    public Task CreateDb()
+    {
+        var db = _uow.Db;
+        db.DbMaintenance.CreateDatabase();
+        db.CodeFirst.InitTables<Blacklist>();
+
+        return Task.CompletedTask;
+    }
+}

+ 211 - 0
src/CallCenter.Api/Controllers/IvrController.cs

@@ -0,0 +1,211 @@
+using CallCenter.Caches;
+using CallCenter.Ivrs;
+using CallCenter.Share.Dtos;
+using CallCenter.Share.Enums;
+using MapsterMapper;
+using Microsoft.AspNetCore.Mvc;
+using XF.Domain.Cache;
+using XF.Domain.Exceptions;
+using XF.Utility.EnumExtensions;
+
+namespace CallCenter.Api.Controllers;
+
+public class IvrController : BaseController
+{
+    private readonly IIvrDomainService _ivrDomainService;
+    private readonly IIvrRepository _ivrRepository;
+    private readonly IIvrCategoryRepository _ivrCategoryRepository;
+    private readonly ITypedCache<IReadOnlyList<Ivr>> _cacheIvrList;
+    private readonly IMapper _mapper;
+
+    public IvrController(
+        IIvrDomainService ivrDomainService,
+        IIvrRepository ivrRepository,
+        IIvrCategoryRepository ivrCategoryRepository,
+        ITypedCache<IReadOnlyList<Ivr>> cacheIvrList,
+        IMapper mapper)
+    {
+        _ivrDomainService = ivrDomainService;
+        _ivrRepository = ivrRepository;
+        _ivrCategoryRepository = ivrCategoryRepository;
+        _cacheIvrList = cacheIvrList;
+        _mapper = mapper;
+    }
+
+    /// <summary>
+    /// 查询所有ivr分类
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("categories")]
+    public async Task<IReadOnlyList<IvrCategory>> GetCategories()
+    {
+        return await _ivrCategoryRepository.QueryExtAsync(d => true, d => d.Includes(x => x.Ivrs));
+    }
+
+    /// <summary>
+    /// 查询ivr分类(含ivr)
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpGet("category/{id}")]
+    public async Task<IvrCategory> GetCategory(string id)
+    {
+        return await _ivrCategoryRepository.GetExtAsync(d => d.Id == id, d => d.Includes(x => x.Ivrs));
+    }
+
+    /// <summary>
+    /// 新增IVR分类
+    /// </summary>
+    /// <returns></returns>
+    [HttpPost("category")]
+    public async Task<string> AddCategory([FromBody] AddIvrCategoryDto request)
+    {
+        var category = _mapper.Map<IvrCategory>(request);
+        return await _ivrCategoryRepository.AddAsync(category, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 更新IVR分类
+    /// </summary>
+    /// <param name="request"></param>
+    /// <returns></returns>
+    [HttpPut("category")]
+    public async Task UpdateCategory([FromBody] UpdateIvrCategoryDto request)
+    {
+        var category = _mapper.Map<IvrCategory>(request);
+        await _ivrCategoryRepository.UpdateAsync(category, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 删除分类,含分类下所有IVR
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpDelete("category/{id}")]
+    public async Task RemoveCategory(string id)
+    {
+        var category = await _ivrCategoryRepository.GetExtAsync(d => d.Id == id, d => d.Includes(x => x.Ivrs));
+        await _ivrRepository.RemoveRangeAsync(category.Ivrs, HttpContext.RequestAborted);
+        await _ivrCategoryRepository.RemoveAsync(category);
+        _cacheIvrList.Remove(Ivr.Key);
+    }
+
+    /// <summary>
+    /// 新增IVR
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost]
+    public async Task<string> Add([FromBody] AddIvrDto dto)
+    {
+        var ivr = _mapper.Map<Ivr>(dto);
+        return await _ivrDomainService.AddIvrAsync(ivr, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 更新IVR
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPut]
+    public async Task Update([FromBody] UpdateIvrDto dto)
+    {
+        var ivr = await _ivrRepository.GetAsync(dto.Id, HttpContext.RequestAborted);
+        _mapper.Map(dto, ivr);
+        await _ivrDomainService.UpdateIvrAsync(ivr, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 构建IVR关系
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    /// <exception cref="UserFriendlyException"></exception>
+    [HttpPost("structure")]
+    public async Task Structure([FromBody] StructureIvrDto dto)
+    {
+        await _ivrDomainService.StructureIvrAsync(dto, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 删除IVR关系(并非删除IVR)
+    /// </summary>
+    /// <param name="ivrId"></param>
+    /// <returns></returns>
+    [HttpPut("destructure/{ivrId}")]
+    public async Task DeStructureIvr(string ivrId)
+    {
+        await _ivrDomainService.DeStructureIvrAsync(ivrId, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 替换某个IVR分组下的起始IVR(根节点)
+    /// </summary>
+    /// <param name="ivrId"></param>
+    /// <returns></returns>
+    [HttpPut("replace-root/{ivrId}")]
+    public async Task ReplaceRootAsync(string ivrId)
+    {
+        await _ivrDomainService.ReplaceRootAsync(ivrId, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 查询所有ivr
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet]
+    public async Task<IReadOnlyList<IvrDto>> QueryIvrs()
+    {
+        var ivrs = await _ivrRepository.QueryAsync();
+        return _mapper.Map<IReadOnlyList<IvrDto>>(ivrs);
+    }
+
+    /// <summary>
+    /// 查询ivr分类,以树形结构返回IVR关系
+    /// </summary>
+    /// <param name="categoryId"></param>
+    /// <returns></returns>
+    [HttpGet("tree/{categoryId}")]
+    public async Task<IvrDto> GetBeginingIvrAsync(string categoryId)
+    {
+        ////todo 1.
+        ////超过1个根节点则视为未配置完成
+        //var count = await _ivrRepository.CountAsync(
+        //    d => d.IvrCategoryId == categoryId && string.IsNullOrEmpty(d.PrevIvrNo), HttpContext.RequestAborted);
+        //if (count > 1)
+        //    return null;
+        var ivrs = await _ivrRepository.QueryAsync(d => d.IvrCategoryId == categoryId);
+        var rootIvr = ivrs.FirstOrDefault(d => d.IsRoot);
+        if (rootIvr == null)
+            throw new UserFriendlyException("每个IVR分类至少存在一个根节点");
+        var rootDto = _mapper.Map<IvrDto>(rootIvr);
+        BuildIvrTree(ivrs, rootDto);
+        return rootDto;
+    }
+
+    /// <summary>
+    /// 页面基础信息
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("base-info")]
+    public async Task<dynamic> GetBaseInfo()
+    {
+        return new
+        {
+            IvrTypes = EnumExts.GetDescriptions<EIvrType>(),
+            IvrStrategeTypes = EnumExts.GetDescriptions<EIvrStrategeType>(),
+            IvrAnswerTypes = EnumExts.GetDescriptions<EIvrAnswerType>(),
+        };
+    }
+
+    private void BuildIvrTree(List<Ivr> ivrs, IvrDto dto)
+    {
+        var children = ivrs.Where(d => d.PrevIvrNo == dto.No && !d.IsRoot).ToList();
+        if (!children.Any()) return;
+        dto.ChildIvrs = _mapper.Map<IReadOnlyList<IvrDto>>(children);
+        foreach (var dtoChildIvr in dto.ChildIvrs)
+        {
+            BuildIvrTree(ivrs, dtoChildIvr);
+        }
+    }
+}

+ 66 - 0
src/CallCenter.Api/Controllers/ListsController.cs

@@ -0,0 +1,66 @@
+using CallCenter.BlackLists;
+using CallCenter.CacheManager;
+using CallCenter.Share.Dtos;
+using MapsterMapper;
+using Microsoft.AspNetCore.Mvc;
+using XF.Domain.Cache;
+
+namespace CallCenter.Api.Controllers;
+
+public class ListsController : BaseController
+{
+    private readonly IBlacklistDomainService _blacklistDomainService;
+    private readonly IBlacklistRepository _blacklistRepository;
+    private readonly IMapper _mapper;
+
+    public ListsController(
+        IBlacklistDomainService blacklistDomainService,
+        IBlacklistRepository blacklistRepository,
+        IMapper mapper)
+    {
+        _blacklistDomainService = blacklistDomainService;
+        _blacklistRepository = blacklistRepository;
+        _mapper = mapper;
+    }
+
+    /// <summary>
+    /// 添加电话号至黑名单
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpPost("blacklist")]
+    public async Task AddBlacklist([FromBody] AddBlacklistDto dto)
+    {
+        var exists = await _blacklistRepository.AnyAsync(d => !d.IsDeleted && d.PhoneNo == dto.PhoneNo);
+        if (exists) return;
+        var blacklist = _mapper.Map<Blacklist>(dto);
+        await _blacklistDomainService.AddAsync(blacklist, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 删除黑名单数据
+    /// </summary>
+    /// <param name="phone"></param>
+    [HttpDelete("blacklist/{phone}")]
+    public void RemoveBlacklist(string phone)
+    {
+        _blacklistDomainService.Remove(phone);
+    }
+
+    /// <summary>
+    /// 分页查询黑名单
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
+    [HttpGet("blacklist/paged")]
+    public async Task<PagedDto<Blacklist>> QueryPaged([FromQuery] BlacklistPagedDto dto)
+    {
+        var (total, items) = await _blacklistRepository.QueryPagedAsync(
+            d => !d.IsDeleted,
+            d => d.OrderByDescending(x => x.CreationTime),
+            dto.PageIndex,
+            dto.PageSize,
+            (!string.IsNullOrEmpty(dto.PhoneNo), d => d.PhoneNo.Contains(dto.PhoneNo!)));
+        return new PagedDto<Blacklist>(total, items);
+    }
+}

+ 26 - 0
src/CallCenter.Api/Controllers/ReportController.cs

@@ -0,0 +1,26 @@
+using CallCenter.Devices;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+
+namespace CallCenter.Api.Controllers;
+
+[ApiController]
+[Route("api/report")]
+public class ReportController : ControllerBase
+{
+    private readonly IOptionsSnapshot<DeviceConfigs> _options;
+    private readonly IDeviceEventHandler _deviceEventHandler;
+
+    public ReportController(IOptionsSnapshot<DeviceConfigs> options, IDeviceEventHandler deviceEventHandler)
+    {
+        _options = options;
+        _deviceEventHandler = deviceEventHandler;
+    }
+
+    [HttpGet]
+    public async Task ReceiveEvents()
+    {
+        await _deviceEventHandler.HandleAsync(Request.Body, _options.Value, HttpContext.RequestAborted);
+    }
+}

+ 83 - 0
src/CallCenter.Api/Controllers/SettingController.cs

@@ -0,0 +1,83 @@
+using CallCenter.Manage;
+using CallCenter.Settings;
+using CallCenter.Share;
+using CallCenter.Share.Requests;
+using Microsoft.AspNetCore.Mvc;
+using XF.Domain.Exceptions;
+
+namespace CallCenter.Api.Controllers
+{
+    public class SettingController : BaseController
+    {
+        private readonly IVoiceFileDomainService _voiceFileDomainService;
+        private readonly ISystemSettingRepository _systemSettingsRepository;
+        private readonly ISystemSettingGroupRepository _systemSettingGroupRepository;
+
+        public SettingController(IVoiceFileDomainService voiceFileDomainService, ISystemSettingRepository systemSettingsRepository, ISystemSettingGroupRepository systemSettingGroupRepository)
+        {
+            _voiceFileDomainService = voiceFileDomainService;
+            _systemSettingsRepository = systemSettingsRepository;
+            _systemSettingGroupRepository = systemSettingGroupRepository;
+        }
+
+        /// <summary>
+        /// 查询语音文件
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("voicequerylist")]
+        public async Task<List<string>> VoiceQueryList()
+        {
+            return await _voiceFileDomainService.VoiceQueryListAsync(HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 删除语音文件
+        /// </summary>
+        /// <param name="voiceFileName"></param>
+        /// <returns></returns>
+        [HttpPost("removevoicefile")]
+        public async Task RemoveVoiceFile(string voiceFileName)
+        {
+            await _voiceFileDomainService.RemoveVoiceFileAsync(new RemoveVoiceFileRequest(voiceFileName), HttpContext.RequestAborted);
+        }
+
+        #region 系统参数
+        /// <summary>
+        /// 获取系统参数列表
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("getsyssettings")]
+        public async Task<List<SystemSettingGroup>> GetSysSettingsAsync()
+        {
+           //return await _systemSettingsRepository.QueryAsync(x => true);
+
+           return await _systemSettingGroupRepository.QueryExtAsync(x => true, x => x.Includes(d => d.SystemSettings));
+
+        }
+
+        /// <summary>
+        /// 更新配置
+        /// </summary>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        /// <exception cref="UserFriendlyException"></exception>
+        [HttpPost("modifysettings")]
+        public async Task ModifySettings([FromBody] ModifySettingsRequest request)
+        {
+            for (int i = 0; i < request.list.Count; i++)
+            {
+                var model = await _systemSettingsRepository.GetAsync(x => x.Id == request.list[i].id,HttpContext.RequestAborted);
+                if (model != null)
+                {
+                    model.SettingValue = request.list[i].value;
+                    await _systemSettingsRepository.UpdateAsync(model, HttpContext.RequestAborted);
+                }
+            }
+        }
+
+        #endregion
+
+
+
+    }
+}

+ 445 - 0
src/CallCenter.Api/Controllers/TelController.cs

@@ -0,0 +1,445 @@
+using CallCenter.Caches;
+using CallCenter.Calls;
+using CallCenter.Devices;
+using CallCenter.Repository.SqlSugar;
+using CallCenter.Share.Dtos;
+using CallCenter.Share.Enums;
+using CallCenter.Tels;
+using CallCenter.Users;
+using MapsterMapper;
+using Microsoft.AspNetCore.Mvc;
+using System.Transactions;
+using XF.Domain.Cache;
+using XF.Domain.Exceptions;
+using XF.Domain.Https;
+using XF.Utility.EnumExtensions;
+
+namespace CallCenter.Api.Controllers
+{
+    /// <summary>
+    /// 话机相关接口
+    /// </summary>
+    public class TelController : BaseController
+    {
+        private readonly ITelDomainService _telDomainService;
+        private readonly IUserCacheManager _userCacheManager;
+        private readonly ITelRepository _telRepository;
+        private readonly ITelGroupRepository _telGroupRepository;
+        private readonly IWorkRepository _workRepository;
+        private readonly ISessionContext _sessionContext;
+        private readonly ITypedCache<Tel> _cacheTel;
+        private readonly ITypedCache<TelGroup> _cacheTelGroup;
+        private readonly ITelCacheManager _telCacheManager;
+        private readonly IDeviceManager _deviceManager;
+        private readonly IMapper _mapper;
+        private readonly ICallDetailRepository _callDetailRepository;
+        private readonly ICallRepository _callRepository;
+
+        public TelController(
+            ITelDomainService telDomainService,
+            IUserCacheManager userCacheManager,
+            ITelRepository telRepository,
+            ITelGroupRepository telGroupRepository,
+            IWorkRepository workRepository,
+            ISessionContext sessionContext,
+            ITypedCache<Tel> cacheTel,
+            ITypedCache<TelGroup> cacheTelGroup,
+            ITelCacheManager telCacheManager,
+            IDeviceManager deviceManager,
+            IMapper mapper,
+            ICallDetailRepository callDetailRepository,
+            ICallRepository callRepository)
+        {
+            _telDomainService = telDomainService;
+            _userCacheManager = userCacheManager;
+            _telRepository = telRepository;
+            _telGroupRepository = telGroupRepository;
+            _workRepository = workRepository;
+            _sessionContext = sessionContext;
+            _cacheTel = cacheTel;
+            _cacheTelGroup = cacheTelGroup;
+            _telCacheManager = telCacheManager;
+            _deviceManager = deviceManager;
+            _mapper = mapper;
+            _callDetailRepository = callDetailRepository;
+            _callRepository = callRepository;
+        }
+        /// <summary>
+        /// 查询所有分机
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("tels")]
+        public async Task<IReadOnlyList<TelDto>> QueryTels()
+        {
+            var tels = await _telRepository.QueryExtAsync(d => !d.IsDeleted, d => d.Includes(x => x.Groups));
+            return _mapper.Map<IReadOnlyList<TelDto>>(tels);
+        }
+
+        /// <summary>
+        /// 查询所有分机(高频)
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("tels-frequency")]
+        public async Task<IReadOnlyList<TelDto>> QueryTelsFrequency()
+        {
+            var tels = _telCacheManager.GetTels();
+            var dtos = new List<TelDto>();
+            foreach (var tel in tels)
+            {
+                var telModel = _mapper.Map<TelDto>(_telCacheManager.GetTel(tel.No));
+                var telDto = await _deviceManager.QueryTelAsync(tel.No, HttpContext.RequestAborted);
+
+                if (telDto!=null)
+                {
+                    telModel.CPN = telDto.CPN;
+                    telModel.CDPN = telDto.CDPN;
+                    telModel.TelStatusInfo = telDto.TelStatusInfo;
+                    telModel.ConversationId = telDto.ConversationId;
+                    if (telModel.TelStatus == Share.Enums.ETelStatus.Active)
+                        telModel.TelStatus = telDto.TelStatus;
+
+                    if (telModel.TelStatusInfo == ETelStatusInfo.Out)
+                    {
+                        var temp = await _callRepository.GetAsync(x => x.FromNo == telDto.CPN && x.ToNo == telDto.CDPN && x.CallDirection == ECallDirection.Out && x.ConversationId == telModel.ConversationId);
+                        telModel.CallId = temp?.Id;
+                    }
+                    else if (telModel.TelStatusInfo == ETelStatusInfo.Into)
+                    {
+                        var temp = await _callRepository.GetAsync(x => x.FromNo == telDto.CPN && x.ToNo == telDto.CDPN && x.CallDirection == ECallDirection.In && x.ConversationId == telModel.ConversationId);
+                        telModel.CallId = temp?.Id;
+                    }
+                }
+                dtos.Add(telModel);
+            }
+            return dtos;
+        }
+
+        /// <summary>
+        /// 查询当前用户分机(高频)
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("tel-frequency")]
+        public async Task<TelDto>  QueryTelByToken()
+        {
+            var userid = _sessionContext.RequiredUserId;
+            var work = _userCacheManager.GetWorkByUser(userid);
+            var telModel = _mapper.Map<TelDto>(_telCacheManager.GetTel(work.TelNo));
+            var telDto = await _deviceManager.QueryTelAsync(work.TelNo, HttpContext.RequestAborted);
+
+            if (telDto!=null)
+            {
+                telModel.CPN = telDto.CPN;
+                telModel.CDPN = telDto.CDPN;
+                telModel.TelStatusInfo = telDto.TelStatusInfo;
+                telModel.ConversationId = telDto.ConversationId;
+                if (telModel.TelStatus == Share.Enums.ETelStatus.Active)
+                    telModel.TelStatus = telDto.TelStatus;
+
+                if(telModel.TelStatusInfo== ETelStatusInfo.Out)
+                {
+                    var temp = await _callRepository.GetAsync(x => x.FromNo == telDto.CPN && x.ToNo == telDto.CDPN && x.CallDirection== ECallDirection.Out && x.ConversationId == telModel.ConversationId);
+                    telModel.CallId = temp?.Id;
+                }
+                else if(telModel.TelStatusInfo == ETelStatusInfo.Into)
+                {
+                    var temp = await _callRepository.GetAsync(x => x.FromNo == telDto.CPN && x.CallDirection == ECallDirection.In && x.ConversationId == telModel.ConversationId);
+                    telModel.CallId = temp?.Id;
+                }
+            }
+            return telModel;
+        }
+
+
+        /// <summary>
+        /// 查询所有分机组
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("groups")]
+        public async Task<IReadOnlyList<TelGroupDto>> QueryTelGroups()
+        {
+            var groups = await _telGroupRepository.QueryExtAsync(d => true, d => d.Includes(x => x.Tels));
+            return _mapper.Map<IReadOnlyList<TelGroupDto>>(groups);
+        }
+
+        /// <summary>
+        /// 新增分机组
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("group")]
+        public async Task AddTelGroup([FromBody] AddTelGroupDto dto)
+        {
+            var works = await _workRepository.QueryAsync(d => dto.TelNos.Contains(d.TelNo) && !d.EndTime.HasValue);
+
+            await _deviceManager.AssginConfigGroupAsync(
+                dto.No,
+                dto.Distribution.ToString().ToLower(),
+                ext: works.Select(d => d.TelNo).ToList(),
+                voiceFile: dto.Voice ?? null,
+                cancellationToken: HttpContext.RequestAborted);
+
+            var group = _mapper.Map<TelGroup>(dto);
+            var tels = await _telRepository.QueryAsync(d => dto.TelNos.Contains(d.No));
+            group.Tels = tels;
+            await _telGroupRepository.AddNavTelsAsync(group, HttpContext.RequestAborted);
+            _cacheTelGroup.Remove(dto.No);
+        }
+
+        /// <summary>
+        /// 更新分机组
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPut("group")]
+        public async Task UpdateTelGroup([FromBody] UpdateTelGroupDto dto)
+        {
+            var works = await _workRepository.QueryAsync(d => dto.TelNos.Contains(d.TelNo) && !d.EndTime.HasValue);
+
+            await _deviceManager.AssginConfigGroupAsync(
+                dto.No,
+                dto.Distribution.ToString().ToLower(),
+                ext: works.Select(d => d.TelNo).ToList(),
+                voiceFile: dto.Voice ?? null,
+                cancellationToken: HttpContext.RequestAborted);
+
+            var group = _mapper.Map<TelGroup>(dto);
+            var tels = await _telRepository.QueryAsync(d => dto.TelNos.Contains(d.No));
+            group.Tels = tels;
+            await _telGroupRepository.UpdateNavTelsAsync(group, HttpContext.RequestAborted);
+            _cacheTelGroup.Remove(dto.No);
+        }
+
+        /// <summary>
+        /// 分机休息
+        /// </summary>
+        /// <returns></returns>
+        [HttpPut("rest")]
+        public async Task Rest()
+        {
+            var work = _userCacheManager.GetWorkByUser(_sessionContext.RequiredUserId);
+            if (work is null)
+                throw new UserFriendlyException("当前坐席暂未进行工作");
+            await _telDomainService.RestAsync(work, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 分机结束休息
+        /// </summary>
+        [HttpPut("unrest")]
+        public async Task<TelRestDto> UnRest()
+        {
+            var work = _userCacheManager.GetWorkByUser(_sessionContext.RequiredUserId);
+            if (work is null)
+                throw new UserFriendlyException("当前坐席暂未进行工作");
+            var telRest = await _telDomainService.UnRestAsync(work.TelId, HttpContext.RequestAborted);
+            return _mapper.Map<TelRestDto>(telRest);
+        }
+
+        /// <summary>
+        /// 保持通话
+        /// </summary>
+        [HttpPut("hold")]
+        public async Task Hold()
+        {
+            var work = _userCacheManager.GetWorkByUser(_sessionContext.RequiredUserId);
+            if (work is null)
+                throw new UserFriendlyException("当前坐席暂未进行工作");
+            await _telDomainService.HoldAsync(work.TelId, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 恢复通话(解除hold状态)
+        /// </summary>
+        [HttpPut("unhold")]
+        public async Task UnHold()
+        {
+            var work = _userCacheManager.GetWorkByUser(_sessionContext.RequiredUserId);
+            if (work is null)
+                throw new UserFriendlyException("当前坐席暂未进行工作");
+            await _telDomainService.UnHoldAsync(work.TelId, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 根据设备自动同步分机数据到数据库
+        /// </summary>
+        /// <returns></returns>
+        [HttpPut("sync-tels")]
+        public async Task SyncTelsAsync()
+        {
+            var dbTels = await _telRepository.QueryAsync();
+            foreach (var dbTel in dbTels)
+            {
+                _cacheTel.Remove(dbTel.No);
+            }
+            //await _telRepository.RemoveRangeAsync(dbTels);
+            var tels = await _telDomainService.QueryTelsAsync(HttpContext.RequestAborted);
+            var list = tels.ExceptBy(dbTels.Select(e => e.No), x => x.No).ToList();
+            var delList = dbTels.ExceptBy(tels.Select(e => e.No), x => x.No).ToList();
+
+            for (int i = 0; i < delList.Count; i++)
+            {
+                delList[i].SoftDelete();
+            }
+            await _telRepository.UpdateRangeAsync(delList);
+            await _telRepository.AddRangeAsync(list);
+        }
+
+        /// <summary>
+        /// 页面基础信息
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("base-info")]
+        public async Task<dynamic> GetBaseInfo()
+        {
+            return new
+            {
+                Distributions = EnumExts.GetDescriptions<EDistribution>()
+            };
+        }
+
+        /// <summary>
+        /// 分机呼分机
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("tel-to-tel")]
+        public async Task TelToTel([FromBody] TelToTelDto dto)
+        {
+            var work = _userCacheManager.GetWorkByUser(_sessionContext.RequiredUserId);
+            if (work is null)
+                throw new UserFriendlyException("当前坐席暂未进行工作");
+            await _deviceManager.ExtToExtAsync(work.TelNo, dto.TelNo, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 分机拨打外部电话
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("tel-to-outer")]
+        public async Task TelToOuter([FromBody] TelToOuterDto dto)
+        {
+            var work = _userCacheManager.GetWorkByUser(_sessionContext.RequiredUserId);
+            if (work is null)
+                throw new UserFriendlyException("当前坐席暂未进行工作");
+            await _deviceManager.ExtToOuterAsync(work.TelNo, dto.OuterNo, HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 来电转分机
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("visitor-to-tel")]
+        public async Task VisitorToTel([FromBody] VisitorToTelDto dto)
+        {
+            var work = _userCacheManager.GetWorkByUser(_sessionContext.RequiredUserId);
+            if (work is null)
+                throw new UserFriendlyException("当前坐席暂未进行工作");
+            var tel = await _deviceManager.QueryTelAsync(work.TelNo, HttpContext.RequestAborted);
+            if (!string.IsNullOrEmpty(tel.ConversationId))
+                await _deviceManager.VisitorToExtAsync(tel.ConversationId, dto.TelNo, HttpContext.RequestAborted);
+            else
+                throw new UserFriendlyException("当前分机没有通话");
+        }
+
+        /// <summary>
+        /// 来电转外部电话
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("visitor-to-outer")]
+        public async Task VisitorToOuter([FromBody] VisitorToOuterDto dto)
+        {
+            var work = _userCacheManager.GetWorkByUser(_sessionContext.RequiredUserId);
+            if (work is null)
+                throw new UserFriendlyException("当前坐席暂未进行工作");
+            var tel = await _deviceManager.QueryTelAsync(work.TelNo, HttpContext.RequestAborted);
+            if (!string.IsNullOrEmpty(tel.ConversationId))
+                await _deviceManager.VisitorToOuterAsync(tel.ConversationId, dto.OuterNo, HttpContext.RequestAborted);
+            else
+                throw new UserFriendlyException("当前分机没有通话");
+        }
+
+
+        /// <summary>
+        /// 来电转分机组队列
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        [HttpPost("visitor-to-group")]
+        public async Task VisitorToGroup([FromBody] VisitorToGroupDto dto)
+        {
+            var work = _userCacheManager.GetWorkByUser(_sessionContext.RequiredUserId);
+            if (work is null)
+                throw new UserFriendlyException("当前坐席暂未进行工作");
+            var tel = await _deviceManager.QueryTelAsync(work.TelNo, HttpContext.RequestAborted);
+            if (!string.IsNullOrEmpty(tel.ConversationId))
+                await _deviceManager.VisitorToGroupAsync(tel.ConversationId, dto.groupid, HttpContext.RequestAborted);
+            else
+                throw new UserFriendlyException("当前分机没有通话");
+        }
+
+        /// <summary>
+        /// 去电转外部电话
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        /// <exception cref="UserFriendlyException"></exception>
+        [HttpPost("outer-to-outer")]
+        public async Task OuterToOuter([FromBody]OuterToOuterDto dto)
+        {
+            var work = _userCacheManager.GetWorkByUser(_sessionContext.RequiredUserId);
+            if (work is null)
+                throw new UserFriendlyException("当前坐席暂未进行工作");
+            var tel = await _deviceManager.QueryTelAsync(work.TelNo, HttpContext.RequestAborted);
+            if (!string.IsNullOrEmpty(tel.ConversationId))
+                await _deviceManager.OuterToOuterAsync(tel.ConversationId, dto.OuterNo, HttpContext.RequestAborted);
+            else
+                throw new UserFriendlyException("当前分机没有通话");
+        }
+
+        /// <summary>
+        /// 去电转分机
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        /// <exception cref="UserFriendlyException"></exception>
+        [HttpPost("outer-to-tel")]
+        public async Task OuterToTel([FromBody]OuterToTelDto dto)
+        {
+            var work = _userCacheManager.GetWorkByUser(_sessionContext.RequiredUserId);
+            if (work is null)
+                throw new UserFriendlyException("当前坐席暂未进行工作");
+            var tel = await _deviceManager.QueryTelAsync(work.TelNo, HttpContext.RequestAborted);
+            if (!string.IsNullOrEmpty(tel.ConversationId))
+                await _deviceManager.OuterToExtAsync(tel.ConversationId, dto.TelNo, HttpContext.RequestAborted);
+            else
+                throw new UserFriendlyException("当前分机没有通话");
+        }
+
+        /// <summary>
+        /// 三方会议
+        /// 先建立两方通话,然后调用保持通话接口,拨通第三方分机,然后再调用三方会议接口
+        /// 1. 分机 A 正在和 B 通话;
+        /// 2. 分机 A 把原通话呼叫保持;
+        /// 3. 分机 A 向 C 发起新的呼叫,并建立通话;
+        /// 4. 此时,使用该 API 能够实现以分机 A 为主持方建立 A、B、C 的三方会议。
+        /// </summary>
+        /// <param name="dto">TelNo:会议发起方分机号</param>
+        /// <returns></returns>
+        /// <exception cref="UserFriendlyException"></exception>
+        [HttpPost("meeting")]
+        public async Task Conference([FromBody]ConferenceDto dto)
+        {
+            var work = _userCacheManager.GetWorkByUser(_sessionContext.RequiredUserId);
+            if (work is null)
+                throw new UserFriendlyException("当前坐席暂未进行工作");
+            var tel = await _deviceManager.QueryTelAsync(work.TelNo, HttpContext.RequestAborted);
+            if (!string.IsNullOrEmpty(tel.ConversationId))
+                await _deviceManager.ConferenceMeetingAsync(dto.TelNo, HttpContext.RequestAborted);
+            else
+                throw new UserFriendlyException("当前分机没有通话");
+        }
+
+    }
+}

+ 118 - 0
src/CallCenter.Api/Controllers/TestController.cs

@@ -0,0 +1,118 @@
+using CallCenter.Api.Realtimes;
+using CallCenter.BlackLists;
+using CallCenter.Devices;
+using CallCenter.Ivrs;
+using CallCenter.Realtimes;
+using CallCenter.Users;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using NewRock.Sdk;
+using NewRock.Sdk.Security;
+using XF.Domain.Cache;
+using XF.Domain.Https;
+
+namespace CallCenter.Api.Controllers
+{
+    public class TestController : BaseController
+    {
+        private readonly ILogger<TestController> _logger;
+        private readonly IAuthorizeGenerator _authorizeGenerator;
+        private readonly IOptionsSnapshot<DeviceConfigs> _options;
+        private readonly ISessionContext _sessionContext;
+        private readonly IUserRepository _userRepository;
+
+        private readonly ITypedCache<User> _cache;
+        private readonly IRealtimeService _realtimeService;
+        private readonly IBlacklistDomainService _blacklistDomainService;
+        private readonly IIvrDomainService _ivrDomainService;
+
+        //private readonly ITypedCache<List<User>> _cache;
+        //private readonly ICacheManager<User> _cache;
+
+        public TestController(
+            INewRockClient client,
+            ILogger<TestController> logger,
+            IAuthorizeGenerator authorizeGenerator,
+            IOptionsSnapshot<DeviceConfigs> options,
+            ISessionContext sessionContext,
+            IUserRepository userRepository,
+            //ICacheManager<User> cache
+            //ITypedCache<List<User>> cache
+            ITypedCache<User> cache,
+            IRealtimeService realtimeService,
+            IBlacklistDomainService blacklistDomainService,
+            IIvrDomainService ivrDomainService
+            )
+        {
+            _logger = logger;
+            _authorizeGenerator = authorizeGenerator;
+            _options = options;
+            _sessionContext = sessionContext;
+            _userRepository = userRepository;
+            _cache = cache;
+            _realtimeService = realtimeService;
+            _blacklistDomainService = blacklistDomainService;
+            _ivrDomainService = ivrDomainService;
+        }
+
+        [HttpGet]
+        public async Task Test1()
+        {
+            var user = await _userRepository.GetAsync("08da8016-72af-48b3-8c8f-b39251229f79");
+            _cache.Add(user.Id, user, ExpireMode.None, TimeSpan.FromMinutes(1));
+            var user1 = _cache.Get(user.Id);
+            user1.NickName = "aaa";
+            _cache.Update(user.Id, d => user1);
+
+            //var config = new ConfigurationBuilder()
+            //    .WithUpdateMode(CacheUpdateMode.Up)
+            //    .WithMicrosoftMemoryCacheHandle()
+            //    .And
+            //    .WithRedisConfiguration("redis", d =>
+            //    {
+            //        d.WithDatabase(0)
+            //            .WithEndpoint("redis.fengwo.com", 6380);
+            //    })
+            //    .WithSerializer(typeof(SystemTextJsonSerializer), new JsonSerializerOptions
+            //    {
+            //        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+            //        DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
+            //        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+            //    })
+            //    .WithRedisBackplane("redis")
+            //    .WithRedisCacheHandle("redis", true)
+            //.Build();
+
+
+            //var cache = CacheFactory.FromConfiguration<User>(config);
+            //var a = cache.Add(new CacheItem<User>(user.Id, user, ExpirationMode.None, TimeSpan.FromMinutes(5)));
+            //var user1 = cache.Get<User>(user.Id);
+
+            //var a = _cache.Add(user.Id, user, ExpireMode.None, TimeSpan.FromMinutes(5));
+            //var user1 = _cache.Get(user.Id);
+
+            //var users = await _userRepository.QueryAsync(d => true);
+
+
+            //_cache.Add("users", users, ExpireMode.None);
+            //var u1 = _cache.Get("users");
+        }
+
+        /// <summary>
+        /// signalR测试(method: Ring)
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("ring")]
+        public async Task RingTest()
+        {
+            await _realtimeService.RingAsync(_sessionContext.RequiredUserId, new RingDto { From = "13512341234" }, HttpContext.RequestAborted);
+        }
+
+        [HttpGet("t2")]
+        public async Task GetVoiceEndAnswerAsyncTest()
+        {
+            var answer = await _ivrDomainService.GetVoiceEndAnswerAsync("3", HttpContext.RequestAborted);
+            Console.WriteLine(answer.Content);
+        }
+    }
+}

+ 794 - 0
src/CallCenter.Api/Controllers/TestSdkController.cs

@@ -0,0 +1,794 @@
+using CallCenter.Devices;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using NewRock.Sdk;
+using NewRock.Sdk.Control.Request;
+using NewRock.Sdk.Control.Request.Base;
+using NewRock.Sdk.Control.Response;
+using NewRock.Sdk.Manage.Request;
+using NewRock.Sdk.Transfer.Conference.Request;
+using NewRock.Sdk.Transfer.Connect.Request;
+using NewRock.Sdk.Transfer.Queue.Request;
+using System.Text.RegularExpressions;
+using XF.Domain.Exceptions;
+using Ext = NewRock.Sdk.Control.Request.Base.Ext;
+using Group = NewRock.Sdk.Control.Request.Group;
+using VisitorToExtVisitor = NewRock.Sdk.Transfer.Connect.Request.VisitorToExtVisitor;
+
+namespace CallCenter.Api.Controllers
+{
+    /// <summary>
+    /// 设备测试专用,外部禁止调用
+    /// </summary>
+    public class TestSdkController : BaseController
+    {
+        private readonly INewRockClient _client;
+        private readonly ILogger<TestController> _logger;
+        private readonly IOptionsSnapshot<DeviceConfigs> _options;
+
+        public TestSdkController(
+            INewRockClient client,
+            ILogger<TestController> logger,
+            IOptionsSnapshot<DeviceConfigs> options
+            )
+        {
+            _client = client;
+            _logger = logger;
+            _options = options;
+        }
+
+        #region  查询(Query)
+
+        /// <summary>
+        /// 查询设备
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("QueryDeviceInfo")]
+        public async Task QueryDeviceInfo()
+        {
+            var result = await _client.QueryDeviceInfo(
+                new QueryDeviceInfoRequest { Attribute = "Query", DeviceInfo = string.Empty },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+            _logger.LogInformation(System.Text.Json.JsonSerializer.Serialize(result));
+        }
+
+
+        /// <summary>
+        /// 查询分机
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("QueryExt")]
+        public async Task QueryExt(string extid)
+        {
+            var result = await _client.QueryExt(
+                new QueryExtRequest() { Attribute = "Query", Ext = new Ext() { Id = extid } },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted
+            );
+            _logger.LogInformation(System.Text.Json.JsonSerializer.Serialize(result));
+        }
+
+        /// <summary>
+        /// 查询分机组
+        /// </summary>
+        /// <param name="groupid"></param>
+        /// <returns></returns>
+        [HttpPost("QueryExtGroup")]
+        public async Task QueryExtGroup(string? groupid)
+        {
+            var result = await _client.QueryExtGroup(new QueryExtGroupRequest()
+            {
+                Attribute = "Query",
+                Group = new QueryExtGroup() { Id = groupid }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+            _logger.LogInformation(System.Text.Json.JsonSerializer.Serialize(result));
+        }
+
+        /// <summary>
+        /// 查询语音菜单
+        /// </summary>
+        /// <param name="menuid"></param>
+        /// <returns></returns>
+        [HttpPost("QueryMenu")]
+        public async Task QueryMenu(string menuid)
+        {
+            var result = await _client.QueryMenu(new QueryMenuRequest()
+            {
+                Attribute = "Query",
+                Menu = new QueryMenuMenu() { Id = menuid }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+            _logger.LogInformation(System.Text.Json.JsonSerializer.Serialize(result));
+        }
+
+        /// <summary>
+        /// 查询中继
+        /// </summary>
+        /// <param name="trunkid"></param>
+        /// <returns></returns>
+        [HttpPost("QueryTrunk")]
+
+        public async Task QueryTrunk(string trunkid)
+        {
+            var result = await _client.QueryTrunk(new QueryTrunkRequest()
+            {
+                Attribute = "Query",
+                Trunk = new QueryTrunkTrunk() { Id = trunkid }
+            },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+            _logger.LogInformation(System.Text.Json.JsonSerializer.Serialize(result));
+        }
+
+        /// <summary>
+        /// 查询来电
+        /// </summary>
+        /// <param name="visitorid"></param>
+        /// <returns></returns>
+        [HttpPost("QueryVisitor")]
+        public async Task QueryVisitor(string visitorid)
+        {
+            var result = await _client.QueryVisitor(new QueryVisitorRequest()
+            {
+                Attribute = "Query",
+                Visitor = new QueryVisitorVisitor() { Id = visitorid }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+            _logger.LogInformation(System.Text.Json.JsonSerializer.Serialize(result));
+        }
+
+        /// <summary>
+        /// 查询去电
+        /// </summary>
+        /// <param name="outerid"></param>
+        /// <returns></returns>
+        [HttpPost("QueryOuter")]
+        public async Task QueryOuter(string outerid)
+        {
+            var result = await _client.QueryOuter(new QueryOuterRequest()
+            {
+                Attribute = "Query",
+                Outer = new QueryOuterOuter() { Id = outerid }
+            },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+            _logger.LogInformation(System.Text.Json.JsonSerializer.Serialize(result));
+        }
+
+        #endregion
+
+        #region 强拆
+
+        /// <summary>
+        /// 强拆分机
+        /// </summary>
+        /// <param name="extid"></param>
+        /// <returns></returns>
+        [HttpPost("ClearExt")]
+        public async Task ClearExt(string extid)
+        {
+            var result = await _client.ClearCall(new ClearCallRequest()
+            {
+                Attribute = "Clear",
+                Ext = new Ext()
+                {
+                    Id = extid
+                },
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 强拆来电
+        /// </summary>
+        /// <param name="visitorId"></param>
+        /// <returns></returns>
+        [HttpPost("ClearVisitor")]
+        public async Task ClearVisitor(string visitorId)
+        {
+            var result = await _client.ClearCall(new ClearCallRequest()
+            {
+                Attribute = "Clear",
+                Visitor = new ClearCallVisitor()
+                {
+                    Id = visitorId
+                }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 强拆去电
+        /// </summary>
+        /// <param name="outerId"></param>
+        /// <returns></returns>
+        [HttpPost("ClearOuter")]
+        public async Task ClearOuter(string outerId)
+        {
+            var result = await _client.ClearCall(new ClearCallRequest()
+            {
+                Attribute = "Clear",
+                Outer = new ClearCallOuter()
+                {
+                    Id = outerId
+                }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        #endregion
+
+        #region 配置(Assign)
+
+        /// <summary>
+        /// 配置分机
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("ConfigExt")]
+        public async Task ConfigExt()
+        {
+            var result = await _client.ConfigExt(new AssginConfigExtRequest()
+            {
+                Attribute = "Assign",
+                Ext = new ConfigExt()
+                {
+                    Lineid = "IPPhone 21",
+                    Id = "212",
+                    //Staffid = "038",
+                    Groups = new List<string>()
+                    {
+                        "1",
+                        "2",
+                    },
+                    //VoiceFile = "user_tts164815.dat",
+                    //Call_Restriction = "2",
+                    //Call_Pickup ="no",
+                    No_Disturb = "on",
+                    Fwd_Type = "0",
+                    //Record="on",
+                    Api = "7"
+                }
+            },
+            _options.Value.ReceiveKey,
+            _options.Value.Expired,
+            HttpContext.RequestAborted);
+            _logger.LogInformation(System.Text.Json.JsonSerializer.Serialize(result));
+        }
+
+
+        /// <summary>
+        /// 配置分机组
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("ConfigExtGroup")]
+        public async Task ConfigExtGroup()
+        {
+            var result = await _client.ConfigExtGroup(new AssginConfigGroupRequest()
+            {
+                Attribute = "Assign",
+                Group = new Group()
+                {
+                    Id = "1",
+                    Voicefile = "NewMorning",
+                    Distribution = "sequential",
+                    Ext = new List<string>()
+                    {
+                        "209",
+                        "210"
+                    }
+                }
+            }, _options.Value.ReceiveKey,
+            _options.Value.Expired,
+            HttpContext.RequestAborted);
+            _logger.LogInformation(System.Text.Json.JsonSerializer.Serialize(result));
+        }
+
+        /// <summary>
+        /// 配置语音菜单
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("ConfigMenu")]
+        public async Task ConfigMenu()
+        {
+            var result = await _client.ConfigMenu(new AssginConfigMenuRequest()
+            {
+                Attribute = "Assign",
+                Menu = new AssginConfigMenuMenu()
+                {
+                    Id = "1",
+                    VoiceFile = "user_tts131742",
+                    Exit = "#",
+                    Repeat = "3",
+                    InfoLength = "5",
+                }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        #endregion
+
+        #region 呼叫保持和接回
+
+        /// <summary>
+        /// 呼叫保持
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("Hold")]
+        public async Task Hold(string extid)
+        {
+            var result = await _client.HoldOrUnHold(new HoldSetRequest()
+            {
+                Attribute = "Hold",
+                Ext = new Ext() { Id = extid }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+            //_logger.LogInformation(result.Manufacturer);
+        }
+
+        /// <summary>
+        /// 呼叫接回
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("UnHold")]
+        public async Task UnHold(string extid)
+        {
+            var result = await _client.HoldOrUnHold(new HoldSetRequest()
+            {
+                Attribute = "Unhold",
+                Ext = new Ext() { Id = extid }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        #endregion
+
+        #region 静音开启与解除
+
+        /// <summary>
+        /// 静音
+        /// </summary>
+        /// <param name="extid"></param>
+        /// <returns></returns>
+        [HttpPost("Mute")]
+        public async Task Mute(string extid)
+        {
+            var result = await _client.MuteOrUnMute(new MuteSetRequest
+            {
+                Attribute = "Mute",
+                Ext = new Ext() { Id = extid }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 解除静音
+        /// </summary>
+        /// <param name="extid"></param>
+        /// <returns></returns>
+        [HttpPost("UnMute")]
+        public async Task UnMute(string extid)
+        {
+            var result = await _client.MuteOrUnMute(new MuteSetRequest
+            {
+                Attribute = "Unmute",
+                Ext = new Ext() { Id = extid }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        #endregion
+
+        #region 呼叫转接命令(Transfer)
+
+        #region 连接(Connect)
+
+        /// <summary>
+        /// 分机呼分机
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("ExtensionToExtension")]
+        public async Task ExtensionToExtension(string fromextid, string toextid)
+        {
+            var result = await _client.ExtensionToExtension(new ExtensionToExtensionRequest()
+            {
+                Attribute = "Connect",
+                Exts = new List<ExtToExtExt>()
+                    {
+                        new ExtToExtExt() { Id = fromextid },
+                        new ExtToExtExt() { Id = toextid }
+                    }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+            _logger.LogInformation(System.Text.Json.JsonSerializer.Serialize(result));
+        }
+
+        /// <summary>
+        /// 分机呼外部电话
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("ExtToOuter")]
+        public async Task ExtToOuter(string fromextid, string to)
+        {
+            var result = await _client.ExtToOuter(new ExtToOuterRequest()
+            {
+                Attribute = "Connect",
+                Ext = new ExtToOuterExtRequest() { Id = fromextid },
+                Outer = new ExtToOuterOuterRequest() { To = to }
+            },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 来电转分机
+        /// </summary>
+        /// <returns></returns>
+        [HttpPost("VisitorToExt")]
+        public async Task VisitorToExt(string visid, string toextid)
+        {
+            var result = await _client.VisitorToExt(new VisitorToExtRequest()
+            {
+                Attribute = "Connect",
+                Visitor = new VisitorToExtVisitor() { Id = visid },
+                Ext = new VisitorToExtExt() { Id = toextid }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 来电转外部电话
+        /// </summary>
+        /// <param name="visid"></param>
+        /// <param name="outerphonenum"></param>
+        /// <param name="display"></param>
+        /// <returns></returns>
+        [HttpPost("VisitorToOuter")]
+        public async Task VisitorToOuter(string visid, string outerphonenum, string display = "")
+        {
+            var result = await _client.VisitorToOuter(new VisitorToOuterRequest()
+            {
+                Attribute = "Connect",
+                Visitor = new VisitorToOuterVisitor() { Id = visid },
+                Outer = new VisitorToOuterOuter() { To = outerphonenum, Display = display },
+
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 来电转语音菜单
+        /// </summary>
+        /// <param name="visid"></param>
+        /// <param name="menuid"></param>
+        /// <returns></returns>
+        [HttpPost("VisitorToMenu")]
+        public async Task VisitorToMenu(string visid, string menuid)
+        {
+            var result = await _client.VisitorToMenu(new VisitorToMenuRequest()
+            {
+                Attribute = "Connect",
+                Visitor = new VisitorToMenuVisitor() { Id = visid },
+                Menu = new VisitorToMenuMenu() { Id = menuid }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 去电转分机
+        /// </summary>
+        /// <param name="outer"></param>
+        /// <param name="extid"></param>
+        /// <returns></returns>
+        [HttpPost("OuterToExt")]
+        public async Task OuterToExt(string outer, string extid)
+        {
+            var result = await _client.OuterToExt(new OuterToExtRequest()
+            {
+                Attribute = "Connect",
+                Outer = new OuterToExtOuter() { Id = outer },
+                Ext = new OuterToExtExt() { Id = extid }
+            },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+
+        /// <summary>
+        /// 去电转外部电话
+        /// </summary>
+        /// <param name="outerid"></param>
+        /// <param name="outerphonenum"></param>
+        /// <returns></returns>
+        [HttpPost("OuterToOuter")]
+        public async Task OuterToOuter(string outerid, string outerphonenum)
+        {
+            var result = await _client.OuterToOuter(new OuterToOuterRequest()
+            {
+                Attribute = "Connect",
+                Outer = new List<OuterToOuterOuterModel>() { new OuterToOuterOuterModel() { Id = outerid }, new OuterToOuterOuterModel() { To = outerphonenum } },
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 去电转语音菜单
+        /// </summary>
+        /// <param name="outerid"></param>
+        /// <param name="menuid"></param>
+        /// <returns></returns>
+        [HttpPost("OuterToMenu")]
+        public async Task OuterToMenu(string outerid, string menuid)
+        {
+            var result = await _client.OuterToMenu(new OuterToMenuRequest()
+            {
+                Attribute = "Connect",
+                Outer = new OuterToMenuOuter() { Id = outerid },
+                Menu = new OuterToMenuMenu() { Id = menuid }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 语音菜单呼分机
+        /// </summary>
+        /// <param name="menuid"></param>
+        /// <param name="extid"></param>
+        /// <returns></returns>
+        [HttpPost("MenuToExt")]
+        public async Task MenuToExt(string menuid, string extid)
+        {
+            var result = await _client.MenuToExt(new MenuToExtRequest()
+            {
+                Attribute = "Connect",
+                Menu = new MenuToExtMenu() { Id = menuid },
+                Ext = new MenuToExtExt() { Id = extid }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 语音菜单呼外部电话
+        /// </summary>
+        /// <param name="menuid"></param>
+        /// <param name="outernum"></param>
+        /// <returns></returns>
+        [HttpPost("MenuToOuter")]
+        public async Task MenuToOuter(string menuid, string outernum)
+        {
+            var result = await _client.MenuToOuter(new MenuToOuterRequest()
+            {
+                Attribute = "Connect",
+                Menu = new MenuToOuterMenu() { Id = menuid },
+                Outer = new MenuToOuterOuter() { To = outernum }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+
+        /// <summary>
+        /// 双向呼叫(回拨)
+        /// </summary>
+        /// <param name="outerone"></param>
+        /// <param name="outertwo"></param>
+        /// <returns></returns>
+        [HttpPost("TwoWayOuter")]
+        public async Task TwoWayOuter(string outerone, string outertwo)
+        {
+            var result = await _client.TwoWayOuter(new TwoWayOuterRequest()
+            {
+                Attribute = "Connect",
+                Outer = new List<TwoWayOuterOuter>()
+                    {
+                        new TwoWayOuterOuter(){ To = outerone},
+                        new TwoWayOuterOuter(){ To = outertwo}
+                    }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 语音插播(分机)
+        /// </summary>
+        /// <param name="extid"></param>
+        /// <param name="visitorid"></param>
+        /// <param name="outerid"></param>
+        /// <param name="voicefile"></param>
+        /// <returns></returns>
+        [HttpPost("VoiceNewsFlash")]
+        public async Task VoiceNewsFlashExt(string voicefile, string extid)
+        {
+            var model = new VoiceNewsFlashRequest
+            {
+                Attribute = "Connect",
+                VoiceFile = voicefile,
+                Ext = new VoiceNewsFlashExt() { Id = extid }
+            };
+
+            var result = await _client.VoiceNewsFlash(model, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 语音插播(来电)
+        /// </summary>
+        /// <param name="voicefile"></param>
+        /// <param name="visiitorid"></param>
+        /// <returns></returns>
+        [HttpPost("VoiceNewsFlashVisitor")]
+        public async Task VoiceNewsFlashVisitor(string voicefile, string visiitorid)
+        {
+            var model = new VoiceNewsFlashRequest
+            {
+                Attribute = "Connect",
+                VoiceFile = voicefile,
+                Visitor = new VoiceNewsFlashVisitor() { Id = visiitorid }
+            };
+
+            var result = await _client.VoiceNewsFlash(model, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        /// 语音插播(去电)
+        /// </summary>
+        /// <param name="voicefile"></param>
+        /// <param name="outerid"></param>
+        /// <returns></returns>
+        [HttpPost("VoiceNewsFlashOuter")]
+        public async Task VoiceNewsFlashOuter(string voicefile, string outerid)
+        {
+            var model = new VoiceNewsFlashRequest
+            {
+                Attribute = "Connect",
+                VoiceFile = voicefile,
+                Outer = new VoiceNewsFlashOuter() { Id = outerid }
+            };
+
+            var result = await _client.VoiceNewsFlash(model, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+
+        #endregion
+
+
+        #region 会议(Conference)
+
+        /// <summary>
+        /// 会议
+        /// </summary>
+        /// <param name="extid"></param>
+        /// <returns></returns>
+        [HttpPost("ConferenceMeeting")]
+        public async Task ConferenceMeeting(string extid)
+        {
+            var result = await _client.ConferenceMeeting(new ConferenceMeetingRequest()
+            {
+                Attribute = "Conference",
+                Ext = new ConferenceMeetingExt() { Id = extid }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        #endregion
+
+
+        #region 队列(Queue)
+
+        /// <summary>
+        /// 来电转分机队列
+        /// </summary>
+        /// <param name="visitorid"></param>
+        /// <param name="extid"></param>
+        /// <returns></returns>
+        [HttpPost("VisitorToExtQueue")]
+        public async Task VisitorToExtQueue(string visitorid, string extid)
+        {
+            var result = await _client.VisitorToExtQueue(new VisitorToExtQueueRequest()
+            {
+                Attribute = "Queue",
+                Visitor = new VisitorToExtQueueVisitor() { Id = visitorid },
+                Ext = new VisitorToExtQueueExt() { Id = extid }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        /// <summary>
+        ///来电转分机组队列
+        /// </summary>
+        /// <param name="visitorid"></param>
+        /// <param name="groupid"></param>
+        /// <returns></returns>
+        [HttpPost("VisitorToGroupQueue")]
+        public async Task VisitorToGroupQueue(string visitorid, string groupid)
+        {
+            var result = await _client.VisitorToGroupQueue(new VisitorToGroupQueueRequest()
+            {
+                Attribute = "Queue",
+                Visitor = new VisitorToGroupQueueVisitor() { Id = visitorid },
+                Group = new VisitorToGroupQueueGroup() { Id = groupid }
+            },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        #endregion
+
+
+        #endregion
+
+
+        #region 语音管理命令
+
+        /// <summary>
+        /// 查询语音文件
+        /// </summary>
+        /// <returns></returns>
+        [HttpGet("VoiceQueryList")]
+        public async Task VoiceQueryList()
+        {
+            var result = await _client.VoiceQueryList(new VoiceQueryListRequest()
+            {
+                Attribute = "Query",
+                VoiceFile = ""
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+            _logger.LogInformation(System.Text.Json.JsonSerializer.Serialize(result));
+        }
+
+        /// <summary>
+        /// 删除语音文件
+        /// </summary>
+        /// <param name="voiceFile"></param>
+        /// <returns></returns>
+        [HttpPost("RemoveVoiceFile")]
+        public async Task RemoveVoiceFile(string voiceFile)
+        {
+            await _client.RemoveVoiceFile(new RemoveVoiceFileRequest()
+            {
+                Attribute = "Remove",
+                VoiceFile = voiceFile
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                HttpContext.RequestAborted);
+        }
+
+        #endregion
+
+    }
+}

+ 162 - 0
src/CallCenter.Api/Controllers/UserController.cs

@@ -0,0 +1,162 @@
+using CallCenter.Application;
+using CallCenter.Caches;
+using CallCenter.Share.Dtos;
+using CallCenter.Share.Requests;
+using CallCenter.Tels;
+using CallCenter.Users;
+using MapsterMapper;
+using Microsoft.AspNetCore.Mvc;
+using XF.Domain.Cache;
+using XF.Domain.Exceptions;
+using XF.Domain.Https;
+
+namespace CallCenter.Api.Controllers;
+
+public class UserController : BaseController
+{
+    private readonly ISessionContext _sessionContext;
+    private readonly IUserDomainService _userDomainService;
+    private readonly ITelRepository _telRepository;
+    private readonly IUserRepository _userRepository;
+    private readonly ITelCacheManager _telCacheManager;
+    private readonly IUserCacheManager _userCacheManager;
+    private readonly IMapper _mapper;
+
+    public UserController(
+        ISessionContext sessionContext,
+        IUserDomainService userDomainService,
+        ITelRepository telRepository,
+        IUserRepository userRepository,
+        ITelCacheManager telCacheManager,
+        IUserCacheManager userCacheManager,
+        IMapper mapper)
+    {
+        _sessionContext = sessionContext;
+        _userDomainService = userDomainService;
+        _telRepository = telRepository;
+        _userRepository = userRepository;
+        _telCacheManager = telCacheManager;
+        _userCacheManager = userCacheManager;
+        _mapper = mapper;
+    }
+
+    /// <summary>
+    /// 上班
+    /// </summary>
+    [HttpPost("on-duty/{telNo}")]
+    public async Task OnDuty([FromRoute] string? telNo)
+    {
+        if (string.IsNullOrEmpty(telNo))
+        {
+            var user = await _userRepository.GetAsync(d => !d.IsDeleted && d.Id == _sessionContext.RequiredUserId,
+                HttpContext.RequestAborted);
+            if (user == null)
+                throw new UserFriendlyException("无效用户编号");
+            if (string.IsNullOrEmpty(user.DefaultTelNo))
+                throw new UserFriendlyException("未设置默认分机号");
+            telNo = user.DefaultTelNo;
+        }
+        var tel = _telCacheManager.GetTel(telNo);
+        await _userDomainService.OnDutyAsync(_sessionContext.RequiredUserId, tel, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 下班
+    /// </summary>
+    [HttpPost("off-duty")]
+    public Task<WorkDto?> OffDuty()
+    {
+        return _userDomainService.OffDutyAsync(_sessionContext.RequiredUserId, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 分页查询用户
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("paged")]
+    public async Task<PagedDto<UserDto>> QueryPaged([FromQuery] UserPagedRequest request)
+    {
+        var (total, items) = await _userRepository.QueryPagedAsync(
+            d => !d.IsDeleted,
+            d => d.OrderByDescending(x => x.CreationTime),
+            request.PageIndex,
+            request.PageSize,
+            (!string.IsNullOrEmpty(request.PhoneNo), d => d.PhoneNo.Contains(request.PhoneNo!)),
+            (!string.IsNullOrEmpty(request.Name), d => d.Name.Contains(request.Name!)),
+            (!string.IsNullOrEmpty(request.NickName), d => !string.IsNullOrEmpty(d.NickName) && d.NickName.Contains(request.NickName!)));
+        return new PagedDto<UserDto>(total, _mapper.Map<IReadOnlyList<UserDto>>(items));
+    }
+
+    /// <summary>
+    /// 更新用户
+    /// </summary>
+    /// <param name="userDto"></param>
+    /// <returns></returns>
+    [HttpPut]
+    public async Task Update([FromBody] UpdateUserDto userDto)
+    {
+        var user = _mapper.Map<User>(userDto);
+        await _userRepository.UpdateAsync(user, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 新增用户
+    /// </summary>
+    /// <param name="userDto"></param>
+    /// <returns></returns>
+    [HttpPost]
+    public async Task<string> Add([FromBody] AddUserDto userDto)
+    {
+        var user = await _userRepository.GetAsync(d => d.PhoneNo == userDto.PhoneNo, HttpContext.RequestAborted);
+        if (user is null)
+        {
+            user = _mapper.Map<User>(userDto);
+            return await _userRepository.AddAsync(user, HttpContext.RequestAborted);
+        }
+        else if (user.IsDeleted)
+        {
+            user.Recover();
+            await _userRepository.UpdateAsync(user);
+            return user.Id;
+        }
+        else
+        {
+            throw new UserFriendlyException("该用户已存在");
+        }
+    }
+
+    /// <summary>
+    /// 删除用户
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [HttpDelete("{id}")]
+    public async Task Remove(string id)
+    {
+        var work = _userCacheManager.GetWorkByUser(id);
+        if (work is not null)
+            throw new UserFriendlyException("该用户正在工作中,请下班以后再删除");
+        await _userRepository.RemoveAsync(id, true, HttpContext.RequestAborted);
+    }
+
+    /// <summary>
+    /// 查询用户当前状态
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("state")]
+    public async Task<UserStateDto> GetUserState()
+    {
+        var userId = _sessionContext.RequiredUserId;
+        var isOnDuty = await _userCacheManager.IsWorkingByUserAsync(userId, HttpContext.RequestAborted);
+        var isResting = false;
+        var telNo = string.Empty;
+        if (isOnDuty)
+        {
+            var work = _userCacheManager.GetWorkByUser(userId);
+            isResting = await _telRepository.IsRestingAsync(work.TelNo, HttpContext.RequestAborted);
+            telNo = work.TelNo;
+        }
+
+        return new UserStateDto(isOnDuty, isResting, telNo);
+    }
+}

+ 50 - 0
src/CallCenter.Api/Filters/TempTokenFilter.cs

@@ -0,0 +1,50 @@
+using System.Security.Authentication;
+using System.Security.Claims;
+using CallCenter.Api.Token;
+using CallCenter.Users;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using NETCore.Encrypt;
+using XF.Domain.Exceptions;
+
+namespace CallCenter.Api.Filters;
+
+public class TempTokenFilter : IAuthorizationFilter
+{
+    /// <summary>
+    /// Called early in the filter pipeline to confirm request is authorized.
+    /// </summary>
+    /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext" />.</param>
+    public void OnAuthorization(AuthorizationFilterContext context)
+    {
+        if (context.RouteData.Values.Contains(new KeyValuePair<string, object?>("Action", "Login"))) return;
+        if (context.RouteData.Values.Contains(new KeyValuePair<string, object?>("Action", "CreateDb"))) return;
+        if (context.RouteData.Values.Contains(new KeyValuePair<string, object?>("Controller", "Report"))) return;
+        if (context.RouteData.Values.Contains(new KeyValuePair<string, object?>("Controller", "TestSdk"))) return;
+
+        var httpContext = context.HttpContext;
+        var authString = httpContext.Request.Headers["Authorization"].ToString();
+        if (string.IsNullOrEmpty(authString))
+            throw new UserFriendlyException(401, "无效验证信息");
+        var auth = authString.Split("Bearer", StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
+        if (string.IsNullOrEmpty(auth))
+            throw new UserFriendlyException(401, "无效验证信息");
+
+        var userString = EncryptProvider.AESDecrypt(auth, Sercurity.Key);
+        var user = System.Text.Json.JsonSerializer.Deserialize<User>(userString);
+        if (user is null)
+            throw new UserFriendlyException(401, "无效验证信息");
+
+        var contextUser = new ClaimsPrincipal(new List<ClaimsIdentity>
+        {
+            new ClaimsIdentity(new List<Claim>
+            {
+                new Claim("UserId", user.Id),
+                new Claim("UserName", user.Name),
+            })
+        });
+
+        httpContext.User = contextUser;
+    }
+
+}

+ 57 - 0
src/CallCenter.Api/Filters/UnifyResponseFilter.cs

@@ -0,0 +1,57 @@
+using CallCenter.Share;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+
+namespace CallCenter.Api.Filters
+{
+    public class UnifyResponseFilter : IActionFilter
+    {
+        /// <summary>
+        /// Called before the action executes, after model binding is complete.
+        /// </summary>
+        /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext" />.</param>
+        public void OnActionExecuting(ActionExecutingContext context)
+        {
+
+        }
+
+        /// <summary>
+        /// Called after the action executes, before the action result.
+        /// </summary>
+        /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutedContext" />.</param>
+        public void OnActionExecuted(ActionExecutedContext context)
+        {
+            if (context.Canceled || context.Exception != null)
+            {
+                return;
+            }
+
+            switch (context.Result)
+            {
+                case JsonResult jsonResult:
+                    if (jsonResult.Value is not ApiResponse)
+                    {
+                        jsonResult.Value =
+                            ApiResponse<object>.Success(jsonResult.Value);
+                    }
+                    return;
+                case BadRequestResult _:
+                case OkResult _:
+                case FileResult _:
+                case StatusCodeResult _:
+                case BadRequestObjectResult _:
+                    // do nothing
+                    return;
+                case ObjectResult objectResult:
+                    if (objectResult.Value is ApiResponse) break;
+                    context.Result = new JsonResult(objectResult.Value == null
+                        ? new ApiResponse()
+                        : ApiResponse<object>.Success(objectResult.Value));
+                    break;
+                case EmptyResult _:
+                    context.Result = new JsonResult(new ApiResponse());
+                    return;
+            }
+        }
+    }
+}

+ 63 - 0
src/CallCenter.Api/Filters/UserFriendlyExceptionFilter.cs

@@ -0,0 +1,63 @@
+using CallCenter.Share;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.AspNetCore.Mvc.Filters;
+using XF.Domain.Exceptions;
+
+namespace CallCenter.Api.Exceptions
+{
+    public class UserFriendlyExceptionFilter : IExceptionFilter
+    {
+        private readonly ILogger<UserFriendlyExceptionFilter> _logger;
+
+        public UserFriendlyExceptionFilter(ILogger<UserFriendlyExceptionFilter> logger)
+        {
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// Called after an action has thrown an <see cref="T:System.Exception" />.
+        /// </summary>
+        /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ExceptionContext" />.</param>
+        public void OnException(ExceptionContext context)
+        {
+            string action;
+            int code = 500;
+            string message = "请求失败!";//前端展示
+            string error = context.Exception.Message;
+            int statusCode = 500;
+            if (context.ActionDescriptor is ControllerActionDescriptor controller)
+            {
+                action = $"{controller.ControllerName}.{controller.ActionName}";
+            }
+            else
+            {
+                action = context.HttpContext.Request.GetDisplayUrl();
+            }
+
+            if (context.Exception is UserFriendlyException userFriendlyException)
+            {
+                code = userFriendlyException.Code;
+                statusCode = 400;
+                error = userFriendlyException.Message;
+                //message = userFriendlyException.Message;
+            }
+
+            if (code == 500)
+            {
+                _logger.LogError(context.Exception, action);
+            }
+
+            context.ExceptionHandled = true;
+
+            if (context.Filters.OfType<ApiControllerAttribute>().Any())
+            {
+                context.Result = new JsonResult(new ApiResponse(code, message, error))
+                {
+                    StatusCode = statusCode
+                };
+            }
+        }
+    }
+}

+ 32 - 0
src/CallCenter.Api/Program.cs

@@ -0,0 +1,32 @@
+using CallCenter.Api;
+using Serilog;
+
+Log.Logger = new LoggerConfiguration()
+    .WriteTo.Console()
+    .CreateBootstrapLogger();
+
+Log.Information("Starting up");
+
+try
+{
+    var builder = WebApplication.CreateBuilder(args);
+
+    builder.Host.UseSerilog((ctx, lc) => lc
+        //.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}")
+        .Enrich.FromLogContext()
+        .ReadFrom.Configuration(ctx.Configuration));
+
+    builder
+          .ConfigureServices()
+          .ConfigurePipelines()
+          .Run();
+}
+catch (Exception ex)
+{
+    Log.Fatal(ex, "Unhandled exception");
+}
+finally
+{
+    Log.Information("Shut down complete");
+    Log.CloseAndFlush();
+}

+ 15 - 0
src/CallCenter.Api/Properties/launchSettings.json

@@ -0,0 +1,15 @@
+{
+  "$schema": "https://json.schemastore.org/launchsettings.json",
+  "profiles": {
+    "CallCenter.Api": {
+      "commandName": "Project",
+      "launchBrowser": true,
+      "launchUrl": "swagger",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      },
+      "applicationUrl": "http://localhost:50001",
+      "dotnetRunMessages": true
+    }
+  }
+}

+ 65 - 0
src/CallCenter.Api/Realtimes/CallCenterHub.cs

@@ -0,0 +1,65 @@
+using CallCenter.Caches;
+using CallCenter.Realtimes;
+using CallCenter.Users;
+using Microsoft.AspNetCore.SignalR;
+using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
+using XF.Domain.Https;
+
+namespace CallCenter.Api.Realtimes;
+
+public class CallCenterHub : Hub
+{
+    private readonly ISessionContext _sessionContext;
+    private readonly IWorkRepository _workRepository;
+
+    public CallCenterHub(ISessionContext sessionContext, IWorkRepository workRepository)
+    {
+        _sessionContext = sessionContext;
+        _workRepository = workRepository;
+    }
+
+    /// <summary>
+    /// Called when a new connection is established with the hub.
+    /// </summary>
+    /// <returns>A <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous connect.</returns>
+    public override async Task OnConnectedAsync()
+    {
+        var userId = _sessionContext.RequiredUserId;
+        var work = await _workRepository.GetCurrentWorkByUserAsync(userId, Context.ConnectionAborted);
+        if (work == null)
+            throw new UserFriendlyException($"未查询到上班记录, userId: {userId}");
+        work.SignalRId = Context.ConnectionId;
+        await _workRepository.UpdateAsync(work, Context.ConnectionAborted);
+        //todo 清理对应work cache
+        await base.OnConnectedAsync();
+    }
+
+    /// <summary>Called when a connection with the hub is terminated.</summary>
+    /// <returns>A <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous disconnect.</returns>
+    public override Task OnDisconnectedAsync(Exception? exception)
+    {
+        //todo 1.清理user与connectionId关联关系记录
+        return base.OnDisconnectedAsync(exception);
+    }
+}
+
+public class RealtimeService : IRealtimeService, IScopeDependency
+{
+    private readonly IHubContext<CallCenterHub> _hubContext;
+    private readonly IUserCacheManager _userCacheManager;
+
+    public RealtimeService(IHubContext<CallCenterHub> hubContext, IUserCacheManager userCacheManager)
+    {
+        _hubContext = hubContext;
+        _userCacheManager = userCacheManager;
+    }
+
+    public async Task RingAsync(string userId, RingDto dto, CancellationToken cancellationToken)
+    {
+        var work = _userCacheManager.GetWorkByUser(userId);
+        if (string.IsNullOrEmpty(work.SignalRId))
+            throw new UserFriendlyException("无效signalr.connectionId");
+        await _hubContext.Clients.Client(work.SignalRId).SendAsync("Ring", dto, cancellationToken);
+    }
+}

+ 145 - 0
src/CallCenter.Api/StartupExtensions.cs

@@ -0,0 +1,145 @@
+using System.Reflection;
+using CallCenter.Api.Exceptions;
+using CallCenter.Api.Filters;
+using CallCenter.Api.Realtimes;
+using CallCenter.Devices;
+using CallCenter.NewRock;
+using CallCenter.Repository.SqlSugar;
+using Mapster;
+using MapsterMapper;
+using MediatR;
+using Microsoft.OpenApi.Models;
+using XF.Domain.Dependency;
+using CallCenter.Application;
+using CallCenter.Application.Contracts;
+using CallCenter.CacheManager;
+using FluentValidation;
+using FluentValidation.AspNetCore;
+using CallCenter.Settings;
+using Serilog;
+
+namespace CallCenter.Api;
+
+internal static class StartupExtensions
+{
+    const string CorsOrigins = "CorsOrigins";
+    internal static WebApplication ConfigureServices(this WebApplicationBuilder builder)
+    {
+        var services = builder.Services;
+        var configuration = builder.Configuration;
+
+        services.AddHttpContextAccessor();
+
+#if DEBUG
+        builder.WebHost.UseUrls("http://192.168.100.36:50001", "http://localhost:50001");
+#endif
+
+        services.Configure<DeviceConfigs>(d => configuration.GetSection("DeviceConfigs").Bind(d));
+
+        services.Configure<WorkTimeSettings>(d => configuration.GetSection("WorkTimeSettings").Bind(d));
+
+        // Add services to the container.
+        services
+            .BatchInjectServices()
+            .AddApplication()
+            ;
+
+        services.AddControllers(options =>
+        {
+            options.Filters.Add<TempTokenFilter>();
+            options.Filters.Add<UnifyResponseFilter>();
+            options.Filters.Add<UserFriendlyExceptionFilter>();
+        });
+        // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+        services.AddEndpointsApiExplorer();
+        services.AddSwaggerGen(c =>
+        {
+            //添加文档
+            c.SwaggerDoc("v1", new OpenApiInfo() { Title = "WebApi", Version = "v1.0" });
+            //使用反射获取xml文件,并构造出文件的路径
+            var xmlFile = "Document.xml";
+            var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
+            // 启用xml注释. 该方法第二个参数启用控制器的注释,默认为false.
+            c.IncludeXmlComments(xmlPath, true);
+        });
+
+        //signalR
+        builder.Services.AddSignalR().AddStackExchangeRedis("redis.fengwo.com:6380", options =>
+        {
+            options.Configuration.ChannelPrefix = "callcenter:signalR:";
+        });
+
+        /* CORS */
+        services.AddCors(options =>
+        {
+            options.AddPolicy(name: CorsOrigins,
+                builder =>
+                {
+                    var origins = configuration.GetSection("Cors:Origins").Get<string[]>();
+                    builder.SetIsOriginAllowed(a =>
+                        {
+                            return origins.Any(origin => origin.StartsWith("*.", StringComparison.Ordinal)
+                                ? a.EndsWith(origin[1..], StringComparison.Ordinal)
+                                : a.Equals(origin, StringComparison.Ordinal));
+                        })
+                        .AllowAnyHeader()
+                        .AllowAnyMethod()
+                        .AllowCredentials();
+                });
+        });
+
+        //mapster
+        var config = TypeAdapterConfig.GlobalSettings;
+        services.AddSingleton(config);
+        services.AddScoped<IMapper, ServiceMapper>();
+
+        //mediatr
+        services.AddMediatR(Assembly.GetExecutingAssembly(), typeof(ApplicationStartupExtensions).Assembly);
+
+        //迅时IPPBX
+        var deviceConfigs = configuration.GetSection("DeviceConfigs").Get<DeviceConfigs>();
+        services.AddNewRock(deviceConfigs.Address);
+
+        //sqlsugar
+        services.AddSqlSugar(configuration, "CallCenter");
+
+        //cache
+        services.AddCache(d =>
+            {
+                d.ConnectionString = configuration.GetConnectionString("Redis");
+                d.Prefix = "CallCenter";
+            });
+
+        //validator
+        services.AddFluentValidationAutoValidation(config =>
+        {
+            config.DisableDataAnnotationsValidation = true;
+        })
+            .AddValidatorsFromAssembly(typeof(AppContractsStartupExtensions).Assembly);
+        return builder.Build();
+    }
+
+    internal static WebApplication ConfigurePipelines(this WebApplication app)
+    {
+        app.UseSerilogRequestLogging();
+
+        var swaggerEnable = app.Configuration.GetSection("Swagger").Get<bool>();
+        // Configure the HTTP request pipeline.
+        if (swaggerEnable)
+        {
+            app.UseSwagger();
+            app.UseSwaggerUI();
+        }
+
+
+        app.UseCors(CorsOrigins);
+
+        app.UseAuthorization();
+        //app.MapHub<CallCenterHub>("/hubs/callcenter");
+        //app.UseMiddleware<TempTokenMiddleware>();
+
+        app.MapControllers();
+
+        return app;
+    }
+}

+ 50 - 0
src/CallCenter.Api/Token/DefaultSessionContext.cs

@@ -0,0 +1,50 @@
+using System.Security.Authentication;
+using System.Security.Claims;
+using XF.Domain.Dependency;
+using XF.Domain.Https;
+
+namespace CallCenter.Api.Token
+{
+    public class DefaultSessionContext : ISessionContext, IScopeDependency
+    {
+        public DefaultSessionContext(IHttpContextAccessor httpContextAccessor)
+        {
+            var httpContext = httpContextAccessor.HttpContext;
+            if (httpContext is null)
+                throw new ArgumentNullException(nameof(httpContext));
+            //var a = Thread.CurrentPrincipal as ClaimsPrincipal;
+            var user = httpContext.User;
+            UserId = user.Claims.FirstOrDefault(d => d.Type == "UserId")?.Value ?? string.Empty;
+            UserName = user.Claims.FirstOrDefault(d => d.Type == "UserName")?.Value ?? string.Empty;
+            
+        }
+
+        /// <summary>
+        /// Id of current tenant or null for host
+        /// </summary>
+        public string? UserId { get; }
+
+        /// <summary>
+        /// Id of current user or throw Exception for guest
+        /// </summary>
+        /// <exception cref="AuthenticationException"></exception>
+        public string RequiredUserId => UserId ?? throw new ArgumentException();
+
+        public string? UserName { get; }
+
+        /// <summary>
+        /// Roles
+        /// </summary>
+        public string[] Roles { get; }
+
+        /// <summary>
+        /// Return the first value of the specific <see cref="claimType"/> claim type, otherwise null if the claim is not present.
+        /// </summary>
+        /// <param name="claimType"></param>
+        /// <returns></returns>
+        public string? FindFirstValue(string claimType)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 7 - 0
src/CallCenter.Api/Token/Sercurity.cs

@@ -0,0 +1,7 @@
+namespace CallCenter.Api.Token
+{
+    public class Sercurity
+    {
+        public const string Key = "BE439FE4A52F48B9BEAC0602235B0868";
+    }
+}

+ 8 - 0
src/CallCenter.Api/appsettings.Development.json

@@ -0,0 +1,8 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft.AspNetCore": "Warning"
+    }
+  }
+}

+ 63 - 0
src/CallCenter.Api/appsettings.json

@@ -0,0 +1,63 @@
+{
+  "Serilog": {
+    "Using": [
+      "Serilog.Enrichers.Span",
+      "Serilog.Sinks.Console"
+    ],
+    "MinimumLevel": {
+      "Default": "Information",
+      "Override": {
+        "Microsoft": "Warning",
+        "Microsoft.Hosting.Lifetime": "Information",
+        "Microsoft.AspNetCore.Authentication": "Debug",
+        "Microsoft.AspNetCore": "Warning",
+        "Microsoft.AspNetCore.SignalR": "Debug",
+        "Microsoft.AspNetCore.Http.Connections": "Debug",
+        "System": "Warning"
+      }
+    },
+    "WriteTo": [
+      {
+        "Name": "Console",
+        "Args": {
+          //"outputTemplate": "time=\"{Timestamp:yyyy-MM-dd HH:mm:ss}\" level={Level:w3} category={SourceContext} trace={TraceId}{NewLine}msg=\"{Message:lj}\"{NewLine}error=\"{Exception}\"{NewLine}",
+          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level}] {SourceContext} [{TraceId}]{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}",
+          "theme": "Serilog.Sinks.SystemConsole.Themes.ConsoleTheme::None, Serilog.Sinks.Console"
+        }
+      },
+      //{
+      //  "Name": "File",
+      //  "Args": {
+      //    "path": "logs/log-.txt",
+      //    "rollingInterval": "Day"
+      //  }
+      //}
+    ],
+    "Enrich": [ "FromLogContext", "WithSpan" ]
+  },
+  "AllowedHosts": "*",
+  "DeviceConfigs": {
+    "Address": "http://192.168.100.100/xml",
+    "Authorize": true,
+    "ReceiveKey": "E1BBD1BB-A269-44",
+    "SendKey": "2A-BA92-160A3B1D",
+    "Expired": 86400 //认证过期时间(秒)
+  },
+  "ConnectionStrings": {
+    "CallCenter": "server=db.fengwo.com;Database=callcenter;Uid=dev;Pwd=fengwo11!!;SslMode=none;",
+    "Redis": "redis.fengwo.com"
+  },
+  "Swagger": true,
+  "Cors": {
+    "Origins": [ "http://localhost:8888", "http://callcenter-admin.fengwo.com" ]
+  },
+  "WorkTimeSettings": {
+    "MorningBegin": "08:00",
+    "MorningEnd": "12:00",
+    "AfterBegin": "15:00",
+    "AfterEnd": "21:00",
+    "WorkDay": [ 1, 2, 3, 4, 5, 0, 6 ],
+    "WorkCategory": "08da9b9f-a35d-4ade-8ea7-55e8abbcdefd",
+    "RestCategory": "08daa5f5-ac7a-4ced-8295-1c78baa15f9e"
+  }
+}

+ 20 - 0
src/CallCenter.Application.Contracts/AppContractsStartupExtensions.cs

@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.Application.Contracts.Mappers;
+using Mapster;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CallCenter.Application.Contracts
+{
+    public static class AppContractsStartupExtensions
+    {
+        public static IServiceCollection AddAppContracts(this IServiceCollection services)
+        {
+            TypeAdapterConfig.GlobalSettings.Scan(typeof(MapperConfigs).Assembly);
+            return services;
+        }
+    }
+}

+ 17 - 0
src/CallCenter.Application.Contracts/CallCenter.Application.Contracts.csproj

@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="FluentValidation" Version="11.2.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\CallCenter\CallCenter.csproj" />
+  </ItemGroup>
+
+</Project>

+ 23 - 0
src/CallCenter.Application.Contracts/Mappers/MapperConfigs.cs

@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.BlackLists;
+using CallCenter.Ivrs;
+using CallCenter.Share.Dtos;
+using CallCenter.Tels;
+using Mapster;
+
+namespace CallCenter.Application.Contracts.Mappers
+{
+    public class MapperConfigs : IRegister
+    {
+        public void Register(TypeAdapterConfig config)
+        {
+            config.NewConfig<AddBlacklistDto, Blacklist>()
+                .Ignore(d => d.Expired)
+                .AfterMapping((s, t) => t.InitExpired());
+        }
+    }
+}

+ 19 - 0
src/CallCenter.Application.Contracts/Validators/AddBlacklistDtoValidator.cs

@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.Share.Dtos;
+using FluentValidation;
+
+namespace CallCenter.Application.Contracts.Validators
+{
+    public class AddBlacklistDtoValidator : AbstractValidator<AddBlacklistDto>
+    {
+        public AddBlacklistDtoValidator()
+        {
+            RuleFor(d => d.PhoneNo).NotEmpty();
+            RuleFor(d => d.Duration).NotEmpty();
+        }
+    }
+}

+ 41 - 0
src/CallCenter.Application.Contracts/Validators/AddIvrDtoValidator.cs

@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.Share.Dtos;
+using FluentValidation;
+
+namespace CallCenter.Application.Contracts.Validators
+{
+    public class AddIvrDtoValidator : AbstractValidator<AddIvrDto>
+    {
+        public AddIvrDtoValidator()
+        {
+            RuleFor(d => d.IvrType).IsInEnum();
+            RuleFor(d => d.No).NotEmpty();
+            RuleFor(d => d.Voice).NotEmpty();
+            RuleFor(d => d.Name).NotEmpty();
+            RuleFor(d => d.IvrCategoryId).IsGuidStructureString();
+            RuleFor(d => d.Exit).MaximumLength(1);
+        }
+    }
+
+    public class UpdateIvrDtoValidator : AbstractValidator<UpdateIvrDto>
+    {
+        public UpdateIvrDtoValidator()
+        {
+            RuleFor(d => d.Id).IsGuidStructureString();
+            Include(new AddIvrDtoValidator());
+        }
+    }
+
+    public class StructureIvrDtoValidator : AbstractValidator<StructureIvrDto>
+    {
+        public StructureIvrDtoValidator()
+        {
+            RuleFor(d => d.Id).IsGuidStructureString();
+            RuleFor(d => d.IvrStrategies).NotEmpty();
+        }
+    }
+}

+ 27 - 0
src/CallCenter.Application.Contracts/Validators/AddTelGroupDtoValidator.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.Share.Dtos;
+using FluentValidation;
+
+namespace CallCenter.Application.Contracts.Validators
+{
+    public class AddTelGroupDtoValidator : AbstractValidator<AddTelGroupDto>
+    {
+        public AddTelGroupDtoValidator()
+        {
+            RuleFor(d => d.No).NotEmpty();
+        }
+    }
+
+    public class UpdateTelGroupDtoValidator : AbstractValidator<UpdateTelGroupDto>
+    {
+        public UpdateTelGroupDtoValidator()
+        {
+            RuleFor(d => d.Id).IsGuidStructureString();
+            Include(new AddTelGroupDtoValidator());
+        }
+    }
+}

+ 18 - 0
src/CallCenter.Application.Contracts/Validators/GetOutCallListRequestValidator.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.Share.Requests;
+using FluentValidation;
+
+namespace CallCenter.Application.Contracts.Validators
+{
+    public class GetOutCallListRequestValidator : AbstractValidator<GetOutCallListRequest>
+    {
+        public GetOutCallListRequestValidator()
+        {
+            RuleFor(d => d.time).NotEmpty();
+        }
+    }
+}

+ 19 - 0
src/CallCenter.Application.Contracts/Validators/IvrDtoValidator.cs

@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.Share.Dtos;
+using FluentValidation;
+
+namespace CallCenter.Application.Contracts.Validators
+{
+    public class IvrDtoValidator : AbstractValidator<IvrDto>
+    {
+        public IvrDtoValidator()
+        {
+            RuleFor(d => d.Exit)
+                .Must(d => !d.EndsWith('T')).Unless(d => string.IsNullOrEmpty(d.Exit));
+        }
+    }
+}

+ 61 - 0
src/CallCenter.Application.Contracts/Validators/TelToTelDtoValidator.cs

@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.Share.Dtos;
+using FluentValidation;
+
+namespace CallCenter.Application.Contracts.Validators
+{
+    public class TelToTelDtoValidator : AbstractValidator<TelToTelDto>
+    {
+        public TelToTelDtoValidator()
+        {
+            RuleFor(d => d.TelNo).NotEmpty();
+        }
+    }
+
+    public class TelToOuterDtoValidator : AbstractValidator<TelToOuterDto>
+    {
+        public TelToOuterDtoValidator()
+        {
+            RuleFor(d => d.OuterNo).NotEmpty();
+        }
+    }
+
+    public class VisitorToTelDtoValidator : AbstractValidator<VisitorToTelDto>
+    {
+        public VisitorToTelDtoValidator()
+        {
+            RuleFor(d => d.TelNo).NotEmpty();
+        }
+    }
+
+    public class VisitorToOuterDtoValidator : AbstractValidator<VisitorToOuterDto>
+    {
+        public VisitorToOuterDtoValidator()
+        {
+            RuleFor(d => d.VisitorNo).NotEmpty();
+            RuleFor(d => d.OuterNo).NotEmpty();
+        }
+    }
+
+    public class OuterToOuterDtoValidator : AbstractValidator<OuterToOuterDto>
+    {
+        public OuterToOuterDtoValidator()
+        {
+            RuleFor(d => d.OuterNo).NotEmpty();
+        }
+    }
+
+    public class OuterToTelDtoValidator : AbstractValidator<OuterToTelDto>
+    {
+        public OuterToTelDtoValidator()
+        {
+            RuleFor(d => d.TelNo).NotEmpty();
+        }
+    }
+    
+        
+}

+ 18 - 0
src/CallCenter.Application.Contracts/Validators/UpdateUserDtoValidator.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.Share.Dtos;
+using FluentValidation;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CallCenter.Application.Contracts.Validators;
+
+public class UpdateUserDtoValidator : AbstractValidator<UpdateUserDto>
+{
+    public UpdateUserDtoValidator()
+    {
+        RuleFor(d => d.Id).IsGuidStructureString();
+    }
+}

+ 10 - 0
src/CallCenter.Application.Contracts/Validators/ValidatorExtensions.cs

@@ -0,0 +1,10 @@
+using FluentValidation;
+using XF.Domain.Extensions;
+
+namespace CallCenter.Application.Contracts.Validators;
+
+public static class ValidatorExtensions
+{
+    public static IRuleBuilderOptions<TDto, string> IsGuidStructureString<TDto>(this IRuleBuilderInitial<TDto, string> initial) =>
+        initial.Cascade(CascadeMode.Stop).NotEmpty().Must(d => d.IsGuidString());
+}

+ 18 - 0
src/CallCenter.Application/ApplicationStartupExtensions.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.Application.Contracts;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CallCenter.Application
+{
+    public static class ApplicationStartupExtensions
+    {
+        public static IServiceCollection AddApplication(this IServiceCollection services)
+        {
+            return services.AddAppContracts();
+        }
+    }
+}

+ 17 - 0
src/CallCenter.Application/CallCenter.Application.csproj

@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\CallCenter.Application.Contracts\CallCenter.Application.Contracts.csproj" />
+    <ProjectReference Include="..\CallCenter.CacheManager\CallCenter.CacheManager.csproj" />
+    <ProjectReference Include="..\CallCenter.NewRock\CallCenter.NewRock.csproj" />
+    <ProjectReference Include="..\CallCenter.Repository.SqlSugar\CallCenter.Repository.SqlSugar.csproj" />
+    <ProjectReference Include="..\CallCenter\CallCenter.csproj" />
+  </ItemGroup>
+
+</Project>

+ 155 - 0
src/CallCenter.Application/Handlers/BaseHandler.cs

@@ -0,0 +1,155 @@
+using CallCenter.Calls;
+using CallCenter.Share.Enums;
+using NewRock.Sdk.Control.Request;
+using NewRock.Sdk.Transfer.Connect.Request;
+using NewRock.Sdk.Transfer.Queue.Request;
+using CallCenter.Share.Dtos;
+using NewRock.Sdk;
+using CallCenter.Devices;
+using Microsoft.Extensions.Options;
+
+namespace CallCenter.Application.Handlers
+{
+    public class BaseHandler
+    {
+        private readonly INewRockClient _newRockClient;
+        private readonly IOptionsSnapshot<DeviceConfigs> _options;
+        public BaseHandler(INewRockClient newRockClient, IOptionsSnapshot<DeviceConfigs> options)
+        {
+            _newRockClient = newRockClient;
+            _options = options;
+        }
+
+        public async Task HandlerIvr(IvrAnswer? runResult,Call model,CancellationToken cancellationToken)
+        {
+            if (runResult != null)
+            {
+                var ivrAnswer = runResult;
+
+                switch (ivrAnswer.IvrAnswerType)
+                {
+                    case EIvrAnswerType.Voice:
+                        var tomenuId = ivrAnswer.Content;
+                        switch (model?.CallType)
+                        {
+                            case ECallType.ExtToOuter:
+                                await _newRockClient.OuterToMenu(new OuterToMenuRequest()
+                                {
+                                    Attribute = "Connect",
+                                    Menu = new OuterToMenuMenu() { Id = tomenuId },
+                                    Outer = new OuterToMenuOuter() { Id = model.ConversationId },
+                                    //VoiceFile = string.IsNullOrEmpty(ivrAnswer.PreVoice) ? "" : ivrAnswer.PreVoice,
+                                }, _options.Value.ReceiveKey, _options.Value.Expired, cancellationToken);
+                                break;
+                            case ECallType.VisitorCallIn:
+                                await _newRockClient.VisitorToMenu(new VisitorToMenuRequest()
+                                {
+                                    Attribute = "Connect",
+                                    Menu = new VisitorToMenuMenu() { Id = tomenuId },
+                                    Visitor = new VisitorToMenuVisitor() { Id = model.ConversationId },
+                                    //VoiceFile = string.IsNullOrEmpty(ivrAnswer.PreVoice) ? "" : ivrAnswer.PreVoice,
+                                }, _options.Value.ReceiveKey, _options.Value.Expired, cancellationToken);
+                                break;
+                            default:
+                                throw new ArgumentOutOfRangeException();
+                        }
+                        break;
+                    case EIvrAnswerType.Tel:
+                        var telNo = ivrAnswer.Content;
+                        switch (model.CallType)
+                        {
+                            case ECallType.VisitorCallIn:
+                                //await _newRockClient.VisitorToExt(new VisitorToExtRequest()
+                                //{
+                                //    Attribute = "Connect",
+                                //    Ext = new VisitorToExtExt() { Id = telNo },
+                                //    VoiceFile = string.IsNullOrEmpty(ivrAnswer.PreVoice) ? "" : ivrAnswer.PreVoice,
+                                //    AutoAnswer = "",
+                                //}, _options.Value.ReceiveKey, _options.Value.Expired, cancellationToken);
+                                //来电去队列
+                                await _newRockClient.VisitorToExtQueue(new VisitorToExtQueueRequest()
+                                {
+                                    Attribute = "Queue",
+                                    Ext = new VisitorToExtQueueExt() { Id = telNo },
+                                    Visitor = new VisitorToExtQueueVisitor() { Id = model.ConversationId }
+                                }, _options.Value.ReceiveKey, _options.Value.Expired, cancellationToken);
+                                break;
+                            case ECallType.ExtToOuter:
+                                await _newRockClient.OuterToExt(new OuterToExtRequest()
+                                {
+                                    Attribute = "Connect",
+                                    Ext = new OuterToExtExt() { Id = telNo },
+                                    Outer = new OuterToExtOuter() { Id = model.ConversationId }
+                                }, _options.Value.ReceiveKey, _options.Value.Expired, cancellationToken);
+                                break;
+                            default:
+                                throw new ArgumentOutOfRangeException();
+                        }
+                        break;
+                    case EIvrAnswerType.TelGroup:
+                        var groupId = ivrAnswer.Content;
+                        await _newRockClient.VisitorToGroupQueue(new VisitorToGroupQueueRequest()
+                        {
+                            Attribute = "Queue",
+                            Group = new VisitorToGroupQueueGroup() { Id = groupId },
+                            Visitor = new VisitorToGroupQueueVisitor() { Id = model.ConversationId }
+                        }, _options.Value.ReceiveKey, _options.Value.Expired, cancellationToken);
+                        break;
+                    case EIvrAnswerType.Out:
+                        var phoneNo = ivrAnswer.Content;
+                        switch (model.CallType)
+                        {
+                            case ECallType.VisitorCallIn:
+                                await _newRockClient.VisitorToOuter(new VisitorToOuterRequest()
+                                {
+                                    Attribute = "Connect",
+                                    Visitor = new VisitorToOuterVisitor() { Id = model.ConversationId },
+                                    Outer = new VisitorToOuterOuter()
+                                    {
+                                        To = phoneNo,
+                                        //TODO DISPLAY属性待定
+                                    }
+                                },
+                                    _options.Value.ReceiveKey, _options.Value.Expired, cancellationToken);
+                                break;
+                            case ECallType.ExtToOuter:
+                                await _newRockClient.OuterToOuter(new OuterToOuterRequest()
+                                {
+                                    Attribute = "Connect",
+                                    Outer = new List<OuterToOuterOuterModel>(){
+                                        new(){Id = model.ConversationId},
+                                            new(){To = phoneNo},
+                                        }
+                                }, _options.Value.ReceiveKey, _options.Value.Expired, cancellationToken);
+                                break;
+                            default:
+                                throw new ArgumentOutOfRangeException();
+                        }
+                        break;
+                    case EIvrAnswerType.HangUp:
+                        var id = ivrAnswer.Content;
+                        switch (model?.CallType)
+                        {
+                            case ECallType.VisitorCallIn:
+                                await _newRockClient.ClearCall(new ClearCallRequest()
+                                {
+                                    Attribute = "Clear",
+                                    Visitor = new ClearCallVisitor() { Id = model.ConversationId },
+                                }, _options.Value.ReceiveKey, _options.Value.Expired, cancellationToken);
+                                break;
+                            case ECallType.ExtToOuter:
+                                await _newRockClient.ClearCall(new ClearCallRequest()
+                                {
+                                    Attribute = "Clear",
+                                    Outer = new ClearCallOuter() { Id = model.ConversationId },
+                                }, _options.Value.ReceiveKey, _options.Value.Expired, cancellationToken);
+                                break;
+                        }
+                        break;
+                    default:
+                        throw new ArgumentOutOfRangeException();
+                }
+            }
+        }
+    }
+}

+ 47 - 0
src/CallCenter.Application/Handlers/CallState/AlertExtToOuterNotificationHandler.cs

@@ -0,0 +1,47 @@
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class AlertExtToOuterNotificationHandler:INotificationHandler<AlertExtToOuterNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+
+        public AlertExtToOuterNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(AlertExtToOuterNotification notification, CancellationToken cancellationToken)
+        {
+            if (!string.IsNullOrEmpty(notification.TelNo))
+            {
+                var model =await _callRepository.GetAsync(x => x.ConversationId==notification.Outer.Id && x.ToNo==notification.Outer.To && x.Trunk==notification.Outer.Trunk && x.CreationTime>=DateTime.Now.AddHours(-2), cancellationToken);
+
+                if (model!=null)
+                {
+                    model.CallStatus = ECallStatus.Alert;
+                    await _callRepository.UpdateAsync(model, cancellationToken);
+                    var detail = new CallDetail()
+                    {
+                        CallId = model.Id,
+                        CallStatus = ECallStatus.Alert,
+                        OMCallId = notification.Outer.CallId,
+                        ConversationId = notification.Outer.Id,
+                        EventName = notification.Attribute,
+                        FromNo = notification.Outer.From,
+                        ToNo = notification.Outer.To
+                    };
+                    await _callDetailRepository.AddAsync(detail, cancellationToken);
+                }
+
+            }
+
+        }
+    }
+}

+ 43 - 0
src/CallCenter.Application/Handlers/CallState/AlertMenuToOuterNotificationHandler.cs

@@ -0,0 +1,43 @@
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class AlertMenuToOuterNotificationHandler:INotificationHandler<AlertMenuToOuterNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public AlertMenuToOuterNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(AlertMenuToOuterNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Outer.Id && x.ToNo == notification.Outer.To &&
+                     x.Trunk == notification.Outer.Trunk && x.CreationTime >= DateTime.Now.AddHours(-2),
+                cancellationToken);
+            if (model!=null)
+            {
+                model.CallStatus = ECallStatus.Alert;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Alert,
+                    OMCallId = notification.Outer.CallId,
+                    ConversationId = notification.Outer.Id,
+                    EventName = notification.Attribute,
+                    FromNo = "",
+                    ToNo = notification.Outer.To,
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 42 - 0
src/CallCenter.Application/Handlers/CallState/AlertVisitorToExtNotificationHandler.cs

@@ -0,0 +1,42 @@
+using CallCenter.Notifications;
+using MediatR;
+using CallCenter.Calls;
+using CallCenter.Share.Enums;
+
+namespace CallCenter.Application.Handlers
+{
+    public class AlertVisitorToExtNotificationHandler: INotificationHandler<AlertVisitorToExtNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public AlertVisitorToExtNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(AlertVisitorToExtNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Visitor.Id &&
+                     x.FromNo == notification.Visitor.From && x.CreationTime>=DateTime.Now.AddHours(-2), cancellationToken);
+            if (model!=null)
+            {
+                model.CallStatus = ECallStatus.Alert;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Alert,
+                    OMCallId = notification.Visitor.CallId,
+                    ConversationId = notification.Visitor.Id,
+                    EventName = notification.Attribute,
+                    FromNo = notification.Visitor.From,
+                    ToNo = notification.Visitor.To
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 79 - 0
src/CallCenter.Application/Handlers/CallState/DtmfNotificationHandler.cs

@@ -0,0 +1,79 @@
+using CallCenter.Calls;
+using CallCenter.Devices;
+using CallCenter.Ivrs;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+using Microsoft.Extensions.Options;
+using NewRock.Sdk;
+using NewRock.Sdk.Control.Request;
+using NewRock.Sdk.Transfer.Connect.Request;
+using NewRock.Sdk.Transfer.Queue.Request;
+
+namespace CallCenter.Application.Handlers
+{
+    public class DtmfNotificationHandler : BaseHandler, INotificationHandler<DtmfNotification>
+    {
+        private readonly IIvrDomainService _ivrDomainService;
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+
+        public DtmfNotificationHandler(IIvrDomainService ivrDomainService, ICallDetailRepository callDetailRepository, ICallRepository callRepository, INewRockClient newRockClient, IOptionsSnapshot<DeviceConfigs> options) :base(newRockClient, options)
+        {
+            _ivrDomainService = ivrDomainService;
+            _callDetailRepository = callDetailRepository;
+            _callRepository = callRepository;
+        }
+
+        public async Task Handle(DtmfNotification notification, CancellationToken cancellationToken)
+        {
+            bool isvis = true;
+            string menuId = string.Empty;
+            string info = string.Empty;
+            var model = await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Visitor.Id && x.FromNo == notification.Visitor.From &&
+                     x.ToNo == notification.Visitor.To, cancellationToken);
+            if (model == null)
+            {
+                model = await _callRepository.GetAsync(
+                    x => x.ConversationId == notification.Outer.Id && x.FromNo == notification.Outer.From &&
+                         x.ToNo == notification.Outer.To, cancellationToken);
+                isvis = false;
+            }
+            if (model != null)
+            {
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Dtmf,
+                    EventName = notification.Attribute,
+                };
+                if (isvis)
+                {
+                    detail.OMCallId = notification.Visitor.CallId;
+                    detail.ConversationId = notification.Visitor.Id;
+                    detail.FromNo = notification.Visitor.From;
+                    detail.ToNo = notification.Visitor.To;
+                    menuId = notification.Visitor.MenuId;
+                    info = notification.Visitor.Info;
+                }
+                else
+                {
+                    detail.OMCallId = notification.Outer.CallId;
+                    detail.ConversationId = notification.Outer.Id;
+                    detail.FromNo = notification.Outer.From;
+                    detail.ToNo = notification.Outer.To;
+                    menuId = notification.Outer.MenuId;
+                    info = notification.Outer.Info;
+                }
+                detail.Remark = info;
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+                //调用事件处理
+                var dtmfResult = await _ivrDomainService.GetDtmfAnswerAsync(menuId, info, cancellationToken);
+
+                await base.HandlerIvr(dtmfResult, model, cancellationToken);
+            }
+        }
+    }
+}

+ 25 - 0
src/CallCenter.Application/Handlers/CallState/FailedNotificationHandler.cs

@@ -0,0 +1,25 @@
+
+
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class FailedNotificationHandler:INotificationHandler<FailedNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public FailedNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(FailedNotification notification, CancellationToken cancellationToken)
+        {
+             //TODO
+        }
+    }
+}

+ 52 - 0
src/CallCenter.Application/Handlers/CallState/RingExtToOuterNotificationHandler.cs

@@ -0,0 +1,52 @@
+using CallCenter.Notifications;
+using CallCenter.Calls;
+using CallCenter.Share.Enums;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class RingExtToOuterNotificationHandler:INotificationHandler<RingExtToOuterNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public RingExtToOuterNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(RingExtToOuterNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(x =>
+                x.FromNo == notification.TelNo && x.ToNo == notification.Outer.To && x.CreationTime >=DateTime.Now.AddHours(-2), cancellationToken);
+            if (model==null)
+            {
+                model = await _callRepository.GetAsync(
+                    x => x.ConversationId == notification.Outer.Id && x.ToNo == notification.Outer.To &&
+                         x.Trunk == notification.Outer.Trunk, cancellationToken);
+            }
+                       
+
+            if (model!=null)
+            {
+                model.CallStatus = ECallStatus.Ring;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Ring,
+                    EventName = notification.Attribute,
+                    ConversationId = notification.Outer.Id,
+                    OMCallId = notification.Outer.CallId,
+                    FromNo = notification.Outer.From,
+                    ToNo = notification.Outer.To,
+                };
+                if (string.IsNullOrEmpty(detail.FromNo))
+                    detail.FromNo = notification.TelNo;
+
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 14 - 0
src/CallCenter.Application/Handlers/CallState/RingMenuToExtNotificationHandler.cs

@@ -0,0 +1,14 @@
+using CallCenter.Notifications;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class RingMenuToExtNotificationHandler:INotificationHandler<RingMenuToExtNotification>
+    {
+        //TODO 无法定位数据暂无操作
+        public async Task Handle(RingMenuToExtNotification notification, CancellationToken cancellationToken)
+        {
+            
+        }
+    }
+}

+ 42 - 0
src/CallCenter.Application/Handlers/CallState/RingVisitorToExtNotificationHandler.cs

@@ -0,0 +1,42 @@
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class RingVisitorToExtNotificationHandler:INotificationHandler<RingVisitorToExtNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+        public RingVisitorToExtNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+
+        public async Task Handle(RingVisitorToExtNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Visitor.Id &&
+                     x.FromNo == notification.Visitor.From && x.CreationTime>=DateTime.Now.AddHours(-2), cancellationToken);
+            if (model!=null)
+            {
+                model.CallStatus = ECallStatus.Ring;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Ring,
+                    OMCallId = notification.Visitor.CallId,
+                    ConversationId = notification.Visitor.Id,
+                    EventName = notification.Attribute,
+                    FromNo = notification.Visitor.From,
+                    ToNo = notification.Visitor.To
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 30 - 0
src/CallCenter.Application/Handlers/ExtState/BusyNotificationHandler.cs

@@ -0,0 +1,30 @@
+using CallCenter.Caches;
+using CallCenter.Notifications;
+using CallCenter.Tels;
+using MediatR;
+using XF.Domain.Cache;
+
+namespace CallCenter.Application.Handlers
+{
+    public class BusyNotificationHandler:INotificationHandler<BusyNotification>
+    {
+        private readonly ITelRepository _telRepository;
+        private readonly ITelCacheManager _telCacheManager;
+        private readonly ITypedCache<Tel> _typedCache;
+
+        public BusyNotificationHandler(ITelRepository telRepository, ITelCacheManager telCacheManager, ITypedCache<Tel> typedCache)
+        {
+            _telRepository = telRepository;
+            _telCacheManager = telCacheManager;
+            _typedCache = typedCache;
+        }
+
+        public async Task Handle(BusyNotification notification, CancellationToken cancellationToken)
+        {
+            var telModel = _telCacheManager.GetTel(notification.TelNo);
+            telModel.TelStatus = ETelStatus.Active;
+            //await _telRepository.UpdateAsync(telModel, cancellationToken);
+            _typedCache.Update(telModel.No,x=>telModel);
+        }
+    }
+}

+ 29 - 0
src/CallCenter.Application/Handlers/ExtState/IdleNotificationHandler.cs

@@ -0,0 +1,29 @@
+using CallCenter.Caches;
+using CallCenter.Notifications;
+using CallCenter.Tels;
+using MediatR;
+using XF.Domain.Cache;
+
+namespace CallCenter.Application.Handlers
+{
+    public class IdleNotificationHandler:INotificationHandler<IdleNotification>
+    {
+        private readonly ITelRepository _telRepository;
+        private readonly ITelCacheManager _telCacheManager;
+        private readonly ITypedCache<Tel> _typedCache;
+        public IdleNotificationHandler(ITelRepository telRepository, ITelCacheManager telCacheManager, ITypedCache<Tel> typedCache)
+        {
+            _telRepository=telRepository;
+            _telCacheManager = telCacheManager;
+            _typedCache = typedCache;
+        }
+
+        public async Task Handle(IdleNotification notification, CancellationToken cancellationToken)
+        {
+            var telModel = _telCacheManager.GetTel(notification.TelNo);
+            telModel.TelStatus = ETelStatus.Ready;
+            //await _telRepository.UpdateAsync(telModel, cancellationToken);
+            _typedCache.Update(notification.TelNo, x => telModel);
+        }
+    }
+}

+ 31 - 0
src/CallCenter.Application/Handlers/ExtState/OfflineNotificationHandler.cs

@@ -0,0 +1,31 @@
+
+using CallCenter.Caches;
+using CallCenter.Notifications;
+using CallCenter.Tels;
+using MediatR;
+using XF.Domain.Cache;
+
+namespace CallCenter.Application.Handlers
+{
+    public class OfflineNotificationHandler: INotificationHandler<OfflineNotification>
+    {
+        private readonly ITelRepository _telRepository;
+        private readonly ITelCacheManager _telCacheManager;
+        private readonly ITypedCache<Tel> _typedCache;
+        public OfflineNotificationHandler(ITelRepository telRepository, ITelCacheManager telCacheManager, ITypedCache<Tel> typedCache)
+        {
+            _telRepository = telRepository;
+            _telCacheManager = telCacheManager;
+            _typedCache = typedCache;
+        }
+
+        public async Task Handle(OfflineNotification notification, CancellationToken cancellationToken)
+        {
+            var telModel = _telCacheManager.GetTel(notification.TelNo);
+            telModel.TelStatus = ETelStatus.Offline;
+            await _telRepository.UpdateAsync(telModel, cancellationToken);
+            _typedCache.Update(notification.TelNo, x => telModel);
+        }
+
+    }
+}

+ 33 - 0
src/CallCenter.Application/Handlers/ExtState/OnlineNotificationHandler.cs

@@ -0,0 +1,33 @@
+using CallCenter.Caches;
+using CallCenter.Notifications;
+using CallCenter.Tels;
+using MediatR;
+using XF.Domain.Cache;
+
+namespace CallCenter.Application.Handlers
+{
+    public class OnlineNotificationHandler : INotificationHandler<OnlineNotification>
+    {
+        private readonly ITelRepository _telRepository;
+        private readonly ITelCacheManager _telCacheManager;
+        private readonly ITypedCache<Tel> _typedCache;
+        public OnlineNotificationHandler(ITelRepository telRepository, ITelCacheManager telCacheManager, ITypedCache<Tel> typedCache)
+        {
+            _telRepository = telRepository;
+            _telCacheManager = telCacheManager;
+            _typedCache = typedCache;
+        }
+
+        /// <summary>Handles a notification</summary>
+        /// <param name="notification">The notification</param>
+        /// <param name="cancellationToken">Cancellation token</param>
+        public async Task Handle(OnlineNotification notification, CancellationToken cancellationToken)
+        {
+            var telModel = _telCacheManager.GetTel(notification.TelNo);
+            telModel.TelStatus = ETelStatus.Ready;
+            telModel.RegisterIP = notification.RegisterIP;
+            await _telRepository.UpdateAsync(telModel,cancellationToken);
+            _typedCache.Update(notification.TelNo, x => telModel);
+        }
+    }
+}

+ 43 - 0
src/CallCenter.Application/Handlers/FlowControl/AnswerExtToOuterNotificationHandler.cs

@@ -0,0 +1,43 @@
+using CallCenter.Share.Notifications;
+using CallCenter.Calls;
+using CallCenter.Share.Enums;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class AnswerExtToOuterNotificationHandler:INotificationHandler<AnswerExtToOuterNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public AnswerExtToOuterNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(AnswerExtToOuterNotification notification, CancellationToken cancellationToken)
+        {
+            var model =await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Outer.Id && x.ToNo == notification.Outer.To &&
+                     x.Trunk == notification.Outer.Trunk && x.CreationTime >= DateTime.Now.AddHours(-2),
+                cancellationToken);
+            if (model!=null)
+            {
+                model.CallStatus = ECallStatus.Answer;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Answer,
+                    OMCallId = notification.Outer.CallId,
+                    ConversationId =notification.Outer.Id,
+                    EventName = notification.Attribute,
+                    FromNo = notification.Outer.From,
+                    ToNo = notification.Outer.To,
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 44 - 0
src/CallCenter.Application/Handlers/FlowControl/AnswerViisitorToExtNotificationHandler.cs

@@ -0,0 +1,44 @@
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class AnswerViisitorToExtNotificationHandler:INotificationHandler<AnswerViisitorToExtNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+
+        public AnswerViisitorToExtNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(AnswerViisitorToExtNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Visitor.Id &&
+                     x.FromNo == notification.Visitor.From && x.CreationTime >= DateTime.Now.AddHours(-2), cancellationToken);
+            if (model != null)
+            {
+                model.CallStatus = ECallStatus.Answer;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Answer,
+                    OMCallId = notification.Visitor.CallId,
+                    ConversationId = notification.Visitor.Id,
+                    EventName = notification.Attribute,
+                    AnswerNo = notification.TelNo,
+                    FromNo = notification.Visitor.From,
+                    ToNo = notification.Visitor.To,
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 43 - 0
src/CallCenter.Application/Handlers/FlowControl/AnsweredExtToOuterNotificationHandler.cs

@@ -0,0 +1,43 @@
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class AnsweredExtToOuterNotificationHandler:INotificationHandler<AnsweredExtToOuterNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public AnsweredExtToOuterNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(AnsweredExtToOuterNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Outer.Id &&
+                     x.FromNo == notification.Outer.From && x.ToNo == notification.Outer.To && x.CreationTime >= DateTime.Now.AddHours(-2), cancellationToken);
+            if (model != null)
+            {
+                model.CallStatus = ECallStatus.Answered;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Answered,
+                    OMCallId = notification.Outer.CallId,
+                    ConversationId = notification.Outer.Id,
+                    EventName = notification.Attribute,
+                    AnswerNo = notification.TelNo,
+                    FromNo = notification.Outer.From,
+                    ToNo = notification.Outer.To
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 43 - 0
src/CallCenter.Application/Handlers/FlowControl/AnsweredExtToOuterToExtNotificationHandler.cs

@@ -0,0 +1,43 @@
+using CallCenter.Calls;
+using CallCenter.Share.Enums;
+using CallCenter.Share.Notifications;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class AnsweredExtToOuterToExtNotificationHandler:INotificationHandler<AnsweredExtToOuterToExtNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public AnsweredExtToOuterToExtNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(AnsweredExtToOuterToExtNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(x =>
+                x.ConversationId == notification.Outer.Id && x.ToNo == notification.Outer.To &&
+                x.Trunk == notification.Outer.Trunk && x.CreationTime >= DateTime.Now.AddHours(-2),cancellationToken);
+            if (model!=null)
+            {
+                model.CallStatus = ECallStatus.Answered;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Answered,
+                    ConversationId = notification.Outer.Id,
+                    OMCallId = notification.Outer.CallId,
+                    EventName = notification.Attribute,
+                    AnswerNo = notification.Outer.To,
+                    FromNo = notification.Outer.From,
+                    ToNo = notification.Outer.To,
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 44 - 0
src/CallCenter.Application/Handlers/FlowControl/AnsweredVisitorToExtNotificationHandler.cs

@@ -0,0 +1,44 @@
+using CallCenter.Calls;
+using CallCenter.Share.Enums;
+using CallCenter.Share.Notifications;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class AnsweredVisitorToExtNotificationHandler:INotificationHandler<AnsweredVisitorToExtNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public AnsweredVisitorToExtNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(AnsweredVisitorToExtNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Visitor.Id &&
+                     x.FromNo == notification.Visitor.From && x.CreationTime >= DateTime.Now.AddHours(-2), cancellationToken);
+            if (model != null)
+            {
+                model.CallStatus = ECallStatus.Answered;
+                model.ToNo = notification.TelNo;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Answered,
+                    OMCallId = notification.Visitor.CallId,
+                    ConversationId = notification.Visitor.Id,
+                    EventName = notification.Attribute,
+                    AnswerNo = notification.TelNo,
+                    FromNo = notification.Visitor.From,
+                    ToNo = notification.Visitor.To,
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 46 - 0
src/CallCenter.Application/Handlers/FlowControl/ByeExtAndOuterOneNotificationHandler.cs

@@ -0,0 +1,46 @@
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class ByeExtAndOuterOneNotificationHandler:INotificationHandler<ByeExtAndOuterOneNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public ByeExtAndOuterOneNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+
+        public async Task Handle(ByeExtAndOuterOneNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Outer.Id && 
+                     x.Trunk==notification.Outer.Trunk && x.ToNo == notification.Outer.To && x.CreationTime>=DateTime.Now.AddHours(-2), cancellationToken);
+            if (model != null)
+            {
+                model.CallStatus = ECallStatus.Bye;
+                model.EndBy = EEndBy.From;
+                model.RingOffType = ERingOffType.Normal;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Bye,
+                    OMCallId = notification.Outer.CallId,
+                    ConversationId = notification.Outer.Id,
+                    EventName = notification.Attribute,
+                    FromNo = notification.Outer.From,
+                    ToNo = notification.Outer.To,
+                    Recording = notification.Recording,
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 44 - 0
src/CallCenter.Application/Handlers/FlowControl/ByeExtAndOuterTwoNotificationHandler.cs

@@ -0,0 +1,44 @@
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class ByeExtAndOuterTwoNotificationHandler : INotificationHandler<ByeExtAndOuterTwoNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public ByeExtAndOuterTwoNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(ByeExtAndOuterTwoNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Outer.Id  && x.ToNo == notification.Outer.To && x.Trunk==notification.Outer.Trunk && x.CreationTime>=DateTime.Now.AddHours(-2), cancellationToken);
+            if (model != null)
+            {
+                model.CallStatus = ECallStatus.Bye;
+                model.EndBy = EEndBy.To;
+                model.RingOffType = ERingOffType.Normal;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Bye,
+                    OMCallId = notification.Outer.CallId,
+                    ConversationId = notification.Outer.Id,
+                    EventName = notification.Attribute,
+                    FromNo = notification.Outer.From,
+                    ToNo = notification.Outer.To,
+                    Recording = notification.Recording
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 45 - 0
src/CallCenter.Application/Handlers/FlowControl/ByeOuterAndOuterNotificationHandler.cs

@@ -0,0 +1,45 @@
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class ByeOuterAndOuterNotificationHandler:INotificationHandler<ByeOuterAndOuterNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public ByeOuterAndOuterNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(ByeOuterAndOuterNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(x =>
+                x.ConversationId == notification.Outer.Id && x.FromNo == notification.Outer.From &&
+                x.ToNo == notification.Outer.To && x.CreationTime >= DateTime.Now.AddHours(-2));
+            if (model!=null)
+            {
+                model.CallStatus = ECallStatus.Bye;
+                model.RingOffType = ERingOffType.Normal;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Answered,
+                    ConversationId = notification.Outer.Id,
+                    OMCallId = notification.Outer.CallId,
+                    EventName = notification.Attribute,
+                    AnswerNo = notification.Outer.To,
+                    FromNo = notification.Outer.From,
+                    ToNo = notification.Outer.To,
+                    Recording = notification.Recording,
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 45 - 0
src/CallCenter.Application/Handlers/FlowControl/ByeVisitorAndExtNotificationHandler.cs

@@ -0,0 +1,45 @@
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class ByeVisitorAndExtNotificationHandler:INotificationHandler<ByeVisitorAndExtNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public ByeVisitorAndExtNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(ByeVisitorAndExtNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Visitor.Id && 
+                     x.FromNo == notification.Visitor.From && x.CreationTime>=DateTime.Now.AddHours(-2), cancellationToken);
+            if (model != null)
+            {
+                model.CallStatus = ECallStatus.Bye;
+                model.EndBy = EEndBy.From;
+                model.RingOffType = ERingOffType.Normal;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Bye,
+                    OMCallId = notification.Visitor.CallId,
+                    ConversationId = notification.Visitor.Id,
+                    EventName = notification.Attribute,
+                    FromNo = notification.Visitor.From,
+                    ToNo = notification.Visitor.To,
+                    Recording = notification.Recording,
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 25 - 0
src/CallCenter.Application/Handlers/FlowControl/ByeVisitorAndOuterNotificationHandler.cs

@@ -0,0 +1,25 @@
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class ByeVisitorAndOuterNotificationHandler:INotificationHandler<ByeVisitorAndOuterNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+
+        public ByeVisitorAndOuterNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(ByeVisitorAndOuterNotification notification, CancellationToken cancellationToken)
+        {
+            //var model = 
+            //TODO 明天测试来电转去电看数据结果
+        }
+    }
+}

+ 45 - 0
src/CallCenter.Application/Handlers/FlowControl/ByeVisitorOffNotificationHandler.cs

@@ -0,0 +1,45 @@
+using CallCenter.Calls;
+using CallCenter.Share.Enums;
+using CallCenter.Share.Notifications;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class ByeVisitorOffNotificationHandler:INotificationHandler<ByeVisitorOffNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public ByeVisitorOffNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(ByeVisitorOffNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Visitor.Id &&
+                     x.FromNo == notification.Visitor.From && x.CreationTime >= DateTime.Now.AddHours(-2), cancellationToken);
+            if (model != null)
+            {
+                model.CallStatus = ECallStatus.Bye;
+                model.EndBy = EEndBy.To;
+                model.RingOffType = ERingOffType.Normal;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Bye,
+                    OMCallId = notification.Visitor.CallId,
+                    ConversationId = notification.Visitor.Id,
+                    EventName = notification.Attribute,
+                    FromNo = notification.Visitor.From,
+                    ToNo = notification.Visitor.To,
+                    Recording = notification.Recording,
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 62 - 0
src/CallCenter.Application/Handlers/FlowControl/CdrNotificationHandler.cs

@@ -0,0 +1,62 @@
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class CdrNotificationHandler:INotificationHandler<CdrNotification>
+    {
+        private readonly ICallRecordRepository _callRecordRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+        private readonly ICallRepository _callRepository;
+
+        public CdrNotificationHandler(ICallRecordRepository callRecordRepository, ICallDetailRepository callDetailRepository, ICallRepository callRepository)
+        {
+            _callRecordRepository = callRecordRepository;
+            _callDetailRepository = callDetailRepository;
+            _callRepository = callRepository;
+        }
+
+        public async Task Handle(CdrNotification notification, CancellationToken cancellationToken)
+        {
+            var callDetail = await 
+                _callDetailRepository.GetAsync(x => x.OMCallId == notification.CallId, cancellationToken);
+            
+            if (callDetail!=null)
+            {
+                var model = new CallRecord()
+                {
+                    CallId = callDetail.CallId,
+                    CdrId = notification.Id,
+                    CDRCallId = notification.CallId,
+                    TimeStart = notification.TimeStart,
+                    Group = notification.Group,
+                    Type = (ECDRType)Enum.Parse(typeof(ECDRType), notification.Type),
+                    Route = (ECDRRoute)Enum.Parse(typeof(ECDRRoute), notification.Route),
+                    CPN = notification.CPN,
+                    CDPN = notification.CDPN,
+                    TimeEnd = notification.TimeEnd,
+                    Duration = notification.Duration,
+                    TrunkNumber = notification.TrunkNumber,
+                    Recording = notification.Recording,
+                    RecCodec = notification.RecCodec,
+                };
+                if (!string.IsNullOrEmpty(notification.VisitorId))
+                    model.VisitorId = notification.VisitorId;
+
+                if (!string.IsNullOrEmpty(notification.OuterId))
+                    model.OuterId = notification.OuterId;
+
+                await _callRecordRepository.AddAsync(model,cancellationToken);
+                
+                var callModel = await _callRepository.GetAsync(x => x.Id == callDetail.CallId,cancellationToken);
+                if (callModel!=null)
+                {
+                    callModel.Duration = double.Parse(model.Duration);
+                    await _callRepository.UpdateAsync(callModel, cancellationToken);
+                }
+            }
+        }
+    }
+}

+ 43 - 0
src/CallCenter.Application/Handlers/FlowControl/DivertVisitorToExtNotificationHandler.cs

@@ -0,0 +1,43 @@
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class DivertVisitorToExtNotificationHandler:INotificationHandler<DivertVisitorToExtNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+        public DivertVisitorToExtNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(DivertVisitorToExtNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Visitor.Id && x.FromNo == notification.Visitor.From &&
+                     x.ToNo == notification.Visitor.To && x.CreationTime >= DateTime.Now.AddHours(-2),
+                cancellationToken);
+            if (model!=null)
+            {
+                model.CallStatus = ECallStatus.Divert;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Divert,
+                    OMCallId = notification.Visitor.CallId,
+                    ConversationId = notification.Visitor.Id,
+                    EventName = notification.Attribute,
+                    FromNo = notification.Visitor.From,
+                    ToNo = notification.Visitor.To
+                };
+                await _callDetailRepository.AddAsync(detail,cancellationToken);
+            }
+        }
+    }
+}

+ 51 - 0
src/CallCenter.Application/Handlers/FlowControl/EndOfAnnOuterToMenuNotificationHandler.cs

@@ -0,0 +1,51 @@
+using CallCenter.Calls;
+using CallCenter.Devices;
+using CallCenter.Ivrs;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+using Microsoft.Extensions.Options;
+using NewRock.Sdk;
+
+namespace CallCenter.Application.Handlers
+{
+    public class EndOfAnnOuterToMenuNotificationHandler:BaseHandler,INotificationHandler<EndOfAnnOuterToMenuNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+        private readonly IIvrDomainService _ivrDomainService;
+        public EndOfAnnOuterToMenuNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository, IIvrDomainService ivrDomainService, INewRockClient newRockClient, IOptionsSnapshot<DeviceConfigs> options) : base(newRockClient, options)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+            _ivrDomainService = ivrDomainService;
+        }
+
+        public async Task Handle(EndOfAnnOuterToMenuNotification notification, CancellationToken cancellationToken)
+        {
+            var model =await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Outer.Id && x.FromNo == notification.Outer.From &&
+                     x.ToNo == notification.Outer.To && x.CreationTime >= DateTime.Now.AddHours(-2), cancellationToken);
+            if (model!=null)
+            {
+                model.CallStatus = ECallStatus.EndOfAnn;
+                await _callRepository.UpdateAsync(model,cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.EndOfAnn,
+                    OMCallId = notification.Outer.CallId,
+                    ConversationId = notification.Outer.Id,
+                    EventName = notification.Attribute,
+                    FromNo = notification.Outer.From,
+                    ToNo = notification.Outer.To
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+
+                var runModel = await _ivrDomainService.GetVoiceEndAnswerAsync(notification.MenuId,cancellationToken);
+
+                await HandlerIvr(runModel, model, cancellationToken);
+            }
+        }
+    }
+}

+ 57 - 0
src/CallCenter.Application/Handlers/FlowControl/EndOfAnnVisitorToMenuNotificationHandler.cs

@@ -0,0 +1,57 @@
+using CallCenter.Calls;
+using CallCenter.Devices;
+using CallCenter.Ivrs;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+using Microsoft.Extensions.Options;
+using NewRock.Sdk;
+
+namespace CallCenter.Application.Handlers.FlowControl
+{
+
+    public class EndOfAnnVisitorToMenuNotificationHandler:BaseHandler,INotificationHandler<EndOfAnnVisitorToMenuNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+        private readonly IIvrDomainService _ivrDomainService;
+
+        public EndOfAnnVisitorToMenuNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository, IIvrDomainService ivrDomainService,INewRockClient newRockClient, IOptionsSnapshot<DeviceConfigs> options):base(newRockClient,options)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+            _ivrDomainService = ivrDomainService;
+        }
+
+        public async Task Handle(EndOfAnnVisitorToMenuNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Visitor.Id && x.FromNo == notification.Visitor.From &&
+                     x.ToNo == notification.Visitor.To && x.CreationTime >= DateTime.Now.AddHours(-2),
+                cancellationToken);
+
+            if (model!=null)
+            {
+                model.CallStatus = ECallStatus.EndOfAnn;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.EndOfAnn,
+                    OMCallId = notification.Visitor.CallId,
+                    ConversationId = notification.Visitor.Id,
+                    EventName = notification.Attribute,
+                    FromNo = notification.Visitor.From,
+                    ToNo = notification.Visitor.To
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+
+                var runModel = await _ivrDomainService.GetVoiceEndAnswerAsync(notification.MenuId,cancellationToken);
+
+                await HandlerIvr(runModel, model, cancellationToken);
+            }
+
+           
+        }
+    }
+}

+ 116 - 0
src/CallCenter.Application/Handlers/FlowControl/IncomingNotificationHandler.cs

@@ -0,0 +1,116 @@
+using CallCenter.Caches;
+using CallCenter.Calls;
+using CallCenter.Ivrs;
+using CallCenter.Devices;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using MediatR;
+using Microsoft.Extensions.Options;
+using NewRock.Sdk;
+using NewRock.Sdk.Transfer.Connect.Request;
+using XF.Domain.Constants;
+using XF.Domain.Cache;
+using CallCenter.Settings;
+using Microsoft.Extensions.Logging;
+
+namespace CallCenter.Application.Handlers
+{
+    public class IncomingNotificationHandler : INotificationHandler<IncomingNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+        private readonly ISystemSettingCacheManager _systemSettingCacheManager;
+        //private readonly ITelCacheManager _telCacheManager;
+        private readonly IIvrCacheManager _ivrCacheManager;
+        private readonly INewRockClient _newRockClient;
+        private readonly IOptionsSnapshot<DeviceConfigs> _options;
+        private readonly ITypedCache<WorkTimeSettings> _worktimeCache;
+        private readonly IOptionsSnapshot<WorkTimeSettings> _worktimeOptions;
+        private readonly ILogger<IncomingNotificationHandler> _logger;
+
+        public IncomingNotificationHandler(
+            ICallRepository callRepository, ICallDetailRepository callDetailRepository,
+            ISystemSettingCacheManager systemSettingCacheManager, IIvrCacheManager ivrCacheManager,
+            INewRockClient newRockClient, IOptionsSnapshot<DeviceConfigs> options,
+            ITypedCache<WorkTimeSettings> worktimeCache, IOptionsSnapshot<WorkTimeSettings> worktimeOptions,
+            ILogger<IncomingNotificationHandler> logger)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+            _systemSettingCacheManager = systemSettingCacheManager;
+            _ivrCacheManager = ivrCacheManager;
+            _newRockClient = newRockClient;
+            _options = options;
+            _worktimeCache = worktimeCache;
+            _worktimeOptions = worktimeOptions;
+            _logger = logger;
+        }
+
+        public async Task Handle(IncomingNotification notification, CancellationToken cancellationToken)
+        {
+            var model = await _callRepository.GetAsync(
+                x => x.ConversationId == notification.Visitor.Id && x.Trunk == notification.TrunkId &&
+                     x.FromNo == notification.Visitor.From && x.CreationTime >= DateTime.Now.AddHours(-2), cancellationToken);
+            if (model != null)
+            {
+                model.CallStatus = ECallStatus.Incoming;
+                await _callRepository.UpdateAsync(model, cancellationToken);
+                var detail = new CallDetail()
+                {
+                    CallId = model.Id,
+                    CallStatus = ECallStatus.Incoming,
+                    OMCallId = notification.Visitor.CallId,
+                    ConversationId = notification.Visitor.Id,
+                    EventName = notification.Attribute,
+                    FromNo = notification.Visitor.From,
+                    ToNo = notification.Visitor.To,
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+            //TODO IVR处理
+            var setting = _systemSettingCacheManager.GetSetting(SettingConstants.IVRConfig);
+            if (bool.Parse(setting.SettingValue))
+            {
+                //TODO 获取工作或休息时间(接听策略)
+                //var ivrList = _ivrCacheManager.GetIvrs();
+                //var ivr = ivrList.First(x => x.IvrCategoryId == "08da9b9f-a35d-4ade-8ea7-55e8abbcdefd" && x.IsRoot);
+
+                var ivr = GetCorrectIvr();
+                _logger.LogInformation("transfer to ivr.no: {ivrNo}", ivr.No);
+                await _newRockClient.VisitorToMenu(
+                    new VisitorToMenuRequest()
+                    {
+                        Attribute = "Connect",
+                        Menu = new VisitorToMenuMenu() { Id = ivr.No },
+                        Visitor = new VisitorToMenuVisitor() { Id = notification.Visitor.Id }
+                    },
+                    _options.Value.ReceiveKey, _options.Value.Expired, cancellationToken);
+            }
+            else
+            {
+                //TODO 跳转默认分机组
+            }
+        }
+
+        private Ivr GetCorrectIvr()
+        {
+            var worktimeSettings = _worktimeCache.GetOrAdd("worktimesettings", d => _worktimeOptions.Value, ExpireMode.Absolute, TimeSpan.FromDays(1));
+            var categoryId = GetCorrectCategory(worktimeSettings);
+            var ivrList = _ivrCacheManager.GetIvrs();
+            var ivr = ivrList.First(x => x.IvrCategoryId == categoryId && x.IsRoot);
+            return ivr;
+        }
+
+        private string GetCorrectCategory(WorkTimeSettings settings)
+        {
+            if (!settings.WorkDay.Contains((int)DateTime.Now.DayOfWeek))
+                return settings.RestCategory;
+            var time = TimeOnly.FromDateTime(DateTime.Now);
+            if ((time >= TimeOnly.Parse(settings.MorningBegin) && time <= TimeOnly.Parse(settings.MorningEnd))
+                || (time >= TimeOnly.Parse(settings.AfterBegin) && time <= TimeOnly.Parse(settings.AfterEnd))
+                )
+                return settings.WorkCategory;
+            return settings.RestCategory;
+        }
+    }
+}

+ 103 - 0
src/CallCenter.Application/Handlers/FlowControl/InviteNotificationHandler.cs

@@ -0,0 +1,103 @@
+using CallCenter.BlackLists;
+using CallCenter.Caches;
+using CallCenter.Notifications;
+using MediatR;
+using CallCenter.Calls;
+using NewRock.Sdk;
+using NewRock.Sdk.Control.Request;
+using CallCenter.Devices;
+using CallCenter.Share.Enums;
+using Microsoft.Extensions.Options;
+using CallCenter.Tools;
+using NewRock.Sdk.Accept.Request;
+using CallCenter.Settings;
+
+namespace CallCenter.Application.Handlers
+{
+    public class InviteNotificationHandler : INotificationHandler<InviteNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+        private readonly IUserCacheManager _userCacheManager;
+        private readonly IBlacklistDomainService _blacklistDomainService;
+        private readonly INewRockClient _newRockClient;
+        private readonly IOptionsSnapshot<DeviceConfigs> _options;
+        private readonly IOptionsSnapshot<WorkTimeSettings> _workTimeOptions;
+        public InviteNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository, IUserCacheManager userCacheManager, IBlacklistDomainService blacklistDomainService, INewRockClient newRockClient, IOptionsSnapshot<DeviceConfigs> options, IOptionsSnapshot<WorkTimeSettings> workTimeOptions)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+            _userCacheManager = userCacheManager;
+            _blacklistDomainService = blacklistDomainService;
+            _newRockClient = newRockClient;
+            _options = options;
+            _workTimeOptions = workTimeOptions;
+        }
+
+        public bool IsInWrokTime()
+        {
+            return false;
+        }
+
+        public async Task Handle(InviteNotification notification, CancellationToken cancellationToken)
+        {
+            //验证来电是否在黑名单中
+            bool isblacklist = _blacklistDomainService.IsInBlacklist(notification.Visitor.From);
+            //获取星期数
+            //string weeekNum = WeekTool.GetWeekNum();
+
+            //TODO 去掉绑定用户信息
+            //var workModel = _userCacheManager.GetWorkByTel(notification.Visitor.To);
+            var isp = PhoneIspTool.GetPhoneIsp(notification.Visitor.From);
+            var model = new Call()
+            {
+                CallStatus = ECallStatus.PreIncoming,
+                CallDirection = ECallDirection.In,
+                CallType = ECallType.VisitorCallIn,
+                ConversationId = notification.Visitor.Id,
+                FromNo = notification.Visitor.From,
+                ToNo = notification.Visitor.To,
+                Trunk = notification.TrunkId,
+                //UserId = workModel.UserId,
+                //UserName = workModel.UserName,
+                PhoneIsp = isp,
+            };
+            if (isblacklist)
+                model.RingOffType = ERingOffType.BlackList;
+
+            model.Modified();
+            var callid = await _callRepository.AddAsync(model, cancellationToken);
+            var detail = new CallDetail()
+            {
+                CallId = callid,
+                CallStatus = ECallStatus.PreIncoming,
+                OMCallId = notification.Visitor.CallId,
+                ConversationId = notification.Visitor.Id,
+                EventName = notification.Attribute,
+                FromNo = notification.Visitor.From,
+                ToNo = notification.Visitor.To,
+            };
+            await _callDetailRepository.AddAsync(detail, cancellationToken);
+
+            //如果是在黑名单中直接挂断
+            if (isblacklist)
+            {
+                await _newRockClient.ClearCall(new ClearCallRequest()
+                {
+                    Attribute = "Clear",
+                    Visitor = new ClearCallVisitor()
+                    {
+                        Id = notification.Visitor.Id
+                    }
+                }, _options.Value.ReceiveKey, _options.Value.Expired, cancellationToken);
+            }
+            else
+            {
+                await _newRockClient.AcceptVisitor(
+                    new AcceptVisitorRequest()
+                    { Attribute = "Accept", Visitor = new AcceptVisitorModel() { Id = notification.Visitor.Id } },
+                    _options.Value.ReceiveKey, _options.Value.Expired, cancellationToken);
+            }
+        }
+    }
+}

+ 24 - 0
src/CallCenter.Application/Handlers/FlowControl/QueueVisitorToGroupBusyNotificationHandler.cs

@@ -0,0 +1,24 @@
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class QueueVisitorToGroupBusyNotificationHandler:INotificationHandler<QueueVisitorToGroupBusyNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+
+
+        public QueueVisitorToGroupBusyNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+        }
+
+        public async Task Handle(QueueVisitorToGroupBusyNotification notification, CancellationToken cancellationToken)
+        {
+            //TODO
+        }
+    }
+}

+ 62 - 0
src/CallCenter.Application/Handlers/System/BootupNotificationHandler.cs

@@ -0,0 +1,62 @@
+using CallCenter.Caches;
+using CallCenter.Devices;
+using CallCenter.Ivrs;
+using CallCenter.Repository.SqlSugar;
+using CallCenter.Share.Notifications;
+using CallCenter.Tels;
+using MediatR;
+using NewRock.Sdk.Control.Request.Base;
+using Newtonsoft.Json.Serialization;
+
+namespace CallCenter.Application.Handlers.System
+{
+    public class BootupNotificationHandler : INotificationHandler<BootupNotification>
+    {
+        private readonly ITelGroupRepository _telGroupRepository;
+        private readonly IIvrRepository _ivrRepository;
+        private readonly IDeviceManager _deviceManager;
+        private readonly IUserCacheManager _userCacheManager;
+
+        public BootupNotificationHandler(ITelGroupRepository telGroupRepository, IDeviceManager deviceManager, IUserCacheManager userCacheManager, IIvrRepository ivrRepository)
+        {
+            _telGroupRepository = telGroupRepository;
+            _deviceManager = deviceManager;
+            _userCacheManager = userCacheManager;
+            _ivrRepository = ivrRepository;
+        }
+
+        public async Task Handle(BootupNotification notification, CancellationToken cancellationToken)
+        {
+            #region 还原所有分机组配置
+            //查询所有分机组
+            var list = await _telGroupRepository.QueryExtAsync(d => true, d => d.Includes(x => x.Tels));
+            foreach (var groupItem in list)
+            {
+                List<string> exts = new List<string>();
+                foreach (var ext in groupItem.Tels)
+                {
+                    var iswork = await _userCacheManager.IsWorkingByTelAsync(ext.No, cancellationToken);
+                    if (iswork)
+                        exts.Add(ext.No);
+
+                }
+                //轮循还原设备分机组信息
+                await _deviceManager.AssginConfigGroupAsync(groupItem.No, groupItem.Distribution.ToString(), exts, groupItem.Voice,
+                    cancellationToken);
+            }
+
+            #endregion
+
+            #region 还原所有语音菜单配置
+            //查询所有IVR
+            var ivrlist = await _ivrRepository.QueryAsync();
+            foreach (var item in ivrlist)
+            {
+                await _deviceManager.AssginConfigMenuAsync(item.No, item.Voice, item.Repeat.ToString(), item.InfoLength.ToString(), item.Exit, cancellationToken);
+            }
+            #endregion
+
+            //TODO 有新还原的设置加在后面
+        }
+    }
+}

+ 57 - 0
src/CallCenter.Application/Handlers/Transient/TransientOuterNotificationHandler.cs

@@ -0,0 +1,57 @@
+using CallCenter.Caches;
+using CallCenter.Calls;
+using CallCenter.Notifications;
+using CallCenter.Share.Enums;
+using CallCenter.Tools;
+using MediatR;
+
+namespace CallCenter.Application.Handlers
+{
+    public class TransientOuterNotificationHandler:INotificationHandler<TransientOuterNotification>
+    {
+        private readonly ICallRepository _callRepository;
+        private readonly ICallDetailRepository _callDetailRepository;
+        private readonly IUserCacheManager _userCacheManager;
+        public TransientOuterNotificationHandler(ICallRepository callRepository, ICallDetailRepository callDetailRepository, IUserCacheManager userCacheManager)
+        {
+            _callRepository = callRepository;
+            _callDetailRepository = callDetailRepository;
+            _userCacheManager = userCacheManager;
+        }
+        public async Task Handle(TransientOuterNotification notification, CancellationToken cancellationToken)
+        {
+            if (!string.IsNullOrEmpty(notification.Outer.Id))
+            {
+                var workModel = _userCacheManager.GetWorkByTel(notification.Outer.From);
+                var isp = PhoneIspTool.GetPhoneIsp(notification.Outer.To);
+                var callModel = new Call()
+                {
+                    CallStatus = ECallStatus.ExtOuterReady,
+                    CallDirection = ECallDirection.Out,
+                    CallType = ECallType.ExtToOuter,
+                    ConversationId = notification.Outer.Id,
+                    FromNo = notification.Outer.From,
+                    ToNo = notification.Outer.To,
+                    Trunk = notification.Outer.Trunk,
+                    UserId = workModel.UserId,
+                    UserName = workModel.UserName,
+                    PhoneIsp = isp
+                };
+                callModel.Modified();
+                var callId = await _callRepository.AddAsync(callModel, cancellationToken);
+                //写入明细
+                var detail = new CallDetail()
+                {
+                    CallId = callId,
+                    CallStatus = ECallStatus.ExtOuterReady,
+                    ConversationId = notification.Outer.Id,
+                    OMCallId = notification.Outer.CallId,
+                    EventName =  "ExtOuterReady", //去电
+                    FromNo = notification.Outer.From,
+                    ToNo = notification.Outer.To,
+                };
+                await _callDetailRepository.AddAsync(detail, cancellationToken);
+            }
+        }
+    }
+}

+ 22 - 0
src/CallCenter.Application/Handlers/Transient/TransinetVisitorNotificationHandler.cs

@@ -0,0 +1,22 @@
+using CallCenter.Calls;
+using CallCenter.Share.Notifications;
+using MediatR;
+
+namespace CallCenter.Application.Handlers.Transient
+{
+    public class TransinetVisitorNotificationHandler:INotificationHandler<TransientVisitorNotification>
+    {
+        private readonly ICallRepository _callRepository;
+
+
+        public TransinetVisitorNotificationHandler(ICallRepository callRepository)
+        {
+            _callRepository = callRepository;
+        }
+
+        public async Task Handle(TransientVisitorNotification notification, CancellationToken cancellationToken)
+        {
+            //throw new NotImplementedException();
+        }
+    }
+}

+ 62 - 0
src/CallCenter.CacheManager/BlacklistManager.cs

@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CacheManager.Core;
+using CallCenter.BlackLists;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using XF.Domain.Dependency;
+
+namespace CallCenter.CacheManager
+{
+    public class BlacklistManager : BackgroundService
+    {
+        //private readonly ICacheManager<Blacklist> _cacheManager;
+        private readonly IServiceScopeFactory _serviceScopeFactory;
+
+        public BlacklistManager(IServiceScopeFactory serviceScopeFactory)
+        {
+            _serviceScopeFactory = serviceScopeFactory;
+
+            //cacheManager.OnAdd += (sender, args) => { Console.WriteLine(args.ToString()); };
+
+            //cacheManager.OnRemove += (sender, args) => { Console.WriteLine(args.ToString()); };
+
+            //cacheManager.OnClear += (sender, args) => { Console.WriteLine(args.ToString()); };
+
+            //cacheManager.OnRemoveByHandle += (sender, args) => { Console.WriteLine(args.ToString()); };
+
+            //cacheManager.OnPut += (sender, args) => { Console.WriteLine(args.ToString()); };
+
+            //cacheManager.OnUpdate += (sender, args) => { Console.WriteLine(args.ToString()); };
+
+        }
+
+        /// <summary>
+        /// This method is called when the <see cref="T:Microsoft.Extensions.Hosting.IHostedService" /> starts. The implementation should return a task that represents
+        /// the lifetime of the long running operation(s) being performed.
+        /// </summary>
+        /// <param name="stoppingToken">Triggered when <see cref="M:Microsoft.Extensions.Hosting.IHostedService.StopAsync(System.Threading.CancellationToken)" /> is called.</param>
+        /// <returns>A <see cref="T:System.Threading.Tasks.Task" /> that represents the long running operations.</returns>
+        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+        {
+            var time = TimeSpan.FromMinutes(5);
+            await Task.Delay(time, stoppingToken);
+            while (!stoppingToken.IsCancellationRequested)
+            {
+                using var scope = _serviceScopeFactory.CreateScope();
+                var blacklistRepository = scope.ServiceProvider.GetService<IBlacklistRepository>();
+                var expiredBlackListItems =
+                    await blacklistRepository!.QueryAsync(d => !d.IsDeleted && d.Expired <= DateTime.Now);
+                foreach (var blacklistItem in expiredBlackListItems)
+                {
+                    await blacklistRepository.RemoveAsync(blacklistItem, true, stoppingToken);
+                }
+
+                await Task.Delay(time, stoppingToken);
+            }
+        }
+    }
+}

+ 21 - 0
src/CallCenter.CacheManager/CallCenter.CacheManager.csproj

@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="CacheManager.Core" Version="1.2.0" />
+    <PackageReference Include="CacheManager.Microsoft.Extensions.Caching.Memory" Version="1.2.0" />
+    <PackageReference Include="CacheManager.Microsoft.Extensions.Configuration" Version="1.2.0" />
+    <PackageReference Include="CacheManager.Microsoft.Extensions.Logging" Version="1.2.0" />
+    <PackageReference Include="CacheManager.StackExchange.Redis" Version="1.2.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\CallCenter\CallCenter.csproj" />
+  </ItemGroup>
+
+</Project>

+ 135 - 0
src/CallCenter.CacheManager/DefaultTypedCache.cs

@@ -0,0 +1,135 @@
+using System.Collections;
+using CacheManager.Core;
+using CacheManager.Core.Internal;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using XF.Domain.Cache;
+
+namespace CallCenter.CacheManager
+{
+    public class DefaultTypedCache<TValue> : ITypedCache<TValue>
+        where TValue : class
+    {
+        private readonly ICacheManager<TValue> _cache;
+        private readonly IServiceScopeFactory _serviceScopeFactory;
+        private readonly string _region;
+
+        public DefaultTypedCache(ICacheManager<TValue> cache, IServiceScopeFactory serviceScopeFactory)
+        {
+            _cache = cache;
+            _serviceScopeFactory = serviceScopeFactory;
+            _region = CreateRegion();
+        }
+
+        public bool Add(string key, TValue value, ExpireMode? expireMode = ExpireMode.None, TimeSpan? timeout = null) =>
+            _cache.Add(CreateCacheItem(key, value, expireMode, timeout));
+
+        public void Put(string key, TValue value, ExpireMode? expireMode, TimeSpan? timeout = null) =>
+            _cache.Put(CreateCacheItem(key, value, expireMode, timeout));
+
+        public TValue Update(string key, Func<TValue, TValue> updateValue) => _cache.Update(key, _region, updateValue);
+
+        public TValue? Get(string key) => _cache.Get<TValue>(key, _region);
+
+        public void Expire(string key, DateTime absoluteExpiration) => _cache.Expire(key, _region, absoluteExpiration);
+
+        public void Expire(string key, TimeSpan slidingExpiration) => _cache.Expire(key, _region, slidingExpiration);
+
+        public void Expire(string key, ExpireMode? expireMode, TimeSpan? timeout = null) =>
+            _cache.Expire(key, _region, SwitchExpireMode(expireMode), timeout ?? TimeSpan.FromMinutes(10));
+
+        public TValue AddOrUpdate(string key, TValue addValue, Func<TValue, TValue> updateValue, ExpireMode? expireMode = ExpireMode.None, TimeSpan? timeout = null) =>
+            _cache.AddOrUpdate(CreateCacheItem(key, addValue, expireMode, timeout), updateValue);
+
+        public TValue GetOrAdd(string key, TValue value, ExpireMode? expireMode, TimeSpan? timeout = null) =>
+            _cache.GetOrAdd(key, _region, (k, r) => CreateCacheItem(key, value, expireMode, timeout)).Value;
+
+        public TValue GetOrAdd(string key, Func<string, TValue> valueFactory, ExpireMode? expireMode, TimeSpan? timeout = null) =>
+            _cache.GetOrAdd(key, _region, (k, r) => CreateCacheItem(key, valueFactory(key), expireMode, timeout)).Value;
+
+        public bool TryGetOrAdd(string key, Func<string, TValue> valueFactory, out TValue? value, ExpireMode? expireMode, TimeSpan? timeout = null)
+        {
+            var result = _cache.TryGetOrAdd(key,
+                _region,
+                (k, r) => CreateCacheItem(key, valueFactory(key), expireMode, timeout),
+                out var valueItem);
+
+            value = result ? valueItem.Value : default;
+            return result;
+        }
+
+        public bool TryUpdate(string key, Func<TValue, TValue> updateValue, out TValue? value)
+        {
+            return _cache.TryUpdate(key, _region, updateValue, out value);
+        }
+
+        public bool Remove(string key) => _cache.Remove(key, _region);
+        public bool Exists(string key) => _cache.Exists(key, _region);
+
+        private static ExpirationMode SwitchExpireMode(ExpireMode? expireMode) =>
+            expireMode switch
+            {
+                ExpireMode.None => ExpirationMode.None,
+                ExpireMode.Sliding => ExpirationMode.Sliding,
+                ExpireMode.Absolute => ExpirationMode.Absolute,
+                null => ExpirationMode.None,
+                _ => throw new ArgumentOutOfRangeException(nameof(expireMode), expireMode, null)
+            };
+
+        private CacheItem<TValue> CreateCacheItem(string key, TValue value, ExpireMode? expireMode, TimeSpan? timeout) =>
+            new CacheItem<TValue>(key, _region, value, SwitchExpireMode(expireMode), timeout ?? TimeSpan.FromMinutes(10));
+
+        public string CreateRegion()
+        {
+            var valueType = typeof(TValue);
+            //if (valueType.IsGenericType)
+            //{
+            //    throw new NotSupportedException("TCachedValue can not be a Generic Type");
+            //}
+
+            //if (valueType == typeof(string))
+            //{
+            //    throw new NotSupportedException("TCachedValue can not be a string");
+            //}
+
+            if (string.IsNullOrWhiteSpace(valueType.FullName))
+            {
+                throw new NotSupportedException("FullName of type TCachedValue can not be null");
+            }
+
+            using var scope = _serviceScopeFactory.CreateScope();
+            var options = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<CacheOptions>>();
+            var prefix = options.Value.Prefix;
+
+            //if (valueType.TryGetCustomAttribute<CacheAttribute>(false, out var attr))
+            //{
+            //    if (!string.IsNullOrEmpty(attr.Region))
+            //    {
+            //        return attr.Region;
+            //    }
+            //}
+
+            string region;
+
+            if (valueType.IsGenericType && valueType.GetInterfaces().Any(d => d == typeof(IEnumerable)))
+            {
+                region = "Collection";
+            }
+            else
+            {
+                region = valueType.FullName.Replace('.', ':');
+            }
+
+            if (!string.IsNullOrEmpty(prefix))
+            {
+                prefix += ':';
+                if (!region.StartsWith(prefix))
+                {
+                    region = prefix + region;
+                }
+            }
+            return region;
+        }
+
+    }
+}

+ 46 - 0
src/CallCenter.CacheManager/JsonCacheItem.cs

@@ -0,0 +1,46 @@
+using System.Text.Json.Serialization;
+using CacheManager.Core;
+using CacheManager.Core.Internal;
+
+namespace CallCenter.CacheManager
+{
+    internal class JsonCacheItem<T> : SerializerCacheItem<T>
+    {
+        [JsonConstructor]
+        public JsonCacheItem()
+        {
+            
+        }
+
+        public JsonCacheItem(ICacheItemProperties properties, object value) : base(properties, value)
+        {
+        }
+
+        [JsonPropertyName("createdUtc")]
+        public override long CreatedUtc { get; set; }
+
+        [JsonPropertyName("expirationMode")]
+        public override ExpirationMode ExpirationMode { get; set; }
+
+        [JsonPropertyName("expirationTimeout")]
+        public override double ExpirationTimeout { get; set; }
+
+        [JsonPropertyName("key")]
+        public override string Key { get; set; }
+
+        [JsonPropertyName("lastAccessedUtc")]
+        public override long LastAccessedUtc { get; set; }
+
+        [JsonPropertyName("region")]
+        public override string Region { get; set; }
+
+        [JsonPropertyName("usesExpirationDefaults")]
+        public override bool UsesExpirationDefaults { get; set; }
+
+        [JsonPropertyName("valueType")]
+        public override string ValueType { get; set; }
+
+        [JsonPropertyName("value")]
+        public override T Value { get; set; }
+    }
+}

+ 57 - 0
src/CallCenter.CacheManager/StartupExtensions.cs

@@ -0,0 +1,57 @@
+using CacheManager.Core;
+using Microsoft.Extensions.DependencyInjection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using XF.Domain.Cache;
+
+namespace CallCenter.CacheManager
+{
+    public static class StartupExtensions
+    {
+        public static IServiceCollection AddCache(this IServiceCollection services, Action<CacheOptions> action)
+        {
+            var options = new CacheOptions();
+            action(options);
+
+            services.AddCacheManagerConfiguration(cfg =>
+            {
+                cfg
+                    .WithUpdateMode(CacheUpdateMode.Up)
+                    .WithMicrosoftMemoryCacheHandle()
+                    .And
+                    .WithRedisConfiguration("redis", options.ConnectionString)
+                //    .WithRedisConfiguration("redis", d =>
+                //{
+                //    d.WithDatabase(0).WithEndpoint("redis.fengwo.com", 6380);
+                //})
+                .WithSerializer(typeof(SystemTextJsonSerializer), new JsonSerializerOptions
+                {
+                    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+                    DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
+                    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+                })
+                    .WithRedisBackplane("redis")
+                    .WithRedisCacheHandle("redis", true);
+
+                //cfg.AddCacheEvents(services);
+            })
+                .AddCacheManager()
+                .Configure(action)
+                .AddSingleton(typeof(ITypedCache<>), typeof(DefaultTypedCache<>))
+                ;
+
+            return services;
+        }
+    }
+
+    public class CacheOptions
+    {
+        public string ConnectionString { get; set; }
+
+        public int Port { get; set; }
+
+        public string Prefix { get; set; }
+
+        public bool EnableKeyspaceNotifications { get; set; }
+    }
+}

+ 42 - 0
src/CallCenter.CacheManager/SystemTextJsonSerializer.cs

@@ -0,0 +1,42 @@
+using System.Text.Json;
+using CacheManager.Core.Internal;
+
+namespace CallCenter.CacheManager
+{
+    public class SystemTextJsonSerializer: CacheSerializer
+    {
+        private static readonly Type OpenGenericType = typeof(JsonCacheItem<>);
+
+        private readonly JsonSerializerOptions _jsonSerializerOptions;
+
+        public SystemTextJsonSerializer(JsonSerializerOptions jsonSerializerOptions)
+        {
+            _jsonSerializerOptions = jsonSerializerOptions;
+        }
+
+        public SystemTextJsonSerializer(): this(new JsonSerializerOptions(JsonSerializerDefaults.Web))
+        {
+            
+        }
+
+        protected override Type GetOpenGeneric()
+        {
+            return OpenGenericType;
+        }
+
+        protected override object CreateNewItem<TCacheValue>(ICacheItemProperties properties, object value)
+        {
+            return new JsonCacheItem<TCacheValue>(properties, value);
+        }
+
+        public override byte[] Serialize<T>(T value)
+        {
+            return JsonSerializer.SerializeToUtf8Bytes(value, _jsonSerializerOptions);
+        }
+
+        public override object Deserialize(byte[] data, Type target)
+        {
+            return JsonSerializer.Deserialize(data, target, _jsonSerializerOptions);
+        }
+    }
+}

+ 18 - 0
src/CallCenter.NewRock/CallCenter.NewRock.csproj

@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\CallCenter\CallCenter.csproj" />
+    <ProjectReference Include="..\NewRock.Sdk\NewRock.Sdk.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
+  </ItemGroup>
+
+</Project>

+ 860 - 0
src/CallCenter.NewRock/DeviceManager.cs

@@ -0,0 +1,860 @@
+using CallCenter.Calls;
+using CallCenter.Devices;
+using CallCenter.Share.Dtos;
+using CallCenter.Tels;
+using MapsterMapper;
+using Microsoft.Extensions.Options;
+using NewRock.Sdk;
+using NewRock.Sdk.Accept.Request;
+using NewRock.Sdk.Control.Request;
+using NewRock.Sdk.Control.Request.Base;
+using NewRock.Sdk.Manage.Request;
+using NewRock.Sdk.Transfer.Conference.Request;
+using NewRock.Sdk.Transfer.Connect.Request;
+using NewRock.Sdk.Transfer.Queue.Request;
+using System.Text.RegularExpressions;
+using NewRock.Sdk.Control.Response;
+using XF.Domain.Dependency;
+using XF.Domain.Exceptions;
+using Group = NewRock.Sdk.Control.Request.Group;
+using CallCenter.Share.Enums;
+
+namespace CallCenter.NewRock
+{
+    public class DeviceManager : IDeviceManager, IScopeDependency
+    {
+        private readonly INewRockClient _newRockClient;
+        private readonly ICallRepository _callRepository;
+        private readonly IOptionsSnapshot<DeviceConfigs> _options;
+        private readonly IMapper _mapper;
+
+        public DeviceManager(INewRockClient newRockClient, IOptionsSnapshot<DeviceConfigs> options, IMapper mapper, ICallRepository callRepository)
+        {
+            _newRockClient = newRockClient;
+            _options = options;
+            _mapper = mapper;
+            _callRepository = callRepository;
+        }
+
+        #region 查询
+
+        public async Task<TelDto> QueryTelAsync(string TelNo, CancellationToken cancellationToken)
+        {
+            try
+            {
+                var result = await _newRockClient.QueryExt(
+                new QueryExtRequest() { Attribute = "Query", Ext = new Ext { Id = TelNo } },
+                _options.Value.ReceiveKey, _options.Value.Expired, cancellationToken);
+
+                if (result?.Ext.Outer != null)
+                {
+                    var telDto = new TelDto();
+                    telDto.CPN = result.Ext.Outer.From;
+                    telDto.CDPN = result.Ext.Outer.To;
+                    telDto.TelStatusInfo = ETelStatusInfo.Out;
+                    telDto.ConversationId = result.Ext.Outer.Id;
+                    if (result.Ext.Outer.State == "talk")
+                    {
+                        telDto.TelStatus = Share.Enums.ETelStatus.Talk;
+                        return telDto;
+                    }
+                    else if (result.Ext.Outer.State == "wait")
+                    {
+                        telDto.TelStatus = Share.Enums.ETelStatus.Wait;
+                    }
+                    return telDto;
+                }
+                if (result?.Ext.Visitor != null)
+                {
+                    var telDto = new TelDto();
+                    telDto.CPN = result.Ext.Visitor.From;
+                    telDto.CDPN = result.Ext.Visitor.To;
+                    telDto.TelStatusInfo = ETelStatusInfo.Into;
+                    telDto.ConversationId = result.Ext.Visitor.Id;
+                    if (result.Ext.Visitor.State == "talk")
+                    {
+                        telDto.TelStatus = Share.Enums.ETelStatus.Talk;
+                        return telDto;
+                    }
+                    else if (result.Ext.Visitor.State == "wait")
+                    {
+                        telDto.TelStatus = Share.Enums.ETelStatus.Wait;
+                    }
+                    return telDto;
+                }
+                if (result?.Ext.Ext != null)
+                {
+                    var telDto = new TelDto();
+                    telDto.CPN = result.Ext.Ext.Id;
+                    telDto.CDPN = result.Ext.Id;
+                    telDto.TelStatusInfo = ETelStatusInfo.Ext;
+                    if (result.Ext.Ext.State == "talk")
+                    {
+                        telDto.TelStatus = Share.Enums.ETelStatus.Talk;
+                        return telDto;
+                    }
+                    else if (result.Ext.Ext.State == "wait")
+                    {
+                        telDto.TelStatus = Share.Enums.ETelStatus.Wait;
+                    }
+                    return telDto;
+                }
+                return null;
+            }
+            catch
+            {
+                return null;
+            }
+        }
+
+
+        /// <summary>
+        /// 查询所有分机
+        /// </summary>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task<List<Tel>> QueryTelsAsync(CancellationToken cancellationToken)
+        {
+            var result = await _newRockClient.QueryDeviceInfo(
+                new QueryDeviceInfoRequest { Attribute = "Query", DeviceInfo = string.Empty },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+            var exts = result.Devices.Ext;
+            return _mapper.Map<List<Tel>>(exts);
+        }
+
+        /// <summary>
+        /// 查询所有分机组
+        /// </summary>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task<List<TelGroup>> QueryTelGroupsAsync(CancellationToken cancellationToken)
+        {
+            var result = await _newRockClient.QueryDeviceInfo(
+                new QueryDeviceInfoRequest { Attribute = "Query", DeviceInfo = string.Empty },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+
+            var groups = result.Devices.Group;
+            return _mapper.Map<List<TelGroup>>(groups);
+        }
+
+        /// <summary>
+        /// 查询语音文件
+        /// </summary>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task<string> VoiceQueryListAsync(CancellationToken cancellationToken)
+        {
+            var result = await _newRockClient.VoiceQueryList(new VoiceQueryListRequest()
+                {
+                    Attribute = "Query",
+                    VoiceFile = "",
+                }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+            return result?.VoiceFile;
+        }
+
+        public async Task QueryGroupAsync(string groupId, CancellationToken cancellationToken)
+        {
+            await _newRockClient.QueryExtGroup(new QueryExtGroupRequest()
+                { Attribute = "Query", Group = new QueryExtGroup() { Id = groupId } },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken
+            );
+        }
+
+
+        #endregion
+
+        #region 配置
+        /// <summary>
+        /// 分机休息
+        /// </summary>
+        /// <param name="telNo">分机号</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task TelRestAsync(string telNo, CancellationToken cancellationToken)
+        {
+            var telModel = await _newRockClient.QueryExt(new QueryExtRequest()
+            {
+                Attribute = "Query",
+                Ext = new Ext() { Id = telNo }
+            }, _options.Value.ReceiveKey,
+            _options.Value.Expired,
+            cancellationToken);
+
+            if (telModel == null)
+                throw new UserFriendlyException("未知分机");
+
+            await _newRockClient.ConfigExt(
+                new AssginConfigExtRequest() { Attribute = "Assign", Ext = new ConfigExt() { Lineid = telModel.Ext.LineId, Groups=telModel.Ext.Group.Select(x=>x.Id).ToList(), No_Disturb = "On" } },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 分机结束休息
+        /// </summary>
+        /// <param name="telNo">分机号</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task TelEndRestAsync(string telNo, CancellationToken cancellationToken)
+        {
+            var telModel = await _newRockClient.QueryExt(new QueryExtRequest()
+            {
+                Attribute = "Query",
+                Ext = new Ext() { Id = telNo }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+
+            if (telModel == null)
+                throw new UserFriendlyException("未知分机");
+
+            await _newRockClient.ConfigExt(
+                new AssginConfigExtRequest() { Attribute = "Assign", Ext = new ConfigExt() { Lineid = telModel.Ext.LineId, Groups = telModel.Ext.Group.Select(x => x.Id).ToList(), No_Disturb = "Off" } },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 删除语音文件
+        /// </summary>
+        /// <param name="voiceName"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task RemoveVoiceFileAsync(string voiceName, CancellationToken cancellationToken)
+        {
+            await _newRockClient.RemoveVoiceFile(new RemoveVoiceFileRequest()
+            {
+                Attribute = "Remove",
+                VoiceFile = voiceName
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 配置语音菜单
+        /// </summary>
+        /// <param name="menuId"></param>
+        /// <param name="voiceFile"></param>
+        /// <param name="repeat"></param>
+        /// <param name="infoLength"></param>
+        /// <param name="exit"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task AssginConfigMenuAsync(string menuId, string voiceFile, string repeat, string infoLength,
+            string exit,
+            CancellationToken cancellationToken)
+        {
+            if (!int.TryParse(menuId, out int mId))
+                throw new UserFriendlyException("请输入数字");
+
+            if (mId < 1 || mId > 50)
+                throw new UserFriendlyException("菜单只允许在1-50范围内");
+
+            if (exit!="")
+            {
+                Regex r = new Regex(@"^[a-d]|[A-D]|[1-9]|[*]|[#]$");
+                if (!r.IsMatch(exit))
+                    throw new UserFriendlyException("输入指令不合法,合法值:A-D、1-9、*、#");
+            }
+
+            var resp = await _newRockClient.ConfigMenu(new AssginConfigMenuRequest()
+            {
+                Attribute = "Assign",
+                Menu = new AssginConfigMenuMenu()
+                {
+                    Id = menuId,
+                    VoiceFile = voiceFile,
+                    InfoLength = infoLength,
+                    Exit = exit,
+                    Repeat = repeat,
+                }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 配置分机组
+        /// </summary>
+        /// <param name="groupId"></param>
+        /// <param name="voiceFile"></param>
+        /// <param name="distribution"></param>
+        /// <param name="ext"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task AssginConfigGroupAsync(string groupId, string distribution, List<string> ext, string? voiceFile = "",CancellationToken cancellationToken=default)
+        {
+            if (!int.TryParse(groupId, out int mId))
+                throw new UserFriendlyException("请输入数字");
+
+            if (mId < 1 || mId > 50)
+                throw new UserFriendlyException("分机组只允许在1-50范围内");
+
+            var groupModel = new Group()
+            {
+                Id = groupId,
+                Distribution = distribution,
+                Ext = ext,
+            };
+            if (!string.IsNullOrEmpty(voiceFile))
+                groupModel.Voicefile = voiceFile;
+
+            var resp = await _newRockClient.ConfigExtGroup(new AssginConfigGroupRequest()
+            {
+                Attribute = "Assign",
+                Group = groupModel
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 更新分机组
+        /// </summary>
+        /// <param name="groupId"></param>
+        /// <param name="ext"></param>
+        /// <param name="cancellationToken"></param>
+        /// <param name="isAdd"></param>
+        /// <returns></returns>
+        public async Task ModifyGroupExtAsync(string groupId, List<string> ext,string voicefile="",bool isAdd=true,CancellationToken cancellationToken = default)
+        {
+            if (!int.TryParse(groupId, out int mId))
+                throw new UserFriendlyException("请输入数字");
+
+            if (mId < 1 || mId > 50)
+                throw new UserFriendlyException("分机组只允许在1-50范围内");
+
+            //查询原设备数据
+            var result = await _newRockClient.QueryExtGroup(
+                new QueryExtGroupRequest() { Attribute = "Query", Group = new QueryExtGroup() { Id = groupId } },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken
+            );
+            //更新
+            var exts = result.Group[0].Ext;
+            var groupModel = new Group()
+            {
+                Id = groupId,
+                Voicefile = voicefile
+            };
+
+            if (isAdd)
+            {
+                for (int i = 0; i < ext.Count; i++)
+                {
+                    exts.Add(new QueryExtGroupExt(){ Id = ext[i] });
+                }
+            }
+            else
+            {
+                for (int i = 0; i < ext.Count; i++)
+                {
+                    exts.Remove(exts.First(x=>x.Id == ext.First()));
+                }
+            }
+
+            groupModel.Ext = exts.Select(x=>x.Id).ToList();
+            await _newRockClient.ConfigExtGroup(
+                new AssginConfigGroupRequest() { Attribute = "Assign", Group = groupModel, },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken
+                );
+        }
+
+        /// <summary>
+        /// 上班/下班
+        /// </summary>
+        /// <param name="telNo"></param>
+        /// <param name="staffNo"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task UpdateStaffNoAsync(string telNo, string staffNo,string lineId, CancellationToken cancellationToken)
+        {
+            var telModel = await _newRockClient.QueryExt(new QueryExtRequest()
+                {
+                    Attribute = "Query",
+                    Ext = new Ext() { Id = telNo }
+                }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+
+            if (telModel == null)
+                throw new UserFriendlyException("未知分机");
+
+            await _newRockClient.ConfigExt(
+                new AssginConfigExtRequest() { Attribute = "Assign", Ext = new ConfigExt() { Id = telNo,Lineid = lineId, Staffid = staffNo } },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+        #endregion
+
+        #region 通话控制
+
+        /// <summary>
+        /// 保持通话
+        /// </summary>
+        /// <param name="telNo">分机号</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task HoldAsync(string telNo, CancellationToken cancellationToken)
+        {
+            await _newRockClient.HoldOrUnHold(
+                new HoldSetRequest() { Attribute = "Hold", Ext = new Ext() { Id = telNo } }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 恢复通话(解除hold状态)
+        /// </summary>
+        /// <param name="telNo">分机号</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task UnHoldAsync(string telNo, CancellationToken cancellationToken)
+        {
+            await _newRockClient.HoldOrUnHold(
+                new HoldSetRequest() { Attribute = "Unhold", Ext = new Ext() { Id = telNo } }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 静音开启
+        /// </summary>
+        /// <param name="telNo">分机号</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task MuteAsync(string telNo, CancellationToken cancellationToken)
+        {
+            await _newRockClient.MuteOrUnMute(
+                new MuteSetRequest() { Attribute = "Mute", Ext = new Ext() { Id = telNo } }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 解除静音
+        /// </summary>
+        /// <param name="telNo">分机号</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task UnMuteAsync(string telNo, CancellationToken cancellationToken)
+        {
+            await _newRockClient.MuteOrUnMute(
+                new MuteSetRequest() { Attribute = "Unmute", Ext = new Ext() { Id = telNo } },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 强拆分机
+        /// </summary>
+        /// <param name="extId"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task ClearExtAsync(string extId, CancellationToken cancellationToken)
+        {
+            await _newRockClient.ClearCall(new ClearCallRequest()
+            {
+                Attribute = "Clear",
+                Ext = new Ext()
+                {
+                    Id = extId
+                }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 强拆来电
+        /// </summary>
+        /// <param name="visitorId"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task ClearVisitorAsync(string visitorId, CancellationToken cancellationToken)
+        {
+            await _newRockClient.ClearCall(new ClearCallRequest()
+            {
+                Attribute = "Clear",
+                Visitor = new ClearCallVisitor() { Id = visitorId }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 强拆去电
+        /// </summary>
+        /// <param name="outerId"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task ClearOuterAsync(string outerId, CancellationToken cancellationToken)
+        {
+            await _newRockClient.ClearCall(new ClearCallRequest()
+            {
+                Attribute = "Clear",
+                Outer = new ClearCallOuter() { Id = outerId }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 来电受理
+        /// </summary>
+        /// <param name="visitorId"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task AcceptVisitorAsync(string visitorId, CancellationToken cancellationToken)
+        {
+            await _newRockClient.AcceptVisitor(new AcceptVisitorRequest()
+            {
+                Attribute = "Accept",
+                Visitor = new AcceptVisitorModel() { Id = visitorId }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+        #endregion
+
+        #region 连接呼叫
+
+        /// <summary>
+        /// 分机呼分机
+        /// </summary>
+        /// <param name="from">主叫分机号</param>
+        /// <param name="to">被叫分机号</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task ExtToExtAsync(string from, string to, CancellationToken cancellationToken)
+        {
+            await _newRockClient.ExtensionToExtension(
+                new ExtensionToExtensionRequest()
+                {
+                    Attribute = "Connect",
+                    Exts = new List<ExtToExtExt>() { new ExtToExtExt() { Id = from }, new ExtToExtExt() { Id = to } }
+                }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 分机呼外部电话
+        /// </summary>
+        /// <param name="from">分机号</param>
+        /// <param name="to">外部电话,外地电话加拨0</param>
+        /// <param name="cancellationToken"></param>
+        /// <param name="trunkid">指定中继线路(可为空),为空时默认由OM分配</param>
+        /// <returns></returns>
+        public async Task ExtToOuterAsync(string from, string to, CancellationToken cancellationToken, string trunkid = "")
+        {
+            await _newRockClient.ExtToOuter(
+                    new ExtToOuterRequest()
+                    {
+                        Attribute = "Connect",
+                        Ext = new ExtToOuterExtRequest() { Id = from },
+                        Outer = new ExtToOuterOuterRequest() { To = to },
+                        Trunk = new ExtToOuterTrunkRequest() { Id = trunkid }
+                    },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 来电转分机
+        /// </summary>
+        /// <param name="visitorId">来电会话ID</param>
+        /// <param name="telNo">分机号</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task VisitorToExtAsync(string visitorId, string telNo, CancellationToken cancellationToken)
+        {
+            await _newRockClient.VisitorToExt(new VisitorToExtRequest()
+            {
+                Attribute = "Connect",
+                Visitor = new VisitorToExtVisitor() { Id = visitorId },
+                Ext = new VisitorToExtExt() { Id = telNo }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 来电转外部电话
+        /// </summary>
+        /// <param name="visitorId">来电会话ID</param>
+        /// <param name="outerPhoneNum">外部电话,外地电话加拨0</param>
+        /// <param name="cancellationToken"></param>
+        /// <param name="display">来电号码,用来透传主叫号码,使去电方的来电显示号码为实际来电号码。</param>
+        /// <returns></returns>
+        public async Task VisitorToOuterAsync(string visitorId, string outerPhoneNum, CancellationToken cancellationToken,
+            string display = "")
+        {
+            await _newRockClient.VisitorToOuter(new VisitorToOuterRequest()
+            {
+                Attribute = "Connect",
+                Visitor = new VisitorToOuterVisitor() { Id = visitorId },
+                Outer = new VisitorToOuterOuter() { To = outerPhoneNum, Display = display },
+
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+
+        /// <summary>
+        /// 来电转语音菜单
+        /// </summary>
+        /// <param name="visitorId">来电会话ID</param>
+        /// <param name="menuId">菜单ID</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task VisitorToMenuAsync(string visitorId, string menuId, CancellationToken cancellationToken)
+        {
+            await _newRockClient.VisitorToMenu(new VisitorToMenuRequest()
+            {
+                Attribute = "Connect",
+                Visitor = new VisitorToMenuVisitor() { Id = visitorId },
+                Menu = new VisitorToMenuMenu() { Id = menuId }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 来电转分机组
+        /// </summary>
+        /// <param name="visitorId"></param>
+        /// <param name="groupId"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task VisitorToGroupAsync(string visitorId, string groupId, CancellationToken cancellationToken)
+        {
+            await _newRockClient.VisitorToGroupQueue(new VisitorToGroupQueueRequest()
+            {
+                Attribute = "Queue",
+                Visitor = new VisitorToGroupQueueVisitor() { Id = visitorId },
+                Group = new VisitorToGroupQueueGroup() { Id = groupId }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 去电转分机
+        /// </summary>
+        /// <param name="outerId">去电会话ID</param>
+        /// <param name="telNo">分机号</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task OuterToExtAsync(string outerId, string telNo, CancellationToken cancellationToken)
+        {
+            await _newRockClient.OuterToExt(new OuterToExtRequest()
+            {
+                Attribute = "Connect",
+                Outer = new OuterToExtOuter() { Id = outerId },
+                Ext = new OuterToExtExt() { Id = telNo }
+            },
+                _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 去电转外部电话
+        /// </summary>
+        /// <param name="outerId">去电会话ID</param>
+        /// <param name="outerPhoneNum">外部电话,外地电话加拨0</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task OuterToOuterAsync(string outerId, string outerPhoneNum, CancellationToken cancellationToken)
+        {
+            await _newRockClient.OuterToOuter(new OuterToOuterRequest()
+            {
+                Attribute = "Connect",
+                Outer = new List<OuterToOuterOuterModel>()
+                    {
+                        new OuterToOuterOuterModel() { Id = outerId },
+                        new OuterToOuterOuterModel() { To = outerPhoneNum }
+                    },
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+
+        /// <summary>
+        /// 语音菜单呼叫分机
+        /// </summary>
+        /// <param name="menuId">语音菜单ID</param>
+        /// <param name="telNo">分机号</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task MenuToExtAsync(string menuId, string telNo, CancellationToken cancellationToken)
+        {
+            await _newRockClient.MenuToExt(new MenuToExtRequest()
+            {
+                Attribute = "Connect",
+                Menu = new MenuToExtMenu() { Id = menuId },
+                Ext = new MenuToExtExt() { Id = telNo }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+
+        /// <summary>
+        /// 语音菜单呼外部电话
+        /// </summary>
+        /// <param name="menuId">语音菜单ID</param>
+        /// <param name="outerPhoneNum">外部电话,外地电话加拨0</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task MenuToOuterAsync(string menuId, string outerPhoneNum, CancellationToken cancellationToken)
+        {
+            await _newRockClient.MenuToOuter(new MenuToOuterRequest()
+            {
+                Attribute = "Connect",
+                Menu = new MenuToOuterMenu() { Id = menuId },
+                Outer = new MenuToOuterOuter() { To = outerPhoneNum }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 双向呼叫(回拨)
+        /// </summary>
+        /// <param name="outerOne">主叫外部电话,外地电话加拨0</param>
+        /// <param name="outerTwo">被叫外部电话,外地电话加拨0</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task TwoWayOuterAsync(string outerOne, string outerTwo, CancellationToken cancellationToken)
+        {
+            await _newRockClient.TwoWayOuter(new TwoWayOuterRequest()
+            {
+                Attribute = "Connect",
+                Outer = new List<TwoWayOuterOuter>()
+                    {
+                        new TwoWayOuterOuter(){ To = outerOne},
+                        new TwoWayOuterOuter(){ To = outerTwo}
+                    }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 语音插播(分机)
+        /// </summary>
+        /// <param name="voiceFileName">语音名称</param>
+        /// <param name="telNo">分机号</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task VoiceNewsFlashExtAsync(string voiceFileName, string telNo, CancellationToken cancellationToken)
+        {
+            await _newRockClient.VoiceNewsFlash(new VoiceNewsFlashRequest
+            {
+                Attribute = "Connect",
+                VoiceFile = voiceFileName,
+                Ext = new VoiceNewsFlashExt() { Id = telNo }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 语音插播(来电)
+        /// </summary>
+        /// <param name="voiceFileName">语音名称</param>
+        /// <param name="visitorId">来电会话ID</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task VoiceNewsFlashVisitorAsync(string voiceFileName, string visitorId,
+            CancellationToken cancellationToken)
+        {
+            await _newRockClient.VoiceNewsFlash(
+                new VoiceNewsFlashRequest
+                {
+                    Attribute = "Connect",
+                    VoiceFile = voiceFileName,
+                    Visitor = new VoiceNewsFlashVisitor() { Id = visitorId }
+                }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 语音插播(去电)
+        /// </summary>
+        /// <param name="voiceFileName">语音名称</param>
+        /// <param name="outerId">去电会话ID</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task VoiceNewsFlashOuterAsync(string voiceFileName, string outerId, CancellationToken cancellationToken)
+        {
+            await _newRockClient.VoiceNewsFlash(
+                new VoiceNewsFlashRequest
+                {
+                    Attribute = "Connect",
+                    VoiceFile = voiceFileName,
+                    Outer = new VoiceNewsFlashOuter() { Id = outerId }
+                }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 会议
+        /// </summary>
+        /// <param name="telNo">发起方分机号</param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task ConferenceMeetingAsync(string telNo, CancellationToken cancellationToken)
+        {
+            await _newRockClient.ConferenceMeeting(new ConferenceMeetingRequest()
+            {
+                Attribute = "Conference",
+                Ext = new ConferenceMeetingExt() { Id = telNo }
+            }, _options.Value.ReceiveKey,
+                _options.Value.Expired,
+                cancellationToken);
+        }
+
+        /// <summary>
+        /// 处理IVR响应
+        /// </summary>
+        /// <param name="callDetail"></param>
+        /// <param name="ivrAnswer"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task HandleIvrAnswerAsync(CallDetail callDetail, IvrAnswer ivrAnswer, CancellationToken cancellationToken)
+        {
+            if (string.IsNullOrEmpty(ivrAnswer.Content))
+                throw new UserFriendlyException("无效IVR应答参数");
+
+            //获取是来电或去电
+            var model = await _callRepository.GetAsync(x => x.Id == callDetail.CallId, cancellationToken);
+            
+        }
+
+        #endregion
+    }
+}

+ 370 - 0
src/CallCenter.NewRock/Handlers/DeviceEventHandler.cs

@@ -0,0 +1,370 @@
+using System.Security.Authentication;
+using CallCenter.Devices;
+using CallCenter.Notifications;
+using CallCenter.Share.Notifications;
+using MapsterMapper;
+using MediatR;
+using Microsoft.Extensions.Logging;
+using NewRock.Sdk.Events;
+using NewRock.Sdk.Extensions;
+using NewRock.Sdk.Security;
+using XF.Domain.Dependency;
+
+namespace CallCenter.NewRock.Handlers
+{
+    public class DeviceEventHandler : IDeviceEventHandler, IScopeDependency
+    {
+        private readonly IMediator _mediator;
+        private readonly IMapper _mapper;
+        private readonly ILogger<DeviceEventHandler> _logger;
+        private static readonly string _cdrMarkup = "<Cdr id=";
+
+        public DeviceEventHandler(
+            IMediator mediator,
+            IMapper mapper,
+            ILogger<DeviceEventHandler> logger)
+        {
+            _mediator = mediator;
+            _mapper = mapper;
+            _logger = logger;
+        }
+
+        public async Task HandleAsync(Stream eventStream, DeviceConfigs deviceConfigs,
+            CancellationToken cancellationToken)
+        {
+            var sr = new StreamReader(eventStream);
+            var content = await sr.ReadToEndAsync();
+            _logger.LogInformation("收到设备事件:\r\n{content}", content);
+
+            if (content.Contains(_cdrMarkup))
+            {
+                // cdr事件
+                //通话记录报告
+                var eventBase = content.DeserializeWithAuthorize<NewRockCdrEvent>();
+                if (eventBase.value == null) return;
+                if (deviceConfigs.Authorize && (eventBase.authorize == null ||
+                                                !eventBase.authorize.IsAuthorized(deviceConfigs.SendKey)))
+                    throw new AuthenticationException("无有效身份认证信息");
+
+                var cdrRcv = content.DeserializeWithAuthorize<CdrEvent>();
+                await _mediator.Publish(_mapper.Map<CdrNotification>(cdrRcv.value!), cancellationToken);
+            }
+            else
+            {
+                var eventBase = content.DeserializeWithAuthorize<NewRockEvent>();
+                if (eventBase.value == null) return;
+                if (deviceConfigs.Authorize && (eventBase.authorize == null ||
+                                                !eventBase.authorize.IsAuthorized(deviceConfigs.SendKey)))
+                    throw new AuthenticationException("无有效身份认证信息");
+
+                switch (eventBase.value.Attribute)
+                {
+                    //分机上线事件发布
+                    case Event.ONLINE:
+                        var onLineRcv = content.DeserializeWithAuthorize<OnlineEvent>();
+                        await _mediator.Publish(_mapper.Map<OnlineNotification>(onLineRcv.value!), cancellationToken);
+                        break;
+                    //分机下线事件
+                    case Event.OFFLINE:
+                        var offLineRcv = content.DeserializeWithAuthorize<OfflineEvent>();
+                        await _mediator.Publish(_mapper.Map<OfflineNotification>(offLineRcv.value!), cancellationToken);
+                        break;
+                    //分机示闲事件
+                    case Event.IDLE:
+                        var idleRcv = content.DeserializeWithAuthorize<IdleEvent>();
+                        await _mediator.Publish(_mapper.Map<IdleNotification>(idleRcv.value!), cancellationToken);
+                        break;
+                    //分机示忙事件
+                    case Event.BUSY:
+                        var busyRcv = content.DeserializeWithAuthorize<BusyEvent>();
+                        await _mediator.Publish(_mapper.Map<BusyNotification>(busyRcv.value!), cancellationToken);
+                        break;
+                    //振铃事件
+                    case Event.RING:
+                        var ringRcv = content.DeserializeWithAuthorize<RingEvent>();
+                        //判断是哪种振铃事件
+                        //分机呼分机
+                        if (ringRcv.value?.Ext.Count > 1)
+                        {
+                            //await _mediator.Publish(_mapper.Map<RingExtToExtNotification>(ringRcv.value!),cancellationToken);
+                        }
+                        //来电转分机
+                        else if (ringRcv.value?.Ext.Count == 1 && ringRcv.value.Visitor != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<RingVisitorToExtNotification>(ringRcv.value!),
+                                cancellationToken);
+                        }
+                        //分机外呼
+                        else if (ringRcv.value?.Ext.Count == 1 && ringRcv.value.Outer != null && ringRcv.value.Outer.Id != "")
+                        {
+                            await _mediator.Publish(_mapper.Map<RingExtToOuterNotification>(ringRcv.value!), cancellationToken);
+                        }
+                        //menu呼叫分机
+                        else if (ringRcv.value?.Ext.Count == 1 && ringRcv.value.Menu != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<RingMenuToExtNotification>(ringRcv.value!),
+                                cancellationToken);
+                        }
+                        break;
+                    //回铃事件
+                    case Event.ALERT:
+                        var alertRcv = content.DeserializeWithAuthorize<AlertEvent>();
+                        //判断是 哪种回铃事件
+                        //来电转分机,分机回铃
+                        if (alertRcv.value?.Ext.Count == 1 && alertRcv.value.Visitor != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<AlertVisitorToExtNotification>(alertRcv.value!),
+                                cancellationToken);
+                        }
+                        //分机呼外部电话,外部电话回铃
+                        else if (alertRcv.value?.Ext.Count == 1 && alertRcv.value.Outer != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<AlertExtToOuterNotification>(alertRcv.value!),
+                                cancellationToken);
+                        }
+                        //分机呼分机,被叫分机回铃
+                        else if (alertRcv.value?.Ext.Count > 1)
+                        {
+                            await _mediator.Publish(_mapper.Map<AlertExtToExtNotification>(alertRcv.value!),
+                                cancellationToken);
+                        }
+                        //Menu外呼,外部电话回铃
+                        else if ((alertRcv.value?.Ext == null || alertRcv.value?.Ext.Count == 0) &&
+                                 alertRcv.value?.Outer != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<AlertMenuToOuterNotification>(alertRcv.value!),
+                                cancellationToken);
+                        }
+                        break;
+                    //呼叫应答事件
+                    case Event.ANSWER:
+                        var answerRcv = content.DeserializeWithAuthorize<AnswerEvent>();
+                        //分机呼分机,被叫分机应答
+                        if (answerRcv.value?.Ext.Count > 1)
+                        {
+                            await _mediator.Publish(_mapper.Map<AnswerExtToExtNotification>(answerRcv.value!),
+                                cancellationToken);
+                        }
+                        //来电转分机,分机应答
+                        else if (answerRcv.value?.Ext.Count == 1 && answerRcv.value.Visitor != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<AnswerViisitorToExtNotification>(answerRcv.value!),
+                                cancellationToken);
+                        }
+                        //去电转分机,分机应答
+                        else if (answerRcv.value?.Ext.Count == 1 && answerRcv.value?.Outer != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<AnswerExtToOuterNotification>(answerRcv.value!),
+                                cancellationToken);
+                        }
+                        break;
+                    //呼叫被应答事件
+                    case Event.ANSWERED:
+                        var answeredRcv = content.DeserializeWithAuthorize<AnsweredEvent>();
+                        //分机呼分机,主叫分机检查到被叫分机应答
+                        if (answeredRcv.value?.Ext.Count > 1)
+                        {
+                            await _mediator.Publish(_mapper.Map<AnsweredExtToExtNotification>(answeredRcv.value!),
+                                cancellationToken);
+                        }
+                        //分机呼外部电话,分机检查到外部电话应答
+                        else if (answeredRcv.value?.Ext.Count == 1 && answeredRcv.value?.Outer != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<AnsweredExtToOuterNotification>(answeredRcv.value!),
+                                cancellationToken);
+                        }
+                        //来电呼叫分机
+                        else if (answeredRcv.value?.Visitor != null && answeredRcv.value?.Ext.Count == 1)
+                        {
+                            await _mediator.Publish(_mapper.Map<AnsweredVisitorToExtNotification>(answeredRcv.value!),
+                                cancellationToken);
+                        }
+                        //分机呼外部电话,转接其他分机,其他分机应答
+                        else if (answeredRcv.value?.Outer != null)
+                        {
+                            await _mediator.Publish(
+                                _mapper.Map<AnsweredExtToOuterToExtNotification>(answeredRcv.value!),
+                                cancellationToken);
+                        }
+                        break;
+                    //通话结束事件
+                    case Event.BYE:
+                        var byeRcv = content.DeserializeWithAuthorize<ByeEvent>();
+                        //来电和分机的通话结束,来电挂断
+                        if (byeRcv.value?.Ext != null && byeRcv.value?.Visitor != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<ByeVisitorAndExtNotification>(byeRcv.value!),
+                                cancellationToken);
+                        }
+                        //来电和分机的通话,分机挂断
+                        else if (byeRcv.value?.Visitor != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<ByeVisitorOffNotification>(byeRcv.value!),
+                                cancellationToken);
+                        }
+                        //来电转去电的通话结束,来电挂断
+                        else if (byeRcv.value?.Outer != null && byeRcv.value?.Visitor != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<ByeVisitorAndOuterNotification>(byeRcv.value!),
+                                cancellationToken);
+                        }
+                        //其他条件
+                        else if (byeRcv.value?.Outer != null)
+                        {
+                            //分机和去电的通话结束,分机挂断 类型一
+                            if (byeRcv.value?.Ext != null)
+                            {
+                                await _mediator.Publish(_mapper.Map<ByeExtAndOuterOneNotification>(byeRcv.value!),
+                                    cancellationToken);
+                            }
+                            //双向外呼的通话结束,两个去电分别各有一个 BYE 事件
+                            else if (byeRcv.value?.Outer.From != "")
+                            {
+                                await _mediator.Publish(_mapper.Map<ByeOuterAndOuterNotification>(byeRcv.value!),
+                                    cancellationToken);
+                            }
+                            //分机和去电的通话结束,分机挂断 类型二
+                            else
+                            {
+                                await _mediator.Publish(_mapper.Map<ByeExtAndOuterTwoNotification>(byeRcv.value!),
+                                    cancellationToken);
+                            }
+                        }
+                        break;
+                    //呼叫转移事件
+                    case Event.DIVERT:
+                        var divertRcv = content.DeserializeWithAuthorize<DivertEvent>();
+                        //来电呼叫分机时,因分机设置了呼叫转移等原因,导致该呼叫被转移
+                        if (divertRcv.value?.Visitor != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<DivertVisitorToExtNotification>(divertRcv.value!),
+                                cancellationToken);
+                        }
+                        //分机呼叫其他分机时,因被叫分机设置了呼叫转移等原因,导致该呼叫被转移
+                        else if (divertRcv.value?.Ext != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<DivertExtToExtNotification>(divertRcv.value!),
+                                cancellationToken);
+                        }
+                        break;
+                    //呼叫临时事件
+                    case Event.TRANSIENT:
+                        var transientRcv = content.DeserializeWithAuthorize<TransientEvent>();
+                        if (transientRcv.value?.Outer != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<TransientOuterNotification>(transientRcv.value!),
+                                cancellationToken);
+                        }
+                        else if (transientRcv.value?.Visitor != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<TransientVisitorNotification>(transientRcv.value!),
+                                cancellationToken);
+                        }
+                        //TODO 处理来电临时事件
+                        break;
+                    //呼叫失败事件
+                    case Event.FAILED:
+                        var failedRcv = content.DeserializeWithAuthorize<FailedEvent>();
+                        await _mediator.Publish(_mapper.Map<FailedNotification>(failedRcv.value!), cancellationToken);
+                        break;
+                    //来电呼叫请求事件
+                    case Event.INVITE:
+                        var inviteRcv = content.DeserializeWithAuthorize<InviteEvent>();
+                        await _mediator.Publish(_mapper.Map<InviteNotification>(inviteRcv.value!), cancellationToken);
+                        break;
+                    //来电呼入事件
+                    case Event.INCOMING:
+                        var incomingRcv = content.DeserializeWithAuthorize<IncomingEvent>();
+                        await _mediator.Publish(_mapper.Map<IncomingNotification>(incomingRcv.value!),
+                            cancellationToken);
+                        break;
+                    //按键信息事件
+                    case Event.DTMF:
+                        var dtmfRcv = content.DeserializeWithAuthorize<DtmfEvent>();
+                        await _mediator.Publish(_mapper.Map<DtmfNotification>(dtmfRcv.value!), cancellationToken);
+                        break;
+                    //语音文件播放完毕事件
+                    case Event.EndOfAnn:
+                        var endOfAnnRcv = content.DeserializeWithAuthorize<EndOfAnnEvent>();
+                        //来电转menu
+                        if (endOfAnnRcv.value?.Visitor != null && endOfAnnRcv.value?.Menu != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<EndOfAnnVisitorToMenuNotification>(endOfAnnRcv.value!),
+                                cancellationToken);
+                        }
+                        //去电转menu
+                        else if (endOfAnnRcv.value?.Outer != null && endOfAnnRcv.value?.Menu != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<EndOfAnnOuterToMenuNotification>(endOfAnnRcv.value!),
+                                cancellationToken);
+                        }
+                        //分机转menu
+                        else if (endOfAnnRcv.value?.Ext != null && endOfAnnRcv.value?.Menu != null)
+                        {
+                            await _mediator.Publish(_mapper.Map<EndOfAnnExtToMenuNotification>(endOfAnnRcv.value!),
+                                cancellationToken);
+                        }
+                        break;
+                    //分机组队列事件
+                    case Event.QUEUE:
+                        var queueRcv = content.DeserializeWithAuthorize<QueueEvent>();
+                        //分机呼入分机组
+                        if (queueRcv.value?.Ext != null && queueRcv.value?.Waiting != null)
+                        {
+                            //组内分机全忙,分机和来电相继呼入分机组
+                            if (queueRcv.value?.Waiting.Reason == "")
+                            {
+                                await _mediator.Publish(_mapper.Map<QueueExtToGroupBusyNotification>(queueRcv.value!),
+                                    cancellationToken);
+                            }
+                            //若所有分机离线,分机和来电呼叫分机组
+                            else if (queueRcv.value?.Waiting.Reason == "offline")
+                            {
+                                await _mediator.Publish(
+                                    _mapper.Map<QueueExtToGroupOfflineNotification>(queueRcv.value!),
+                                    cancellationToken);
+                            }
+                            //若排队满了,分机和来电呼叫分机组
+                            else if (queueRcv.value?.Waiting.Reason == "full")
+                            {
+                                await _mediator.Publish(_mapper.Map<QueueExtToGroupFullNotification>(queueRcv.value!),
+                                    cancellationToken);
+                            }
+                        }
+                        //组内分机全忙,分机和来电相继呼入分机组
+                        //来电呼入分机组
+                        else if (queueRcv.value?.Visitor != null && queueRcv.value?.Waiting != null)
+                        {
+                            //组内分机全忙,分机和来电相继呼入分机组
+                            if (queueRcv.value?.Waiting.Reason == "")
+                            {
+                                await _mediator.Publish(
+                                    _mapper.Map<QueueVisitorToGroupBusyNotification>(queueRcv.value!),
+                                    cancellationToken);
+                            }
+                            else if (queueRcv.value?.Waiting.Reason == "offline")
+                            {
+                                await _mediator.Publish(
+                                    _mapper.Map<QueueVisitorToGroupOfflineNotification>(queueRcv.value!),
+                                    cancellationToken);
+                            }
+                            else if (queueRcv.value?.Waiting.Reason == "full")
+                            {
+                                await _mediator.Publish(
+                                    _mapper.Map<QueueVisitorToGroupFullNotification>(queueRcv.value!),
+                                    cancellationToken);
+                            }
+                        }
+                        break;
+                    //系统重启事件
+                    case Event.BOOTUP:
+                        var bootupRcv = content.DeserializeWithAuthorize<BootupEvent>();
+                        await _mediator.Publish(_mapper.Map<BootupNotification>(bootupRcv.value!), cancellationToken);
+                        break;
+                    default:
+                        break;
+                }
+            }
+        }
+    }
+}

+ 207 - 0
src/CallCenter.NewRock/Mappers/EventConfigs.cs

@@ -0,0 +1,207 @@
+using CallCenter.Notifications;
+using CallCenter.Share.Dtos;
+using CallCenter.Share.Notifications;
+using CallCenter.Tels;
+using Mapster;
+using NewRock.Sdk.Control.Response;
+using NewRock.Sdk.Events;
+
+namespace CallCenter.NewRock.Mappers
+{
+    public class EventConfigs : IRegister
+    {
+        public void Register(TypeAdapterConfig config)
+        {
+            #region 上线事件
+            //上线事件
+            config.NewConfig<OnlineEvent, OnlineNotification>()
+                .Map(d => d.TelNo, x => x.Ext.Id);
+            #endregion
+
+            #region 离线事件
+
+            //下线事件
+            config.NewConfig<OfflineEvent, OfflineNotification>()
+                .Map(d => d.TelNo, x => x.Ext.Id);
+
+            #endregion
+
+            #region 空闲事件
+
+            config.NewConfig<IdleEvent, IdleNotification>()
+                .Map(d => d.TelNo, x => x.Ext.Id);
+
+            #endregion
+
+            #region 忙事件
+
+            config.NewConfig<BusyEvent, BusyNotification>()
+                .Map(d => d.TelNo, x => x.Ext.Id);
+
+            #endregion
+
+
+            #region 振铃时间
+            //分机呼分机
+            config.NewConfig<RingEvent, RingExtToExtNotification>()
+                .Map(d => d.FromTelNo, x => x.Ext.First().Id)
+                .Map(d => d.ToTelNo, x => x.Ext.Last().Id);
+            //来电转分机
+            config.NewConfig<RingEvent, RingVisitorToExtNotification>()
+                .Map(d => d.TelNo, x => x.Ext.First().Id);
+            //分机外呼/分机呼分机
+            config.NewConfig<RingEvent, RingExtToOuterNotification>()
+                .Map(d => d.TelNo, x => x.Ext.First().Id);
+            //menu呼叫分机
+            config.NewConfig<RingEvent, RingMenuToExtNotification>()
+                .Map(d => d.TelNo, x => x.Ext.First().Id)
+                .Map(d => d.MenuId, x => x.Menu.Id);
+
+            #endregion
+
+            #region 回铃事件
+            //来电转分机,分机回铃
+            config.NewConfig<AlertEvent, AlertVisitorToExtNotification>()
+                .Map(d => d.TelNo, x => x.Ext.First().Id);
+
+            //分机呼外部电话,外部电话回铃
+            config.NewConfig<AlertEvent, AlertExtToOuterNotification>()
+                .Map(d => d.TelNo, x => x.Ext.First().Id);
+
+            //分机呼分机,被叫分机回铃
+            config.NewConfig<AlertEvent, AlertExtToExtNotification>()
+                .Map(d => d.FromTelNo, x => x.Ext.First().Id)
+                .Map(d => d.ToTelNo, x => x.Ext.Last().Id);
+            #endregion
+
+            #region 呼叫应答事件
+
+            //分机呼分机,被叫分机应答
+            config.NewConfig<AnswerEvent, AnswerExtToExtNotification>()
+                .Map(d => d.FromTelNo, x => x.Ext.First().Id)
+                .Map(d=>d.ToTelNo,x=>x.Ext.Last().Id);
+            //来电转分机,分机应答
+            config.NewConfig<AnswerEvent, AnswerViisitorToExtNotification>()
+                .Map(d => d.TelNo, x => x.Ext.First().Id);
+
+            #endregion
+
+            #region 呼叫被应答事件
+            //分机呼分机,主叫分机检查到被叫分机应答
+            config.NewConfig<AnsweredEvent,AnsweredExtToExtNotification>()
+                .Map(d => d.FromTelNo, x => x.Ext.First().Id)
+                .Map(d => d.ToTelNo, x => x.Ext.Last().Id);
+            //分机呼外部电话,分机检查到外部电话应答
+            config.NewConfig<AnsweredEvent, AnsweredExtToOuterNotification>()
+                .Map(d => d.TelNo, x => x.Ext.First().Id);
+            //来电呼分机,分机应答
+            config.NewConfig<AnsweredEvent, AnsweredVisitorToExtNotification>()
+                .Map(d => d.TelNo, x => x.Ext.First().Id);
+            #endregion
+
+            #region 通话结束事件
+            //来电和分机的通话结束,来电挂断
+            config.NewConfig<ByeEvent, ByeVisitorAndExtNotification>()
+                .Map(d => d.TelNo, x => x.Ext.Id);
+            //分机和去电的通话结束,分机挂断 类型一
+            config.NewConfig<ByeEvent, ByeExtAndOuterOneNotification>()
+                .Map(d => d.TelNo, x => x.Ext.Id);
+            #endregion
+
+            #region 呼叫转移事件
+
+            config.NewConfig<DivertEvent, DivertExtToExtNotification>()
+                .Map(d => d.TelNo, x => x.Ext.Id);
+
+            #endregion
+
+            #region 呼叫临时事件
+
+            config.NewConfig<TransientEvent, TransientOuterNotification>()
+                .Map(d => d.TelNo, x => x.Ext.Id);
+
+            #endregion
+
+            #region 呼叫失败事件
+
+            config.NewConfig<FailedEvent, FailedNotification>()
+                .Map(d => d.CalledId, x => x.Called.Id)
+                .Map(d => d.Code, x => x.Err.Code)
+                .Map(d => d.Reason, x => x.Err.Reason);
+
+            #endregion
+
+            #region 来电呼叫请求事件
+
+            config.NewConfig<InviteEvent, InviteNotification>()
+                .Map(d => d.TrunkId, x => x.Trunk.Id);
+
+            #endregion
+
+            #region 来电呼入事件
+
+            config.NewConfig<IncomingEvent, IncomingNotification>()
+                .Map(d => d.TrunkId, x => x.Trunk.Id);
+
+            #endregion
+
+            #region 按键信息事件
+
+            config.NewConfig<DtmfEvent, DtmfNotification>()
+                .Map(d => d.Outer.MenuId, x => x.Outer.Menu.Id)
+                .Map(d => d.Outer.TrunkId, x => x.Outer.Trunk.Id)
+                .Map(d => d.Visitor.MenuId, x => x.Visitor.Menu.Id)
+                .Map(d => d.Visitor.TrunkId, x => x.Visitor.Trunk.Id);
+
+            #endregion
+
+
+            #region 语音文件播放完毕
+
+            config.NewConfig<EndOfAnnEvent, EndOfAnnVisitorToMenuNotification>()
+                .Map(d => d.MenuId, x => x.Menu.Id);
+            config.NewConfig<EndOfAnnEvent, EndOfAnnOuterToMenuNotification>()
+                .Map(d => d.MenuId, x => x.Menu.Id);
+            config.NewConfig<EndOfAnnEvent, EndOfAnnExtToMenuNotification>()
+                .Map(d => d.MenuId, x => x.Menu.Id)
+                .Map(d => d.TelNo, x => x.Ext.Id);
+
+            #endregion
+
+            #region 分机组队列事件
+
+            config.NewConfig<QueueEvent, QueueExtToGroupBusyNotification>()
+                .Map(d => d.TelNo, x => x.Ext.Id);
+            config.NewConfig<QueueEvent, QueueExtToGroupOfflineNotification>()
+                .Map(d => d.TelNo, x => x.Ext.Id);
+            config.NewConfig<QueueEvent, QueueExtToGroupFullNotification>()
+                .Map(d => d.TelNo, x => x.Ext.Id);
+
+
+            #endregion
+
+            #region 接口调用
+
+            //查询全部分机
+            config.NewConfig<DevicesExt, Tel>()
+                .Map(d => d.No, x => x.Id)
+                .Ignore(d=>d.Id);
+            //查询全部分机组
+            config.NewConfig<DevicesGroupExt, Tel>()
+                .Map(d => d.No, x => x.Id)
+                .Ignore(d => d.Id);
+            config.NewConfig<DevicesGroup, TelGroup>()
+                .Map(d => d.No, x => x.Id)
+                .Map(d => d.Tels, x => x.Ext)
+                .Ignore(d => d.Id);
+            //查询分机
+            config.NewConfig<QueryExtResponse, TelDto>()
+                .Map(d => d.CPN, x => x.Ext.Visitor.From)
+                .Map(d => d.CPN, x => x.Ext.Outer.From)
+                .Map(d => d.CDPN, x => x.Ext.Visitor.To)
+                .Map(d => d.CDPN, x => x.Ext.Visitor.To);
+            #endregion
+
+        }
+    }
+}

+ 21 - 0
src/CallCenter.NewRock/NewRockStartupExtensions.cs

@@ -0,0 +1,21 @@
+using CallCenter.NewRock.Mappers;
+using Mapster;
+using Microsoft.Extensions.DependencyInjection;
+using NewRock.Sdk;
+
+namespace CallCenter.NewRock
+{
+    public static class NewRockStartupExtensions
+    {
+        public static IServiceCollection AddNewRock(this IServiceCollection services, string deviceAddress)
+        {
+            services
+                .AddNewRockSdk(deviceAddress)
+                ;
+
+            TypeAdapterConfig.GlobalSettings.Scan(typeof(EventConfigs).Assembly);
+
+            return services;
+        }
+    }
+}

+ 187 - 0
src/CallCenter.Repository.SqlSugar/BaseRepository.cs

@@ -0,0 +1,187 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Text;
+using System.Threading.Tasks;
+using SqlSugar;
+using XF.Domain;
+using XF.Domain.Repository;
+
+namespace CallCenter.Repository.SqlSugar
+{
+    public abstract class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : class, IEntity<string>, IHasCreationTime, new()
+    {
+        protected ISugarUnitOfWork<CallCenterDbContext> Uow { get; }
+        protected ISqlSugarClient Db { get; }
+
+        public BaseRepository(ISugarUnitOfWork<CallCenterDbContext> uow)
+        {
+            Uow = uow;
+            Db = uow.Db;
+        }
+
+        public async Task<string> AddAsync(TEntity entity, CancellationToken cancellationToken = default)
+        {
+            var excEntity = await Db.Insertable(entity).ExecuteReturnEntityAsync();
+            return excEntity.Id;
+        }
+
+        /// <summary>
+        /// 批量插入(应用场景:小数据量,超出1万条建议另行实现)
+        /// </summary>
+        /// <param name="entities"></param>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task AddRangeAsync(List<TEntity> entities, CancellationToken cancellationToken = default)
+        {
+            await Db.Insertable(entities).ExecuteCommandAsync();
+        }
+
+        public async Task RemoveAsync(TEntity entity, bool? soft = false, CancellationToken cancellationToken = default)
+        {
+            if (soft.HasValue && soft.Value)
+            {
+                await Db.Deleteable(entity).IsLogic().ExecuteCommandAsync("IsDeleted", true, "DeletionTime");
+            }
+            else
+            {
+                await Db.Deleteable(entity).ExecuteCommandAsync();
+            }
+        }
+
+        public async Task RemoveAsync(string id, bool? soft = false, CancellationToken cancellationToken = default)
+        {
+            if (soft.HasValue && soft.Value)
+            {
+                await Db.Deleteable<TEntity>().In(id).IsLogic().ExecuteCommandAsync("IsDeleted", true, "DeletionTime");
+            }
+            else
+            {
+                await Db.Deleteable<TEntity>().In(id).ExecuteCommandAsync();
+            }
+        }
+
+        public async Task RemoveAsync(Expression<Func<TEntity, bool>> predicate, bool? soft, CancellationToken cancellationToken = default)
+        {
+            if (soft.HasValue && soft.Value)
+            {
+                await Db.Deleteable<TEntity>().Where(predicate).IsLogic().ExecuteCommandAsync("IsDeleted", true, "DeletionTime");
+            }
+            else
+            {
+                await Db.Deleteable<TEntity>().Where(predicate).ExecuteCommandAsync();
+            }
+        }
+
+        public async Task RemoveRangeAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default)
+        {
+            await Db.Deleteable<TEntity>(entities).ExecuteCommandAsync();
+        }
+
+        public async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
+        {
+            await Db.Updateable(entity)
+                .IgnoreColumns(ignoreAllNullColumns: true)
+                .IgnoreColumns(d => d.CreationTime)
+                .ExecuteCommandAsync();
+        }
+
+        public async Task UpdateRangeAsync(List<TEntity> entities, CancellationToken cancellationToken = default)
+        {
+            await Db.Updateable(entities)
+                .IgnoreColumns(d => d.CreationTime)
+                .ExecuteCommandAsync();
+        }
+
+        public async Task<TEntity?> GetAsync(string id, CancellationToken cancellationToken = default)
+        {
+            return await Db.Queryable<TEntity>().FirstAsync(d => d.Id == id);
+        }
+
+        public async Task<TEntity?> GetAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
+        {
+            return await Db.Queryable<TEntity>().FirstAsync(predicate);
+        }
+
+        public async Task<List<TEntity>> QueryAsync(Expression<Func<TEntity, bool>>? predicate = null, params (bool isWhere, Expression<Func<TEntity, bool>> expression)[] whereIfs)
+        {
+            var query = Db.Queryable<TEntity>().Where(predicate ??= d => true);
+            if (whereIfs.Any())
+            {
+                foreach (var whereIf in whereIfs)
+                {
+                    query = query.WhereIF(whereIf.isWhere, whereIf.expression);
+                }
+            }
+
+            return await query.ToListAsync();
+        }
+
+        public async Task<bool> AnyAsync(CancellationToken cancellationToken = default) =>
+            await Db.Queryable<TEntity>().AnyAsync();
+
+        public async Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) =>
+            await Db.Queryable<TEntity>().AnyAsync(predicate);
+
+        public async Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) =>
+            await Db.Queryable<TEntity>().CountAsync(predicate);
+
+        /// <summary>
+        /// 基础分页
+        /// </summary>
+        /// <param name="predicate"></param>
+        /// <param name="orderByCreator"></param>
+        /// <param name="pageIndex"></param>
+        /// <param name="pageSize"></param>
+        /// <returns></returns>
+        public async Task<(int Total, List<TEntity> Items)> QueryPagedAsync(
+            Expression<Func<TEntity, bool>> predicate,
+            Func<ISugarQueryable<TEntity>, ISugarQueryable<TEntity>> orderByCreator,
+            int pageIndex,
+            int pageSize,
+            params (bool isWhere, Expression<Func<TEntity, bool>> expression)[] whereIfs)
+        {
+            RefAsync<int> total = 0;
+            var query = Db.Queryable<TEntity>().Where(predicate);
+            if (whereIfs.Any())
+            {
+                foreach (var whereIf in whereIfs)
+                {
+                    query = query.WhereIF(whereIf.isWhere, whereIf.expression);
+                }
+            }
+            var items = await orderByCreator(query).ToPageListAsync(pageIndex, pageSize, total);
+            return (total.Value, items);
+        }
+
+        public async Task<List<TEntity>> QueryExtAsync(Expression<Func<TEntity, bool>> predicate, Func<ISugarQueryable<TEntity>, ISugarQueryable<TEntity>> includes)
+        {
+            var query = Db.Queryable<TEntity>().Where(predicate);
+            query = includes(query);
+            return await query.ToListAsync();
+        }
+
+        public async Task<TEntity> GetExtAsync(Expression<Func<TEntity, bool>> predicate, Func<ISugarQueryable<TEntity>, ISugarQueryable<TEntity>> includes)
+        {
+            var query = Db.Queryable<TEntity>();
+            query = includes(query);
+            return await query.FirstAsync(predicate);
+        }
+
+        public async Task<TEntity> GetExtAsync(string id, Func<ISugarQueryable<TEntity>, ISugarQueryable<TEntity>> includes)
+        {
+            var query = Db.Queryable<TEntity>();
+            query = includes(query);
+            return await query.FirstAsync(d => d.Id == id);
+        }
+
+        public async Task UpdateAsync(TEntity entity, bool ignoreNullColumns = true, CancellationToken cancellationToken = default)
+        {
+            await Db.Updateable(entity)
+                .IgnoreColumns(ignoreAllNullColumns: ignoreNullColumns)
+                .IgnoreColumns(d => d.CreationTime)
+                .ExecuteCommandAsync();
+        }
+    }
+}

+ 18 - 0
src/CallCenter.Repository.SqlSugar/BlacklistRepository.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.BlackLists;
+using SqlSugar;
+using XF.Domain.Dependency;
+
+namespace CallCenter.Repository.SqlSugar
+{
+    public class BlacklistRepository : BaseRepository<Blacklist>, IBlacklistRepository, IScopeDependency
+    {
+        public BlacklistRepository(ISugarUnitOfWork<CallCenterDbContext> uow) : base(uow)
+        {
+        }
+    }
+}

+ 13 - 0
src/CallCenter.Repository.SqlSugar/CallCenter.Repository.SqlSugar.csproj

@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\CallCenter\CallCenter.csproj" />
+  </ItemGroup>
+
+</Project>

+ 7 - 0
src/CallCenter.Repository.SqlSugar/CallCenterDbContext.cs

@@ -0,0 +1,7 @@
+using SqlSugar;
+
+namespace CallCenter.Repository.SqlSugar;
+
+public class CallCenterDbContext : SugarUnitOfWork
+{
+}

+ 19 - 0
src/CallCenter.Repository.SqlSugar/CallDetailRepository.cs

@@ -0,0 +1,19 @@
+using CallCenter.Tels;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.Calls;
+using SqlSugar;
+using XF.Domain.Dependency;
+
+namespace CallCenter.Repository.SqlSugar
+{
+    public class CallDetailRepository : BaseRepository<CallDetail>, ICallDetailRepository, IScopeDependency
+    {
+        public CallDetailRepository(ISugarUnitOfWork<CallCenterDbContext> uow) : base(uow)
+        {
+        }
+    }
+}

+ 19 - 0
src/CallCenter.Repository.SqlSugar/CallRecordRepository.cs

@@ -0,0 +1,19 @@
+using CallCenter.Users;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.Calls;
+using SqlSugar;
+using XF.Domain.Dependency;
+
+namespace CallCenter.Repository.SqlSugar
+{
+    public class CallRecordRepository: BaseRepository<CallRecord>, ICallRecordRepository, IScopeDependency
+    {
+        public CallRecordRepository(ISugarUnitOfWork<CallCenterDbContext> uow) : base(uow)
+        {
+        }
+    }
+}

+ 29 - 0
src/CallCenter.Repository.SqlSugar/CallRepository.cs

@@ -0,0 +1,29 @@
+using CallCenter.Calls;
+using CallCenter.Share.Enums;
+using SqlSugar;
+using XF.Domain.Dependency;
+
+namespace CallCenter.Repository.SqlSugar
+{
+    public class CallRepository : BaseRepository<Call>, ICallRepository, IScopeDependency
+    {
+        public CallRepository(ISugarUnitOfWork<CallCenterDbContext> uow) : base(uow)
+        {
+        }
+
+        public async Task<IReadOnlyList<Call>> QueryPartAsync(DateTime datetime)
+        {
+            var a = await Db.Queryable<Call>()
+                .Includes(d => d.CallDetails)
+                //.Where(d => d.CallDetails.Any(x => x.EventName == "BYE") 
+                .Where(d => !d.IsDeleted
+                            && d.CallStatus == ECallStatus.Bye
+                            && SqlFunc.Subqueryable<CallDetail>().Where(s => s.CallId == d.Id && s.EventName == "BYE" && s.CreationTime > datetime).Any())
+                .OrderBy(d => d.CreationTime)
+                .Take(10)
+                .ToListAsync();
+
+            return a;
+        }
+    }
+}

+ 43 - 0
src/CallCenter.Repository.SqlSugar/IvrCategoryRepository.cs

@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.Ivrs;
+using SqlSugar;
+using XF.Domain.Dependency;
+
+namespace CallCenter.Repository.SqlSugar
+{
+    public class IvrCategoryRepository : BaseRepository<IvrCategory>, IIvrCategoryRepository, IScopeDependency
+    {
+        public IvrCategoryRepository(ISugarUnitOfWork<CallCenterDbContext> uow) : base(uow)
+        {
+        }
+
+        /// <summary>
+        /// 查询分类(含IVR)
+        /// </summary>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task<IReadOnlyList<IvrCategory>> QueryCascadeAsync(CancellationToken cancellationToken = default)
+        {
+            return await Db.Queryable<IvrCategory>()
+                .Includes(d => d.Ivrs)
+                .ToListAsync();
+        }
+
+        /// <summary>
+        /// 查询某个分类(含IVR)
+        /// </summary>
+        /// <param name="cancellationToken"></param>
+        /// <returns></returns>
+        public async Task<IvrCategory> GetCascadeAsync(string id, CancellationToken cancellationToken = default)
+        {
+            return await Db.Queryable<IvrCategory>()
+                .Includes(d => d.Ivrs)
+                .Where(d => d.Ivrs.Any())
+                .FirstAsync(d => d.Id == id);
+        }
+    }
+}

+ 23 - 0
src/CallCenter.Repository.SqlSugar/IvrRepository.cs

@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.Ivrs;
+using SqlSugar;
+using XF.Domain.Dependency;
+
+namespace CallCenter.Repository.SqlSugar
+{
+    public class IvrRepository : BaseRepository<Ivr>, IIvrRepository, IScopeDependency
+    {
+        public IvrRepository(ISugarUnitOfWork<CallCenterDbContext> uow) : base(uow)
+        {
+        }
+
+        public async Task UpdateWithoutStrategiesAsync(Ivr ivr, CancellationToken cancellationToken = default)
+        {
+            await Db.Updateable(ivr).IgnoreColumns(d => d.IvrStrategies).ExecuteCommandAsync();
+        }
+    }
+}

+ 170 - 0
src/CallCenter.Repository.SqlSugar/SqlSugarStartupExtensions.cs

@@ -0,0 +1,170 @@
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Reflection;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Serilog;
+using SqlSugar;
+using XF.Domain;
+using XF.Domain.Entities;
+using XF.Domain.Extensions;
+
+namespace CallCenter.Repository.SqlSugar
+{
+    public static class SqlSugarStartupExtensions
+    {
+        public static void AddSqlSugar(this IServiceCollection services, IConfiguration configuration, string dbName = "CallCenter")
+        {
+            //多租户 new SqlSugarScope(List<ConnectionConfig>,db=>{});
+
+            SqlSugarScope sqlSugar = new SqlSugarScope(new ConnectionConfig()
+            {
+                DbType = DbType.MySql,
+                ConnectionString = configuration.GetConnectionString(dbName),
+                IsAutoCloseConnection = true,
+                ConfigureExternalServices = new ConfigureExternalServices
+                {
+                    EntityService = (property, column) =>
+                    {
+                        var attributes = property.GetCustomAttributes(true);//get all attributes 
+
+                        //if (attributes.Any(it => it is KeyAttribute))// by attribute set primarykey
+                        //{
+                        //    column.IsPrimarykey = true; //有哪些特性可以看 1.2 特性明细
+                        //}
+                        ////可以写多个,这边可以断点调试
+                        //// if (attributes.Any(it => it is NotMappedAttribute))
+                        ////{
+                        ////    column.IsIgnore= true; 
+                        ////}
+                        //if (attributes.Any(it => it is DbNullableAttribute))
+                        //{
+                        //    column.IsNullable = true;
+                        //}
+                        //if (attributes.Any(it => it is DbJsonAttribute))
+                        //{
+                        //    column.DataType = "varchar(3000)";
+                        //    column.IsJson = true;
+                        //}
+                        //if (attributes.Any(it => it is DbLengthAttribute))
+                        //{
+                        //    column.Length = (attributes.First(d => d is DbLengthAttribute) as DbLengthAttribute)?.MaxLength ?? 255;
+                        //}
+                        //if (attributes.Any(it => it is DbIgnoreAttribute))
+                        //{
+                        //    column.IsIgnore = true;
+                        //}
+                        if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
+                        {
+                            column.IsNullable = true;
+                        }
+                        if (column.PropertyName.ToLower() == "id" || attributes.Any(it => it is KeyAttribute)) //是id的设为主键
+                        {
+                            column.IsPrimarykey = true;
+                            column.Length = 36;
+                        }
+
+                        //column.ColumnDescription = (attributes.FirstOrDefault(d => d is DescriptionAttribute) as DescriptionAttribute)?.Description ?? string.Empty;
+                    },
+                    EntityNameService = (type, entity) =>
+                    {
+                        var attributes = type.GetCustomAttributes(true);
+                        //if (attributes.Any(it => it is TableAttribute))
+                        //{
+                        //    entity.DbTableName = (attributes.First(it => it is TableAttribute) as TableAttribute).Name;
+                        //}
+                        entity.DbTableName = entity.DbTableName.ToSnakeCase();
+                        if (attributes.Any(d => d is DescriptionAttribute))
+                        {
+                            entity.TableDescription =
+                                (attributes.First(d => d is DescriptionAttribute) as DescriptionAttribute).Description;
+                        }
+                    }
+                }
+            },
+                db =>
+                {
+                    /***写AOP等方法***/
+                    db.Aop.OnLogExecuting = (sql, pars) =>
+                    {
+                        //Log.Information(sql);
+                    };
+                    db.Aop.OnError = (exp) =>//SQL报错
+                    {
+                        //exp.sql 这样可以拿到错误SQL,性能无影响拿到ORM带参数使用的SQL
+                        Log.Error("SqlError: {0}", exp.Sql);
+
+                        //5.0.8.2 获取无参数化 SQL  对性能有影响,特别大的SQL参数多的,调试使用
+                        //UtilMethods.GetSqlString(DbType.SqlServer,exp.sql,exp.parameters)           
+                    };
+                    db.Aop.OnExecutingChangeSql = (sql, pars) => //可以修改SQL和参数的值
+                    {
+                        //sql=newsql
+                        //foreach(var p in pars) //修改
+
+                        return new KeyValuePair<string, SugarParameter[]>(sql, pars);
+                    };
+
+                    db.Aop.OnLogExecuted = (sql, p) =>
+                    {
+                        //执行时间超过1秒
+                        if (db.Ado.SqlExecutionTime.TotalSeconds > 1)
+                        {
+                            //代码CS文件名
+                            var fileName = db.Ado.SqlStackTrace.FirstFileName;
+                            //代码行数
+                            var fileLine = db.Ado.SqlStackTrace.FirstLine;
+                            //方法名
+                            var FirstMethodName = db.Ado.SqlStackTrace.FirstMethodName;
+                            //db.Ado.SqlStackTrace.MyStackTraceList[1].xxx 获取上层方法的信息
+
+                            Log.Warning("slow query ==> fileName: {fileName}, fileLine: {fileLine}, FirstMethodName: {FirstMethodName}",
+                                fileName, fileLine, FirstMethodName);
+                        }
+                        //相当于EF的 PrintToMiniProfiler
+                    };
+
+                    db.Aop.DataExecuting = (oldValue, entityInfo) =>
+                    {
+                        //inset生效
+                        if (entityInfo.PropertyName == "CreationTime" && entityInfo.OperationType == DataFilterType.InsertByObject)
+                        {
+                            entityInfo.SetValue(DateTime.Now);//修改CreateTime字段
+                            //entityInfo有字段所有参数
+                        }
+                        //update生效        
+                        else if (entityInfo.PropertyName == "LastModificationTime" && entityInfo.OperationType == DataFilterType.UpdateByObject)
+                        {
+                            entityInfo.SetValue(DateTime.Now);//修改UpdateTime字段
+                        }
+
+                        //根据当前列修改另一列 可以么写
+                        //if(当前列逻辑==XXX)
+                        //var properyDate = entityInfo.EntityValue.GetType().GetProperty("Date");
+                        //if(properyDate!=null)
+                        //properyDate.SetValue(entityInfo.EntityValue,1);
+
+                        else if (entityInfo.EntityColumnInfo.IsPrimarykey
+                                 && entityInfo.EntityColumnInfo.PropertyName.ToLower() == "id"
+                                 && entityInfo.OperationType == DataFilterType.InsertByObject) //通过主键保证只进一次事件
+                        {
+                            //这样每条记录就只执行一次 
+                            entityInfo.SetValue(SequentialStringGenerator.Create());
+                        }
+                    };
+                });
+            ISugarUnitOfWork<CallCenterDbContext> context = new SugarUnitOfWork<CallCenterDbContext>(sqlSugar);
+
+            ////全局过滤
+            //var deletionTypes = AppDomain.CurrentDomain.GetAssemblies().ToList()
+            //    .SelectMany(d=>d.GetTypes()).Where(d=>d.GetInterfaces()/*.Any(x=>x == typeof(ISoftDelete))*/);//todo  找出即实现ISoftDelete 又实现IEntity
+            //foreach (var deletionType in deletionTypes)
+            //{
+
+            //}
+
+            services.AddSingleton<ISugarUnitOfWork<CallCenterDbContext>>(context);
+        }
+    }
+}

+ 18 - 0
src/CallCenter.Repository.SqlSugar/SystemSettingGroupRepository.cs

@@ -0,0 +1,18 @@
+using CallCenter.Settings;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using SqlSugar;
+using XF.Domain.Dependency;
+
+namespace CallCenter.Repository.SqlSugar
+{
+    public class SystemSettingGroupRepository : BaseRepository<SystemSettingGroup>, ISystemSettingGroupRepository, IScopeDependency
+    {
+        public SystemSettingGroupRepository(ISugarUnitOfWork<CallCenterDbContext> uow) : base(uow)
+        {
+        }
+    }
+}

+ 19 - 0
src/CallCenter.Repository.SqlSugar/SystemSettingRepository.cs

@@ -0,0 +1,19 @@
+using CallCenter.Users;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using CallCenter.Settings;
+using SqlSugar;
+using XF.Domain.Dependency;
+
+namespace CallCenter.Repository.SqlSugar
+{
+    public class SystemSettingRepository : BaseRepository<SystemSetting>, ISystemSettingRepository, IScopeDependency
+    {
+        public SystemSettingRepository(ISugarUnitOfWork<CallCenterDbContext> uow) : base(uow)
+        {
+        }
+    }
+}

Some files were not shown because too many files changed in this diff