CrazyIter_Bin 11 달 전
부모
커밋
429df042e1
100개의 변경된 파일62048개의 추가작업 그리고 0개의 파일을 삭제
  1. 15 0
      AML.APP/AML.APP.csproj
  2. 149 0
      AML.APP/MLService.cs
  3. 118 0
      AML.APP/Program.cs
  4. 12 0
      HTEX.Complex/.config/dotnet-tools.json
  5. 32 0
      HTEX.Complex/Controllers/HomeController.cs
  6. 208 0
      HTEX.Complex/Controllers/IndexController.cs
  7. 224 0
      HTEX.Complex/Controllers/OfficialController.cs
  8. 24 0
      HTEX.Complex/Dockerfile
  9. 23 0
      HTEX.Complex/Dockerfile.original
  10. 35 0
      HTEX.Complex/HTEX.Complex.csproj
  11. 26 0
      HTEX.Complex/Helpers/CollectionHelper.cs
  12. 150 0
      HTEX.Complex/Helpers/DateTimeHelper.cs
  13. 76 0
      HTEX.Complex/Helpers/JsonExtensions.cs
  14. BIN
      HTEX.Complex/JsonFiles/ip2region.db
  15. 42708 0
      HTEX.Complex/JsonFiles/latlng.json
  16. 32 0
      HTEX.Complex/Models/Constant.cs
  17. 9 0
      HTEX.Complex/Models/ErrorViewModel.cs
  18. 28 0
      HTEX.Complex/Models/FileType.cs
  19. 135 0
      HTEX.Complex/Program.cs
  20. 38 0
      HTEX.Complex/Properties/launchSettings.json
  21. 250 0
      HTEX.Complex/Service/AzureCosmos3/AzureCosmos3Extensions.cs
  22. 110 0
      HTEX.Complex/Service/AzureCosmos3/AzureCosmos3Factory.cs
  23. 22 0
      HTEX.Complex/Service/AzureCosmos3/AzureCosmos3FactoryExtensions.cs
  24. 14 0
      HTEX.Complex/Service/AzureCosmos3/AzureCosmos3FactoryOptions.cs
  25. 11 0
      HTEX.Complex/Service/AzureCosmos3/CosmosEntity.cs
  26. 70 0
      HTEX.Complex/Service/AzureRedis/AzureRedisFactory.cs
  27. 216 0
      HTEX.Complex/Service/AzureServiceBus/AzureServiceBusExtensions.cs
  28. 46 0
      HTEX.Complex/Service/AzureServiceBus/AzureServiceBusFactory.cs
  29. 22 0
      HTEX.Complex/Service/AzureServiceBus/AzureServiceBusFactoryExtensions.cs
  30. 13 0
      HTEX.Complex/Service/AzureServiceBus/AzureServiceBusFactoryOptions.cs
  31. 162 0
      HTEX.Complex/Service/AzureStorage/AzureStorageBlobExtensions.cs
  32. 55 0
      HTEX.Complex/Service/AzureStorage/AzureStorageFactory.cs
  33. 13 0
      HTEX.Complex/Service/AzureStorage/AzureStorageTableExtensions.cs
  34. 15 0
      HTEX.Complex/Service/AzureStorage/TableEntity.cs
  35. 391 0
      HTEX.Complex/Service/ContentTypeDict.cs
  36. 81 0
      HTEX.Complex/Service/CoreHangfire/VisitSettleJob.cs
  37. 154 0
      HTEX.Complex/Service/DingDing.cs
  38. 437 0
      HTEX.Complex/Service/IP2Region/IPSearcher.cs
  39. 22 0
      HTEX.Complex/Service/IP2Region/IPSearcherExtensions.cs
  40. 46 0
      HTEX.Complex/Service/IP2Region/Models/DataBlock.cs
  41. 45 0
      HTEX.Complex/Service/IP2Region/Models/HeaderBlock.cs
  42. 45 0
      HTEX.Complex/Service/IP2Region/Models/IPConfig.cs
  43. 63 0
      HTEX.Complex/Service/IP2Region/Models/IndexBlock.cs
  44. 117 0
      HTEX.Complex/Service/IP2Region/Utils.cs
  45. 147 0
      HTEX.Complex/Service/MLService.cs
  46. 25 0
      HTEX.Complex/Service/Region2LongitudeLatitudeTranslator.cs
  47. 3257 0
      HTEX.Complex/Service/SystemService.cs
  48. 8 0
      HTEX.Complex/Views/Home/Index.cshtml
  49. 6 0
      HTEX.Complex/Views/Home/Privacy.cshtml
  50. 25 0
      HTEX.Complex/Views/Shared/Error.cshtml
  51. 49 0
      HTEX.Complex/Views/Shared/_Layout.cshtml
  52. 48 0
      HTEX.Complex/Views/Shared/_Layout.cshtml.css
  53. 2 0
      HTEX.Complex/Views/Shared/_ValidationScriptsPartial.cshtml
  54. 3 0
      HTEX.Complex/Views/_ViewImports.cshtml
  55. 3 0
      HTEX.Complex/Views/_ViewStart.cshtml
  56. 27 0
      HTEX.Complex/appsettings.Development.json
  57. 27 0
      HTEX.Complex/appsettings.json
  58. 28 0
      HTEXGpt/Controllers/ChatController.cs
  59. 32 0
      HTEXGpt/Controllers/HomeController.cs
  60. 217 0
      HTEXGpt/Controllers/SparkDeskChatController.cs
  61. 19 0
      HTEXGpt/HTEXGpt.csproj
  62. 80 0
      HTEXGpt/Models/ChatRequest.cs
  63. 11 0
      HTEXGpt/Models/ChatResponseVO.cs
  64. 9 0
      HTEXGpt/Models/ErrorViewModel.cs
  65. 16 0
      HTEXGpt/Models/ModelTypeEnum.cs
  66. 37 0
      HTEXGpt/Program.cs
  67. 38 0
      HTEXGpt/Properties/launchSettings.json
  68. 86 0
      HTEXGpt/Services/AiAppServiceImpl.cs
  69. 132 0
      HTEXGpt/Services/ChatGlmServiceImpl.cs
  70. 154 0
      HTEXGpt/Services/ErnieBotServiceImpl.cs
  71. 15 0
      HTEXGpt/Services/IAiAppService.cs
  72. 14 0
      HTEXGpt/Services/IModelService.cs
  73. 138 0
      HTEXGpt/Services/QianWenServiceImpl.cs
  74. 277 0
      HTEXGpt/Services/SparkDeskServiceImpl.cs
  75. 8 0
      HTEXGpt/Views/Home/Index.cshtml
  76. 6 0
      HTEXGpt/Views/Home/Privacy.cshtml
  77. 25 0
      HTEXGpt/Views/Shared/Error.cshtml
  78. 49 0
      HTEXGpt/Views/Shared/_Layout.cshtml
  79. 48 0
      HTEXGpt/Views/Shared/_Layout.cshtml.css
  80. 2 0
      HTEXGpt/Views/Shared/_ValidationScriptsPartial.cshtml
  81. 3 0
      HTEXGpt/Views/_ViewImports.cshtml
  82. 3 0
      HTEXGpt/Views/_ViewStart.cshtml
  83. 8 0
      HTEXGpt/appsettings.Development.json
  84. 9 0
      HTEXGpt/appsettings.json
  85. 22 0
      HTEXGpt/wwwroot/css/site.css
  86. BIN
      HTEXGpt/wwwroot/favicon.ico
  87. 4 0
      HTEXGpt/wwwroot/js/site.js
  88. 22 0
      HTEXGpt/wwwroot/lib/bootstrap/LICENSE
  89. 4997 0
      HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
  90. 1 0
      HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
  91. 7 0
      HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
  92. 1 0
      HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
  93. 4996 0
      HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
  94. 1 0
      HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
  95. 7 0
      HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
  96. 1 0
      HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
  97. 427 0
      HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
  98. 1 0
      HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
  99. 8 0
      HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
  100. 0 0
      HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map

+ 15 - 0
AML.APP/AML.APP.csproj

@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <InvariantGlobalization>true</InvariantGlobalization>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.ML" Version="3.0.1" />
+  </ItemGroup>
+
+</Project>

+ 149 - 0
AML.APP/MLService.cs

@@ -0,0 +1,149 @@
+
+ 
+using Microsoft.ML;
+using Microsoft.ML.Data;
+ 
+
+namespace HTEXScreen.Service
+{
+
+    public static class MLService
+    {
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="datas">数据需要去掉0</param>
+        /// <param name="numberOfClusters"></param>
+        /// <returns></returns>
+        public static List<ClusterData> KMeans(float[] datas, int numberOfClusters = 5)
+        {
+            List<DataPoint> data = new List<DataPoint>();
+            foreach (var d in datas)
+            {
+                data.Add(new DataPoint { Feature = d });
+            }
+
+            // 定义数据视图  
+            var mlContext = new MLContext();
+            var dataView = mlContext.Data.LoadFromEnumerable(data);
+            // 定义聚类管道  
+            var pipeline = mlContext.Transforms.Concatenate("Features", new[] { "Feature" })
+                                .Append(mlContext.Clustering.Trainers.KMeans(numberOfClusters: numberOfClusters)); // 假设我们想要将数据分成3个集群  
+                                                                                                                   // 训练模型  
+            var model = pipeline.Fit(dataView);
+
+            // 转换数据以获取聚类结果  
+            var predictions = model.Transform(dataView);
+
+            // 提取聚类结果  
+            var inMemoryCollection = mlContext.Data.CreateEnumerable<ClusterPrediction>(predictions, reuseRowObject: false);
+
+
+
+            // 打印聚类结果  
+            //var clusterSizes = new int[3]; // 假设有3个聚类  
+            int index = 0;
+            List<ClusterData> clusterDatas = new List<ClusterData>();
+            foreach (var prediction in inMemoryCollection)
+            {
+                //Console.WriteLine($"Data point: {data[index].Feature}, Cluster: {prediction.ClusterId}");
+                var clusterData = clusterDatas.Find(x => x.ClusterId.Equals(prediction.ClusterId));
+                if (clusterData!=null)
+                {
+                    clusterData.count +=1;
+                    clusterData.datas.Add(data[index].Feature);
+                    clusterData.avg=clusterData.datas.Sum() / clusterData.datas.Count();
+                }
+                else
+                {
+                    clusterDatas.Add(new ClusterData { avg=data[index].Feature, count=1, ClusterId=prediction.ClusterId, datas=new List<float> { data[index].Feature } });
+                }
+                index++;
+                //计算每个聚类的数据点数量
+                //clusterSizes[prediction.ClusterId-1]++;
+            }
+            // 预测聚类
+
+            // 确定最密集的部分  
+            // 这通常需要对聚类结果进行分析,比如计算每个聚类的平均距离、大小等  
+            // 在这里,你可以通过比较不同聚类的数据点数量或计算聚类中心周围的密度来估计哪个是最密集的  
+
+
+
+
+            // 找出最大的聚类  
+            //  var maxClusterIndex = clusterSizes.ToList().IndexOf(clusterSizes.Max());
+            //Console.WriteLine($"The densest cluster is cluster {maxClusterIndex} with {clusterSizes[maxClusterIndex]} data points.");
+
+            // 你还可以进一步分析聚类的特性,比如找出聚类中心、计算聚类内的方差等  
+
+            return clusterDatas;
+        }
+       /// <summary>
+       /// 
+       /// </summary>
+       /// <param name="datas"></param>
+       /// <param name="numberOfClusters"></param>
+       /// <param name="dropPercent">最大平均数的聚类与数量最多的聚类数量的落差小于30% 则以更高的为准</param>
+       /// <returns></returns>
+        public static ClusterData GetNormalCluster (float[] datas, int numberOfClusters = 5,double dropPercent=0.3)
+        {
+            List<ClusterData> clusterDatas = KMeans(datas, numberOfClusters);
+            clusterDatas=clusterDatas.OrderByDescending(dr => dr.count).ToList();
+            ClusterData clusterData = FindSatisfactoryRecord(clusterDatas, 0, dropPercent);
+            return clusterData;
+        }
+
+        static ClusterData FindSatisfactoryRecord(List<ClusterData> data, int currentIndex,double dropPercent)
+        {        // 如果当前索引小于0,说明已经到达列表开头,返回null
+            if (currentIndex < 0) { return null; }
+
+            // 获取当前数据
+            ClusterData current = data.ElementAt(currentIndex);
+            if (currentIndex+1>=data.Count())
+            {
+                return current;
+            }
+            else
+            {
+                ClusterData next = data.ElementAt(currentIndex +1);        // 检查平均值和人数差是否满足条件
+                if (current.avg > next.avg)
+                {
+                    return current;
+                }
+                else
+                {
+                    var d = (current.count- next.count)*1.0/current.count;
+                    if (d>=dropPercent)
+                    {
+                        return current;
+                    }
+                    else
+                    { // 递归调用,继续向前比较
+                        return FindSatisfactoryRecord(data, currentIndex + 1, dropPercent);
+                    }
+                }
+            }
+        }
+    }
+    // 定义数据模型  
+    public class DataPoint
+    {
+        public float Feature { get; set; }
+    }
+    // 聚类预测类  
+    public class ClusterPrediction
+    {
+        [ColumnName("PredictedLabel")]
+        public uint ClusterId;
+
+        // 你可以添加其他预测列,比如距离聚类中心的距离等  
+    }
+    public class ClusterData
+    {
+        public List<float> datas = new List<float>();
+        public uint ClusterId { get; set; }
+        public int count { get; set; }
+        public float avg { get; set; }
+    }
+}

+ 118 - 0
AML.APP/Program.cs

@@ -0,0 +1,118 @@
+using HTEXScreen.Service;
+using Microsoft.ML;
+using Microsoft.ML.Data;
+using Microsoft.ML.Trainers;
+using System;
+using static System.Runtime.InteropServices.JavaScript.JSType;
+namespace AML.APP
+{
+    internal class Program
+    {
+        static void Main(string[] args)
+        {
+            // 时区偏移字符串
+            // 时区偏移字符串
+            string timeZoneOffsetString = "-08:59";
+            bool plus = true;
+            if (timeZoneOffsetString.Contains("-"))
+            {
+                plus=false;
+            }
+            int timeZoneOffsetHours = 8;
+            // 去除时区偏移字符串中的正负号
+            timeZoneOffsetString = timeZoneOffsetString.Replace("+", "").Replace("-", "");
+            // 尝试解析格式化后的时区偏移字符串
+            if (TimeSpan.TryParse(timeZoneOffsetString, out TimeSpan timeZoneOffset))
+            {
+                // 将时区偏移转换为小时数
+                 timeZoneOffsetHours = plus ? (int)timeZoneOffset.TotalHours : -(int)timeZoneOffset.TotalHours;
+            }
+            List<DataPoint> data = new List<DataPoint>();
+            // 创建一个新的 ML.NET 环境
+            //Random random = new Random();
+            //
+            //for (int i = 0; i < 500; i++)
+            //{
+            //    data.Add(new DataPoint { Feature = random.Next(1, 1001) });
+            //}
+            float[] ds= new float[] { 2,1,5,8,7,6,30,35,5,23,78,28,28,30,31,29,21,25,99,101,98,900};
+
+            var datasc= MLService.GetNormalCluster(ds, 5, 0.3);
+            foreach (var d   in ds) {
+                data.Add(new DataPoint { Feature = d });
+            }
+            // 定义数据视图  
+            var mlContext = new MLContext();
+            var dataView = mlContext.Data.LoadFromEnumerable(data);
+            // 定义聚类管道  
+            var pipeline = mlContext.Transforms.Concatenate("Features", new[] { "Feature" })
+                                .Append(mlContext.Clustering.Trainers.KMeans(numberOfClusters:3)); // 假设我们想要将数据分成3个集群  
+                                                                                                    // 训练模型  
+            var model = pipeline.Fit(dataView);
+
+            // 转换数据以获取聚类结果  
+            var predictions = model.Transform(dataView);
+
+            // 提取聚类结果  
+            var inMemoryCollection = mlContext.Data.CreateEnumerable<ClusterPrediction>(predictions, reuseRowObject: false);
+
+
+
+            // 打印聚类结果  
+            //var clusterSizes = new int[3]; // 假设有3个聚类  
+            int index  =0;
+            List<ClusterData> clusterDatas = new List<ClusterData>();
+            foreach (var prediction in inMemoryCollection)
+            {
+                //Console.WriteLine($"Data point: {data[index].Feature}, Cluster: {prediction.ClusterId}");
+             
+                //clusterSizes[prediction.ClusterId-1]++;
+                var clusterData= clusterDatas.Find(x => x.ClusterId.Equals(prediction.ClusterId));
+                if (clusterData!=null)
+                {
+                    clusterData.count +=1;
+                    clusterData.datas.Add(data[index].Feature);
+                }
+
+                else {
+                    var datas = new List<float> { data[index].Feature };
+                    clusterDatas.Add(new ClusterData { count=1, ClusterId=prediction.ClusterId, datas=datas });
+                }
+                index++;
+            }
+            // 预测聚类
+
+            // 确定最密集的部分  
+            // 这通常需要对聚类结果进行分析,比如计算每个聚类的平均距离、大小等  
+            // 在这里,你可以通过比较不同聚类的数据点数量或计算聚类中心周围的密度来估计哪个是最密集的  
+
+            // 示例:计算每个聚类的数据点数量  
+
+            // 找出最大的聚类  
+            //var maxClusterIndex = clusterSizes.ToList().IndexOf(clusterSizes.Max());
+            //Console.WriteLine($"The densest cluster is cluster {maxClusterIndex} with {clusterSizes[maxClusterIndex]} data points.");
+            // 你还可以进一步分析聚类的特性,比如找出聚类中心、计算聚类内的方差等  
+            var cluster=  clusterDatas.OrderByDescending(x =>x.count).FirstOrDefault();
+        }
+    }
+   
+    // 定义数据模型  
+    public class DataPoint
+    {
+        public float Feature { get; set; }
+    }
+    // 聚类预测类  
+    public class ClusterPrediction
+    {
+        [ColumnName("PredictedLabel")]
+        public uint ClusterId;
+
+        // 你可以添加其他预测列,比如距离聚类中心的距离等  
+    }
+    public class ClusterData { 
+        public List<float> datas { get; set; } = new List<float>();
+        public uint ClusterId {  get; set; }
+        public int count { get; set; }
+        public float avg { get; set; }
+    }
+}

+ 12 - 0
HTEX.Complex/.config/dotnet-tools.json

@@ -0,0 +1,12 @@
+{
+  "version": 1,
+  "isRoot": true,
+  "tools": {
+    "dotnet-ef": {
+      "version": "8.0.4",
+      "commands": [
+        "dotnet-ef"
+      ]
+    }
+  }
+}

+ 32 - 0
HTEX.Complex/Controllers/HomeController.cs

@@ -0,0 +1,32 @@
+using HTEX.Complex.Models;
+using Microsoft.AspNetCore.Mvc;
+using System.Diagnostics;
+
+namespace HTEX.Complex.Controllers
+{
+    public class HomeController : Controller
+    {
+        private readonly ILogger<HomeController> _logger;
+
+        public HomeController(ILogger<HomeController> logger)
+        {
+            _logger = logger;
+        }
+
+        public IActionResult Index()
+        {
+            return View();
+        }
+
+        public IActionResult Privacy()
+        {
+            return View();
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+    }
+}

+ 208 - 0
HTEX.Complex/Controllers/IndexController.cs

@@ -0,0 +1,208 @@
+using Azure.Storage.Blobs.Models;
+using Azure.Storage.Blobs.Specialized;
+using HTEX.Complex.Service;
+using HTEX.Complex.Service.AzureRedis;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using PuppeteerSharp;
+using PuppeteerSharp.Media;
+using System.Collections.Concurrent;
+using System.Configuration;
+using System.Drawing;
+using System.IO;
+using System.Net;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Web;
+using TEAMModelOS.SDK;
+using TEAMModelOS.SDK.DI;
+using static HTEX.Complex.Service.SystemService;
+
+namespace HTEX.Complex.Controllers
+{
+    [ApiController]
+    [Route("api")]
+    public class IndexController : ControllerBase
+    {
+        private readonly DingDing _dingDing;
+        private readonly IHttpClientFactory _httpClient;
+        private readonly IConfiguration _configuration;
+        private readonly AzureStorageFactory _azureStorage;
+        private readonly AzureRedisFactory _azureRedis;
+        private readonly IPSearcher _ipSearcher;
+        private readonly Region2LongitudeLatitudeTranslator _longitudeLatitudeTranslator;
+        public IndexController(AzureRedisFactory azureRedis, Region2LongitudeLatitudeTranslator longitudeLatitudeTranslator, IHttpClientFactory httpClient, IConfiguration configuration, AzureStorageFactory azureStorage, IPSearcher searcher, DingDing dingDing)
+        {
+            _httpClient = httpClient;
+            _configuration = configuration;
+            _azureStorage = azureStorage;
+            _ipSearcher = searcher;
+            _dingDing = dingDing;
+            _azureRedis=azureRedis;
+            _longitudeLatitudeTranslator = longitudeLatitudeTranslator;
+        }
+        /// <summary>
+        ///  上传到blob
+        /// </summary>
+        /// <param name="request"></param>
+        /// <returns></returns>
+        [HttpPost("http-log")]
+
+        [RequestSizeLimit(102_400_000_00)] //最大10000m左右
+        public async Task<IActionResult> HttpLog(JsonElement json)
+        {
+            try
+            {
+                string data = json.ToJsonString();
+                var gmt8Time = DateTimeOffset.Now.GetGMTTime(8);
+                var appendBlob = _azureStorage.GetBlobContainerClient("0-service-log").GetAppendBlobClient($"http-log/{gmt8Time:yyyy}/{gmt8Time:MM}/{gmt8Time:dd}/{gmt8Time:HH}.log");
+                if (!await appendBlob.ExistsAsync())
+                {
+                    await appendBlob.CreateAsync();
+                }
+                using (var stream = new MemoryStream(Encoding.UTF8.GetBytes($"{data},\n")))
+                {
+                    await appendBlob.AppendBlockAsync(stream);
+                }
+                return Ok(new { code = 1 });
+            }
+            catch (Exception ex)
+            {
+                return Ok(new { code = 1 });
+            }
+        }
+
+        [HttpPost("report-api-settle")]
+      
+        [AllowAnonymous]
+        [RequestSizeLimit(102_400_000_00)] //最大10000m左右
+        public async Task<IActionResult> ReportApiSettle(JsonElement json)
+        {
+
+            List<string> times = json.GetProperty("times").ToObject<List<string>>();
+
+            return Ok(new { });
+        }
+        [HttpPost("report-api-reload")]
+      
+        [AllowAnonymous]
+        [RequestSizeLimit(102_400_000_00)] //最大10000m左右
+        public async Task<IActionResult> ReportApiTest(JsonElement json)
+        {
+            try
+            {
+                List<string> times = json.GetProperty("times").ToObject<List<string>>();
+                foreach (var timeDate in times)
+                {
+                    if (DateTimeOffset.TryParse(timeDate, out DateTimeOffset date))
+                    {
+                        var gmt8Time = date.GetGMTTime(8);
+                        var nowGmt8Time = DateTimeOffset.Now.GetGMTTime(8);
+                        List<string> logs = await _azureStorage.GetBlobContainerClient("0-service-log").List($"http-log/{date:yyyy}/{date:MM}/{date:dd}");
+                        List<HttpLog> httpLogs = new List<HttpLog>();
+                        foreach (var log in logs)
+                        {
+                            string nowPath = $"{nowGmt8Time:yyyy/MM/dd/HH}.log";
+                            if (!log.Contains("index.log")  && !log.Contains(".json") &&!log.Contains(nowPath))
+                            {
+                                BlobDownloadResult result = await _azureStorage.GetBlobContainerClient("0-service-log").GetBlobBaseClient(log).DownloadContentAsync();
+                                var content = result.Content.ToString();
+                                content= content.Substring(0, content.Length-2);
+                                if (content.EndsWith("}"))
+                                {
+                                    content=$"[{content}]";
+                                }
+                                else
+                                {
+                                    content=$"[{content}}}]";
+                                }
+                                httpLogs.AddRange(content.ToObject<List<HttpLog>>());
+                            }
+                        }
+                        (ConcurrentBag<ApiVisit> visits, ConcurrentBag<(string uuid, HttpLog httpLog, List<string> tmdid, List<string> school)> uuidInfo)   =
+                            await SystemService.ConvertHttpLog(httpLogs, _azureRedis, _ipSearcher, _longitudeLatitudeTranslator, gmt8Time, true);
+                        if (visits!=null  && visits.Count>0)
+                        {
+                            var appendDayBlob = _azureStorage.GetBlobContainerClient("0-service-log").GetAppendBlobClient($"http-log/{gmt8Time:yyyy}/{gmt8Time:MM}/{gmt8Time:dd}/index.log");
+                            if (!await appendDayBlob.ExistsAsync())
+                            {
+
+                                await appendDayBlob.CreateAsync();
+                            }
+                            else
+                            {
+                                await appendDayBlob.DeleteAsync();
+                                await appendDayBlob.CreateAsync();
+                            }
+                            int maxSize = 4*1024 * 1024; // 1M
+                            List<string> parts = new List<string>();
+                            StringBuilder sb = new StringBuilder();
+
+                            foreach (var item in visits)
+                            {
+                                string jsonString = $"{item.ToJsonString()},\n";
+                                int currentSize = Encoding.UTF8.GetByteCount(sb.ToString());
+
+                                if (currentSize + Encoding.UTF8.GetByteCount(jsonString) > maxSize)
+                                {
+                                    parts.Add(sb.ToString());
+                                    sb.Clear();
+                                }
+
+                                sb.Append(jsonString);
+                            }
+
+                            if (sb.Length > 0)
+                            {
+                                parts.Add(sb.ToString());
+                            }
+
+                            foreach (string part in parts)
+                            {
+                                using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(part)))
+                                {
+                                    await appendDayBlob.AppendBlockAsync(stream);
+                                }
+                            }
+
+
+                            //using (var stream = new MemoryStream(Encoding.UTF8.GetBytes($"{sb}")))
+                            //{
+                            //    await appendDayBlob.AppendBlockAsync(stream);
+                            //}
+                        }
+                    }
+                }
+
+                //全网:用户访问数,学校访问数,学生访问数,不同业务访问量,
+                return Ok(new { });
+            }
+            catch (Exception ex)
+            {
+                await _dingDing.SendBotMsg($"{ex.Message}\n{ex.StackTrace}", GroupNames.成都开发測試群組);
+            }
+            return Ok();
+        }
+
+        /// <summary>
+        /// 使用时长,一天内累计
+        /// </summary>
+        /// <param name="json"></param>
+        /// <returns></returns>
+        [HttpPost("report-api")]
+      
+        [AllowAnonymous]
+        [RequestSizeLimit(102_400_000_00)] //最大10000m左右
+        public async Task<IActionResult> ReportApi(JsonElement json)
+        {
+            var force = json.GetProperty("force").GetString();
+            List<string> times = json.GetProperty("times").ToObject<List<string>>();
+            await SystemService.VisitSettle(times, _azureStorage, _azureRedis, _longitudeLatitudeTranslator, _ipSearcher, $"{force}");
+            return Ok(new { code = 200 });
+        }
+
+    }
+}

+ 224 - 0
HTEX.Complex/Controllers/OfficialController.cs

