This commit is contained in:
xiaolipro 2025-08-15 17:19:27 +08:00
parent 9870fb0ae7
commit 297e91cc04
26 changed files with 293 additions and 233 deletions

View File

@ -1,3 +0,0 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=dels/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=SJZY/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -18,7 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution items", "solution
NuGet.Config = NuGet.Config
.gitignore = .gitignore
.editorconfig = .editorconfig
global.json = global.json
EndProjectSection
EndProject
Global

View File

@ -1,9 +1,17 @@
FROM harbor.sjzy.local/images/dotnet:7.0
COPY publish/ /app/
COPY version.log /app/
COPY --chown=appuser:appuser publish/ /app/
COPY --chown=appuser:appuser version.log /app/
ENV ASPNETCORE_HOSTINGSTARTUPASSEMBLIES SJZY.Expand.ABP.Core.HostingStartup;SkyAPM.Agent.AspNetCore
ENV SKYWALKING__SERVICENAME Package_Track_Dev
WORKDIR /app
CMD ["/usr/bin/dotnet","SJZY.Track.Api.dll"]
FROM mcr.microsoft.com/dotnet/sdk:8.0.303 AS base
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY . /src
RUN dotnet restore "src/TrackCenter.Api/TrackCenter.Api.csproj"
WORKDIR "/src/src/TrackCenter.Api"
RUN dotnet build "TrackCenter.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM base AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "TrackCenter.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
# 使用运行时镜像
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "TrackCenter.Api.dll"]

View File

@ -1,15 +1,15 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"SJZY.Track.Api": {
"http": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "SJZY.Expand.ABP.Core",
"SKYWALKING__SERVICENAME": "Track"
},
"dotnetRunMessages": true,
"applicationUrl": "http://*:5056"
"launchBrowser": true,
"launchUrl": "swagger/index.html",
"applicationUrl": "http://localhost:8889",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
},
"$schema": "https://json.schemastore.org/launchsettings.json"
}
}

View File

@ -24,8 +24,11 @@
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="kafkalib\**\*" CopyToPublishDirectory="Always" CopyToOutputDirectory="Always" />
<Content Include="..\..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
<PropertyGroup>
<RetainVMGarbageCollection>true</RetainVMGarbageCollection>

View File

