diff --git a/Backend/Backend.csproj b/Backend/Backend.csproj index ae412bd..daebb6f 100644 --- a/Backend/Backend.csproj +++ b/Backend/Backend.csproj @@ -23,8 +23,10 @@ + + diff --git a/Backend/Quartz/JobSchedule.cs b/Backend/Quartz/JobSchedule.cs new file mode 100644 index 0000000..7d62e67 --- /dev/null +++ b/Backend/Quartz/JobSchedule.cs @@ -0,0 +1,198 @@ +using Microsoft.Extensions.Logging; +using Repository.BackendRepository.Implement; +using Repository.BackendRepository.Interface; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; +using System.Threading.Tasks; +using NCrontab; +using static NCrontab.CrontabSchedule; + +namespace Backend.Quartz +{ + /// + /// Job調度中間對象 + /// + public class JobSchedule + { + public JobSchedule(Type jobType, string cronExpression) + { + this.JobType = jobType ?? throw new ArgumentNullException(nameof(jobType)); + CronExpression = cronExpression ?? throw new ArgumentNullException(nameof(cronExpression)); + } + /// + /// Job類型 + /// + public Type JobType { get; private set; } + /// + /// Cron表達式 + /// + public string CronExpression { get; private set; } + /// + /// Job狀態 + /// + public JobStatus JobStatu { get; set; } = JobStatus.Init; + } + /// + /// Job運行狀態 + /// + public enum JobStatus : byte + { + [Description("初始化")] + Init = 0, + [Description("運行中")] + Running = 1, + [Description("調度中")] + Scheduling = 2, + [Description("已停止")] + Stopped = 3, + } + + public class Task_Detail + { + private readonly IBackendRepository backendRepository; + private readonly ILogger logger; + public Task_Detail(ILogger logger, IBackendRepository backendRepository) + { + this.logger = logger; + this.backendRepository = backendRepository; + } + /// + /// 取得時間規則 + /// + /// + /// + /// + private async Task GetWorkRule(string task,string task_item) + { + string Times = null; + try + { + var sql = $@"select b.system_value from task_detail a + join variable b on a.variable_id = b.id + where a.task = '{task}' and a.task_item = '{task_item}'"; + Times = await backendRepository.GetOneAsync(sql); + } + catch(Exception exception) + { + logger.LogError("【Task_Detail】【任務時間獲取失敗】"); + logger.LogError("【Task_Detail】【任務時間獲取失敗】[Exception]:{0},Task:{1},task_item:{2}", exception.ToString(),task,task_item); + } + return Times; + } + /// + /// 是否執行任務 + /// + /// + /// + /// + public async Task GetNeedWorkTask(string task, string task_item) + { + try + { + var sql = $@"select a.lastwork_time from task_detail a + where a.task = '{task}' and a.task_item = '{task_item}'"; + + var lastworkTime = await backendRepository.GetOneAsync(sql); + + + + DateTime dateTime = lastworkTime != null ? Convert.ToDateTime(lastworkTime) : Convert.ToDateTime("1970-01-01 00:00:01"); + + var nextTime = CrontabSchedule.Parse(await GetWorkRule(task, task_item), new ParseOptions { IncludingSeconds = true } ).GetNextOccurrence(dateTime); + if (DateTime.Now >= nextTime) + { + return true; + } + } + catch(Exception exception) + { + logger.LogError("【Task_Detail】【時間解析失敗】[Exception]:{0},Task:{1},task_item:{2}", exception.ToString(), task, task_item); + } + return false; + } + /// + /// 紀錄任務開始執行時間 + /// + /// + /// + /// + public async Task InsertWorkTime(string task, string task_item ,string LoggerWord = null) + { + try + { + Dictionary worktime = new Dictionary() + { + { "@lastwork_time", DateTime.Now}, + { "@success", 2}, + { "@updated_at", DateTime.Now}, + }; + await backendRepository.UpdateOneByCustomTable(worktime, "task_detail", $" task = '{task}' and task_item = '{task_item}'"); + + if(LoggerWord == null) + { + logger.LogInformation($"【Task_Detail】【開始{task},{task_item}任務】"); + } + logger.LogInformation($"【Task_Detail】【{LoggerWord}】"); + } + catch (Exception exception) + { + logger.LogError("【Task_Detail】【任務輸入開始時間】[Exception]:{0},Task:{1},task_item:{2}", exception.ToString(), task, task_item); + } + } + /// + /// 紀錄任務結束時間 + /// + /// + /// + /// + public async Task InsertWorkTime_End(string task, string task_item,string LoggerWord = null) + { + try + { + Dictionary worktime = new Dictionary() + { + { "@success", 0}, + { "@lastwork_end_time", DateTime.Now}, + { "@updated_at", DateTime.Now}, + }; + await backendRepository.UpdateOneByCustomTable(worktime, "task_detail", $" task = '{task}' and task_item = '{task_item}'"); + if (LoggerWord == null) + { + logger.LogInformation($"【Task_Detail】【結束{task},{task_item}任務】"); + } + logger.LogInformation($"【Task_Detail】【{LoggerWord}】"); + } + catch (Exception exception) + { + logger.LogError("【Task_Detail】【任務輸入結束時間】[Exception]:{0},Task:{1},task_item:{2}", exception.ToString(), task, task_item); + } + } + /// + /// 執行失敗紀錄 + /// + /// + /// + /// + public async Task WorkFail(string task, string task_item,string reason = "") + { + try + { + Dictionary worktime = new Dictionary() + { + { "@success", 1}, + { "@updated_at", DateTime.Now}, + }; + await backendRepository.UpdateOneByCustomTable(worktime, "task_detail", $" task = '{task}' and task_item = '{task_item}'"); + logger.LogError("【Task_Detail】【任務執行失敗】[Exception]:{0},Task:{1},task_item:{2}", reason, task, task_item); + } + catch (Exception exception) + { + logger.LogError("【Task_Detail】【任務執行失敗】[Exception]:{0},Task:{1},task_item:{2}", exception.ToString(), task, task_item); + } + } + + } + +} diff --git a/Backend/Quartz/Jobs/ArchiveElectricMeterDayJob.cs b/Backend/Quartz/Jobs/ArchiveElectricMeterDayJob.cs new file mode 100644 index 0000000..49244b1 --- /dev/null +++ b/Backend/Quartz/Jobs/ArchiveElectricMeterDayJob.cs @@ -0,0 +1,1461 @@ +using Backend.Models; +using BackendWorkerService.Services.Implement; +using Dapper; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Quartz; +using Repository.BackendRepository.Interface; +using Repository.Helper; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + + +namespace Backend.Quartz.Jobs +{ + [DisallowConcurrentExecution] + class ArchiveElectricMeterDayJob : IJob + { + private readonly ILogger logger; + private readonly IBackgroundServiceRepository backgroundServiceRepository; + private readonly IBackgroundServiceMsSqlRepository backgroundServiceMsSqlRepository; + protected readonly IDatabaseHelper _databaseHelper; + private readonly ILogger loggers; + + + public ArchiveElectricMeterDayJob( + ILogger logger, + IBackgroundServiceRepository backgroundServiceRepository, + IBackgroundServiceMsSqlRepository backgroundServiceMySqlRepository, + IDatabaseHelper databaseHelper, ILogger loggers, IBackendRepository backendRepository) + { + this.logger = logger; + this.backgroundServiceRepository = backgroundServiceRepository; + this.backgroundServiceMsSqlRepository = backgroundServiceMySqlRepository; + this._databaseHelper = databaseHelper; + this.loggers = loggers; + } + + public async Task Execute(IJobExecutionContext context) + { + Task_Detail task_Detail = new Task_Detail(loggers, backgroundServiceRepository); + try + { + if(await task_Detail.GetNeedWorkTask("ArchiveElectricMeterDayJob", "All")) + { + await task_Detail.InsertWorkTime("ArchiveElectricMeterDayJob", "All", "水電表開始任務開始"); + EDFunction ed = new EDFunction(); + XmlDocument xmlDocument = new XmlDocument(); + + var sqlArchive = $@"SELECT system_value as Value, system_key as Name FROM variable WHERE deleted = 0 AND system_type = 'archiveConfig'"; + var saveToMSDB = await backgroundServiceRepository.GetOneAsync("select system_value from variable where system_type = 'save_to_ms_db' and deleted = 0"); + + var variableArchive = await backgroundServiceRepository.GetAllAsync(sqlArchive); + var electricMeterGuid = variableArchive.Where(x => x.Name == "ElectricMeterGuid").Select(x => x.Value).FirstOrDefault(); + var waterMeterGuid = variableArchive.Where(x => x.Name == "WaterMeterGuid").Select(x => x.Value).FirstOrDefault(); + + #region 找出所有電錶設備 + var sWhere = "deleted = 0 AND device_name_tag = @sub_system_guid"; + var electricMeters = await backgroundServiceRepository.GetAllAsync("device", sWhere, new { sub_system_guid = electricMeterGuid }); + var waterMeters = await backgroundServiceRepository.GetAllAsync("device", sWhere, new { sub_system_guid = waterMeterGuid }); + #endregion 找出所有電錶設備 + + #region 找出所有電錶系統的點位 + var sPointWhere = "deleted = 0 AND device_name_tag = @sub_system_guid"; + var electricPoints = await backgroundServiceRepository.GetAllAsync("device_item", sPointWhere, new { sub_system_guid = electricMeterGuid }); + var waterPoints = await backgroundServiceRepository.GetAllAsync("device_item", sPointWhere, new { sub_system_guid = waterMeterGuid }); + #endregion 找出所有電錶系統的點位 + + #region 組合出所有電錶設備點位 + List electricDeviceNumberPoints = new List(); + foreach (var electricMeter in electricMeters) + { + foreach (var point in electricPoints) + { + DeviceNumberPoint deviceNumberPoint = new DeviceNumberPoint(); + deviceNumberPoint.DeviceNumber = electricMeter.Device_number; + deviceNumberPoint.Point = point.points; + deviceNumberPoint.FullDeviceNumberPoint = string.Format("{0}_{1}", electricMeter.Device_number, point.points); + + electricDeviceNumberPoints.Add(deviceNumberPoint); + } + } + #endregion 組合出所有電錶設備點位 + #region 組合出所有水錶設備點位 + List waterDeviceNumberPoints = new List(); + foreach (var waterMeter in waterMeters) + { + foreach (var point in waterPoints) + { + DeviceNumberPoint deviceNumberPoint = new DeviceNumberPoint(); + deviceNumberPoint.DeviceNumber = waterMeter.Device_number; + deviceNumberPoint.Point = point.points; + deviceNumberPoint.FullDeviceNumberPoint = string.Format("{0}_{1}", waterMeter.Device_number, point.points); + + waterDeviceNumberPoints.Add(deviceNumberPoint); + } + } + #endregion 組合出所有電錶設備點位 + + #region 取得obix 設定 + var obixApiConfig = new ObixApiConfig(); + + var sqlObix = $@"SELECT system_value as Value, system_key as Name FROM variable WHERE deleted = 0 AND system_type = 'obixConfig'"; + + var variableObix = await backgroundServiceRepository.GetAllAsync(sqlObix); + obixApiConfig.ApiBase = variableObix.Where(x => x.Name == "ApiBase").Select(x => x.Value).FirstOrDefault(); + obixApiConfig.UserName = ed.AESDecrypt(variableObix.Where(x => x.Name == "UserName").Select(x => x.Value).FirstOrDefault()); + obixApiConfig.Password = ed.AESDecrypt(variableObix.Where(x => x.Name == "Password").Select(x => x.Value).FirstOrDefault()); + + String encoded = System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(obixApiConfig.UserName + ":" + obixApiConfig.Password)); + #endregion 取得obix 設定 + + var now = DateTime.Now; + + #region 天歸檔 + if (await task_Detail.GetNeedWorkTask("ArchiveElectricMeterDayJob", "Day")) + { + try + { + await task_Detail.InsertWorkTime("ArchiveElectricMeterDayJob", "Day", "水電表天任務開始"); + var preDay = now.AddDays(-1); //取得前一天 + var dbDateName = preDay.Year.ToString().PadLeft(4, '0') + preDay.Month.ToString().PadLeft(2, '0'); + + var startTimestamp = string.Format("{0}T00:00:00.000+08:00", preDay.ToString("yyyy-MM-dd")); + var endTimestamp = string.Format("{0}T23:59:59.000+08:00", preDay.ToString("yyyy-MM-dd")); + + var historyQueryFilter = $@" + + + + "; + + //Stopwatch stopWatch = new Stopwatch(); + //stopWatch.Start(); + + //抓取每個設備的資料 + List> electericArchiveDayRawDatas = new List>(); + List> waterArchiveDayRawDatas = new List>(); + foreach (var deviceNumberPoint in electricDeviceNumberPoints) + { + + HttpWebRequest archiveDayRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/{deviceNumberPoint.FullDeviceNumberPoint}/~historyRollup/"); + //HttpWebRequest archiveDayRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/H_E1_B1F_MVCB_MVCBH_V1/~historyRollup/"); + archiveDayRequest.Method = "POST"; + archiveDayRequest.Headers.Add("Authorization", "Basic " + encoded); + archiveDayRequest.PreAuthenticate = true; + + byte[] byteArray = Encoding.UTF8.GetBytes(historyQueryFilter); + using (Stream reqStream = archiveDayRequest.GetRequestStream()) + { + reqStream.Write(byteArray, 0, byteArray.Length); + } + + HttpWebResponse archiveDayResponse = (HttpWebResponse)archiveDayRequest.GetResponse(); + var archiveDayResponseContent = new StreamReader(archiveDayResponse.GetResponseStream()).ReadToEnd(); + + xmlDocument.LoadXml(archiveDayResponseContent); + string archiveDayJson = JsonConvert.SerializeXmlNode(xmlDocument); + JObject archiveDayJsonResult = (JObject)JsonConvert.DeserializeObject(archiveDayJson); + + if (archiveDayJsonResult.ContainsKey("err")) //抓取錯誤 + { + //logger.LogError("【ArchiveElectricMeterDayJob】【天歸檔】【取得資料失敗】"); + //logger.LogError("【ArchiveElectricMeterDayJob】【天歸檔】【取得資料失敗】[錯誤內容]:{0}", archiveDayJsonResult); + + Dictionary archiveDayRawData = new Dictionary(); + archiveDayRawData.Add("@device_number", deviceNumberPoint.DeviceNumber); + archiveDayRawData.Add("@point", deviceNumberPoint.Point); + archiveDayRawData.Add("@start_timestamp", startTimestamp.Replace("T", " ").Substring(0, 19)); + archiveDayRawData.Add("@end_timestamp", endTimestamp.Replace("T", " ").Substring(0, 19)); + archiveDayRawData.Add("@is_complete", 0); + archiveDayRawData.Add("@repeat_times", 0); + archiveDayRawData.Add("@fail_reason", archiveDayJson); + + archiveDayRawData.Add("@count_rawdata", 0); + archiveDayRawData.Add("@min_rawdata", 0); + archiveDayRawData.Add("@max_rawdata", 0); + archiveDayRawData.Add("@avg_rawdata", 0); + archiveDayRawData.Add("@sum_rawdata", 0); + archiveDayRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + electericArchiveDayRawDatas.Add(archiveDayRawData); + } + + if (archiveDayJsonResult.ContainsKey("obj")) //表示可以讀取到內容 + { + var ArrangeRawDatas = ArrangeRawData(deviceNumberPoint, archiveDayJsonResult); + if (ArrangeRawDatas != null && ArrangeRawDatas.Count() > 0) + { + electericArchiveDayRawDatas.AddRange(ArrangeRawDatas); + } + } + } + foreach (var deviceNumberPoint in waterDeviceNumberPoints) + { + + HttpWebRequest archiveDayRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/{deviceNumberPoint.FullDeviceNumberPoint}/~historyRollup/"); + //HttpWebRequest archiveDayRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/H_E1_B1F_MVCB_MVCBH_V1/~historyRollup/"); + archiveDayRequest.Method = "POST"; + archiveDayRequest.Headers.Add("Authorization", "Basic " + encoded); + archiveDayRequest.PreAuthenticate = true; + + byte[] byteArray = Encoding.UTF8.GetBytes(historyQueryFilter); + using (Stream reqStream = archiveDayRequest.GetRequestStream()) + { + reqStream.Write(byteArray, 0, byteArray.Length); + } + + HttpWebResponse archiveDayResponse = (HttpWebResponse)archiveDayRequest.GetResponse(); + var archiveDayResponseContent = new StreamReader(archiveDayResponse.GetResponseStream()).ReadToEnd(); + + xmlDocument.LoadXml(archiveDayResponseContent); + string archiveDayJson = JsonConvert.SerializeXmlNode(xmlDocument); + JObject archiveDayJsonResult = (JObject)JsonConvert.DeserializeObject(archiveDayJson); + + if (archiveDayJsonResult.ContainsKey("err")) //抓取錯誤 + { + //logger.LogError("【ArchiveElectricMeterDayJob】【天歸檔】【取得資料失敗】"); + //logger.LogError("【ArchiveElectricMeterDayJob】【天歸檔】【取得資料失敗】[錯誤內容]:{0}", archiveDayJsonResult); + + Dictionary archiveDayRawData = new Dictionary(); + archiveDayRawData.Add("@device_number", deviceNumberPoint.DeviceNumber); + archiveDayRawData.Add("@point", deviceNumberPoint.Point); + archiveDayRawData.Add("@start_timestamp", startTimestamp.Replace("T", " ").Substring(0, 19)); + archiveDayRawData.Add("@end_timestamp", endTimestamp.Replace("T", " ").Substring(0, 19)); + archiveDayRawData.Add("@is_complete", 0); + archiveDayRawData.Add("@repeat_times", 0); + archiveDayRawData.Add("@fail_reason", archiveDayJson); + + archiveDayRawData.Add("@count_rawdata", 0); + archiveDayRawData.Add("@min_rawdata", 0); + archiveDayRawData.Add("@max_rawdata", 0); + archiveDayRawData.Add("@avg_rawdata", 0); + archiveDayRawData.Add("@sum_rawdata", 0); + archiveDayRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + waterArchiveDayRawDatas.Add(archiveDayRawData); + } + + if (archiveDayJsonResult.ContainsKey("obj")) //表示可以讀取到內容 + { + var ArrangeRawDatas = ArrangeRawData(deviceNumberPoint, archiveDayJsonResult); + if (ArrangeRawDatas != null && ArrangeRawDatas.Count() > 0) + { + waterArchiveDayRawDatas.AddRange(ArrangeRawDatas); + } + } + } + + //stopWatch.Stop(); + //logger.LogInformation("【ArchiveElectricMeterDayJob】【天歸檔】【效能檢驗】[取得資料花費時間]{0} 毫秒", stopWatch.ElapsedMilliseconds); + + if (electericArchiveDayRawDatas.Count() > 0) + { + var sql = $@"CREATE TABLE IF NOT EXISTS `archive_electric_meter_day_{dbDateName}` ( + `device_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `point` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `start_timestamp` datetime(6) NOT NULL, + `end_timestamp` datetime(6) NULL DEFAULT NULL, + `count_rawdata` int(11) NULL DEFAULT NULL, + `min_rawdata` decimal(15, 3) NULL DEFAULT NULL, + `max_rawdata` decimal(15, 3) NULL DEFAULT NULL, + `avg_rawdata` decimal(15, 3) NULL DEFAULT NULL, + `sum_rawdata` decimal(15, 3) NULL DEFAULT NULL, + `is_complete` tinyint(3) UNSIGNED NULL DEFAULT NULL COMMENT '是否完成,0:未完成 1:完成', + `repeat_times` int(11) NULL DEFAULT 0 COMMENT '重複次數', + `fail_reason` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '失敗原因', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime(6) NULL DEFAULT NULL, + PRIMARY KEY (`device_number`, `point`, `start_timestamp`) USING BTREE + ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + + SET FOREIGN_KEY_CHECKS = 1; + UPDATE archive_electric_meter_day_{dbDateName} SET + count_rawdata = @count_rawdata, + min_rawdata = @min_rawdata, + max_rawdata = @max_rawdata, + avg_rawdata = @avg_rawdata, + sum_rawdata = @sum_rawdata, + is_complete = @is_complete, + repeat_times = @repeat_times, + fail_reason = @fail_reason, + updated_at = @updated_at + WHERE device_number = @device_number + AND point = @point + AND start_timestamp = @start_timestamp; + + INSERT INTO archive_electric_meter_day_{dbDateName} ( + device_number, + point, + start_timestamp, + end_timestamp, + count_rawdata, + min_rawdata, + max_rawdata, + avg_rawdata, + sum_rawdata, + is_complete, + repeat_times, + fail_reason) + SELECT + @device_number, + @point, + @start_timestamp, + @end_timestamp, + @count_rawdata, + @min_rawdata, + @max_rawdata, + @avg_rawdata, + @sum_rawdata, + @is_complete, + @repeat_times, + @fail_reason + WHERE ROW_COUNT() = 0;"; + + var mySql = $@"BEGIN TRANSACTION; + IF OBJECT_ID(N'dbo.archive_electric_meter_day_{dbDateName}', N'U') is null + BEGIN + CREATE TABLE [dbo].[archive_electric_meter_day_{dbDateName}]( + [device_number] [varchar](50) NOT NULL, + [point] [varchar](20) NOT NULL, + [start_timestamp] [datetime] NOT NULL, + [end_timestamp] [datetime] NULL, + [count_rawdata] [int] NULL, + [min_rawdata] [decimal](15, 3) NULL, + [max_rawdata] [decimal](15, 3) NULL, + [avg_rawdata] [decimal](15, 3) NULL, + [sum_rawdata] [decimal](15, 3) NULL, + [is_complete] [tinyint] NULL, + [repeat_times] [int] NULL, + [fail_reason] [nvarchar](max) NULL, + [created_at] [datetime] NULL, + [updated_at] [datetime] NULL, + CONSTRAINT [PK_archive_electric_meter_day_{dbDateName}] PRIMARY KEY CLUSTERED + ( + [device_number] ASC, + [point] ASC, + [start_timestamp] ASC + )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] + ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] + + ALTER TABLE [dbo].[archive_electric_meter_day_{dbDateName}] ADD CONSTRAINT [DF_archive_electric_meter_day_{dbDateName}_repeat_times] DEFAULT ((0)) FOR [repeat_times] + + ALTER TABLE [dbo].[archive_electric_meter_day_{dbDateName}] ADD CONSTRAINT [DF_archive_electric_meter_day_{dbDateName}_created_at] DEFAULT (getdate()) FOR [created_at] + + ALTER TABLE [dbo].[archive_electric_meter_day_{dbDateName}] ADD CONSTRAINT [DF_archive_electric_meter_day_{dbDateName}_updated_at] DEFAULT (NULL) FOR [updated_at] + + EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'是否完成,0:未完成 1:完成' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'archive_electric_meter_day_{dbDateName}', @level2type=N'COLUMN',@level2name=N'is_complete' + + EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'重複次數' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'archive_electric_meter_day_{dbDateName}', @level2type=N'COLUMN',@level2name=N'repeat_times' + + EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'失敗原因' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'archive_electric_meter_day_{dbDateName}', @level2type=N'COLUMN',@level2name=N'fail_reason' + END + + UPDATE archive_electric_meter_day_{dbDateName} SET + count_rawdata = @count_rawdata, + min_rawdata = @min_rawdata, + max_rawdata = @max_rawdata, + avg_rawdata = @avg_rawdata, + sum_rawdata = @sum_rawdata, + is_complete = @is_complete, + repeat_times = @repeat_times, + fail_reason = @fail_reason, + updated_at = @updated_at + WHERE device_number = @device_number + AND point = @point + AND start_timestamp = @start_timestamp; + + IF @@ROWCOUNT = 0 + BEGIN + INSERT INTO archive_electric_meter_day_{dbDateName} ( + device_number, + point, + start_timestamp, + end_timestamp, + count_rawdata, + min_rawdata, + max_rawdata, + avg_rawdata, + sum_rawdata, + is_complete, + repeat_times, + fail_reason) + VALUES ( + @device_number, + @point, + @start_timestamp, + @end_timestamp, + @count_rawdata, + @min_rawdata, + @max_rawdata, + @avg_rawdata, + @sum_rawdata, + @is_complete, + @repeat_times, + @fail_reason) + END + + COMMIT TRANSACTION;"; + await backgroundServiceRepository.ExecuteSql(sql, electericArchiveDayRawDatas); + if (!string.IsNullOrEmpty(saveToMSDB) && saveToMSDB == "1") + { + await backgroundServiceMsSqlRepository.ExecuteSql(mySql, electericArchiveDayRawDatas); + } + } + if (waterArchiveDayRawDatas.Count() > 0) + { + var sql = $@" + CREATE TABLE IF NOT EXISTS `archive_water_meter_day_{dbDateName}` ( + `device_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `point` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `start_timestamp` datetime(6) NOT NULL, + `end_timestamp` datetime(6) NULL DEFAULT NULL, + `count_rawdata` int(11) NULL DEFAULT NULL, + `min_rawdata` decimal(15, 3) NULL DEFAULT NULL, + `max_rawdata` decimal(15, 3) NULL DEFAULT NULL, + `avg_rawdata` decimal(15, 3) NULL DEFAULT NULL, + `sum_rawdata` decimal(15, 3) NULL DEFAULT NULL, + `is_complete` tinyint(3) UNSIGNED NULL DEFAULT NULL COMMENT '是否完成,0:未完成 1:完成', + `repeat_times` int(11) NULL DEFAULT 0 COMMENT '重複次數', + `fail_reason` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '失敗原因', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime(6) NULL DEFAULT NULL, + PRIMARY KEY (`device_number`, `point`, `start_timestamp`) USING BTREE + ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + + UPDATE archive_water_meter_day_{dbDateName} SET + count_rawdata = @count_rawdata, + min_rawdata = @min_rawdata, + max_rawdata = @max_rawdata, + avg_rawdata = @avg_rawdata, + sum_rawdata = @sum_rawdata, + is_complete = @is_complete, + repeat_times = @repeat_times, + fail_reason = @fail_reason, + updated_at = @updated_at + WHERE device_number = @device_number + AND point = @point + AND start_timestamp = @start_timestamp; + + + INSERT INTO archive_water_meter_day_{dbDateName} ( + device_number, + point, + start_timestamp, + end_timestamp, + count_rawdata, + min_rawdata, + max_rawdata, + avg_rawdata, + sum_rawdata, + is_complete, + repeat_times, + fail_reason) + SELECT + @device_number, + @point, + @start_timestamp, + @end_timestamp, + @count_rawdata, + @min_rawdata, + @max_rawdata, + @avg_rawdata, + @sum_rawdata, + @is_complete, + @repeat_times, + @fail_reason + WHERE ROW_COUNT() = 0;"; + + var mySql = $@"BEGIN TRANSACTION; + IF OBJECT_ID(N'dbo.archive_water_meter_day_{dbDateName}', N'U') is null + BEGIN + CREATE TABLE [dbo].[archive_water_meter_day_{dbDateName}]( + [device_number] [varchar](50) NOT NULL, + [point] [varchar](20) NOT NULL, + [start_timestamp] [datetime] NOT NULL, + [end_timestamp] [datetime] NULL, + [count_rawdata] [int] NULL, + [min_rawdata] [decimal](15, 3) NULL, + [max_rawdata] [decimal](15, 3) NULL, + [avg_rawdata] [decimal](15, 3) NULL, + [sum_rawdata] [decimal](15, 3) NULL, + [is_complete] [tinyint] NULL, + [repeat_times] [int] NULL, + [fail_reason] [nvarchar](max) NULL, + [created_at] [datetime] NULL, + [updated_at] [datetime] NULL, + CONSTRAINT [PK_archive_water_meter_day_{dbDateName}] PRIMARY KEY CLUSTERED + ( + [device_number] ASC, + [point] ASC, + [start_timestamp] ASC + )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] + ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] + + ALTER TABLE [dbo].[archive_water_meter_day_{dbDateName}] ADD CONSTRAINT [DF_archive_water_meter_day_{dbDateName}_repeat_times] DEFAULT ((0)) FOR [repeat_times] + + ALTER TABLE [dbo].[archive_water_meter_day_{dbDateName}] ADD CONSTRAINT [DF_archive_water_meter_day_{dbDateName}_created_at] DEFAULT (getdate()) FOR [created_at] + + ALTER TABLE [dbo].[archive_water_meter_day_{dbDateName}] ADD CONSTRAINT [DF_archive_water_meter_day_{dbDateName}_updated_at] DEFAULT (NULL) FOR [updated_at] + + EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'是否完成,0:未完成 1:完成' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'archive_water_meter_day_{dbDateName}', @level2type=N'COLUMN',@level2name=N'is_complete' + + EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'重複次數' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'archive_water_meter_day_{dbDateName}', @level2type=N'COLUMN',@level2name=N'repeat_times' + + EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'失敗原因' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'archive_water_meter_day_{dbDateName}', @level2type=N'COLUMN',@level2name=N'fail_reason' + END + + UPDATE archive_water_meter_day_{dbDateName} SET + count_rawdata = @count_rawdata, + min_rawdata = @min_rawdata, + max_rawdata = @max_rawdata, + avg_rawdata = @avg_rawdata, + sum_rawdata = @sum_rawdata, + is_complete = @is_complete, + repeat_times = @repeat_times, + fail_reason = @fail_reason, + updated_at = @updated_at + WHERE device_number = @device_number + AND point = @point + AND start_timestamp = @start_timestamp; + + IF @@ROWCOUNT = 0 + BEGIN + INSERT INTO archive_water_meter_day_{dbDateName} ( + device_number, + point, + start_timestamp, + end_timestamp, + count_rawdata, + min_rawdata, + max_rawdata, + avg_rawdata, + sum_rawdata, + is_complete, + repeat_times, + fail_reason) + VALUES ( + @device_number, + @point, + @start_timestamp, + @end_timestamp, + @count_rawdata, + @min_rawdata, + @max_rawdata, + @avg_rawdata, + @sum_rawdata, + @is_complete, + @repeat_times, + @fail_reason) + END + + COMMIT TRANSACTION;"; + await backgroundServiceRepository.ExecuteSql(sql, waterArchiveDayRawDatas); + if (!string.IsNullOrEmpty(saveToMSDB) && saveToMSDB == "1") + { + await backgroundServiceMsSqlRepository.ExecuteSql(mySql, waterArchiveDayRawDatas); + } + } + await task_Detail.InsertWorkTime_End("ArchiveElectricMeterDayJob", "Day", "任務完成"); + } + catch (Exception exception) + { + await task_Detail.WorkFail("ArchiveElectricMeterDayJob", "Day", exception.ToString()); + logger.LogError("【ArchiveElectricMeterDayJob】【天歸檔】【任務失敗】"); + logger.LogError("【ArchiveElectricMeterDayJob】【天歸檔】【任務失敗】[Exception]:{0}", exception.ToString()); + } + } + #endregion 天歸檔 + + #region 週歸檔 + if (await task_Detail.GetNeedWorkTask("ArchiveElectricMeterDayJob", "Week")) + { + try + { + await task_Detail.InsertWorkTime("ArchiveElectricMeterDayJob", "Week", "水電表周任務開始"); + int week = Convert.ToInt32(now.DayOfWeek); + week = week == 0 ? 7 : week; + + var startTimestamp = string.Format("{0}T00:00:00.000+08:00", now.AddDays(1 - week).ToString("yyyy-MM-dd")); + var endTimestamp = string.Format("{0}T23:59:59.000+08:00", now.AddDays(7 - week).ToString("yyyy-MM-dd")); + + var historyQueryFilter = $@" + + + + "; + + //Stopwatch stopWatch = new Stopwatch(); + //stopWatch.Start(); + + //抓取每個設備的資料 + List> electricArchiveWeekRawDatas = new List>(); + List> waterArchiveWeekRawDatas = new List>(); + foreach (var deviceNumberPoint in electricDeviceNumberPoints) + { + HttpWebRequest archiveWeekRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/{deviceNumberPoint.FullDeviceNumberPoint}/~historyRollup/"); + //HttpWebRequest archiveWeekRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/H_E1_B1F_MVCB_MVCBH_V1/~historyRollup/"); + archiveWeekRequest.Method = "POST"; + archiveWeekRequest.Headers.Add("Authorization", "Basic " + encoded); + archiveWeekRequest.PreAuthenticate = true; + + byte[] byteArray = Encoding.UTF8.GetBytes(historyQueryFilter); + using (Stream reqStream = archiveWeekRequest.GetRequestStream()) + { + reqStream.Write(byteArray, 0, byteArray.Length); + } + + HttpWebResponse archiveWeekResponse = (HttpWebResponse)archiveWeekRequest.GetResponse(); + var archiveWeekResponseContent = new StreamReader(archiveWeekResponse.GetResponseStream()).ReadToEnd(); + + xmlDocument.LoadXml(archiveWeekResponseContent); + string archiveWeekJson = JsonConvert.SerializeXmlNode(xmlDocument); + JObject archiveWeekJsonResult = (JObject)JsonConvert.DeserializeObject(archiveWeekJson); + + if (archiveWeekJsonResult.ContainsKey("err")) //抓取錯誤 + { + //logger.LogError("【ArchiveElectricMeterDayJob】【週歸檔】【取得資料失敗】"); + //logger.LogError("【ArchiveElectricMeterDayJob】【週歸檔】【取得資料失敗】[錯誤內容]:{0}", archiveWeekJsonResult); + + Dictionary archiveWeekRawData = new Dictionary(); + archiveWeekRawData.Add("@device_number", deviceNumberPoint.DeviceNumber); + archiveWeekRawData.Add("@point", deviceNumberPoint.Point); + archiveWeekRawData.Add("@start_timestamp", startTimestamp.Replace("T", " ").Substring(0, 19)); + archiveWeekRawData.Add("@end_timestamp", endTimestamp.Replace("T", " ").Substring(0, 19)); + archiveWeekRawData.Add("@is_complete", 0); + archiveWeekRawData.Add("@repeat_times", 0); + archiveWeekRawData.Add("@fail_reason", archiveWeekJson); + + archiveWeekRawData.Add("@count_rawdata", 0); + archiveWeekRawData.Add("@min_rawdata", 0); + archiveWeekRawData.Add("@max_rawdata", 0); + archiveWeekRawData.Add("@avg_rawdata", 0); + archiveWeekRawData.Add("@sum_rawdata", 0); + archiveWeekRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + electricArchiveWeekRawDatas.Add(archiveWeekRawData); + } + + if (archiveWeekJsonResult.ContainsKey("obj")) //表示可以讀取到內容 + { + var ArrangeRawDatas = ArrangeRawData(deviceNumberPoint, archiveWeekJsonResult); + if (ArrangeRawDatas != null && ArrangeRawDatas.Count() > 0) + { + electricArchiveWeekRawDatas.AddRange(ArrangeRawDatas); + } + } + } + foreach (var deviceNumberPoint in waterDeviceNumberPoints) + { + HttpWebRequest archiveWeekRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/{deviceNumberPoint.FullDeviceNumberPoint}/~historyRollup/"); + //HttpWebRequest archiveWeekRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/H_E1_B1F_MVCB_MVCBH_V1/~historyRollup/"); + archiveWeekRequest.Method = "POST"; + archiveWeekRequest.Headers.Add("Authorization", "Basic " + encoded); + archiveWeekRequest.PreAuthenticate = true; + + byte[] byteArray = Encoding.UTF8.GetBytes(historyQueryFilter); + using (Stream reqStream = archiveWeekRequest.GetRequestStream()) + { + reqStream.Write(byteArray, 0, byteArray.Length); + } + + HttpWebResponse archiveWeekResponse = (HttpWebResponse)archiveWeekRequest.GetResponse(); + var archiveWeekResponseContent = new StreamReader(archiveWeekResponse.GetResponseStream()).ReadToEnd(); + + xmlDocument.LoadXml(archiveWeekResponseContent); + string archiveWeekJson = JsonConvert.SerializeXmlNode(xmlDocument); + JObject archiveWeekJsonResult = (JObject)JsonConvert.DeserializeObject(archiveWeekJson); + + if (archiveWeekJsonResult.ContainsKey("err")) //抓取錯誤 + { + //logger.LogError("【ArchiveElectricMeterDayJob】【週歸檔】【取得資料失敗】"); + //logger.LogError("【ArchiveElectricMeterDayJob】【週歸檔】【取得資料失敗】[錯誤內容]:{0}", archiveWeekJsonResult); + + Dictionary archiveWeekRawData = new Dictionary(); + archiveWeekRawData.Add("@device_number", deviceNumberPoint.DeviceNumber); + archiveWeekRawData.Add("@point", deviceNumberPoint.Point); + archiveWeekRawData.Add("@start_timestamp", startTimestamp.Replace("T", " ").Substring(0, 19)); + archiveWeekRawData.Add("@end_timestamp", endTimestamp.Replace("T", " ").Substring(0, 19)); + archiveWeekRawData.Add("@is_complete", 0); + archiveWeekRawData.Add("@repeat_times", 0); + archiveWeekRawData.Add("@fail_reason", archiveWeekJson); + + archiveWeekRawData.Add("@count_rawdata", 0); + archiveWeekRawData.Add("@min_rawdata", 0); + archiveWeekRawData.Add("@max_rawdata", 0); + archiveWeekRawData.Add("@avg_rawdata", 0); + archiveWeekRawData.Add("@sum_rawdata", 0); + archiveWeekRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + waterArchiveWeekRawDatas.Add(archiveWeekRawData); + } + + if (archiveWeekJsonResult.ContainsKey("obj")) //表示可以讀取到內容 + { + var ArrangeRawDatas = ArrangeRawData(deviceNumberPoint, archiveWeekJsonResult); + if (ArrangeRawDatas != null && ArrangeRawDatas.Count() > 0) + { + waterArchiveWeekRawDatas.AddRange(ArrangeRawDatas); + } + } + } + + //stopWatch.Stop(); + //logger.LogInformation("【ArchiveElectricMeterDayJob】【週歸檔】【效能檢驗】[取得資料花費時間]{0} 毫秒", stopWatch.ElapsedMilliseconds); + + if (electricArchiveWeekRawDatas.Count() > 0) + { + var sql = $@" + + UPDATE archive_electric_meter_week SET + count_rawdata = @count_rawdata, + min_rawdata = @min_rawdata, + max_rawdata = @max_rawdata, + avg_rawdata = @avg_rawdata, + sum_rawdata = @sum_rawdata, + is_complete = @is_complete, + repeat_times = @repeat_times, + fail_reason = @fail_reason, + updated_at = @updated_at + WHERE device_number = @device_number + AND point = @point + AND start_timestamp = @start_timestamp; + + + INSERT INTO archive_electric_meter_week ( + device_number, + point, + start_timestamp, + end_timestamp, + count_rawdata, + min_rawdata, + max_rawdata, + avg_rawdata, + sum_rawdata, + is_complete, + repeat_times, + fail_reason) + SELECT + @device_number, + @point, + @start_timestamp, + @end_timestamp, + @count_rawdata, + @min_rawdata, + @max_rawdata, + @avg_rawdata, + @sum_rawdata, + @is_complete, + @repeat_times, + @fail_reason + WHERE ROW_COUNT() = 0; + "; + + var mySql = $@"BEGIN TRANSACTION; + + UPDATE archive_electric_meter_week SET + count_rawdata = @count_rawdata, + min_rawdata = @min_rawdata, + max_rawdata = @max_rawdata, + avg_rawdata = @avg_rawdata, + sum_rawdata = @sum_rawdata, + is_complete = @is_complete, + repeat_times = @repeat_times, + fail_reason = @fail_reason, + updated_at = @updated_at + WHERE device_number = @device_number + AND point = @point + AND start_timestamp = @start_timestamp; + + IF @@ROWCOUNT = 0 + BEGIN + INSERT INTO archive_electric_meter_week ( + device_number, + point, + start_timestamp, + end_timestamp, + count_rawdata, + min_rawdata, + max_rawdata, + avg_rawdata, + sum_rawdata, + is_complete, + repeat_times, + fail_reason) + VALUES ( + @device_number, + @point, + @start_timestamp, + @end_timestamp, + @count_rawdata, + @min_rawdata, + @max_rawdata, + @avg_rawdata, + @sum_rawdata, + @is_complete, + @repeat_times, + @fail_reason) + END + + COMMIT TRANSACTION;"; + await backgroundServiceRepository.ExecuteSql(sql, electricArchiveWeekRawDatas); + if (!string.IsNullOrEmpty(saveToMSDB) && saveToMSDB == "1") + { + await backgroundServiceMsSqlRepository.ExecuteSql(mySql, electricArchiveWeekRawDatas); + } + + } + if (waterArchiveWeekRawDatas.Count() > 0) + { + var sql = $@" + + UPDATE archive_water_meter_week SET + count_rawdata = @count_rawdata, + min_rawdata = @min_rawdata, + max_rawdata = @max_rawdata, + avg_rawdata = @avg_rawdata, + sum_rawdata = @sum_rawdata, + is_complete = @is_complete, + repeat_times = @repeat_times, + fail_reason = @fail_reason, + updated_at = @updated_at + WHERE device_number = @device_number + AND point = @point + AND start_timestamp = @start_timestamp; + + + INSERT INTO archive_water_meter_week ( + device_number, + point, + start_timestamp, + end_timestamp, + count_rawdata, + min_rawdata, + max_rawdata, + avg_rawdata, + sum_rawdata, + is_complete, + repeat_times, + fail_reason) + SELECT + @device_number, + @point, + @start_timestamp, + @end_timestamp, + @count_rawdata, + @min_rawdata, + @max_rawdata, + @avg_rawdata, + @sum_rawdata, + @is_complete, + @repeat_times, + @fail_reason + WHERE ROW_COUNT() = 0; + "; + + var mySql = $@"BEGIN TRANSACTION; + + UPDATE archive_water_meter_week SET + count_rawdata = @count_rawdata, + min_rawdata = @min_rawdata, + max_rawdata = @max_rawdata, + avg_rawdata = @avg_rawdata, + sum_rawdata = @sum_rawdata, + is_complete = @is_complete, + repeat_times = @repeat_times, + fail_reason = @fail_reason, + updated_at = @updated_at + WHERE device_number = @device_number + AND point = @point + AND start_timestamp = @start_timestamp; + + IF @@ROWCOUNT = 0 + BEGIN + INSERT INTO archive_water_meter_week ( + device_number, + point, + start_timestamp, + end_timestamp, + count_rawdata, + min_rawdata, + max_rawdata, + avg_rawdata, + sum_rawdata, + is_complete, + repeat_times, + fail_reason) + VALUES ( + @device_number, + @point, + @start_timestamp, + @end_timestamp, + @count_rawdata, + @min_rawdata, + @max_rawdata, + @avg_rawdata, + @sum_rawdata, + @is_complete, + @repeat_times, + @fail_reason) + END + + COMMIT TRANSACTION;"; + await backgroundServiceRepository.ExecuteSql(sql, waterArchiveWeekRawDatas); + if (!string.IsNullOrEmpty(saveToMSDB) && saveToMSDB == "1") + { + await backgroundServiceMsSqlRepository.ExecuteSql(mySql, waterArchiveWeekRawDatas); + } + } + await task_Detail.InsertWorkTime_End("ArchiveElectricMeterDayJob", "Week", "任務完成"); + } + catch (Exception exception) + { + await task_Detail.WorkFail("ArchiveElectricMeterDayJob", "Week", exception.ToString()); + logger.LogError("【ArchiveElectricMeterDayJob】【週歸檔】【任務失敗】"); + logger.LogError("【ArchiveElectricMeterDayJob】【週歸檔】【任務失敗】[Exception]:{0}", exception.ToString()); + } + } + #endregion 週歸檔 + + #region 月歸檔 + if (await task_Detail.GetNeedWorkTask("ArchiveElectricMeterDayJob", "Month")) + { + try + { + await task_Detail.InsertWorkTime("ArchiveElectricMeterDayJob", "Month", "水電表月任務開始"); + var FirstDay = now.AddDays(-now.Day + 1); + var LastDay = now.AddMonths(1).AddDays(-now.AddMonths(1).Day); + + var dayInMonth = DateTime.DaysInMonth(now.Year, now.Month); + + var startTimestamp = string.Format("{0}T00:00:00.000+08:00", FirstDay.ToString("yyyy-MM-dd")); + var endTimestamp = string.Format("{0}T23:59:59.000+08:00", LastDay.ToString("yyyy-MM-dd")); + + var historyQueryFilter = $@" + + + + "; + + //Stopwatch stopWatch = new Stopwatch(); + //stopWatch.Start(); + + //抓取每個設備的資料 + List> electricArchiveMonthRawDatas = new List>(); + List> waterArchiveMonthRawDatas = new List>(); + foreach (var deviceNumberPoint in electricDeviceNumberPoints) + { + HttpWebRequest archiveMonthRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/{deviceNumberPoint.FullDeviceNumberPoint}/~historyRollup/"); + //HttpWebRequest archiveMonthRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/H_E1_B1F_MVCB_MVCBH_V1/~historyRollup/"); + archiveMonthRequest.Method = "POST"; + archiveMonthRequest.Headers.Add("Authorization", "Basic " + encoded); + archiveMonthRequest.PreAuthenticate = true; + + byte[] byteArray = Encoding.UTF8.GetBytes(historyQueryFilter); + using (Stream reqStream = archiveMonthRequest.GetRequestStream()) + { + reqStream.Write(byteArray, 0, byteArray.Length); + } + + HttpWebResponse archiveMonthResponse = (HttpWebResponse)archiveMonthRequest.GetResponse(); + var archiveMonthResponseContent = new StreamReader(archiveMonthResponse.GetResponseStream()).ReadToEnd(); + + xmlDocument.LoadXml(archiveMonthResponseContent); + string archiveMonthJson = JsonConvert.SerializeXmlNode(xmlDocument); + JObject archiveMonthJsonResult = (JObject)JsonConvert.DeserializeObject(archiveMonthJson); + + if (archiveMonthJsonResult.ContainsKey("err")) //抓取錯誤 + { + //logger.LogError("【ArchiveElectricMeterDayJob】【月歸檔】【取得資料失敗】"); + //logger.LogError("【ArchiveElectricMeterDayJob】【月歸檔】【取得資料失敗】[錯誤內容]:{0}", archiveMonthJsonResult); + + Dictionary archiveMonthRawData = new Dictionary(); + archiveMonthRawData.Add("@device_number", deviceNumberPoint.DeviceNumber); + archiveMonthRawData.Add("@point", deviceNumberPoint.Point); + archiveMonthRawData.Add("@start_timestamp", startTimestamp.Replace("T", " ").Substring(0, 19)); + archiveMonthRawData.Add("@end_timestamp", endTimestamp.Replace("T", " ").Substring(0, 19)); + archiveMonthRawData.Add("@is_complete", 0); + archiveMonthRawData.Add("@repeat_times", 0); + archiveMonthRawData.Add("@fail_reason", archiveMonthJson); + + archiveMonthRawData.Add("@count_rawdata", 0); + archiveMonthRawData.Add("@min_rawdata", 0); + archiveMonthRawData.Add("@max_rawdata", 0); + archiveMonthRawData.Add("@avg_rawdata", 0); + archiveMonthRawData.Add("@sum_rawdata", 0); + archiveMonthRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + electricArchiveMonthRawDatas.Add(archiveMonthRawData); + } + + if (archiveMonthJsonResult.ContainsKey("obj")) //表示可以讀取到內容 + { + var ArrangeRawDatas = ArrangeRawData(deviceNumberPoint, archiveMonthJsonResult); + if (ArrangeRawDatas != null && ArrangeRawDatas.Count() > 0) + { + electricArchiveMonthRawDatas.AddRange(ArrangeRawDatas); + } + } + } + foreach (var deviceNumberPoint in waterDeviceNumberPoints) + { + HttpWebRequest archiveMonthRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/{deviceNumberPoint.FullDeviceNumberPoint}/~historyRollup/"); + //HttpWebRequest archiveMonthRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/H_E1_B1F_MVCB_MVCBH_V1/~historyRollup/"); + archiveMonthRequest.Method = "POST"; + archiveMonthRequest.Headers.Add("Authorization", "Basic " + encoded); + archiveMonthRequest.PreAuthenticate = true; + + byte[] byteArray = Encoding.UTF8.GetBytes(historyQueryFilter); + using (Stream reqStream = archiveMonthRequest.GetRequestStream()) + { + reqStream.Write(byteArray, 0, byteArray.Length); + } + + HttpWebResponse archiveMonthResponse = (HttpWebResponse)archiveMonthRequest.GetResponse(); + var archiveMonthResponseContent = new StreamReader(archiveMonthResponse.GetResponseStream()).ReadToEnd(); + + xmlDocument.LoadXml(archiveMonthResponseContent); + string archiveMonthJson = JsonConvert.SerializeXmlNode(xmlDocument); + JObject archiveMonthJsonResult = (JObject)JsonConvert.DeserializeObject(archiveMonthJson); + + if (archiveMonthJsonResult.ContainsKey("err")) //抓取錯誤 + { + //logger.LogError("【ArchiveElectricMeterDayJob】【月歸檔】【取得資料失敗】"); + //logger.LogError("【ArchiveElectricMeterDayJob】【月歸檔】【取得資料失敗】[錯誤內容]:{0}", archiveMonthJsonResult); + + Dictionary archiveMonthRawData = new Dictionary(); + archiveMonthRawData.Add("@device_number", deviceNumberPoint.DeviceNumber); + archiveMonthRawData.Add("@point", deviceNumberPoint.Point); + archiveMonthRawData.Add("@start_timestamp", startTimestamp.Replace("T", " ").Substring(0, 19)); + archiveMonthRawData.Add("@end_timestamp", endTimestamp.Replace("T", " ").Substring(0, 19)); + archiveMonthRawData.Add("@is_complete", 0); + archiveMonthRawData.Add("@repeat_times", 0); + archiveMonthRawData.Add("@fail_reason", archiveMonthJson); + + archiveMonthRawData.Add("@count_rawdata", 0); + archiveMonthRawData.Add("@min_rawdata", 0); + archiveMonthRawData.Add("@max_rawdata", 0); + archiveMonthRawData.Add("@avg_rawdata", 0); + archiveMonthRawData.Add("@sum_rawdata", 0); + archiveMonthRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + waterArchiveMonthRawDatas.Add(archiveMonthRawData); + } + + if (archiveMonthJsonResult.ContainsKey("obj")) //表示可以讀取到內容 + { + var ArrangeRawDatas = ArrangeRawData(deviceNumberPoint, archiveMonthJsonResult); + if (ArrangeRawDatas != null && ArrangeRawDatas.Count() > 0) + { + waterArchiveMonthRawDatas.AddRange(ArrangeRawDatas); + } + } + } + + //stopWatch.Stop(); + //logger.LogInformation("【ArchiveElectricMeterDayJob】【月歸檔效能檢驗】[取得資料花費時間]{0} 毫秒", stopWatch.ElapsedMilliseconds); + + if (electricArchiveMonthRawDatas.Count() > 0) + { + var sql = $@" + UPDATE archive_electric_meter_month SET + count_rawdata = @count_rawdata, + min_rawdata = @min_rawdata, + max_rawdata = @max_rawdata, + avg_rawdata = @avg_rawdata, + sum_rawdata = @sum_rawdata, + is_complete = @is_complete, + repeat_times = @repeat_times, + fail_reason = @fail_reason, + updated_at = @updated_at + WHERE device_number = @device_number + AND point = @point + AND start_timestamp = @start_timestamp; + + INSERT INTO archive_electric_meter_month ( + device_number, + point, + start_timestamp, + end_timestamp, + count_rawdata, + min_rawdata, + max_rawdata, + avg_rawdata, + sum_rawdata, + is_complete, + repeat_times, + fail_reason) + SELECT + @device_number, + @point, + @start_timestamp, + @end_timestamp, + @count_rawdata, + @min_rawdata, + @max_rawdata, + @avg_rawdata, + @sum_rawdata, + @is_complete, + @repeat_times, + @fail_reason + WHERE ROW_COUNT() = 0;"; + + var mySql = $@"BEGIN TRANSACTION; + + UPDATE archive_electric_meter_month SET + count_rawdata = @count_rawdata, + min_rawdata = @min_rawdata, + max_rawdata = @max_rawdata, + avg_rawdata = @avg_rawdata, + sum_rawdata = @sum_rawdata, + is_complete = @is_complete, + repeat_times = @repeat_times, + fail_reason = @fail_reason, + updated_at = @updated_at + WHERE device_number = @device_number + AND point = @point + AND start_timestamp = @start_timestamp; + + IF @@ROWCOUNT = 0 + BEGIN + INSERT INTO archive_electric_meter_month ( + device_number, + point, + start_timestamp, + end_timestamp, + count_rawdata, + min_rawdata, + max_rawdata, + avg_rawdata, + sum_rawdata, + is_complete, + repeat_times, + fail_reason) + VALUES ( + @device_number, + @point, + @start_timestamp, + @end_timestamp, + @count_rawdata, + @min_rawdata, + @max_rawdata, + @avg_rawdata, + @sum_rawdata, + @is_complete, + @repeat_times, + @fail_reason) + END + + COMMIT TRANSACTION;"; + await backgroundServiceRepository.ExecuteSql(sql, electricArchiveMonthRawDatas); + if (!string.IsNullOrEmpty(saveToMSDB) && saveToMSDB == "1") + { + await backgroundServiceMsSqlRepository.ExecuteSql(mySql, electricArchiveMonthRawDatas); + } + } + if (waterArchiveMonthRawDatas.Count() > 0) + { + var sql = $@" + UPDATE archive_water_meter_month SET + count_rawdata = @count_rawdata, + min_rawdata = @min_rawdata, + max_rawdata = @max_rawdata, + avg_rawdata = @avg_rawdata, + sum_rawdata = @sum_rawdata, + is_complete = @is_complete, + repeat_times = @repeat_times, + fail_reason = @fail_reason, + updated_at = @updated_at + WHERE device_number = @device_number + AND point = @point + AND start_timestamp = @start_timestamp; + + INSERT INTO archive_water_meter_month ( + device_number, + point, + start_timestamp, + end_timestamp, + count_rawdata, + min_rawdata, + max_rawdata, + avg_rawdata, + sum_rawdata, + is_complete, + repeat_times, + fail_reason) + SELECT + @device_number, + @point, + @start_timestamp, + @end_timestamp, + @count_rawdata, + @min_rawdata, + @max_rawdata, + @avg_rawdata, + @sum_rawdata, + @is_complete, + @repeat_times, + @fail_reason + WHERE ROW_COUNT() = 0;"; + + var mySql = $@"BEGIN TRANSACTION; + + UPDATE archive_water_meter_month SET + count_rawdata = @count_rawdata, + min_rawdata = @min_rawdata, + max_rawdata = @max_rawdata, + avg_rawdata = @avg_rawdata, + sum_rawdata = @sum_rawdata, + is_complete = @is_complete, + repeat_times = @repeat_times, + fail_reason = @fail_reason, + updated_at = @updated_at + WHERE device_number = @device_number + AND point = @point + AND start_timestamp = @start_timestamp; + + IF @@ROWCOUNT = 0 + BEGIN + INSERT INTO archive_water_meter_month ( + device_number, + point, + start_timestamp, + end_timestamp, + count_rawdata, + min_rawdata, + max_rawdata, + avg_rawdata, + sum_rawdata, + is_complete, + repeat_times, + fail_reason) + VALUES ( + @device_number, + @point, + @start_timestamp, + @end_timestamp, + @count_rawdata, + @min_rawdata, + @max_rawdata, + @avg_rawdata, + @sum_rawdata, + @is_complete, + @repeat_times, + @fail_reason) + END + + COMMIT TRANSACTION;"; + await backgroundServiceRepository.ExecuteSql(sql, waterArchiveMonthRawDatas); + if (!string.IsNullOrEmpty(saveToMSDB) && saveToMSDB == "1") + { + await backgroundServiceMsSqlRepository.ExecuteSql(mySql, waterArchiveMonthRawDatas); + } + } + await task_Detail.InsertWorkTime_End("ArchiveElectricMeterDayJob", "Month", "任務完成"); + } + catch (Exception exception) + { + await task_Detail.WorkFail("ArchiveElectricMeterDayJob", "Month", exception.ToString()); + logger.LogError("【ArchiveElectricMeterDayJob】【月歸檔】【任務失敗】"); + logger.LogError("【ArchiveElectricMeterDayJob】【月歸檔】【任務失敗】[Exception]:{0}", exception.ToString()); + } + } + #endregion 月歸檔 + + #region 補償機制 + //取得連線字串 + if (await task_Detail.GetNeedWorkTask("ArchiveElectricMeterDayJob", "Compensate")) + { + try + { + await task_Detail.InsertWorkTime("ArchiveElectricMeterDayJob", "Compensate", "水電表補償任務開始"); + ProcEletricMeterService procEletricMeterService = new ProcEletricMeterService(backgroundServiceRepository, backgroundServiceMsSqlRepository); + await procEletricMeterService.ArchiveData(); + await task_Detail.InsertWorkTime_End("ArchiveElectricMeterDayJob", "Compensate", "任務完成"); + } + catch(Exception ex) + { + await task_Detail.WorkFail("ArchiveElectricMeterDayJob", "Compensate", ex.ToString()); + logger.LogError("【ArchiveElectricMeterDayJob】【補償機制】【任務失敗】"); + logger.LogError("【ArchiveElectricMeterDayJob】【補償機制】【任務失敗】[Exception]:{0}", ex.ToString()); + } + } + #endregion 補償機制 + + await task_Detail.InsertWorkTime_End("ArchiveElectricMeterDayJob", "All","任務完成"); + } + } + catch (Exception exception) + { + await task_Detail.WorkFail("ArchiveElectricMeterDayJob", "All", exception.ToString()); + logger.LogError("【ArchiveElectricMeterDayJob】【任務失敗】"); + logger.LogError("【ArchiveElectricMeterDayJob】【任務失敗】[Exception]:{0}", exception.ToString()); + } + } + + private List> ArrangeRawData(DeviceNumberPoint deviceNumberPoint, JObject jsonResult) + { + List> arrangeRawDatas = new List>(); + var histories = jsonResult["obj"]["list"]; + var rawdateCount = Convert.ToInt32(jsonResult["obj"]["int"]["@val"].ToString()); + + if(rawdateCount == 0) + { + return null; + } + + if (histories != null && histories.HasValues) + { + if (rawdateCount > 1) + { //多筆資料 + foreach (var history in histories) + { + Dictionary arrangeRawData = new Dictionary(); + arrangeRawData.Add("@device_number", deviceNumberPoint.DeviceNumber); + arrangeRawData.Add("@point", deviceNumberPoint.Point); + + //時間 + if (history["abstime"] != null && history["abstime"].HasValues) + { + foreach (var abstime in history["abstime"]) + { + var name = abstime["@name"].ToString(); + switch (name) + { + case "start": + var startTimstamp = Convert.ToDateTime(abstime["@val"].ToString()).ToString("yyyy-MM-dd HH:mm:ss"); + arrangeRawData.Add("@start_timestamp", startTimstamp); + break; + case "end": + var endTimstamp = Convert.ToDateTime(abstime["@val"].ToString()).ToString("yyyy-MM-dd HH:mm:ss"); + arrangeRawData.Add("@end_timestamp", endTimstamp); + break; + } + } + } + + //區間內資料筆數 + if (history["int"] != null && history["int"].HasValues) + { + var count = Convert.ToInt32(histories["obj"]["int"]["@val"].ToString()); + arrangeRawData.Add("@count_rawdata", count); + } + + //整合數值(最大、最小、平均、總和) + if (history["real"] != null && history["real"].HasValues) + { + foreach (var real in history["real"]) + { + var name = real["@name"].ToString(); + switch (name) + { + case "min": + var min = Convert.ToDecimal(real["@val"].ToString()); + arrangeRawData.Add("@min_rawdata", min); + break; + case "max": + var max = Convert.ToDecimal(real["@val"].ToString()); + arrangeRawData.Add("@max_rawdata", max); + break; + case "avg": + var avg = Convert.ToDecimal(real["@val"].ToString()); + arrangeRawData.Add("@avg_rawdata", avg); + break; + case "sum": + var sum = Decimal.Parse(real["@val"].ToString(), System.Globalization.NumberStyles.Float); + arrangeRawData.Add("@sum_rawdata", sum); + break; + } + } + } + arrangeRawData.Add("@is_complete", 1); + arrangeRawData.Add("@repeat_times", 0); + arrangeRawData.Add("@fail_reason", null); + arrangeRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + arrangeRawDatas.Add(arrangeRawData); + } + } + else + { //單筆資料 + Dictionary arrangeRawData = new Dictionary(); + arrangeRawData.Add("@device_number", deviceNumberPoint.DeviceNumber); + arrangeRawData.Add("@point", deviceNumberPoint.Point); + + //時間 + if (histories["obj"]["abstime"] != null && histories["obj"]["abstime"].HasValues) + { + foreach (var abstime in histories["obj"]["abstime"]) + { + var name = abstime["@name"].ToString(); + switch (name) + { + case "start": + var startTimstamp = Convert.ToDateTime(abstime["@val"].ToString()).ToString("yyyy-MM-dd HH:mm:ss"); + arrangeRawData.Add("@start_timestamp", startTimstamp); + break; + case "end": + var endTimstamp = Convert.ToDateTime(abstime["@val"].ToString()).ToString("yyyy-MM-dd HH:mm:ss"); + arrangeRawData.Add("@end_timestamp", endTimstamp); + break; + } + } + } + + //區間內資料筆數 + if (histories["obj"]["int"] != null && histories["obj"]["int"].HasValues) + { + var count = Convert.ToInt32(histories["obj"]["int"]["@val"].ToString()); + arrangeRawData.Add("@count_rawdata", count); + } + + //整合數值(最大、最小、平均、總和) + if (histories["obj"]["real"] != null && histories["obj"]["real"].HasValues) + { + foreach (var real in histories["obj"]["real"]) + { + var name = real["@name"].ToString(); + switch (name) + { + case "min": + var min = Convert.ToDecimal(real["@val"].ToString()); + arrangeRawData.Add("@min_rawdata", min); + break; + case "max": + var max = Convert.ToDecimal(real["@val"].ToString()); + arrangeRawData.Add("@max_rawdata", max); + break; + case "avg": + var avg = Convert.ToDecimal(real["@val"].ToString()); + arrangeRawData.Add("@avg_rawdata", avg); + break; + case "sum": + var sum = Decimal.Parse(real["@val"].ToString(), System.Globalization.NumberStyles.Float); + arrangeRawData.Add("@sum_rawdata", sum); + break; + } + } + } + arrangeRawData.Add("@is_complete", 1); + arrangeRawData.Add("@repeat_times", 0); + arrangeRawData.Add("@fail_reason", null); + arrangeRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + arrangeRawDatas.Add(arrangeRawData); + } + } + + return arrangeRawDatas; + } + } +} diff --git a/Backend/Quartz/Jobs/ArchiveElectricMeterHourJob.cs b/Backend/Quartz/Jobs/ArchiveElectricMeterHourJob.cs new file mode 100644 index 0000000..dda8ae2 --- /dev/null +++ b/Backend/Quartz/Jobs/ArchiveElectricMeterHourJob.cs @@ -0,0 +1,335 @@ +using Backend.Models; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Quartz; +using Repository.BackendRepository.Interface; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace Backend.Quartz.Jobs +{ + /// + /// 電錶歸檔,每小時執行,只執行小時歸檔 + /// + [DisallowConcurrentExecution] + class ArchiveElectricMeterHourJob : IJob + { + private readonly ILogger logger; + private readonly IBackgroundServiceRepository backgroundServiceRepository; + private readonly ILogger loggers; + + public ArchiveElectricMeterHourJob( + ILogger logger, + IBackgroundServiceRepository backgroundServiceRepository, ILogger loggers) + { + this.logger = logger; + this.backgroundServiceRepository = backgroundServiceRepository; + this.loggers = loggers; + } + + public async Task Execute(IJobExecutionContext context) + { + Task_Detail task_Detail = new Task_Detail(loggers, backgroundServiceRepository); + try + { + if(await task_Detail.GetNeedWorkTask("ArchiveElectricMeterHourJob", "All")) + { + await task_Detail.InsertWorkTime("ArchiveElectricMeterHourJob", "All", "任務開始"); + EDFunction ed = new EDFunction(); + XmlDocument xmlDocument = new XmlDocument(); + + var sqlArchive = $@"SELECT system_value as Value, system_key as Name FROM variable WHERE deleted = 0 AND system_type = 'archiveConfig'"; + + var variableArchive = await backgroundServiceRepository.GetAllAsync(sqlArchive); + var electricMeterGuid = variableArchive.Where(x => x.Name == "ElectricMeterGuid").Select(x => x.Value).FirstOrDefault(); + + #region 找出所有電錶設備 + var sWhere = "deleted = 0 AND sub_system_guid = @sub_system_guid"; + var electricMeters = await backgroundServiceRepository.GetAllAsync("device", sWhere, new { sub_system_guid = electricMeterGuid }); + #endregion 找出所有電錶設備 + + #region 找出所有電錶系統的點位 + var sPointWhere = "deleted = 0 AND sub_system_guid = @sub_system_guid"; + var points = await backgroundServiceRepository.GetAllAsync("device_item", sPointWhere, new { sub_system_guid = electricMeterGuid }); + #endregion 找出所有電錶系統的點位 + + #region 組合出所有電錶設備點位 + List deviceNumberPoints = new List(); + foreach (var electricMeter in electricMeters) + { + foreach (var point in points) + { + DeviceNumberPoint deviceNumberPoint = new DeviceNumberPoint(); + deviceNumberPoint.DeviceNumber = electricMeter.Device_number; + deviceNumberPoint.Point = point.points; + deviceNumberPoint.FullDeviceNumberPoint = string.Format("{0}_{1}", electricMeter.Device_number, point.points); + + deviceNumberPoints.Add(deviceNumberPoint); + } + } + #endregion 組合出所有電錶設備點位 + + #region 取得obix 設定 + var obixApiConfig = new ObixApiConfig(); + + var sqlObix = $@"SELECT system_value as Value, system_key as Name FROM variable WHERE deleted = 0 AND system_type = 'obixConfig'"; + + var variableObix = await backgroundServiceRepository.GetAllAsync(sqlObix); + obixApiConfig.ApiBase = variableObix.Where(x => x.Name == "ApiBase").Select(x => x.Value).FirstOrDefault(); + obixApiConfig.UserName = ed.AESDecrypt(variableObix.Where(x => x.Name == "UserName").Select(x => x.Value).FirstOrDefault()); + obixApiConfig.Password = ed.AESDecrypt(variableObix.Where(x => x.Name == "Password").Select(x => x.Value).FirstOrDefault()); + + String encoded = System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(obixApiConfig.UserName + ":" + obixApiConfig.Password)); + #endregion 取得obix 設定 + + var now = DateTime.Now; + + #region 小時歸檔 + try + { + var preHour = now.AddHours(-1); //取得前一小時 + + var tempHour = string.Empty; + if (preHour.Hour < 10) + { + tempHour = "0" + preHour.Hour.ToString(); + } + else + { + tempHour = preHour.Hour.ToString(); + } + + var startTimestamp = string.Format("{0}T{1}:00:00.000+08:00", preHour.ToString("yyyy-MM-dd"), tempHour); + var endTimestamp = string.Format("{0}T{1}:59:59.000+08:00", preHour.ToString("yyyy-MM-dd"), tempHour); + + var historyQueryFilter = $@" + + + + "; + + //Stopwatch stopWatch = new Stopwatch(); + //stopWatch.Start(); + + //抓取每個設備的資料 + List> archiveHourRawDatas = new List>(); + foreach (var deviceNumberPoint in deviceNumberPoints) + { + + HttpWebRequest archiveHourRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/{deviceNumberPoint.FullDeviceNumberPoint}/~historyRollup/"); + //HttpWebRequest archiveHourRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/H_E1_B1F_MVCB_MVCBH_V1/~historyRollup/"); + archiveHourRequest.Method = "POST"; + archiveHourRequest.Headers.Add("Authorization", "Basic " + encoded); + archiveHourRequest.PreAuthenticate = true; + + byte[] byteArray = Encoding.UTF8.GetBytes(historyQueryFilter); + using (Stream reqStream = archiveHourRequest.GetRequestStream()) + { + reqStream.Write(byteArray, 0, byteArray.Length); + } + + HttpWebResponse archiveHourResponse = (HttpWebResponse)archiveHourRequest.GetResponse(); + var archiveHourResponseContent = new StreamReader(archiveHourResponse.GetResponseStream()).ReadToEnd(); + + xmlDocument.LoadXml(archiveHourResponseContent); + string archiveHourJson = JsonConvert.SerializeXmlNode(xmlDocument); + JObject archiveHourJsonResult = (JObject)JsonConvert.DeserializeObject(archiveHourJson); + + if (archiveHourJsonResult.ContainsKey("err")) //抓取錯誤 + { + //logger.LogError("【ArchiveElectricMeterHourJob】【小時歸檔】【取得資料失敗】"); + //logger.LogError("【ArchiveElectricMeterHourJob】【小時歸檔】【取得資料失敗】[錯誤內容]:{0}", archiveHourJsonResult); + + Dictionary archiveHourRawData = new Dictionary(); + archiveHourRawData.Add("@device_number", deviceNumberPoint.DeviceNumber); + archiveHourRawData.Add("@point", deviceNumberPoint.Point); + archiveHourRawData.Add("@start_timestamp", startTimestamp.Replace("T", " ").Substring(0, 19)); + archiveHourRawData.Add("@end_timestamp", endTimestamp.Replace("T", " ").Substring(0, 19)); + archiveHourRawData.Add("@is_complete", 0); + archiveHourRawData.Add("@repeat_times", 0); + archiveHourRawData.Add("@fail_reason", archiveHourJson); + + archiveHourRawDatas.Add(archiveHourRawData); + } + + if (archiveHourJsonResult.ContainsKey("obj")) //表示可以讀取到內容 + { + var histories = archiveHourJsonResult["obj"]["list"]; + var rawdateCount = Convert.ToInt32(archiveHourJsonResult["obj"]["int"]["@val"].ToString()); + if (histories != null && histories.HasValues) + { + if (rawdateCount > 1) + { //多筆資料 + foreach (var history in histories) + { + Dictionary archiveHourRawData = new Dictionary(); + archiveHourRawData.Add("@device_number", deviceNumberPoint.DeviceNumber); + archiveHourRawData.Add("@point", deviceNumberPoint.Point); + + //時間 + if (history["abstime"] != null && history["abstime"].HasValues) + { + foreach (var abstime in history["abstime"]) + { + var name = abstime["@name"].ToString(); + switch (name) + { + case "start": + var startTimstamp = Convert.ToDateTime(abstime["@val"].ToString()).ToString("yyyy-MM-dd HH:mm:ss"); + archiveHourRawData.Add("@start_timestamp", startTimstamp); + break; + case "end": + var endTimstamp = Convert.ToDateTime(abstime["@val"].ToString()).ToString("yyyy-MM-dd HH:mm:ss"); + archiveHourRawData.Add("@end_timestamp", endTimstamp); + break; + } + } + } + + //區間內資料筆數 + if (history["int"] != null && history["int"].HasValues) + { + var count = Convert.ToInt32(histories["obj"]["int"]["@val"].ToString()); + archiveHourRawData.Add("@count_rawdata", count); + } + + //整合數值(最大、最小、平均、總和) + if (history["real"] != null && history["real"].HasValues) + { + foreach (var real in history["real"]) + { + var name = real["@name"].ToString(); + switch (name) + { + case "min": + var min = Convert.ToDecimal(real["@val"].ToString()); + archiveHourRawData.Add("@min_rawdata", min); + break; + case "max": + var max = Convert.ToDecimal(real["@val"].ToString()); + archiveHourRawData.Add("@max_rawdata", max); + break; + case "avg": + var avg = Convert.ToDecimal(real["@val"].ToString()); + archiveHourRawData.Add("@avg_rawdata", avg); + break; + case "sum": + var sum = Convert.ToDecimal(real["@val"].ToString()); + archiveHourRawData.Add("@sum_rawdata", sum); + break; + } + } + } + archiveHourRawData.Add("@is_complete", 1); + + archiveHourRawDatas.Add(archiveHourRawData); + } + } + else + { //單筆資料 + Dictionary archiveHourRawData = new Dictionary(); + archiveHourRawData.Add("@device_number", deviceNumberPoint.DeviceNumber); + archiveHourRawData.Add("@point", deviceNumberPoint.Point); + + //時間 + if (histories["obj"]["abstime"] != null && histories["obj"]["abstime"].HasValues) + { + foreach (var abstime in histories["obj"]["abstime"]) + { + var name = abstime["@name"].ToString(); + switch (name) + { + case "start": + var startTimstamp = Convert.ToDateTime(abstime["@val"].ToString()).ToString("yyyy-MM-dd HH:mm:ss"); + archiveHourRawData.Add("@start_timestamp", startTimstamp); + break; + case "end": + var endTimstamp = Convert.ToDateTime(abstime["@val"].ToString()).ToString("yyyy-MM-dd HH:mm:ss"); + archiveHourRawData.Add("@end_timestamp", endTimstamp); + break; + } + } + } + + //區間內資料筆數 + if (histories["obj"]["int"] != null && histories["obj"]["int"].HasValues) + { + var count = Convert.ToInt32(histories["obj"]["int"]["@val"].ToString()); + archiveHourRawData.Add("@count_rawdata", count); + } + + //整合數值(最大、最小、平均、總和) + if (histories["obj"]["real"] != null && histories["obj"]["real"].HasValues) + { + foreach (var real in histories["obj"]["real"]) + { + var name = real["@name"].ToString(); + switch (name) + { + case "min": + var min = Convert.ToDecimal(real["@val"].ToString()); + archiveHourRawData.Add("@min_rawdata", min); + break; + case "max": + var max = Convert.ToDecimal(real["@val"].ToString()); + archiveHourRawData.Add("@max_rawdata", max); + break; + case "avg": + var avg = Convert.ToDecimal(real["@val"].ToString()); + archiveHourRawData.Add("@avg_rawdata", avg); + break; + case "sum": + var sum = Convert.ToDecimal(real["@val"].ToString()); + archiveHourRawData.Add("@sum_rawdata", sum); + break; + } + } + } + archiveHourRawData.Add("@is_complete", 1); + + archiveHourRawDatas.Add(archiveHourRawData); + } + } + } + } + + //stopWatch.Stop(); + //logger.LogInformation("【ArchiveElectricMeterHourJob】【小時歸檔】【效能檢驗】[取得資料花費時間]{0} 毫秒", stopWatch.ElapsedMilliseconds); + + if (archiveHourRawDatas.Count() > 0) + { + await backgroundServiceRepository.AddMutiByCustomTable(archiveHourRawDatas, "archive_electric_meter_hour"); + } + + + await task_Detail.InsertWorkTime_End("ArchiveElectricMeterHourJob", "All", "任務完成"); + } + catch (Exception exception) + { + await task_Detail.WorkFail("ArchiveElectricMeterHourJob", "All", exception.ToString()); + logger.LogError("【ArchiveElectricMeterHourJob】【小時歸檔】【任務失敗】"); + logger.LogError("【ArchiveElectricMeterHourJob】【小時歸檔】【任務失敗】[Exception]:{0}", exception.ToString()); + } + #endregion 小時歸檔 + + + } + + } + catch (Exception exception) + { + await task_Detail.WorkFail("ArchiveElectricMeterHourJob", "All", exception.ToString()); + logger.LogError("【ArchiveElectricMeterHourJob】【任務失敗】"); + logger.LogError("【ArchiveElectricMeterHourJob】【任務失敗】[Exception]:{0}", exception.ToString()); + } + } + } +} diff --git a/Backend/Quartz/Jobs/DataDeliveryJob.cs b/Backend/Quartz/Jobs/DataDeliveryJob.cs new file mode 100644 index 0000000..7a2b7b9 --- /dev/null +++ b/Backend/Quartz/Jobs/DataDeliveryJob.cs @@ -0,0 +1,269 @@ +using Backend.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using Quartz; +using Repository.BackendRepository.Interface; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Backend.Quartz.Jobs +{ + [DisallowConcurrentExecution] + class DataDeliveryJob : IJob + { + private readonly ILogger logger; + private readonly IBackgroundServiceRepository backgroundServiceRepository; + + public DataDeliveryJob( + ILogger logger, + IBackgroundServiceRepository backgroundServiceRepository) + { + this.logger = logger; + this.backgroundServiceRepository = backgroundServiceRepository; + } + + public async Task Execute(IJobExecutionContext context) + { + Dictionary insertLog = new Dictionary() + { + { "@task_id", 0 }, + { "@log_level", "" }, + { "@log_content", "" } + }; + + try + { + logger.LogInformation("【DataDeliveryJob】【任務開始】"); + insertLog["@log_level"] = $@"INFO"; + insertLog["@log_content"] = $@"【DataDeliveryJob】任務開始"; + await backgroundServiceRepository.AddOneByCustomTable(insertLog, "background_service_task_log"); + + //找出所有要派送的資料 + string sWhere = @"is_complete = 0 AND task_type = @Task_type AND repeat_times < 10"; + var backgroundServiceTasks = await backgroundServiceRepository.GetAllAsync("background_service_task", sWhere, new { Task_type = BackgroundServiceTaskType.data_delivery }); + + if (backgroundServiceTasks.Count == 0) + { + logger.LogInformation("【DataDeliveryJob】【查無任務列表】"); + + insertLog["@log_level"] = $@"INFO"; + insertLog["@log_content"] = $@"【DataDeliveryJob】查無任務列表"; + await backgroundServiceRepository.AddOneByCustomTable(insertLog, "background_service_task_log"); + } + else + { + List> updateObjs = new List>(); + foreach (var task in backgroundServiceTasks) + { + var DateTimeNow = DateTime.Now; + Dictionary updateObj = new Dictionary() + { + { "Id", task.Id }, + { "@repeat_times", 0 }, + { "@is_complete", 0 }, + { "@fail_reason", null }, + { "@complete_at", null }, + { "@updated_at", DateTimeNow } + }; + + insertLog["@task_id"] = task.Id; + + //var parameters = JsonSerializer.Deserialize>(task.Target_data); + try + { + logger.LogInformation("【DataDeliveryJob】【開始派送】[棟別IP]:{0}", task.Target_ip); + insertLog["@log_level"] = $@"INFO"; + insertLog["@log_content"] = $@"開始派送"; + await backgroundServiceRepository.AddOneByCustomTable(insertLog, "background_service_task_log"); + + var boundary = "----------------------------" + DateTime.Now.Ticks.ToString("x"); + + var boundaryBytes = Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n"); + var endBoundaryBytes = Encoding.ASCII.GetBytes("\r\n--" + boundary + "--"); + + HttpWebRequest Postrequest = (HttpWebRequest)WebRequest.Create($"http://{task.Target_ip}/api/ReceiveDataDelivery/GetData"); + Postrequest.ContentType = "multipart/form-data; boundary=" + boundary; + Postrequest.Method = "POST"; + + if (!string.IsNullOrEmpty(task.Target_table)) + { + using (Stream requestStream = Postrequest.GetRequestStream()) + { + //Id + requestStream.Write(boundaryBytes, 0, boundaryBytes.Length); + string task_id = "Content-Disposition: form-data; name=\"" + "Id" + "\"\r\n\r\n" + task.Id; + byte[] task_id_bytes = System.Text.Encoding.UTF8.GetBytes(task_id); + requestStream.Write(task_id_bytes, 0, task_id_bytes.Length); + + //Target Table + requestStream.Write(boundaryBytes, 0, boundaryBytes.Length); + string target_table = "Content-Disposition: form-data; name=\"" + "TargetTable" + "\"\r\n\r\n" + task.Target_table; + byte[] target_table_bytes = System.Text.Encoding.UTF8.GetBytes(target_table); + requestStream.Write(target_table_bytes, 0, target_table_bytes.Length); + + //mode + requestStream.Write(boundaryBytes, 0, boundaryBytes.Length); + string target_mode = "Content-Disposition: form-data; name=\"" + "mode" + "\"\r\n\r\n" + task.Mode; + byte[] target_mode_bytes = System.Text.Encoding.UTF8.GetBytes(target_mode); + requestStream.Write(target_mode_bytes, 0, target_mode_bytes.Length); + + //Target data + requestStream.Write(boundaryBytes, 0, boundaryBytes.Length); + string target_data = "Content-Disposition: form-data; name=\"" + "TargetData" + "\"\r\n\r\n" + task.Target_data; + byte[] target_data_bytes = System.Text.Encoding.UTF8.GetBytes(target_data); + requestStream.Write(target_data_bytes, 0, target_data_bytes.Length); + + //解析Files + if (task.Target_files != null) + { + var target_files = JsonSerializer.Deserialize>(task.Target_files); + var file_index = 0; + + foreach (var file in target_files) + { + //file Folder + requestStream.Write(boundaryBytes, 0, boundaryBytes.Length); + string file_folder = "Content-Disposition: form-data; name=\"" + $"FileInfos[{file_index}].Folder" + "\"\r\n\r\n" + file.Folder; + byte[] file_folder_bytes = System.Text.Encoding.UTF8.GetBytes(file_folder); + requestStream.Write(file_folder_bytes, 0, file_folder_bytes.Length); + + //file OriginalFileName + requestStream.Write(boundaryBytes, 0, boundaryBytes.Length); + string orig_file_name = "Content-Disposition: form-data; name=\"" + $"FileInfos[{file_index}].OriginalFileName" + "\"\r\n\r\n" + file.OriginalFileName; + byte[] orig_file_name_bytes = System.Text.Encoding.UTF8.GetBytes(orig_file_name); + requestStream.Write(orig_file_name_bytes, 0, orig_file_name_bytes.Length); + + //file FileName + requestStream.Write(boundaryBytes, 0, boundaryBytes.Length); + string file_name = "Content-Disposition: form-data; name=\"" + $"FileInfos[{file_index}].FileName" + "\"\r\n\r\n" + file.FileName; + byte[] file_name_bytes = System.Text.Encoding.UTF8.GetBytes(file_name); + requestStream.Write(file_name_bytes, 0, file_name_bytes.Length); + + //取得Content-Type + var content_type = string.Empty; + string ext = Path.GetExtension(file.File); + using (Microsoft.Win32.RegistryKey registryKey = Microsoft.Win32.Registry.ClassesRoot.OpenSubKey(ext)) + { + if (registryKey != null) + { + var value = registryKey.GetValue("Content Type"); + if (value != null) + { + content_type = value.ToString(); + } + } + } + + if (file.File != null) + { + string file_header = "Content-Disposition: form-data; name=\"" + $"FileInfos[{file_index}].File" + "\"; filename=\"" + file.FileName + "\"\r\nContent-Type: " + content_type + "\r\n\r\n"; + byte[] file_header_bytes = System.Text.Encoding.UTF8.GetBytes(file_header); + + requestStream.Write(boundaryBytes, 0, boundaryBytes.Length); + requestStream.Write(file_header_bytes, 0, file_header_bytes.Length); + byte[] buffer = new byte[32768]; + int bytesRead; + + // upload from file + using (FileStream fileStream = File.OpenRead(file.File)) + { + while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0) + requestStream.Write(buffer, 0, bytesRead); + fileStream.Close(); + } + } + } + } + + requestStream.Write(endBoundaryBytes, 0, endBoundaryBytes.Length); + requestStream.Close(); + } + } + + HttpWebResponse response = (HttpWebResponse)Postrequest.GetResponse(); + var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd(); + var statusNumber = (int)response.StatusCode; + if (statusNumber != 200) + { + logger.LogError("【DataDeliveryJob】【派送失敗】[棟別IP]:{0}", task.Target_ip); + logger.LogError("【DataDeliveryJob】【派送失敗】[response]:{0}", responseString); + updateObj["@repeat_times"] = task.Repeat_times + 1; + updateObj["@fail_reason"] = responseString; + + insertLog["@log_level"] = $@"ERR"; + insertLog["@log_content"] = $@"派送失敗 - [失敗原因]:{responseString}"; + await backgroundServiceRepository.AddOneByCustomTable(insertLog, "background_service_task_log"); + } + else + { + //解析回傳內容 + var final = JObject.Parse(responseString); + var code = final["code"].ToString(); + if (code == "0000") + { + logger.LogInformation("【DataDeliveryJob】【派送成功】[棟別IP]:{0}", task.Target_ip); + updateObj["@repeat_times"] = task.Repeat_times; + updateObj["@is_complete"] = 1; + updateObj["@complete_at"] = DateTime.Now; + + insertLog["@log_level"] = $@"INFO"; + insertLog["@log_content"] = $@"派送成功"; + await backgroundServiceRepository.AddOneByCustomTable(insertLog, "background_service_task_log"); + } + else + { + logger.LogError("【DataDeliveryJob】【派送失敗】[棟別IP]:{0}", task.Target_ip); + logger.LogError("【DataDeliveryJob】【派送失敗】[response]:{0}", responseString); + updateObj["@repeat_times"] = task.Repeat_times + 1; + updateObj["@fail_reason"] = responseString; + + insertLog["@log_level"] = $@"ERR"; + insertLog["@log_content"] = $@"派送失敗 - [失敗原因]:{responseString}"; + await backgroundServiceRepository.AddOneByCustomTable(insertLog, "background_service_task_log"); + } + } + } + catch (Exception exception) + { + logger.LogError("【DataDeliveryJob】【派送失敗】[棟別IP]:{0}", task.Target_ip); + logger.LogError("【DataDeliveryJob】【派送失敗】[Exception]:{0}", exception.ToString()); + updateObj["@repeat_times"] = task.Repeat_times + 1; + updateObj["@fail_reason"] = exception.ToString(); + + insertLog["@log_level"] = $@"ERR"; + insertLog["@log_content"] = $@"派送失敗 - [失敗原因(Exception)]:{exception.ToString()}"; + await backgroundServiceRepository.AddOneByCustomTable(insertLog, "background_service_task_log"); + } + + updateObjs.Add(updateObj); + } + + await backgroundServiceRepository.UpdateListByCustomTable(updateObjs, "background_service_task", "id = @Id"); + logger.LogInformation("【DataDeliveryJob】【任務完成】"); + + insertLog["@task_id"] = 0; + insertLog["@log_level"] = $@"INFO"; + insertLog["@log_content"] = $@"任務完成"; + await backgroundServiceRepository.AddOneByCustomTable(insertLog, "background_service_task_log"); + } + } + catch (Exception exception) + { + logger.LogError("【DataDeliveryJob】【任務失敗】"); + logger.LogError("【DataDeliveryJob】【任務失敗】[Exception]:{0}", exception.ToString()); + + insertLog["@task_id"] = 0; + insertLog["@log_level"] = $@"ERR"; + insertLog["@log_content"] = $@"任務失敗"; + await backgroundServiceRepository.AddOneByCustomTable(insertLog, "background_service_task_log"); + } + + } + } +} diff --git a/Backend/Quartz/Jobs/ExecutionBackgroundServicePlanJob.cs b/Backend/Quartz/Jobs/ExecutionBackgroundServicePlanJob.cs new file mode 100644 index 0000000..fae06f2 --- /dev/null +++ b/Backend/Quartz/Jobs/ExecutionBackgroundServicePlanJob.cs @@ -0,0 +1,113 @@ +using Backend.Models; +using Microsoft.Extensions.Logging; +using Quartz; +using Repository.BackendRepository.Interface; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Backend.Quartz.Jobs +{ + [DisallowConcurrentExecution] + public class ExecutionBackgroundServicePlanJob : IJob + { + private readonly ILogger logger; + private readonly IBackgroundServiceRepository backgroundServiceRepository; + + public ExecutionBackgroundServicePlanJob( + ILogger logger, + IBackgroundServiceRepository backgroundServiceRepository) + { + this.logger = logger; + this.backgroundServiceRepository = backgroundServiceRepository; + } + + public async Task Execute(IJobExecutionContext context) + { + try + { + logger.LogInformation("【ExecutionBackgroundServicePlanJob】【任務開始】"); + + // 找出當前在起始與結束時間所有計畫 + var DateTimeNow = DateTime.Now; + var sPlanWhere = @"deleted = 0 + AND + ( + @DateTimeNow Between start_time AND end_time + OR (end_time IS NULL AND @DateTimeNow > start_time) + ) + "; + var backgroundServicePlans = await backgroundServiceRepository.GetAllAsync("background_service_plan", sPlanWhere, new { DateTimeNow = DateTimeNow.ToString("yyyy-MM-dd HH:mm:ss") }); + + foreach (var plan in backgroundServicePlans) + { + //logger.LogInformation("【ExecutionBackgroundServicePlanJob】【計畫編號:{0},計畫名稱:{1}】 - 開始生成下次任務項目", plan.Id, plan.Plane_name); + #region 紀錄最後生成任務的時間 + try + { + var lastCreateTime = Convert.ToDateTime(plan.Last_create_time); + if (lastCreateTime == default(DateTime)) + { + lastCreateTime = Convert.ToDateTime(plan.Start_time); + } + + DateTime nextCreateTime; //下次待生成的時間 + nextCreateTime = plan.Execution_type switch + { + (byte)ExecutionTypeEnum.Min => Convert.ToDateTime(lastCreateTime).AddMinutes(plan.Execution_time), + (byte)ExecutionTypeEnum.Hour => Convert.ToDateTime(lastCreateTime).AddHours(plan.Execution_time), + (byte)ExecutionTypeEnum.Day => Convert.ToDateTime(lastCreateTime).AddDays(plan.Execution_time), + (byte)ExecutionTypeEnum.Week => Convert.ToDateTime(lastCreateTime).AddDays(plan.Execution_time * 7), + (byte)ExecutionTypeEnum.Month => Convert.ToDateTime(lastCreateTime).AddMonths(plan.Execution_time), + _ => default(DateTime) + }; + + if (nextCreateTime != default(DateTime) && nextCreateTime < DateTimeNow) + { + Dictionary servicePlanDic = new Dictionary() + { + { "@last_create_time", nextCreateTime}, + { "@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}, + }; + + await backgroundServiceRepository.UpdateOneByCustomTable(servicePlanDic, "background_service_plan", "id=" + plan.Id + ""); + + #region 建立任務 + try + { + BackgroundServiceTask backgroundServiceTask = new BackgroundServiceTask() + { + + }; + } + catch(Exception exception) + { + + } + #endregion 建立任務 + } + + } + catch (Exception exception) + { + //logger.LogError("【ExecutionBackgroundServicePlanJob】【計畫編號:{0},計畫名稱:{1}】 - 產生下次任務時間失敗", plan.Id, plan.Plane_name); + //logger.LogError("【ExecutionBackgroundServicePlanJob】【計畫編號:{0},計畫名稱:{1}】 - 產生下次任務時間失敗[Exception]- {2}", plan.Id, plan.Plane_name, exception.Message); ; + } + #endregion 紀錄最後該生成任務的時間 + + } + + + + logger.LogInformation("【ExecutionBackgroundServicePlanJob】【任務完成】"); + } + catch (Exception exception) + { + logger.LogError("【ExecutionBackgroundServicePlanJob】【任務失敗】"); + logger.LogError("【ExecutionBackgroundServicePlanJob】【任務失敗】[Exception]:{0}", exception.Message); ; + } + + } + } +} diff --git a/Backend/Quartz/Jobs/MessageNotificationJob.cs b/Backend/Quartz/Jobs/MessageNotificationJob.cs new file mode 100644 index 0000000..50e6100 --- /dev/null +++ b/Backend/Quartz/Jobs/MessageNotificationJob.cs @@ -0,0 +1,163 @@ +using Backend.Models; +using BackendWorkerService.Services.Interface; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Quartz; +using Repository.BackendRepository.Interface; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Backend.Quartz.Jobs +{ + [DisallowConcurrentExecution] + class MessageNotificationJob : IJob + { + private readonly ILogger logger; + private readonly IBackgroundServiceRepository backgroundServiceRepository; + + private readonly ISendEmailService sendEmailService; + private readonly ISendSMSService sendSMSService; + private readonly ISendLineNotifyService sendLineNotifyService; + + + public MessageNotificationJob( + ILogger logger, + IBackgroundServiceRepository backgroundServiceRepository, + ISendEmailService sendEmailService, + ISendSMSService sendSMSService, + ISendLineNotifyService sendLineNotifyService) + { + this.logger = logger; + this.backgroundServiceRepository = backgroundServiceRepository; + this.sendEmailService = sendEmailService; + this.sendSMSService = sendSMSService; + this.sendLineNotifyService = sendLineNotifyService; + } + + public async Task Execute(IJobExecutionContext context) + { + Dictionary insertLog = new Dictionary() + { + { "@task_id", 0 }, + { "@log_level", "" }, + { "@log_content", "" } + }; + + try + { + logger.LogInformation("【MessageNotificationJob】【任務開始】"); + insertLog["@log_level"] = $@"INFO"; + insertLog["@log_content"] = $@"任務開始"; + await backgroundServiceRepository.AddOneByCustomTable(insertLog, "background_service_message_notification_task_log"); + + //找出所有要通知的清單 + string sWhere = @"is_complete = 0"; + var messageNotificationTasks = await backgroundServiceRepository.GetAllAsync("background_service_message_notification_task", sWhere); + + if(messageNotificationTasks.Count == 0) + { + logger.LogInformation("【MessageNotificationJob】【查無任務列表】"); + + insertLog["@log_level"] = $@"INFO"; + insertLog["@log_content"] = $@"查無任務列表"; + await backgroundServiceRepository.AddOneByCustomTable(insertLog, "background_service_message_notification_task_log"); + } + else + { + List> updateObjs = new List>(); + foreach (var task in messageNotificationTasks) + { + var DateTimeNow = DateTime.Now; + Dictionary updateObj = new Dictionary() + { + { "Id", task.Id }, + { "@updated_at", DateTimeNow } + }; + + try + { + switch (task.Task_type) + { + case (byte)MessageNotificationTaskType.email: + List recipientEmails = new List() + { + task.Recipient_email + }; + + if (sendEmailService.Send(task.Id, recipientEmails, task.Email_subject, task.Message_content)) + { + updateObj.Add("@is_complete", 1); + updateObj.Add("@complete_at", DateTimeNow); + } + else + { + updateObj.Add("@repeat_times", task.Repeat_times + 1); + } + + updateObjs.Add(updateObj); + break; + case (byte)MessageNotificationTaskType.sms: + if (sendSMSService.Send(task.Id, task.Recipient_phone, task.Message_content)) + { + updateObj.Add("@is_complete", 1); + updateObj.Add("@complete_at", DateTimeNow); + } + else + { + updateObj.Add("@repeat_times", task.Repeat_times + 1); + } + + updateObjs.Add(updateObj); + break; + case (byte)MessageNotificationTaskType.line_notify: + if (sendLineNotifyService.Send(task.Id, task.Line_token, task.Message_content)) + { + updateObj.Add("@is_complete", 1); + updateObj.Add("@complete_at", DateTimeNow); + } + else + { + updateObj.Add("@repeat_times", task.Repeat_times + 1); + } + + updateObjs.Add(updateObj); + break; + } + } + catch(Exception exception) + { + logger.LogError("【MessageNotificationJob】【通知失敗】[任務id]:{0}", task.Id); + logger.LogError("【MessageNotificationJob】【通知失敗】[Exception]:{0}", exception.ToString()); + + insertLog["@log_level"] = $@"ERR"; + insertLog["@log_content"] = $@"通知失敗 - [失敗原因(Exception)]:{exception.ToString()}"; + await backgroundServiceRepository.AddOneByCustomTable(insertLog, "background_service_message_notification_task_log"); + } + + } + + await backgroundServiceRepository.UpdateListByCustomTable(updateObjs, "background_service_message_notification_task", "id = @Id"); + logger.LogInformation("【MessageNotificationJob】【任務完成】"); + + insertLog["@task_id"] = 0; + insertLog["@log_level"] = $@"INFO"; + insertLog["@log_content"] = $@"任務完成"; + await backgroundServiceRepository.AddOneByCustomTable(insertLog, "background_service_message_notification_task_log"); + } + } + catch (Exception exception) + { + logger.LogError("【MessageNotificationJob】【任務失敗】"); + logger.LogError("【MessageNotificationJob】【任務失敗】[Exception]:{0}", exception.ToString()); + + insertLog["@task_id"] = 0; + insertLog["@log_level"] = $@"ERR"; + insertLog["@log_content"] = $@"任務失敗"; + await backgroundServiceRepository.AddOneByCustomTable(insertLog, "background_service_message_notification_task_log"); + } + + } + } +} diff --git a/Backend/Quartz/Jobs/ParkingJob.cs b/Backend/Quartz/Jobs/ParkingJob.cs new file mode 100644 index 0000000..2486bf4 --- /dev/null +++ b/Backend/Quartz/Jobs/ParkingJob.cs @@ -0,0 +1,324 @@ +using Backend.Models; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Quartz; +using Repository.BackendRepository.Interface; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace Backend.Quartz.Jobs +{ + /// + /// 停車場管理 + /// + [DisallowConcurrentExecution] + class ParkingJob : IJob + { + private readonly ILogger logger; + private readonly IBackgroundServiceRepository backgroundServiceRepository; + private readonly IBackendRepository backendRepository; + private readonly ILogger loggers; + + public ParkingJob( + ILogger logger, + IBackgroundServiceRepository backgroundServiceRepository, ILogger loggers, IBackendRepository backendRepository) + { + this.logger = logger; + this.backgroundServiceRepository = backgroundServiceRepository; + this.backendRepository = backendRepository; + this.loggers = loggers; + } + + public async Task Execute(IJobExecutionContext context) + { + Task_Detail task_Detail = new Task_Detail(loggers, backendRepository); + try + { + if(await task_Detail.GetNeedWorkTask("ParkingJob", "All")) + { + await task_Detail.InsertWorkTime("ParkingJob", "All", "任務開始"); + EDFunction ed = new EDFunction(); + var parkingConfig = new ParkingConfig(); + + var sqlParking = $@"SELECT system_value as Value, system_key as Name FROM variable WHERE deleted = 0 AND system_type = 'parkingConfig'"; + + var variable = await backgroundServiceRepository.GetAllAsync(sqlParking); + parkingConfig.Host = variable.Where(x => x.Name == "Host").Select(x => x.Value).FirstOrDefault(); + parkingConfig.Prefix = variable.Where(x => x.Name == "Prefix").Select(x => x.Value).FirstOrDefault(); + parkingConfig.ApiBase = variable.Where(x => x.Name == "ApiBase").Select(x => x.Value).FirstOrDefault(); + parkingConfig.UserName = ed.AESDecrypt(variable.Where(x => x.Name == "UserName").Select(x => x.Value).FirstOrDefault()); + parkingConfig.Password = ed.AESDecrypt(variable.Where(x => x.Name == "Password").Select(x => x.Value).FirstOrDefault()); + + String encoded = System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(parkingConfig.UserName + ":" + parkingConfig.Password)); + + #region 取得停車場資訊 + if (await task_Detail.GetNeedWorkTask("ParkingJob", "Parking")) + { + try + { + await task_Detail.InsertWorkTime("ParkingJob", "Parking", "開始執行停車場剩餘車位Job"); + HttpWebRequest spaceRequest = (HttpWebRequest)WebRequest.Create($"{parkingConfig.Host}{parkingConfig.Prefix}/api/space/details"); + spaceRequest.Method = "GET"; + //request.Headers.Add("Authorization", "Basic " + encoded); + spaceRequest.PreAuthenticate = true; + + //Stopwatch stopWatch = new Stopwatch(); + //stopWatch.Start(); + + HttpWebResponse spaceResponse = (HttpWebResponse)spaceRequest.GetResponse(); + var spaceResponseContent = new StreamReader(spaceResponse.GetResponseStream()).ReadToEnd(); + //logger.LogInformation("【ParkingJob】【取得成功停車場車位資訊】"); + //stopWatch.Stop(); + //logger.LogInformation("【ParkingJob】【取得停車場車位資訊】【效能檢驗】[取得資料花費時間]{0} 毫秒", stopWatch.ElapsedMilliseconds); + + var spaceResponseResult = JsonConvert.DeserializeObject(spaceResponseContent); + + //取得停車場車位對應表 + var sqlSapceMapping = $@"SELECT * FROM variable WHERE deleted = 0 AND system_type = 'parkingSapceMapping'"; + var parkingSapceMapping = await backgroundServiceRepository.GetAllAsync(sqlSapceMapping); + + if (spaceResponseResult != null && spaceResponseResult.Code == "20000") + { + foreach (var area in spaceResponseResult.Payload.Areas) + { + //找出對定的設備代碼 + var selectedMapping = parkingSapceMapping.Where(x => x.System_key == area.Name).FirstOrDefault(); + if (selectedMapping != null) + { + var tagName = selectedMapping.system_value; + var apiFormat = @"{0}obix/config/Arena/{1}/{2}/{3}/{4}/{5}/CV/set"; + + var tagNameSplit = tagName.Split("_"); + + var parames = new List(); + parames.Add(parkingConfig.ApiBase); + for (var i = 0; i < tagNameSplit.Length; i++) + { + if (i != tagNameSplit.Length - 1) + { + parames.Add(tagNameSplit[i]); + } + else + { + parames.Add(tagName); + } + } + + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(string.Format(apiFormat, parames.ToArray())); + request.Method = "POST"; + request.Headers.Add("Authorization", "Basic " + encoded); + request.PreAuthenticate = true; + + var real = $@""; + byte[] realByteArray = Encoding.UTF8.GetBytes(real); + using (Stream reqStream = request.GetRequestStream()) + { + reqStream.Write(realByteArray, 0, realByteArray.Length); + } + + HttpWebResponse response = (HttpWebResponse)request.GetResponse(); + var responseContent = new StreamReader(response.GetResponseStream()).ReadToEnd(); + + XmlDocument xmlDocument = new XmlDocument(); + xmlDocument.LoadXml(responseContent); + string json = JsonConvert.SerializeXmlNode(xmlDocument); + JObject jsonResult = (JObject)JsonConvert.DeserializeObject(json); + + if (jsonResult.ContainsKey("err")) //抓取錯誤 + { + logger.LogError("【ParkingJob】【停車場剩餘車位資訊】"); + logger.LogError("【ParkingJob】【停車場剩餘車位資訊】[錯誤內容]:{0}", json); + } + else + { + if (jsonResult.ContainsKey("real")) //表示可以讀取到內容 + { + List> ontimeRawDatas = new List>(); + + var realList = jsonResult["real"]; + var display = realList["@display"]; + if (display != null) + { + var tempStrSplit = display.ToString().Split(" "); + if (tempStrSplit[0] != area.Remain.ToString()) + { + logger.LogError("【ParkingJob】【停車場剩餘車位資訊】[修改失敗]:{0}", display.ToString()); + } + } + } + } + } + else + { + logger.LogWarning("【ParkingJob】【停車場剩餘車位資訊】[查無該名稱對應表]:{0}", area.Name); + } + } + + } + else + { + logger.LogWarning("【ParkingJob】【停車場剩餘車位資訊】 - [查無資料]"); + } + await task_Detail.InsertWorkTime_End("ParkingJob", "Parking", "執行成功停車場剩餘車位Job"); + //logger.LogInformation("【ParkingJob】【執行成功停車場剩餘車位Job】"); + } + catch (Exception exception) + { + await task_Detail.WorkFail("ParkingJob", "Parking", exception.ToString()); + logger.LogInformation("【ParkingJob】【執行失敗停車場剩餘車位Job】"); + logger.LogInformation("【ParkingJob】【執行失敗停車場剩餘車位Job】[Exception]:{0}", exception.ToString()); + } + } + #endregion + + #region 取得設備資訊 + if (await task_Detail.GetNeedWorkTask("ParkingJob", "Device")) + { + try + { + await task_Detail.InsertWorkTime("ParkingJob", "Device", "開始執行設備資訊Job"); + //logger.LogInformation("【ParkingJob】【開始執行設備資訊Job】"); + + HttpWebRequest equipmentRequest = (HttpWebRequest)WebRequest.Create($"{parkingConfig.Host}{parkingConfig.Prefix}/api/equipment/state"); + equipmentRequest.Method = "GET"; + //request.Headers.Add("Authorization", "Basic " + encoded); + equipmentRequest.PreAuthenticate = true; + + HttpWebResponse equipmentResponse = (HttpWebResponse)equipmentRequest.GetResponse(); + var equipmentResponseContent = new StreamReader(equipmentResponse.GetResponseStream()).ReadToEnd(); + + var equipmentResponseResult = JsonConvert.DeserializeObject(equipmentResponseContent); + + //取得設備對應表 + var sqlEquipmentMapping = $@"SELECT * FROM variable WHERE deleted = 0 AND system_type = 'parkingEquipmentMapping'"; + var parkingEquipmentMapping = await backgroundServiceRepository.GetAllAsync(sqlEquipmentMapping); + + //List parkingEquipmentMapping = new List(); + //VariableInfo variableInfo_1 = new VariableInfo(); + //variableInfo_1.System_key = "APS-000"; + //variableInfo_1.system_value = "D3_P_B4F_CAPS_B413"; + + //parkingEquipmentMapping.Add(variableInfo_1); + + //VariableInfo variableInfo_2 = new VariableInfo(); + //variableInfo_2.System_key = "APS-001"; + //variableInfo_2.system_value = "D3_P_B4F_CAPS_B414"; + //parkingEquipmentMapping.Add(variableInfo_2); + + if (equipmentResponseResult != null && equipmentResponseResult.Code == "20000") + { + foreach (var equipment in equipmentResponseResult.Payload) + { + //找出對定的設備代碼 + var selectedMapping = parkingEquipmentMapping.Where(x => x.System_key == equipment.Name).FirstOrDefault(); + if (selectedMapping != null) + { + + var tagName = selectedMapping.system_value; + var apiFormat = @"{0}obix/config/Arena/{1}/{2}/{3}/{4}/{5}/ST/set"; + + var tagNameSplit = tagName.Split("_"); + + var parames = new List(); + parames.Add(parkingConfig.ApiBase); + for (var i = 0; i < tagNameSplit.Length; i++) + { + if (i != tagNameSplit.Length - 1) + { + parames.Add(tagNameSplit[i]); + } + else + { + parames.Add(tagName); + } + } + + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(string.Format(apiFormat, parames.ToArray())); + request.Method = "POST"; + request.Headers.Add("Authorization", "Basic " + encoded); + request.PreAuthenticate = true; + + var real = $@""; + byte[] realByteArray = Encoding.UTF8.GetBytes(real); + using (Stream reqStream = request.GetRequestStream()) + { + reqStream.Write(realByteArray, 0, realByteArray.Length); + } + + HttpWebResponse response = (HttpWebResponse)request.GetResponse(); + var responseContent = new StreamReader(response.GetResponseStream()).ReadToEnd(); + + XmlDocument xmlDocument = new XmlDocument(); + xmlDocument.LoadXml(responseContent); + string json = JsonConvert.SerializeXmlNode(xmlDocument); + JObject jsonResult = (JObject)JsonConvert.DeserializeObject(json); + + if (jsonResult.ContainsKey("err")) //抓取錯誤 + { + logger.LogError("【ParkingJob】【設備資訊】"); + logger.LogError("【ParkingJob】【設備資訊】[錯誤內容]:{0}", json); + } + else + { + if (jsonResult.ContainsKey("bool")) //表示可以讀取到內容 + { + List> ontimeRawDatas = new List>(); + + var realList = jsonResult["bool"]; + var val = realList["@val"]; + if (val != null) + { + var tempStrSplit = val.ToString(); + if (tempStrSplit != equipment.Alive.ToString().ToLower()) + { + logger.LogError("【ParkingJob】【設備資訊】[修改失敗]:{0}", val.ToString()); + } + } + } + } + } + else + { + logger.LogWarning("【ParkingJob】【設備資訊】[查無該名稱對應表]:{0}", equipment.Name); + } + } + } + else + { + logger.LogWarning("【ParkingJob】【設備資訊】 - [查無資料]"); + } + await task_Detail.InsertWorkTime_End("ParkingJob", "Device", "執行成功設備資訊Job"); + //logger.LogInformation("【ParkingJob】【執行成功設備資訊Job】"); + } + catch (Exception exception) + { + await task_Detail.WorkFail("ParkingJob", "Device", exception.ToString()); + logger.LogInformation("【ParkingJob】【執行失敗設備資訊Job】"); + logger.LogInformation("【ParkingJob】【執行失敗設備資訊Job】[Exception]:{0}", exception.ToString()); + } + } + #endregion + + await task_Detail.InsertWorkTime_End("ParkingJob", "All", "任務完成"); + } + + + } + catch (Exception exception) + { + await task_Detail.WorkFail("ParkingJob", "All", exception.ToString()); + logger.LogError("【ParkingJob】【任務失敗】"); + logger.LogError("【ParkingJob】【任務失敗】[Exception]:{0}", exception.ToString()); + } + + } + } +} diff --git a/Backend/Quartz/Jobs/RegularUpdateDBTableJob.cs b/Backend/Quartz/Jobs/RegularUpdateDBTableJob.cs new file mode 100644 index 0000000..dc9d577 --- /dev/null +++ b/Backend/Quartz/Jobs/RegularUpdateDBTableJob.cs @@ -0,0 +1,77 @@ +using Backend.Services.Implement; +using Microsoft.Extensions.Logging; +using Quartz; +using Repository.BackendRepository.Interface; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Backend.Quartz.Jobs +{ + /// + /// 定時將資料表加入派送任務 + /// + [DisallowConcurrentExecution] + class RegularUpdateDBTableJob: IJob + { + private readonly ILogger logger; + private readonly IBackgroundServiceRepository backgroundServiceRepository; + private readonly IBackendRepository backendRepository; + + public RegularUpdateDBTableJob( + ILogger logger, + IBackgroundServiceRepository backgroundServiceRepository, + IBackendRepository backendRepository) + { + this.logger = logger; + this.backgroundServiceRepository = backgroundServiceRepository; + this.backendRepository = backendRepository; + } + + public async Task Execute(IJobExecutionContext context) + { + try + { + logger.LogInformation("【RegularUpdateDBTableJob】【任務開始】"); + + //要派送的資料表 + List db_tables = new List() { "variable" }; + + foreach (var db_table in db_tables) + { + try + { + logger.LogInformation("【RegularUpdateDBTableJob】【新增資料表 [{0}] 至派送任務】", db_table); + //取得資料表所有資料 + var temp_datas = (await backgroundServiceRepository.GetAllAsync(db_table, "")); + + List> dicts = new List>(); + foreach (var temp_data in temp_datas) + { + var dict = new Dictionary((IDictionary)temp_data); + + dicts.Add(dict); + } + + await backendRepository.ManualInsertBackgroundServiceTask("", "", db_table, "purge_all_insert", dicts); + + logger.LogError("【RegularUpdateDBTableJob】【新增成功 資料表 [{0}] 至派送任務】", db_table); + } + catch(Exception exception) + { + logger.LogError("【RegularUpdateDBTableJob】【新增失敗 資料表 [{0}] 至派送任務】", db_table); + logger.LogError("【RegularUpdateDBTableJob】【新增失敗 資料表 [{0}] 至派送任務】[Exception]:{1}", db_table, exception.ToString()); + } + } + } + catch (Exception exception) + { + logger.LogError("【RegularUpdateDBTableJob】【任務失敗】"); + logger.LogError("【RegularUpdateDBTableJob】【任務失敗】[Exception]:{0}", exception.ToString()); + } + + } + } +} diff --git a/Backend/Quartz/Jobs/WeatherAPIJob.cs b/Backend/Quartz/Jobs/WeatherAPIJob.cs new file mode 100644 index 0000000..eb850d8 --- /dev/null +++ b/Backend/Quartz/Jobs/WeatherAPIJob.cs @@ -0,0 +1,559 @@ +using Backend.Models; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Quartz; +using Repository.BackendRepository.Implement; +using Repository.BackendRepository.Interface; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Serialization; +using System.Linq; +using NCrontab; +using BackendWorkerService.Services.Implement; + +namespace Backend.Quartz.Jobs +{ + [DisallowConcurrentExecution] + class WeatherAPIJob : IJob + { + private readonly ILogger logger; + private readonly IBackgroundServiceRepository backgroundServiceRepository; + private readonly IBackendRepository backendRepository; + private readonly ILogger loggers; + public WeatherAPIJob(ILogger logger, + IBackgroundServiceRepository backgroundServiceRepository, IBackendRepository backendRepository, ILogger loggers) + { + this.logger = logger; + this.backgroundServiceRepository = backgroundServiceRepository; + this.backendRepository = backendRepository; + this.loggers = loggers; + } + public string Fetch_PostWithJSONFormat(string url, string paramter) + { + try + { + string username = "stanGG"; + string password = "St12345678"; + string encoded = Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(username + ":" + password)); + HttpWebRequest Postrequest = (HttpWebRequest)WebRequest.Create(url); + Postrequest.Method = "POST"; + Postrequest.Headers.Add("Authorization", "Basic " + encoded); + Postrequest.PreAuthenticate = true; + + using (var streamWriter = new StreamWriter(Postrequest.GetRequestStream())) + { + if (paramter != "NULL") + { + string json = ""; + streamWriter.Write(json); + } + else + { + string json = ""; + streamWriter.Write(json); + } + } + HttpWebResponse response = (HttpWebResponse)Postrequest.GetResponse(); + var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd(); + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(responseString); + string jsonText = JsonConvert.SerializeXmlNode(xmlDoc); + //JObject resultVal = (JObject)JsonConvert.DeserializeObject(jsonText); + + return jsonText; + + } + catch (Exception ex) + { + logger.LogError("【WeatherAPIJob】" + "Fetch_PostWithJSONFormat:" + ex.ToString()); + throw ex; + } + } + + + public async Task Execute(IJobExecutionContext context) + { + Task_Detail task_Detail = new Task_Detail(loggers, backendRepository); + try + { + var obixApiConfig = new ObixApiConfig(); + string encoded = string.Empty; + #region 取得obix 設定 + var sqlObix = $@"SELECT system_value as Value, system_key as Name FROM variable WHERE deleted = 0 AND system_type = 'obixConfig'"; + + var variableObix = await backgroundServiceRepository.GetAllAsync(sqlObix); + obixApiConfig.ApiBase = variableObix.Where(x => x.Name == "ApiBase").Select(x => x.Value).FirstOrDefault(); + obixApiConfig.UserName = variableObix.Where(x => x.Name == "UserName").Select(x => x.Value).FirstOrDefault(); + obixApiConfig.Password = variableObix.Where(x => x.Name == "Password").Select(x => x.Value).FirstOrDefault(); + + encoded = System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(obixApiConfig.UserName + ":" + obixApiConfig.Password)); + #endregion 取得obix 設定 + //取得氣象預報 + if (await task_Detail.GetNeedWorkTask("WeatherAPI", "api_weateher")) + { + try + { + await task_Detail.InsertWorkTime("WeatherAPI", "api_weateher"); + var client = new HttpClient(); + var DataNO = "F-D0047-061"; + var UVUri = "https://opendata.cwb.gov.tw/api/v1/rest/datastore/F-D0047-061?Authorization=CWB-EA24220B-DDCC-4188-84E5-AD37A0E03F80&format=JSON&locationName=%E4%BF%A1%E7%BE%A9%E5%8D%80&elementName=Wx,PoP12h,T,RH"; + HttpResponseMessage response = client.GetAsync(UVUri).Result; + String jsonUVs = response.Content.ReadAsStringAsync().Result.ToString(); + + var observation = JsonConvert.DeserializeObject(jsonUVs); + logger.LogInformation("【WeatherAPIJob】【取得成功氣象預報】"); + + if (observation.Success != "true") + { + logger.LogInformation("【WeatherAPIJob】【取得氣象預報資料不正確】"); + } + else + { + logger.LogInformation("【WeatherAPIJob】【開始存入氣象預報到資料庫】"); + List> WeatherAPIdbS = new List>(); + var Type_ALL = observation.Records.Locations[0].Location[0].WeatherElement; + + foreach (var a in Type_ALL) + { + + foreach (var b in a.Time) + { + if (a.ElementName == "PoP12h" || a.ElementName == "Wx") + { + if (Convert.ToDateTime(b.StartTime) > DateTime.Now.AddDays(1)) + { + break; + } + } + else + { + if (Convert.ToDateTime(b.DataTime) > DateTime.Now.AddDays(1)) + { + break; + } + } + Dictionary WeatherAPIdb = new Dictionary() + { + { "@weather_type", a.ElementName}, + { "@data_no", DataNO}, + { "@get_value", b.ElementValue[0].Value}, + { "@measures", b.ElementValue[0].Measures}, + { "@created_by", "system"}, + { "@created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}, + }; + if (a.ElementName == "PoP12h" || a.ElementName == "Wx") + { + WeatherAPIdb.Add("@start_time", b.StartTime); + WeatherAPIdb.Add("@end_time", b.EndTime); + } + else + { + WeatherAPIdb.Add("@start_time", b.DataTime); + WeatherAPIdb.Add("@end_time", null); + } + WeatherAPIdbS.Add(WeatherAPIdb); + + if (a.ElementName == "Wx") + { + Dictionary TWeatherAPIdb = new Dictionary() + { + { "@weather_type", "WxV"}, + { "@data_no", DataNO}, + { "@get_value", b.ElementValue[1].Value}, + { "@measures", b.ElementValue[1].Measures}, + { "@start_time", b.StartTime}, + { "@end_time", b.EndTime}, + { "@created_by", "system"}, + { "@created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}, + }; + WeatherAPIdbS.Add(TWeatherAPIdb); + } + + } + } + await backendRepository.AddMutiByCustomTable(WeatherAPIdbS, "api_weateher"); + } + + await task_Detail.InsertWorkTime_End("WeatherAPI", "api_weateher"); + } + catch (Exception ex) + { + await task_Detail.WorkFail("WeatherAPI", "api_weateher", ex.Message.ToString()); + } + } + + if (await task_Detail.GetNeedWorkTask("WeatherAPI", "api_rain")) + { + try + { + await task_Detail.InsertWorkTime("WeatherAPI", "api_rain"); + WebClient mywebClient = new WebClient(); + mywebClient.DownloadFile("https://opendata.cwb.gov.tw/fileapi/v1/opendataapi/W-C0033-003?Authorization=CWB-EA24220B-DDCC-4188-84E5-AD37A0E03F80&downloadType=WEB&format=CAP", @"root/PowerfulRain.xml"); + XmlDocument doc = new XmlDocument(); + doc.Load("root/PowerfulRain.xml"); + var json = JsonConvert.SerializeXmlNode(doc); + var haveinfo = json.Split("info"); + //if (haveinfo.Length > 2) + //{ + // var observation = RainApi.Welcome.FromJson(json); + // var area = observation.Alert.Info[0].Area.Where(a => a.Geocode.Value == "63").Select(a => a.AreaDesc).FirstOrDefault(); + // var sql = $"select id from api_rain where msgType = '{observation.Alert.MsgType}' and onset = '{observation.Alert.Info[0].Onset.ToString("yyyy-MM-dd HH:mm:ss")}' and expires = '{observation.Alert.Info[0].Expires.ToString("yyyy-MM-dd HH:mm:ss")}'"; + // var NeedCallApi = await backendRepository.GetOneAsync(sql); + + // Dictionary RainAPIdb = new Dictionary() + // { + // { "@msgType", observation.Alert.MsgType}, + // { "@headline", observation.Alert.Info[0].Headline}, + // { "@areaDesc", area}, + // { "@onset", observation.Alert.Info[0].Onset}, + // { "@expires", observation.Alert.Info[0].Expires}, + // { "@created_by", "system"}, + // { "@created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}, + // }; + // var id = await backendRepository.AddOneByCustomTableReturnId(RainAPIdb, "api_rain"); + // if (NeedCallApi != 0) + // { + // var val = RainValue(observation.Alert.MsgType, observation.Alert.Info[0].Headline); + // if (val < 5) + // { + // var ReStr = Fetch_PostWithJSONFormat($@"{obixApiConfig.ApiBase}obix/config/Arena/D2/CWB/L110/CAP/D2_CWB_L110_CAP_MET1/SeverityLEVL_RAIN/set", val.ToString()); + // UpdatedNiagara("api_rain", ReStr, id); + // } + // } + + // FolderFunction folderFunction = new FolderFunction(); + // folderFunction.DeleteFile("root/PowerfulRain.xml"); + // await task_Detail.InsertWorkTime_End("WeatherAPI", "api_rain"); + //} + //else + //{ + // var observation = RainApi.Welcome.FromJson(json); + // var area = observation.Alert.Info[0].Area.Where(a => a.Geocode.Value == "63").Select(a => a.AreaDesc).FirstOrDefault(); + // var sql = $"select id from api_rain where msgType = '{observation.Alert.MsgType}' and onset = '{observation.Alert.Info[0].Onset.ToString("yyyy-MM-dd HH:mm:ss")}' and expires = '{observation.Alert.Info[0].Expires.ToString("yyyy-MM-dd HH:mm:ss")}'"; + // var NeedCallApi = await backendRepository.GetOneAsync(sql); + + // Dictionary RainAPIdb = new Dictionary() + // { + // { "@msgType", observation.Alert.MsgType}, + // { "@headline", observation.Alert.Info[0].Headline}, + // { "@areaDesc", area}, + // { "@onset", observation.Alert.Info[0].Onset.ToString("yyyy-MM-dd HH:mm:ss")}, + // { "@expires", observation.Alert.Info[0].Expires.ToString("yyyy-MM-dd HH:mm:ss")}, + // { "@created_by", "system"}, + // { "@created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}, + // }; + // var id = await backendRepository.AddOneByCustomTableReturnId(RainAPIdb, "api_rain"); + // if (NeedCallApi != 0) + // { + // var val = RainValue(observation.Alert.MsgType, observation.Alert.Info[0].Headline); + // if (val < 5) + // { + // var ReStr = Fetch_PostWithJSONFormat($@"{obixApiConfig.ApiBase}obix/config/Arena/D2/CWB/L110/CAP/D2_CWB_L110_CAP_MET1/SeverityLEVL_RAIN/set", val.ToString()); + // UpdatedNiagara("api_rain", ReStr, id); + // } + // } + + // FolderFunction folderFunction = new FolderFunction(); + // folderFunction.DeleteFile("root/PowerfulRain.xml"); + // await task_Detail.InsertWorkTime_End("WeatherAPI", "api_rain"); + + //} + } + catch (Exception ex) + { + await task_Detail.WorkFail("WeatherAPI", "api_rain", ex.Message.ToString()); + } + } + + if (await task_Detail.GetNeedWorkTask("WeatherAPI", "api_typhoon")) + { + try + { + await task_Detail.InsertWorkTime("WeatherAPI", "api_typhoon"); + WebClient mywebClient = new WebClient(); + mywebClient.DownloadFile("https://opendata.cwb.gov.tw/fileapi/v1/opendataapi/W-C0034-001?Authorization=CWB-EA24220B-DDCC-4188-84E5-AD37A0E03F80&downloadType=WEB&format=CAP", @"root/Typhoon.xml"); + XmlDocument doc = new XmlDocument(); + doc.Load("root/Typhoon.xml"); + var json = JsonConvert.SerializeXmlNode(doc); + var haveinfo = json.Split("info"); + + //if (haveinfo.Length > 2) + //{ + // var observation = TyphoonApi.Welcome.FromJson(json); + // var area = observation.Alert.Info.Area.Where(a => a.Geocode.Value == "63").Select(a => a.AreaDesc).FirstOrDefault(); + // var sql = $"select id from api_typhoon where msgType = '{observation.Alert.MsgType}' and onset = '{observation.Alert.Info[0].Onset.ToString("yyyy-MM-dd HH:mm:ss")}' and expires = '{observation.Alert.Info[0].Expires.ToString("yyyy-MM-dd HH:mm:ss")}'"; + // var NeedCallApi = await backendRepository.GetOneAsync(sql); + // Dictionary EarthquakeAPIdb = new Dictionary() + // { + // { "@msgType", observation.Alert.MsgType}, + // { "@headline", observation.Alert.Info[0].Headline}, + // { "@areaDesc", area}, + // { "@urgency",observation.Alert.Info[0].Urgency}, + // { "@severity",observation.Alert.Info[0].Severity}, + // { "@onset", observation.Alert.Info[0].Onset}, + // { "@expires", observation.Alert.Info[0].Expires}, + // { "@created_by", "system"}, + // { "@created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}, + // }; + // var id = await backendRepository.AddOneByCustomTableReturnId(EarthquakeAPIdb, "api_typhoon"); + // if (NeedCallApi != 0) + // { + // if (observation.Alert.Info[0].Urgency != null && observation.Alert.Info[0].Urgency != "Expected") + // { + + // var ReStr = Fetch_PostWithJSONFormat($@"{obixApiConfig.ApiBase}obix/config/Arena/D2/CWB/L110/CAP/D2_CWB_L110_CAP_MET2/SeverityLEVL_Typhoon/set", observation.Alert.Info[0].Urgency); + // UpdatedNiagara("api_typhoon", ReStr, id); + // } + // } + // FolderFunction folderFunction = new FolderFunction(); + // folderFunction.DeleteFile("root/Typhoon.xml"); + // await task_Detail.InsertWorkTime_End("WeatherAPI", "api_typhoon"); + //} + //else + //{ + // var observation = TyphoonApi.Welcome.FromJson(json); + // //var area = observation.Alert.Info.Area.Where(a => a.Geocode.Value == "63").Select(a => a.AreaDesc).FirstOrDefault(); + // var sql = $"select id from api_typhoon where msgType = '{observation.Alert.MsgType}' and onset = '{observation.Alert.Info.Onset.ToString("yyyy-MM-dd HH:mm:ss")}' and expires = '{observation.Alert.Info.Expires.ToString("yyyy-MM-dd HH:mm:ss")}'"; + // var NeedCallApi = await backendRepository.GetOneAsync(sql); + // Dictionary EarthquakeAPIdb = new Dictionary() + // { + // { "@msgType", observation.Alert.MsgType}, + // { "@headline", observation.Alert.Info.Headline}, + // //{ "@areaDesc", area}, + // { "@urgency",observation.Alert.Info.Urgency}, + // { "@severity",observation.Alert.Info.Severity}, + // { "@onset", observation.Alert.Info.Onset.ToString("yyyy-MM-dd HH:mm:ss")}, + // { "@expires", observation.Alert.Info.Expires.ToString("yyyy-MM-dd HH:mm:ss")}, + // { "@created_by", "system"}, + // { "@created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}, + // }; + // var id = await backendRepository.AddOneByCustomTableReturnId(EarthquakeAPIdb, "api_typhoon"); + // if (NeedCallApi != 0) + // { + // if (observation.Alert.Info.Urgency != null && observation.Alert.Info.Urgency != "Expected") + // { + + // var ReStr = Fetch_PostWithJSONFormat($@"{obixApiConfig.ApiBase}obix/config/Arena/D2/CWB/L110/CAP/D2_CWB_L110_CAP_MET2/SeverityLEVL_Typhoon/set", observation.Alert.Info.Urgency); + // UpdatedNiagara("api_typhoon", ReStr, id); + // } + // } + // FolderFunction folderFunction = new FolderFunction(); + // folderFunction.DeleteFile("root/Typhoon.xml"); + // await task_Detail.InsertWorkTime_End("WeatherAPI", "api_typhoon"); + + //} + + } + catch (Exception ex) + { + await task_Detail.WorkFail("WeatherAPI", "api_typhoon", ex.Message.ToString()); + } + } + if (await task_Detail.GetNeedWorkTask("WeatherAPI", "api_earthquake")) + { + try + { + await task_Detail.InsertWorkTime("WeatherAPI", "api_earthquake"); + var client = new HttpClient(); + var UVUri = "https://opendata.cwb.gov.tw/api/v1/rest/datastore/E-A0015-001?Authorization=CWB-EA24220B-DDCC-4188-84E5-AD37A0E03F80&format=JSON&areaName=%E8%87%BA%E5%8C%97%E5%B8%82"; + HttpResponseMessage response = client.GetAsync(UVUri).Result; + String jsonUVs = response.Content.ReadAsStringAsync().Result.ToString(); + var observation = QuickType.Welcome.FromJson(jsonUVs); + if (!observation.Success) + { + logger.LogInformation("【WeatherAPIJob】【取得地震觀測資料不正確】"); + } + else + { + logger.LogInformation("【WeatherAPIJob】【開始存入地震觀測到資料庫】"); + List> EarthquakeAPIdbS = new List>(); + var nowNo = await backendRepository.GetOneAsync("select if( Max(earthquakeNo) is null ,0,Max(earthquakeNo)) earthquakeNo from api_earthquake where newEarthquake = 1"); + foreach (var a in observation.Records.Earthquake) + { + if (a.EarthquakeNo <= nowNo) + { + Dictionary EarthquakeAPIdb = new Dictionary() + { + { "@earthquakeNo", a.EarthquakeNo}, + { "@newEarthquake", 0}, + { "@originTime", DateTime.Parse(a.EarthquakeInfo.OriginTime.ToString(), System.Globalization.CultureInfo.CurrentCulture)}, + { "@magnitudeValue", a.EarthquakeInfo.EarthquakeMagnitude.MagnitudeValue}, + { "@areaName", null}, + { "@areaIntensity", null}, + { "@created_by", "system"}, + { "@created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}, + }; + EarthquakeAPIdbS.Add(EarthquakeAPIdb); + break; + } + else + { + if (a.Intensity.ShakingArea.Length > 0) + { + Dictionary EarthquakeAPIdb = new Dictionary() + { + { "@earthquakeNo", a.EarthquakeNo}, + { "@newEarthquake", 1}, + { "@originTime", DateTime.Parse(a.EarthquakeInfo.OriginTime.ToString(), System.Globalization.CultureInfo.CurrentCulture)}, + { "@magnitudeValue", a.EarthquakeInfo.EarthquakeMagnitude.MagnitudeValue}, + { "@areaName", a.Intensity.ShakingArea[0].CountyName}, + { "@areaIntensity", a.Intensity.ShakingArea[0].AreaIntensity}, + { "@created_by", "system"}, + { "@created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}, + }; + + + var Nag = Fetch_PostWithJSONFormat($@"{obixApiConfig.ApiBase}obix/config/Arena/D2/CWB/L110/CAP/D2_CWB_L110_CAP_GEO/SeverityLEVL_LMI/set", a.Intensity.ShakingArea[0].AreaIntensity.ToString()); + + if (Nag.Contains("err")) + { + EarthquakeAPIdb.Add("@niagara", Nag); + } + else + { + EarthquakeAPIdb.Add("@niagara", "success"); + } + + + EarthquakeAPIdbS.Add(EarthquakeAPIdb); + } + else + { + Dictionary EarthquakeAPIdb = new Dictionary() + { + { "@earthquakeNo", a.EarthquakeNo}, + { "@newEarthquake", 1}, + { "@originTime", DateTime.Parse(a.EarthquakeInfo.OriginTime.ToString(), System.Globalization.CultureInfo.CurrentCulture)}, + { "@magnitudeValue", a.EarthquakeInfo.EarthquakeMagnitude.MagnitudeValue}, + { "@areaName", null}, + { "@areaIntensity", null}, + { "@created_by", "system"}, + { "@created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}, + }; + + var Nag = Fetch_PostWithJSONFormat($@"{obixApiConfig.ApiBase}obix/config/Arena/D2/CWB/L110/CAP/D2_CWB_L110_CAP_GEO/SeverityLEVL_LMI/set", "NULL"); + if (Nag.Contains("err")) + { + EarthquakeAPIdb.Add("@niagara", Nag); + } + else + { + EarthquakeAPIdb.Add("@niagara", "success"); + } + EarthquakeAPIdbS.Add(EarthquakeAPIdb); + } + } + } + await backendRepository.AddMutiByCustomTable(EarthquakeAPIdbS, "api_earthquake"); + } + await task_Detail.InsertWorkTime_End("WeatherAPI", "api_earthquake"); + } + catch (Exception ex) + { + await task_Detail.WorkFail("WeatherAPI", "api_earthquake", ex.Message.ToString()); + } + } + if (await task_Detail.GetNeedWorkTask("WeatherAPI", "set_weather")) + { + try + { + await task_Detail.InsertWorkTime("WeatherAPI", "set_weather"); + var sql = @$"SELECT + id, + weather_type, + get_value + FROM api_weateher + where id in (select MAX(id) from api_weateher where start_time < NOW() group by weather_type) + order by start_time desc"; + var types = await backendRepository.GetAllAsync(sql); + var T = types.Where(a => a.weather_type == "T").FirstOrDefault(); + var RbT = Fetch_PostWithJSONFormat($@"{obixApiConfig.ApiBase}obix/config/Arena/D2/CWB/L110/OPD/D2_CWB_L110_OPD_element/T/set", T.get_value); + UpdatedNiagara("api_weateher", RbT, T.id); + + var RH = types.Where(a => a.weather_type == "RH").FirstOrDefault(); + var RHT = Fetch_PostWithJSONFormat($@"{obixApiConfig.ApiBase}obix/config/Arena/D2/CWB/L110/OPD/D2_CWB_L110_OPD_element/RH/set", RH.get_value); + UpdatedNiagara("api_weateher", RHT, RH.id); + + var PoP12h = types.Where(a => a.weather_type == "PoP12h").FirstOrDefault(); + var PoP12hT = Fetch_PostWithJSONFormat($@"{obixApiConfig.ApiBase}obix/config/Arena/D2/CWB/L110/OPD/D2_CWB_L110_OPD_element/PoP6h/set", PoP12h.get_value); + UpdatedNiagara("api_weateher", PoP12hT, PoP12h.id); + + var Wx = types.Where(a => a.weather_type == "Wx").FirstOrDefault(); + var WxT = Fetch_PostWithJSONFormat($@"{obixApiConfig.ApiBase}obix/config/Arena/D2/CWB/L110/OPD/D2_CWB_L110_OPD_element/Wx/set", Wx.get_value); + UpdatedNiagara("api_weateher", WxT, Wx.id); + await task_Detail.InsertWorkTime_End("WeatherAPI", "set_weather"); + } + catch (Exception ex) + { + await task_Detail.WorkFail("WeatherAPI", "set_weather", ex.Message.ToString()); + } + } + } + catch (Exception exception) + { + logger.LogError("【WeatherAPIJob】【任務失敗】"); + logger.LogError("【WeatherAPIJob】【任務失敗】[Exception]:{0}", exception.ToString()); + } + } + + public int RainValue(string Ty, string Headline) + { + var rint = 5; + if (Ty == "") + { + return 0; + + } + else + { + if (Headline.Contains("大雨")) + { + rint = 1; + } + else if (Headline.Contains("豪雨")) + { + rint = 2; + } + else if (Headline.Contains("大豪雨")) + { + rint = 3; + } + else if (Headline.Contains("超大豪雨")) + { + rint = 4; + } + } + return rint; + } + + public async void UpdatedNiagara(string DBTableName, string ResponseStr, int CheckNumId) + { + try + { + var Su = "success"; + + if (ResponseStr.Contains("err")) + { + Su = ResponseStr; + } + Dictionary RainAPIdb = new Dictionary() + { + { "@niagara", Su} + }; + await backendRepository.UpdateOneByCustomTable(RainAPIdb, DBTableName, " id = " + CheckNumId); + } + catch (Exception ex) + { + logger.LogError("【WeatherAPIJob】" + "UpdatedNiagara:" + ex.ToString()); + throw ex; + } + } + } +} diff --git a/Backend/Quartz/QuartzHostedService.cs b/Backend/Quartz/QuartzHostedService.cs new file mode 100644 index 0000000..b111409 --- /dev/null +++ b/Backend/Quartz/QuartzHostedService.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.Hosting; +using Quartz; +using Quartz.Spi; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Backend.Quartz +{ + public class QuartzHostedService : IHostedService + { + private readonly ISchedulerFactory _schedulerFactory; + private readonly IJobFactory _jobFactory; + private readonly IEnumerable _jobSchedules; + public QuartzHostedService(ISchedulerFactory schedulerFactory, IJobFactory jobFactory, IEnumerable jobSchedules) + { + _schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory)); + _jobFactory = jobFactory ?? throw new ArgumentNullException(nameof(jobFactory)); + _jobSchedules = jobSchedules ?? throw new ArgumentNullException(nameof(jobSchedules)); + } + public IScheduler Scheduler { get; set; } + public async Task StartAsync(CancellationToken cancellationToken) + { + Scheduler = await _schedulerFactory.GetScheduler(cancellationToken); + Scheduler.JobFactory = _jobFactory; + foreach (var jobSchedule in _jobSchedules) + { + var job = CreateJob(jobSchedule); + var trigger = CreateTrigger(jobSchedule); + await Scheduler.ScheduleJob(job, trigger, cancellationToken); + jobSchedule.JobStatu = JobStatus.Scheduling; + } + await Scheduler.Start(cancellationToken); + foreach (var jobSchedule in _jobSchedules) + { + jobSchedule.JobStatu = JobStatus.Running; + } + } + public async Task StopAsync(CancellationToken cancellationToken) + { + await Scheduler?.Shutdown(cancellationToken); + foreach (var jobSchedule in _jobSchedules) + { + + jobSchedule.JobStatu = JobStatus.Stopped; + } + } + private static IJobDetail CreateJob(JobSchedule schedule) + { + var jobType = schedule.JobType; + return JobBuilder + .Create(jobType) + .WithIdentity(jobType.FullName) + .WithDescription(jobType.Name) + .Build(); + } + private static ITrigger CreateTrigger(JobSchedule schedule) + { + return TriggerBuilder + .Create() + .WithIdentity($"{schedule.JobType.FullName}.trigger") + .WithCronSchedule(schedule.CronExpression) + .WithDescription(schedule.CronExpression) + .Build(); + } + } +} diff --git a/Backend/Quartz/SingletonJobFactory.cs b/Backend/Quartz/SingletonJobFactory.cs new file mode 100644 index 0000000..c925edd --- /dev/null +++ b/Backend/Quartz/SingletonJobFactory.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.DependencyInjection; +using Quartz; +using Quartz.Spi; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Backend.Quartz +{ + public class SingletonJobFactory : IJobFactory + { + private readonly IServiceProvider _serviceProvider; + public SingletonJobFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + } + public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) + { + return _serviceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob; + } + public void ReturnJob(IJob job) + { + + } + } +} diff --git a/Backend/Services/Implement/ProcEletricMeterService.cs b/Backend/Services/Implement/ProcEletricMeterService.cs new file mode 100644 index 0000000..8478c30 --- /dev/null +++ b/Backend/Services/Implement/ProcEletricMeterService.cs @@ -0,0 +1,935 @@ +using Backend.Models; +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Data.SqlClient; +using System.Text; +using Dapper; +using System.Linq; +using Newtonsoft.Json.Linq; +using System.Xml; +using System.Net; +using System.IO; +using Newtonsoft.Json; +using Repository.Helper; +using Repository.BackendRepository.Interface; +using Repository.BackendRepository.Implement; +using System.Threading.Tasks; + +namespace BackendWorkerService.Services.Implement +{ + /// + /// 電錶補償歸檔 + /// + public class ProcEletricMeterService + { + private readonly IBackgroundServiceRepository backgroundServiceRepository; + private readonly IBackgroundServiceMsSqlRepository backgroundServiceMsSqlRepository; + + public ProcEletricMeterService(IBackgroundServiceRepository backgroundServiceRepository, + IBackgroundServiceMsSqlRepository backgroundServiceMySqlRepository) + { + this.backgroundServiceRepository = backgroundServiceRepository; + this.backgroundServiceMsSqlRepository = backgroundServiceMySqlRepository; + } + + public async Task ArchiveData() + { + bool result = false; + int repeatTimes = 0; + string targetTable = string.Empty; + + EDFunction ed = new EDFunction(); + XmlDocument xmlDocument = new XmlDocument(); + var obixApiConfig = new ObixApiConfig(); + string encoded = string.Empty; + + try + { + //取得可錯誤次數 + var sqlArchive = $@"SELECT system_value as Value, system_key as Name FROM variable WHERE deleted = 0 AND system_type = 'archiveConfig'"; + + var variableArchive = await backgroundServiceRepository.GetAllAsync(sqlArchive); + repeatTimes = Convert.ToInt32(variableArchive.Where(x => x.Name == "RepeatTimes").Select(x => x.Value).FirstOrDefault()); + var saveToMSDB = await backgroundServiceRepository.GetOneAsync("select system_value from variable where system_type = 'save_to_ms_db' and deleted = 0"); + + #region 取得obix 設定 + var sqlObix = $@"SELECT system_value as Value, system_key as Name FROM variable WHERE deleted = 0 AND system_type = 'obixConfig'"; + + var variableObix = await backgroundServiceRepository.GetAllAsync(sqlObix); + obixApiConfig.ApiBase = variableObix.Where(x => x.Name == "ApiBase").Select(x => x.Value).FirstOrDefault(); + obixApiConfig.UserName = variableObix.Where(x => x.Name == "UserName").Select(x => x.Value).FirstOrDefault(); + obixApiConfig.Password = variableObix.Where(x => x.Name == "Password").Select(x => x.Value).FirstOrDefault(); + + encoded = System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(obixApiConfig.UserName + ":" + obixApiConfig.Password)); + #endregion 取得obix 設定 + + //取得錯誤的設備sql format + var sql_error_format = @"SELECT * FROM {0} WHERE is_complete = 0 AND repeat_times < @RepeatTimes"; + + //MY 新增/修改sql format + var MYsql_update_format = @" + UPDATE {0} SET + count_rawdata = @count_rawdata, + min_rawdata = @min_rawdata, + max_rawdata = @max_rawdata, + avg_rawdata = @avg_rawdata, + sum_rawdata = @sum_rawdata, + is_complete = @is_complete, + repeat_times = @repeat_times, + fail_reason = @fail_reason, + updated_at = @updated_at + WHERE device_number = @device_number + AND point = @point + AND start_timestamp = @start_timestamp; + + INSERT INTO {0} ( + device_number, + point, + start_timestamp, + end_timestamp, + count_rawdata, + min_rawdata, + max_rawdata, + avg_rawdata, + sum_rawdata, + is_complete, + repeat_times, + fail_reason) + SELECT + @device_number, + @point, + @start_timestamp, + @end_timestamp, + @count_rawdata, + @min_rawdata, + @max_rawdata, + @avg_rawdata, + @sum_rawdata, + @is_complete, + @repeat_times, + @fail_reason + WHERE ROW_COUNT() = 0;"; + //新增/修改sql format + var sql_update_format = @"BEGIN TRANSACTION; + + UPDATE {0} SET + count_rawdata = @count_rawdata, + min_rawdata = @min_rawdata, + max_rawdata = @max_rawdata, + avg_rawdata = @avg_rawdata, + sum_rawdata = @sum_rawdata, + is_complete = @is_complete, + repeat_times = @repeat_times, + fail_reason = @fail_reason, + updated_at = @updated_at + WHERE device_number = @device_number + AND point = @point + AND start_timestamp = @start_timestamp; + + IF @@ROWCOUNT = 0 + BEGIN + INSERT INTO {0} ( + device_number, + point, + start_timestamp, + end_timestamp, + count_rawdata, + min_rawdata, + max_rawdata, + avg_rawdata, + sum_rawdata, + is_complete, + repeat_times, + fail_reason) + VALUES ( + @device_number, + @point, + @start_timestamp, + @end_timestamp, + @count_rawdata, + @min_rawdata, + @max_rawdata, + @avg_rawdata, + @sum_rawdata, + @is_complete, + @repeat_times, + @fail_reason) + END + + COMMIT TRANSACTION;"; + + #region 時歸檔補償 + //using (IDbConnection conn = new SqlConnection(Connection1)) + //{ + // //取得所有須補償的設備資訊 + // targetTable = "archive_electric_meter_hour"; + // var sql_error_hour = string.Format(sql_error_format, targetTable); + // var error_hours = conn.Query(sql_error_hour, new { RepeatTimes = repeatTimes }).ToList(); + + // List> archiveHourRawDatas = new List>(); + // if (error_hours.Count() > 0) + // { + // foreach (var error_hour in error_hours) + // { + // DeviceNumberPoint deviceNumberPoint = new DeviceNumberPoint(); + // deviceNumberPoint.DeviceNumber = error_hour.Device_number; + // deviceNumberPoint.Point = error_hour.Point; + // deviceNumberPoint.FullDeviceNumberPoint = string.Format("{0}_{1}", error_hour.Device_number, error_hour.Point); + + // var startTimestamp = string.Format("{0}+08:00", error_hour.Start_timestamp.Replace(" ", "T")); + // var endTimestamp = string.Format("{0}+08:00", error_hour.End_timestamp.Replace(" ", "T")); + + // var historyQueryFilter = $@" + // + // + // + // "; + + // HttpWebRequest archiveHourRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/{deviceNumberPoint.FullDeviceNumberPoint}/~historyRollup/"); + // //HttpWebRequest archiveHourRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/H_E1_B1F_MVCB_MVCBH_V1/~historyRollup/"); + // archiveHourRequest.Method = "POST"; + // archiveHourRequest.Headers.Add("Authorization", "Basic " + encoded); + // archiveHourRequest.PreAuthenticate = true; + + // byte[] byteArray = Encoding.UTF8.GetBytes(historyQueryFilter); + // using (Stream reqStream = archiveHourRequest.GetRequestStream()) + // { + // reqStream.Write(byteArray, 0, byteArray.Length); + // } + + // HttpWebResponse archiveHourResponse = (HttpWebResponse)archiveHourRequest.GetResponse(); + // var archiveHourResponseContent = new StreamReader(archiveHourResponse.GetResponseStream()).ReadToEnd(); + + // xmlDocument.LoadXml(archiveHourResponseContent); + // string archiveHourJson = JsonConvert.SerializeXmlNode(xmlDocument); + // JObject archiveHourJsonResult = (JObject)JsonConvert.DeserializeObject(archiveHourJson); + + // if (archiveHourJsonResult.ContainsKey("err")) //抓取錯誤 + // { + // Dictionary archiveDayRawData = new Dictionary(); + // archiveDayRawData.Add("@device_number", error_hour.Device_number); + // archiveDayRawData.Add("@point", error_hour.Point); + // archiveDayRawData.Add("@start_timestamp", error_hour.Start_timestamp); + // archiveDayRawData.Add("@end_timestamp", error_hour.End_timestamp); + // archiveDayRawData.Add("@is_complete", 0); + // archiveDayRawData.Add("@repeat_times", ++error_hour.Repeat_times); + // archiveDayRawData.Add("@fail_reason", archiveHourJson); + + // archiveDayRawData.Add("@count_rawdata", 0); + // archiveDayRawData.Add("@min_rawdata", 0); + // archiveDayRawData.Add("@max_rawdata", 0); + // archiveDayRawData.Add("@avg_rawdata", 0); + // archiveDayRawData.Add("@sum_rawdata", 0); + // archiveDayRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + // archiveHourRawDatas.Add(archiveDayRawData); + // } + + // if (archiveHourJsonResult.ContainsKey("obj")) //表示可以讀取到內容 + // { + // var ArrangeRawDatas = ArrangeRawData(deviceNumberPoint, archiveHourJsonResult); + // if (ArrangeRawDatas != null && ArrangeRawDatas.Count() > 0) + // { + // archiveHourRawDatas.AddRange(ArrangeRawDatas); + // } + // } + // } + + // if (archiveHourRawDatas.Count() > 0) + // { + // var sql_error_update = string.Format(sql_update_format, targetTable); + // conn.Execute(sql_error_update, archiveHourRawDatas); + // } + // } + // conn.Close(); + //} + #endregion 時歸檔補償 + + #region 天歸檔補償 + //取得所有須補償的設備資訊 + targetTable = "archive_electric_meter_day"; + var sql_error_day = string.Format(sql_error_format, targetTable); + var electric_error_days = await backgroundServiceRepository.GetAllAsync(sql_error_day, new { RepeatTimes = repeatTimes }); + List> electricArchiveDayRawDatas = new List>(); + if (electric_error_days.Count() > 0) + { + foreach (var error_day in electric_error_days) + { + DeviceNumberPoint deviceNumberPoint = new DeviceNumberPoint(); + deviceNumberPoint.DeviceNumber = error_day.Device_number; + deviceNumberPoint.Point = error_day.Point; + deviceNumberPoint.FullDeviceNumberPoint = string.Format("{0}_{1}", error_day.Device_number, error_day.Point); + + var startTimestamp = string.Format("{0}+08:00", error_day.Start_timestamp.Replace(" ", "T")); + var endTimestamp = string.Format("{0}+08:00", error_day.End_timestamp.Replace(" ", "T")); + + var historyQueryFilter = $@" + + + + "; + + HttpWebRequest archiveDayRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/{deviceNumberPoint.FullDeviceNumberPoint}/~historyRollup/"); + //HttpWebRequest archiveDayRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/H_E1_B1F_MVCB_MVCBH_V1/~historyRollup/"); + archiveDayRequest.Method = "POST"; + archiveDayRequest.Headers.Add("Authorization", "Basic " + encoded); + archiveDayRequest.PreAuthenticate = true; + + byte[] byteArray = Encoding.UTF8.GetBytes(historyQueryFilter); + using (Stream reqStream = archiveDayRequest.GetRequestStream()) + { + reqStream.Write(byteArray, 0, byteArray.Length); + } + + HttpWebResponse archiveDayResponse = (HttpWebResponse)archiveDayRequest.GetResponse(); + var archiveDayResponseContent = new StreamReader(archiveDayResponse.GetResponseStream()).ReadToEnd(); + + xmlDocument.LoadXml(archiveDayResponseContent); + string archiveDayJson = JsonConvert.SerializeXmlNode(xmlDocument); + JObject archiveDayJsonResult = (JObject)JsonConvert.DeserializeObject(archiveDayJson); + + if (archiveDayJsonResult.ContainsKey("err")) //抓取錯誤 + { + Dictionary archiveDayRawData = new Dictionary(); + archiveDayRawData.Add("@device_number", error_day.Device_number); + archiveDayRawData.Add("@point", error_day.Point); + archiveDayRawData.Add("@start_timestamp", DateTime.Parse(error_day.Start_timestamp, System.Globalization.CultureInfo.CurrentCulture)); + archiveDayRawData.Add("@end_timestamp", DateTime.Parse(error_day.End_timestamp, System.Globalization.CultureInfo.CurrentCulture)); + archiveDayRawData.Add("@is_complete", 0); + archiveDayRawData.Add("@repeat_times", ++error_day.Repeat_times); + archiveDayRawData.Add("@fail_reason", archiveDayJson); + + archiveDayRawData.Add("@count_rawdata", 0); + archiveDayRawData.Add("@min_rawdata", 0); + archiveDayRawData.Add("@max_rawdata", 0); + archiveDayRawData.Add("@avg_rawdata", 0); + archiveDayRawData.Add("@sum_rawdata", 0); + archiveDayRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + electricArchiveDayRawDatas.Add(archiveDayRawData); + } + + if (archiveDayJsonResult.ContainsKey("obj")) //表示可以讀取到內容 + { + var ArrangeRawDatas = ArrangeRawData(deviceNumberPoint, archiveDayJsonResult); + if (ArrangeRawDatas != null && ArrangeRawDatas.Count() > 0) + { + electricArchiveDayRawDatas.AddRange(ArrangeRawDatas); + } + } + } + + if (electricArchiveDayRawDatas.Count() > 0) + { + var Mysql_error_update = string.Format(MYsql_update_format, targetTable); + var sql_error_update = string.Format(sql_update_format, targetTable); + if (!string.IsNullOrEmpty(saveToMSDB) && saveToMSDB == "1") + { + await backgroundServiceMsSqlRepository.ExecuteSql(sql_error_update, electricArchiveDayRawDatas); + } + await backgroundServiceRepository.ExecuteSql(Mysql_error_update, electricArchiveDayRawDatas); + } + } + + targetTable = "archive_water_meter_day"; + sql_error_day = string.Format(sql_error_format, targetTable); + var water_error_days = await backgroundServiceRepository.GetAllAsync(sql_error_day, new { RepeatTimes = repeatTimes }); + List> waterArchiveDayRawDatas = new List>(); + if (water_error_days.Count() > 0) + { + foreach (var error_day in water_error_days) + { + DeviceNumberPoint deviceNumberPoint = new DeviceNumberPoint(); + deviceNumberPoint.DeviceNumber = error_day.Device_number; + deviceNumberPoint.Point = error_day.Point; + deviceNumberPoint.FullDeviceNumberPoint = string.Format("{0}_{1}", error_day.Device_number, error_day.Point); + + var startTimestamp = string.Format("{0}+08:00", error_day.Start_timestamp.Replace(" ", "T")); + var endTimestamp = string.Format("{0}+08:00", error_day.End_timestamp.Replace(" ", "T")); + + var historyQueryFilter = $@" + + + + "; + + HttpWebRequest archiveDayRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/{deviceNumberPoint.FullDeviceNumberPoint}/~historyRollup/"); + //HttpWebRequest archiveDayRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/H_E1_B1F_MVCB_MVCBH_V1/~historyRollup/"); + archiveDayRequest.Method = "POST"; + archiveDayRequest.Headers.Add("Authorization", "Basic " + encoded); + archiveDayRequest.PreAuthenticate = true; + + byte[] byteArray = Encoding.UTF8.GetBytes(historyQueryFilter); + using (Stream reqStream = archiveDayRequest.GetRequestStream()) + { + reqStream.Write(byteArray, 0, byteArray.Length); + } + + HttpWebResponse archiveDayResponse = (HttpWebResponse)archiveDayRequest.GetResponse(); + var archiveDayResponseContent = new StreamReader(archiveDayResponse.GetResponseStream()).ReadToEnd(); + + xmlDocument.LoadXml(archiveDayResponseContent); + string archiveDayJson = JsonConvert.SerializeXmlNode(xmlDocument); + JObject archiveDayJsonResult = (JObject)JsonConvert.DeserializeObject(archiveDayJson); + + if (archiveDayJsonResult.ContainsKey("err")) //抓取錯誤 + { + Dictionary archiveDayRawData = new Dictionary(); + archiveDayRawData.Add("@device_number", error_day.Device_number); + archiveDayRawData.Add("@point", error_day.Point); + archiveDayRawData.Add("@start_timestamp", DateTime.Parse(error_day.Start_timestamp, System.Globalization.CultureInfo.CurrentCulture)); + archiveDayRawData.Add("@end_timestamp", DateTime.Parse(error_day.End_timestamp, System.Globalization.CultureInfo.CurrentCulture)); + archiveDayRawData.Add("@is_complete", 0); + archiveDayRawData.Add("@repeat_times", ++error_day.Repeat_times); + archiveDayRawData.Add("@fail_reason", archiveDayJson); + + archiveDayRawData.Add("@count_rawdata", 0); + archiveDayRawData.Add("@min_rawdata", 0); + archiveDayRawData.Add("@max_rawdata", 0); + archiveDayRawData.Add("@avg_rawdata", 0); + archiveDayRawData.Add("@sum_rawdata", 0); + archiveDayRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + waterArchiveDayRawDatas.Add(archiveDayRawData); + } + + if (archiveDayJsonResult.ContainsKey("obj")) //表示可以讀取到內容 + { + var ArrangeRawDatas = ArrangeRawData(deviceNumberPoint, archiveDayJsonResult); + if (ArrangeRawDatas != null && ArrangeRawDatas.Count() > 0) + { + waterArchiveDayRawDatas.AddRange(ArrangeRawDatas); + } + } + } + + if (waterArchiveDayRawDatas.Count() > 0) + { + var Mysql_error_update = string.Format(MYsql_update_format, targetTable); + var sql_error_update = string.Format(sql_update_format, targetTable); + if (!string.IsNullOrEmpty(saveToMSDB) && saveToMSDB == "1") + { + await backgroundServiceMsSqlRepository.ExecuteSql(sql_error_update, waterArchiveDayRawDatas); + } + await backgroundServiceRepository.ExecuteSql(Mysql_error_update, waterArchiveDayRawDatas); + } + } + #endregion 天歸檔補償 + + #region 週歸檔補償 + //取得所有須補償的設備資訊 + targetTable = "archive_electric_meter_week"; + var sql_error_week = string.Format(sql_error_format, targetTable); + var eletric_error_weeks = await backgroundServiceRepository.GetAllAsync(sql_error_week, new { RepeatTimes = repeatTimes }); + List> electricArchiveWeekRawDatas = new List>(); + if (eletric_error_weeks.Count() > 0) + { + foreach (var error_week in eletric_error_weeks) + { + DeviceNumberPoint deviceNumberPoint = new DeviceNumberPoint(); + deviceNumberPoint.DeviceNumber = error_week.Device_number; + deviceNumberPoint.Point = error_week.Point; + deviceNumberPoint.FullDeviceNumberPoint = string.Format("{0}_{1}", error_week.Device_number, error_week.Point); + + var startTimestamp = string.Format("{0}+08:00", error_week.Start_timestamp.Replace(" ", "T")); + var endTimestamp = string.Format("{0}+08:00", error_week.End_timestamp.Replace(" ", "T")); + + var historyQueryFilter = $@" + + + + "; + + HttpWebRequest archiveWeekRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/{deviceNumberPoint.FullDeviceNumberPoint}/~historyRollup/"); + //HttpWebRequest archiveWeekRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/H_E1_B1F_MVCB_MVCBH_V1/~historyRollup/"); + archiveWeekRequest.Method = "POST"; + archiveWeekRequest.Headers.Add("Authorization", "Basic " + encoded); + archiveWeekRequest.PreAuthenticate = true; + + byte[] byteArray = Encoding.UTF8.GetBytes(historyQueryFilter); + using (Stream reqStream = archiveWeekRequest.GetRequestStream()) + { + reqStream.Write(byteArray, 0, byteArray.Length); + } + + HttpWebResponse archiveWeekResponse = (HttpWebResponse)archiveWeekRequest.GetResponse(); + var archiveWeekResponseContent = new StreamReader(archiveWeekResponse.GetResponseStream()).ReadToEnd(); + + xmlDocument.LoadXml(archiveWeekResponseContent); + string archiveWeekJson = JsonConvert.SerializeXmlNode(xmlDocument); + JObject archiveWeekJsonResult = (JObject)JsonConvert.DeserializeObject(archiveWeekJson); + + if (archiveWeekJsonResult.ContainsKey("err")) //抓取錯誤 + { + Dictionary archiveWeekRawData = new Dictionary(); + archiveWeekRawData.Add("@device_number", error_week.Device_number); + archiveWeekRawData.Add("@point", error_week.Point); + archiveWeekRawData.Add("@start_timestamp", DateTime.Parse(error_week.Start_timestamp, System.Globalization.CultureInfo.CurrentCulture)); + archiveWeekRawData.Add("@end_timestamp", DateTime.Parse(error_week.End_timestamp, System.Globalization.CultureInfo.CurrentCulture)); + archiveWeekRawData.Add("@is_complete", 0); + archiveWeekRawData.Add("@repeat_times", ++error_week.Repeat_times); + archiveWeekRawData.Add("@fail_reason", archiveWeekJson); + + archiveWeekRawData.Add("@count_rawdata", 0); + archiveWeekRawData.Add("@min_rawdata", 0); + archiveWeekRawData.Add("@max_rawdata", 0); + archiveWeekRawData.Add("@avg_rawdata", 0); + archiveWeekRawData.Add("@sum_rawdata", 0); + archiveWeekRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + electricArchiveWeekRawDatas.Add(archiveWeekRawData); + } + + if (archiveWeekJsonResult.ContainsKey("obj")) //表示可以讀取到內容 + { + var ArrangeRawDatas = ArrangeRawData(deviceNumberPoint, archiveWeekJsonResult); + if (ArrangeRawDatas != null && ArrangeRawDatas.Count() > 0) + { + electricArchiveWeekRawDatas.AddRange(ArrangeRawDatas); + } + } + } + + if (electricArchiveWeekRawDatas.Count() > 0) + { + var Mysql_error_update = string.Format(MYsql_update_format, targetTable); + var sql_error_update = string.Format(sql_update_format, targetTable); + if (!string.IsNullOrEmpty(saveToMSDB) && saveToMSDB == "1") + { + await backgroundServiceMsSqlRepository.ExecuteSql(sql_error_update, electricArchiveWeekRawDatas); + } + await backgroundServiceRepository.ExecuteSql(Mysql_error_update, electricArchiveWeekRawDatas); + } + } + + targetTable = "archive_water_meter_week"; + sql_error_week = string.Format(sql_error_format, targetTable); + var water_error_weeks = await backgroundServiceRepository.GetAllAsync(sql_error_week, new { RepeatTimes = repeatTimes }); + List> waterArchiveWeekRawDatas = new List>(); + if (water_error_weeks.Count() > 0) + { + foreach (var error_week in water_error_weeks) + { + DeviceNumberPoint deviceNumberPoint = new DeviceNumberPoint(); + deviceNumberPoint.DeviceNumber = error_week.Device_number; + deviceNumberPoint.Point = error_week.Point; + deviceNumberPoint.FullDeviceNumberPoint = string.Format("{0}_{1}", error_week.Device_number, error_week.Point); + + var startTimestamp = string.Format("{0}+08:00", error_week.Start_timestamp.Replace(" ", "T")); + var endTimestamp = string.Format("{0}+08:00", error_week.End_timestamp.Replace(" ", "T")); + + var historyQueryFilter = $@" + + + + "; + + HttpWebRequest archiveWeekRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/{deviceNumberPoint.FullDeviceNumberPoint}/~historyRollup/"); + //HttpWebRequest archiveWeekRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/H_E1_B1F_MVCB_MVCBH_V1/~historyRollup/"); + archiveWeekRequest.Method = "POST"; + archiveWeekRequest.Headers.Add("Authorization", "Basic " + encoded); + archiveWeekRequest.PreAuthenticate = true; + + byte[] byteArray = Encoding.UTF8.GetBytes(historyQueryFilter); + using (Stream reqStream = archiveWeekRequest.GetRequestStream()) + { + reqStream.Write(byteArray, 0, byteArray.Length); + } + + HttpWebResponse archiveWeekResponse = (HttpWebResponse)archiveWeekRequest.GetResponse(); + var archiveWeekResponseContent = new StreamReader(archiveWeekResponse.GetResponseStream()).ReadToEnd(); + + xmlDocument.LoadXml(archiveWeekResponseContent); + string archiveWeekJson = JsonConvert.SerializeXmlNode(xmlDocument); + JObject archiveWeekJsonResult = (JObject)JsonConvert.DeserializeObject(archiveWeekJson); + + if (archiveWeekJsonResult.ContainsKey("err")) //抓取錯誤 + { + Dictionary archiveWeekRawData = new Dictionary(); + archiveWeekRawData.Add("@device_number", error_week.Device_number); + archiveWeekRawData.Add("@point", error_week.Point); + archiveWeekRawData.Add("@start_timestamp", DateTime.Parse(error_week.Start_timestamp, System.Globalization.CultureInfo.CurrentCulture)); + archiveWeekRawData.Add("@end_timestamp", DateTime.Parse(error_week.End_timestamp, System.Globalization.CultureInfo.CurrentCulture)); + archiveWeekRawData.Add("@is_complete", 0); + archiveWeekRawData.Add("@repeat_times", ++error_week.Repeat_times); + archiveWeekRawData.Add("@fail_reason", archiveWeekJson); + + archiveWeekRawData.Add("@count_rawdata", 0); + archiveWeekRawData.Add("@min_rawdata", 0); + archiveWeekRawData.Add("@max_rawdata", 0); + archiveWeekRawData.Add("@avg_rawdata", 0); + archiveWeekRawData.Add("@sum_rawdata", 0); + archiveWeekRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + waterArchiveWeekRawDatas.Add(archiveWeekRawData); + } + + if (archiveWeekJsonResult.ContainsKey("obj")) //表示可以讀取到內容 + { + var ArrangeRawDatas = ArrangeRawData(deviceNumberPoint, archiveWeekJsonResult); + if (ArrangeRawDatas != null && ArrangeRawDatas.Count() > 0) + { + waterArchiveWeekRawDatas.AddRange(ArrangeRawDatas); + } + } + } + + if (waterArchiveWeekRawDatas.Count() > 0) + { + var Mysql_error_update = string.Format(MYsql_update_format, targetTable); + var sql_error_update = string.Format(sql_update_format, targetTable); + if (!string.IsNullOrEmpty(saveToMSDB) && saveToMSDB == "1") + { + await backgroundServiceMsSqlRepository.ExecuteSql(sql_error_update, waterArchiveWeekRawDatas); + } + await backgroundServiceRepository.ExecuteSql(Mysql_error_update, waterArchiveWeekRawDatas); + } + } + #endregion 週歸檔補償 + + #region 月歸檔補償 + //取得所有須補償的設備資訊 + targetTable = "archive_electric_meter_month"; + var sql_error_month = string.Format(sql_error_format, targetTable); + var electric_error_months = await backgroundServiceRepository.GetAllAsync(sql_error_month, new { RepeatTimes = repeatTimes }); + List> electricArchiveMonthRawDatas = new List>(); + if (electric_error_months.Count() > 0) + { + foreach (var error_month in electric_error_months) + { + DeviceNumberPoint deviceNumberPoint = new DeviceNumberPoint(); + deviceNumberPoint.DeviceNumber = error_month.Device_number; + deviceNumberPoint.Point = error_month.Point; + deviceNumberPoint.FullDeviceNumberPoint = string.Format("{0}_{1}", error_month.Device_number, error_month.Point); + + var startTimestamp = string.Format("{0}+08:00", error_month.Start_timestamp.Replace(" ", "T")); + var endTimestamp = string.Format("{0}+08:00", error_month.End_timestamp.Replace(" ", "T")); + + var startDateTime = Convert.ToDateTime(error_month.Start_timestamp); + var dayInMonth = DateTime.DaysInMonth(startDateTime.Year, startDateTime.Month); + + var historyQueryFilter = $@" + + + + "; + + HttpWebRequest archiveMonthRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/{deviceNumberPoint.FullDeviceNumberPoint}/~historyRollup/"); + //HttpWebRequest archiveMonthRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/H_E1_B1F_MVCB_MVCBH_V1/~historyRollup/"); + archiveMonthRequest.Method = "POST"; + archiveMonthRequest.Headers.Add("Authorization", "Basic " + encoded); + archiveMonthRequest.PreAuthenticate = true; + + byte[] byteArray = Encoding.UTF8.GetBytes(historyQueryFilter); + using (Stream reqStream = archiveMonthRequest.GetRequestStream()) + { + reqStream.Write(byteArray, 0, byteArray.Length); + } + + HttpWebResponse archiveMonthResponse = (HttpWebResponse)archiveMonthRequest.GetResponse(); + var archiveMonthResponseContent = new StreamReader(archiveMonthResponse.GetResponseStream()).ReadToEnd(); + + xmlDocument.LoadXml(archiveMonthResponseContent); + string archiveMonthJson = JsonConvert.SerializeXmlNode(xmlDocument); + JObject archiveMonthJsonResult = (JObject)JsonConvert.DeserializeObject(archiveMonthJson); + + if (archiveMonthJsonResult.ContainsKey("err")) //抓取錯誤 + { + Dictionary archiveMonthRawData = new Dictionary(); + archiveMonthRawData.Add("@device_number", error_month.Device_number); + archiveMonthRawData.Add("@point", error_month.Point); + archiveMonthRawData.Add("@start_timestamp", DateTime.Parse(error_month.Start_timestamp, System.Globalization.CultureInfo.CurrentCulture)); + archiveMonthRawData.Add("@end_timestamp", DateTime.Parse(error_month.End_timestamp, System.Globalization.CultureInfo.CurrentCulture)); + archiveMonthRawData.Add("@is_complete", 0); + archiveMonthRawData.Add("@repeat_times", ++error_month.Repeat_times); + archiveMonthRawData.Add("@fail_reason", archiveMonthJson); + + archiveMonthRawData.Add("@count_rawdata", 0); + archiveMonthRawData.Add("@min_rawdata", 0); + archiveMonthRawData.Add("@max_rawdata", 0); + archiveMonthRawData.Add("@avg_rawdata", 0); + archiveMonthRawData.Add("@sum_rawdata", 0); + archiveMonthRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + electricArchiveMonthRawDatas.Add(archiveMonthRawData); + } + + if (archiveMonthJsonResult.ContainsKey("obj")) //表示可以讀取到內容 + { + var ArrangeRawDatas = ArrangeRawData(deviceNumberPoint, archiveMonthJsonResult); + if (ArrangeRawDatas != null && ArrangeRawDatas.Count() > 0) + { + electricArchiveMonthRawDatas.AddRange(ArrangeRawDatas); + } + } + } + + if (electricArchiveMonthRawDatas.Count() > 0) + { + var Mysql_error_update = string.Format(MYsql_update_format, targetTable); + var sql_error_update = string.Format(sql_update_format, targetTable); + if (!string.IsNullOrEmpty(saveToMSDB) && saveToMSDB == "1") + { + await backgroundServiceMsSqlRepository.ExecuteSql(sql_error_update, electricArchiveMonthRawDatas); + } + await backgroundServiceRepository.ExecuteSql(MYsql_update_format, electricArchiveMonthRawDatas); + } + } + + targetTable = "archive_electric_meter_month"; + sql_error_month = string.Format(sql_error_format, targetTable); + var water_error_months = await backgroundServiceRepository.GetAllAsync(sql_error_month, new { RepeatTimes = repeatTimes }); + List> waterArchiveMonthRawDatas = new List>(); + if (water_error_months.Count() > 0) + { + foreach (var error_month in water_error_months) + { + DeviceNumberPoint deviceNumberPoint = new DeviceNumberPoint(); + deviceNumberPoint.DeviceNumber = error_month.Device_number; + deviceNumberPoint.Point = error_month.Point; + deviceNumberPoint.FullDeviceNumberPoint = string.Format("{0}_{1}", error_month.Device_number, error_month.Point); + + var startTimestamp = string.Format("{0}+08:00", error_month.Start_timestamp.Replace(" ", "T")); + var endTimestamp = string.Format("{0}+08:00", error_month.End_timestamp.Replace(" ", "T")); + + var startDateTime = Convert.ToDateTime(error_month.Start_timestamp); + var dayInMonth = DateTime.DaysInMonth(startDateTime.Year, startDateTime.Month); + + var historyQueryFilter = $@" + + + + "; + + HttpWebRequest archiveMonthRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/{deviceNumberPoint.FullDeviceNumberPoint}/~historyRollup/"); + //HttpWebRequest archiveMonthRequest = (HttpWebRequest)WebRequest.Create($"{obixApiConfig.ApiBase}obix/histories/FIC_Center/H_E1_B1F_MVCB_MVCBH_V1/~historyRollup/"); + archiveMonthRequest.Method = "POST"; + archiveMonthRequest.Headers.Add("Authorization", "Basic " + encoded); + archiveMonthRequest.PreAuthenticate = true; + + byte[] byteArray = Encoding.UTF8.GetBytes(historyQueryFilter); + using (Stream reqStream = archiveMonthRequest.GetRequestStream()) + { + reqStream.Write(byteArray, 0, byteArray.Length); + } + + HttpWebResponse archiveMonthResponse = (HttpWebResponse)archiveMonthRequest.GetResponse(); + var archiveMonthResponseContent = new StreamReader(archiveMonthResponse.GetResponseStream()).ReadToEnd(); + + xmlDocument.LoadXml(archiveMonthResponseContent); + string archiveMonthJson = JsonConvert.SerializeXmlNode(xmlDocument); + JObject archiveMonthJsonResult = (JObject)JsonConvert.DeserializeObject(archiveMonthJson); + + if (archiveMonthJsonResult.ContainsKey("err")) //抓取錯誤 + { + Dictionary archiveMonthRawData = new Dictionary(); + archiveMonthRawData.Add("@device_number", error_month.Device_number); + archiveMonthRawData.Add("@point", error_month.Point); + archiveMonthRawData.Add("@start_timestamp", DateTime.Parse(error_month.Start_timestamp, System.Globalization.CultureInfo.CurrentCulture)); + archiveMonthRawData.Add("@end_timestamp", DateTime.Parse(error_month.End_timestamp, System.Globalization.CultureInfo.CurrentCulture)); + archiveMonthRawData.Add("@is_complete", 0); + archiveMonthRawData.Add("@repeat_times", ++error_month.Repeat_times); + archiveMonthRawData.Add("@fail_reason", archiveMonthJson); + + archiveMonthRawData.Add("@count_rawdata", 0); + archiveMonthRawData.Add("@min_rawdata", 0); + archiveMonthRawData.Add("@max_rawdata", 0); + archiveMonthRawData.Add("@avg_rawdata", 0); + archiveMonthRawData.Add("@sum_rawdata", 0); + archiveMonthRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + waterArchiveMonthRawDatas.Add(archiveMonthRawData); + } + + if (archiveMonthJsonResult.ContainsKey("obj")) //表示可以讀取到內容 + { + var ArrangeRawDatas = ArrangeRawData(deviceNumberPoint, archiveMonthJsonResult); + if (ArrangeRawDatas != null && ArrangeRawDatas.Count() > 0) + { + waterArchiveMonthRawDatas.AddRange(ArrangeRawDatas); + } + } + } + + if (waterArchiveMonthRawDatas.Count() > 0) + { + var Mysql_error_update = string.Format(MYsql_update_format, targetTable); + var sql_error_update = string.Format(sql_update_format, targetTable); + if (!string.IsNullOrEmpty(saveToMSDB) && saveToMSDB == "1") + { + await backgroundServiceMsSqlRepository.ExecuteSql(sql_error_update, waterArchiveMonthRawDatas); + } + await backgroundServiceRepository.ExecuteSql(MYsql_update_format, waterArchiveMonthRawDatas); + } + } + #endregion 月歸檔補償 + + result = true; + } + catch (Exception exception) + { + throw exception; + } + + return result; + } + + private List> ArrangeRawData(DeviceNumberPoint deviceNumberPoint, JObject jsonResult) + { + List> arrangeRawDatas = new List>(); + var histories = jsonResult["obj"]["list"]; + var rawdateCount = Convert.ToInt32(jsonResult["obj"]["int"]["@val"].ToString()); + + if (rawdateCount == 0) + { + return null; + } + + if (histories != null && histories.HasValues) + { + if (rawdateCount > 1) + { //多筆資料 + foreach (var history in histories) + { + Dictionary arrangeRawData = new Dictionary(); + arrangeRawData.Add("@device_number", deviceNumberPoint.DeviceNumber); + arrangeRawData.Add("@point", deviceNumberPoint.Point); + + //時間 + if (history["abstime"] != null && history["abstime"].HasValues) + { + foreach (var abstime in history["abstime"]) + { + var name = abstime["@name"].ToString(); + switch (name) + { + case "start": + var startTimstamp = Convert.ToDateTime(abstime["@val"].ToString()).ToString("yyyy-MM-dd HH:mm:ss"); + arrangeRawData.Add("@start_timestamp", startTimstamp); + break; + case "end": + var endTimstamp = Convert.ToDateTime(abstime["@val"].ToString()).ToString("yyyy-MM-dd HH:mm:ss"); + arrangeRawData.Add("@end_timestamp", endTimstamp); + break; + } + } + } + + //區間內資料筆數 + if (history["int"] != null && history["int"].HasValues) + { + var count = Convert.ToInt32(histories["obj"]["int"]["@val"].ToString()); + arrangeRawData.Add("@count_rawdata", count); + } + + //整合數值(最大、最小、平均、總和) + if (history["real"] != null && history["real"].HasValues) + { + foreach (var real in history["real"]) + { + var name = real["@name"].ToString(); + switch (name) + { + case "min": + var min = Convert.ToDecimal(real["@val"].ToString()); + arrangeRawData.Add("@min_rawdata", min); + break; + case "max": + var max = Convert.ToDecimal(real["@val"].ToString()); + arrangeRawData.Add("@max_rawdata", max); + break; + case "avg": + var avg = Convert.ToDecimal(real["@val"].ToString()); + arrangeRawData.Add("@avg_rawdata", avg); + break; + case "sum": + var sum = Decimal.Parse(real["@val"].ToString(), System.Globalization.NumberStyles.Float); + arrangeRawData.Add("@sum_rawdata", sum); + break; + } + } + } + arrangeRawData.Add("@is_complete", 1); + arrangeRawData.Add("@repeat_times", 0); + arrangeRawData.Add("@fail_reason", null); + arrangeRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + arrangeRawDatas.Add(arrangeRawData); + } + } + else + { //單筆資料 + Dictionary arrangeRawData = new Dictionary(); + arrangeRawData.Add("@device_number", deviceNumberPoint.DeviceNumber); + arrangeRawData.Add("@point", deviceNumberPoint.Point); + + //時間 + if (histories["obj"]["abstime"] != null && histories["obj"]["abstime"].HasValues) + { + foreach (var abstime in histories["obj"]["abstime"]) + { + var name = abstime["@name"].ToString(); + switch (name) + { + case "start": + var startTimstamp = Convert.ToDateTime(abstime["@val"].ToString()).ToString("yyyy-MM-dd HH:mm:ss"); + arrangeRawData.Add("@start_timestamp", startTimstamp); + break; + case "end": + var endTimstamp = Convert.ToDateTime(abstime["@val"].ToString()).ToString("yyyy-MM-dd HH:mm:ss"); + arrangeRawData.Add("@end_timestamp", endTimstamp); + break; + } + } + } + + //區間內資料筆數 + if (histories["obj"]["int"] != null && histories["obj"]["int"].HasValues) + { + var count = Convert.ToInt32(histories["obj"]["int"]["@val"].ToString()); + arrangeRawData.Add("@count_rawdata", count); + } + + //整合數值(最大、最小、平均、總和) + if (histories["obj"]["real"] != null && histories["obj"]["real"].HasValues) + { + foreach (var real in histories["obj"]["real"]) + { + var name = real["@name"].ToString(); + switch (name) + { + case "min": + var min = Convert.ToDecimal(real["@val"].ToString()); + arrangeRawData.Add("@min_rawdata", min); + break; + case "max": + var max = Convert.ToDecimal(real["@val"].ToString()); + arrangeRawData.Add("@max_rawdata", max); + break; + case "avg": + var avg = Convert.ToDecimal(real["@val"].ToString()); + arrangeRawData.Add("@avg_rawdata", avg); + break; + case "sum": + var sum = Decimal.Parse(real["@val"].ToString(), System.Globalization.NumberStyles.Float); + arrangeRawData.Add("@sum_rawdata", sum); + break; + } + } + } + + arrangeRawData.Add("@is_complete", 1); + arrangeRawData.Add("@repeat_times", 0); + arrangeRawData.Add("@fail_reason", null); + arrangeRawData.Add("@updated_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); + + arrangeRawDatas.Add(arrangeRawData); + } + } + + return arrangeRawDatas; + } + } +} diff --git a/Backend/Services/Implement/Quicktype.cs b/Backend/Services/Implement/Quicktype.cs new file mode 100644 index 0000000..35dab94 --- /dev/null +++ b/Backend/Services/Implement/Quicktype.cs @@ -0,0 +1,640 @@ +using System; +using System.Collections.Generic; +using System.Text; + +// +// +// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do: +// +// using QuickType; +// +// var welcome = Welcome.FromJson(jsonString); + +namespace QuickType +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class Welcome + { + [JsonProperty("success")] + [JsonConverter(typeof(ParseStringConverter))] + public bool Success { get; set; } + + [JsonProperty("result")] + public Result Result { get; set; } + + [JsonProperty("records")] + public Records Records { get; set; } + } + + public partial class Records + { + [JsonProperty("datasetDescription")] + public DatasetDescription DatasetDescription { get; set; } + + [JsonProperty("Earthquake")] + public Earthquake[] Earthquake { get; set; } + } + + public partial class Earthquake + { + [JsonProperty("EarthquakeNo")] + public long EarthquakeNo { get; set; } + + [JsonProperty("ReportType")] + public DatasetDescription ReportType { get; set; } + + [JsonProperty("ReportColor")] + public ReportColor ReportColor { get; set; } + + [JsonProperty("ReportContent")] + public string ReportContent { get; set; } + + [JsonProperty("ReportImageURI")] + public Uri ReportImageUri { get; set; } + + [JsonProperty("ReportRemark")] + public ReportRemark ReportRemark { get; set; } + + [JsonProperty("Web")] + public Uri Web { get; set; } + + [JsonProperty("ShakemapImageURI")] + public Uri ShakemapImageUri { get; set; } + + [JsonProperty("EarthquakeInfo")] + public EarthquakeInfo EarthquakeInfo { get; set; } + + [JsonProperty("Intensity")] + public Intensity Intensity { get; set; } + } + + public partial class EarthquakeInfo + { + [JsonProperty("OriginTime")] + public DateTimeOffset OriginTime { get; set; } + + [JsonProperty("Source")] + public Source Source { get; set; } + + [JsonProperty("FocalDepth")] + public double FocalDepth { get; set; } + + [JsonProperty("Epicenter")] + public Epicenter Epicenter { get; set; } + + [JsonProperty("EarthquakeMagnitude")] + public EarthquakeMagnitude EarthquakeMagnitude { get; set; } + } + + public partial class EarthquakeMagnitude + { + [JsonProperty("MagnitudeType")] + public MagnitudeType MagnitudeType { get; set; } + + [JsonProperty("MagnitudeValue")] + public double MagnitudeValue { get; set; } + } + + public partial class Epicenter + { + [JsonProperty("Location")] + public string Location { get; set; } + + [JsonProperty("EpicenterLatitude")] + public double EpicenterLatitude { get; set; } + + [JsonProperty("EpicenterLongitude")] + public double EpicenterLongitude { get; set; } + } + + public partial class Intensity + { + [JsonProperty("ShakingArea")] + public ShakingArea[] ShakingArea { get; set; } + } + + public partial class ShakingArea + { + [JsonProperty("AreaDesc")] + public string AreaDesc { get; set; } + + [JsonProperty("CountyName")] + public string CountyName { get; set; } + + [JsonProperty("InfoStatus", NullValueHandling = NullValueHandling.Ignore)] + public InfoStatus? InfoStatus { get; set; } + + [JsonProperty("AreaIntensity")] + public AreaIntensityEnum AreaIntensity { get; set; } + + [JsonProperty("EqStation")] + public EqStation[] EqStation { get; set; } + } + + public partial class EqStation + { + [JsonProperty("pga", NullValueHandling = NullValueHandling.Ignore)] + public Pga Pga { get; set; } + + [JsonProperty("pgv", NullValueHandling = NullValueHandling.Ignore)] + public Pga Pgv { get; set; } + + [JsonProperty("StationName")] + public string StationName { get; set; } + + [JsonProperty("StationID")] + public string StationId { get; set; } + + [JsonProperty("InfoStatus", NullValueHandling = NullValueHandling.Ignore)] + public InfoStatus? InfoStatus { get; set; } + + [JsonProperty("BackAzimuth")] + public double BackAzimuth { get; set; } + + [JsonProperty("EpicenterDistance")] + public double EpicenterDistance { get; set; } + + [JsonProperty("SeismicIntensity")] + public AreaIntensityEnum SeismicIntensity { get; set; } + + [JsonProperty("StationLatitude")] + public double StationLatitude { get; set; } + + [JsonProperty("StationLongitude")] + public double StationLongitude { get; set; } + + [JsonProperty("WaveImageURI", NullValueHandling = NullValueHandling.Ignore)] + public Uri WaveImageUri { get; set; } + } + + public partial class Pga + { + [JsonProperty("unit")] + public Unit Unit { get; set; } + + [JsonProperty("EWComponent")] + public double EwComponent { get; set; } + + [JsonProperty("NSComponent")] + public double NsComponent { get; set; } + + [JsonProperty("VComponent")] + public double VComponent { get; set; } + + [JsonProperty("IntScaleValue")] + public double IntScaleValue { get; set; } + } + + public partial class Result + { + [JsonProperty("resource_id")] + public string ResourceId { get; set; } + + [JsonProperty("fields")] + public Field[] Fields { get; set; } + } + + public partial class Field + { + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("type")] + public TypeEnum Type { get; set; } + } + + public enum DatasetDescription { 地震報告 }; + + public enum MagnitudeType { 芮氏規模 }; + + public enum Source { 中央氣象局 }; + + public enum AreaIntensityEnum { The1級, The2級, The3級, The4級 }; + + public enum InfoStatus { Observe }; + + public enum Unit { Gal, Kine }; + + public enum ReportColor { 綠色 }; + + public enum ReportRemark { 本報告係中央氣象局地震觀測網即時地震資料地震速報之結果 }; + + public enum TypeEnum { Float, Integer, String, Timestamp }; + + public partial class Welcome + { + public static Welcome FromJson(string json) => JsonConvert.DeserializeObject(json, QuickType.Converter.Settings); + } + + public static class Serialize + { + public static string ToJson(this Welcome self) => JsonConvert.SerializeObject(self, QuickType.Converter.Settings); + } + + internal static class Converter + { + public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings + { + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + DateParseHandling = DateParseHandling.None, + Converters = + { + MagnitudeTypeConverter.Singleton, + SourceConverter.Singleton, + AreaIntensityEnumConverter.Singleton, + InfoStatusConverter.Singleton, + UnitConverter.Singleton, + ReportColorConverter.Singleton, + ReportRemarkConverter.Singleton, + DatasetDescriptionConverter.Singleton, + TypeEnumConverter.Singleton, + new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal } + }, + }; + } + + internal class MagnitudeTypeConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(MagnitudeType) || t == typeof(MagnitudeType?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize(reader); + if (value == "芮氏規模") + { + return MagnitudeType.芮氏規模; + } + throw new Exception("Cannot unmarshal type MagnitudeType"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (MagnitudeType)untypedValue; + if (value == MagnitudeType.芮氏規模) + { + serializer.Serialize(writer, "芮氏規模"); + return; + } + throw new Exception("Cannot marshal type MagnitudeType"); + } + + public static readonly MagnitudeTypeConverter Singleton = new MagnitudeTypeConverter(); + } + + internal class SourceConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(Source) || t == typeof(Source?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize(reader); + if (value == "中央氣象局") + { + return Source.中央氣象局; + } + throw new Exception("Cannot unmarshal type Source"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (Source)untypedValue; + if (value == Source.中央氣象局) + { + serializer.Serialize(writer, "中央氣象局"); + return; + } + throw new Exception("Cannot marshal type Source"); + } + + public static readonly SourceConverter Singleton = new SourceConverter(); + } + + internal class AreaIntensityEnumConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(AreaIntensityEnum) || t == typeof(AreaIntensityEnum?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize(reader); + switch (value) + { + case "1級": + return AreaIntensityEnum.The1級; + case "2級": + return AreaIntensityEnum.The2級; + case "3級": + return AreaIntensityEnum.The3級; + case "4級": + return AreaIntensityEnum.The4級; + } + throw new Exception("Cannot unmarshal type AreaIntensityEnum"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (AreaIntensityEnum)untypedValue; + switch (value) + { + case AreaIntensityEnum.The1級: + serializer.Serialize(writer, "1級"); + return; + case AreaIntensityEnum.The2級: + serializer.Serialize(writer, "2級"); + return; + case AreaIntensityEnum.The3級: + serializer.Serialize(writer, "3級"); + return; + case AreaIntensityEnum.The4級: + serializer.Serialize(writer, "4級"); + return; + } + throw new Exception("Cannot marshal type AreaIntensityEnum"); + } + + public static readonly AreaIntensityEnumConverter Singleton = new AreaIntensityEnumConverter(); + } + + internal class InfoStatusConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(InfoStatus) || t == typeof(InfoStatus?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize(reader); + if (value == "observe") + { + return InfoStatus.Observe; + } + throw new Exception("Cannot unmarshal type InfoStatus"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (InfoStatus)untypedValue; + if (value == InfoStatus.Observe) + { + serializer.Serialize(writer, "observe"); + return; + } + throw new Exception("Cannot marshal type InfoStatus"); + } + + public static readonly InfoStatusConverter Singleton = new InfoStatusConverter(); + } + + internal class UnitConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(Unit) || t == typeof(Unit?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize(reader); + switch (value) + { + case "gal": + return Unit.Gal; + case "kine": + return Unit.Kine; + } + throw new Exception("Cannot unmarshal type Unit"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (Unit)untypedValue; + switch (value) + { + case Unit.Gal: + serializer.Serialize(writer, "gal"); + return; + case Unit.Kine: + serializer.Serialize(writer, "kine"); + return; + } + throw new Exception("Cannot marshal type Unit"); + } + + public static readonly UnitConverter Singleton = new UnitConverter(); + } + + internal class ReportColorConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(ReportColor) || t == typeof(ReportColor?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize(reader); + if (value == "綠色") + { + return ReportColor.綠色; + } + throw new Exception("Cannot unmarshal type ReportColor"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (ReportColor)untypedValue; + if (value == ReportColor.綠色) + { + serializer.Serialize(writer, "綠色"); + return; + } + throw new Exception("Cannot marshal type ReportColor"); + } + + public static readonly ReportColorConverter Singleton = new ReportColorConverter(); + } + + internal class ReportRemarkConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(ReportRemark) || t == typeof(ReportRemark?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize(reader); + if (value == "本報告係中央氣象局地震觀測網即時地震資料地震速報之結果。") + { + return ReportRemark.本報告係中央氣象局地震觀測網即時地震資料地震速報之結果; + } + throw new Exception("Cannot unmarshal type ReportRemark"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (ReportRemark)untypedValue; + if (value == ReportRemark.本報告係中央氣象局地震觀測網即時地震資料地震速報之結果) + { + serializer.Serialize(writer, "本報告係中央氣象局地震觀測網即時地震資料地震速報之結果。"); + return; + } + throw new Exception("Cannot marshal type ReportRemark"); + } + + public static readonly ReportRemarkConverter Singleton = new ReportRemarkConverter(); + } + + internal class DatasetDescriptionConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(DatasetDescription) || t == typeof(DatasetDescription?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize(reader); + if (value == "地震報告") + { + return DatasetDescription.地震報告; + } + throw new Exception("Cannot unmarshal type DatasetDescription"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (DatasetDescription)untypedValue; + if (value == DatasetDescription.地震報告) + { + serializer.Serialize(writer, "地震報告"); + return; + } + throw new Exception("Cannot marshal type DatasetDescription"); + } + + public static readonly DatasetDescriptionConverter Singleton = new DatasetDescriptionConverter(); + } + + internal class TypeEnumConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(TypeEnum) || t == typeof(TypeEnum?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize(reader); + switch (value) + { + case "Float": + return TypeEnum.Float; + case "Integer": + return TypeEnum.Integer; + case "String": + return TypeEnum.String; + case "Timestamp": + return TypeEnum.Timestamp; + } + throw new Exception("Cannot unmarshal type TypeEnum"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (TypeEnum)untypedValue; + switch (value) + { + case TypeEnum.Float: + serializer.Serialize(writer, "Float"); + return; + case TypeEnum.Integer: + serializer.Serialize(writer, "Integer"); + return; + case TypeEnum.String: + serializer.Serialize(writer, "String"); + return; + case TypeEnum.Timestamp: + serializer.Serialize(writer, "Timestamp"); + return; + } + throw new Exception("Cannot marshal type TypeEnum"); + } + + public static readonly TypeEnumConverter Singleton = new TypeEnumConverter(); + } + + internal class ParseStringConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(bool) || t == typeof(bool?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize(reader); + bool b; + if (Boolean.TryParse(value, out b)) + { + return b; + } + throw new Exception("Cannot unmarshal type bool"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (bool)untypedValue; + var boolString = value ? "true" : "false"; + serializer.Serialize(writer, boolString); + return; + } + + public static readonly ParseStringConverter Singleton = new ParseStringConverter(); + } +} diff --git a/Backend/Services/Implement/RainApi.cs b/Backend/Services/Implement/RainApi.cs new file mode 100644 index 0000000..ec870f9 --- /dev/null +++ b/Backend/Services/Implement/RainApi.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.Text; + +// +// +// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do: +// +// using QuickType; +// +// var welcome = Welcome.FromJson(jsonString); + +namespace RainApi +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class Welcome + { + [JsonProperty("?xml")] + public Xml Xml { get; set; } + + [JsonProperty("alert")] + public Alert Alert { get; set; } + } + + public partial class Alert + { + [JsonProperty("@xmlns")] + public string Xmlns { get; set; } + + [JsonProperty("identifier")] + public string Identifier { get; set; } + + [JsonProperty("sender")] + public string Sender { get; set; } + + [JsonProperty("sent")] + public DateTimeOffset Sent { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("msgType")] + public string MsgType { get; set; } + + [JsonProperty("scope")] + public string Scope { get; set; } + + [JsonProperty("references")] + public string References { get; set; } + + [JsonProperty("info")] + public Info[] Info { get; set; } + } + + public partial class Info + { + [JsonProperty("language")] + public string Language { get; set; } + + [JsonProperty("category")] + public string Category { get; set; } + + [JsonProperty("event")] + public string Event { get; set; } + + [JsonProperty("responseType")] + public string ResponseType { get; set; } + + [JsonProperty("urgency")] + public string Urgency { get; set; } + + [JsonProperty("severity")] + public string Severity { get; set; } + + [JsonProperty("certainty")] + public string Certainty { get; set; } + + [JsonProperty("eventCode")] + public EventCode EventCode { get; set; } + + [JsonProperty("effective")] + public DateTimeOffset Effective { get; set; } + + [JsonProperty("onset")] + public DateTimeOffset Onset { get; set; } + + [JsonProperty("expires")] + public DateTimeOffset Expires { get; set; } + + [JsonProperty("senderName")] + public string SenderName { get; set; } + + [JsonProperty("headline")] + public string Headline { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("web")] + public Uri Web { get; set; } + + [JsonProperty("parameter")] + public EventCode[] Parameter { get; set; } + + [JsonProperty("area")] + public Area[] Area { get; set; } + } + + public partial class Area + { + [JsonProperty("areaDesc")] + public string AreaDesc { get; set; } + + [JsonProperty("geocode")] + public EventCode Geocode { get; set; } + } + + public partial class EventCode + { + [JsonProperty("valueName")] + public ValueName ValueName { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } + } + + public partial class Xml + { + [JsonProperty("@version")] + public string Version { get; set; } + + [JsonProperty("@encoding")] + public string Encoding { get; set; } + } + + public enum ValueName { AlertColor, AlertTitle, ProfileCapTwpEvent10, SeverityLevel, TaiwanGeocode103, WebsiteColor }; + + public partial class Welcome + { + public static Welcome FromJson(string json) => JsonConvert.DeserializeObject(json, QuickType.Converter.Settings); + } + + public static class Serialize + { + public static string ToJson(this Welcome self) => JsonConvert.SerializeObject(self, QuickType.Converter.Settings); + } + + internal static class Converter + { + public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings + { + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + DateParseHandling = DateParseHandling.None, + Converters = + { + ValueNameConverter.Singleton, + new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal } + }, + }; + } + + internal class ValueNameConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(ValueName) || t == typeof(ValueName?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize(reader); + switch (value) + { + case "Taiwan_Geocode_103": + return ValueName.TaiwanGeocode103; + case "alert_color": + return ValueName.AlertColor; + case "alert_title": + return ValueName.AlertTitle; + case "profile:CAP-TWP:Event:1.0": + return ValueName.ProfileCapTwpEvent10; + case "severity_level": + return ValueName.SeverityLevel; + case "website_color": + return ValueName.WebsiteColor; + } + throw new Exception("Cannot unmarshal type ValueName"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (ValueName)untypedValue; + switch (value) + { + case ValueName.TaiwanGeocode103: + serializer.Serialize(writer, "Taiwan_Geocode_103"); + return; + case ValueName.AlertColor: + serializer.Serialize(writer, "alert_color"); + return; + case ValueName.AlertTitle: + serializer.Serialize(writer, "alert_title"); + return; + case ValueName.ProfileCapTwpEvent10: + serializer.Serialize(writer, "profile:CAP-TWP:Event:1.0"); + return; + case ValueName.SeverityLevel: + serializer.Serialize(writer, "severity_level"); + return; + case ValueName.WebsiteColor: + serializer.Serialize(writer, "website_color"); + return; + } + throw new Exception("Cannot marshal type ValueName"); + } + + public static readonly ValueNameConverter Singleton = new ValueNameConverter(); + } +} diff --git a/Backend/Services/Implement/SendEmailService.cs b/Backend/Services/Implement/SendEmailService.cs new file mode 100644 index 0000000..b72b5ee --- /dev/null +++ b/Backend/Services/Implement/SendEmailService.cs @@ -0,0 +1,81 @@ +using Microsoft.Extensions.Options; +using Backend.Models; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Mail; +using System.Threading.Tasks; +using BackendWorkerService.Services.Interface; +using Microsoft.Extensions.Logging; + +namespace BackendWorkerService.Services.Implement +{ + public class SendEmailService : ISendEmailService + { + private readonly ILogger logger; + private readonly IOptions _options; + + private SMTPConfig smtp; + + public SendEmailService(ILogger logger, IOptions options) + { + this.logger = logger; + smtp = options.Value; + } + + public bool Send(int id, List recipientEmails, string subject, string content) + { + try + { + logger.LogInformation("【SendEmailSMSService】【Email開始寄送】[任務編號]:{0}", id); + + MailMessage MyMail = new MailMessage(); + + MyMail.SubjectEncoding = System.Text.Encoding.UTF8;//郵件標題編碼 + MyMail.BodyEncoding = System.Text.Encoding.UTF8; //郵件內容編碼 + MyMail.IsBodyHtml = true; //是否使用html格式 + + var mailFrom = $"FIC IBMS管理系統通知 <{smtp.UserName}>"; + + MyMail.From = new System.Net.Mail.MailAddress(mailFrom); //寄件人 + foreach (var email in recipientEmails) + { + MyMail.To.Add(email); //設定收件者Email + } + + MyMail.Subject = subject; //主題 + MyMail.Body = content; //設定信件內容 + + //讀取 SMTP Config + SmtpClient MySMTP = new SmtpClient(smtp.Host, smtp.Port); + MySMTP.EnableSsl = smtp.EnableSsl; + MySMTP.Credentials = new System.Net.NetworkCredential(smtp.UserName, smtp.Password); + + try + { + MySMTP.Send(MyMail); + MySMTP.Dispose(); + MyMail.Dispose(); //釋放資源 + + logger.LogInformation("【SendEmailSMSService】【Email寄送成功】[任務編號]:{0}", id); + return true; + } + catch(Exception exception) + { + logger.LogError("【SendEmailSMSService】【Email寄送失敗】[任務編號]:{0}", id); + logger.LogError("【SendEmailSMSService】【Email寄送失敗】[Exception]:{0}", exception.ToString()); + return false; + } + } + catch (Exception exception) + { + logger.LogError("【SendEmailSMSService】【Email寄送失敗】[任務編號]:{0}", id); + logger.LogError("【SendEmailSMSService】【Email寄送失敗】[Exception]:{0}", exception.ToString()); + throw; + //return false; + } + + } + } +} diff --git a/Backend/Services/Implement/SendLineNotifyService.cs b/Backend/Services/Implement/SendLineNotifyService.cs new file mode 100644 index 0000000..b291d6a --- /dev/null +++ b/Backend/Services/Implement/SendLineNotifyService.cs @@ -0,0 +1,54 @@ +using BackendWorkerService.Services.Interface; +using Microsoft. Extensions.Logging; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; + +namespace BackendWorkerService.Services.Implement +{ + class SendLineNotifyService: ISendLineNotifyService + { + + private readonly ILogger logger; + + public SendLineNotifyService(ILogger logger) + { + this.logger = logger; + } + + public bool Send(int id, string lineToken, string message) + { + try + { + logger.LogInformation("【SendLineNotifyService】【Line Notify開始發送】[任務編號]:{0}", id); + HttpWebRequest Postrequest = (HttpWebRequest)WebRequest.Create("https://notify-api.line.me/api/notify?message=" + message); + Postrequest.Method = "POST"; + Postrequest.Headers.Add("Authorization", "Bearer " + lineToken); + Postrequest.PreAuthenticate = true; + HttpWebResponse response = (HttpWebResponse)Postrequest.GetResponse(); + var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd(); + var final = JObject.Parse(responseString); + var get = final["status"].ToString(); + if (get != "200") + { + logger.LogError("【SendLineNotifyService】【Line Notify發送失敗】[任務編號]:{0}", id); + logger.LogError("【SendLineNotifyService】【Line Notify發送失敗】[失敗內容]:{0}", responseString); + return false; + } + + logger.LogInformation("【SendLineNotifyService】【Line Notify發送成功】[任務編號]:{0}", id); + return true; + } + catch (Exception exception) + { + logger.LogError("【SendLineNotifyService】【Line Notify發送失敗】[任務編號]:{0}", id); + logger.LogError("【SendLineNotifyService】【Line Notify發送失敗】[Exception]:{0}", exception.ToString()); + throw; + //return false; + } + } + } +} diff --git a/Backend/Services/Implement/SendSMSService.cs b/Backend/Services/Implement/SendSMSService.cs new file mode 100644 index 0000000..97e4259 --- /dev/null +++ b/Backend/Services/Implement/SendSMSService.cs @@ -0,0 +1,84 @@ +using Backend.Models; +using BackendWorkerService.Services.Interface; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Xml; + +namespace BackendWorkerService.Services.Implement +{ + class SendSMSService : ISendSMSService + { + private readonly ILogger logger; + private readonly IOptions _options; + + private SMSConfig sms; + + public SendSMSService(ILogger logger, IOptions options) + { + this.logger = logger; + sms = options.Value; + sms.Encoded = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(sms.UserName + ":" + sms.Password)); + } + + public bool Send(int id, string phone, string content) + { + try + { + logger.LogError("【SendSMSService】【SMS開始寄送】[任務編號]:{0}", id); + //設定傳送的Request + HttpWebRequest Postrequest = (HttpWebRequest)WebRequest.Create(sms.Api); + Postrequest.Method = "POST"; + Postrequest.Headers.Add("Authorization", "Basic " + sms.Encoded); + Postrequest.PreAuthenticate = true; + using (var streamWriter = new StreamWriter(Postrequest.GetRequestStream())) + { + string json = string.Format("", phone, content); + + streamWriter.Write(json); + } + + //設定回傳的Request + HttpWebResponse response = (HttpWebResponse)Postrequest.GetResponse(); + var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd(); + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(responseString); + string jsonText = JsonConvert.SerializeXmlNode(xmlDoc); + JObject resultVal = (JObject)JsonConvert.DeserializeObject(jsonText); + + if (resultVal.ContainsKey("obj")) + { + var display_split = resultVal["obj"]["@display"].ToString().Split(' '); + var responseStatus = display_split[display_split.Length - 1]; + + if (responseStatus == "{ok}") + { + logger.LogInformation("【SendSMSService】【SMS寄送成功】[任務編號]:{0}", id); + return true; + } + } + else + { + logger.LogError("【SendSMSService】【SMS寄送失敗】[任務編號]:{0}", id); + logger.LogError("【SendSMSService】【SMS寄送失敗】[失敗內容]:{0}", resultVal); + return false; + } + + return false; + } + catch (Exception exception) + { + logger.LogError("【SendSMSService】【SMS寄送失敗】[任務編號]:{0}", id); + logger.LogError("【SendSMSService】【SMS寄送失敗】[Exception]:{0}", exception.ToString()); + throw; + //return false; + } + } + } +} diff --git a/Backend/Services/Implement/TyphoonApi.cs b/Backend/Services/Implement/TyphoonApi.cs new file mode 100644 index 0000000..07c8546 --- /dev/null +++ b/Backend/Services/Implement/TyphoonApi.cs @@ -0,0 +1,283 @@ +using System; +using System.Collections.Generic; +using System.Text; + +// +// +// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do: +// +// using QuickType; +// +// var welcome = Welcome.FromJson(jsonString); + +namespace TyphoonApi +{ + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class Welcome + { + [JsonProperty("?xml")] + public Xml Xml { get; set; } + + [JsonProperty("alert")] + public Alert Alert { get; set; } + } + + public partial class Alert + { + [JsonProperty("@xmlns")] + public string Xmlns { get; set; } + + [JsonProperty("identifier")] + public string Identifier { get; set; } + + [JsonProperty("sender")] + public string Sender { get; set; } + + [JsonProperty("sent")] + public DateTimeOffset Sent { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("msgType")] + public string MsgType { get; set; } + + [JsonProperty("scope")] + public string Scope { get; set; } + + [JsonProperty("references")] + public string References { get; set; } + + [JsonProperty("info")] + public Info Info { get; set; } + } + + public partial class Info + { + [JsonProperty("language")] + public string Language { get; set; } + + [JsonProperty("category")] + public string Category { get; set; } + + [JsonProperty("event")] + public string Event { get; set; } + + [JsonProperty("responseType")] + public string ResponseType { get; set; } + + [JsonProperty("urgency")] + public string Urgency { get; set; } + + [JsonProperty("severity")] + public string Severity { get; set; } + + [JsonProperty("certainty")] + public string Certainty { get; set; } + + [JsonProperty("eventCode")] + public EventCode EventCode { get; set; } + + [JsonProperty("effective")] + public DateTimeOffset Effective { get; set; } + + [JsonProperty("onset")] + public DateTimeOffset Onset { get; set; } + + [JsonProperty("expires")] + public DateTimeOffset Expires { get; set; } + + [JsonProperty("senderName")] + public string SenderName { get; set; } + + [JsonProperty("headline")] + public string Headline { get; set; } + + [JsonProperty("description")] + public Description Description { get; set; } + + [JsonProperty("web")] + public Uri Web { get; set; } + + [JsonProperty("parameter")] + public EventCode[] Parameter { get; set; } + + [JsonProperty("area")] + public Area[] Area { get; set; } + } + + public partial class Area + { + [JsonProperty("areaDesc")] + public string AreaDesc { get; set; } + + [JsonProperty("polygon")] + public string Polygon { get; set; } + } + + public partial class Description + { + [JsonProperty("typhoon-info")] + public TyphoonInfo TyphoonInfo { get; set; } + + [JsonProperty("section")] + public DescriptionSection[] Section { get; set; } + } + + public partial class DescriptionSection + { + [JsonProperty("@title")] + public string Title { get; set; } + + [JsonProperty("#text")] + public string Text { get; set; } + } + + public partial class TyphoonInfo + { + [JsonProperty("section")] + public TyphoonInfoSection[] Section { get; set; } + } + + public partial class TyphoonInfoSection + { + [JsonProperty("@title")] + public string Title { get; set; } + + [JsonProperty("#text", NullValueHandling = NullValueHandling.Ignore)] + public string Text { get; set; } + + [JsonProperty("typhoon_name", NullValueHandling = NullValueHandling.Ignore)] + public string TyphoonName { get; set; } + + [JsonProperty("cwb_typhoon_name", NullValueHandling = NullValueHandling.Ignore)] + public string CwbTyphoonName { get; set; } + + [JsonProperty("analysis", NullValueHandling = NullValueHandling.Ignore)] + public Analysis Analysis { get; set; } + + [JsonProperty("prediction", NullValueHandling = NullValueHandling.Ignore)] + public Analysis Prediction { get; set; } + } + + public partial class Analysis + { + [JsonProperty("time")] + public DateTimeOffset Time { get; set; } + + [JsonProperty("position")] + public string Position { get; set; } + + [JsonProperty("max_winds")] + public Gust MaxWinds { get; set; } + + [JsonProperty("gust")] + public Gust Gust { get; set; } + + [JsonProperty("pressure")] + public Gust Pressure { get; set; } + + [JsonProperty("radius_of_15mps")] + public Gust RadiusOf15Mps { get; set; } + + [JsonProperty("scale", NullValueHandling = NullValueHandling.Ignore)] + public Scale[] Scale { get; set; } + } + + public partial class Gust + { + [JsonProperty("@unit")] + public string Unit { get; set; } + + [JsonProperty("#text")] + [JsonConverter(typeof(ParseStringConverter))] + public long Text { get; set; } + } + + public partial class Scale + { + [JsonProperty("@lang")] + public string Lang { get; set; } + + [JsonProperty("#text")] + public string Text { get; set; } + } + + public partial class EventCode + { + [JsonProperty("valueName")] + public string ValueName { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } + } + + public partial class Xml + { + [JsonProperty("@version")] + public string Version { get; set; } + + [JsonProperty("@encoding")] + public string Encoding { get; set; } + } + + public partial class Welcome + { + public static Welcome FromJson(string json) => JsonConvert.DeserializeObject(json, QuickType.Converter.Settings); + } + + public static class Serialize + { + public static string ToJson(this Welcome self) => JsonConvert.SerializeObject(self, QuickType.Converter.Settings); + } + + internal static class Converter + { + public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings + { + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + DateParseHandling = DateParseHandling.None, + Converters = + { + new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal } + }, + }; + } + + internal class ParseStringConverter : JsonConverter + { + public override bool CanConvert(Type t) => t == typeof(long) || t == typeof(long?); + + public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) return null; + var value = serializer.Deserialize(reader); + long l; + if (Int64.TryParse(value, out l)) + { + return l; + } + throw new Exception("Cannot unmarshal type long"); + } + + public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer) + { + if (untypedValue == null) + { + serializer.Serialize(writer, null); + return; + } + var value = (long)untypedValue; + serializer.Serialize(writer, value.ToString()); + return; + } + + public static readonly ParseStringConverter Singleton = new ParseStringConverter(); + } +} diff --git a/Backend/Services/Interface/ISendEmailService.cs b/Backend/Services/Interface/ISendEmailService.cs new file mode 100644 index 0000000..a349ed7 --- /dev/null +++ b/Backend/Services/Interface/ISendEmailService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BackendWorkerService.Services.Interface +{ + public interface ISendEmailService + { + bool Send(int id, List recipientEmails, string subject, string content); + } +} diff --git a/Backend/Services/Interface/ISendLineNotifyService.cs b/Backend/Services/Interface/ISendLineNotifyService.cs new file mode 100644 index 0000000..295dd8c --- /dev/null +++ b/Backend/Services/Interface/ISendLineNotifyService.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BackendWorkerService.Services.Interface +{ + public interface ISendLineNotifyService + { + bool Send(int id, string lineToken, string message); + } +} diff --git a/Backend/Services/Interface/ISendSMSService.cs b/Backend/Services/Interface/ISendSMSService.cs new file mode 100644 index 0000000..c414a93 --- /dev/null +++ b/Backend/Services/Interface/ISendSMSService.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BackendWorkerService.Services.Interface +{ + public interface ISendSMSService + { + bool Send(int id, string phone, string message); + } +} diff --git a/Backend/Startup.cs b/Backend/Startup.cs index 37e1ba6..a85040b 100644 --- a/Backend/Startup.cs +++ b/Backend/Startup.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -16,15 +15,18 @@ using Repository.FrontendRepository.Interface; using Repository.Models; using System; -using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Backend.Jwt; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; using System.IdentityModel.Tokens.Jwt; using Microsoft.AspNetCore.Http; +using Backend.Quartz.Jobs; +using Backend.Quartz; +using Quartz.Impl; +using Quartz.Spi; +using Quartz; namespace Backend { @@ -115,8 +117,16 @@ namespace Backend services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); #endregion Repository `J + #region K[QuartzA + services.AddTransient(); + services.AddTransient(); + services.AddHostedService(); + #endregion + #region JWT `J services.AddTransient(); services @@ -153,6 +163,20 @@ namespace Backend }); #endregion JWT `J + #region qk(]wC @) + services.AddSingleton(); + services.AddSingleton( + new JobSchedule(jobType: typeof(ArchiveElectricMeterDayJob), cronExpression: Configuration.GetValue("BackgroundServiceCron:ArchiveElectricMeterDayJob")) + ); + #endregion + + #region wɨoHAPI + services.AddSingleton(); + services.AddSingleton( + new JobSchedule(jobType: typeof(WeatherAPIJob), cronExpression: Configuration.GetValue("BackgroundServiceCron:WeatherAPIJob")) + ); + #endregion + double loginExpireMinute = this.Configuration.GetValue("LoginExpireMinute"); services.AddSession(options => { diff --git a/Backend/appsettings.Development.json b/Backend/appsettings.Development.json index fa903a2..7f80719 100644 --- a/Backend/appsettings.Development.json +++ b/Backend/appsettings.Development.json @@ -12,6 +12,17 @@ "SignKey": "TaipeiDome123456", //簽章//最少16字元 "JwtLifeSeconds": 3600 }, + "LoggerPath": "C:\\inetpub\\Taipei_dome_background_service\\Logs", + "BackgroundServiceCron": { + "ExecutionBackgroundServicePlanJob": "0 0 2 * * ?", + "MessageNotificationJob": "0 0 2 * * ?", + "DataDeliveryJob": "0 0 2 * * ?", + "RegularUpdateDBTableJob": "0 0 2 * * ?", + "ParkingJob": "0 0 2 * * ?", + "ArchiveElectricMeterHourJob": "0 0 2 * * ?", + "ArchiveElectricMeterDayJob": "0/5 * * * * ?", + "WeatherAPIJob": "0/5 * * * * ?" + }, "DBConfig": { "MySqlDBConfig": { "Server": "FYlY+w0XDIz+jmF2rlZWJw==", //0.201 @@ -23,6 +34,13 @@ //"Database": "iuaY0h0+TWkir44/eZLDqw==", //tpe_dome_office "Root": "SzdxEgaJJ7tcTCrUl2zKsA==", "Password": "FVAPxztxpY4gJJKQ/se4bQ==" + }, + "MSSqlDBConfig": { + "Server": "bJm+UAtbeaTjDmp/A5ep2w==", //0.130 + "Port": "S5cUXKnKOacFtFy9+0dtpw==", + "Database": "VvfWH/59gQguY2eA2xBCug==", //taipei_dome + "Root": "sD8GZ9UPiIQGU6dU011/4A==", + "Password": "0O24es2ZRF5uoJ4aU+YCdg==" } //"MSSqlDBConfig": { // "Server": "zp3Nilx0PISEEC4caZWqCg==", //172.16.220.250 diff --git a/Backend/appsettings.json b/Backend/appsettings.json index 4a53716..54ca26f 100644 --- a/Backend/appsettings.json +++ b/Backend/appsettings.json @@ -12,6 +12,7 @@ "SignKey": "TaipeiDome123456", //簽章//最少16字元 "JwtLifeSeconds": 3600 }, + "LoggerPath": "C:\\inetpub\\Taipei_dome_background_service\\Logs", "DBConfig": { "MySqlDBConfig": { "Server": "FYlY+w0XDIz+jmF2rlZWJw==", //0.201 @@ -22,6 +23,13 @@ //"Database": "iuaY0h0+TWkir44/eZLDqw==", //tpe_dome_office "Root": "SzdxEgaJJ7tcTCrUl2zKsA==", "Password": "FVAPxztxpY4gJJKQ/se4bQ==" + }, + "MSSqlDBConfig": { + "Server": "bJm+UAtbeaTjDmp/A5ep2w==", //0.130 + "Port": "S5cUXKnKOacFtFy9+0dtpw==", + "Database": "VvfWH/59gQguY2eA2xBCug==", //taipei_dome + "Root": "sD8GZ9UPiIQGU6dU011/4A==", + "Password": "0O24es2ZRF5uoJ4aU+YCdg==" } //"MSSqlDBConfig": { // "Server": "ueFp+VFb200lhh1Uctc97WH0/tX6tfXYU2v1oxCWuuM=", diff --git a/Backend/root/PowerfulRain.xml b/Backend/root/PowerfulRain.xml new file mode 100644 index 0000000..01f5439 --- /dev/null +++ b/Backend/root/PowerfulRain.xml @@ -0,0 +1,136 @@ + + + + CWB-Weather_extremely-rain_202306011035001 + weather@cwb.gov.tw + 2023-06-01T10:43:55+08:00 + Actual + Update + Public + weather@cwb.gov.tw,CWB-Weather_extremely-rain_202306010655001,2023-06-01T07:02:17+08:00 weather@cwb.gov.tw,CWB-Weather_extremely-rain_202306010345001,2023-06-01T03:56:19+08:00 + + zh-TW + Met + 降雨 + Monitor + Future + Moderate + Likely + + profile:CAP-TWP:Event:1.0 + rainfall + + 2023-06-01T10:35:00+08:00 + 2023-06-01T10:36:00+08:00 + 2023-06-01T23:00:00+08:00 + 中央氣象局 + 大雨特報 + +颱風外圍環流影響,今(1)日新竹以北及宜蘭山區有局部大雨發生的機率,請注意瞬間大雨,山區請慎防坍方及落石。 + + https://www.cwb.gov.tw/V8/C/P/Warning/FIFOWS.html + + alert_title + 大雨特報 + + + severity_level + 大雨 + + + alert_color + 黃色 + + + website_color + 255,255,0 + + + + 新竹縣橫山鄉 + + Taiwan_Geocode_103 + 1000408 + + + 新竹縣北埔鄉 + + Taiwan_Geocode_103 + 1000409 + + + 桃園市復興區 + + Taiwan_Geocode_103 + 6801300 + + + 臺北市士林區 + + Taiwan_Geocode_103 + 6301100 + + + 新北市三峽區 + + Taiwan_Geocode_103 + 6500900 + + + 新北市石碇區 + + Taiwan_Geocode_103 + 6501900 + + + 臺北市北投區 + + Taiwan_Geocode_103 + 6301200 + + + 新竹縣五峰鄉 + + Taiwan_Geocode_103 + 1000413 + + + 新竹縣尖石鄉 + + Taiwan_Geocode_103 + 1000412 + + + 新北市烏來區 + + Taiwan_Geocode_103 + 6502900 + + + 新北市坪林區 + + Taiwan_Geocode_103 + 6502000 + + + 新北市平溪區 + + Taiwan_Geocode_103 + 6502400 + + + 宜蘭縣南澳鄉 + + Taiwan_Geocode_103 + 1000212 + + + 宜蘭縣大同鄉 + + Taiwan_Geocode_103 + 1000211 + + + + + \ No newline at end of file diff --git a/Backend/root/Typhoon.xml b/Backend/root/Typhoon.xml new file mode 100644 index 0000000..9dd9749 --- /dev/null +++ b/Backend/root/Typhoon.xml @@ -0,0 +1,197 @@ + + + + CWB-Weather_typhoon-warning_202305311730001 + weather@cwb.gov.tw + 2023-05-31T17:17:30+08:00 + Actual + Cancel + Public + weather@cwb.gov.tw,CWB-Weather_typhoon-warning_202305311430001,2023-05-31T14:22:23+08:00 + + zh-TW + Met + 颱風 + Past + Minor + Observed + + profile:CAP-TWP:Event:1.0 + typhoon + + 2023-05-31T17:30:00+08:00 + 2023-05-31T17:30:00+08:00 + 2023-05-31T17:40:00+08:00 + 中央氣象局 + 解除颱風警報 + +[颱風動態] +根據最新資料顯示,第2號颱風暴風半徑略為縮小,中心目前在鵝鑾鼻東北東方海面,向北北東轉東北移動,對巴士海峽及臺灣東半部近海威脅已解除。 + +[注意事項] +*巴士海峽及臺灣附近各海面風浪明顯偏大;基隆北海岸、南部、東半部(含蘭嶼、綠島)、恆春半島沿海及澎湖、馬祖易有長浪發生,尤其東半部(含蘭嶼、綠島)、基隆北海岸、恆春半島沿海易有4至5米浪高,請避免前往海邊活動。*陸上強風特報:今(31)日臺南至苗栗沿海空曠地區及澎湖、蘭嶼、綠島易有9至10級強陣風,新竹以北、基隆北海岸、東半部沿海空曠地區、臺南至苗栗地區、恆春半島、金門、馬祖亦有較強陣風,請特別注意。*30日0時至31日17時出現較大累積雨量如下:宜蘭縣翠峰湖334.5毫米,臺中市南湖圈谷257.0毫米。*本警報單之颱風半徑為平均半徑,第2號颱風之7級風暴風半徑近似正圓,平均半徑約為280公里。颱風詳細特性請參考本局颱輔助說明(https://www.cwb.gov.tw/Data/typhoon/TY_PDF.pdf)。*此為第2號颱風警報最後一次報告。 + + + + https://www.cwb.gov.tw/V8/C/P/Warning/FIFOWS.html + + alert_title + 颱風警報 + + + + 基隆市 + + Taiwan_Geocode_103 + 10017 + + + + 臺北市 + + Taiwan_Geocode_103 + 63 + + + + 新北市 + + Taiwan_Geocode_103 + 65 + + + + 桃園市 + + Taiwan_Geocode_103 + 68 + + + + 新竹市 + + Taiwan_Geocode_103 + 10018 + + + + 新竹縣 + + Taiwan_Geocode_103 + 10004 + + + + 苗栗縣 + + Taiwan_Geocode_103 + 10005 + + + + 臺中市 + + Taiwan_Geocode_103 + 66 + + + + 彰化縣 + + Taiwan_Geocode_103 + 10007 + + + + 雲林縣 + + Taiwan_Geocode_103 + 10009 + + + + 南投縣 + + Taiwan_Geocode_103 + 10008 + + + + 嘉義縣 + + Taiwan_Geocode_103 + 10010 + + + + 嘉義市 + + Taiwan_Geocode_103 + 10020 + + + + 臺南市 + + Taiwan_Geocode_103 + 67 + + + + 高雄市 + + Taiwan_Geocode_103 + 64 + + + + 屏東縣 + + Taiwan_Geocode_103 + 10013 + + + + 宜蘭縣 + + Taiwan_Geocode_103 + 10002 + + + + 花蓮縣 + + Taiwan_Geocode_103 + 10015 + + + + 臺東縣 + + Taiwan_Geocode_103 + 10014 + + + + 澎湖縣 + + Taiwan_Geocode_103 + 10016 + + + + 金門縣 + + Taiwan_Geocode_103 + 09020 + + + + 連江縣 + + Taiwan_Geocode_103 + 09007 + + + + \ No newline at end of file diff --git a/BackendWorkerService/root/PowerfulRain.xml b/BackendWorkerService/root/PowerfulRain.xml index d650403..df76532 100644 --- a/BackendWorkerService/root/PowerfulRain.xml +++ b/BackendWorkerService/root/PowerfulRain.xml @@ -1,254 +1,68 @@ - CWB-Weather_extremely-rain_202305311525001 + CWB-Weather_extremely-rain_202306010655001 weather@cwb.gov.tw - 2023-05-31T15:33:17+08:00 + 2023-06-01T07:02:17+08:00 Actual Update Public - weather@cwb.gov.tw,CWB-Weather_extremely-rain_202305311515001,2023-05-31T15:23:25+08:00 weather@cwb.gov.tw,CWB-Weather_extremely-rain_202305311455001,2023-05-31T15:03:04+08:00 weather@cwb.gov.tw,CWB-Weather_extremely-rain_202305311310001,2023-05-31T13:18:35+08:00 weather@cwb.gov.tw,CWB-Weather_extremely-rain_202305311305001,2023-05-31T13:10:42+08:00 weather@cwb.gov.tw,CWB-Weather_extremely-rain_202305311150001,2023-05-31T11:55:41+08:00 weather@cwb.gov.tw,CWB-Weather_extremely-rain_202305311050001,2023-05-31T10:55:42+08:00 weather@cwb.gov.tw,CWB-Weather_extremely-rain_202305310915001,2023-05-31T09:22:56+08:00 + weather@cwb.gov.tw,CWB-Weather_extremely-rain_202306010345001,2023-06-01T03:56:19+08:00 zh-TW Met 降雨 Monitor - Expected - Severe + Future + Moderate Likely profile:CAP-TWP:Event:1.0 rainfall - 2023-05-31T15:25:00+08:00 - 2023-05-31T15:29:00+08:00 - 2023-05-31T23:00:00+08:00 + 2023-06-01T06:55:00+08:00 + 2023-06-01T06:59:00+08:00 + 2023-06-01T11:00:00+08:00 中央氣象局 - 豪雨特報 + 大雨特報 -颱風外圍環流影響及午後對流雲系發展旺盛,易有短延時強降雨,今(31)日新北市及桃園市山區、宜蘭縣山區、臺中市山區有局部大雨或豪雨發生,大臺北、桃園、新竹、花蓮、臺南、高雄地區及南投山區(奇萊山區)有局部大雨發生的機率,請注意雷擊、強陣風及溪水暴漲,山區請注意落石及坍方。 +颱風外圍環流影響,今(1)日大臺北、桃園、新竹、宜蘭地區有局部大雨發生的機率,請注意瞬間大雨、強陣風及溪水暴漲,山區請注意落石及坍方。 https://www.cwb.gov.tw/V8/C/P/Warning/FIFOWS.html alert_title - 豪雨特報 + 大雨特報 severity_level - 豪雨 + 大雨 alert_color - 橙色 + 黃色 website_color - 255,128,0 + 255,255,0 - 基隆市安樂區 + 新竹縣橫山鄉 Taiwan_Geocode_103 - 1001706 + 1000408 - 基隆市信義區 + 新竹縣北埔鄉 Taiwan_Geocode_103 - 1001707 + 1000409 - 基隆市仁愛區 + 桃園市觀音區 Taiwan_Geocode_103 - 1001704 - - - 基隆市暖暖區 - - Taiwan_Geocode_103 - 1001703 - - - 基隆市中正區 - - Taiwan_Geocode_103 - 1001701 - - - 基隆市中山區 - - Taiwan_Geocode_103 - 1001705 - - - 基隆市七堵區 - - Taiwan_Geocode_103 - 1001702 - - - 臺中市新社區 - - Taiwan_Geocode_103 - 6601900 - - - 臺中市東勢區 - - Taiwan_Geocode_103 - 6601000 - - - 宜蘭縣南澳鄉 - - Taiwan_Geocode_103 - 1000212 - - - 宜蘭縣大同鄉 - - Taiwan_Geocode_103 - 1000211 - - - 新北市淡水區 - - Taiwan_Geocode_103 - 6501000 - - - 臺中市太平區 - - Taiwan_Geocode_103 - 6602700 - - - 臺中市石岡區 - - Taiwan_Geocode_103 - 6602000 - - - 臺中市和平區 - - Taiwan_Geocode_103 - 6602900 - - - 新北市萬里區 - - Taiwan_Geocode_103 - 6502800 - - - 新北市烏來區 - - Taiwan_Geocode_103 - 6502900 - - - 新北市坪林區 - - Taiwan_Geocode_103 - 6502000 - - - 新北市三芝區 - - Taiwan_Geocode_103 - 6502100 - - - 新北市石門區 - - Taiwan_Geocode_103 - 6502200 - - - 新北市八里區 - - Taiwan_Geocode_103 - 6502300 - - - 新北市平溪區 - - Taiwan_Geocode_103 - 6502400 - - - 新北市貢寮區 - - Taiwan_Geocode_103 - 6502600 - - - 新北市金山區 - - Taiwan_Geocode_103 - 6502700 - - - 新北市五股區 - - Taiwan_Geocode_103 - 6501500 - - - 新北市蘆洲區 - - Taiwan_Geocode_103 - 6501400 - - - 新北市林口區 - - Taiwan_Geocode_103 - 6501700 - - - 新北市泰山區 - - Taiwan_Geocode_103 - 6501600 - - - 新北市汐止區 - - Taiwan_Geocode_103 - 6501100 - - - 新北市土城區 - - Taiwan_Geocode_103 - 6501300 - - - 新北市瑞芳區 - - Taiwan_Geocode_103 - 6501200 - - - 新北市石碇區 - - Taiwan_Geocode_103 - 6501900 - - - 新北市深坑區 - - Taiwan_Geocode_103 - 6501800 - - - 新北市雙溪區 - - Taiwan_Geocode_103 - 6502500 + 6801200 桃園市復興區 @@ -256,6 +70,114 @@ Taiwan_Geocode_103 6801300 + + 臺北市信義區 + + Taiwan_Geocode_103 + 6300200 + + + 桃園市新屋區 + + Taiwan_Geocode_103 + 6801100 + + + 新竹縣竹東鎮 + + Taiwan_Geocode_103 + 1000402 + + + 臺北市文山區 + + Taiwan_Geocode_103 + 6300800 + + + 新竹縣竹北市 + + Taiwan_Geocode_103 + 1000401 + + + 新竹縣新豐鄉 + + Taiwan_Geocode_103 + 1000406 + + + 臺北市松山區 + + Taiwan_Geocode_103 + 6300100 + + + 新竹縣關西鎮 + + Taiwan_Geocode_103 + 1000404 + + + 新竹縣湖口鄉 + + Taiwan_Geocode_103 + 1000405 + + + 宜蘭縣冬山鄉 + + Taiwan_Geocode_103 + 1000208 + + + 宜蘭縣五結鄉 + + Taiwan_Geocode_103 + 1000209 + + + 宜蘭縣頭城鎮 + + Taiwan_Geocode_103 + 1000204 + + + 宜蘭縣礁溪鄉 + + Taiwan_Geocode_103 + 1000205 + + + 宜蘭縣壯圍鄉 + + Taiwan_Geocode_103 + 1000206 + + + 宜蘭縣員山鄉 + + Taiwan_Geocode_103 + 1000207 + + + 宜蘭縣宜蘭市 + + Taiwan_Geocode_103 + 1000201 + + + 宜蘭縣羅東鎮 + + Taiwan_Geocode_103 + 1000202 + + + 臺北市大安區 + + Taiwan_Geocode_103 + 6300300 + 新北市三重區 @@ -268,6 +190,12 @@ Taiwan_Geocode_103 6500300 + + 新竹縣尖石鄉 + + Taiwan_Geocode_103 + 1000412 + 新北市板橋區 @@ -310,76 +238,11 @@ Taiwan_Geocode_103 6500900 - - - - - zh-TW - Met - 降雨 - Monitor - Future - Moderate - Likely - - profile:CAP-TWP:Event:1.0 - rainfall - - 2023-05-31T15:25:00+08:00 - 2023-05-31T15:29:00+08:00 - 2023-05-31T23:00:00+08:00 - 中央氣象局 - 豪雨特報 - -颱風外圍環流影響及午後對流雲系發展旺盛,易有短延時強降雨,今(31)日新北市及桃園市山區、宜蘭縣山區、臺中市山區有局部大雨或豪雨發生,大臺北、桃園、新竹、花蓮、臺南、高雄地區及南投山區(奇萊山區)有局部大雨發生的機率,請注意雷擊、強陣風及溪水暴漲,山區請注意落石及坍方。 - - https://www.cwb.gov.tw/V8/C/P/Warning/FIFOWS.html - - alert_title - 大雨特報 - - - severity_level - 大雨 - - - alert_color - 黃色 - - - website_color - 255,255,0 - - - - 高雄市新興區 - - Taiwan_Geocode_103 - 6400600 - - 高雄市前鎮區 + 新北市萬里區 Taiwan_Geocode_103 - 6400900 - - - 桃園市大溪區 - - Taiwan_Geocode_103 - 6800300 - - - 桃園市中壢區 - - Taiwan_Geocode_103 - 6800200 - - - 高雄市鼓山區 - - Taiwan_Geocode_103 - 6400200 + 6502800 臺北市中山區 @@ -388,40 +251,40 @@ 6300400 - 臺南市官田區 + 新北市烏來區 Taiwan_Geocode_103 - 6701000 + 6502900 - 臺南市善化區 + 宜蘭縣大同鄉 Taiwan_Geocode_103 - 6701900 + 1000211 - 臺南市新化區 + 新竹縣新埔鎮 Taiwan_Geocode_103 - 6701800 + 1000403 - 臺南市學甲區 + 新北市坪林區 Taiwan_Geocode_103 - 6701300 + 6502000 - 臺南市佳里區 + 新北市三芝區 Taiwan_Geocode_103 - 6701200 + 6502100 - 臺南市大內區 + 新北市石門區 Taiwan_Geocode_103 - 6701100 + 6502200 桃園市大園區 @@ -430,238 +293,28 @@ 6800600 - 臺南市北門區 + 新北市平溪區 Taiwan_Geocode_103 - 6701700 + 6502400 - 臺南市將軍區 + 新北市雙溪區 Taiwan_Geocode_103 - 6701600 + 6502500 - 臺南市七股區 + 新北市貢寮區 Taiwan_Geocode_103 - 6701500 + 6502600 - 臺南市西港區 + 新北市金山區 Taiwan_Geocode_103 - 6701400 - - - 高雄市小港區 - - Taiwan_Geocode_103 - 6401100 - - - 臺北市中正區 - - Taiwan_Geocode_103 - 6300500 - - - 臺北市北投區 - - Taiwan_Geocode_103 - 6301200 - - - 高雄市三民區 - - Taiwan_Geocode_103 - 6400500 - - - 臺南市下營區 - - Taiwan_Geocode_103 - 6700800 - - - 臺南市六甲區 - - Taiwan_Geocode_103 - 6700900 - - - 臺南市新營區 - - Taiwan_Geocode_103 - 6700100 - - - 臺南市鹽水區 - - Taiwan_Geocode_103 - 6700200 - - - 臺南市白河區 - - Taiwan_Geocode_103 - 6700300 - - - 臺南市柳營區 - - Taiwan_Geocode_103 - 6700400 - - - 臺南市後壁區 - - Taiwan_Geocode_103 - 6700500 - - - 臺南市東山區 - - Taiwan_Geocode_103 - 6700600 - - - 臺南市麻豆區 - - Taiwan_Geocode_103 - 6700700 - - - 臺北市松山區 - - Taiwan_Geocode_103 - 6300100 - - - 花蓮縣卓溪鄉 - - Taiwan_Geocode_103 - 1001513 - - - 花蓮縣萬榮鄉 - - Taiwan_Geocode_103 - 1001512 - - - 花蓮縣秀林鄉 - - Taiwan_Geocode_103 - 1001511 - - - 花蓮縣富里鄉 - - Taiwan_Geocode_103 - 1001510 - - - 高雄市鹽埕區 - - Taiwan_Geocode_103 - 6400100 - - - 臺南市安南區 - - Taiwan_Geocode_103 - 6703500 - - - 臺南市北區 - - Taiwan_Geocode_103 - 6703400 - - - 臺南市中西區 - - Taiwan_Geocode_103 - 6703700 - - - 高雄市彌陀區 - - Taiwan_Geocode_103 - 6402800 - - - 臺南市永康區 - - Taiwan_Geocode_103 - 6703100 - - - 臺南市龍崎區 - - Taiwan_Geocode_103 - 6703000 - - - 臺南市南區 - - Taiwan_Geocode_103 - 6703300 - - - 臺南市東區 - - Taiwan_Geocode_103 - 6703200 - - - 高雄市阿蓮區 - - Taiwan_Geocode_103 - 6402300 - - - 高雄市田寮區 - - Taiwan_Geocode_103 - 6402200 - - - 高雄市燕巢區 - - Taiwan_Geocode_103 - 6402100 - - - 高雄市橋頭區 - - Taiwan_Geocode_103 - 6402000 - - - 高雄市永安區 - - Taiwan_Geocode_103 - 6402700 - - - 高雄市茄萣區 - - Taiwan_Geocode_103 - 6402600 - - - 高雄市湖內區 - - Taiwan_Geocode_103 - 6402500 - - - 高雄市路竹區 - - Taiwan_Geocode_103 - 6402400 + 6502700 臺北市大同區 @@ -669,12 +322,6 @@ Taiwan_Geocode_103 6300600 - - 高雄市杉林區 - - Taiwan_Geocode_103 - 6403400 - 臺北市士林區 @@ -682,202 +329,16 @@ 6301100 - 花蓮縣新城鄉 + 臺北市中正區 Taiwan_Geocode_103 - 1001504 + 6300500 - 高雄市楠梓區 + 宜蘭縣三星鄉 Taiwan_Geocode_103 - 6400400 - - - 花蓮縣壽豐鄉 - - Taiwan_Geocode_103 - 1001506 - - - 花蓮縣光復鄉 - - Taiwan_Geocode_103 - 1001507 - - - 花蓮縣花蓮市 - - Taiwan_Geocode_103 - 1001501 - - - 花蓮縣鳳林鎮 - - Taiwan_Geocode_103 - 1001502 - - - 花蓮縣玉里鎮 - - Taiwan_Geocode_103 - 1001503 - - - 花蓮縣豐濱鄉 - - Taiwan_Geocode_103 - 1001508 - - - 花蓮縣瑞穗鄉 - - Taiwan_Geocode_103 - 1001509 - - - 高雄市那瑪夏區 - - Taiwan_Geocode_103 - 6403800 - - - 臺南市玉井區 - - Taiwan_Geocode_103 - 6702300 - - - 臺南市新市區 - - Taiwan_Geocode_103 - 6702000 - - - 臺南市安定區 - - Taiwan_Geocode_103 - 6702100 - - - 臺南市左鎮區 - - Taiwan_Geocode_103 - 6702600 - - - 臺南市仁德區 - - Taiwan_Geocode_103 - 6702700 - - - 臺南市楠西區 - - Taiwan_Geocode_103 - 6702400 - - - 臺南市南化區 - - Taiwan_Geocode_103 - 6702500 - - - 高雄市旗山區 - - Taiwan_Geocode_103 - 6403000 - - - 高雄市美濃區 - - Taiwan_Geocode_103 - 6403100 - - - 高雄市六龜區 - - Taiwan_Geocode_103 - 6403200 - - - 臺南市關廟區 - - Taiwan_Geocode_103 - 6702900 - - - 臺北市文山區 - - Taiwan_Geocode_103 - 6300800 - - - 高雄市內門區 - - Taiwan_Geocode_103 - 6403500 - - - 高雄市茂林區 - - Taiwan_Geocode_103 - 6403600 - - - 高雄市桃源區 - - Taiwan_Geocode_103 - 6403700 - - - 臺北市信義區 - - Taiwan_Geocode_103 - 6300200 - - - 南投縣水里鄉 - - Taiwan_Geocode_103 - 1000811 - - - 南投縣國姓鄉 - - Taiwan_Geocode_103 - 1000810 - - - 南投縣仁愛鄉 - - Taiwan_Geocode_103 - 1000813 - - - 南投縣信義鄉 - - Taiwan_Geocode_103 - 1000812 - - - 新竹縣北埔鄉 - - Taiwan_Geocode_103 - 1000409 - - - 高雄市苓雅區 - - Taiwan_Geocode_103 - 6400800 - - - 臺南市山上區 - - Taiwan_Geocode_103 - 6702200 + 1000210 新竹縣峨眉鄉 @@ -886,10 +347,10 @@ 1000411 - 新竹縣寶山鄉 + 臺北市北投區 Taiwan_Geocode_103 - 1000410 + 6301200 新竹縣五峰鄉 @@ -898,10 +359,10 @@ 1000413 - 新竹縣尖石鄉 + 新竹縣芎林鄉 Taiwan_Geocode_103 - 1000412 + 1000407 桃園市龍潭區 @@ -910,34 +371,16 @@ 6800900 - 高雄市林園區 + 桃園市八德區 Taiwan_Geocode_103 - 6401300 + 6800800 - 高雄市大社區 + 宜蘭縣蘇澳鎮 Taiwan_Geocode_103 - 6401600 - - - 高雄市仁武區 - - Taiwan_Geocode_103 - 6401700 - - - 高雄市大寮區 - - Taiwan_Geocode_103 - 6401400 - - - 高雄市大樹區 - - Taiwan_Geocode_103 - 6401500 + 1000203 桃園市桃園區 @@ -946,16 +389,16 @@ 6800100 - 高雄市鳥松區 + 桃園市大溪區 Taiwan_Geocode_103 - 6401800 + 6800300 - 高雄市岡山區 + 臺北市萬華區 Taiwan_Geocode_103 - 6401900 + 6300700 桃園市蘆竹區 @@ -981,6 +424,36 @@ Taiwan_Geocode_103 6301000 + + 新北市五股區 + + Taiwan_Geocode_103 + 6501500 + + + 新北市蘆洲區 + + Taiwan_Geocode_103 + 6501400 + + + 新北市林口區 + + Taiwan_Geocode_103 + 6501700 + + + 新北市泰山區 + + Taiwan_Geocode_103 + 6501600 + + + 新北市汐止區 + + Taiwan_Geocode_103 + 6501100 + 臺北市南港區 @@ -988,28 +461,40 @@ 6300900 - 臺南市歸仁區 + 新北市土城區 Taiwan_Geocode_103 - 6702800 + 6501300 - 新竹縣橫山鄉 + 新北市瑞芳區 Taiwan_Geocode_103 - 1000408 + 6501200 - 高雄市甲仙區 + 宜蘭縣南澳鄉 Taiwan_Geocode_103 - 6403300 + 1000212 - 桃園市觀音區 + 桃園市中壢區 Taiwan_Geocode_103 - 6801200 + 6800200 + + + 新北市石碇區 + + Taiwan_Geocode_103 + 6501900 + + + 新北市深坑區 + + Taiwan_Geocode_103 + 6501800 桃園市平鎮區 @@ -1018,136 +503,22 @@ 6801000 - 桃園市新屋區 + 新竹縣寶山鄉 Taiwan_Geocode_103 - 6801100 + 1000410 - 新竹縣竹東鎮 + 新北市淡水區 Taiwan_Geocode_103 - 1000402 + 6501000 - 新竹縣新埔鎮 + 新北市八里區 Taiwan_Geocode_103 - 1000403 - - - 高雄市前金區 - - Taiwan_Geocode_103 - 6400700 - - - 新竹縣竹北市 - - Taiwan_Geocode_103 - 1000401 - - - 新竹縣新豐鄉 - - Taiwan_Geocode_103 - 1000406 - - - 新竹縣芎林鄉 - - Taiwan_Geocode_103 - 1000407 - - - 新竹縣關西鎮 - - Taiwan_Geocode_103 - 1000404 - - - 新竹縣湖口鄉 - - Taiwan_Geocode_103 - 1000405 - - - 臺北市萬華區 - - Taiwan_Geocode_103 - 6300700 - - - 臺北市大安區 - - Taiwan_Geocode_103 - 6300300 - - - 高雄市左營區 - - Taiwan_Geocode_103 - 6400300 - - - 南投縣鹿谷鄉 - - Taiwan_Geocode_103 - 1000807 - - - 南投縣竹山鎮 - - Taiwan_Geocode_103 - 1000804 - - - 南投縣埔里鎮 - - Taiwan_Geocode_103 - 1000802 - - - 南投縣魚池鄉 - - Taiwan_Geocode_103 - 1000809 - - - 高雄市旗津區 - - Taiwan_Geocode_103 - 6401000 - - - 高雄市梓官區 - - Taiwan_Geocode_103 - 6402900 - - - 臺南市安平區 - - Taiwan_Geocode_103 - 6703600 - - - 高雄市鳳山區 - - Taiwan_Geocode_103 - 6401200 - - - 桃園市八德區 - - Taiwan_Geocode_103 - 6800800 - - - 花蓮縣吉安鄉 - - Taiwan_Geocode_103 - 1001505 + 6502300 diff --git a/BackendWorkerService/root/Typhoon.xml b/BackendWorkerService/root/Typhoon.xml index 02bdb86..9dd9749 100644 --- a/BackendWorkerService/root/Typhoon.xml +++ b/BackendWorkerService/root/Typhoon.xml @@ -1,64 +1,197 @@ - CWB-Weather_typhoon-warning_202305311430001 + CWB-Weather_typhoon-warning_202305311730001 weather@cwb.gov.tw - 2023-05-31T14:22:22+08:00 + 2023-05-31T17:17:30+08:00 Actual - Update + Cancel Public - weather@cwb.gov.tw,CWB-Weather_typhoon-warning_202305311130001,2023-05-31T11:24:11+08:00 + weather@cwb.gov.tw,CWB-Weather_typhoon-warning_202305311430001,2023-05-31T14:22:23+08:00 zh-TW Met 颱風 - Monitor - Future - Moderate - Likely + Past + Minor + Observed profile:CAP-TWP:Event:1.0 typhoon - 2023-05-31T14:30:00+08:00 - 2023-05-31T14:30:00+08:00 - 2023-05-31T18:30:00+08:00 + 2023-05-31T17:30:00+08:00 + 2023-05-31T17:30:00+08:00 + 2023-05-31T17:40:00+08:00 中央氣象局 - 海上颱風警報 + 解除颱風警報 -
15
SEA
2
MAWAR瑪娃22.20,125.203343970300中度颱風TYPHOON24.80,126.203038975250
中度颱風 瑪娃(國際命名 MAWAR)31日14時的中心位置在北緯 22.2 度,東經 125.2 度,即在鵝鑾鼻的東方約 440 公里之海面上。
中心氣壓 970 百帕,近中心最大風速每秒 33 公尺(約每小時 119 公里),相當於 12 級風,瞬間最大陣風每秒 43 公尺(約每小時 155 公里),相當於 14 級風,七級風暴風半徑 300 公里,十級風暴風半徑 100 公里。
以每小時14公里速度,向北北東進行,預測1日14時的中心位置在北緯 24.8 度,東經 126.2 度,即在宜蘭的東方約 450 公里之海面上。
根據最新資料顯示,第2號颱風中心目前在鵝鑾鼻東方海面,向北北東移動,其暴風圈正掠過臺灣東南部近海,對巴士海峽及臺灣東半部海面構成威脅。預計此颱風未來強度有稍減弱且暴風圈亦有縮小的趨勢。
巴士海峽、臺灣東南部海面(含蘭嶼、綠島)、臺灣東北部海面航行及作業船隻應嚴加戒備。
颱風外圍環流影響,易有短延時強降雨,今(31)日宜蘭縣、新北市及臺中市山區有局部大雨或豪雨發生,大臺北、桃園、新竹、花蓮地區及南投山區(奇萊山區)有局部大雨發生的機率,請注意強陣風,山區請注意落石及坍方。
*巴士海峽及臺灣附近各海面風浪明顯偏大;基隆北海岸、南部、東半部(含蘭嶼、綠島)、恆春半島沿海及澎湖、馬祖易有長浪發生,尤其東半部(含蘭嶼、綠島)、基隆北海岸、恆春半島沿海易有4至5米浪高,請避免前往海邊活動。*陸上強風特報:今(31)日臺南至苗栗沿海空曠地區及澎湖、蘭嶼、綠島易有9至10級強陣風,新竹以北、基隆北海岸、東半部沿海空曠地區、臺南至苗栗地區、恆春半島、金門、馬祖亦有較強陣風,請特別注意。*颱風外圍沉降影響,今(31日)白天臺南、高雄、屏東及金門有局部36度以上高溫出現的機率,請注意。*30日0時至31日14時出現較大累積雨量如下:宜蘭縣翠峰湖316.0毫米,臺中市南湖圈谷232.0毫米。*本警報單之颱風半徑為平均半徑,第2號颱風之7級風暴風半徑近似正圓,平均半徑約為300公里。颱風詳細特性請參考本局颱輔助說明(https://www.cwb.gov.tw/Data/typhoon/TY_PDF.pdf)。*若此颱風行徑無特殊變化,本局預計於今(31)日17時30分解除海上颱風警報。
+[颱風動態] +根據最新資料顯示,第2號颱風暴風半徑略為縮小,中心目前在鵝鑾鼻東北東方海面,向北北東轉東北移動,對巴士海峽及臺灣東半部近海威脅已解除。 + +[注意事項] +*巴士海峽及臺灣附近各海面風浪明顯偏大;基隆北海岸、南部、東半部(含蘭嶼、綠島)、恆春半島沿海及澎湖、馬祖易有長浪發生,尤其東半部(含蘭嶼、綠島)、基隆北海岸、恆春半島沿海易有4至5米浪高,請避免前往海邊活動。*陸上強風特報:今(31)日臺南至苗栗沿海空曠地區及澎湖、蘭嶼、綠島易有9至10級強陣風,新竹以北、基隆北海岸、東半部沿海空曠地區、臺南至苗栗地區、恆春半島、金門、馬祖亦有較強陣風,請特別注意。*30日0時至31日17時出現較大累積雨量如下:宜蘭縣翠峰湖334.5毫米,臺中市南湖圈谷257.0毫米。*本警報單之颱風半徑為平均半徑,第2號颱風之7級風暴風半徑近似正圓,平均半徑約為280公里。颱風詳細特性請參考本局颱輔助說明(https://www.cwb.gov.tw/Data/typhoon/TY_PDF.pdf)。*此為第2號颱風警報最後一次報告。 +
+ https://www.cwb.gov.tw/V8/C/P/Warning/FIFOWS.html alert_title 颱風警報 - - severity_level - 海上颱風警報 - - - alert_color - 橙色 - - - website_color - 255,0,0 - - - 巴士海峽東部 - 20.00,121.50 20.00,121.00 20.50,121.00 21.00,121.00 21.50,121.00 22.00,121.00 22.00,121.50 22.00,122.00 22.00,122.50 22.00,123.00 21.50,123.00 21.00,123.00 20.50,123.00 20.00,123.00 20.00,122.50 20.00,122.00 20.00,121.50 + 基隆市 + + Taiwan_Geocode_103 + 10017 + - 臺灣東北部海面 - 23.50,122.00 23.50,121.50 23.70,121.54 23.90,121.60 23.98,121.61 24.00,121.61 24.03,121.62 24.04,121.61 24.06,121.60 24.08,121.61 24.18,121.66 24.21,121.68 24.29,121.74 24.29,121.75 24.30,121.77 24.33,121.76 24.41,121.78 24.43,121.78 24.45,121.80 24.46,121.80 24.46,121.82 24.47,121.83 24.48,121.84 24.50,121.85 24.53,121.85 24.56,121.85 24.59,121.85 24.61,121.84 24.61,121.83 24.62,121.82 24.64,121.82 24.75,121.80 24.76,121.80 24.79,121.81 24.84,121.81 24.85,121.82 24.87,121.83 24.93,121.88 24.96,121.91 24.98,121.95 24.99,121.97 24.99,121.98 25.00,121.99 25.00,122.00 25.00,122.00 25.00,122.50 25.00,123.00 24.50,123.00 24.00,123.00 23.50,123.00 23.50,122.50 23.50,122.00 + 臺北市 + + Taiwan_Geocode_103 + 63 + - 臺灣東南部海面 - 22.00,121.50 22.00,121.00 22.00,120.86 22.04,120.88 22.10,120.88 22.23,120.88 22.28,120.87 22.29,120.87 22.32,120.87 22.34,120.88 22.50,120.94 22.54,120.96 22.57,120.97 22.61,121.00 22.64,121.01 22.66,121.03 22.71,121.08 22.72,121.11 22.72,121.12 22.73,121.13 22.74,121.14 22.76,121.17 22.90,121.26 23.00,121.31 23.09,121.36 23.33,121.45 23.40,121.47 23.41,121.48 23.42,121.48 23.49,121.50 23.50,121.50 23.50,122.00 23.50,122.50 23.50,123.00 23.00,123.00 22.50,123.00 22.00,123.00 22.00,122.50 22.00,122.00 22.00,121.50 + 新北市 + + Taiwan_Geocode_103 + 65 + + + + 桃園市 + + Taiwan_Geocode_103 + 68 + + + + 新竹市 + + Taiwan_Geocode_103 + 10018 + + + + 新竹縣 + + Taiwan_Geocode_103 + 10004 + + + + 苗栗縣 + + Taiwan_Geocode_103 + 10005 + + + + 臺中市 + + Taiwan_Geocode_103 + 66 + + + + 彰化縣 + + Taiwan_Geocode_103 + 10007 + + + + 雲林縣 + + Taiwan_Geocode_103 + 10009 + + + + 南投縣 + + Taiwan_Geocode_103 + 10008 + + + + 嘉義縣 + + Taiwan_Geocode_103 + 10010 + + + + 嘉義市 + + Taiwan_Geocode_103 + 10020 + + + + 臺南市 + + Taiwan_Geocode_103 + 67 + + + + 高雄市 + + Taiwan_Geocode_103 + 64 + + + + 屏東縣 + + Taiwan_Geocode_103 + 10013 + + + + 宜蘭縣 + + Taiwan_Geocode_103 + 10002 + + + + 花蓮縣 + + Taiwan_Geocode_103 + 10015 + + + + 臺東縣 + + Taiwan_Geocode_103 + 10014 + + + + 澎湖縣 + + Taiwan_Geocode_103 + 10016 + + + + 金門縣 + + Taiwan_Geocode_103 + 09020 + + + + 連江縣 + + Taiwan_Geocode_103 + 09007 +
-
\ No newline at end of file