@@ -0,0 +1,224 @@
+using Azure;
+using Azure.Data.Tables;
+using HTEX.Complex.Models;
+using HTEX.Complex.Service;
+using HTEX.Complex.Service.AzureRedis;
+using HTEX.Complex.Service.AzureStorage;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Azure.Cosmos;
+using System.Text.Json;
+using TEAMModelOS.SDK;
+using TEAMModelOS.SDK.DI.AzureCosmos3;
+using TableEntity = Azure.Data.Tables.TableEntity;
+
+namespace HTEX.Complex.Controllers
+{
+    [Route("official")]
+    [ApiController]
+    public class OfficialController : ControllerBase
+    {
+
+        private readonly DingDing _dingDing;
+       // private readonly SnowflakeId _snowflakeId;
+       // private readonly ServerSentEventsService _sse;
+        private readonly AzureCosmos3Factory _azureCosmos3Factory;
+        private readonly System.Net.Http.IHttpClientFactory _httpClientFactory;
+        //private readonly Models.Option _option;
+       // private readonly MailFactory _mailFactory;
+        private readonly AzureRedisFactory _azureRedis;
+        private readonly AzureStorageFactory _azureStorage;
+        private readonly IWebHostEnvironment _environment;
+        private readonly IConfiguration _configuration;
+       // private readonly CoreAPIHttpService _coreAPIHttpService;
+        private readonly IPSearcher _searcher;
+        public OfficialController(IWebHostEnvironment environment,   IConfiguration configuration,
+            AzureStorageFactory azureStorage, AzureRedisFactory azureRedis,  System.Net.Http.IHttpClientFactory httpClientFactory, AzureCosmos3Factory azureCosmos3Factory,
+           // IOptionsSnapshot<Option> option,  
+            DingDing dingDing,   IPSearcher searcher)
+        {
+            _dingDing = dingDing;
+            _azureCosmos3Factory = azureCosmos3Factory;
+            _httpClientFactory = httpClientFactory;
+            _azureRedis = azureRedis;
+            _azureStorage = azureStorage;
+            _environment = environment;
+            _configuration = configuration;
+            _searcher = searcher;
+        }
+        
+        [ProducesDefaultResponseType]
+        [RequestSizeLimit(102_400_000_00)] //最大10000m左右
+        [HttpPost("video-ls")]
+        public async Task<IActionResult> VideoUpload()
+        {
+            List<string> students = new List<string>();
+            //var result= await _azureCosmos3Factory.GetCosmosClient().GetContainer("winteachos", Constant.Teacher)
+            //    .GetList<Teacher>( "select value c from c", "Base");
+
+            await foreach (var item in _azureCosmos3Factory.GetCosmosClient().GetContainer(Constant.TEAMModelOS, Constant.Student)
+                .GetItemQueryIteratorList<string>(queryText: "select value c.id from c", requestOptions: new QueryRequestOptions {MaxItemCount=5, PartitionKey = new PartitionKey("Base-hbcn") }))
+            {
+                students.AddRange(item.list);
+            }
+            return Ok(students);
+        }
+     
+        [ProducesDefaultResponseType]
+        [HttpPost("video-list")]
+        public async Task<IActionResult> VideoList(JsonElement json)
+        {
+            List<OfficialVideo> videos = new List<OfficialVideo>();
+            var table = _azureStorage.TableServiceClient().GetTableClient("ShortUrl");
+            
+            if (json.TryGetProperty("rowKey", out JsonElement _rowKey) && !string.IsNullOrWhiteSpace($"{_rowKey}"))
+            {
+                videos =    table.Query<OfficialVideo>($"{Constant.PartitionKey} {Constant.Equal} 'OfficialVideo' and {Constant.RowKey} {Constant.Equal} '{_rowKey}'").ToList();
+               
+                
+            }
+            else
+            {
+                videos =    table.Query<OfficialVideo>(filter: $"{Constant.PartitionKey} eq  'OfficialVideo'").ToList();
+
+            }
+            return Ok(new { videos });
+        }
+      
+        [ProducesDefaultResponseType]
+        [RequestSizeLimit(102_400_000_00)] //最大10000m左右
+        [HttpPost("video-upload")]
+        public async Task<IActionResult> VideoUpload( [FromForm] string name, [FromForm] string type, [FromForm] string? rowKey=null, [FromForm] IFormFile? videoFile = null)
+        {
+            OfficialVideo video = null;
+            string url = string.Empty;
+            var table = _azureStorage.TableServiceClient().GetTableClient("ShortUrl");
+            if (videoFile == null    )
+            {
+                if (!string.IsNullOrWhiteSpace(rowKey))
+                {
+                    try
+                    {
+                        video=await table.GetEntityAsync<OfficialVideo>("OfficialVideo", rowKey);
+                    }
+                    catch { return BadRequest(); }
+                }
+                else {
+                    return BadRequest();
+                }
+            }
+            else {
+                if (!string.IsNullOrWhiteSpace(rowKey))
+                {
+                    try
+                    {
+                        video=await table.GetEntityAsync<OfficialVideo>("OfficialVideo", rowKey);
+                    }
+                    catch { }
+                }
+                if (video== null)
+                {
+                    video= new OfficialVideo { RowKey=$"{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}", PartitionKey="OfficialVideo" };
+                }
+                string fileExt = FileType.GetExtention(videoFile.FileName).ToLower();
+                if (fileExt.Equals("mp4", StringComparison.OrdinalIgnoreCase))
+                {
+                    var url_blob = await _azureStorage.GetBlobContainerClient("0-public").UploadFileByContainer(videoFile.OpenReadStream(), "official", $"{video.RowKey}.{fileExt}", false);
+                    url=url_blob.url;
+                }
+            }
+            video.name= name;
+            video.type= type;
+            video.url=string.IsNullOrWhiteSpace(url)?video.url:url;
+            table.UpsertEntity<OfficialVideo>(video);
+            return Ok(new { video });
+        }
+    }
+    //[TableName(Name = "ShortUrl")]
+    public   class OfficialVideo : HTableEntity
+    {
+        ///// <summary>
+        ///// OfficialVideo
+        ///// </summary>
+        //public string PartitionKey { get; set; }
+        ///// <summary>
+        ///// 视频id
+        ///// </summary>
+        //public string RowKey { get; set; }
+        /// <summary>
+        /// 名称
+        /// </summary>
+        public string? name { get; set; }
+        /// <summary>
+        /// 类型
+        /// </summary>
+        public string?type { get; set; }
+        /// <summary>
+        /// 地址
+        /// </summary>
+        public string? url { get; set; }
+    }
+    public class Student : CosmosEntity
+    {
+        public string mail { get; set; }
+        public string mobile { get; set; }
+        public string country { get; set; }
+        public string name { get; set; }
+        public string picture { get; set; }
+        public string schoolId { get; set; }
+        public string pw { get; set; }
+        public string salt { get; set; }
+        public int year { get; set; }
+        //座位号
+        public string no { get; set; }   //座位号
+        public string irs { get; set; }
+        //绑定班级Id
+        public string classId { get; set; }
+        //分组信息
+        public string groupId { get; set; }
+        public string groupName { get; set; }
+        public string periodId { get; set; }
+        /// <summary>
+        /// 性别 M( male,男) F (female 女)  N(secret 保密) 
+        /// </summary>
+        public string gender { get; set; }
+
+        //补充留级信息
+        //0在校,1毕业 
+        public int graduate { get; set; } = 0;
+       
+        /// <summary>
+        /// 创建时间  十位 时间戳
+        /// </summary>
+        public long createTime { get; set; }
+     
+        /// <summary>
+        /// 学生的专业id
+        /// </summary>
+        public string majorId { get; set; }
+        /// <summary>
+        /// 學生的OpenID (TW教育雲綁定ID)
+        /// </summary>
+        public string openId { get; set; }
+    }
+
+    /// <summary>
+    /// 教师
+    /// </summary>
+    public class Teacher : CosmosEntity
+    {
+        public Teacher()
+        {
+            pk = "Teacher";
+        }
+         
+        /// <summary>
+        /// 系统权限信息
+        /// </summary>
+        public HashSet<string> permissions { get; set; } = new HashSet<string>();
+        /// <summary>
+        /// 组织信息
+        /// </summary>
+     
+        //常用设备信息,及IP登录信息。
+    }
+}

+ 24 - 0
HTEX.Complex/Dockerfile

@@ -0,0 +1,24 @@
+#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
+WORKDIR /app
+EXPOSE 80
+EXPOSE 443
+
+#FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
+#ARG BUILD_CONFIGURATION=Release
+#WORKDIR /src
+#COPY ["HTEX.Complex/HTEX.Complex.csproj", "HTEX.Complex/"]
+#RUN dotnet restore "./HTEX.Complex/HTEX.Complex.csproj"
+COPY . .
+#WORKDIR "/src/HTEX.Complex"
+#RUN dotnet build "./HTEX.Complex.csproj" -c $BUILD_CONFIGURATION -o /app/build
+#
+#FROM build AS publish
+#ARG BUILD_CONFIGURATION=Release
+#RUN dotnet publish "./HTEX.Complex.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+#
+#FROM base AS final
+#WORKDIR /app
+#COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "HTEX.Complex.dll"]

+ 23 - 0
HTEX.Complex/Dockerfile.original

@@ -0,0 +1,23 @@
+#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
+FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
+WORKDIR /app
+EXPOSE 80
+EXPOSE 443
+
+
+#FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
+#WORKDIR /src
+#
+#COPY ["HTEX.Complex/HTEX.Complex.csproj", "HTEX.Complex/"]
+#RUN dotnet restore "HTEX.Complex/HTEX.Complex.csproj"
+COPY . .
+#WORKDIR "/src/HTEX.Complex"
+#RUN dotnet build "HTEX.Complex.csproj" -c Release -o /app/build
+#
+#FROM build AS publish
+#RUN dotnet publish "HTEX.Complex.csproj" -c Release -o /app/publish
+#
+#FROM base AS final
+#WORKDIR /app
+#COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "HTEX.Complex.dll"]

+ 35 - 0
HTEX.Complex/HTEX.Complex.csproj

@@ -0,0 +1,35 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <Nullable>enable</Nullable>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <UserSecretsId>ffe6ccf7-5ca6-4450-9b74-98d4fc8e66b7</UserSecretsId>
+    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Azure.Data.Tables" Version="12.8.3" />
+    <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.39.0" />
+    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
+	<PackageReference Include="Azure.Storage.Blobs.Batch" Version="12.10.0" />
+	<PackageReference Include="Azure.Storage.Queues" Version="12.11.1" />
+	<PackageReference Include="Hangfire" Version="1.8.11" />
+	<PackageReference Include="Hangfire.Dashboard.BasicAuthorization" Version="1.0.2" />
+	<PackageReference Include="Hangfire.Redis.StackExchange" Version="1.9.3" />
+	<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.23" />
+	<PackageReference Include="Microsoft.ML" Version="3.0.1" />
+	<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.16.1" />
+	<PackageReference Include="PuppeteerSharp" Version="7.1.0" />
+	<PackageReference Include="StackExchange.Redis" Version="2.6.111" />
+	<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
+	<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.11.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Update="Dockerfile">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
+</Project>

+ 26 - 0
HTEX.Complex/Helpers/CollectionHelper.cs

@@ -0,0 +1,26 @@
+using System.Collections;
+namespace HTEX.Complex
+{
+    public static class CollectionHelper
+    {
+        public static bool IsEmpty(this ICollection collection)
+        {
+            if (collection != null && collection.Count > 0)
+            {
+                return false;
+            }
+
+            return true;
+        }
+
+        public static bool IsNotEmpty(this ICollection collection)
+        {
+            if (collection != null && collection.Count > 0)
+            {
+                return true;
+            }
+
+            return false;
+        }
+    }
+}

+ 150 - 0
HTEX.Complex/Helpers/DateTimeHelper.cs

@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace TEAMModelOS.SDK
+{
+    public static class DateTimeHelper
+    {
+
+        public static DateTime FromUnixTimestamp(this long unixtime)
+        {
+            DateTime sTime = TimeZoneInfo.ConvertTime(new DateTime(1970, 1, 1), TimeZoneInfo.Utc, TimeZoneInfo.Local);
+            return sTime.AddMilliseconds(unixtime);
+        }
+
+         
+        public static long ToUnixTimestamp(this DateTime datetime)
+        {
+            //DateTime sTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+            DateTime sTime = TimeZoneInfo.ConvertTime(new DateTime(1970, 1, 1), TimeZoneInfo.Utc, TimeZoneInfo.Local);
+            return (long)(datetime - sTime).TotalMilliseconds;
+        }
+       
+        /// <summary>
+        /// 获得 GMT+8 时间
+        /// </summary>
+        /// <returns></returns>
+        public static DateTime GetGMTTime(this DateTime dateTime, int GMT = 8)
+        {
+            //SystemTimeZoneById  :  https://learn.microsoft.com/zh-cn/previous-versions/windows/embedded/ms912391(v=winembedded.11)
+            //TimeZoneInfo easternZone = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time");//设置时区
+            //DateTime easternTime = TimeZoneInfo.ConvertTimeFromUtc(dateTime, easternZone);
+            //return easternTime;
+
+
+            //处理UTC时差
+            TimeZoneInfo localTimezone = TimeZoneInfo.Local;
+            var Hours = localTimezone.BaseUtcOffset.Hours;
+            dateTime = dateTime.AddHours(GMT-Hours);
+            return dateTime;
+        }
+        /// <summary>
+        /// 默认东八区+8
+        /// </summary>
+        /// <param name="dateTime"></param>
+        /// <param name="GMT"></param>
+        /// <returns></returns>
+        public static DateTimeOffset GetGMTTime(this DateTimeOffset dateTime,int GMT=+8)
+        {
+            //处理UTC时差
+            TimeZoneInfo localTimezone = TimeZoneInfo.Local;
+            var Hours = localTimezone.BaseUtcOffset.Hours;
+            dateTime = dateTime.AddHours(GMT-Hours);
+            return dateTime;
+        }
+
+        public static  int getDays(int year) { 
+            int day = 0;
+            for (int i = 0;i<=12;i++) {
+                int days = 0;
+                if (i != 2)
+                {
+                    switch (i)
+                    {
+                        case 1:
+                        case 3:
+                        case 5:
+                        case 7:
+                        case 8:
+                        case 10:
+                        case 12:
+                            days = 31;
+                            break;
+                        case 4:
+                        case 6:
+                        case 9:
+                        case 11:
+                            days = 30;
+                            break;
+                    }
+                }
+                else {
+                    if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)
+                    {
+                        days = 29;
+                    }
+                    else { 
+                        days = 28;
+                    }
+                }
+                day += days; 
+            }
+            return day;
+        }
+
+        #region 获取 本周、本月、本季度、本年 的开始时间或结束时间
+        /// <summary>
+        /// 获取开始时间
+        /// </summary>
+        /// <param name="TimeType">Week、Month、Season、Year</param>
+        /// <param name="now"></param>
+        /// <returns></returns>
+        public static DateTime? GetTimeStartByType(string TimeType, DateTime now)
+        {
+            
+            switch (TimeType)
+            {
+                case "Week":
+                    return now.AddDays(-(int)now.DayOfWeek + 1);
+                case "Month":
+                    return now.AddDays(-now.Day + 1);
+                case "Season":
+                    var time = now.AddMonths(0 - ((now.Month - 1) % 3));
+                    return time.AddDays(-time.Day + 1);
+                case "Year":
+                    return now.AddDays(-now.DayOfYear + 1);
+                default:
+                    return null;
+            }
+        }
+
+        /// <summary>
+        /// 获取结束时间
+        /// </summary>
+        /// <param name="TimeType">Week、Month、Season、Year</param>
+        /// <param name="now"></param>
+        /// <returns></returns>
+        public static DateTime? GetTimeEndByType(string TimeType, DateTime now)
+        {
+            switch (TimeType)
+            {
+                case "Week":
+                    return now.AddDays(7 - (int)now.DayOfWeek);
+                case "Month":
+                    return now.AddMonths(1).AddDays(-now.AddMonths(1).Day + 1).AddDays(-1);
+                case "Season":
+                    var time = now.AddMonths((3 - ((now.Month - 1) % 3) - 1));
+                    return time.AddMonths(1).AddDays(-time.AddMonths(1).Day + 1).AddDays(-1);
+                case "Year":
+                    var time2 = now.AddYears(1);
+                    return time2.AddDays(-time2.DayOfYear);
+                default:
+                    return null;
+            }
+        }
+        #endregion
+
+
+    }
+}

+ 76 - 0
HTEX.Complex/Helpers/JsonExtensions.cs