@ -13,6 +13,7 @@ using TrackCenter.Repository;
namespace TrackCenter.Api;
[DependsOn(typeof(FakeAutofacModule))]
[DependsOn(typeof(FakeAspNetCoreModule))]
[DependsOn(typeof(TrackCenterApplicationModule))]
[DependsOn(typeof(TrackCenterRepositoryModule))]
public class TrackCenterApiModule : FakeModule
@ -54,7 +55,13 @@ public class TrackCenterApiModule : FakeModule
public override void ConfigureApplication(ApplicationConfigureContext context)
{
var app = context.GetWebApplication();
var configuration = context.GetConfiguration();
app.Use(async (httpContext, next) =>
{
httpContext.Request.EnableBuffering();
await next.Invoke();
});
app.UseFakeExceptionHandling();
app.MapControllers();
// Configure the HTTP request pipeline.

View File

@ -20,11 +20,11 @@ public class CapFilter : SubscribeFilter
if (!string.IsNullOrEmpty(argumentJsonStr))
{
context.DeliverMessage.Headers.TryGetValue("sjzy-arguments-hash", out var hash);
context.DeliverMessage.Headers.TryGetValue("track-arguments-hash", out var hash);
if (hash.IsNullOrEmpty()) // 计算参数hash
{
hash = StringHelper.GetHash(argumentJsonStr);
context.DeliverMessage.Headers["sjzy-arguments-hash"] = hash;
context.DeliverMessage.Headers["track-arguments-hash"] = hash;
}
if (await RedisHelper.Client.ExistsAsync($"CapExecuteFilter:{hash}{DateTime.Now:G}"))
@ -36,7 +36,7 @@ public class CapFilter : SubscribeFilter
public override async Task OnSubscribeExecutedAsync(ExecutedContext context)
{
context.DeliverMessage.Headers.TryGetValue("sjzy-arguments-hash", out var hash);
context.DeliverMessage.Headers.TryGetValue("track-arguments-hash", out var hash);
if (!hash.IsNullOrEmpty())
{
await RedisHelper.Client.SetAsync($"CapExecuteFilter:{hash}{DateTime.Now:G}", 1, TimeSpan.FromMinutes(3));

View File

@ -8,7 +8,6 @@ public class LinkOrderInput
/// 应用
/// </summary>
[Required]
[Range(1, int.MaxValue)]
public string AppKey { get; set; }
/// <summary>
@ -33,7 +32,6 @@ public class LinkOrderInput
/// 关联应用
/// </summary>
[Required]
[Range(1, int.MaxValue)]
public string LinkAppKey { get; set; }
/// <summary>

View File

@ -11,14 +11,12 @@ public class OrderNoInfo
/// 应用
/// </summary>
[Required]
[Range(1, int.MaxValue)]
public string AppKey { get; set; }
/// <summary>
/// 订单类型,必填
/// </summary>
[Required]
[Range(1, int.MaxValue)]
public TrackOrderType OrderType { get; set; }
/// <summary>
@ -35,14 +33,12 @@ public class OrderNosInfo
/// 应用
/// </summary>
[Required]
[Range(1, int.MaxValue)]
public string AppKey { get; set; }
/// <summary>
/// 订单类型
/// </summary>
[Required]
[Range(1, int.MaxValue)]
public TrackOrderType OrderType { get; set; }
/// <summary>

View File

@ -6,12 +6,10 @@ public class QueryPushRecordInput
{
/// <summary> 应用 </summary>
[Required]
[Range(1, int.MaxValue)]
public string AppKey { get; set; }
/// <summary> 订单类型 </summary>
[Required]
[Range(1, int.MaxValue)]
public TrackOrderType OrderType { get; set; }
/// <summary> 订单号集合 </summary>

View File

@ -0,0 +1,91 @@
using FeishuNetSdk;
using FeishuNetSdk.Im;
using FeishuNetSdk.Im.Dtos;
using TrackCenter.Application.Helpers;
namespace TrackCenter.Application.Hangfire;
// 标题:预警通知
// 内容:尊敬的用户,截止到2024-12-13 10:00:01(UTC +8:00),轨迹中心监测到与您相关的预警风险信息共计299 条,请及时处理,详情如下:
// 运输时效预警共计100条,其中过去4小时内新增的预警10条,查看详情
// 回传时效预警共计99条,其中过去4小时内新增的预警10条,查看详情
// 轨迹完整预警共计100条,其中过去4小时内新增的预警10条,查看详情
public static class WarningNotifyBuilder
{
private const string Title = "预警通知";
// 构建飞书卡片发消息请求体
public static PostImV1MessagesBodyDto BuildFeiShuCard(WarningNotifyParam param, string fsUserId, string baseUrl)
{
var totalCount = param.TransportCount + param.ReturnCount + param.CompleteCount;
var body = new List<object>
{
new MarkdownElement(Content: $"尊敬的用户,截止到{DateTime.Now:yyyy-MM-dd HH:mm:ss}({BuildLocalTimezoneStr()}),轨迹中心监测到与您相关的预警风险信息共计{totalCount} 条,请及时处理,详情如下:", TextAlign: "left", TextSize: "normal_v2")
};
// 按需构建预警消息体
if (param.TransportCount > 0)
{
var url1 = WarningUrlBuilder.BuildUrl(baseUrl, WarningRuleType.运输时效预警, param.UserId);
body.Add(new MarkdownElement(Content: $"运输时效预警共计{param.TransportCount}条,其中过去{param.Hours}小时内新增的预警{param.TransportLastNHoursCount}条 [查看详情]({url1})", TextAlign: "left", TextSize: "normal_v2"));
}
if (param.ReturnCount > 0)
{
var url2 = WarningUrlBuilder.BuildUrl(baseUrl, WarningRuleType.回传时效预警, param.UserId);
body.Add(new MarkdownElement(Content: $"轨迹回传时效预警共计{param.ReturnCount}条,其中过去{param.Hours}小时内新增的预警{param.ReturnLastNHoursCount}条 [查看详情]({url2})", TextAlign: "left", TextSize: "normal_v2"));
}
if (param.CompleteCount > 0)
{
var url3 = WarningUrlBuilder.BuildUrl(baseUrl, WarningRuleType.完整预警, param.UserId);
body.Add(new MarkdownElement(Content: $"轨迹完整预警共计{param.CompleteCount}条,其中过去{param.Hours}小时内新增的预警{param.CompleteLastNHoursCount}条 [查看详情]({url3})", TextAlign: "left", TextSize: "normal_v2"));
}
var dto = new PostImV1MessagesBodyDto
{
MsgType = "interactive",
ReceiveId = fsUserId
};
dto.SetContent(new ElementsCardV2Dto
{
// 配置卡片全局行为
Config = new ElementsCardV2Dto.ConfigSuffix(),
Header = new ElementsCardV2Dto.HeaderSuffix()
{
Title = new HeaderTitleElement(Title),
Template = "red"
},
Body = new ElementsCardV2Dto.BodySuffix(body.ToArray()),
});
return dto;
}
// 构建本地时区字符串标识 e.g UTC +8:00
private static string BuildLocalTimezoneStr()
{
var offset = TimeZoneInfo.Local.BaseUtcOffset;
var flag = offset.Hours > 0 ? "+" : "";
var offsetStr = offset.ToString().RemovePostFix(":00");
return $"UTC {flag}{offsetStr}";
}
public static class WarningUrlBuilder
{
public const string TransportPath = "/trajectoryCenter/trajectoryWarningOrder/timeWaring";
public const string ReturnPath = "/trajectoryCenter/trajectoryWarningOrder/ReturnWarning";
public const string CompletePath = "/trajectoryCenter/trajectoryWarningOrder/CompleteWarning";
public static string BuildUrl(string baseUrl, WarningRuleType warningRuleType, int userId)
{
return warningRuleType switch
{
WarningRuleType.运输时效预警 => $"{baseUrl}{TransportPath}?userid={userId}",
WarningRuleType.回传时效预警 => $"{baseUrl}{ReturnPath}?userid={userId}",
WarningRuleType.完整预警 => $"{baseUrl}{CompletePath}?userid={userId}",
_ => throw new ArgumentOutOfRangeException()
};
}
}
}

View File

@ -1,76 +0,0 @@
using Hangfire;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using TrackCenter.Application.Helpers;
using TrackCenter.Application.Options;
namespace TrackCenter.Application.Hangfire;
/// <summary>
/// 预警任务组件初始化
/// tips:支持定时任务执行周期配置 动态更新并应用 √
/// </summary>
public class WarningTaskComponentBackgroundService : BackgroundService
{
private readonly IOptionsMonitor<TrackWarningTaskOptions> _optionsMonitor;
private TrackWarningTaskOptions? _oldOptions;
private readonly ILogger<WarningTaskComponentBackgroundService> _logger;
public WarningTaskComponentBackgroundService(IOptionsMonitor<TrackWarningTaskOptions> optionsMonitor, ILogger<WarningTaskComponentBackgroundService> logger)
{
_optionsMonitor = optionsMonitor;
_logger = logger;
// 防抖配置变更包装
var onChanged = DebounceHelper.WrapMethod(TimeSpan.FromSeconds(1), () => { ReloadConfig(optionsMonitor); });
optionsMonitor.OnChange(_ => onChanged.Debounce());
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
// 立即初始化一次
ReloadConfig(_optionsMonitor);
return Task.CompletedTask;
}
private void ReloadConfig(IOptionsMonitor<TrackWarningTaskOptions> optionsMonitor)
{
var newVal = optionsMonitor.CurrentValue;
_logger.LogInformation("扫描任务配置,预警通知扫描周期{a}h,回传重试推送任务扫描周期{b}min,运输预警明细扫描周期{c}min",
newVal.ScanWarningNotifyTaskHourInterval,
newVal.ScanReturnWarningRetryPushTaskMinuteInterval,
newVal.ScanTransportWarningDetailsGenerateTaskMinuteInterval);
if ((_oldOptions?.ScanWarningNotifyTaskHourInterval ?? 0) != newVal.ScanWarningNotifyTaskHourInterval)
{
var q = GlobalConst.TaskQueueLevelDict[_optionsMonitor.CurrentValue.ScanWarningNotifyTaskLevel];
// 预警通知任务扫描
RecurringJob.AddOrUpdate<IWarningTaskScanner>("Scan_WarningNotifyTask",
q,
x => x.ScanNotifyTaskAndSchedulingAsync(),
$"0 */{newVal.ScanWarningNotifyTaskHourInterval} * * *");
}
if ((_oldOptions?.ScanReturnWarningRetryPushTaskMinuteInterval ?? 0) != newVal.ScanReturnWarningRetryPushTaskMinuteInterval)
{
var q = GlobalConst.TaskQueueLevelDict[_optionsMonitor.CurrentValue.ScanReturnWarningRetryPushTaskLevel];
// 回传预警通知重试推送任务扫描
RecurringJob.AddOrUpdate<IWarningTaskScanner>("Scan_ReturnWarningRetryPushTask",
q,
x => x.ScanReturnRetryPushTaskAndSchedulingAsync(),
$"*/{newVal.ScanReturnWarningRetryPushTaskMinuteInterval} * * * *");
}
if ((_oldOptions?.ScanTransportWarningDetailsGenerateTaskMinuteInterval ?? 0) != newVal.ScanTransportWarningDetailsGenerateTaskMinuteInterval)
{
var q = GlobalConst.TaskQueueLevelDict[_optionsMonitor.CurrentValue.ScanTransportWarningDetailsGenerateTaskLevel];
// 运输预警明细生成任务扫描
RecurringJob.AddOrUpdate<IWarningTaskScanner>("Scan_TransportWarningDetailsGenerateTask",
q,
x => x.ScanTransportDetailsGenerateTaskAndSchedulingAsync(),
$"*/{newVal.ScanTransportWarningDetailsGenerateTaskMinuteInterval} * * * *");
}
_oldOptions = newVal;
}
}

View File

@ -29,7 +29,9 @@ public class WarningTaskScanner : IWarningTaskScanner, ITransientDependency
private readonly IWarningTaskService _warningTaskService;
private readonly IOptionsMonitor<TrackWarningTaskOptions> _optionsMonitor;
public WarningTaskScanner(IWarningTaskRepository warningTaskRepository, ITransportWarningHandler transportWarningHandler, WarningNotifyAggregator warningNotifyAggregator, IWarningTaskService warningTaskService, IOptionsMonitor<TrackWarningTaskOptions> optionsMonitor)
public WarningTaskScanner(IWarningTaskRepository warningTaskRepository,
ITransportWarningHandler transportWarningHandler, WarningNotifyAggregator warningNotifyAggregator,
IWarningTaskService warningTaskService, IOptionsMonitor<TrackWarningTaskOptions> optionsMonitor)
{
_warningTaskRepository = warningTaskRepository;
_transportWarningHandler = transportWarningHandler;
@ -67,7 +69,8 @@ public class WarningTaskScanner : IWarningTaskScanner, ITransientDependency
if (tasks.Count > 0)
{
var taskIds = tasks.Select(x => x.Id).ToList();
await _warningTaskRepository.CloseReturnWarningRetryPushTaskAsync(x => !x.IsClosed && taskIds.Contains(x.Id));
await _warningTaskRepository.CloseReturnWarningRetryPushTaskAsync(x =>
!x.IsClosed && taskIds.Contains(x.Id));
}
}
@ -83,6 +86,7 @@ public class WarningTaskScanner : IWarningTaskScanner, ITransientDependency
// 3.关闭预警生成任务
var taskIds = tasks.Select(x => x.Id).ToList();
await _warningTaskRepository.CloseTransportWarningDetailsGenerateTaskAsync(x => !x.IsClosed && taskIds.Contains(x.Id));
await _warningTaskRepository.CloseTransportWarningDetailsGenerateTaskAsync(x =>
!x.IsClosed && taskIds.Contains(x.Id));
}
}

View File

@ -20,41 +20,33 @@ public interface IWarningTaskService
Task CallTrackRetryPushAsync(ReturnWarningRetryPushTaskEntity entity, CancellationToken ct);
}
public class WarningTaskService : IWarningTaskService, ITransientDependency
public class WarningTaskService(
IOptionsMonitor<InterfaceRelayParamsConfigOptions> interfaceRelayOptions,
IOptionsMonitor<TrackWarningTaskOptions> warningTaskOptions,
IOptionsMonitor<TrackAppOptions> trackAppOptions,
IFeishuTenantApi feishuTenantApi,
IWarningDetailsEsRepository warningDetailsEsRepository,
IWarningTaskRepository warningTaskRepository,
InterfaceForwardService interfaceForwardService)
: IWarningTaskService, ITransientDependency
{
private readonly IOptionsMonitor<InterfaceRelayParamsConfigOptions> _interfaceRelayOptions;
private readonly IOptionsMonitor<TrackWarningTaskOptions> _warningTaskOptions;
private readonly IFeishuTenantApi _feishuTenantApi;
private readonly IWarningDetailsEsRepository _warningDetailsEsRepository;
private readonly IWarningTaskRepository _warningTaskRepository;
public WarningTaskService(
IOptionsMonitor<InterfaceRelayParamsConfigOptions> interfaceRelayOptions,
IOptionsMonitor<TrackWarningTaskOptions> warningTaskOptions,
IFeishuTenantApi feishuTenantApi,
IWarningDetailsEsRepository warningDetailsEsRepository,
IWarningTaskRepository warningTaskRepository)
{
_interfaceRelayOptions = interfaceRelayOptions;
_warningTaskOptions = warningTaskOptions;
_feishuTenantApi = feishuTenantApi;
_warningDetailsEsRepository = warningDetailsEsRepository;
_warningTaskRepository = warningTaskRepository;
}
#region 用户中心调用,通知(一次)
[AutomaticRetry(Attempts = 1)]
public async Task CallNotifyAsync(WarningNotifyParam param, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var options = _warningTaskOptions.CurrentValue;
var options = warningTaskOptions.CurrentValue;
var baseUrl = trackAppOptions.CurrentValue.WarningNotifyUrl;
// 发送站内信
// https://centerapi.test.shijizhongyun.com/userApi/index.html?urls.primaryName=%E6%B6%88%E6%81%AF%E8%AE%A2%E9%98%85%E5%AE%9E%E4%BE%8B
// MsgSubscribeInstance/SendMessageByTemplate
// 发送FeiShu卡片信息
// todo:发送FeiShu卡片信息
var fsUserId = "";
if (string.IsNullOrWhiteSpace(fsUserId))
throw new BusinessException(message: "查询用户中心失败:SendMessageByTemplateAsync Msg:未找到用户对应的飞书用户Id");
var dto = WarningNotifyBuilder.BuildFeiShuCard(param, fsUserId, baseUrl);
var res = await feishuTenantApi.PostImV1MessagesAsync("user_id", dto);
if (res.Code != 0)
throw new BusinessException(message: $"飞书调用失败:SendMessageByTemplateAsync Msg:{res.Msg}");
}
#endregion
@ -87,27 +79,27 @@ public class WarningTaskService : IWarningTaskService, ITransientDependency
using (Track2InterfaceRelayAppContext.Use(new Track2InterfaceRelayAppContext(task.AppCode)))
{
var upStreamCode = isAdd
? _interfaceRelayOptions.CurrentValue.PushTrackInterfaceCode
: _interfaceRelayOptions.CurrentValue.TrackUpdateInterfaceCode;
? interfaceRelayOptions.CurrentValue.PushTrackInterfaceCode
: interfaceRelayOptions.CurrentValue.TrackUpdateInterfaceCode;
// var r = await _interfaceForwardServiceV2.Service.BatchForwardThenPushToQueueAsync(
// upStreamCode: upStreamCode,
// serviceProviderCode: task.ServiceProviderCode,
// data: items,
// resSubscribeName: GlobalConst.TrackPushCompletedSubscribe,
// attach: Base64Helper.SerializeObject(task.OptionType));
var r = await interfaceForwardService.BatchForwardThenPushToQueueAsync(
upStreamCode: upStreamCode,
serviceProviderCode: task.ServiceProviderCode,
data: items,
resSubscribeName: GlobalConst.TrackPushCompletedSubscribe,
attach: Base64Helper.SerializeObject(task.OptionType));
// if (r.Code != GlobalConst.SuccessCode)
// {
// var msg = $"接口通调用失败:BatchForwardThenPushToQueueAsync" +
// $"RequestId: {r.RequestId};AppCode:{task.AppCode};" +
// $" serviceProviderCode:{task.ServiceProviderCode} Msg: {r.Msg}";
//
// // 处理调用接口通失败,直接处于完结状态
// await HandlerCallBusinessException(task, msg);
// retryCountChanged = true;
// throw new BusinessException(message: msg);
// }
if (r.Code != GlobalConst.SuccessCode)
{
var msg = $"接口通调用失败:BatchForwardThenPushToQueueAsync" +
$"RequestId: {r.RequestId};AppCode:{task.AppCode};" +
$" serviceProviderCode:{task.ServiceProviderCode} Msg: {r.Msg}";
// 处理调用接口通失败,直接处于完结状态
await HandlerCallBusinessException(task, msg);
retryCountChanged = true;
throw new BusinessException(message: msg);
}
}
}
catch (Exception e)
@ -121,7 +113,7 @@ public class WarningTaskService : IWarningTaskService, ITransientDependency
if (!retryCountChanged)
{
var warningDetailsIds = task.WarningDetailIds.Split(',').ToHashSet();
await _warningDetailsEsRepository.ReturnWarningDetailsRetryIncrementAsync(warningDetailsIds);
await warningDetailsEsRepository.ReturnWarningDetailsRetryIncrementAsync(warningDetailsIds);
}
}
}
@ -130,7 +122,7 @@ public class WarningTaskService : IWarningTaskService, ITransientDependency
{
var detailIds = task.WarningDetailIds.Split(',').ToHashSet();
// 1.查询处理(重试中/人工介入)的预警明细
var warningDetails = await _warningDetailsEsRepository.GetReturnWarningDetailsAsync(new GetReturnWarningDetailsVO
var warningDetails = await warningDetailsEsRepository.GetReturnWarningDetailsAsync(new GetReturnWarningDetailsVO
{
DetailsIds = detailIds
});
@ -157,8 +149,8 @@ public class WarningTaskService : IWarningTaskService, ITransientDependency
}
// 更新预警明细,添加回传报文记录
await _warningDetailsEsRepository.BulkUpdateReturnWarningDetailsAsync(warningDetails);
await _warningDetailsEsRepository.InsertReturnMessageRecordAsync(new ReturnMessageRecordIndex
await warningDetailsEsRepository.BulkUpdateReturnWarningDetailsAsync(warningDetails);
await warningDetailsEsRepository.InsertReturnMessageRecordAsync(new ReturnMessageRecordIndex
{
WarningDetailsIds = task.WarningDetailIds,
IsSuccess = false,
@ -168,7 +160,7 @@ public class WarningTaskService : IWarningTaskService, ITransientDependency
// 添加通知任务
if (addNotifyTasks.Count > 0)
await _warningTaskRepository.BulkInsertWarningNotifyTaskAsync(addNotifyTasks);
await warningTaskRepository.BulkInsertWarningNotifyTaskAsync(addNotifyTasks);
}
private static WarningNotifyTaskEntity BuildWarningNotifyTask(ReturnWarningDetailsIndex detail)

View File

@ -15,10 +15,14 @@ namespace TrackCenter.Application.Kafka;
/// </summary>
public sealed class WarningCalcBackgroundService : BackgroundService
{
private const int MinWarningCalcInterval = 10 * 1000;
private const int MaxWarningCalcBatchSize = 2000;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IConsumer<WarningSignalKey, WarningSignal> _consumer;
private readonly WarningSignalPublisher _warningSignalPublisher;
private TrackWarningTaskOptions _options;
private int _interval;
private int _batchSize;
public WarningCalcBackgroundService(
IOptionsMonitor<TrackWarningTaskOptions> optionsMonitor,
@ -31,7 +35,14 @@ public sealed class WarningCalcBackgroundService : BackgroundService
_consumer = consumer;
_warningSignalPublisher = warningSignalPublisher;
_options = optionsMonitor.CurrentValue;
optionsMonitor.OnChange(x => _options = x);
_interval = Math.Max(MinWarningCalcInterval, _options.WarningCalcInterval);
_batchSize = Math.Min(MaxWarningCalcBatchSize, _options.WarningCalcBatchSize);
optionsMonitor.OnChange(newVal =>
{
_options = newVal;
_interval = Math.Max(MinWarningCalcInterval, _options.WarningCalcInterval);
_batchSize = Math.Min(MaxWarningCalcBatchSize, _options.WarningCalcBatchSize);
});
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@ -39,7 +50,7 @@ public sealed class WarningCalcBackgroundService : BackgroundService
while (!stoppingToken.IsCancellationRequested)
{
await DoExecuteAsync(stoppingToken);
await Task.Delay(_options.WarningCalcInterval, stoppingToken);
await Task.Delay(_interval, stoppingToken);
}
}
@ -75,7 +86,7 @@ public sealed class WarningCalcBackgroundService : BackgroundService
private async ValueTask FetchAndHandlerAsync(CancellationToken ct)
{
// 500ms或者提取到1000条消息任意以条件到达既可返回
var list = _consumer.ConsumeBatch(_options.WarningCalcBatchSize, TimeSpan.FromMilliseconds(500)).ToList();
var list = _consumer.ConsumeBatch(_batchSize, TimeSpan.FromMilliseconds(500)).ToList();
// 没有提取到任何消息跳过
if (list.Count == 0) return;
@ -96,6 +107,48 @@ public sealed class WarningCalcBackgroundService : BackgroundService
// 提交内存中所有的偏移到服务器,适用于EnableAutoCommit为false的手动提交场景
_consumer.Commit();
// 动态调整消费速率
AdjustSettings(list.Count);
}
private void AdjustSettings(int actualFetchSize)
{
// MaxBatchSize 2000 MinBatchSize _options
// MinInterval 10s MaxInterval _options
// 实际拉取为批次大小,设置为最高速率
if (actualFetchSize >= _batchSize)
{
_interval = MinWarningCalcInterval;
_batchSize = MaxWarningCalcBatchSize;
return;
}
// 低负载场景:拉取量低于当前批次70%时
if (actualFetchSize < _batchSize * 0.7)
{
// 逐步缩小批次(每次减少15%)
_batchSize = Math.Max(_options.WarningCalcBatchSize, (int) (_batchSize * 0.85));
// 适度增加间隔(当前间隔+20%)
_interval = Math.Min(_options.WarningCalcInterval, (int) (_interval * 1.2));
return;
}
// 中等负载场景:拉取量处于70%-90%区间时
if (actualFetchSize < _batchSize * 0.9)
{
// 温和调整策略
_batchSize = Math.Min(MaxWarningCalcBatchSize, (int) (_batchSize * 1.05)); // 缓慢扩容
_interval = Math.Max(MinWarningCalcInterval, (int) (_interval * 0.95)); // 适度缩短间隔
return;
}
// 高负载临界:拉取量接近当前批次大小时
// 直接扩容5%并缩短间隔
_batchSize = Math.Min(MaxWarningCalcBatchSize, (int) (_batchSize * 1.05));
_interval = Math.Max(MinWarningCalcInterval, (int) (_interval * 0.9));
}
private async ValueTask MessageHandler(IEnumerable<Message<WarningSignalKey, WarningSignal>> messages, CancellationToken ct)
@ -171,8 +224,10 @@ public sealed class WarningCalcBackgroundService : BackgroundService
}).ToArray();
await Task.WhenAll(tasks);
_activity?.SetTag("EndPublicRetryMessage", $"[预警计算] 已重新投递[{needRetryMetadata.Count}]条消息");
LogHelper.Warn($"[预警计算] 已重新投递[{needRetryMetadata.Count}]条消息", true);
var errorReasons = needRetryMetadata.Select(x => x.LastErrorReason).ToHashSet();
var errorMessage = "------------\r\n" + string.Join("\r\n------------\r\n", errorReasons);
_activity?.SetTag("EndPublicRetryMessage", $"[预警计算] 已重新投递[{needRetryMetadata.Count}]条消息\r\n{errorMessage}");
LogHelper.Warn($"[预警计算] 已重新投递[{needRetryMetadata.Count}]条消息\r\n{errorMessage}", true);
}
// 4.进入死信的消息,投入到死信队列,等待人工处理
@ -194,8 +249,10 @@ public sealed class WarningCalcBackgroundService : BackgroundService
}).ToArray();
await Task.WhenAll(tasks);
_activity?.SetTag("StartPublicDLXMessage", $"[预警计算] 已进入死信队列[{needEnqueueDlxMetadata.Count}]条消息");
LogHelper.Warn($"[预警计算] 已进入死信队列[{needEnqueueDlxMetadata.Count}]条消息", true);
var errorReasons = needRetryMetadata.Select(x => x.LastErrorReason).ToHashSet();
var errorMessage = "------------\r\n" + string.Join("\r\n------------\r\n", errorReasons);
_activity?.SetTag("StartPublicDLXMessage", $"[预警计算] 已进入死信队列[{needEnqueueDlxMetadata.Count}]条消息\r\n{errorMessage}");
LogHelper.Warn($"[预警计算] 已进入死信队列[{needEnqueueDlxMetadata.Count}]条消息\r\n{errorMessage}", true);
}
}