@@ -0,0 +1,76 @@
+using System.Buffers;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text;
+
+namespace HTEX.Complex
+{
+    public static class JsonExtensions
+    {
+        public static string ToJsonString(this JsonDocument jdoc)
+        {
+            var bufferWriter = new ArrayBufferWriter<byte>();
+            using var writer = new Utf8JsonWriter(bufferWriter, new JsonWriterOptions { Indented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
+            jdoc.WriteTo(writer);
+            writer.Flush();
+            return Encoding.UTF8.GetString(bufferWriter.WrittenSpan);
+        }
+
+        public static string ToJsonString(this JsonElement jelement)
+        {
+            var bufferWriter = new ArrayBufferWriter<byte>();
+            using var writer = new Utf8JsonWriter(bufferWriter, new JsonWriterOptions { Indented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
+            jelement.WriteTo(writer);
+            writer.Flush();
+            return Encoding.UTF8.GetString(bufferWriter.WrittenSpan);
+        }
+
+        public static string ToJsonString(this Object obj, JsonSerializerOptions option = null)
+        {
+            if (option == null)
+            {
+                option = new System.Text.Json.JsonSerializerOptions
+                {
+                    Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+                };
+            }
+            var json = JsonSerializer.Serialize(obj, option);
+            return json;
+        }
+
+        public static T ToObject<T>(this string json, JsonSerializerOptions option = null)
+        {
+            var obj = JsonSerializer.Deserialize<T>(json);
+            return obj;
+        }
+
+
+        public static T ToObject<T>(this JsonDocument jdoc, JsonSerializerOptions options = null)
+        {
+            if (options == null)
+            {
+                options = new System.Text.Json.JsonSerializerOptions
+                {
+                    Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+                };
+            }
+            return jdoc.RootElement.ToObject<T>(options);
+        }
+
+        public static T ToObject<T>(this JsonElement jelement, JsonSerializerOptions options = null)
+        {
+            if (options == null)
+            {
+                options = new System.Text.Json.JsonSerializerOptions
+                {
+                    Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+                };
+            }
+            var bufferWriter = new ArrayBufferWriter<byte>();
+            using var writer = new Utf8JsonWriter(bufferWriter);
+            jelement.WriteTo(writer);
+            writer.Flush();
+            return JsonSerializer.Deserialize<T>(bufferWriter.WrittenSpan, options);
+        }
+    }
+}

BIN
HTEX.Complex/JsonFiles/ip2region.db


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 42708 - 0
HTEX.Complex/JsonFiles/latlng.json


+ 32 - 0
HTEX.Complex/Models/Constant.cs

@@ -0,0 +1,32 @@
+namespace HTEX.Complex
+{
+    public class Constant
+    {
+        public static readonly List<string> BlobPrefix = new List<string> { "exam", "vote", "survey", "item", "paper", "syllabus", "records", "doc", "image", "res", "video", "audio", "other", "thum", "train", "temp", "jyzx" };
+        public static readonly List<string> ContentPrefix = new List<string> { "doc", "image", "res", "video", "audio", "other" };
+        public static readonly string TEAMModelOS = "TEAMModelOS";
+        public static readonly string ScopeTeacher = "teacher";
+        public static readonly string ScopeTmdUser = "tmduser";
+        public static readonly string ScopeStudent = "student";
+        public static readonly string ScopeBusiness = "business";
+        public static readonly string RowKey = "RowKey";
+        public static readonly string PartitionKey = "PartitionKey";
+        public static readonly string School = "School";
+        public static readonly string Normal = "Normal";
+        public static readonly string Common = "Common";
+        public static readonly string Teacher = "Teacher";
+        public static readonly string Student = "Student";
+
+        public const string Equal = "eq";
+
+        public const string NotEqual = "ne";
+
+        public const string GreaterThan = "gt";
+
+        public const string GreaterThanOrEqual = "ge";
+
+        public const string LessThan = "lt";
+
+        public const string LessThanOrEqual = "le";
+    }
+}

+ 9 - 0
HTEX.Complex/Models/ErrorViewModel.cs

@@ -0,0 +1,9 @@
+namespace HTEX.Complex.Models
+{
+    public class ErrorViewModel
+    {
+        public string? RequestId { get; set; }
+
+        public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
+    }
+}

+ 28 - 0
HTEX.Complex/Models/FileType.cs

@@ -0,0 +1,28 @@
+namespace HTEX.Complex.Models
+{
+    public class FileType
+    {
+        public string Id { get; set; }
+        public string Extention { get; set; }
+        public string Type { get; set; }
+
+        public FileType(string id, string extention, string type)
+        {
+            Id = id;
+            Extention = extention;
+            Type = type;
+        }
+        public static string GetExtention(string fileName)
+        {
+            if (string.IsNullOrEmpty(fileName))
+            {
+                return "";
+            }
+            else
+            {
+                return fileName.Substring(fileName.LastIndexOf(".") + 1);
+            }
+
+        }
+    }
+}

+ 135 - 0
HTEX.Complex/Program.cs

@@ -0,0 +1,135 @@
+using Hangfire;
+using Hangfire.Redis.StackExchange;
+using HTEX.Complex.Service;
+using HTEX.Complex.Service.AzureRedis;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.IdentityModel.Tokens;
+using System.IdentityModel.Tokens.Jwt;
+using TEAMModelOS.SDK;
+using TEAMModelOS.SDK.DI;
+using TEAMModelOS.SDK.DI.AzureCosmos3;
+
+namespace HTEX.Complex
+{
+    public class Program
+    {
+        public static void Main(string[] args)
+        {
+            var builder = WebApplication.CreateBuilder(args);
+
+
+            // Add services to the container.
+
+            JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
+            builder.Services.AddAuthentication(options => options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme)
+                .AddJwtBearer(options => //AzureADJwtBearer
+                {
+                    //options.SaveToken = true; //驗證令牌由服務器生成才有效,不適用於服務重啟或分布式架構
+                    options.Authority ="https://login.chinacloudapi.cn/4807e9cf-87b8-4174-aa5b-e76497d7392b/v2.0";// builder.Configuration["Option:Authority"];
+                    options.Audience = "72643704-b2e7-4b26-b881-bd5865e7a7a5";//builder.Configuration["Option:Audience"];
+                    options.RequireHttpsMetadata = true;
+                    options.TokenValidationParameters = new TokenValidationParameters
+                    {
+                        RoleClaimType = "roles",
+                        //ValidAudiences = new string[] { builder.Configuration["Option:Audience"], $"api://{builder.Configuration["Option:Audience"]}" }
+                        ValidAudiences = new string[] { "72643704-b2e7-4b26-b881-bd5865e7a7a5", $"api://72643704-b2e7-4b26-b881-bd5865e7a7a5" }
+                    };
+                    options.Events = new JwtBearerEvents();
+                    //下列事件有需要紀錄則打開
+                    //options.Events.OnMessageReceived = async context => { await Task.FromResult(0); };
+                    //options.Events.OnForbidden = async context => { await Task.FromResult(0); };
+                    //options.Events.OnChallenge = async context => { await Task.FromResult(0); };
+                    //options.Events.OnAuthenticationFailed = async context => { await Task.FromResult(0); };
+                    options.Events.OnTokenValidated = async context =>
+                    {
+                        if (!context.Principal.Claims.Any(x => x.Type.Equals("http://schemas.microsoft.com/identity/claims/scope")) //ClaimConstants.Scope
+                        && !context.Principal.Claims.Any(y => y.Type.Equals("roles"))) //ClaimConstants.Roles //http://schemas.microsoft.com/ws/2008/06/identity/claims/role
+                        {
+                            //TODO 需處理額外授權非角色及範圍的訪問異常紀錄
+                            throw new UnauthorizedAccessException("Neither scope or roles claim was found in the bearer token.");
+                        }
+                        await Task.FromResult(0);
+                    };
+                });
+            builder.Services.AddControllers();
+#if DEBUG
+            builder.WebHost.UseUrls(new[] { "https://*:7298" });
+#endif
+            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+            builder.Services.AddEndpointsApiExplorer();
+            //builder.Services.AddSwaggerGen();
+            builder.Services.AddHttpClient();
+            string StorageConnectionString = builder.Configuration.GetValue<string>("Azure:Storage:ConnectionString");
+            string StorageConnectionStringTest = builder.Configuration.GetValue<string>("Azure:Storage:ConnectionString-Test");
+
+            //string ServiceBusConnectionString = builder.Configuration.GetValue<string>("Azure:ServiceBus:ConnectionString");
+            //string ServiceBusConnectionStringTest = builder.Configuration.GetValue<string>("Azure:ServiceBus:ConnectionString-Test");
+
+            string RedisConnectionString = builder.Configuration.GetValue<string>("Azure:Redis:ConnectionString");
+            string RedisConnectionStringTest = builder.Configuration.GetValue<string>("Azure:Redis:ConnectionString-Test");
+
+            string CosmosConnectionString = builder.Configuration.GetValue<string>("Azure:Cosmos:ConnectionString");
+            string CosmosConnectionStringTest = builder.Configuration.GetValue<string>("Azure:Cosmos:ConnectionString-Test");
+
+            //Storage
+            builder.Services.AddAzureStorage(StorageConnectionString, "Default");
+            builder.Services.AddAzureStorage(StorageConnectionStringTest, "Test");
+            //ServiceBus
+            //builder.Services.AddAzureServiceBus(ServiceBusConnectionString, "Default");
+            //builder.Services.AddAzureServiceBus(ServiceBusConnectionStringTest, "Test");
+            //Redis
+            builder.Services.AddAzureRedis(RedisConnectionString, "Default");
+            builder.Services.AddAzureRedis(RedisConnectionStringTest, "Test");
+            //Cosmos
+            builder.Services.AddAzureCosmos3(CosmosConnectionString, "Default");
+            builder.Services.AddAzureCosmos3(CosmosConnectionStringTest, "Test");
+
+            builder.Services.AddHttpContextAccessor();
+            builder.Services.AddHttpClient<DingDing>();
+            string path = $"{builder.Environment.ContentRootPath}/JsonFiles";
+            builder.Services.TryAddSingleton(new Region2LongitudeLatitudeTranslator(path));
+            builder.Services.AddIPSearcher(path);
+            builder.Services.AddCors(options =>
+            {
+                options.AddDefaultPolicy(
+                builder =>
+                {
+
+                builder.AllowAnyOrigin() 
+                        .AllowAnyHeader()
+                        .AllowAnyMethod();
+                });
+            });
+            builder.Services.AddHangfire(config => {
+                config.UseRedisStorage(builder.Configuration.GetValue<string>("Azure:Redis:ConnectionString"));
+            });
+            builder.Services.AddHangfireServer();
+
+            builder.Services.AddControllersWithViews();
+
+            var app = builder.Build();
+
+            // Configure the HTTP request pipeline.
+            if (!app.Environment.IsDevelopment())
+            {
+                app.UseExceptionHandler("/Home/Error");
+                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
+                app.UseHsts();
+            }
+
+            app.UseHttpsRedirection();
+            app.UseStaticFiles();
+
+            app.UseRouting();
+            app.UseCors(); //使用跨域設定
+            app.UseHttpsRedirection(); //開發中暫時關掉
+            app.UseAuthentication();
+            app.UseAuthorization();
+            app.MapControllerRoute(
+                name: "default",
+                pattern: "{controller=Home}/{action=Index}/{id?}");
+            app.Run();
+        }
+    }
+}

+ 38 - 0
HTEX.Complex/Properties/launchSettings.json

@@ -0,0 +1,38 @@
+{
+  "$schema": "https://json.schemastore.org/launchsettings.json",
+  "iisSettings": {
+    "windowsAuthentication": false,
+    "anonymousAuthentication": true,
+    "iisExpress": {
+      "applicationUrl": "http://localhost:65213",
+      "sslPort": 44398
+    }
+  },
+  "profiles": {
+    "HTEX.Complex": {
+      "commandName": "Project",
+      "launchBrowser": true,
+      "launchUrl": "swagger",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      },
+      "applicationUrl": "https://localhost:7298;http://localhost:5298",
+      "dotnetRunMessages": true
+    },
+    "IIS Express": {
+      "commandName": "IISExpress",
+      "launchBrowser": true,
+      "launchUrl": "swagger",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    },
+    "Docker": {
+      "commandName": "Docker",
+      "launchBrowser": true,
+      "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
+      "publishAllPorts": true,
+      "useSSL": true
+    }
+  }
+}

+ 250 - 0
HTEX.Complex/Service/AzureCosmos3/AzureCosmos3Extensions.cs

@@ -0,0 +1,250 @@
+using Azure;
+using HTEX.Complex;
+using Microsoft.Azure.Cosmos;
+using Microsoft.IdentityModel.Tokens;
+using System;
+using System.Collections.Generic;
+using System.Drawing.Printing;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace TEAMModelOS.SDK.DI.AzureCosmos3
+{
+    public static class AzureCosmos3Extensions
+    {
+        public static double RU(this Response response)
+        {
+            try
+            {
+                response.Headers.TryGetValue("x-ms-request-charge", out var value);
+                var ru = Convert.ToDouble(value);
+                return ru;
+            }
+            catch
+            {
+                return 0;
+            }
+        }
+
+        public static string? GetContinuationToken(this Response response)
+        {
+            try
+            {
+                response.Headers.TryGetValue("x-ms-continuation", out var value);
+                return value;
+            }
+            catch
+            {
+                return null;
+            }
+        }
+        public static async Task<T?> GetOne<T>(this Container container, QueryDefinition queryDefinition, string? partitionkey = null)
+        {
+            List<T?> list = new List<T?>();
+            //if (string.IsNullOrWhiteSpace(partitionkey))
+            //{
+            //    return default;
+            //}
+            FeedIterator<T> iterator = container.GetItemQueryIterator<T>(queryDefinition: queryDefinition,
+                requestOptions: new QueryRequestOptions { MaxItemCount = 2, PartitionKey =!string.IsNullOrWhiteSpace(partitionkey) ? new PartitionKey(partitionkey) : null });
+            while (iterator.HasMoreResults)
+            {
+                FeedResponse<T> currentResultSet = await iterator.ReadNextAsync();
+                list.AddRange(currentResultSet);
+            }
+            if (list.Count > 1)
+            {
+                throw new Exception("当前查询条件返回的结果不唯一");
+            }
+            else if (list.Count <= 0)
+            {
+                return default;
+            }
+            else
+            {
+                return list.First();
+            }
+        }
+
+        public static async Task<T?> GetOne<T>(this Container container, string sql, string? partitionkey = null)
+        {
+            List<T?> list = new List<T?>();
+            //if (string.IsNullOrWhiteSpace(partitionkey))
+            //{
+            //    return default;
+            //}
+            FeedIterator<T> iterator = container.GetItemQueryIterator<T>(queryText: sql,
+                 requestOptions: new QueryRequestOptions { MaxItemCount = 2, PartitionKey =!string.IsNullOrWhiteSpace(partitionkey) ? new PartitionKey(partitionkey) : null });
+            while (iterator.HasMoreResults)
+            {
+                FeedResponse<T> currentResultSet = await iterator.ReadNextAsync();
+                list.AddRange(currentResultSet);
+            }
+            if (list.Count > 1)
+            {
+                throw new Exception("当前查询条件返回的结果不唯一");
+            }
+            else if (list.Count <=0)
+            {
+                return default;
+            }
+            else
+            {
+                return list.First();
+            }
+        }
+        public static async IAsyncEnumerable<CosmosDBResult<T>> GetItemQueryIteratorList<T>(this Container container, string queryText, QueryRequestOptions? requestOptions= null,  string? continuationToken = null)
+        {
+           
+            //if (string.IsNullOrWhiteSpace(partitionkey))
+            //{
+            //    return (new CosmosDBResult<T> { list = list, ru=RU, continuationToken=continuationToken });
+            //}
+            FeedIterator<T> iterator = container.GetItemQueryIterator<T>(queryText: queryText, continuationToken: continuationToken, requestOptions: requestOptions);
+            while (iterator.HasMoreResults)
+            {
+                List<T> list = new List<T>();
+                double RU = 0;
+                FeedResponse<T> currentResultSet = await iterator.ReadNextAsync();
+                list.AddRange(currentResultSet);
+                RU += currentResultSet.RequestCharge;
+                //此处需要优化 ,检查相关的 关键字 用正则
+                if (queryText.Contains(" distinct ", StringComparison.OrdinalIgnoreCase)
+                    || (queryText.Contains("order ", StringComparison.OrdinalIgnoreCase)
+                    && !queryText.Contains(".order ", StringComparison.OrdinalIgnoreCase)))
+                {
+                    continuationToken = null;
+                }
+                else
+                {
+                    continuationToken = currentResultSet.ContinuationToken;
+                }
+                if (requestOptions!=null && requestOptions.MaxItemCount.HasValue && requestOptions.MaxItemCount.Value >=0 && list.Count>=requestOptions.MaxItemCount)
+                {
+                    //记录日志,RU开销大于400(开发测试),1000(正式)
+                    yield return (new CosmosDBResult<T> { list = list, ru = RU, continuationToken = continuationToken });
+                    break;
+                }
+                else
+                {
+                    //记录日志,RU开销大于400(开发测试),1000(正式)
+                    yield return (new CosmosDBResult<T> { list = list, ru = RU, continuationToken = continuationToken });
+                }
+                
+            }
+           
+        }
+        public static async Task<CosmosDBResult<T>> GetList<T>(this Container container, string sql, string? partitionkey = null, string? continuationToken = null, int? pageSize = null)
+        {
+            List<T> list = new List<T>();
+            double RU = 0;
+            //if (string.IsNullOrWhiteSpace(partitionkey))
+            //{
+            //    return (new CosmosDBResult<T> { list = list, ru=RU, continuationToken=continuationToken });
+            //}
+            FeedIterator<T> iterator = container.GetItemQueryIterator<T>(queryText: sql, continuationToken: continuationToken, requestOptions: new QueryRequestOptions { MaxItemCount=pageSize, PartitionKey =!string.IsNullOrWhiteSpace(partitionkey) ? new PartitionKey(partitionkey) : null });
+            while (iterator.HasMoreResults)
+            {
+                FeedResponse<T> currentResultSet = await iterator.ReadNextAsync();
+                list.AddRange(currentResultSet);
+                RU += currentResultSet.RequestCharge;
+                //此处需要优化 ,检查相关的 关键字 用正则
+                if (sql.Contains(" distinct ", StringComparison.OrdinalIgnoreCase)
+                    || (sql.Contains("order ", StringComparison.OrdinalIgnoreCase)
+                    && !sql.Contains(".order ", StringComparison.OrdinalIgnoreCase)))
+                {
+                    continuationToken = null;
+                }
+                else
+                {
+                    continuationToken = currentResultSet.ContinuationToken;
+                }
+                if (pageSize.HasValue && pageSize.Value >=0 && list.Count>=pageSize)
+                {
+                    break;
+                }
+            }
+            //记录日志,RU开销大于400(开发测试),1000(正式)
+            return (new CosmosDBResult<T> { list = list, ru = RU, continuationToken = continuationToken }); 
+        }
+        /// <summary>
+        /// 取得当前容器指定分区键的Count数,支持SQL Where条件,不支持排序
+        /// </summary>
+        /// <param name="container"></param>
+        /// <param name="partitionkey"></param>
+        /// <param name="queryWhere"></param>
+        /// <returns></returns>
+        public static async Task<int> GetCount(this Container container, string queryWhere = "WHERE 1=1", string? partitionkey = null)
+        {
+            int totalCount = 0;
+            var items = container.GetItemQueryStreamIterator(
+                queryText: $"SELECT VALUE COUNT(1) From c {queryWhere}",
+                requestOptions: new QueryRequestOptions() { PartitionKey =!string.IsNullOrWhiteSpace(partitionkey) ? new PartitionKey(partitionkey) : null, MaxItemCount = -1 });
+            while (items.HasMoreResults)
+            {
+                using var response = await items.ReadNextAsync();
+                using var json = await JsonDocument.ParseAsync(response.Content);
+                if (json.RootElement.TryGetProperty("_count", out JsonElement count) && count.GetUInt16() > 0)
+                {
+                    foreach (var obj in json.RootElement.GetProperty("Documents").EnumerateArray())
+                    {
+                        totalCount = obj.GetInt32();
+                    }
+                }
+            }
+
+            return totalCount;
+        }
+
+        public static async Task<ResponseMessage[]> DeleteItemsStreamAsync(this Container container, List<string> ids, string partitionkey)
+        {
+            List<Task<ResponseMessage>> responses = new List<Task<ResponseMessage>>();
+            if (ids.IsNotEmpty())
+            {
+                foreach (var id in ids)
+                {
+                    try
+                    {
+                        responses.Add(container.DeleteItemStreamAsync(id, new PartitionKey(partitionkey)));
+                    }
+                    catch
+                    {
+                        continue;
+                    }
+                }
+            }
+            var response = await Task.WhenAll(responses);
+            return response;
+        }
+
+        public static async Task<ItemResponse<T>[]> DeleteItemsAsync<T>(this Container container, List<string> ids, string partitionkey)
+        {
+            List<Task<ItemResponse<T>>> responses = new List<Task<ItemResponse<T>>>(); ;
+            if (ids.IsNotEmpty())
+            {
+                foreach (var id in ids)
+                {
+                    try
+                    {
+                        responses.Add(container.DeleteItemAsync<T>(id, new PartitionKey(partitionkey)));
+                    }
+                    catch
+                    {
+                        continue;
+                    }
+                }
+            }
+            var response = await Task.WhenAll(responses);
+            return response;
+        }
+    }
+
+    public class CosmosDBResult<T>
+    {
+        public List<T>? list { get; set; } = new List<T>();
+        public string? continuationToken { get; set; }
+        //RU数据库查询开销
+        public double ru { get; set; }
+    }
+}

+ 110 - 0
HTEX.Complex/Service/AzureCosmos3/AzureCosmos3Factory.cs

@@ -0,0 +1,110 @@
+using Azure.Core.Serialization;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.Azure.Cosmos;
+
+namespace TEAMModelOS.SDK.DI.AzureCosmos3
+{
+    public class AzureCosmos3Factory
+    {
+        private readonly IServiceProvider _services;
+        private readonly IOptionsMonitor<AzureCosmos3FactoryOptions> _optionsMonitor;
+        private readonly ILogger _logger;
+        //private Option _option;
+        private ConcurrentDictionary<string, CosmosClient> CosmosClients { get; } = new ConcurrentDictionary<string, CosmosClient>();
+
+        //   private CosmosDatabase database { get; set; }
+
+        public AzureCosmos3Factory(IServiceProvider services, IOptionsMonitor<AzureCosmos3FactoryOptions> optionsMonitor, ILogger<AzureCosmos3Factory> logger)
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            if (optionsMonitor == null) throw new ArgumentNullException(nameof(optionsMonitor));
+
+            _services = services;
+            _optionsMonitor = optionsMonitor;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// 取得CosmosClient,支持安全執行緒
+        /// </summary>
+        /// <param name="name"></param>
+        /// <param name="region">可以使用Regions.{區域}設置,指定此屬性後,SDK會首選該區域來執行操作。此外,SDK會自動選擇後備的地理複製區域以實現高可用性。如果未指定此屬性,則SDK會將寫區域用作所有操作的首選區域</param>
+        /// <returns></returns>
+        public CosmosClient GetCosmosClient(string? region = null, string name = "Default")
+        {
+            try
+            {
+                //CosmosClientOptions 的 SerializerOptions = new CosmosSerializationOptions() { PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase } 
+                //需等待官方修正
+
+                JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions()
+                {
+                    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+                };
+                var cm = CosmosClients.GetOrAdd(name, x => new CosmosClient(_optionsMonitor.Get(name).CosmosConnectionString, new CosmosClientOptions()
+                {
+                    Serializer= new CosmosSystemTextJsonSerializer(jsonSerializerOptions),
+                    ApplicationRegion = region
+                }));
+                return cm;
+            }
+            catch (Exception e)
+            {
+                _logger?.LogWarning(e, e.Message);
+                throw;
+            }
+        }
+    }
+    /// <summary>
+    /// Uses <see cref="Azure.Core.Serialization.JsonObjectSerializer"/> which leverages System.Text.Json providing a simple API to interact with on the Azure SDKs.
+    /// </summary>
+    // <SystemTextJsonSerializer>
+    public class CosmosSystemTextJsonSerializer : CosmosSerializer
+    {
+        private readonly JsonObjectSerializer systemTextJsonSerializer;
+
+        public CosmosSystemTextJsonSerializer(JsonSerializerOptions jsonSerializerOptions)
+        {
+            this.systemTextJsonSerializer = new JsonObjectSerializer(jsonSerializerOptions);
+        }
+
+        public override T FromStream<T>(Stream stream)
+        {
+            using (stream)
+            {
+                if (stream.CanSeek
+                       && stream.Length == 0)
+                {
+                    return default;
+                }
+
+                if (typeof(Stream).IsAssignableFrom(typeof(T)))
+                {
+                    return (T)(object)stream;
+                }
+
+                return (T)this.systemTextJsonSerializer.Deserialize(stream, typeof(T), default);
+            }
+        }
+
+        public override Stream ToStream<T>(T input)
+        {
+            MemoryStream streamPayload = new MemoryStream();
+            this.systemTextJsonSerializer.Serialize(streamPayload, input, input.GetType(), default);
+            streamPayload.Position = 0;
+            return streamPayload;
+        }
+    }
+    // </SystemTextJsonSerializer>
+}
+

+ 22 - 0
HTEX.Complex/Service/AzureCosmos3/AzureCosmos3FactoryExtensions.cs

@@ -0,0 +1,22 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TEAMModelOS.SDK.DI.AzureCosmos3
+{
+    public static class AzureCosmos3FactoryExtensions
+    {
+        public static IServiceCollection AddAzureCosmos3(this IServiceCollection services, string connectionString, string name = "Default")
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            if (connectionString == null) throw new ArgumentNullException(nameof(connectionString));
+            services.TryAddSingleton<AzureCosmos3Factory>();
+            services.Configure<AzureCosmos3FactoryOptions>(name, o => { o.Name = name; o.CosmosConnectionString = connectionString; });
+            return services;
+        }
+    }
+}

+ 14 - 0
HTEX.Complex/Service/AzureCosmos3/AzureCosmos3FactoryOptions.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TEAMModelOS.SDK.DI.AzureCosmos3
+{
+    public class AzureCosmos3FactoryOptions
+    {
+        public string? Name { get; set; }
+        public string? CosmosConnectionString { get; set; }
+    }
+}

+ 11 - 0
HTEX.Complex/Service/AzureCosmos3/CosmosEntity.cs

@@ -0,0 +1,11 @@
+namespace HTEX.Complex
+{
+    public class CosmosEntity
+    {
+        public string? id { get; set; }
+        public string? code { get; set; }
+        public string? pk { get; set; }
+        public int? ttl { get; set; } = -1;
+        
+    }
+}

+ 70 - 0
HTEX.Complex/Service/AzureRedis/AzureRedisFactory.cs

@@ -0,0 +1,70 @@
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using StackExchange.Redis;
+using System.Collections.Concurrent;
+
+namespace HTEX.Complex.Service.AzureRedis
+{
+    public class AzureRedisFactoryOptions
+    {
+        public string Name { get; set; }
+        public string RedisConnectionString { get; set; }
+    }
+    public static class AzureRedisFactoryExtensions
+    {
+        public static IServiceCollection AddAzureRedis(this IServiceCollection services, string connectionString, string name = "Default")
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            if (connectionString == null) throw new ArgumentNullException(nameof(connectionString));
+
+            services.TryAddSingleton<AzureRedisFactory>();
+            services.Configure<AzureRedisFactoryOptions>(name, o => { o.Name = name; o.RedisConnectionString = connectionString; });
+
+            return services;
+        }
+    }
+    public class AzureRedisFactory
+    {
+        private readonly IServiceProvider _services;
+        private readonly IOptionsMonitor<AzureRedisFactoryOptions> _optionsMonitor;
+        private readonly ILogger _logger;
+        private ConcurrentDictionary<string, ConnectionMultiplexer> ConnectionMultiplexers { get; } = new ConcurrentDictionary<string, ConnectionMultiplexer>();
+        public AzureRedisFactory(IServiceProvider services, IOptionsMonitor<AzureRedisFactoryOptions> optionsMonitor, ILogger<AzureRedisFactory> logger)
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            if (optionsMonitor == null) throw new ArgumentNullException(nameof(optionsMonitor));
+
+            _services = services;
+            _optionsMonitor = optionsMonitor;
+            _logger = logger;
+        }
+
+        public IDatabase GetRedisClient(int dbnum = -1, string name = "Default")
+        {
+            try
+            {
+                var cm = ConnectionMultiplexers.GetOrAdd(name, x => ConnectionMultiplexer.Connect(_optionsMonitor.Get(name).RedisConnectionString));
+                return cm.GetDatabase(dbnum);
+            }
+            catch (OptionsValidationException e)
+            {
+                _logger?.LogWarning(e, e.Message);
+                return null;
+            }
+        }
+        //public (IDatabase db , IServer server ) GetRedisDbCmClient(int dbnum = -1, string name = "Default")
+        //{
+        //    try
+        //    {
+        //        var cm = ConnectionMultiplexers.GetOrAdd(name, x => ConnectionMultiplexer.Connect(_optionsMonitor.Get(name).RedisConnectionString));
+
+        //        return (cm.GetDatabase(dbnum),cm.GetServer(cm.GetEndPoints()[0]));
+        //    }
+        //    catch (OptionsValidationException e)
+        //    {
+        //        _logger?.LogWarning(e, e.Message);
+        //        return (null,null);
+        //    }
+        //}
+    }
+}

+ 216 - 0
HTEX.Complex/Service/AzureServiceBus/AzureServiceBusExtensions.cs

@@ -0,0 +1,216 @@
+using Azure.Messaging.ServiceBus;
+using System.Collections.Concurrent;
+using System.Text.Json;
+
+namespace TEAMModelOS.SDK.DI
+{
+    public static class AzureServiceBusExtensions
+    {
+        private static ConcurrentDictionary<string, ServiceBusSender> ServiceBusSenders { get; } = new ConcurrentDictionary<string, ServiceBusSender>();
+        private static ConcurrentDictionary<string, ServiceBusReceiver> ServiceBusReceivers { get; } = new ConcurrentDictionary<string, ServiceBusReceiver>();
+        /// <summary>
+        /// 订阅发布接收信息
+        /// </summary>       
+        /// <param name="name">QueueName or TopicName</param>
+        /// <param name="message">訊息</param>
+        /// <returns></returns>
+        public static async Task<List<string>> ReceiverMessageAsync(this ServiceBusClient client, string name,string subscriptionName)
+        {
+            try
+            {
+                ServiceBusReceiver receiver = ServiceBusReceivers.GetOrAdd(name, x => client.CreateReceiver(name));
+                IReadOnlyList<ServiceBusReceivedMessage> receivedMessages = await receiver.ReceiveMessagesAsync(maxMessages: 1);
+                List<string> bodys = new List<string>();
+                foreach (ServiceBusReceivedMessage receivedMessage in receivedMessages)
+                {
+                    // get the message body as a string
+                    string body = receivedMessage.Body.ToString();
+                    bodys.Add(body);
+                }
+                return bodys;
+            }
+            catch
+            {
+                throw;
+            }
+        }
+        /// <summary>
+        /// 队列接收信息
+        /// </summary>       
+        /// <param name="name">QueueName or TopicName</param>
+        /// <param name="message">訊息</param>
+        /// <returns></returns>
+        public static async Task<List<string>> ReceiverMessageAsync(this ServiceBusClient client, string name)
+        {
+            try
+            {//ReceiveAndDelete
+                ServiceBusReceiver receiver = ServiceBusReceivers.GetOrAdd(name, x => client.CreateReceiver(name,new ServiceBusReceiverOptions() { ReceiveMode=ServiceBusReceiveMode.ReceiveAndDelete}));
+              
+                IReadOnlyList<ServiceBusReceivedMessage> receivedMessages   = await receiver.ReceiveMessagesAsync(maxMessages:1);
+              
+                List<string> bodys = new List<string>();
+                foreach (ServiceBusReceivedMessage receivedMessage in receivedMessages)
+                {
+                    // get the message body as a string
+                    string body = receivedMessage.Body.ToString();
+                   
+                    bodys.Add(body);
+                }
+                return bodys;
+            }
+            catch
+            {
+                throw;
+            }
+        }
+
+        /// <summary>
+        /// 發送信息至對列或主題
+        /// </summary>       
+        /// <param name="name">QueueName or TopicName</param>
+        /// <param name="message">訊息</param>
+        /// <returns></returns>
+        public static async Task SendMessageAsync(this ServiceBusClient client, string name, ServiceBusMessage message)
+        {
+            try
+            {
+                ServiceBusSender sender = ServiceBusSenders.GetOrAdd(name, x => client.CreateSender(name));                
+                await sender.SendMessageAsync(message);                
+            }
+            catch
+            {
+                throw;
+            }
+        }
+
+        /// <summary>
+        /// 批量發送訊息至對列或主題,如果批量失敗返回False
+        /// </summary>       
+        /// <param name="name">QueueName or TopicName</param>
+        /// <param name="messages">批量訊息</param>
+        /// <returns></returns>
+        public static async Task<bool> SendBatchMessageAsync(this ServiceBusClient client, string name, IList<ServiceBusMessage> messages)
+        {
+            try
+            {
+                ServiceBusSender sender = ServiceBusSenders.GetOrAdd(name, x => client.CreateSender(name));
+                ServiceBusMessageBatch messageBatch = await sender.CreateMessageBatchAsync();
+                foreach (var msg in messages)
+                {
+                    if (!messageBatch.TryAddMessage(msg)) return false;
+                }
+                await sender.SendMessagesAsync(messageBatch);                
+                return true;
+            }
+            catch
+            {
+                return false;
+            }
+        }
+        
+        /// <summary>
+        /// 發送信息至對列或主題(指定時間排程)
+        /// </summary>       
+        /// <param name="name">QueueName or TopicName</param>
+        /// <param name="message">訊息</param>
+        /// <returns>排程訊息的序列號。</returns>
+        public static async Task<long> SendScheduleMessageAsync(this ServiceBusClient client, string name, ServiceBusMessage message, DateTimeOffset scheduleTime)
+        {
+            try
+            {
+                ServiceBusSender sender = ServiceBusSenders.GetOrAdd(name, x => client.CreateSender(name));
+                long num= await sender.ScheduleMessageAsync(message, scheduleTime);                
+                return num;
+            }
+            catch
+            {
+                throw;
+            }
+        }
+
+        /// <summary>
+        /// 批量發送訊息至對列或主題(指定時間排程),如果批量失敗返回False
+        /// </summary>       
+        /// <param name="name">QueueName or TopicName</param>
+        /// <param name="messages">批量訊息</param>
+        /// <returns>排程訊息的序列號</returns>
+        public static async Task<IReadOnlyList<long>> SendScheduleMessagesAsync(this ServiceBusClient client, string name, IList<ServiceBusMessage> messages, DateTimeOffset scheduleTime)
+        {
+            try
+            {
+                ServiceBusSender sender = ServiceBusSenders.GetOrAdd(name, x => client.CreateSender(name));
+                List<ServiceBusMessage> msgs = new() { };
+                foreach (var msg in messages)
+                {
+                    msgs.Add(msg);
+                }
+                var nums= await sender.ScheduleMessagesAsync(msgs, scheduleTime);                
+                return nums;
+            }
+            catch
+            {
+                return null;
+            }
+        }
+
+
+        public static async Task<long> SendLeamMessage<T>(this ServiceBusClient client, string name, string id, string pk, long startTime)
+        {
+            //微調代碼
+            var timer = DateTimeOffset.FromUnixTimeMilliseconds(startTime);
+            /*if (type.Equals("start"))
+            {
+                if (timer.CompareTo(DateTimeOffset.UtcNow) < 0)
+                {
+                    progress = "going";
+                }
+            }
+            else if(type.Equals("end"))
+            {
+                if (timer.CompareTo(DateTimeOffset.UtcNow) < 0)
+                {
+                    progress = "finish";
+                }
+            }*/
+            
+            
+            //设定开始时间
+            Dictionary<string, object> dict = new() {
+                    { "name",typeof(T).Name},
+                    { "id",id},
+                    { "code",pk}
+                };
+            //var msgId = "1";
+            string messageBody = $"Message {dict}";
+
+            long SequenceNumber = await client.SendScheduleMessageAsync(name, new ServiceBusMessage(dict.ToJsonString()), timer);
+
+            return SequenceNumber;
+        }
+
+        public static async Task CancelMessageAsync(this ServiceBusClient client, string name, long number)
+        {
+            try
+            {
+                ServiceBusSender sender = ServiceBusSenders.GetOrAdd(name, x => client.CreateSender(name));
+                await sender.CancelScheduledMessageAsync(number);                
+            }
+            catch
+            {
+                throw;
+            }
+        }
+        public static string ToJsonString(this Object obj, JsonSerializerOptions option = null)
+        {
+            if (option == null)
+            {
+                option = new System.Text.Json.JsonSerializerOptions
+                {
+                    Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+                };
+            }
+            var json = JsonSerializer.Serialize(obj, option);
+            return json;
+        }
+    }
+}

+ 46 - 0
HTEX.Complex/Service/AzureServiceBus/AzureServiceBusFactory.cs

@@ -0,0 +1,46 @@
+
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.Extensions.DependencyInjection;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Azure.Storage.Blobs.Specialized;
+using System.Collections.Concurrent;
+using Azure.Messaging.ServiceBus;
+
+namespace TEAMModelOS.SDK.DI
+{
+    public class AzureServiceBusFactory
+    {
+        private readonly IServiceProvider _services;
+        private readonly IOptionsMonitor<AzureServiceBusFactoryOptions> _optionsMonitor;
+        private readonly ILogger _logger;
+        private ConcurrentDictionary<string, ServiceBusClient> ServiceBusClients { get; } = new ConcurrentDictionary<string, ServiceBusClient>();
+        public AzureServiceBusFactory(IServiceProvider services, IOptionsMonitor<AzureServiceBusFactoryOptions> optionsMonitor, ILogger<AzureServiceBusFactory> logger)
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            if (optionsMonitor == null) throw new ArgumentNullException(nameof(optionsMonitor));
+
+            _services = services;
+            _optionsMonitor = optionsMonitor;
+            _logger = logger;            
+        }
+
+        public ServiceBusClient GetServiceBusClient(string name = "Default")
+        {           
+            try
+            {
+                var client = ServiceBusClients.GetOrAdd(name, x => new ServiceBusClient(_optionsMonitor.Get(name).ServiceBusConnectionString));               
+                return client;
+            }
+            catch (OptionsValidationException e)
+            {
+                _logger?.LogWarning(e, e.Message);
+                return null;
+            }
+        }       
+    }
+}

+ 22 - 0
HTEX.Complex/Service/AzureServiceBus/AzureServiceBusFactoryExtensions.cs

@@ -0,0 +1,22 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace TEAMModelOS.SDK.DI
+{
+    public static class AzureServiceBusFactoryExtensions
+    {
+        public static IServiceCollection AddAzureServiceBus(this IServiceCollection services, string connectionString, string name = "Default")
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));            
+            if (connectionString == null) throw new ArgumentNullException(nameof(connectionString));
+
+            services.TryAddSingleton<AzureServiceBusFactory>();
+            services.Configure<AzureServiceBusFactoryOptions>(name, o => { o.Name = name; o.ServiceBusConnectionString = connectionString; });
+
+            return services;
+        }
+    }
+}

+ 13 - 0
HTEX.Complex/Service/AzureServiceBus/AzureServiceBusFactoryOptions.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace TEAMModelOS.SDK.DI
+{
+    public class AzureServiceBusFactoryOptions
+    {
+        public string Name { get; set; }
+        public string ServiceBusConnectionString { get; set; }
+      
+    }
+}

+ 162 - 0
HTEX.Complex/Service/AzureStorage/AzureStorageBlobExtensions.cs

@@ -0,0 +1,162 @@
+using Azure.Storage;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Azure.Storage.Sas;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using System.Configuration;
+
+namespace HTEX.Complex.Service
+{
+    public static class AzureStorageBlobExtensions
+    {
+        public static IServiceCollection AddAzureStorage(this IServiceCollection services, string connectionString, string name = "Default")
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            if (connectionString == null) throw new ArgumentNullException(nameof(connectionString));
+
+            services.TryAddSingleton<AzureStorageFactory>();
+            services.Configure<AzureStorageFactoryOptions>(name, o => { o.Name = name; o.StorageAccountConnectionString = connectionString; });
+
+            return services;
+        }
+        /// <summary>
+        /// 系统管理员 资源,题目关联,htex关联,学习活动学生上传文件关联,基本信息关联,教室平面图关联,评测冷数据关联
+        /// "system": [ "res", "item", "htex", "task", "info", "room", "exam" ],
+        /// 资源,题目关联,htex关联,学习活动学生上传文件关联,基本信息关联,教室平面图关联,评测冷数据关联
+        /// "school": [ "res", "item", "htex", "task", "info", "room", "exam" ],
+        /// 资源,题目关联,htex关联,学习活动关联,教师基本信息关联
+        /// "teacher": [ "res", "item", "htex", "task", "info" ],
+        /// 答案及学习活动上传的文件,学生基本信息关联
+        ///"student": [ "stu/{studentId}/ans", "stu/{studentId}/task" ]
+        /// </summary>
+        /// <param name="name">容器名称</param>
+        /// <param name="stream">文件内容的流</param>
+        /// <param name="folder">业务文件夹</param>
+        /// <param name="fileName">文件名</param>
+        /// <param name="contentTypeDefault">是否存放文件后缀对应的contentType</param>
+        /// <returns></returns>
+        public static async Task<(string blobName ,string url )> UploadFileByContainer(this BlobContainerClient blobContainer, Stream stream, string root, string blobpath, bool contentTypeDefault = true)
+        {
+            //BlobContainerClient blobContainer = azureStorage.GetBlobContainerClient(name.ToLower().Replace("#", "")); //blobClient.GetContainerReference(groupName); 
+            Uri url = blobContainer.Uri;
+            var blockBlob = blobContainer.GetBlobClient($"{root}/{blobpath}");
+            string content_type = "application/octet-stream";
+            if (!contentTypeDefault)
+            {
+                string fileext = blobpath.Substring(blobpath.LastIndexOf(".") > 0 ? blobpath.LastIndexOf(".") : 0);
+                ContentTypeDict.dict.TryGetValue(fileext, out string contenttype);
+                if (!string.IsNullOrEmpty(contenttype))
+                {
+                    content_type = contenttype;
+                }
+            }
+            await blockBlob.UploadAsync(stream, true);
+            blockBlob.SetHttpHeaders(new BlobHttpHeaders { ContentType = content_type });
+          
+            return (blockBlob.Name,blockBlob.Uri.ToString());
+        }
+
+        public static async Task<string> UploadFileByContainer(this BlobContainerClient blobContainer, string json, string root, string blobpath, bool contentTypeDefault = true)
+        {
+            // string groupName =folder;
+            //BlobContainerClient blobContainer = azureStorage.GetBlobContainerClient(name.ToLower().Replace("#", "")); //blobClient.GetContainerReference(groupName);            
+            var blockBlob = blobContainer.GetBlobClient($"{root}/{blobpath}");
+            string content_type = "application/octet-stream";
+            if (!contentTypeDefault)
+            {
+                string fileext = blobpath.Substring(blobpath.LastIndexOf(".") > 0 ? blobpath.LastIndexOf(".") : 0);
+                ContentTypeDict.dict.TryGetValue(fileext, out string contenttype);
+                if (!string.IsNullOrEmpty(contenttype))
+                {
+                    content_type = contenttype;
+                }
+            }
+            byte[] bytes = System.Text.Encoding.Default.GetBytes(json);
+            Stream streamBlob = new MemoryStream(bytes);
+            await blockBlob.UploadAsync(streamBlob, true);
+            blockBlob.SetHttpHeaders(new BlobHttpHeaders { ContentType = content_type });
+            return blockBlob.Uri.ToString();
+        }
+
+
+        /// <summary>
+        /// 取得Blob SAS (有效期預設一天)
+        /// </summary>
+        /// <param name="containerName">容器名稱</param>
+        /// <param name="blobName"></param>
+        /// <param name="blobSasPermissions"></param>
+        /// <param name="name"></param>
+        /// <returns></returns>
+        public static BlobAuth GetBlobSasUriRead(this BlobContainerClient blobContainer,IConfiguration _configuration , string containerName, string blobName, string name = "Default")
+        {
+            try
+            {
+                var ConnectionString =   name.Equals("Default") ? _configuration.GetSection("Azure:Storage:ConnectionString").Value : _configuration.GetSection("Azure:Storage:ConnectionString-Test").Value;
+                var keys = ParseConnectionString(ConnectionString);
+                var accountname = keys["AccountName"];
+                var accountkey = keys["AccountKey"];
+                var endpoint = keys["EndpointSuffix"];
+                DateTimeOffset dateTime = DateTimeOffset.UtcNow.Add(new TimeSpan(365 * 99, 0, 15, 0));
+                long time = dateTime.ToUnixTimeMilliseconds();
+                var blobSasBuilder = new BlobSasBuilder
+                {
+                    StartsOn = DateTimeOffset.UtcNow.Subtract(new TimeSpan(0, 15, 0)),
+                    ExpiresOn = dateTime,
+                    BlobContainerName = containerName.ToLower(),
+                    BlobName = blobName
+                };
+
+                blobSasBuilder.SetPermissions(BlobSasPermissions.Read);
+                var sskc = new StorageSharedKeyCredential(accountname, accountkey);
+                BlobSasQueryParameters sasQueryParameters = blobSasBuilder.ToSasQueryParameters(sskc);
+                UriBuilder fullUri = new UriBuilder()
+                {
+                    Scheme = "https",
+                    Host = $"{accountname}.blob.{endpoint}",
+                    Path = $"{containerName.ToLower()}/{blobName}",
+                    Query = sasQueryParameters.ToString()
+                };
+                return new BlobAuth { url = fullUri.Uri.ToString(), sas = sasQueryParameters.ToString(), timeout = time };
+                // return fullUri.Uri.ToString();
+            }
+            catch
+            {
+                return null;
+            }
+        }
+        public static async Task<List<string>> List(this BlobContainerClient client, string prefix = null)
+        {
+            try
+            {
+                List<string> items = new List<string>();
+                await foreach (BlobItem item in client.GetBlobsAsync(BlobTraits.None, BlobStates.None, prefix))
+                {
+                    items.Add(item.Name);
+                }
+                return items;
+            }
+            catch
+            {
+                return null;
+            }
+        }
+
+        public static Dictionary<string, string> ParseConnectionString(string connectionString)
+        {
+            var d = new Dictionary<string, string>();
+            foreach (var item in connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries))
+            {
+                var a = item.IndexOf('=');
+                d.Add(item.Substring(0, a), item.Substring(a + 1));
+            }
+            return d;
+        }
+        public class BlobAuth
+        {
+            public string url { get; set; }
+            public string sas { get; set; }
+            public long timeout { get; set; }
+            public string name { get; set; }
+        }
+    }
+}

+ 55 - 0
HTEX.Complex/Service/AzureStorage/AzureStorageFactory.cs

@@ -0,0 +1,55 @@
+using Azure.Data.Tables;
+using Azure.Storage.Blobs;
+using Microsoft.Extensions.Options;
+
+namespace HTEX.Complex.Service
+{
+    public class AzureStorageFactoryOptions
+    {
+        public string Name { get; set; }
+        public string StorageAccountConnectionString { get; set; }
+    }
+
+    public class AzureStorageFactory
+    {
+        private readonly IServiceProvider _services;
+        private readonly IOptionsMonitor<AzureStorageFactoryOptions> _optionsMonitor;
+        private readonly ILogger _logger;
+        public AzureStorageFactory(IServiceProvider services, IOptionsMonitor<AzureStorageFactoryOptions> optionsMonitor, ILogger<AzureStorageFactory> logger)
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            if (optionsMonitor == null) throw new ArgumentNullException(nameof(optionsMonitor));
+
+            _services = services;
+            _optionsMonitor = optionsMonitor;
+            _logger = logger;
+        }
+        public BlobContainerClient GetBlobContainerClient(string containerName, string name = "Default")
+        {
+            try
+            {
+                var options = _optionsMonitor.Get(name);
+                return new BlobContainerClient(options.StorageAccountConnectionString, containerName);
+            }
+            catch (OptionsValidationException e)
+            {
+                _logger?.LogWarning(e, e.Message);
+                return null;
+            }
+        }
+        public TableServiceClient TableServiceClient(string name = "Default")
+        {
+            try
+            {
+                var options = _optionsMonitor.Get(name);
+                TableServiceClient tableServiceClient = new TableServiceClient(options.StorageAccountConnectionString);
+               
+                return tableServiceClient;
+            }
+            catch (OptionsValidationException e)
+            {
+                return null;
+            }
+        }
+    }
+}

+ 13 - 0
HTEX.Complex/Service/AzureStorage/AzureStorageTableExtensions.cs

@@ -0,0 +1,13 @@
+using Azure.Data.Tables;
+using System.Reflection;
+using System.Text;
+
+namespace HTEX.Complex.Service.AzureStorage
+{
+    public static class AzureStorageTableExtensions
+    {
+
+        
+    }
+
+}

+ 15 - 0
HTEX.Complex/Service/AzureStorage/TableEntity.cs

@@ -0,0 +1,15 @@
+using Azure;
+using Azure.Data.Tables;
+using System.Collections;
+using System.Globalization;
+
+namespace HTEX.Complex.Service.AzureStorage
+{
+    public class HTableEntity : ITableEntity
+    {
+        public string? PartitionKey { get; set; }
+        public string? RowKey { get; set; }
+        public DateTimeOffset? Timestamp { get; set; }
+        public ETag ETag { get; set; }
+    }
+}

+ 391 - 0
HTEX.Complex/Service/ContentTypeDict.cs

@@ -0,0 +1,391 @@
+namespace HTEX.Complex.Service
+{
+    public class ContentTypeDict
+    {
+        public static readonly Dictionary<string, string> extdict = new Dictionary<string, string> {
+        //{ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",".xlsx"},
+        { "audio/x-ms-wma",".wma"},
+        {"video/3gpp",".3g2"},
+        {"audio/mp4",".aac"},
+        {"audio/ac3",".ac3"},
+        {"text/x-adasrc",".adb"},
+        {"image/x-applix-graphics",".ag"},
+        {"audio/amr",".amr"},
+        {"audio/x-ape",".ape"},
+        {"image/x-sony-arw",".arw"},
+        {"text/plain",".asc"},
+        {"video/x-ms-asf",".asf"},
+        {"text/x-ssa",".ass"},
+        {"video/x-msvideo",".avi"},
+        {"text/x-bibtex",".bib"},
+        {"image/bmp",".bmp"},
+        {"text/x-csrc",".c"},
+        {"text/x-c++src",".c++"},
+        {"image/cgm",".cgm"},
+        {"text/x-tex",".cls"},
+        {"text/x-cmake",".cmake"},
+        {"image/x-canon-cr2",".cr2"},
+        {"image/x-canon-crw",".crw"},
+        {"text/x-csharp",".cs"},
+        {"text/css",".css"},
+        {"text/csv",".csv"},
+        {"image/x-win-bitmap",".cur"},
+        {"text/x-dsrc",".d"},
+        {"text/x-dcl",".dcl"},
+        {"image/x-kodak-dcr",".dcr"},
+        {"image/x-dds",".dds"},
+        {"text/x-patch",".diff"},
+        {"image/vnd.djvu",".djv"},
+        {"image/x-adobe-dng",".dng"},
+        {"text/vnd.graphviz",".dot"},
+        {"text/x-dsl",".dsl"},
+        {"image/vnd.dwg",".dwg"},
+        {"image/vnd.dxf",".dxf"},
+        {"text/x-eiffel",".e"},
+        {"text/x-emacs-lisp",".el"},
+        {"image/x-emf",".emf"},
+        {"image/x-eps",".eps"},
+        {"image/x-bzeps",".eps.bz2"},
+        {"image/x-gzeps",".eps.gz"},
+        {"text/x-erlang",".erl"},
+        {"text/x-setext",".etx"},
+        {"image/x-exr",".exr"},
+        {"text/x-fortran",".f"},
+        {"image/x-xfig",".fig"},
+        {"image/fits",".fits"},
+        {"video/x-flv",".flv"},
+        {"text/x-xslfo",".fo"},
+        {"image/fax-g3",".g3"},
+        {"text/directory",".gcrd"},
+        {"image/gif",".gif"},
+        {"text/x-google-video-pointer",".gvp"},
+        {"text/x-chdr",".h"},
+        {"text/x-c++hdr",".h++"},
+        {"text/x-haskell",".hs"},
+        {"text/html",".htm"},
+        {"image/x-tga",".icb"},
+        {"image/x-icns",".icns"},
+        {"image/x-icon",".ico"},
+        {"text/calendar",".ics"},
+        {"text/x-idl",".idl"},
+        {"image/ief",".ief"},
+        {"image/x-iff",".iff"},
+        {"image/x-ilbm",".ilbm"},
+        {"text/x-imelody",".ime"},
+        {"text/x-iptables",".iptables"},
+        {"image/jp2",".j2k"},
+        {"text/vnd.sun.j2me.app-descriptor",".jad"},
+        {"text/x-java",".java"},
+        {"image/x-jng",".jng"},
+        {"image/jpeg",".jpeg"},
+        {"application/javascript",".js"},
+        {"application/json",".json"},
+        {"application/jsonp",".jsonp"},
+        {"image/x-kodak-k25",".k25"},
+        {"image/x-kodak-kdc",".kdc"},
+        {"text/x-ldif",".ldif"},
+        {"text/x-literate-haskell",".lhs"},
+        {"text/x-log",".log"},
+        {"text/x-lua",".lua"},
+        {"image/x-lwo",".lwo"},
+        {"image/x-lws",".lws"},
+        {"text/x-lilypond",".ly"},
+        {"text/x-matlab",".m"},
+        {"video/mpeg",".m2t"},
+        {"audio/x-mpegurl",".m3u"},
+        {"application/vnd.apple.mpegurl",".m3u8"},
+        {"text/x-troff-me",".me"},
+        {"video/x-matroska",".mkv"},
+        {"text/x-ocaml",".ml"},
+        {"text/x-troff-mm",".mm"},
+        {"text/mathml",".mml"},
+        {"text/x-moc",".moc"},
+        {"text/x-mof",".mof"},
+        {"audio/mpeg",".mp3"},
+        {"video/mp4",".mp4"},
+        {"text/x-mrml",".mrl"},
+        {"image/x-minolta-mrw",".mrw"},
+        {"text/x-troff-ms",".ms"},
+        {"image/x-msod",".msod"},
+        {"text/x-mup",".mup"},
+        {"image/x-nikon-nef",".nef"},
+        {"text/x-nfo",".nfo"},
+        {"text/x-ocl",".ocl"},
+        {"text/x-opml+xml",".opml"},
+        {"image/openraster",".ora"},
+        {"image/x-olympus-orf",".orf"},
+        {"text/x-pascal",".p"},
+        {"image/x-portable-bitmap",".pbm"},
+        {"image/x-photo-cd",".pcd"},
+        {"image/x-pcx",".pcx"},
+        {"application/pdf",".pdf"},
+        {"image/x-pentax-pef",".pef"},
+        {"image/x-portable-graymap",".pgm"},
+        {"image/x-pict",".pict"},
+        {"image/png",".png"},
+        {"image/x-portable-anymap",".pnm"},
+        {"image/x-macpaint",".pntg"},
+        {"text/x-gettext-translation",".po"},
+        {"text/x-gettext-translation-template",".pot"},
+        {"text/x-python",".py"},
+        {"image/x-quicktime",".qif"},
+        {"image/x-fuji-raf",".raf"},
+        {"image/x-cmu-raster",".ras"},
+        {"image/x-panasonic-raw",".raw"},
+        {"text/x-ms-regedit",".reg"},
+        {"image/x-rgb",".rgb"},
+        {"image/rle",".rle"},
+        {"text/troff",".roff"},
+        {"image/vnd.rn-realpix",".rp"},
+        {"text/vnd.rn-realtext",".rt"},
+        {"text/richtext",".rtx"},
+        {"text/x-scheme",".scm"},
+        {"image/x-sgi",".sgi"},
+        {"text/sgml",".sgm"},
+        {"image/x-skencil",".sk"},
+        {"text/spreadsheet",".slk"},
+        {"text/x-rpm-spec",".spec"},
+        {"text/x-sql",".sql"},
+        {"image/x-sony-sr2",".sr2"},
+        {"image/x-sony-srf",".srf"},
+        {"text/x-subviewer",".sub"},
+        {"image/x-sun-raster",".sun"},
+        {"image/svg+xml",".svg"},
+        {"image/svg+xml-compressed",".svgz"},
+        {"text/x-txt2tags",".t2t"},
+        {"text/x-tcl",".tcl"},
+        {"text/x-texinfo",".texi"},
+        {"image/tiff",".tif"},
+        {"audio/x-voc",".voc"},
+        {"image/x-wmf",".wmf"},
+        {"text/vnd.wap.wml",".wml"},
+        {"text/vnd.wap.wmlscript",".wmls"},
+        {"video/x-ms-wmv",".wmv"},
+        {"application/xhtml+xml",".xhtml"},
+        {"application/xml",".xml"}
+    };
+
+        public static readonly Dictionary<string, string> dict = new Dictionary<string, string> {
+        {".3g2", "video/3gpp"},
+        {".3ga", "video/3gpp"},
+        {".3gp", "video/3gpp"},
+        {".aac", "audio/mp4"},
+        {".ac3", "audio/ac3"},
+        {".adb", "text/x-adasrc"},
+        {".ads", "text/x-adasrc"},
+        {".ag", "image/x-applix-graphics"},
+        {".amr", "audio/amr"},
+        {".ape", "audio/x-ape"},
+        {".arw", "image/x-sony-arw"},
+        {".asc", "text/plain"},
+        {".asf", "video/x-ms-asf"},
+        {".ass", "text/x-ssa"},
+        {".avi", "video/x-msvideo"},
+        {".bib", "text/x-bibtex"},
+        {".bmp", "image/bmp"},
+        {".c", "text/x-csrc"},
+        {".c++", "text/x-c++src"},
+        {".cc", "text/x-c++src"},
+        {".cgm", "image/cgm"},
+        {".cls", "text/x-tex"},
+        {".cmake", "text/x-cmake"},
+        {".cpp", "text/x-c++src"},
+        {".cr2", "image/x-canon-cr2"},
+        {".crw", "image/x-canon-crw"},
+        {".cs", "text/x-csharp"},
+        {".css", "text/css"},
+        {".cssl", "text/css"},
+        {".csv", "text/csv"},
+        {".cur", "image/x-win-bitmap"},
+        {".cxx", "text/x-c++src"},
+        {".d", "text/x-dsrc"},
+        {".dcl", "text/x-dcl"},
+        {".dcr", "image/x-kodak-dcr"},
+        {".dds", "image/x-dds"},
+        {".diff", "text/x-patch"},
+        {".djv", "image/vnd.djvu"},
+        {".djvu", "image/vnd.djvu"},
+        {".dng", "image/x-adobe-dng"},
+        {".dot", "text/vnd.graphviz"},
+        {".dsl", "text/x-dsl"},
+        {".dtx", "text/x-tex"},
+        {".dwg", "image/vnd.dwg"},
+        {".dxf", "image/vnd.dxf"},
+        {".e", "text/x-eiffel"},
+        {".eif", "text/x-eiffel"},
+        {".el", "text/x-emacs-lisp"},
+        {".emf", "image/x-emf"},
+        {".eps", "image/x-eps"},
+        {".eps.bz2", "image/x-bzeps"},
+        {".eps.gz", "image/x-gzeps"},
+        {".epsf", "image/x-eps"},
+        {".epsf.bz2", "image/x-bzeps"},
+        {".epsf.gz", "image/x-gzeps"},
+        {".epsi", "image/x-eps"},
+        {".epsi.bz2", "image/x-bzeps"},
+        {".epsi.gz", "image/x-gzeps"},
+        {".erl", "text/x-erlang"},
+        {".etx", "text/x-setext"},
+        {".exr", "image/x-exr"},
+        {".f", "text/x-fortran"},
+        {".f90", "text/x-fortran"},
+        {".f95", "text/x-fortran"},
+        {".fig", "image/x-xfig"},
+        {".fits", "image/fits"},
+        {".flv", "video/x-flv"},
+        {".fo", "text/x-xslfo"},
+        {".for", "text/x-fortran"},
+        {".g3", "image/fax-g3"},
+        {".gcrd", "text/directory"},
+        {".gif", "image/gif"},
+        {".gv", "text/vnd.graphviz"},
+        {".gvp", "text/x-google-video-pointer"},
+        {".h", "text/x-chdr"},
+        {".h++", "text/x-c++hdr"},
+        {".hh", "text/x-c++hdr"},
+        {".hp", "text/x-c++hdr"},
+        {".hpp", "text/x-c++hdr"},
+        {".hs", "text/x-haskell"},
+        {".htm", "text/html"},
+        {".html", "text/html"},
+        {".hxx", "text/x-c++hdr"},
+        {".icb", "image/x-tga"},
+        {".icns", "image/x-icns"},
+        {".ico", "image/x-icon"},
+        {".ics", "text/calendar"},
+        {".idl", "text/x-idl"},
+        {".ief", "image/ief"},
+        {".iff", "image/x-iff"},
+        {".ilbm", "image/x-ilbm"},
+        {".ime", "text/x-imelody"},
+        {".imy", "text/x-imelody"},
+        {".ins", "text/x-tex"},
+        {".iptables", "text/x-iptables"},
+        {".j2k", "image/jp2"},
+        {".jad", "text/vnd.sun.j2me.app-descriptor"},
+        {".java", "text/x-java"},
+        {".jng", "image/x-jng"},
+        {".jp2", "image/jp2"},
+        {".jpc", "image/jp2"},
+        {".jpe", "image/jpeg"},
+        {".jpeg", "image/jpeg"},
+        {".jpf", "image/jp2"},
+        {".jpg", "image/jpeg"},
+        {".jpx", "image/jp2"},
+        {".js", "application/javascript"},
+        {".json", "application/json"},
+        {".jsonp", "application/jsonp"},
+        {".k25", "image/x-kodak-k25"},
+        {".kdc", "image/x-kodak-kdc"},
+        {".latex", "text/x-tex"},
+        {".ldif", "text/x-ldif"},
+        {".lhs", "text/x-literate-haskell"},
+        {".log", "text/x-log"},
+        {".ltx", "text/x-tex"},
+        {".lua", "text/x-lua"},
+        {".lwo", "image/x-lwo"},
+        {".lwob", "image/x-lwo"},
+        {".lws", "image/x-lws"},
+        {".ly", "text/x-lilypond"},
+        {".m", "text/x-matlab"},
+        {".m2t", "video/mpeg"},
+        {".m3u", "audio/x-mpegurl"},
+        {".m3u8", "application/vnd.apple.mpegurl"},
+        {".me", "text/x-troff-me"},
+        {".mkv", "video/x-matroska"},
+        {".ml", "text/x-ocaml"},
+        {".mli", "text/x-ocaml"},
+        {".mm", "text/x-troff-mm"},
+        {".mml", "text/mathml"},
+        {".moc", "text/x-moc"},
+        {".mof", "text/x-mof"},
+        {".mp2", "video/mpeg"},
+        {".mp3", "audio/mpeg"},
+        {".mp4", "video/mp4"},
+        {".mpe", "video/mpeg"},
+        {".mpeg", "video/mpeg"},
+        {".mpg", "video/mpeg"},
+        {".mrl", "text/x-mrml"},
+        {".mrml", "text/x-mrml"},
+        {".mrw", "image/x-minolta-mrw"},
+        {".ms", "text/x-troff-ms"},
+        {".msod", "image/x-msod"},
+        {".mup", "text/x-mup"},
+        {".nef", "image/x-nikon-nef"},
+        {".nfo", "text/x-nfo"},
+        {".not", "text/x-mup"},
+        {".ocl", "text/x-ocl"},
+        {".opml", "text/x-opml+xml"},
+        {".ora", "image/openraster"},
+        {".orf", "image/x-olympus-orf"},
+        {".p", "text/x-pascal"},
+        {".pas", "text/x-pascal"},
+        {".patch", "text/x-patch"},
+        {".pbm", "image/x-portable-bitmap"},
+        {".pcd", "image/x-photo-cd"},
+        {".pcx", "image/x-pcx"},
+        {".pdf", "application/pdf"},
+        {".pef", "image/x-pentax-pef"},
+        {".pgm", "image/x-portable-graymap"},
+        {".pict", "image/x-pict"},
+        {".pict1", "image/x-pict"},
+        {".pict2", "image/x-pict"},
+        {".png", "image/png"},
+        {".pnm", "image/x-portable-anymap"},
+        {".pntg", "image/x-macpaint"},
+        {".po", "text/x-gettext-translation"},
+        {".pot", "text/x-gettext-translation-template"},
+        {".py", "text/x-python"},
+        {".qif", "image/x-quicktime"},
+        {".qtif", "image/x-quicktime"},
+        {".raf", "image/x-fuji-raf"},
+        {".ras", "image/x-cmu-raster"},
+        {".raw", "image/x-panasonic-raw"},
+        {".reg", "text/x-ms-regedit"},
+        {".rgb", "image/x-rgb"},
+        {".rle", "image/rle"},
+        {".roff", "text/troff"},
+        {".rp", "image/vnd.rn-realpix"},
+        {".rt", "text/vnd.rn-realtext"},
+        {".rtx", "text/richtext"},
+        {".scm", "text/x-scheme"},
+        {".sgi", "image/x-sgi"},
+        {".sgm", "text/sgml"},
+        {".sgml", "text/sgml"},
+        {".sk", "image/x-skencil"},
+        {".sk1", "image/x-skencil"},
+        {".slk", "text/spreadsheet"},
+        {".spec", "text/x-rpm-spec"},
+        {".sql", "text/x-sql"},
+        {".sr2", "image/x-sony-sr2"},
+        {".srf", "image/x-sony-srf"},
+        {".ssa", "text/x-ssa"},
+        {".sty", "text/x-tex"},
+        {".sub", "text/x-subviewer"},
+        {".sun", "image/x-sun-raster"},
+        {".svg", "image/svg+xml"},
+        {".svgz", "image/svg+xml-compressed"},
+        {".sylk", "text/spreadsheet"},
+        {".t2t", "text/x-txt2tags"},
+        {".tcl", "text/x-tcl"},
+        {".tex", "text/x-tex"},
+        {".texi", "text/x-texinfo"},
+        {".texinfo", "text/x-texinfo"},
+        {".tga", "image/x-tga"},
+        {".tif", "image/tiff"},
+        {".tiff", "image/tiff"},
+        {".tk", "text/x-tcl"},
+        {".tpic", "image/x-tga"},
+        {".tr", "text/troff"},
+        {".txt", "text/plain"},
+        {".vob", "video/mpeg"},
+        {".voc", "audio/x-voc"},
+        {".wmf", "image/x-wmf"},
+        {".wml", "text/vnd.wap.wml"},
+        {".wmls", "text/vnd.wap.wmlscript"},
+        {".wmv", "video/x-ms-wmv"},
+        {".xhtml", "application/xhtml+xml"},
+        {".xml", "application/xml"}
+        };
+    }
+}

+ 81 - 0
HTEX.Complex/Service/CoreHangfire/VisitSettleJob.cs