View File

@ -14,7 +14,6 @@ public static class WarningCalcServiceExtension
{
public static void AddWarningCalcServices(this IServiceCollection services, IConfiguration conf)
{
PrintKafkaLibVersion();
var env = "-" + conf.GetValue<string>("Environment")!.ToLower();
// 预警信号批处理后台任务
services.AddHostedService<WarningCalcBackgroundService>();
@ -167,20 +166,6 @@ public static class WarningCalcServiceExtension
return list;
}
public static void PrintKafkaLibVersion()
{
var loadSuccess = Library.Load(null);
Console.WriteLine($@"
+-------------------------------------------------------+
| |
| LibrdKafka |
| 加载:{loadSuccess},版本号:{Library.VersionString} |
| |
+-------------------------------------------------------+
");
}
}
internal sealed class JsonCoreSerializer<T> : ISerializer<T>, IDeserializer<T>

View File

@ -4,8 +4,6 @@ namespace TrackCenter.Application.Rabbit;
public class RabbitOptions
{
public string ConnectionString { get; set; }
public List<ConsumeOptions> Consumes { get; set; } = new();
public List<Action<IDeclaration>> Declarations { get; set; } = new();

View File

@ -3,7 +3,7 @@ using TrackCenter.Domain.VEntities;
namespace TrackCenter.Application.Track2InterfaceRelay;
public class InterfaceForwardService
public class InterfaceForwardService : ITransientDependency
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IBasicRepository<VAppEntity> _appRepository;
@ -30,12 +30,14 @@ public class InterfaceForwardService
{
throw new BusinessException(message: $"请在{nameof(Track2InterfaceRelayAppContext)}中调用接口通");
}
var key = Track2InterfaceRelayAppContext.Current.AppKey;
var secret = await _appRepository.GetFirstAsync(x => x.AppKey == key, x => x.AppSecret);
if (secret == null)
{
throw new BusinessException(message: $"未能找到相应app,请联系管理员");
}
content.Headers.Add(nameof(InterfaceForwardOptions.AppKey), key);
content.Headers.Add(nameof(InterfaceForwardOptions.AppSecret), secret);

View File

@ -10,20 +10,22 @@ using Hangfire;
using Hangfire.Redis.StackExchange;
using InterfaceForward.Domain.Shared.Helpers;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
using Savorboard.CAP.InMemoryMessageQueue;
using TrackCenter.Application.Kafka;
using TrackCenter.Application.CAP;
using TrackCenter.Application.Hangfire;
using TrackCenter.Application.HostServices;
using TrackCenter.Application.Options;
using TrackCenter.Application.Rabbit;
namespace TrackCenter.Application;
[DependsOn(typeof(FakeRabbitMqModule))]
[DependsOn(typeof(FakeAspNetCoreModule))]
[DependsOn(typeof(FakeObjectMappingAutoMapperModule))]
[DependsOn(typeof(FakeEventBusRabbitMqModule))]
[DependsOn(typeof(TrackCenterDomainModule))]
public class TrackCenterApplicationModule : FakeModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
@ -35,19 +37,21 @@ public class TrackCenterApplicationModule : FakeModule
options.ScanProfiles<TrackCenterApplicationModule>();
});
var rabbitOptions = configuration.GetSection("RabbitMQ").Get<FakeRabbitMqOptions>()!.Default;
//CAP
context.Services.AddSingleton<ISubscribeExecutor, MySubscribeExecutor>();
if (!System.Diagnostics.Debugger.IsAttached)
{
context.Services.AddCap(options =>
{
options.UseMySql(configuration["ConnectionStrings:DefaultConnection"]!);
options.UseMySql(configuration["ConnectionStrings:Default"]!);
options.UseRabbitMQ(configure =>
{
configure.HostName = configuration["RabbitMQ:HostName"]!;
configure.UserName = configuration["RabbitMQ:UserName"]!;
configure.Password = configuration["RabbitMQ:Password"]!;
configure.Port = configuration.GetValue<int>("RabbitMQ:Port");
configure.HostName = rabbitOptions.HostName;
configure.UserName = rabbitOptions.UserName;
configure.Password = rabbitOptions.Password;
configure.Port = rabbitOptions.Port;
configure.BasicQosOptions =
new RabbitMQOptions.BasicQos(configuration.GetValue<ushort>("CAP:PrefetchCount"));
});
@ -86,14 +90,12 @@ public class TrackCenterApplicationModule : FakeModule
capOptions.UseInMemoryMessageQueue();
capOptions.UseDashboard();
}).AddSubscribeFilter<CapFilter>();
Console.WriteLine("-- 当前为VS本地环境,用内存存储和内存队列");
LogHelper.Info("-- 当前为本地环境,用内存存储和内存队列");
}
context.Services.AddHostedService<RabbitClient>();
context.Services.Configure<RabbitOptions>(options =>
{
options.ConnectionString =
$"amqp://{configuration["RabbitMQ:UserName"]}:{configuration["RabbitMQ:Password"]}@{configuration["RabbitMQ:HostName"]}:{configuration["RabbitMQ:Port"]}";
options.Declarations = new List<Action<IDeclaration>>()
{
o => o.ExchangeDeclare(GlobalConst.TrackChangeExchange, ExchangeType.Topic, true),
@ -149,11 +151,11 @@ public class TrackCenterApplicationModule : FakeModule
$",defaultDatabase={configuration["Redis:Database"]}" +
$",abortConnect=false";
// 注册预警计算服务
context.Services.AddWarningCalcServices(configuration);
// todo:注册预警计算服务
//context.Services.AddWarningCalcServices(configuration);
// 注册预警任务后台服务
context.Services.AddHostedService<WarningTaskComponentBackgroundService>();
context.Services.AddHostedService<WarningTaskComponentHostedService>();
// 注册飞书服务(用于预警通知)
context.Services.AddFeishuNetSdk(o =>
@ -188,20 +190,7 @@ public class TrackCenterApplicationModule : FakeModule
context.Services.AddHttpClient("itfx", client =>
{
client.Timeout = TimeSpan.FromSeconds(20);
client.BaseAddress = new Uri(configuration["itfx:BaseAddress"]!);
client.BaseAddress = new Uri(configuration["TrackApp:ItfxBaseAddress"]!);
});
}
public override void PreConfigureServices(ServiceConfigurationContext context)
{
// PreConfigure<AbpHttpClientBuilderOptions>(options =>
// {
// options.ProxyClientActions.Add((_, _, httpClient) =>
// {
// //如果设置Token,前端需要传入Token 否则引发异常
// httpClient.SetTokenHeader();
// httpClient.Timeout = TimeSpan.FromMinutes(3);
// });
// });
}
}

View File

@ -1,3 +1,4 @@
using Fake.Domain.Entities;
using Fake.Domain.Entities.Auditing;
using TrackCenter.Domain.SeedWork;
@ -7,7 +8,8 @@ namespace TrackCenter.Domain.Aggregates.OrderAggregate;
/// 订单轨迹拉取状态表(1:N)
/// </summary>
[SugarTable("t_order_pull_status")]
public class OrderPullStatusEntity : FullAuditedEntity<int>
public class OrderPullStatusEntity : Entity<int>, IHasCreateUserId<int>, IHasCreateTime, IHasUpdateUserId<int>,
IHasUpdateTime
{
/// <summary>
/// 自增主键
@ -88,7 +90,7 @@ public class OrderPullStatusEntity : FullAuditedEntity<int>
public DateTime CreateTime { get; set; }
[SugarColumn(ColumnName = "UpdateTime")]
public DateTime? UpdateTime { get; set; }
public DateTime UpdateTime { get; set; }
[SugarColumn(ColumnName = "UpdateUserId")]
public int UpdateUserId { get; set; }

View File

@ -5,36 +5,36 @@ public partial class GlobalConst
/// <summary>
/// 轨迹明细 es索引名称(模板按月)
/// </summary>
public const string TrackDetailsIndex = "sjzy_track_details";
public const string TrackDetailsIndex = "track_details";
/// <summary>
/// 轨迹推送记录 es索引名称(模板按月)
/// </summary>
public const string TrackPushRecordIndex = "sjzy_track_push_records";
public const string TrackPushRecordIndex = "track_push_records";
/// <summary>
/// 订单操作记录 es索引名称(模板按月)
/// </summary>
public const string OrderOperationRecordIndex = "sjzy_track_order_operation_records";
public const string OrderOperationRecordIndex = "track_order_operation_records";
// todo:以下为新增需要配置索引模板(prod) 生产环境还需要建立索引!!
/// <summary>
/// 运输时效预警明细 es索引名称(模板按月)
/// </summary>
public const string TransportWarningDetailsIndex = "sjzy_track_transport_warning_details";
public const string TransportWarningDetailsIndex = "track_transport_warning_details";
/// <summary>
/// 回传时效预警明细 es索引名称(模板按月)
/// </summary>
public const string ReturnWarningDetailsIndex = "sjzy_track_return_warning_details";
public const string ReturnWarningDetailsIndex = "track_return_warning_details";
/// <summary>
/// 回传报文日志 es索引名称(模板按月)
/// </summary>
public const string ReturnMessageRecordIndex = "sjzy_track_return_message_records";
public const string ReturnMessageRecordIndex = "track_return_message_records";
/// <summary>
/// 完整预警明细 es索引名称(模板按月)
/// </summary>
public const string CompleteWarningDetailsIndex = "sjzy_track_complete_warning_details";
public const string CompleteWarningDetailsIndex = "track_complete_warning_details";
}

View File

@ -1,7 +0,0 @@
using Fake.Modularity;
namespace TrackCenter.Domain;
public class SjzyTrackDomainModule : FakeModule
{
}

View File

@ -0,0 +1,17 @@
using Fake.DomainDrivenDesign;
using Fake.Modularity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using TrackCenter.Domain.Extensions;
namespace TrackCenter.Domain;
[DependsOn(typeof(FakeDomainDrivenDesignModule))]
public class TrackCenterDomainModule : FakeModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
LogHelper.Init(configuration.GetSection("FeiShuNotice").Get<FeiShuNoticeOptions>()!);
}
}

View File

@ -804,7 +804,7 @@ public class TrackDetailsEsRepository : ElasticSearchContextAbstract<TrackDetail
public async Task<TrackDetailsIndex?> GetDetailAsync(string orderNo, string trackCode, TrackOrderType orderType, string appKey, DateTime trackTime)
{
var res = await Context.SearchAsync<TrackDetailsIndex>(s => s
.Index("sjzy_track_details")
.Index(GlobalConst.TrackDetailsIndex)
.Query(q => q
.Bool(b => b
.Must(mu => mu