@@ -0,0 +1,81 @@
+using Azure.Storage.Blobs.Models;
+using Azure.Storage.Blobs.Specialized;
+using HTEX.Complex.Service.AzureRedis;
+using System.Collections.Concurrent;
+using System.Text;
+using TEAMModelOS.SDK;
+using TEAMModelOS.SDK.DI;
+using static HTEX.Complex.Service.SystemService;
+
+namespace HTEX.Complex.Service.CoreHangfire
+{
+    public class VisitSettleJob
+    {
+        private readonly IConfiguration _configuration;
+        private readonly AzureRedisFactory _azureRedis;
+        private readonly AzureStorageFactory _azureStorage;
+        private readonly DingDing _dingDing;
+        private readonly  IPSearcher _ipSearcher;
+        private readonly Region2LongitudeLatitudeTranslator _longitudeLatitudeTranslator;
+        public VisitSettleJob(Region2LongitudeLatitudeTranslator longitudeLatitudeTranslator,IPSearcher ipSearcher, DingDing dingDing,IConfiguration configuration, AzureRedisFactory azureRedis,   AzureStorageFactory azureStorage)
+        {
+            _azureRedis = azureRedis;
+            _configuration = configuration;
+            _azureStorage = azureStorage;
+            _dingDing=dingDing;
+            _ipSearcher=ipSearcher;
+            _longitudeLatitudeTranslator=longitudeLatitudeTranslator;
+        }
+        public async Task Run()
+        {
+            var gmt8Time = DateTimeOffset.Now.GetGMTTime(8).AddHours(-1);
+            await _dingDing.SendBotMsg($"{DateTimeOffset.Now.GetGMTTime(8):yyyy-MM-dd HH:mm:ss}Http日志访问统计开始,统计时间:{gmt8Time:yyyy-MM-dd HH}", GroupNames.成都开发測試群組);
+            try
+            {
+              //  string location = _configuration.GetValue<string>("Option:Location");
+                //获取上一个小时的数据
+                var appendBlob = _azureStorage.GetBlobContainerClient("0-service-log").GetAppendBlobClient($"http-log/{gmt8Time:yyyy}/{gmt8Time:MM}/{gmt8Time:dd}/{gmt8Time:HH}.log");
+                if (await appendBlob.ExistsAsync())
+                {
+                    BlobDownloadResult result = await appendBlob.DownloadContentAsync();
+                    var content = result.Content.ToString();
+                    content= content.Substring(0, content.Length-2);
+                    if (content.EndsWith("}"))
+                    {
+                        content=$"[{content}]";
+                    }
+                    else
+                    {
+                        content=$"[{content}}}]";
+                    }
+                    var httpLogList = content.ToObject<List<HttpLog>>();
+                    (ConcurrentBag<ApiVisit> vists, ConcurrentBag<(string uuid, HttpLog httpLog, List<string> tmdid, List<string> school)> uuidInfo)   =
+                        await SystemService.ConvertHttpLog(httpLogList, _azureRedis, _ipSearcher, _longitudeLatitudeTranslator, gmt8Time);
+                    if (vists!=null  && vists.Count>0)
+                    {
+                        var appendDayBlob = _azureStorage.GetBlobContainerClient("0-service-log").GetAppendBlobClient($"http-log/{gmt8Time:yyyy}/{gmt8Time:MM}/{gmt8Time:dd}/index.log");
+                        if (!await appendDayBlob.ExistsAsync())
+                        {
+                            await appendDayBlob.CreateAsync();
+                        }
+                        StringBuilder sb = new StringBuilder();
+                        foreach (var item in vists)
+                        {
+                            sb.Append($"{item.ToJsonString()},\n");
+
+                        }
+                        using (var stream = new MemoryStream(Encoding.UTF8.GetBytes($"{sb}")))
+                        {
+                            await appendDayBlob.AppendBlockAsync(stream);
+                        }
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                await _dingDing.SendBotMsg($"日志访问统计异常{ex.Message}\n{ex.StackTrace}", GroupNames.成都开发測試群組);
+            }
+            await _dingDing.SendBotMsg($"{DateTimeOffset.Now.GetGMTTime(8):yyyy-MM-dd HH:mm:ss}Http日志访问统计结束,统计时间:{gmt8Time:yyyy-MM-dd HH}", GroupNames.成都开发測試群組);
+        }
+    }
+}

+ 154 - 0
HTEX.Complex/Service/DingDing.cs

@@ -0,0 +1,154 @@
+using System.ComponentModel;
+using System.Reflection;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace HTEX.Complex.Service
+{
+    public class DingDing
+    {
+        private const string url = "https://oapi.dingtalk.com/robot/send?access_token=";
+        private readonly HttpClient _httpClient;
+
+        public DingDing(HttpClient httpClient)
+        {
+            _httpClient = httpClient;
+        }
+        /// <summary>
+        /// 發送需要加簽驗證的Bot訊息(msgtype為text)
+        /// </summary>
+        /// <param name="robotUrl">釘釘Robot發送Url</param>
+        /// <param name="secret">加簽密鑰</param>
+        /// <param name="msg">發送訊息</param>
+        /// <returns></returns>
+        public async Task SendBotMarkdown(string title, string text, GroupNames groupkey, List<string> mobiles = null)
+        {
+            // TODO 有空處理自動抓取方法名,代碼行數顯示
+            //StackTrace st = new StackTrace(new StackFrame(1, true));
+            //StackFrame sf = st.GetFrame(0);            
+            //var f = $"Func:{sf.GetMethod().Name},Line : {sf.GetFileLineNumber()}";
+            List<string> atMobiles = new List<string>();
+            if (mobiles!=null  && mobiles.Count>0)
+            {
+                atMobiles=mobiles;
+            }
+            var content = new { msgtype = "markdown", markdown = new { title = title, text = text }, at = new { atMobiles } };
+#if DEBUG
+            var keys = GroupNames.成都开发測試群組.GetDescriptionText().Split(',');
+#else
+            var keys = groupkey.GetDescriptionText().Split(',');
+#endif
+            if (keys.Length == 1) await _httpClient.PostAsJsonAsync($"{url}{keys[0]}", content);
+            else
+            {
+                var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+                await _httpClient.PostAsJsonAsync($"{url}{keys[0]}&timestamp={timestamp}&sign={BotAddSign(keys[1], timestamp)}", content);
+            }
+        }
+        /// <summary>
+        /// 發送需要加簽驗證的Bot訊息(msgtype為text)
+        /// </summary>
+        /// <param name="robotUrl">釘釘Robot發送Url</param>
+        /// <param name="secret">加簽密鑰</param>
+        /// <param name="msg">發送訊息</param>
+        /// <returns></returns>
+        public async Task SendBotMsg(string msg, GroupNames groupkey, List<string> mobiles = null)
+        {
+            // TODO 有空處理自動抓取方法名,代碼行數顯示
+            //StackTrace st = new StackTrace(new StackFrame(1, true));
+            //StackFrame sf = st.GetFrame(0);            
+            //var f = $"Func:{sf.GetMethod().Name},Line : {sf.GetFileLineNumber()}";
+            List<string> atMobiles = new List<string>();
+            if (mobiles != null && mobiles.Count > 0)
+            {
+                atMobiles=mobiles;
+            }
+            var content = new { msgtype = "text", text = new { content = msg }, at = new { atMobiles } };
+
+            var keys = groupkey.GetDescriptionText().Split(',');
+            if (keys.Length == 1) await _httpClient.PostAsJsonAsync($"{url}{keys[0]}", content);
+            else
+            {
+                var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+                await _httpClient.PostAsJsonAsync($"{url}{keys[0]}&timestamp={timestamp}&sign={BotAddSign(keys[1], timestamp)}", content);
+            }
+        }
+        // <summary>
+        /// 發送需要加簽驗證的Bot訊息(msgtype為text)
+        /// </summary>
+        /// <param name="robotUrl">釘釘Robot發送Url</param>
+        /// <param name="secret">加簽密鑰</param>
+        /// <param name="msg">發送訊息</param>
+        /// <returns></returns>
+        public async Task SendBotMsg(string msg, string accesstoken, string secret = null)
+        {
+            var content = new { msgtype = "text", text = new { content = msg } };
+            if (string.IsNullOrWhiteSpace(secret))
+                await _httpClient.PostAsJsonAsync($"{url}{accesstoken}", content);
+            else
+            {
+                var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+                await _httpClient.PostAsJsonAsync($"{url}{accesstoken}&timestamp={timestamp}&sign={BotAddSign(secret, timestamp)}", content);
+            }
+
+
+        }
+
+        #region private
+        /// <summary>
+        /// 釘釘Bot簽名生成方法
+        /// </summary>
+        /// <param name="zTime"></param>
+        /// <returns></returns>
+        private static string BotAddSign(string secret, long zTime)
+        {
+            //"SEC6a6822db6567f79854a474002407cd9ac36da4d194b5fb79e073c35ef61119ed";
+            string stringToSign = zTime + "\n" + secret;
+            var encoding = new System.Text.ASCIIEncoding();
+            byte[] keyByte = encoding.GetBytes(secret);
+            byte[] messageBytes = encoding.GetBytes(stringToSign);
+            using (var hmacsha256 = new HMACSHA256(keyByte))
+            {
+                byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
+                return System.Web.HttpUtility.UrlEncode(Convert.ToBase64String(hashmessage), Encoding.UTF8);
+            }
+        }
+        #endregion
+    }
+    public static class EnumExtensions
+    {
+        /// <summary>
+        /// 取得Enum描述值
+        /// </summary>       
+        public static string GetDescriptionText(this Enum source)
+        {
+            FieldInfo? fi = source.GetType().GetField(source.ToString());
+            DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
+            if (attributes.Length > 0) return attributes[0].Description;
+            else return source.ToString();
+        }
+    }
+    public enum GroupNames
+    {
+        [Description("ce63217d9c734a92fd91c7c9ceaa9b25e109cce94615a7f75288dc43865a6e20,SEC6a6822db6567f79854a474002407cd9ac36da4d194b5fb79e073c35ef61119ed")]
+        研發C組,
+        [Description("2c5ced0725b592b1a96f1fb800d6d9a15727986ef75923a838a4d24e6b8c9147,SEC66ff954306c1fd98b5b160e23c253649f22205e69a16fb26e18d136f49875a9e")]
+        客戶回饋群組,
+        [Description("f8bf19c9363e3b288e018856014bcbf89708f19b3aae009e203edd68af25c9fe")]
+        BB訂單群組,
+        [Description("a27b099b15a054374da41b9f66f72e5fc6b378e98418859f7c0ef46408941808")]
+        台北開發測試群組,
+        [Description("82aba2fccfa8442c575db3ac0442fa5aea90cd7574bb9a71d5abf210ea72a3aa,SEC3f38eca87cd4fd10505d136f91071a2e8b14cd863bd6bbafb24c612fc751a59a")]
+        成都开发測試群組,
+        [Description("1a316ce4edc2db88231d40d80072b00f2751d7d9e2e5871c5dc061885b01c48d,SECff60201ac9b219943b9f8fc397fda1a617d0cbc140850f5ea9cb4f131479d39a")]
+        醍摩豆服務運維群組,
+        [Description("a83ea4ead63bf1b4e087723b3a7ccdf7f4c96708a22493f489bb928999f50d87,SECf1d22db7d00580dc7c0e597e31112a25ae1025500fc998b5b30961d91e115271")]
+        國際客戶聯繫通知群,
+        [Description("b1293e05c6aaeece746a2e46d69164a4373bab071bfafb5d7b5141f947e493cb,SEC658c7c70204f18976fa5e02f554d4fc72e9892c9bb82c5c6b98ecfd3c4eb0531")]
+        大陸客戶聯繫通知群,
+        [Description("feac70431e5b3cf68c621c5397ca62c573499275557335f0d140e05c3e2437fa,SECb365526c7d7a3fd230f8f1c8ee82b869db9a3fa81d035233b5350028d19c527b")]
+        醍摩豆小财神,
+        [Description("8e3c5efdf8ad02eb44584dfd08ee66cfa9d31860af51670df69fc9d7bb55f3bb,SECb1267571406d89d2ac451a11495be417d3cbfe2b04ed7f1742d28e1a1d098c9d")]
+        高飛
+    }
+}

+ 437 - 0
HTEX.Complex/Service/IP2Region/IPSearcher.cs

@@ -0,0 +1,437 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using TEAMModelOS.SDK.IP2Region;
+
+namespace TEAMModelOS.SDK
+{
+    public class IPSearcher : IDisposable
+    {
+        const int BTREE_ALGORITHM = 1;
+        const int BINARY_ALGORITHM = 2;
+        const int MEMORY_ALGORITYM = 3;
+
+        private IPConfig _dbConfig = null;
+
+        /// <summary>
+        /// db file access handler
+        /// </summary>
+        private Stream _raf = null;
+
+        /// <summary>
+        /// header blocks buffer
+        /// </summary>
+        private long[] _headerSip = null;
+        private int[] _headerPtr = null;
+        private int _headerLength;
+
+        /// <summary>
+        /// super blocks info 
+        /// </summary>
+        private long _firstIndexPtr = 0;
+        private long _lastIndexPtr = 0;
+        private int _totalIndexBlocks = 0;
+
+        /// <summary>
+        /// for memory mode
+        /// the original db binary string
+        /// </summary>
+        private byte[] _dbBinStr = null;
+
+        /// <summary>
+        /// Get by index ptr.
+        /// </summary>
+        private DataBlock GetByIndexPtr(long ptr)
+        {
+            _raf.Seek(ptr, SeekOrigin.Begin);
+            byte[] buffer = new byte[12];
+            _raf.Read(buffer, 0, buffer.Length);
+            long extra = Utils.GetIntLong(buffer, 8);
+            int dataLen = (int)((extra >> 24) & 0xFF);
+            int dataPtr = (int)((extra & 0x00FFFFFF));
+            _raf.Seek(dataPtr, SeekOrigin.Begin);
+            byte[] data = new byte[dataLen];
+            _raf.Read(data, 0, data.Length);
+            int city_id = (int)Utils.GetIntLong(data, 0);
+            string region = Encoding.UTF8.GetString(data, 4, data.Length - 4);
+            return new DataBlock(city_id, region, dataPtr);
+        }
+
+        public IPSearcher(IPConfig dbConfig, string dbFile)
+        {
+            if (_dbConfig == null)
+            {
+                _dbConfig = dbConfig;
+            }
+            try {
+                _raf = new FileStream(dbFile, FileMode.Open, FileAccess.Read, FileShare.Read);
+            } catch { }
+        }
+
+        public IPSearcher(string dbFile) : this(null, dbFile) { }
+
+        public IPSearcher(IPConfig dbConfig, Stream dbFileStream)
+        {
+            if (_dbConfig == null)
+            {
+                _dbConfig = dbConfig;
+            }
+
+            _raf = dbFileStream;
+        }
+
+        public IPSearcher(Stream dbFileStream) : this(null, dbFileStream) { }
+
+        #region Sync Methods
+        /// <summary>
+        /// Get the region with a int ip address with memory binary search algorithm.
+        /// </summary>
+        private DataBlock MemorySearch(long ip)
+        {
+            int blen = IndexBlock.LENGTH;
+            if (_dbBinStr == null)
+            {
+                if (_raf == null) { return null; }
+                try { _dbBinStr = new byte[(int)_raf.Length]; } catch (Exception) { return null; }
+                _raf.Seek(0L, SeekOrigin.Begin);
+                _raf.Read(_dbBinStr, 0, _dbBinStr.Length);
+
+                //initialize the global vars
+                _firstIndexPtr = Utils.GetIntLong(_dbBinStr, 0);
+                _lastIndexPtr = Utils.GetIntLong(_dbBinStr, 4);
+                _totalIndexBlocks = (int)((_lastIndexPtr - _firstIndexPtr) / blen) + 1;
+            }
+
+            //search the index blocks to define the data
+            int l = 0, h = _totalIndexBlocks;
+            long sip = 0;
+
+            while (l <= h)
+            {
+                int m = (l + h) >> 1;
+                int p = (int)(_firstIndexPtr + m * blen);
+
+                sip = Utils.GetIntLong(_dbBinStr, p);
+
+                if (ip < sip)
+                {
+                    h = m - 1;
+                }
+                else
+                {
+                    sip = Utils.GetIntLong(_dbBinStr, p + 4);
+                    if (ip > sip)
+                    {
+                        l = m + 1;
+                    }
+                    else
+                    {
+                        sip = Utils.GetIntLong(_dbBinStr, p + 8);
+                        break;
+                    }
+                }
+            }
+
+            //not matched
+            if (sip == 0) return null;
+
+            //get the data
+            int dataLen = (int)((sip >> 24) & 0xFF);
+            int dataPtr = (int)((sip & 0x00FFFFFF));
+            int city_id = (int)Utils.GetIntLong(_dbBinStr, dataPtr);
+            string region = Encoding.UTF8.GetString(_dbBinStr, dataPtr + 4, dataLen - 4);//new String(dbBinStr, dataPtr + 4, dataLen - 4, Encoding.UTF8);
+
+            return new DataBlock(city_id, region, dataPtr);
+        }
+
+        /// <summary>
+        /// Get the region throught the ip address with memory binary search algorithm.
+        /// </summary>
+        public DataBlock MemorySearch(string ip)
+        {
+            return MemorySearch(Utils.Ip2long(ip));
+        }
+
+        /// <summary>
+        /// Get the region with a int ip address with b-tree algorithm.
+        /// </summary>
+        private DataBlock BtreeSearch(long ip)
+        {
+            //check and load the header
+            if (_headerSip == null)
+            {
+                _raf.Seek(8L, SeekOrigin.Begin);    //pass the super block
+                byte[] b = new byte[4096];
+                _raf.Read(b, 0, b.Length);
+                //fill the header
+                int len = b.Length >> 3, idx = 0;  //b.lenght / 8
+                _headerSip = new long[len];
+                _headerPtr = new int[len];
+                long startIp, dataPtrTemp;
+                for (int i = 0; i < b.Length; i += 8)
+                {
+                    startIp = Utils.GetIntLong(b, i);
+                    dataPtrTemp = Utils.GetIntLong(b, i + 4);
+                    if (dataPtrTemp == 0) break;
+
+                    _headerSip[idx] = startIp;
+                    _headerPtr[idx] = (int)dataPtrTemp;
+                    idx++;
+                }
+                _headerLength = idx;
+            }
+
+            //1. define the index block with the binary search
+            if (ip == _headerSip[0])
+            {
+                return GetByIndexPtr(_headerPtr[0]);
+            }
+            else if (ip == _headerPtr[_headerLength - 1])
+            {
+                return GetByIndexPtr(_headerPtr[_headerLength - 1]);
+            }
+            int l = 0, h = _headerLength, sptr = 0, eptr = 0;
+            int m = 0;
+            while (l <= h)
+            {
+                m = (l + h) >> 1;
+                //perfectly matched, just return it
+                if (ip == _headerSip[m])
+                {
+                    if (m > 0)
+                    {
+                        sptr = _headerPtr[m - 1];
+                        eptr = _headerPtr[m];
+                    }
+                    else
+                    {
+                        sptr = _headerPtr[m];
+                        eptr = _headerPtr[m + 1];
+                    }
+                }
+                //less then the middle value
+                else if (ip < _headerSip[m])
+                {
+                    if (m == 0)
+                    {
+                        sptr = _headerPtr[m];
+                        eptr = _headerPtr[m + 1];
+                        break;
+                    }
+                    else if (ip > _headerSip[m - 1])
+                    {
+                        sptr = _headerPtr[m - 1];
+                        eptr = _headerPtr[m];
+                        break;
+                    }
+                    h = m - 1;
+                }
+                else
+                {
+                    if (m == _headerLength - 1)
+                    {
+                        sptr = _headerPtr[m - 1];
+                        eptr = _headerPtr[m];
+                        break;
+                    }
+                    else if (ip <= _headerSip[m + 1])
+                    {
+                        sptr = _headerPtr[m];
+                        eptr = _headerPtr[m + 1];
+                        break;
+                    }
+                    l = m + 1;
+                }
+            }
+            //match nothing just stop it
+            if (sptr == 0) return null;
+            //2. search the index blocks to define the data
+            int blockLen = eptr - sptr, blen = IndexBlock.LENGTH;
+            byte[] iBuffer = new byte[blockLen + blen];    //include the right border block
+            _raf.Seek(sptr, SeekOrigin.Begin);
+            _raf.Read(iBuffer, 0, iBuffer.Length);
+            l = 0; h = blockLen / blen;
+            long sip = 0;
+            int p = 0;
+            while (l <= h)
+            {
+                m = (l + h) >> 1;
+                p = m * blen;
+                sip = Utils.GetIntLong(iBuffer, p);
+                if (ip < sip)
+                {
+                    h = m - 1;
+                }
+                else
+                {
+                    sip = Utils.GetIntLong(iBuffer, p + 4);
+                    if (ip > sip)
+                    {
+                        l = m + 1;
+                    }
+                    else
+                    {
+                        sip = Utils.GetIntLong(iBuffer, p + 8);
+                        break;
+                    }
+                }
+            }
+            //not matched
+            if (sip == 0) return null;
+            //3. get the data
+            int dataLen = (int)((sip >> 24) & 0xFF);
+            int dataPtr = (int)((sip & 0x00FFFFFF));
+            _raf.Seek(dataPtr, SeekOrigin.Begin);
+            byte[] data = new byte[dataLen];
+            _raf.Read(data, 0, data.Length);
+            int city_id = (int)Utils.GetIntLong(data, 0);
+            String region = Encoding.UTF8.GetString(data, 4, data.Length - 4);// new String(data, 4, data.Length - 4, "UTF-8");
+            return new DataBlock(city_id, region, dataPtr);
+        }
+
+        /// <summary>
+        /// Get the region throught the ip address with b-tree search algorithm.
+        /// </summary>
+        public DataBlock BtreeSearch(string ip)
+        {
+            return BtreeSearch(Utils.Ip2long(ip));
+        }
+
+        /// <summary>
+        /// Get the region with a int ip address with binary search algorithm.
+        /// </summary>
+        private DataBlock BinarySearch(long ip)
+        {
+            int blen = IndexBlock.LENGTH;
+            if (_totalIndexBlocks == 0)
+            {
+                _raf.Seek(0L, SeekOrigin.Begin);
+                byte[] superBytes = new byte[8];
+                _raf.Read(superBytes, 0, superBytes.Length);
+                //initialize the global vars
+                _firstIndexPtr = Utils.GetIntLong(superBytes, 0);
+                _lastIndexPtr = Utils.GetIntLong(superBytes, 4);
+                _totalIndexBlocks = (int)((_lastIndexPtr - _firstIndexPtr) / blen) + 1;
+            }
+
+            //search the index blocks to define the data
+            int l = 0, h = _totalIndexBlocks;
+            byte[] buffer = new byte[blen];
+            long sip = 0;
+            while (l <= h)
+            {
+                int m = (l + h) >> 1;
+                _raf.Seek(_firstIndexPtr + m * blen, SeekOrigin.Begin);    //set the file pointer
+                _raf.Read(buffer, 0, buffer.Length);
+                sip = Utils.GetIntLong(buffer, 0);
+                if (ip < sip)
+                {
+                    h = m - 1;
+                }
+                else
+                {
+                    sip = Utils.GetIntLong(buffer, 4);
+                    if (ip > sip)
+                    {
+                        l = m + 1;
+                    }
+                    else
+                    {
+                        sip = Utils.GetIntLong(buffer, 8);
+                        break;
+                    }
+                }
+            }
+            //not matched
+            if (sip == 0) return null;
+            //get the data
+            int dataLen = (int)((sip >> 24) & 0xFF);
+            int dataPtr = (int)((sip & 0x00FFFFFF));
+            _raf.Seek(dataPtr, SeekOrigin.Begin);
+            byte[] data = new byte[dataLen];
+            _raf.Read(data, 0, data.Length);
+            int city_id = (int)Utils.GetIntLong(data, 0);
+            String region = Encoding.UTF8.GetString(data, 4, data.Length - 4);//new String(data, 4, data.Length - 4, "UTF-8");
+            return new DataBlock(city_id, region, dataPtr);
+        }
+
+        /// <summary>
+        /// Get the region throught the ip address with binary search algorithm.
+        /// </summary>
+        public DataBlock BinarySearch(String ip)
+        {
+            return BinarySearch(Utils.Ip2long(ip));
+        }
+        #endregion
+
+        #region Async Methods
+        /// <summary>
+        /// Get the region throught the ip address with memory binary search algorithm.
+        /// </summary>
+        public Task<DataBlock> MemorySearchAsync(string ip)
+        {
+            return Task.FromResult(MemorySearch(ip));
+        }
+        public async Task<string> SearchIpAsync( string ip)
+        {
+            if (ip.Contains("::"))
+            {
+                ip = "127.0.0.1";
+            }
+            try
+            {
+                DataBlock block = await MemorySearchAsync(ip);
+                if (block != null)
+                {
+                    string region = block.Region.Replace("0|0|0|0|", "").Replace("0|0|0|", "").Replace("|0|0|0|0", "").Replace("|0|0|0|", "").Replace("|0|0|0", "").Replace("|0|0|", "").Replace("|0|0", "").Replace("|0|", "·").Replace("|0", "").Replace("|", "·");
+                    //if (!string.IsNullOrWhiteSpace(region))
+                   // {
+                       // region = region.Replace("中国·", "").Replace("中国", "").Replace("台湾省", "台湾");
+                   // }
+                    return region;
+                }
+                else { return null; }
+               
+            }
+            catch (IPInValidException)
+            {
+                return "IP Illigel.";
+            }
+            catch (Exception) {
+                return null;
+            }
+        }
+        /// <summary>
+        /// Get the region throught the ip address with b-tree search algorithm.
+        /// </summary>
+        public Task<DataBlock> BtreeSearchAsync(string ip)
+        {
+            return Task.FromResult(BtreeSearch(ip));
+        }
+        /// <summary>
+        /// Get the region throught the ip address with binary search algorithm.
+        /// </summary>
+        public Task<DataBlock> BinarySearchAsync(string ip)
+        {
+            return Task.FromResult(BinarySearch(ip));
+        }
+        #endregion
+
+        /// <summary>
+        /// Close the db.
+        /// </summary>
+        public void Close()
+        {
+            _headerSip = null;
+            _headerPtr = null;
+            _dbBinStr = null;
+            _raf.Close();
+        }
+
+        public void Dispose()
+        {
+            Close();
+        }
+    }
+}

+ 22 - 0
HTEX.Complex/Service/IP2Region/IPSearcherExtensions.cs

@@ -0,0 +1,22 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TEAMModelOS.SDK
+{
+    public static class IPSearcherExtensions
+    {
+        public static IServiceCollection AddIPSearcher(this IServiceCollection services, string path, string name = "Default")
+        {
+            if (services == null) throw new ArgumentNullException(nameof(services));
+            if (path == null) throw new ArgumentNullException(nameof(path));
+            services.TryAddSingleton(new IPSearcher(Path.Combine(path, "ip2region.db")));
+            return services;
+        }
+    }
+}

+ 46 - 0
HTEX.Complex/Service/IP2Region/Models/DataBlock.cs

@@ -0,0 +1,46 @@
+ 
+namespace TEAMModelOS.SDK
+{
+    public class DataBlock
+    {
+        #region Private Properties
+        public int CityID
+        {
+            get;
+            private set;
+        }
+
+        public string Region
+        {
+            get;
+            private set;
+        }
+
+        public int DataPtr
+        {
+            get;
+            private set;
+        }
+        #endregion
+
+        #region Constructor
+        public DataBlock(int city_id, string region, int dataPtr = 0)
+        {
+            CityID = city_id;
+            Region = region;
+            DataPtr = dataPtr;
+        }
+
+        public DataBlock(int city_id, string region):this(city_id,region,0)
+        {
+        }
+        #endregion
+
+        public override string ToString()
+        {
+            return $"{CityID}|{Region}|{DataPtr}";
+        }
+
+    }
+
+}

+ 45 - 0
HTEX.Complex/Service/IP2Region/Models/HeaderBlock.cs

@@ -0,0 +1,45 @@
+using TEAMModelOS.SDK.IP2Region;
+
+namespace TEAMModelOS.SDK
+{
+    internal class HeaderBlock
+    {
+        public long IndexStartIp
+        {
+            get;
+            private set;
+        }
+
+        public int IndexPtr
+        {
+            get;
+            private set;
+        }
+
+        public HeaderBlock(long indexStartIp, int indexPtr)
+        {
+            IndexStartIp = indexStartIp;
+            IndexPtr = indexPtr;
+        }
+
+        /// <summary>
+        /// Get the bytes for total storage
+        /// </summary>
+        /// <returns>
+        /// Bytes gotten.
+        /// </returns>
+        public byte[] GetBytes()
+        {
+            /*
+             * +------------+-----------+
+             * | 4bytes     | 4bytes    |
+             * +------------+-----------+
+             *  start ip      index ptr
+            */
+            byte[] b = new byte[8];
+            Utils.WriteIntLong(b, 0, IndexStartIp);
+            Utils.WriteIntLong(b, 4, IndexPtr);
+            return b;
+        }
+    }
+}

+ 45 - 0
HTEX.Complex/Service/IP2Region/Models/IPConfig.cs

@@ -0,0 +1,45 @@
+ 
+using System;
+
+namespace TEAMModelOS.SDK
+{
+    public class DbMakerConfigException : Exception
+    {
+        public string ErrMsg { get; private set; }
+        public DbMakerConfigException(string errMsg)
+        {
+            ErrMsg = errMsg;
+        }
+    }
+
+    public class IPConfig
+    {
+        public int TotalHeaderSize
+        {
+            get;
+            private set;
+        }
+
+        public int indexBlockSize
+        {
+            get;
+            private set;
+        }
+
+        public IPConfig(int totalHeaderSize)
+        {
+            if ((totalHeaderSize % 8) != 0)
+            {
+                throw new DbMakerConfigException("totalHeaderSize must be times of 8");
+            }
+            TotalHeaderSize = totalHeaderSize;
+            //4 * 2048
+            indexBlockSize = 8192; 
+        }
+
+        public IPConfig():this(8 * 2048)
+        {
+        }
+    }
+
+}

+ 63 - 0
HTEX.Complex/Service/IP2Region/Models/IndexBlock.cs

@@ -0,0 +1,63 @@
+
+
+using TEAMModelOS.SDK.IP2Region;
+
+namespace TEAMModelOS.SDK
+{
+    internal class IndexBlock
+    {
+        public const int LENGTH = 12;
+
+        public long StartIP
+        {
+            get;
+            private set;
+        }
+
+        public long EndIp
+        {
+            get;
+            private set;
+        }
+
+        public uint DataPtr
+        {
+            get;
+            private set;
+        }
+
+        public int DataLen
+        {
+            get;
+            private set;
+        }
+
+        public IndexBlock(long startIp, long endIp, uint dataPtr, int dataLen)
+        {
+            StartIP = startIp;
+            EndIp = endIp;
+            DataPtr = dataPtr;
+            DataLen = dataLen;
+        }
+
+        public byte[] GetBytes()
+        {
+            /*
+             * +------------+-----------+-----------+
+             * | 4bytes     | 4bytes    | 4bytes    |
+             * +------------+-----------+-----------+
+             *  start ip      end ip      data ptr + len 
+            */
+            byte[] b = new byte[12];
+
+            Utils.WriteIntLong(b, 0, StartIP);    //start ip
+            Utils.WriteIntLong(b, 4, EndIp);        //end ip
+
+            //write the data ptr and the length
+            long mix = DataPtr | ((DataLen << 24) & 0xFF000000L);
+            Utils.WriteIntLong(b, 8, mix);
+
+            return b;
+        }
+    }
+}

+ 117 - 0
HTEX.Complex/Service/IP2Region/Utils.cs

@@ -0,0 +1,117 @@
+ 
+using System;
+
+namespace TEAMModelOS.SDK.IP2Region
+{
+    public class IPInValidException : Exception
+    {
+        const string ERROR_MSG = "IP Illigel. Please input a valid IP.";
+        public IPInValidException() : base(ERROR_MSG) { }
+    }
+    internal static class Utils
+    {
+        /// <summary>
+        /// Write specfield bytes to a byte array start from offset.
+        /// </summary>
+        public static void Write(byte[] b, int offset, ulong v, int bytes)
+        {
+            for (int i = 0; i < bytes; i++)
+            {
+                b[offset++] = (byte)((v >> (8 * i)) & 0xFF);
+            }
+        }
+
+        /// <summary>
+        /// Write a int to a byte array.
+        /// </summary>
+        public static void WriteIntLong(byte[] b, int offset, long v)
+        {
+            b[offset++] = (byte)((v >> 0) & 0xFF);
+            b[offset++] = (byte)((v >> 8) & 0xFF);
+            b[offset++] = (byte)((v >> 16) & 0xFF);
+            b[offset] = (byte)((v >> 24) & 0xFF);
+        }
+
+        /// <summary>
+        /// Get a int from a byte array start from the specifiled offset.
+        /// </summary>
+        public static long GetIntLong(byte[] b, int offset)
+        {
+            return (
+                ((b[offset++] & 0x000000FFL)) |
+                ((b[offset++] << 8) & 0x0000FF00L) |
+                ((b[offset++] << 16) & 0x00FF0000L) |
+                ((b[offset] << 24) & 0xFF000000L)
+            );
+        }
+
+        /// <summary>
+        /// Get a int from a byte array start from the specifield offset.
+        /// </summary>
+        public static int GetInt3(byte[] b, int offset)
+        {
+            return (
+                (b[offset++] & 0x000000FF) |
+                (b[offset++] & 0x0000FF00) |
+                (b[offset] & 0x00FF0000)
+            );
+        }
+
+        public static int GetInt2(byte[] b, int offset)
+        {
+            return (
+                (b[offset++] & 0x000000FF) |
+                (b[offset] & 0x0000FF00)
+            );
+        }
+
+        public static int GetInt1(byte[] b, int offset)
+        {
+            return (
+                (b[offset] & 0x000000FF)
+            );
+        }
+        /// <summary>
+        /// String ip to long ip.
+        /// </summary>
+        public static long Ip2long(string ip)
+        {
+            string[] p = ip.Split('.');
+            if (p.Length != 4) throw new IPInValidException();
+
+            foreach (string pp in p)
+            {
+                if (pp.Length > 3) throw new IPInValidException();
+                if (!int.TryParse(pp, out int value) || value > 255)
+                {
+                    throw new IPInValidException();
+                }
+            }
+            var bip1 = long.TryParse(p[0], out long ip1);
+            var bip2 = long.TryParse(p[1], out long ip2);
+            var bip3 = long.TryParse(p[2], out long ip3);
+            var bip4 = long.TryParse(p[3], out long ip4);
+
+            if (!bip1 || !bip2 || !bip3 || !bip4
+                || ip4 > 255 || ip1 > 255 || ip2 > 255 || ip3 > 255
+                || ip4 < 0 || ip1 < 0 || ip2 < 0 || ip3 < 0)
+            {
+                throw new IPInValidException();
+            }
+            long p1 = ((ip1 << 24) & 0xFF000000);
+            long p2 = ((ip2 << 16) & 0x00FF0000);
+            long p3 = ((ip3 << 8) & 0x0000FF00);
+            long p4 = ((ip4 << 0) & 0x000000FF);
+            return ((p1 | p2 | p3 | p4) & 0xFFFFFFFFL);
+        }
+
+        /// <summary>
+        /// Int to ip string.
+        /// </summary>
+        public static string Long2ip(long ip)
+        {
+            return $"{(ip >> 24) & 0xFF}.{(ip >> 16) & 0xFF}.{(ip >> 8) & 0xFF}.{ip & 0xFF}";
+        }
+    }
+
+}

+ 147 - 0
HTEX.Complex/Service/MLService.cs

@@ -0,0 +1,147 @@
+
+using Microsoft.Azure.Amqp.Framing;
+using Microsoft.ML;
+using Microsoft.ML.Data;
+
+namespace HTEX.Complex.Service
+{
+
+    public static class MLService
+    {
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="datas">数据需要去掉0</param>
+        /// <param name="numberOfClusters"></param>
+        /// <returns></returns>
+        public static List<ClusterData> KMeans(double[] datas, int numberOfClusters = 5)
+        {
+            List<DataPoint> data = new List<DataPoint>();
+            foreach (var d in datas)
+            {
+                data.Add(new DataPoint { Feature = d });
+            }
+
+            // 定义数据视图  
+            var mlContext = new MLContext();
+            var dataView = mlContext.Data.LoadFromEnumerable(data);
+            // 定义聚类管道  
+            var pipeline = mlContext.Transforms.Concatenate("Features", new[] { "Feature" })
+                                .Append(mlContext.Clustering.Trainers.KMeans(numberOfClusters: numberOfClusters)); // 假设我们想要将数据分成3个集群  
+                                                                                                                   // 训练模型  
+            var model = pipeline.Fit(dataView);
+
+            // 转换数据以获取聚类结果  
+            var predictions = model.Transform(dataView);
+
+            // 提取聚类结果  
+            var inMemoryCollection = mlContext.Data.CreateEnumerable<ClusterPrediction>(predictions, reuseRowObject: false);
+
+
+
+            // 打印聚类结果  
+            //var clusterSizes = new int[3]; // 假设有3个聚类  
+            int index = 0;
+            List<ClusterData> clusterDatas = new List<ClusterData>();
+            foreach (var prediction in inMemoryCollection)
+            {
+                //Console.WriteLine($"Data point: {data[index].Feature}, Cluster: {prediction.ClusterId}");
+                var clusterData = clusterDatas.Find(x => x.ClusterId.Equals(prediction.ClusterId));
+                if (clusterData!=null)
+                {
+                    clusterData.count +=1;
+                    clusterData.datas.Add(data[index].Feature);
+                    clusterData.avg=clusterData.datas.Sum() / clusterData.datas.Count();
+                }
+                else
+                {
+                    clusterDatas.Add(new ClusterData { avg=data[index].Feature, count=1, ClusterId=prediction.ClusterId, datas=new List<double> { data[index].Feature } });
+                }
+                index++;
+                //计算每个聚类的数据点数量
+                //clusterSizes[prediction.ClusterId-1]++;
+            }
+            // 预测聚类
+
+            // 确定最密集的部分  
+            // 这通常需要对聚类结果进行分析,比如计算每个聚类的平均距离、大小等  
+            // 在这里,你可以通过比较不同聚类的数据点数量或计算聚类中心周围的密度来估计哪个是最密集的  
+
+
+
+
+            // 找出最大的聚类  
+            //  var maxClusterIndex = clusterSizes.ToList().IndexOf(clusterSizes.Max());
+            //Console.WriteLine($"The densest cluster is cluster {maxClusterIndex} with {clusterSizes[maxClusterIndex]} data points.");
+
+            // 你还可以进一步分析聚类的特性,比如找出聚类中心、计算聚类内的方差等  
+
+            return clusterDatas;
+        }
+       /// <summary>
+       /// 
+       /// </summary>
+       /// <param name="datas"></param>
+       /// <param name="numberOfClusters"></param>
+       /// <param name="dropPercent">最大平均数的聚类与数量最多的聚类数量的落差小于30% 则以更高的为准</param>
+       /// <returns></returns>
+        public static ClusterData GetNormalCluster (double[] datas, int numberOfClusters = 5,double dropPercent=0.3)
+        {
+            List<ClusterData> clusterDatas = KMeans(datas, numberOfClusters);
+            clusterDatas=clusterDatas.OrderByDescending(dr => dr.count).ToList();
+            ClusterData clusterData = FindSatisfactoryRecord(clusterDatas, 0, dropPercent);
+            return clusterData;
+        }
+
+        static ClusterData FindSatisfactoryRecord(List<ClusterData> data, int currentIndex,double dropPercent)
+        {        // 如果当前索引小于0,说明已经到达列表开头,返回null
+            if (currentIndex < 0) { return null; }
+
+            // 获取当前数据
+            ClusterData current = data.ElementAt(currentIndex);
+            if (currentIndex+1>=data.Count())
+            {
+                return current;
+            }
+            else
+            {
+                ClusterData next = data.ElementAt(currentIndex +1);        // 检查平均值和人数差是否满足条件
+                if (current.avg > next.avg)
+                {
+                    return current;
+                }
+                else
+                {
+                    if ((current.count- next.count)/current.count>=dropPercent)
+                    {
+                        return current;
+                    }
+                    else
+                    { // 递归调用,继续向前比较
+                        return FindSatisfactoryRecord(data, currentIndex + 1, dropPercent);
+                    }
+                }
+            }
+        }
+    }
+    // 定义数据模型  
+    public class DataPoint
+    {
+        public double Feature { get; set; }
+    }
+    // 聚类预测类  
+    public class ClusterPrediction
+    {
+        [ColumnName("PredictedLabel")]
+        public uint ClusterId;
+
+        // 你可以添加其他预测列,比如距离聚类中心的距离等  
+    }
+    public class ClusterData
+    {
+        public List<double> datas = new List<double>();
+        public uint ClusterId { get; set; }
+        public int count { get; set; }
+        public double avg { get; set; }
+    }
+}

+ 25 - 0
HTEX.Complex/Service/Region2LongitudeLatitudeTranslator.cs

@@ -0,0 +1,25 @@
+using Newtonsoft.Json.Linq;
+using System.Text;
+
+namespace HTEX.Complex.Service
+{
+    public class Region2LongitudeLatitudeTranslator
+    {
+
+        public readonly JArray regionJson;
+        public Region2LongitudeLatitudeTranslator(string configPath)
+        {
+            if (configPath == null) throw new ArgumentNullException(nameof(configPath));
+            StreamReader streamReader = new StreamReader(new FileStream(Path.Combine(configPath, "latlng.json"), FileMode.Open, FileAccess.Read, FileShare.ReadWrite), Encoding.UTF8);
+            StringBuilder stringBuilder = new StringBuilder();
+            string text;
+            while ((text = streamReader.ReadLine()) != null)
+            {
+                stringBuilder.Append(text.ToString());
+            }
+            streamReader.Close();
+            string input = stringBuilder.ToString();
+            regionJson=  JArray.Parse(input);
+        }
+    }
+}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 3257 - 0
HTEX.Complex/Service/SystemService.cs


+ 8 - 0
HTEX.Complex/Views/Home/Index.cshtml

@@ -0,0 +1,8 @@
+@{
+    ViewData["Title"] = "Home Page";
+}
+
+<div class="text-center">
+    <h1 class="display-4">Welcome</h1>
+    <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
+</div>

+ 6 - 0
HTEX.Complex/Views/Home/Privacy.cshtml

@@ -0,0 +1,6 @@
+@{
+    ViewData["Title"] = "Privacy Policy";
+}
+<h1>@ViewData["Title"]</h1>
+
+<p>Use this page to detail your site's privacy policy.</p>

+ 25 - 0
HTEX.Complex/Views/Shared/Error.cshtml

@@ -0,0 +1,25 @@
+@model ErrorViewModel
+@{
+    ViewData["Title"] = "Error";
+}
+
+<h1 class="text-danger">Error.</h1>
+<h2 class="text-danger">An error occurred while processing your request.</h2>
+
+@if (Model.ShowRequestId)
+{
+    <p>
+        <strong>Request ID:</strong> <code>@Model.RequestId</code>
+    </p>
+}
+
+<h3>Development Mode</h3>
+<p>
+    Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
+</p>
+<p>
+    <strong>The Development environment shouldn't be enabled for deployed applications.</strong>
+    It can result in displaying sensitive information from exceptions to end users.
+    For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
+    and restarting the app.
+</p>

+ 49 - 0
HTEX.Complex/Views/Shared/_Layout.cshtml

@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>@ViewData["Title"] - HTEX.Complex</title>
+    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
+    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
+    <link rel="stylesheet" href="~/HTEX.Complex.styles.css" asp-append-version="true" />
+</head>
+<body>
+    <header>
+        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
+            <div class="container-fluid">
+                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">HTEX.Complex</a>
+                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
+                        aria-expanded="false" aria-label="Toggle navigation">
+                    <span class="navbar-toggler-icon"></span>
+                </button>
+                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
+                    <ul class="navbar-nav flex-grow-1">
+                        <li class="nav-item">
+                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+        </nav>
+    </header>
+    <div class="container">
+        <main role="main" class="pb-3">
+            @RenderBody()
+        </main>
+    </div>
+
+    <footer class="border-top footer text-muted">
+        <div class="container">
+            &copy; 2024 - HTEX.Complex - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
+        </div>
+    </footer>
+    <script src="~/lib/jquery/dist/jquery.min.js"></script>
+    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
+    <script src="~/js/site.js" asp-append-version="true"></script>
+    @await RenderSectionAsync("Scripts", required: false)
+</body>
+</html>

+ 48 - 0
HTEX.Complex/Views/Shared/_Layout.cshtml.css

@@ -0,0 +1,48 @@
+/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
+for details on configuring this project to bundle and minify static web assets. */
+
+a.navbar-brand {
+  white-space: normal;
+  text-align: center;
+  word-break: break-all;
+}
+
+a {
+  color: #0077cc;
+}
+
+.btn-primary {
+  color: #fff;
+  background-color: #1b6ec2;
+  border-color: #1861ac;
+}
+
+.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
+  color: #fff;
+  background-color: #1b6ec2;
+  border-color: #1861ac;
+}
+
+.border-top {
+  border-top: 1px solid #e5e5e5;
+}
+.border-bottom {
+  border-bottom: 1px solid #e5e5e5;
+}
+
+.box-shadow {
+  box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
+}
+
+button.accept-policy {
+  font-size: 1rem;
+  line-height: inherit;
+}
+
+.footer {
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+  white-space: nowrap;
+  line-height: 60px;
+}

+ 2 - 0
HTEX.Complex/Views/Shared/_ValidationScriptsPartial.cshtml

@@ -0,0 +1,2 @@
+<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
+<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

+ 3 - 0
HTEX.Complex/Views/_ViewImports.cshtml

@@ -0,0 +1,3 @@
+@using HTEX.Complex
+@using HTEX.Complex.Models
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

+ 3 - 0
HTEX.Complex/Views/_ViewStart.cshtml

@@ -0,0 +1,3 @@
+@{
+    Layout = "_Layout";
+}

+ 27 - 0
HTEX.Complex/appsettings.Development.json

@@ -0,0 +1,27 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft.AspNetCore": "Warning"
+    }
+  },
+  "AllowedHosts": "*",
+  "Azure": {
+    "Storage": {
+      "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodelos;AccountKey=Dl04mfZ9hE9cdPVO1UtqTUQYN/kz/dD/p1nGvSq4tUu/4WhiKcNRVdY9tbe8620nPXo/RaXxs+1F9sVrWRo0bg==;EndpointSuffix=core.chinacloudapi.cn",
+      "ConnectionString-Test": "DefaultEndpointsProtocol=https;AccountName=teammodeltest;AccountKey=O2W2vadCqexDxWO+px+QK7y1sHwsYj8f/WwKLdOdG5RwHgW/Dupz9dDUb4c1gi6ojzQaRpFUeAAmOu4N9E+37A==;EndpointSuffix=core.chinacloudapi.cn"
+    },
+    "ServiceBus": {
+      "ConnectionString": "Endpoint=sb://coreservicebuscn.servicebus.chinacloudapi.cn/;SharedAccessKeyName=TEAMModelOS;SharedAccessKey=xO8HcvXXuuEkuFI0KlV5uXs8o6vyuVqTR+ASbPGMhHo=",
+      "ConnectionString-Test": "Endpoint=sb://coreservicebuscn.servicebus.chinacloudapi.cn/;SharedAccessKeyName=TEAMModelOS;SharedAccessKey=xO8HcvXXuuEkuFI0KlV5uXs8o6vyuVqTR+ASbPGMhHo="
+    },
+    "Redis": {
+      "ConnectionString": "CoreRedisCN.redis.cache.chinacloudapi.cn:6380,password=LyJWP1ORJdv+poXWofAF97lhCEQPg1wXWqvtzXGXQuE=,ssl=True,abortConnect=False",
+      "ConnectionString-Test": "52.130.252.100:6379,password=habook,ssl=false,abortConnect=False,writeBuffer=10240"
+    },
+    "Cosmos": {
+      "ConnectionString": "AccountEndpoint=https://cdhabookdep-free.documents.azure.cn:443/;AccountKey=JTUVk92Gjsx17L0xqxn0X4wX2thDPMKiw4daeTyV1HzPb6JmBeHdtFY1MF1jdctW1ofgzqkDMFOtcqS46by31A==;",
+      "ConnectionString-Test": "AccountEndpoint=https://cdhabookdep-free.documents.azure.cn:443/;AccountKey=JTUVk92Gjsx17L0xqxn0X4wX2thDPMKiw4daeTyV1HzPb6JmBeHdtFY1MF1jdctW1ofgzqkDMFOtcqS46by31A==;"
+    }
+  }
+}

+ 27 - 0
HTEX.Complex/appsettings.json

@@ -0,0 +1,27 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft.AspNetCore": "Warning"
+    }
+  },
+  "AllowedHosts": "*",
+  "Azure": {
+    "Storage": {
+      "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=teammodelos;AccountKey=Dl04mfZ9hE9cdPVO1UtqTUQYN/kz/dD/p1nGvSq4tUu/4WhiKcNRVdY9tbe8620nPXo/RaXxs+1F9sVrWRo0bg==;EndpointSuffix=core.chinacloudapi.cn",
+      "ConnectionString-Test": "DefaultEndpointsProtocol=https;AccountName=teammodeltest;AccountKey=O2W2vadCqexDxWO+px+QK7y1sHwsYj8f/WwKLdOdG5RwHgW/Dupz9dDUb4c1gi6ojzQaRpFUeAAmOu4N9E+37A==;EndpointSuffix=core.chinacloudapi.cn"
+    },
+    "ServiceBus": {
+      "ConnectionString": "Endpoint=sb://coreservicebuscn.servicebus.chinacloudapi.cn/;SharedAccessKeyName=TEAMModelOS;SharedAccessKey=xO8HcvXXuuEkuFI0KlV5uXs8o6vyuVqTR+ASbPGMhHo=",
+      "ConnectionString-Test": "Endpoint=sb://coreservicebuscn.servicebus.chinacloudapi.cn/;SharedAccessKeyName=TEAMModelOS;SharedAccessKey=xO8HcvXXuuEkuFI0KlV5uXs8o6vyuVqTR+ASbPGMhHo="
+    },
+    "Redis": {
+      "ConnectionString": "CoreRedisCN.redis.cache.chinacloudapi.cn:6380,password=LyJWP1ORJdv+poXWofAF97lhCEQPg1wXWqvtzXGXQuE=,ssl=True,abortConnect=False",
+      "ConnectionString-Test": "52.130.252.100:6379,password=habook,ssl=false,abortConnect=False,writeBuffer=10240"
+    },
+    "Cosmos": {
+      "ConnectionString": "AccountEndpoint=https://teammodelos.documents.azure.cn:443/;AccountKey=clF73GwPECfP1lKZTCvs8gLMMyCZig1HODFbhDUsarsAURO7TcOjVz6ZFfPqr1HzYrfjCXpMuVD5TlEG5bFGGg==;",
+      "ConnectionString-Test": "AccountEndpoint=https://cdhabookdep-free.documents.azure.cn:443/;AccountKey=JTUVk92Gjsx17L0xqxn0X4wX2thDPMKiw4daeTyV1HzPb6JmBeHdtFY1MF1jdctW1ofgzqkDMFOtcqS46by31A==;"
+    }
+  }
+}

+ 28 - 0
HTEXGpt/Controllers/ChatController.cs

@@ -0,0 +1,28 @@
+using HTEXGpt.Models;
+using HTEXGpt.Services;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.AspNetCore.Mvc;
+using System.Diagnostics.Tracing;
+using System.Net;
+
+namespace HTEXGpt.Controllers
+{
+    [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status400BadRequest)]
+    [Route("chat")]
+    [ApiController]
+    public class ChatController:ControllerBase
+    {
+        private static int _eventCounter = 0;
+        private readonly IAiAppService _aiAppService;
+        public ChatController(IAiAppService aiAppService) {
+            _aiAppService=aiAppService;
+        }
+
+        [HttpPost("message")]
+        public async Task<IActionResult > Message(ChatRequest dto) {
+        var chatResponse =   await  _aiAppService.ChatMessage(dto.modelType, dto, HttpContext, Response);
+            return Ok(new { response = chatResponse });
+        }
+    }
+}

+ 32 - 0
HTEXGpt/Controllers/HomeController.cs

@@ -0,0 +1,32 @@
+using HTEXGpt.Models;
+using Microsoft.AspNetCore.Mvc;
+using System.Diagnostics;
+
+namespace HTEXGpt.Controllers
+{
+    public class HomeController : Controller
+    {
+        private readonly ILogger<HomeController> _logger;
+
+        public HomeController(ILogger<HomeController> logger)
+        {
+            _logger = logger;
+        }
+
+        public IActionResult Index()
+        {
+            return View();
+        }
+
+        public IActionResult Privacy()
+        {
+            return View();
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+    }
+}

+ 217 - 0
HTEXGpt/Controllers/SparkDeskChatController.cs

@@ -0,0 +1,217 @@
+using HTEXGpt.Models;
+using HTEXGpt.Services;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.AspNetCore.Mvc;
+using System.Diagnostics.Tracing;
+using System.Net;
+using System.Net.Http;
+using System.Net.WebSockets;
+using System.Text;
+using System.Text.Json.Nodes;
+using System.Text.Json;
+namespace HTEXGpt.Controllers
+{
+    [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status400BadRequest)]
+    [Route("chat")]
+    [ApiController]
+    public class SparkDeskChatController : ControllerBase
+    {
+        static ClientWebSocket webSocket0;
+        static CancellationToken cancellation;
+        private string appId = "33cc7806";
+        private string appSecret = "YmVmNWI4YTRmNzFmZDg4MzUyMGM1MzYx";
+
+        private string appKey = "7f6165ce496c92a7e231e80a87fe62e9";
+        private static string HOST_URL = "https://spark-api.xf-yun.com/v3.5/chat";
+        private static string domain = "generalv3.5";
+
+        public SparkDeskChatController( ) {
+            
+        }
+
+       
+
+
+        [HttpPost("test/completion")]
+       
+        public async Task TestCompletion(ChatRequest dto)
+        {  
+            //测试,debug到这里的时候你会发现,协议使用的是HTTP/2. APS.NET Core 2.1以上就默认支持HTTP/2,无需额外的配置。再Windows Server2016/Windows10+会自动提供支持。
+            string requestProtocol = HttpContext.Request.Protocol;
+            var response = Response;
+            //响应头部添加text/event-stream,这是HTTP/2协议的一部分。
+            response.Headers.Add("Content-Type", "text/event-stream");
+            //for (int i = 0; i<100; i++)
+            //{
+            //    // event:ping event是事件字段名,冒号后面是事件名称,不要忘了\n换行符。
+            //    await HttpContext.Response.WriteAsync($"event:ping\n");
+
+            //    // data: 是数据字段名称,冒号后面是数据字段内容。注意数据内容仅仅支持UTF-8,不支持二进制格式。
+            //    // data后面的数据当然可以传递JSON String的。
+            //    await HttpContext.Response.WriteAsync($"data:Controller {i} at {DateTime.Now}\r\r");
+
+            //    // 写入数据到响应后不要忘记 FlushAsync(),因为该api方法是异步的,所以要全程异步,调用同步方法会报错。
+            //    await HttpContext.Response.Body.FlushAsync();
+
+            //    //模拟一个1秒的延迟。
+            //    //await Task.Delay(1000);
+            //}
+            string authUrl = GetAuthUrl();
+            string url = authUrl.Replace("http://", "ws://").Replace("https://", "wss://");
+            using (webSocket0 = new ClientWebSocket())
+            {
+                try
+                {
+                    await webSocket0.ConnectAsync(new Uri(url), cancellation);
+
+                    SparkDeskRequest request = new SparkDeskRequest();
+                    request.header = new SparkDeskHeader()
+                    {
+                        app_id = appId,
+                        uid = dto.uid
+                    };
+                    request.parameter = new SparkDeskParameter()
+                    {
+                        chat = new SparkDeskChat()
+                        {
+                            domain = domain,//模型领域,默认为星火通用大模型
+                           // temperature = dto.@params.temperature,//温度采样阈值,用于控制生成内容的随机性和多样性,值越大多样性越高;范围(0,1)
+                            max_tokens = 8192,//生成内容的最大长度,范围(0,4096)
+                        }
+                    };
+                    request.payload = new SparkDeskPayload()
+                    {
+                        //  new Content() { role = "user", content = "你是谁" },
+                        // new Content() { role = "assistant", content = "....." }, // AI的历史回答结果,这里省略了具体内容,可以根据需要添加更多历史对话信息和最新问题的内容。
+                        message = new SparkDeskMessage()
+                        {
+                            text=dto.messages
+                        }
+                    };
+
+                    string jsonString = JsonSerializer.Serialize(request);
+                    //连接成功,开始发送数据
+
+
+                    var frameData2 = System.Text.Encoding.UTF8.GetBytes(jsonString.ToString());
+
+
+                    await webSocket0.SendAsync(new ArraySegment<byte>(frameData2), WebSocketMessageType.Text, true, cancellation);
+
+                    // 接收流式返回结果进行解析
+                    byte[] receiveBuffer = new byte[1024];
+                    WebSocketReceiveResult result = await webSocket0.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), cancellation);
+                    string resp = "";
+                    while (!result.CloseStatus.HasValue)
+                    {
+                        if (result.MessageType == WebSocketMessageType.Text)
+                        {
+                            string receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, result.Count);
+                            //将结果构造为json
+                            var node = JsonNode.Parse(receivedMessage);
+                            int? code = (int?)node?["header"]?["code"];
+                            //JObject jsonObj = JObject.Parse(receivedMessage);
+                            // int code = (int)jsonObj["header"]["code"];
+
+
+                            if (0==code)
+                            {
+                                // int status = (int)jsonObj["payload"]["choices"]["status"];
+                                int? status = (int?)node?["payload"]?["choices"]?["status"];
+
+                                //JArray textArray = (JArray)jsonObj["payload"]["choices"]["text"];
+                                JsonArray? textArray = (JsonArray?)node?["payload"]?["choices"]?["text"];
+                                string? content = (string?)textArray?[0]?["content"];
+                                resp += content;
+
+                                if (status != 2)
+                                {
+
+                                    await HttpContext.Response.WriteAsync($"event:ping\n");
+                                    await HttpContext.Response.WriteAsync($"data:Controller {receivedMessage} at {DateTime.Now}\r\r");
+                                    await HttpContext.Response.Body.FlushAsync();
+
+                                    Console.WriteLine($"已接收到数据: {receivedMessage}");
+                                }
+                                else
+                                {
+                                    Console.WriteLine($"最后一帧: {receivedMessage}");
+                                    //int totalTokens = (int)jsonObj["payload"]["usage"]["text"]["total_tokens"];
+                                    int? totalTokens = (int?)node?["payload"]?["usage"]?["text"]?["total_tokens"];
+                                    Console.WriteLine($"整体返回结果: {resp}");
+                                    Console.WriteLine($"本次消耗token数: {totalTokens}");
+                                    await HttpContext.Response.WriteAsync($"event:ping\n");
+                                    await HttpContext.Response.WriteAsync($"data: {resp} at {DateTime.Now}\r\r");
+                                    await HttpContext.Response.Body.FlushAsync();
+                                    break;
+                                }
+
+                            }
+                            else
+                            {
+                                Console.WriteLine($"请求报错: {receivedMessage}");
+                            }
+
+
+                        }
+                        else if (result.MessageType == WebSocketMessageType.Close)
+                        {
+                            Console.WriteLine("已关闭WebSocket连接");
+                            //数据发送完毕后关闭连接。
+                          
+                            break;
+                        }
+
+                        result = await webSocket0.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), cancellation);
+                    }
+                }
+                catch (Exception e)
+                {
+                    response.Body.Close();
+                    Console.WriteLine($"{e.Message}\n{e.StackTrace}");
+                }
+            }
+            response.Body.Close();
+
+        }
+
+        // 返回code为错误码时,请查询https://www.xfyun.cn/document/error-code解决方案
+        public string GetAuthUrl()
+        {
+            string date = DateTime.UtcNow.ToString("r");
+
+            Uri uri = new Uri(HOST_URL);
+            StringBuilder builder = new StringBuilder("host: ").Append(uri.Host).Append("\n").//
+                                    Append("date: ").Append(date).Append("\n").//
+                                    Append("GET ").Append(uri.LocalPath).Append(" HTTP/1.1");
+
+            string sha = HMACsha256(appSecret, builder.ToString());
+            string authorization = string.Format("api_key=\"{0}\", algorithm=\"{1}\", headers=\"{2}\", signature=\"{3}\"", appKey, "hmac-sha256", "host date request-line", sha);
+            //System.Web.HttpUtility.UrlEncode
+
+            string NewUrl = "https://" + uri.Host + uri.LocalPath;
+
+            string path1 = "authorization" + "=" + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(authorization));
+            date = date.Replace(" ", "%20").Replace(":", "%3A").Replace(",", "%2C");
+            string path2 = "date" + "=" + date;
+            string path3 = "host" + "=" + uri.Host;
+
+            NewUrl = NewUrl + "?" + path1 + "&" + path2 + "&" + path3;
+            return NewUrl;
+        }
+        public static string HMACsha256(string apiSecretIsKey, string buider)
+        {
+            byte[] bytes = System.Text.Encoding.UTF8.GetBytes(apiSecretIsKey);
+            System.Security.Cryptography.HMACSHA256 hMACSHA256 = new System.Security.Cryptography.HMACSHA256(bytes);
+            byte[] date = System.Text.Encoding.UTF8.GetBytes(buider);
+            date = hMACSHA256.ComputeHash(date);
+            hMACSHA256.Clear();
+
+            return Convert.ToBase64String(date);
+
+        }
+
+    }
+}

+ 19 - 0
HTEXGpt/HTEXGpt.csproj

@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <Nullable>enable</Nullable>
+    <ImplicitUsings>enable</ImplicitUsings>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.6.1" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Reference Include="Newtonsoft.Json">
+      <HintPath>C:\Program Files\IIS\Microsoft Web Deploy V3\Newtonsoft.Json.dll</HintPath>
+    </Reference>
+  </ItemGroup>
+
+</Project>

+ 80 - 0
HTEXGpt/Models/ChatRequest.cs

@@ -0,0 +1,80 @@
+using System.Net;
+using System.Text.Json;
+
+namespace HTEXGpt.Models
+{
+    public class ChatRequest
+    {
+        /// <summary>
+        /// @ApiModelProperty(value = "聊天上下文信息", notes = "
+        /// (1)最后一个message为当前请求的信息,前面的message为历史对话信息\n 
+        /// (2)成员数目必须为奇数\n 
+        /// (3)示例中message中的role值分别为user、assistant;
+        /// 奇数位message中的role值为user;
+        /// 偶数位值为assistant  example = "[{\"role\":\"user\",\"content\":\"你好\"},{\"role\":\"assistant\",\"content\":\"需要什么帮助\"},{\"role\":\"user\",\"content\":\"自我介绍下\"}]")
+        /// @NotNull(message = "聊天上下文信息不能为空")
+        /// </summary>
+        public List<MessageDTO> messages{  get; set; }= new List<MessageDTO>();
+
+        /// <summary>
+        ///  @ApiModelProperty(value = "模型人设", notes = "主要用于人设设定,例如,你是xxx公司制作的AI助手,最大20000字符", example = "你是一名天气助手,需要提供天气查询服务")
+        /// </summary>
+        public string? system { get; set; }
+
+        /// <summary>
+        ///   @ApiModelProperty(value = "请求参数", notes = "请求参数", example = "{\"key\":\"value\"}")
+        /// </summary>
+        public ChatParam @params { get; set; }
+        /// <summary>
+        /// 模型类型
+        /// </summary>
+        public string? modelType { get; set; }
+        public string? uid { get; set; }
+    }
+    public class MessageDTO
+    {
+        /// <summary>
+        ///  @ApiModelProperty(value = "角色", notes = "说明: user-用户, assistant-助手", example = "user")
+        /// </summary>
+        public string? role { get; set; }
+        /// <summary>
+        /// @ApiModelProperty(value = "消息内容", notes = "说明: 消息内容", example = "你好")
+        /// </summary>
+        public string? content { get; set; }
+    }
+    public class ChatParam 
+    {
+        //  public int max_tokens { get; set; }
+
+        /// <summary>
+        /// 通义千问0-2 ,讯飞星火0-1,默认0.5  ,文心一言 0-1,默认0.95
+        /// </summary>
+        public double temperature { get; set; }
+        /// <summary>
+        /// 通义千问0-2 ,讯飞星火0-1,文心一言 0-1,默认0.7
+        /// </summary>
+        public double top_p { get; set; }
+       // public double presencePenalty { get; set; }
+      //  public double frequencyPenalty { get; set; }
+    }
+
+    public class ChatResponse
+    {
+        public string? result{ get; set; }
+        /// <summary>
+        /// 总花费
+        /// </summary>
+        public double total_tokens { get; set; }
+        /// <summary>
+        /// 输出花费
+        /// </summary>
+        public double completion_tokens { get; set; }
+        /// <summary>
+        /// 输入花费
+        /// </summary>
+        public double prompt_tokens { get; set; }
+        public long time { get; set; }
+        public string? error {  get; set; }
+        public HttpStatusCode statusCode { get; set; }
+    }
+}

+ 11 - 0
HTEXGpt/Models/ChatResponseVO.cs

@@ -0,0 +1,11 @@
+namespace HTEXGpt.Models
+{
+    public class ChatResponseVO
+    {
+        /// <summary>
+        ///  @ApiModelProperty(value = "结果", notes = "结果")
+        /// </summary>
+        public string? result {  get; set; }
+        public int token {  get; set; }
+    }
+}

+ 9 - 0
HTEXGpt/Models/ErrorViewModel.cs

@@ -0,0 +1,9 @@
+namespace HTEXGpt.Models
+{
+    public class ErrorViewModel
+    {
+        public string? RequestId { get; set; }
+
+        public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
+    }
+}

+ 16 - 0
HTEXGpt/Models/ModelTypeEnum.cs

@@ -0,0 +1,16 @@
+using System.ComponentModel;
+
+namespace HTEXGpt.Models
+{
+    public enum ModelTypeEnum
+    {
+        [Description("文心一言")]
+        ErnieBot,
+        [Description("讯飞星火")]
+        SparkDesk,
+        [Description("智谱清言")]
+        ChatGlm,
+        [Description("通义千问")]
+        QianWen
+    }
+}

+ 37 - 0
HTEXGpt/Program.cs

@@ -0,0 +1,37 @@
+using HTEXGpt.Services;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using System.Net.WebSockets;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+builder.Services.AddControllersWithViews();
+builder.Services.AddScoped<IAiAppService,AiAppServiceImpl>();
+builder.Services.AddScoped<SparkDeskServiceImpl>();
+builder.Services.AddScoped<QianWenServiceImpl>();
+builder.Services.AddScoped<ErnieBotServiceImpl>();
+builder.Services.AddScoped<ClientWebSocket>();
+builder.Services.AddScoped<ChatGlmServiceImpl>();
+builder.Services.AddHttpClient();
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (!app.Environment.IsDevelopment())
+{
+    app.UseExceptionHandler("/Home/Error");
+    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
+    app.UseHsts();
+}
+
+app.UseHttpsRedirection();
+app.UseStaticFiles();
+
+app.UseRouting();
+
+app.UseAuthorization();
+
+app.MapControllerRoute(
+    name: "default",
+    pattern: "{controller=Home}/{action=Index}/{id?}");
+
+app.Run();

+ 38 - 0
HTEXGpt/Properties/launchSettings.json

@@ -0,0 +1,38 @@
+{
+  "$schema": "http://json.schemastore.org/launchsettings.json",
+  "iisSettings": {
+    "windowsAuthentication": false,
+    "anonymousAuthentication": true,
+    "iisExpress": {
+      "applicationUrl": "http://localhost:14156",
+      "sslPort": 44304
+    }
+  },
+  "profiles": {
+    "http": {
+      "commandName": "Project",
+      "dotnetRunMessages": true,
+      "launchBrowser": true,
+      "applicationUrl": "http://localhost:5023",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    },
+    "https": {
+      "commandName": "Project",
+      "dotnetRunMessages": true,
+      "launchBrowser": true,
+      "applicationUrl": "https://localhost:7022;http://localhost:5023",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    },
+    "IIS Express": {
+      "commandName": "IISExpress",
+      "launchBrowser": true,
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    }
+  }
+}

+ 86 - 0
HTEXGpt/Services/AiAppServiceImpl.cs

@@ -0,0 +1,86 @@
+using HTEXGpt.Models;
+
+namespace HTEXGpt.Services
+{
+    public class AiAppServiceImpl : IAiAppService
+    {
+
+        private readonly IServiceProvider _serviceProvider;
+
+        public AiAppServiceImpl(IServiceProvider  serviceProvider) 
+        {
+            _serviceProvider = serviceProvider;
+        }
+        public async Task<ChatResponse> ChatMessage(string? modelType, ChatRequest dto, HttpContext httpContext,HttpResponse response)
+        {
+            this.CheckMessages(dto.messages);
+            IModelService? modelService = GetModelService(modelType!);
+            return   await   modelService!.ChatMessage(dto,httpContext, response);
+        }
+
+        /// <summary>
+        /// 根据模型类型获取对应的模型服务
+        /// </summary>
+        /// <param name="modelType"></param>
+        /// <returns></returns>
+        /// <exception cref="InvalidOperationException"></exception>
+        private IModelService? GetModelService(string  modelType)
+        {
+            try
+            {
+                // 将字符串转换为枚举值
+                ModelTypeEnum modelTypeEnum = (ModelTypeEnum)Enum.Parse(typeof(ModelTypeEnum), modelType!);
+                // 根据枚举值获取对应的服务类型
+                Type? serviceType = typeof(IModelService).Assembly.GetTypes()
+                    .FirstOrDefault(t => t.IsClass && !t.IsAbstract && t.GetInterfaces().Contains(typeof(IModelService)) && t.Name.StartsWith(modelTypeEnum.ToString()));
+
+                if (serviceType == null)
+                {
+                    throw new InvalidOperationException($"未找到与模型类型 {modelType} 对应的服务。");
+                }
+                // 获取服务实例
+                var s=  _serviceProvider.GetRequiredService(serviceType);
+                var s1 = _serviceProvider.GetService(serviceType);
+                return (IModelService?)_serviceProvider.GetService(serviceType);
+            }
+            catch (ArgumentException e)
+            {
+                throw new InvalidOperationException("模型类型错误", e);
+            }
+        }
+
+        /// <summary>
+        /// 检查消息参数是否符合规范@param messages 消息参数
+        /// </summary>
+        /// <param name="messages"></param>
+        /// <exception cref="RuntimeException"></exception>
+        private void CheckMessages(List<MessageDTO> messages)
+        {
+            if (messages!=null   && messages.Count>0)
+            {
+                // messages参数个数必须为奇数并且奇数个数的消息role必须为user,偶数个数的消息role必须为assistant
+                if (messages.Count() % 2 == 0)
+                {
+                    throw new Exception("messages参数个数必须为奇数");
+                }
+                for (int i = 0; i < messages.Count(); i++)
+                {
+                    if (i % 2 == 0)
+                    {
+                        if (!"user".Equals(messages[i].role))
+                        {
+                            throw new Exception("messages奇数参数的role必须为user");
+                        }
+                    }
+                    else
+                    {
+                        if (!"assistant".Equals(messages[i].role))
+                        {
+                            throw new Exception("messages偶数参数的role必须为assistant");
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 132 - 0
HTEXGpt/Services/ChatGlmServiceImpl.cs

@@ -0,0 +1,132 @@
+using HTEXGpt.Models;
+using Microsoft.IdentityModel.Tokens;
+using System.Diagnostics;
+using System.IdentityModel.Tokens.Jwt;
+using System.Net;
+using System.Security.Claims;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+using System.Xml.Linq;
+namespace HTEXGpt.Services
+{
+    public class ChatGlmServiceImpl : IModelService
+    {
+        private string apiKey = "2c012f8a5f430c93bff19b24a3428dc2.882B3AzRmv1UZiKt";
+        private string HOST_URL = "https://open.bigmodel.cn/api/paas/v4/chat/completions";
+        private readonly IHttpClientFactory _httpClientFactory;
+        public ChatGlmServiceImpl(IHttpClientFactory httpClientFactory)
+        {
+            _httpClientFactory=httpClientFactory;
+        }
+
+        public async Task<ChatResponse> ChatMessage(ChatRequest dto, HttpContext httpContext, HttpResponse response)
+        {
+            ChatResponse chatResponse = new ChatResponse();
+            Stopwatch stopwatch = Stopwatch.StartNew();
+            try
+            {
+                ChatGlmDTO chatGlmDTO = new ChatGlmDTO();
+                chatGlmDTO.messages = dto.messages;
+                if (!string.IsNullOrWhiteSpace(dto.system))
+                {
+                    chatGlmDTO.messages.Insert(0, new MessageDTO { role="system", content= dto.system });
+                }
+                chatGlmDTO.model="glm-4";
+                chatGlmDTO.temperature=dto.@params.temperature;
+                //chatGlmDTO.top_p=dto.@params.top_p;
+                chatGlmDTO.user_id= dto.uid;
+                var token = GenerateToken(apiKey);
+                HttpClient httpClient = _httpClientFactory.CreateClient();
+                if (httpClient.DefaultRequestHeaders.Contains("Authorization"))
+                {
+                    httpClient.DefaultRequestHeaders.Remove("Authorization");
+                }
+                httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
+                HttpResponseMessage httpResponse = await httpClient.PostAsJsonAsync(HOST_URL, chatGlmDTO);
+                if (httpResponse.StatusCode.Equals(HttpStatusCode.OK))
+                {
+                    string content = await httpResponse.Content.ReadAsStringAsync();
+                    chatResponse.result = content;
+                }
+                else { 
+                    chatResponse.statusCode = httpResponse.StatusCode;
+                    string content = await httpResponse.Content.ReadAsStringAsync();
+                    chatResponse.error = content;
+                }
+
+            } catch (Exception ex) {
+                chatResponse.error=$"{ex.Message},{ex.StackTrace}";
+            } finally { 
+                stopwatch.Stop();
+                chatResponse.time= stopwatch.ElapsedMilliseconds;
+            }
+            return chatResponse;
+        }
+
+        public static String GenerateToken(string apikey, int expSeconds= 3600)
+        {
+            string[]parts= apikey.Split('.');
+            try
+            {
+                var payload = new JwtPayload {
+                    { JwtRegisteredClaimNames.Exp,DateTimeOffset.UtcNow.AddHours(expSeconds).ToUnixTimeSeconds()},
+                    { "api_key",parts[0]},
+                    { "timestamp",DateTimeOffset.UtcNow.ToUnixTimeSeconds()},
+                    //{ "api_key",parts[0]},
+                    //{ JwtRegisteredClaimNames.Exp,1718696388},
+                    //{ "timestamp",1718692788},
+                };
+                //var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(parts[1]));
+                //var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
+                //var header = new JwtHeader(signingCredentials);
+                //header.Add("sign_type", "SIGN");
+                string? header_base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"typ\":\"JWT\",\"alg\":\"HS256\",\"sign_type\":\"SIGN\"}")) .Replace("=","");
+                string? payload_base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(payload))).Replace("=", "");
+                var signature_base64 =  SignToken(header_base64, payload_base64, parts[1]).Replace("=","");
+                string? token  = $"{header_base64}.{payload_base64}.{signature_base64}";
+                //Java生成的Token 
+                //string s = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsInNpZ25fdHlwZSI6IlNJR04ifQ.eyJhcGlfa2V5IjoiMmMwMTJmOGE1ZjQzMGM5M2JmZjE5YjI0YTM0MjhkYzIiLCJleHAiOjE3MTg2OTYzODgsInRpbWVzdGFtcCI6MTcxODY5Mjc4OH0.ZCgsRMqFkbpx0eig1ouItkdrZ9HjRNWgKQVuqFU2JZ4";
+                return token;
+            }
+            catch (Exception e)
+            {
+                throw new Exception("Error generating token", e);
+            }
+        }
+
+        /// <summary>
+        /// 报错IDX10720: Unable to create KeyedHashAlgorithm for algorithmHS256'. the key size must be greater than: '256' bits, key has '128' bits. Arg ParamName Name
+        /// 由于.net8 HMACSHA256算法强制验证secret必须至少是256位,所以用这种方式
+        /// </summary>
+        /// <param name="header"></param>
+        /// <param name="payload"></param>
+        /// <param name="secret"></param>
+        /// <returns></returns>
+        public static  string SignToken(string header, string payload, string secret)
+        {
+            header= header.Replace("=", "");
+            var encoding = new UTF8Encoding();
+            byte[] keyByte = encoding.GetBytes(secret);
+            byte[] messageBytes = encoding.GetBytes(header + "." + payload);
+            using (var hmacsha256 = new HMACSHA256(keyByte))
+            {
+                byte[] hash = hmacsha256.ComputeHash(messageBytes);
+                return Convert.ToBase64String(hash);
+            }
+        }
+    }
+
+    public class ChatGlmDTO
+    {
+        public string? model {  get; set; }
+        public List<MessageDTO>? messages { get; set; }= new List<MessageDTO>();
+        public string? request_id {  get; set; }
+        public bool do_sample { get; set; }
+        public bool stream {  get; set; }
+        public double temperature {  get; set; }
+        // public double top_p { get; set; }
+        public int max_tokens { get; set; } = 8192;
+        public string? user_id {  get; set; }
+    }
+}

+ 154 - 0
HTEXGpt/Services/ErnieBotServiceImpl.cs

@@ -0,0 +1,154 @@
+using HTEXGpt.Models;
+using Microsoft.AspNetCore.Http;
+using System.Diagnostics;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace HTEXGpt.Services
+{
+    public class ErnieBotServiceImpl : IModelService
+    {
+        private readonly string appSecret = "7E0C6SzWE7kb1lSp1Dkb7k6Eg2xFkJoR";
+
+        private readonly string apiKey = "vnxaIH9aJLrsiwMI8OchFKEf";
+
+        private readonly static string TOKEN_URL_TEMPLATE = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id={0}&client_secret={1}";
+        //最大输出Token  max_output_tokens=4096
+        // private readonly static String CHAT_URL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-speed-128k?access_token={0}";
+
+        //最大输出Token  max_output_tokens=2048
+        private readonly static String CHAT_URL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie_speed?access_token={0}";
+        private readonly string appId = "71670766";
+
+        private readonly  IHttpClientFactory _httpClientFactory;
+        public ErnieBotServiceImpl(IHttpClientFactory httpClientFactory) 
+        {
+            _httpClientFactory=httpClientFactory; 
+        }
+
+        public async Task<ChatResponse> ChatMessage(ChatRequest dto, HttpContext httpContext, HttpResponse response)
+        {
+            // response.Headers.ContentType="text/event-stream";
+            //response.Headers.CacheControl="no-cache";
+            //response.Headers.Connection="keep-alive";
+            Stopwatch stopwatch = Stopwatch.StartNew(); // 开始计时
+            ChatResponse chatResponse= new ChatResponse();
+            try {
+                var token = await getAccessToken();
+                JsonElement json = JsonSerializer.Deserialize<JsonElement>(token);
+                var access_token= json.GetProperty("access_token").GetString();
+                ErnieBotDTO ernieBotDTO = new ErnieBotDTO
+                {
+                    messages= dto.messages,
+                    system=dto.system,
+                    stream=false,
+                  //  temperature=dto.@params.temperature,
+                   // top_p=dto.@params.top_p,
+                  //  max_output_tokens=4096
+                    user_id =dto.uid
+                };
+                var httpClient = _httpClientFactory.CreateClient();
+                string url = string.Format(CHAT_URL, access_token);
+                var data = JsonSerializer.Serialize(ernieBotDTO);
+                HttpResponseMessage httpResponse = await httpClient.PostAsJsonAsync(url, ernieBotDTO);
+                if (httpResponse.IsSuccessStatusCode)
+                {
+
+                    var content = await httpResponse.Content.ReadAsStringAsync();
+                    //using (var responseStream = await httpResponse.Content.ReadAsStreamAsync())
+                    //{
+                    //    StringBuilder sb = new StringBuilder();
+                    //    var buffer = new byte[1024];
+                    //    int bytesRead;
+                    //    while ((bytesRead =   responseStream.Read(buffer, 0, buffer.Length)) > 0)
+                    //    {
+                    //        string contentData = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead);
+                    //        Console.WriteLine(contentData);
+                    //        await response.WriteAsync($"event:result\n");
+                    //        await response.WriteAsync(@$"{contentData}");
+                    //        await response.Body.FlushAsync();
+                    //        await Task.Delay(500);
+                    //    }
+                    //}
+                    StringBuilder sb = new StringBuilder();
+                    var datas = content.Split("data:");
+                    foreach (var jsonData in datas)
+                    {
+                        try
+                        {
+                            JsonNode j = JsonSerializer.Deserialize<JsonNode>(jsonData);
+                            sb.Append($"{j["result"]}");
+                            var total_tokens = j?["usage"]?["total_tokens"];
+                            if (total_tokens!= null)
+                            {
+                                chatResponse.total_tokens=int.Parse($"{total_tokens}");
+                            }
+                            var completion_tokens = j?["usage"]?["completion_tokens"];
+                            if (completion_tokens!= null)
+                            {
+                                chatResponse.completion_tokens=int.Parse($"{completion_tokens}");
+                            }
+
+                            var prompt_tokens = j?["usage"]?["prompt_tokens"];
+                            if (prompt_tokens!= null)
+                            {
+                                chatResponse.prompt_tokens=int.Parse($"{prompt_tokens}");
+                            }
+                        }
+                        catch { }
+                    }
+                    chatResponse.result= sb.ToString();
+                    chatResponse.statusCode= System.Net.HttpStatusCode.OK;
+                    // await response.WriteAsync(@$"{content}");
+                    // await response.Body.FlushAsync();
+                }
+                else {
+                    chatResponse.statusCode=httpResponse.StatusCode;
+                    string content = await httpResponse.Content.ReadAsStringAsync();
+                    chatResponse.error=$"{content}";
+                }
+            } catch (Exception ex) {
+                chatResponse.statusCode=System.Net.HttpStatusCode.InternalServerError;
+                chatResponse.error=$"{ex.Message},{ex.StackTrace}";
+            }
+            finally
+            {
+                //   response.Body.Close();
+                stopwatch.Stop(); // 停止计时
+                chatResponse.time= stopwatch.ElapsedMilliseconds;
+            }
+            return chatResponse;
+        }
+
+        private async Task<string> getAccessToken() 
+        {
+
+            var token_url=  string.Format(TOKEN_URL_TEMPLATE, apiKey, appSecret);
+            var httpClient = _httpClientFactory.CreateClient();
+            httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
+            var response =  await httpClient.PostAsJsonAsync(token_url,new { });
+            if (response.StatusCode==System.Net.HttpStatusCode.OK) {
+
+                var content = await  response.Content.ReadAsStringAsync();
+                return content;
+            }
+            return null; 
+
+        }
+    }
+
+    public class ErnieBotDTO 
+    {
+        public List<MessageDTO> messages { get; set; } = new List<MessageDTO>();
+        public string? system { get; set; }
+        public double temperature { get; set; } = 0.95;
+        public bool stream { get; set; } = true;
+        public double top_p { get; set; } = 0.7;
+        public string response_format { get; set; } = "text";
+       // public double penalty_score { get; set; }
+       // public bool enable_citation { get; set; }
+       // public bool disable_search { get; set; }
+        public string? user_id { get; set; }
+    }
+}

+ 15 - 0
HTEXGpt/Services/IAiAppService.cs

@@ -0,0 +1,15 @@
+using HTEXGpt.Models;
+
+namespace HTEXGpt.Services
+{
+    public interface IAiAppService
+    {
+        /// <summary>
+        /// 向大模型发起对话请求-根据模型编码、用户ID
+        /// </summary>
+        /// <param name="modelType">模型类型</param>
+        /// <param name="dto">消息参数</param>
+        /// <returns></returns>
+        public    Task<ChatResponse> ChatMessage(string? modelType, ChatRequest dto, HttpContext httpContext,HttpResponse response);
+    }
+}

+ 14 - 0
HTEXGpt/Services/IModelService.cs

@@ -0,0 +1,14 @@
+using HTEXGpt.Models;
+
+namespace HTEXGpt.Services
+{
+    public interface IModelService
+    {
+        /// <summary>
+        /// 发起请求
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        public Task<ChatResponse> ChatMessage(ChatRequest dto, HttpContext httpContext, HttpResponse response );
+    }
+}

+ 138 - 0
HTEXGpt/Services/QianWenServiceImpl.cs

@@ -0,0 +1,138 @@
+using HTEXGpt.Models;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using static System.Runtime.InteropServices.JavaScript.JSType;
+
+namespace HTEXGpt.Services
+{
+    public class QianWenServiceImpl : IModelService
+    {
+        private string HOST_URL= "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation";
+        private string apiKey = "sk-b8354384e92d466ea4876410c53eb4ce";
+        private string model = "qwen-turbo";
+        //qwen-turbo最大值和默认值为1500 tokens。
+        //qwen-max、qwen-max-1201、qwen-max-longcontext和qwen-plus模型,最大值和默认值均为2000 tokens。
+        private readonly IHttpClientFactory _httpClientFactory;
+        public QianWenServiceImpl(IHttpClientFactory httpClientFactory) {
+            _httpClientFactory = httpClientFactory;
+        }
+        public async Task<ChatResponse> ChatMessage(ChatRequest dto, HttpContext httpContext, HttpResponse response)
+        {
+             Stopwatch stopwatch = Stopwatch.StartNew();
+            ChatResponse chatResponse = new ChatResponse();
+           // response.Headers.ContentType="text/event-stream";
+            try {
+                QianWenDTO qianWenDTO = new QianWenDTO();
+                qianWenDTO.model=model;
+                QianWenInputDTO input = new QianWenInputDTO();
+                string? system = dto.system;
+                if (!string.IsNullOrWhiteSpace(system))
+                {
+                    MessageDTO messageDTO = new MessageDTO { role="system", content=system };
+                    dto.messages.Insert(0, messageDTO);
+                }
+                input.messages.AddRange(dto.messages);
+                qianWenDTO.input=input;
+              //  qianWenDTO.parameters= JsonSerializer.Deserialize<JsonElement>(JsonSerializer.Serialize(dto.@params));
+                var httpClient = _httpClientFactory.CreateClient();
+                httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
+                // httpClient.DefaultRequestHeaders.Add("Content-Type", $"application/json");
+              // httpClient.DefaultRequestHeaders.Add("X-DashScope-SSE", "enable");
+
+                JsonSerializerOptions option = new System.Text.Json.JsonSerializerOptions
+                {
+                    Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+                };
+
+                var json = JsonSerializer.Serialize(qianWenDTO, option);
+
+                var content = new StringContent(json, Encoding.UTF8, "application/json");
+                var httpRequest = new HttpRequestMessage(HttpMethod.Post, HOST_URL) { Content =content };
+                HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead);
+                if (httpResponse.IsSuccessStatusCode)
+                {
+                    // 读取响应内容的异步流
+                    StringBuilder data = new StringBuilder();
+                    var contentData = await httpResponse.Content.ReadAsStringAsync();
+                    JsonNode jsonNode = JsonSerializer.Deserialize<JsonNode>(contentData);
+                    contentData= $"{jsonNode["output"]["text"]}";
+
+                    var total_tokens = jsonNode?["usage"]?["total_tokens"];
+                    if (total_tokens!= null)
+                    {
+                        chatResponse.total_tokens=int.Parse($"{total_tokens}");
+                    }
+                    var output_tokens = jsonNode?["usage"]?["output_tokens"];
+                    if (output_tokens!= null)
+                    {
+                        chatResponse.completion_tokens=int.Parse($"{output_tokens}");
+                    }
+
+                    var input_tokens = jsonNode?["usage"]?["input_tokens"];
+                    if (input_tokens!= null)
+                    {
+                        chatResponse.prompt_tokens=int.Parse($"{input_tokens}");
+                    }
+                    //using (var responseStream = await httpResponse.Content.ReadAsStreamAsync())
+                    //{
+                    //    //Debug.Print("============start response use minseconds=" + (DateTime.Now - d).TotalMilliseconds + "  =================\r\n");
+                    //    // 逐块读取并处理响应内容
+                    //    var buffer = new byte[1024];
+                    //    int bytesRead;
+                    //    while ((bytesRead =   responseStream.Read(buffer, 0, buffer.Length)) > 0)
+                    //    {
+                    //        // 处理响应内容
+                    //        string contentData = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead);
+                    //        Console.WriteLine(contentData);
+                    //        await response.WriteAsync(@$"{contentData}");
+                    //        await response.Body.FlushAsync();
+                    //        await Task.Delay(500);
+                    //    }
+                    //}
+
+                    chatResponse.result= contentData;
+                    chatResponse.statusCode= httpResponse.StatusCode;
+                }
+                else {
+                    var contentData = await httpResponse.Content.ReadAsStringAsync();
+                    chatResponse.error=$"{contentData}";
+                    chatResponse.statusCode= httpResponse.StatusCode;
+                }
+            } catch (Exception ex) 
+            {
+                chatResponse.error=$"{ex.Message},{ex.StackTrace}";
+                chatResponse.statusCode= System.Net.HttpStatusCode.InternalServerError;
+
+            }finally {
+                //  response.Body.Close(); 
+                stopwatch.Stop(); // 停止计时
+                chatResponse.time= stopwatch.ElapsedMilliseconds;
+            }
+            return chatResponse;
+        }
+    }
+
+    public class  QianWenDTO
+    {
+        public string? model { get; set; }
+
+        public QianWenInputDTO? input { get; set; }
+
+        public QianWenParameters parameters { get; set; }
+    }
+
+    public class QianWenParameters 
+    {
+        public string result_format { get; set; } = "message";
+        public double temperature { get; set; } = 0.85;
+        public double top_p { get; set; } = 0.8;
+    }
+    public class QianWenInputDTO 
+    {
+        public List<MessageDTO> messages { get; set; } = new List<MessageDTO>();
+    }
+}

+ 277 - 0
HTEXGpt/Services/SparkDeskServiceImpl.cs

@@ -0,0 +1,277 @@
+using HTEXGpt.Models;
+using Newtonsoft.Json.Linq;
+using System.Diagnostics;
+using System.Net.WebSockets;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using static System.Runtime.InteropServices.JavaScript.JSType;
+
+namespace HTEXGpt.Services
+{
+    public class SparkDeskServiceImpl : IModelService
+    {
+        static ClientWebSocket? webSocket0;
+        static CancellationToken cancellation;
+        private string appId = "33cc7806";
+        private string appSecret = "YmVmNWI4YTRmNzFmZDg4MzUyMGM1MzYx";
+
+        private string appKey = "7f6165ce496c92a7e231e80a87fe62e9";
+        private static string HOST_URL = "https://spark-api.xf-yun.com/v3.5/chat";
+        private static string domain = "generalv3.5";
+
+        public SparkDeskServiceImpl(ClientWebSocket webSocket) {
+            webSocket0=webSocket;
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="dto"></param>
+        /// <returns></returns>
+        public async Task<ChatResponse> ChatMessage(ChatRequest dto,HttpContext httpContext ,HttpResponse? response)
+        {
+            //测试,debug到这里的时候你会发现,协议使用的是HTTP/2. APS.NET Core 2.1以上就默认支持HTTP/2,无需额外的配置。再Windows Server2016/Windows10+会自动提供支持。
+            //string requestProtocol = httpContext.Request.Protocol;
+            //响应头部添加text/event-stream,这是HTTP/2协议的一部分。
+            //  response!.Headers.Add("Content-Type", "text/event-stream");
+
+            ChatResponse chatResponse = new ChatResponse();
+            Stopwatch stopwatch = Stopwatch.StartNew();
+          //  response!.Headers.ContentType="text/event-stream";
+            //for (int i = 0; i<100; i++)
+            //{
+            //    // event:ping event是事件字段名,冒号后面是事件名称,不要忘了\n换行符。
+            //    await HttpContext.Response.WriteAsync($"event:ping\n");
+
+            //    // data: 是数据字段名称,冒号后面是数据字段内容。注意数据内容仅仅支持UTF-8,不支持二进制格式。
+            //    // data后面的数据当然可以传递JSON String的。
+            //    await HttpContext.Response.WriteAsync($"data:Controller {i} at {DateTime.Now}\r\r");
+
+            //    // 写入数据到响应后不要忘记 FlushAsync(),因为该api方法是异步的,所以要全程异步,调用同步方法会报错。
+            //    await HttpContext.Response.Body.FlushAsync();
+
+            //    //模拟一个1秒的延迟。
+            //    //await Task.Delay(1000);
+            //}
+            string authUrl = GetAuthUrl();
+
+            StringBuilder sb = new StringBuilder();
+            string url = authUrl.Replace("http://", "ws://").Replace("https://", "wss://");
+            //if (webSocket0!=null) {
+            //    Console.WriteLine();
+            //}
+            //using (webSocket0 = new ClientWebSocket())
+            {
+                try
+                {
+                    await webSocket0!.ConnectAsync(new Uri(url), cancellation);
+
+                    SparkDeskRequest request = new SparkDeskRequest();
+                    request.header = new SparkDeskHeader()
+                    {
+                        app_id = appId,
+                        uid = dto.uid
+                    };
+                    request.parameter = new SparkDeskParameter()
+                    {
+                        chat = new SparkDeskChat()
+                        {
+                            domain = domain,//模型领域,默认为星火通用大模型
+                            temperature = dto.@params.temperature,//温度采样阈值,用于控制生成内容的随机性和多样性,值越大多样性越高;范围(0,1)
+                            //generalv3.5  max_tokens=8192
+                            max_tokens = 8192,//生成内容的最大长度,范围(0,4096)
+                        }
+                    };
+                    request.payload = new SparkDeskPayload()
+                    {
+                        //  new Content() { role = "user", content = "你是谁" },
+                        // new Content() { role = "assistant", content = "....." }, // AI的历史回答结果,这里省略了具体内容,可以根据需要添加更多历史对话信息和最新问题的内容。
+                        message = new SparkDeskMessage()
+                        {
+                            text=dto.messages
+                        }
+                    };
+                    if (null == request.payload.message.text.Find(x=>x.role!.Equals("system"))) 
+                    {
+                        request.payload.message.text.Insert(0, new MessageDTO { role="system", content= dto.system });
+                    }
+                    string jsonString = JsonSerializer.Serialize(request);
+                    //连接成功,开始发送数据
+
+
+                    var frameData2 = System.Text.Encoding.UTF8.GetBytes(jsonString.ToString());
+
+
+                    await webSocket0.SendAsync(new ArraySegment<byte>(frameData2), WebSocketMessageType.Text, true, cancellation);
+
+                    // 接收流式返回结果进行解析
+                    byte[] receiveBuffer = new byte[1024];
+                    WebSocketReceiveResult result = await webSocket0.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), cancellation);
+                   // string resp = "";
+                    while (!result.CloseStatus.HasValue)
+                    {
+                        if (result.MessageType == WebSocketMessageType.Text)
+                        {
+                            string receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, result.Count);
+                            //将结果构造为json
+                            var node = JsonNode.Parse(receivedMessage);
+                            int? code = (int?)node?["header"]?["code"];
+                            //JObject jsonObj = JObject.Parse(receivedMessage);
+                            // int code = (int)jsonObj["header"]["code"];
+
+
+                            if (0==code)
+                            {
+                                // int status = (int)jsonObj["payload"]["choices"]["status"];
+                                int? status = (int?)node?["payload"]?["choices"]?["status"];
+
+                                //JArray textArray = (JArray)jsonObj["payload"]["choices"]["text"];
+                                JsonArray? textArray = (JsonArray?)node?["payload"]?["choices"]?["text"];
+                                string? content = (string?)textArray?[0]?["content"];
+                                // resp += content;
+                                sb.Append(content);
+                                if (status != 2)
+                                {
+
+                                   // await httpContext.Response.WriteAsync($"event:ping\n");
+                                    //await httpContext.Response.WriteAsync($"data:Controller {receivedMessage} at {DateTime.Now}\r\r");
+                                   // await httpContext.Response.Body.FlushAsync();
+
+                                   // Console.WriteLine($"已接收到数据: {receivedMessage}");
+                                }
+                                else
+                                {
+                                    Console.WriteLine($"最后一帧: {receivedMessage}");
+                                    //int totalTokens = (int)jsonObj["payload"]["usage"]["text"]["total_tokens"];
+                                    int? total_tokens = (int?)node?["payload"]?["usage"]?["text"]?["total_tokens"];
+                                    int? prompt_tokens = (int?)node?["payload"]?["usage"]?["text"]?["prompt_tokens"];
+                                    int? completion_tokens = (int?)node?["payload"]?["usage"]?["text"]?["completion_tokens"];
+                                    if (total_tokens.HasValue) { 
+                                        chatResponse.total_tokens = total_tokens.Value;
+                                    }
+                                    if (prompt_tokens.HasValue)
+                                    {
+                                        chatResponse.prompt_tokens = prompt_tokens.Value;
+                                    }
+                                    if (completion_tokens.HasValue)
+                                    {
+                                        chatResponse.completion_tokens = completion_tokens.Value;
+                                    }
+                                    // Console.WriteLine($"整体返回结果: {resp}");
+                                    //Console.WriteLine($"本次消耗token数: {totalTokens}");
+                                    // await httpContext.Response.WriteAsync($"event:ping\n");
+                                    //await httpContext.Response.WriteAsync($"data: {resp} at {DateTime.Now}\r\r");
+                                    // await httpContext.Response.Body.FlushAsync();
+                                    break;
+                                }
+                            }
+                            else
+                            {
+                               // Console.WriteLine($"请求报错: {receivedMessage}");
+                            }
+                        }
+                        else if (result.MessageType == WebSocketMessageType.Close)
+                        {
+                           // Console.WriteLine("已关闭WebSocket连接");
+                            //数据发送完毕后关闭连接。
+                            break;
+                        }
+                        result = await webSocket0.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), cancellation);
+                    }
+                    chatResponse.result= sb.ToString();
+                    chatResponse.statusCode=System.Net.HttpStatusCode.OK;
+                }
+                catch (Exception e)
+                {
+                    chatResponse.error=$"{e.Message},{e.StackTrace}";
+                    chatResponse.statusCode=System.Net.HttpStatusCode.InternalServerError;
+                    //Console.WriteLine($"{e.Message}\n{e.StackTrace}");
+                }
+                finally {
+                    //response.Body.Close();
+                    await webSocket0!.CloseAsync(WebSocketCloseStatus.NormalClosure, "正常关闭", cancellation);
+                    webSocket0.Dispose();
+                    stopwatch.Stop();
+                    chatResponse.time= stopwatch.ElapsedMilliseconds;
+                }
+            }
+            // response.Body.Close();
+            return chatResponse; 
+        }
+        // 返回code为错误码时,请查询https://www.xfyun.cn/document/error-code解决方案
+        public  string GetAuthUrl()
+        {
+            string date = DateTime.UtcNow.ToString("r");
+
+            Uri uri = new Uri(HOST_URL);
+            StringBuilder builder = new StringBuilder("host: ").Append(uri.Host).Append("\n").//
+                                    Append("date: ").Append(date).Append("\n").//
+                                    Append("GET ").Append(uri.LocalPath).Append(" HTTP/1.1");
+
+            string sha = HMACsha256(appSecret, builder.ToString());
+            string authorization = string.Format("api_key=\"{0}\", algorithm=\"{1}\", headers=\"{2}\", signature=\"{3}\"", appKey, "hmac-sha256", "host date request-line", sha);
+            //System.Web.HttpUtility.UrlEncode
+
+            string NewUrl = "https://" + uri.Host + uri.LocalPath;
+
+            string path1 = "authorization" + "=" + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(authorization));
+            date = date.Replace(" ", "%20").Replace(":", "%3A").Replace(",", "%2C");
+            string path2 = "date" + "=" + date;
+            string path3 = "host" + "=" + uri.Host;
+
+            NewUrl = NewUrl + "?" + path1 + "&" + path2 + "&" + path3;
+            return NewUrl;
+        }
+        public static string HMACsha256(string apiSecretIsKey, string buider)
+        {
+            byte[] bytes = System.Text.Encoding.UTF8.GetBytes(apiSecretIsKey);
+            System.Security.Cryptography.HMACSHA256 hMACSHA256 = new System.Security.Cryptography.HMACSHA256(bytes);
+            byte[] date = System.Text.Encoding.UTF8.GetBytes(buider);
+            date = hMACSHA256.ComputeHash(date);
+            hMACSHA256.Clear();
+
+            return Convert.ToBase64String(date);
+
+        }
+    }
+
+
+    //构造请求体
+    public class SparkDeskRequest
+    {
+        public SparkDeskHeader header { get; set; }
+        public SparkDeskParameter parameter { get; set; }
+        public SparkDeskPayload payload { get; set; }
+    }
+
+    public class SparkDeskHeader
+    {
+        public string? app_id { get; set; }
+        public string? uid { get; set; }
+    }
+
+    public class SparkDeskParameter
+    {
+        public SparkDeskChat chat { get; set; }
+    }
+
+    public class SparkDeskChat
+    {
+        public string domain { get; set; }
+        public double temperature { get; set; } = 0.5;
+        public int max_tokens { get; set; }
+    }
+
+    public class SparkDeskPayload
+    {
+        public SparkDeskMessage message { get; set; }
+    }
+
+    public class SparkDeskMessage
+    {
+        public List<MessageDTO> text { get; set; } = new List<MessageDTO>();
+    }
+
+    
+}

+ 8 - 0
HTEXGpt/Views/Home/Index.cshtml

@@ -0,0 +1,8 @@
+@{
+    ViewData["Title"] = "Home Page";
+}
+
+<div class="text-center">
+    <h1 class="display-4">Welcome</h1>
+    <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
+</div>

+ 6 - 0
HTEXGpt/Views/Home/Privacy.cshtml

@@ -0,0 +1,6 @@
+@{
+    ViewData["Title"] = "Privacy Policy";
+}
+<h1>@ViewData["Title"]</h1>
+
+<p>Use this page to detail your site's privacy policy.</p>

+ 25 - 0
HTEXGpt/Views/Shared/Error.cshtml

@@ -0,0 +1,25 @@
+@model ErrorViewModel
+@{
+    ViewData["Title"] = "Error";
+}
+
+<h1 class="text-danger">Error.</h1>
+<h2 class="text-danger">An error occurred while processing your request.</h2>
+
+@if (Model.ShowRequestId)
+{
+    <p>
+        <strong>Request ID:</strong> <code>@Model.RequestId</code>
+    </p>
+}
+
+<h3>Development Mode</h3>
+<p>
+    Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
+</p>
+<p>
+    <strong>The Development environment shouldn't be enabled for deployed applications.</strong>
+    It can result in displaying sensitive information from exceptions to end users.
+    For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
+    and restarting the app.
+</p>

+ 49 - 0
HTEXGpt/Views/Shared/_Layout.cshtml

@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>@ViewData["Title"] - HTEXGpt</title>
+    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
+    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
+    <link rel="stylesheet" href="~/HTEXGpt.styles.css" asp-append-version="true" />
+</head>
+<body>
+    <header>
+        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
+            <div class="container-fluid">
+                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">HTEXGpt</a>
+                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
+                        aria-expanded="false" aria-label="Toggle navigation">
+                    <span class="navbar-toggler-icon"></span>
+                </button>
+                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
+                    <ul class="navbar-nav flex-grow-1">
+                        <li class="nav-item">
+                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+        </nav>
+    </header>
+    <div class="container">
+        <main role="main" class="pb-3">
+            @RenderBody()
+        </main>
+    </div>
+
+    <footer class="border-top footer text-muted">
+        <div class="container">
+            &copy; 2024 - HTEXGpt - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
+        </div>
+    </footer>
+    <script src="~/lib/jquery/dist/jquery.min.js"></script>
+    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
+    <script src="~/js/site.js" asp-append-version="true"></script>
+    @await RenderSectionAsync("Scripts", required: false)
+</body>
+</html>

+ 48 - 0
HTEXGpt/Views/Shared/_Layout.cshtml.css

@@ -0,0 +1,48 @@
+/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
+for details on configuring this project to bundle and minify static web assets. */
+
+a.navbar-brand {
+  white-space: normal;
+  text-align: center;
+  word-break: break-all;
+}
+
+a {
+  color: #0077cc;
+}
+
+.btn-primary {
+  color: #fff;
+  background-color: #1b6ec2;
+  border-color: #1861ac;
+}
+
+.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
+  color: #fff;
+  background-color: #1b6ec2;
+  border-color: #1861ac;
+}
+
+.border-top {
+  border-top: 1px solid #e5e5e5;
+}
+.border-bottom {
+  border-bottom: 1px solid #e5e5e5;
+}
+
+.box-shadow {
+  box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
+}
+
+button.accept-policy {
+  font-size: 1rem;
+  line-height: inherit;
+}
+
+.footer {
+  position: absolute;
+  bottom: 0;
+  width: 100%;
+  white-space: nowrap;
+  line-height: 60px;
+}

+ 2 - 0
HTEXGpt/Views/Shared/_ValidationScriptsPartial.cshtml

@@ -0,0 +1,2 @@
+<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
+<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

+ 3 - 0
HTEXGpt/Views/_ViewImports.cshtml

@@ -0,0 +1,3 @@
+@using HTEXGpt
+@using HTEXGpt.Models
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

+ 3 - 0
HTEXGpt/Views/_ViewStart.cshtml

@@ -0,0 +1,3 @@
+@{
+    Layout = "_Layout";
+}

+ 8 - 0
HTEXGpt/appsettings.Development.json

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

+ 9 - 0
HTEXGpt/appsettings.json

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

+ 22 - 0
HTEXGpt/wwwroot/css/site.css

@@ -0,0 +1,22 @@
+html {
+  font-size: 14px;
+}
+
+@media (min-width: 768px) {
+  html {
+    font-size: 16px;
+  }
+}
+
+.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
+  box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
+}
+
+html {
+  position: relative;
+  min-height: 100%;
+}
+
+body {
+  margin-bottom: 60px;
+}

BIN
HTEXGpt/wwwroot/favicon.ico


+ 4 - 0
HTEXGpt/wwwroot/js/site.js

@@ -0,0 +1,4 @@
+// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
+// for details on configuring this project to bundle and minify static web assets.
+
+// Write your JavaScript code.

+ 22 - 0
HTEXGpt/wwwroot/lib/bootstrap/LICENSE

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2011-2021 Twitter, Inc.
+Copyright (c) 2011-2021 The Bootstrap Authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 4997 - 0
HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 7 - 0
HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 4996 - 0
HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 7 - 0
HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map


+ 427 - 0
HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css

@@ -0,0 +1,427 @@
+/*!
+ * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
+ * Copyright 2011-2021 The Bootstrap Authors
+ * Copyright 2011-2021 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
+ */
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+  :root {
+    scroll-behavior: smooth;
+  }
+}
+
+body {
+  margin: 0;
+  font-family: var(--bs-body-font-family);
+  font-size: var(--bs-body-font-size);
+  font-weight: var(--bs-body-font-weight);
+  line-height: var(--bs-body-line-height);
+  color: var(--bs-body-color);
+  text-align: var(--bs-body-text-align);
+  background-color: var(--bs-body-bg);
+  -webkit-text-size-adjust: 100%;
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+hr {
+  margin: 1rem 0;
+  color: inherit;
+  background-color: currentColor;
+  border: 0;
+  opacity: 0.25;
+}
+
+hr:not([size]) {
+  height: 1px;
+}
+
+h6, h5, h4, h3, h2, h1 {
+  margin-top: 0;
+  margin-bottom: 0.5rem;
+  font-weight: 500;
+  line-height: 1.2;
+}
+
+h1 {
+  font-size: calc(1.375rem + 1.5vw);
+}
+@media (min-width: 1200px) {
+  h1 {
+    font-size: 2.5rem;
+  }
+}
+
+h2 {
+  font-size: calc(1.325rem + 0.9vw);
+}
+@media (min-width: 1200px) {
+  h2 {
+    font-size: 2rem;
+  }
+}
+
+h3 {
+  font-size: calc(1.3rem + 0.6vw);
+}
+@media (min-width: 1200px) {
+  h3 {
+    font-size: 1.75rem;
+  }
+}
+
+h4 {
+  font-size: calc(1.275rem + 0.3vw);
+}
+@media (min-width: 1200px) {
+  h4 {
+    font-size: 1.5rem;
+  }
+}
+
+h5 {
+  font-size: 1.25rem;
+}
+
+h6 {
+  font-size: 1rem;
+}
+
+p {
+  margin-top: 0;
+  margin-bottom: 1rem;
+}
+
+abbr[title],
+abbr[data-bs-original-title] {
+  -webkit-text-decoration: underline dotted;
+  text-decoration: underline dotted;
+  cursor: help;
+  -webkit-text-decoration-skip-ink: none;
+  text-decoration-skip-ink: none;
+}
+
+address {
+  margin-bottom: 1rem;
+  font-style: normal;
+  line-height: inherit;
+}
+
+ol,
+ul {
+  padding-left: 2rem;
+}
+
+ol,
+ul,
+dl {
+  margin-top: 0;
+  margin-bottom: 1rem;
+}
+
+ol ol,
+ul ul,
+ol ul,
+ul ol {
+  margin-bottom: 0;
+}
+
+dt {
+  font-weight: 700;
+}
+
+dd {
+  margin-bottom: 0.5rem;
+  margin-left: 0;
+}
+
+blockquote {
+  margin: 0 0 1rem;
+}
+
+b,
+strong {
+  font-weight: bolder;
+}
+
+small {
+  font-size: 0.875em;
+}
+
+mark {
+  padding: 0.2em;
+  background-color: #fcf8e3;
+}
+
+sub,
+sup {
+  position: relative;
+  font-size: 0.75em;
+  line-height: 0;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+sup {
+  top: -0.5em;
+}
+
+a {
+  color: #0d6efd;
+  text-decoration: underline;
+}
+a:hover {
+  color: #0a58ca;
+}
+
+a:not([href]):not([class]), a:not([href]):not([class]):hover {
+  color: inherit;
+  text-decoration: none;
+}
+
+pre,
+code,
+kbd,
+samp {
+  font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+  font-size: 1em;
+  direction: ltr /* rtl:ignore */;
+  unicode-bidi: bidi-override;
+}
+
+pre {
+  display: block;
+  margin-top: 0;
+  margin-bottom: 1rem;
+  overflow: auto;
+  font-size: 0.875em;
+}
+pre code {
+  font-size: inherit;
+  color: inherit;
+  word-break: normal;
+}
+
+code {
+  font-size: 0.875em;
+  color: #d63384;
+  word-wrap: break-word;
+}
+a > code {
+  color: inherit;
+}
+
+kbd {
+  padding: 0.2rem 0.4rem;
+  font-size: 0.875em;
+  color: #fff;
+  background-color: #212529;
+  border-radius: 0.2rem;
+}
+kbd kbd {
+  padding: 0;
+  font-size: 1em;
+  font-weight: 700;
+}
+
+figure {
+  margin: 0 0 1rem;
+}
+
+img,
+svg {
+  vertical-align: middle;
+}
+
+table {
+  caption-side: bottom;
+  border-collapse: collapse;
+}
+
+caption {
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+  color: #6c757d;
+  text-align: left;
+}
+
+th {
+  text-align: inherit;
+  text-align: -webkit-match-parent;
+}
+
+thead,
+tbody,
+tfoot,
+tr,
+td,
+th {
+  border-color: inherit;
+  border-style: solid;
+  border-width: 0;
+}
+
+label {
+  display: inline-block;
+}
+
+button {
+  border-radius: 0;
+}
+
+button:focus:not(:focus-visible) {
+  outline: 0;
+}
+
+input,
+button,
+select,
+optgroup,
+textarea {
+  margin: 0;
+  font-family: inherit;
+  font-size: inherit;
+  line-height: inherit;
+}
+
+button,
+select {
+  text-transform: none;
+}
+
+[role=button] {
+  cursor: pointer;
+}
+
+select {
+  word-wrap: normal;
+}
+select:disabled {
+  opacity: 1;
+}
+
+[list]::-webkit-calendar-picker-indicator {
+  display: none;
+}
+
+button,
+[type=button],
+[type=reset],
+[type=submit] {
+  -webkit-appearance: button;
+}
+button:not(:disabled),
+[type=button]:not(:disabled),
+[type=reset]:not(:disabled),
+[type=submit]:not(:disabled) {
+  cursor: pointer;
+}
+
+::-moz-focus-inner {
+  padding: 0;
+  border-style: none;
+}
+
+textarea {
+  resize: vertical;
+}
+
+fieldset {
+  min-width: 0;
+  padding: 0;
+  margin: 0;
+  border: 0;
+}
+
+legend {
+  float: left;
+  width: 100%;
+  padding: 0;
+  margin-bottom: 0.5rem;
+  font-size: calc(1.275rem + 0.3vw);
+  line-height: inherit;
+}
+@media (min-width: 1200px) {
+  legend {
+    font-size: 1.5rem;
+  }
+}
+legend + * {
+  clear: left;
+}
+
+::-webkit-datetime-edit-fields-wrapper,
+::-webkit-datetime-edit-text,
+::-webkit-datetime-edit-minute,
+::-webkit-datetime-edit-hour-field,
+::-webkit-datetime-edit-day-field,
+::-webkit-datetime-edit-month-field,
+::-webkit-datetime-edit-year-field {
+  padding: 0;
+}
+
+::-webkit-inner-spin-button {
+  height: auto;
+}
+
+[type=search] {
+  outline-offset: -2px;
+  -webkit-appearance: textfield;
+}
+
+/* rtl:raw:
+[type="tel"],
+[type="url"],
+[type="email"],
+[type="number"] {
+  direction: ltr;
+}
+*/
+::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+::-webkit-color-swatch-wrapper {
+  padding: 0;
+}
+
+::file-selector-button {
+  font: inherit;
+}
+
+::-webkit-file-upload-button {
+  font: inherit;
+  -webkit-appearance: button;
+}
+
+output {
+  display: inline-block;
+}
+
+iframe {
+  border: 0;
+}
+
+summary {
+  display: list-item;
+  cursor: pointer;
+}
+
+progress {
+  vertical-align: baseline;
+}
+
+[hidden] {
+  display: none !important;
+}
+
+/*# sourceMappingURL=bootstrap-reboot.css.map */

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 8 - 0
HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css


+ 0 - 0
HTEXGpt/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.