feat(order): 问题件/理赔件标记、通知投递与自动触发
新增 mark-problematic/mark-claimed/handle-status 接口,异常事件审计表与占位符渲染;inApp/email 通知及下单/入库/出库自动触发运行时。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
9359c9cb5b
commit
90f6c25b04
40
db.sql
40
db.sql
@ -75,6 +75,19 @@ CREATE TABLE `t_order` (
|
|||||||
`SalesProductRulesVersion` VARCHAR(64) NULL COMMENT '销售产品规则快照版本',
|
`SalesProductRulesVersion` VARCHAR(64) NULL COMMENT '销售产品规则快照版本',
|
||||||
`SalesProductPriceVersion` VARCHAR(64) NULL COMMENT '销售产品价格快照版本',
|
`SalesProductPriceVersion` VARCHAR(64) NULL COMMENT '销售产品价格快照版本',
|
||||||
`OrderStatus` VARCHAR(32) NOT NULL COMMENT '订单状态',
|
`OrderStatus` VARCHAR(32) NOT NULL COMMENT '订单状态',
|
||||||
|
`HandleStatus` VARCHAR(16) NULL COMMENT '处理子状态:pending/processed',
|
||||||
|
`IssueType` VARCHAR(32) NULL COMMENT '问题类型',
|
||||||
|
`IssueTitle` VARCHAR(128) NULL COMMENT '问题标题',
|
||||||
|
`IssueContent` TEXT NULL COMMENT '问题内容',
|
||||||
|
`IssueTemplateId` INT NULL COMMENT '问题模板Id',
|
||||||
|
`IssueRemark` VARCHAR(512) NULL COMMENT '问题备注',
|
||||||
|
`IssueTime` DATETIME NULL COMMENT '标记问题件时间',
|
||||||
|
`ClaimType` VARCHAR(32) NULL COMMENT '理赔类型',
|
||||||
|
`ClaimAmount` DECIMAL(18,4) NULL COMMENT '索赔金额',
|
||||||
|
`ClaimCurrency` VARCHAR(16) NULL COMMENT '索赔币种',
|
||||||
|
`ClaimReason` VARCHAR(512) NULL COMMENT '理赔原因',
|
||||||
|
`ClaimTime` DATETIME NULL COMMENT '创建理赔件时间',
|
||||||
|
`PreviousOrderStatus` VARCHAR(32) NULL COMMENT '进入异常状态前的订单状态',
|
||||||
`WarehouseId` INT NULL COMMENT '仓库ID',
|
`WarehouseId` INT NULL COMMENT '仓库ID',
|
||||||
`CustomerId` INT NULL COMMENT '客户ID',
|
`CustomerId` INT NULL COMMENT '客户ID',
|
||||||
`SenderId` INT NULL COMMENT '发件人ID',
|
`SenderId` INT NULL COMMENT '发件人ID',
|
||||||
@ -307,6 +320,33 @@ CREATE TABLE `t_order_fee` (
|
|||||||
KEY `idx_ord_fee_price_no` (`PriceNo`)
|
KEY `idx_ord_fee_price_no` (`PriceNo`)
|
||||||
) COMMENT='订单费用表';
|
) COMMENT='订单费用表';
|
||||||
|
|
||||||
|
CREATE TABLE `t_order_exception_event` (
|
||||||
|
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`OrderId` INT NOT NULL COMMENT '订单Id',
|
||||||
|
`EventType` VARCHAR(32) NOT NULL COMMENT 'mark_problematic/mark_claimed/handle_processed',
|
||||||
|
`FromStatus` VARCHAR(32) NULL COMMENT '变更前OrderStatus',
|
||||||
|
`ToStatus` VARCHAR(32) NULL COMMENT '变更后OrderStatus',
|
||||||
|
`FromHandleStatus` VARCHAR(16) NULL COMMENT '变更前HandleStatus',
|
||||||
|
`ToHandleStatus` VARCHAR(16) NULL COMMENT '变更后HandleStatus',
|
||||||
|
`IssueType` VARCHAR(32) NULL COMMENT '问题类型',
|
||||||
|
`Title` VARCHAR(128) NULL COMMENT '标题快照',
|
||||||
|
`Content` TEXT NULL COMMENT '内容快照',
|
||||||
|
`IssueTemplateId` INT NULL COMMENT '模板Id',
|
||||||
|
`NotifyMethod` VARCHAR(64) NULL COMMENT '通知方式快照',
|
||||||
|
`ClaimType` VARCHAR(32) NULL COMMENT '理赔类型',
|
||||||
|
`ClaimAmount` DECIMAL(18,4) NULL COMMENT '索赔金额',
|
||||||
|
`ClaimCurrency` VARCHAR(16) NULL COMMENT '索赔币种',
|
||||||
|
`ClaimReason` VARCHAR(512) NULL COMMENT '理赔原因',
|
||||||
|
`Remark` VARCHAR(512) NULL COMMENT '操作备注',
|
||||||
|
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||||
|
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '操作人',
|
||||||
|
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
|
||||||
|
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||||
|
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`Id`),
|
||||||
|
KEY `idx_oee_order_time` (`OrderId`, `CreateTime`)
|
||||||
|
) COMMENT='订单异常事件表';
|
||||||
|
|
||||||
CREATE TABLE `t_sales_product` (
|
CREATE TABLE `t_sales_product` (
|
||||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1',
|
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1',
|
||||||
`ProductCode` VARCHAR(64) NOT NULL COMMENT '产品编码',
|
`ProductCode` VARCHAR(64) NOT NULL COMMENT '产品编码',
|
||||||
|
|||||||
27
sql/migrate_t_order_exception_event.sql
Normal file
27
sql/migrate_t_order_exception_event.sql
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
-- 订单异常事件审计表
|
||||||
|
CREATE TABLE IF NOT EXISTS `t_order_exception_event` (
|
||||||
|
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`OrderId` INT NOT NULL COMMENT '订单Id',
|
||||||
|
`EventType` VARCHAR(32) NOT NULL COMMENT 'mark_problematic/mark_claimed/handle_processed',
|
||||||
|
`FromStatus` VARCHAR(32) NULL COMMENT '变更前OrderStatus',
|
||||||
|
`ToStatus` VARCHAR(32) NULL COMMENT '变更后OrderStatus',
|
||||||
|
`FromHandleStatus` VARCHAR(16) NULL COMMENT '变更前HandleStatus',
|
||||||
|
`ToHandleStatus` VARCHAR(16) NULL COMMENT '变更后HandleStatus',
|
||||||
|
`IssueType` VARCHAR(32) NULL COMMENT '问题类型',
|
||||||
|
`Title` VARCHAR(128) NULL COMMENT '标题快照',
|
||||||
|
`Content` TEXT NULL COMMENT '内容快照',
|
||||||
|
`IssueTemplateId` INT NULL COMMENT '模板Id',
|
||||||
|
`NotifyMethod` VARCHAR(64) NULL COMMENT '通知方式快照',
|
||||||
|
`ClaimType` VARCHAR(32) NULL COMMENT '理赔类型',
|
||||||
|
`ClaimAmount` DECIMAL(18,4) NULL COMMENT '索赔金额',
|
||||||
|
`ClaimCurrency` VARCHAR(16) NULL COMMENT '索赔币种',
|
||||||
|
`ClaimReason` VARCHAR(512) NULL COMMENT '理赔原因',
|
||||||
|
`Remark` VARCHAR(512) NULL COMMENT '操作备注',
|
||||||
|
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||||
|
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '操作人',
|
||||||
|
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
|
||||||
|
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||||
|
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`Id`),
|
||||||
|
KEY `idx_oee_order_time` (`OrderId`, `CreateTime`)
|
||||||
|
) COMMENT='订单异常事件表';
|
||||||
14
sql/migrate_t_order_exception_fields.sql
Normal file
14
sql/migrate_t_order_exception_fields.sql
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
-- 问题件 / 理赔件快照字段
|
||||||
|
ALTER TABLE `t_order`
|
||||||
|
ADD COLUMN `IssueType` VARCHAR(32) NULL COMMENT '问题类型' AFTER `HandleStatus`,
|
||||||
|
ADD COLUMN `IssueTitle` VARCHAR(128) NULL COMMENT '问题标题' AFTER `IssueType`,
|
||||||
|
ADD COLUMN `IssueContent` TEXT NULL COMMENT '问题内容' AFTER `IssueTitle`,
|
||||||
|
ADD COLUMN `IssueTemplateId` INT NULL COMMENT '问题模板Id' AFTER `IssueContent`,
|
||||||
|
ADD COLUMN `IssueRemark` VARCHAR(512) NULL COMMENT '问题备注' AFTER `IssueTemplateId`,
|
||||||
|
ADD COLUMN `IssueTime` DATETIME NULL COMMENT '标记问题件时间' AFTER `IssueRemark`,
|
||||||
|
ADD COLUMN `ClaimType` VARCHAR(32) NULL COMMENT '理赔类型' AFTER `IssueTime`,
|
||||||
|
ADD COLUMN `ClaimAmount` DECIMAL(18,4) NULL COMMENT '索赔金额' AFTER `ClaimType`,
|
||||||
|
ADD COLUMN `ClaimCurrency` VARCHAR(16) NULL COMMENT '索赔币种' AFTER `ClaimAmount`,
|
||||||
|
ADD COLUMN `ClaimReason` VARCHAR(512) NULL COMMENT '理赔原因' AFTER `ClaimCurrency`,
|
||||||
|
ADD COLUMN `ClaimTime` DATETIME NULL COMMENT '创建理赔件时间' AFTER `ClaimReason`,
|
||||||
|
ADD COLUMN `PreviousOrderStatus` VARCHAR(32) NULL COMMENT '进入异常状态前的订单状态' AFTER `ClaimTime`;
|
||||||
12
sql/migrate_t_order_handle_status.sql
Normal file
12
sql/migrate_t_order_handle_status.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
-- 问题件 / 已理赔 子状态:pending-待处理 processed-已处理
|
||||||
|
ALTER TABLE `t_order`
|
||||||
|
ADD COLUMN `HandleStatus` VARCHAR(16) NULL DEFAULT 'pending' COMMENT '处理子状态:pending/processed' AFTER `OrderStatus`;
|
||||||
|
|
||||||
|
UPDATE `t_order`
|
||||||
|
SET `HandleStatus` = 'pending'
|
||||||
|
WHERE `HandleStatus` IS NULL
|
||||||
|
AND `OrderStatus` IN ('problematic', 'claimed');
|
||||||
|
|
||||||
|
UPDATE `t_order`
|
||||||
|
SET `HandleStatus` = NULL
|
||||||
|
WHERE `OrderStatus` NOT IN ('problematic', 'claimed');
|
||||||
56
src/Oms.Application/Dtos/Orders/OrderExceptionDtos.cs
Normal file
56
src/Oms.Application/Dtos/Orders/OrderExceptionDtos.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
namespace Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
|
public class OrderBatchActionFailureOutput
|
||||||
|
{
|
||||||
|
public int OrderId { get; set; }
|
||||||
|
|
||||||
|
public string? ServiceNo { get; set; }
|
||||||
|
|
||||||
|
public string Reason { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderBatchActionResultOutput
|
||||||
|
{
|
||||||
|
public int SuccessCount { get; set; }
|
||||||
|
|
||||||
|
public List<OrderBatchActionFailureOutput> Failures { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderMarkProblematicInput
|
||||||
|
{
|
||||||
|
public List<int> OrderIds { get; set; } = [];
|
||||||
|
|
||||||
|
public int? IssueTemplateId { get; set; }
|
||||||
|
|
||||||
|
public string? IssueType { get; set; }
|
||||||
|
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
public string? Content { get; set; }
|
||||||
|
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderMarkClaimedInput
|
||||||
|
{
|
||||||
|
public List<int> OrderIds { get; set; } = [];
|
||||||
|
|
||||||
|
public string? ClaimType { get; set; }
|
||||||
|
|
||||||
|
public decimal? ClaimAmount { get; set; }
|
||||||
|
|
||||||
|
public string? ClaimCurrency { get; set; }
|
||||||
|
|
||||||
|
public string? ClaimReason { get; set; }
|
||||||
|
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderHandleStatusUpdateInput
|
||||||
|
{
|
||||||
|
public List<int> OrderIds { get; set; } = [];
|
||||||
|
|
||||||
|
public string? HandleStatus { get; set; }
|
||||||
|
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
namespace Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 问题件/已理赔子状态数量。
|
||||||
|
/// </summary>
|
||||||
|
public class OrderHandleStatusCountsOutput
|
||||||
|
{
|
||||||
|
public int Pending { get; set; }
|
||||||
|
|
||||||
|
public int Processed { get; set; }
|
||||||
|
|
||||||
|
public int Total => Pending + Processed;
|
||||||
|
}
|
||||||
@ -145,6 +145,44 @@ public class OrderOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Status { get; set; } = string.Empty;
|
public string Status { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 子状态 code:pending / processed。
|
||||||
|
/// </summary>
|
||||||
|
public string? HandleStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 子状态名称。
|
||||||
|
/// </summary>
|
||||||
|
public string? HandleStatusName { get; set; }
|
||||||
|
|
||||||
|
public string? IssueType { get; set; }
|
||||||
|
|
||||||
|
public string? IssueTypeName { get; set; }
|
||||||
|
|
||||||
|
public string? IssueTitle { get; set; }
|
||||||
|
|
||||||
|
public string? IssueContent { get; set; }
|
||||||
|
|
||||||
|
public int? IssueTemplateId { get; set; }
|
||||||
|
|
||||||
|
public string? IssueRemark { get; set; }
|
||||||
|
|
||||||
|
public DateTime? IssueTime { get; set; }
|
||||||
|
|
||||||
|
public string? ClaimType { get; set; }
|
||||||
|
|
||||||
|
public string? ClaimTypeName { get; set; }
|
||||||
|
|
||||||
|
public decimal? ClaimAmount { get; set; }
|
||||||
|
|
||||||
|
public string? ClaimCurrency { get; set; }
|
||||||
|
|
||||||
|
public string? ClaimReason { get; set; }
|
||||||
|
|
||||||
|
public DateTime? ClaimTime { get; set; }
|
||||||
|
|
||||||
|
public string? PreviousOrderStatus { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 收货时间。
|
/// 收货时间。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -24,6 +24,13 @@ public class OrderPaginatedInput
|
|||||||
[global::System.Text.Json.Serialization.JsonPropertyName("status")]
|
[global::System.Text.Json.Serialization.JsonPropertyName("status")]
|
||||||
public string? Status { get; set; }
|
public string? Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 子状态:all / pending / processed(仅问题件、已理赔 Tab 使用)。
|
||||||
|
/// </summary>
|
||||||
|
[Newtonsoft.Json.JsonProperty("handleStatus")]
|
||||||
|
[global::System.Text.Json.Serialization.JsonPropertyName("handleStatus")]
|
||||||
|
public string? HandleStatus { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 关键词。
|
/// 关键词。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -14,4 +14,10 @@ public class OrderStatusCountsOutput
|
|||||||
/// 按 OrderStatus 分组的数量,key 为状态码。
|
/// 按 OrderStatus 分组的数量,key 为状态码。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, int> Counts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, int> Counts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 问题件/已理赔子状态数量,key 为 OrderStatus(problematic/claimed)。
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, OrderHandleStatusCountsOutput> HandleCounts { get; set; } =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Oms.Application.Dtos.Operations;
|
using Oms.Application.Dtos.Operations;
|
||||||
using Oms.Application.Dtos.Shared;
|
using Oms.Application.Dtos.Shared;
|
||||||
using Oms.Application.Services.Orders;
|
using Oms.Application.Services.Orders;
|
||||||
|
using Oms.Application.Services.Orders.Issues;
|
||||||
using Oms.Application.Shared;
|
using Oms.Application.Shared;
|
||||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
using Oms.Domain.OperationsAggregate.Entities;
|
using Oms.Domain.OperationsAggregate.Entities;
|
||||||
@ -24,6 +25,8 @@ public class InboundRecordService : ApplicationService
|
|||||||
private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>();
|
private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>();
|
||||||
private ICountryRepository CountryRepository => LazyServiceProvider.GetRequiredService<ICountryRepository>();
|
private ICountryRepository CountryRepository => LazyServiceProvider.GetRequiredService<ICountryRepository>();
|
||||||
private OrderService OrderService => LazyServiceProvider.GetRequiredService<OrderService>();
|
private OrderService OrderService => LazyServiceProvider.GetRequiredService<OrderService>();
|
||||||
|
private AutoTriggerRuntimeService AutoTriggerRuntimeService =>
|
||||||
|
LazyServiceProvider.GetRequiredService<AutoTriggerRuntimeService>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 按单号查询订单(入库扫描展示销售产品、目的国,只读)。
|
/// 按单号查询订单(入库扫描展示销售产品、目的国,只读)。
|
||||||
@ -130,6 +133,20 @@ public class InboundRecordService : ApplicationService
|
|||||||
input.Weight);
|
input.Weight);
|
||||||
|
|
||||||
var savedEntity = await InboundRecordRepository.InsertAsync(entity);
|
var savedEntity = await InboundRecordRepository.InsertAsync(entity);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var refreshed = await OrderRepository.GetByWaybillNoAsync(input.WaybillNo.Trim());
|
||||||
|
if (refreshed != null)
|
||||||
|
{
|
||||||
|
await AutoTriggerRuntimeService.FireAsync(refreshed, AutoTriggerTiming.Inbound, operatorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 自动触发失败不影响入库主流程
|
||||||
|
}
|
||||||
|
|
||||||
return ObjectMapper.Map<InboundRecord, InboundRecordOutput>(savedEntity);
|
return ObjectMapper.Map<InboundRecord, InboundRecordOutput>(savedEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Oms.Application.Dtos.Operations;
|
using Oms.Application.Dtos.Operations;
|
||||||
using Oms.Application.Dtos.Shared;
|
using Oms.Application.Dtos.Shared;
|
||||||
using Oms.Application.Services.Orders;
|
using Oms.Application.Services.Orders;
|
||||||
|
using Oms.Application.Services.Orders.Issues;
|
||||||
using Oms.Application.Shared;
|
using Oms.Application.Shared;
|
||||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
using Oms.Domain.OperationsAggregate.Entities;
|
using Oms.Domain.OperationsAggregate.Entities;
|
||||||
@ -28,6 +29,8 @@ public class OutboundRecordService : ApplicationService
|
|||||||
private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>();
|
private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>();
|
||||||
private ICountryRepository CountryRepository => LazyServiceProvider.GetRequiredService<ICountryRepository>();
|
private ICountryRepository CountryRepository => LazyServiceProvider.GetRequiredService<ICountryRepository>();
|
||||||
private OrderService OrderService => LazyServiceProvider.GetRequiredService<OrderService>();
|
private OrderService OrderService => LazyServiceProvider.GetRequiredService<OrderService>();
|
||||||
|
private AutoTriggerRuntimeService AutoTriggerRuntimeService =>
|
||||||
|
LazyServiceProvider.GetRequiredService<AutoTriggerRuntimeService>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 按单号查询订单(出库扫描展示销售产品、目的国、服务渠道,只读)。
|
/// 按单号查询订单(出库扫描展示销售产品、目的国、服务渠道,只读)。
|
||||||
@ -157,6 +160,20 @@ public class OutboundRecordService : ApplicationService
|
|||||||
input.Weight);
|
input.Weight);
|
||||||
|
|
||||||
var savedEntity = await OutboundRecordRepository.InsertAsync(entity);
|
var savedEntity = await OutboundRecordRepository.InsertAsync(entity);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var refreshed = await OrderRepository.GetByWaybillNoAsync(input.WaybillNo.Trim());
|
||||||
|
if (refreshed != null)
|
||||||
|
{
|
||||||
|
await AutoTriggerRuntimeService.FireAsync(refreshed, AutoTriggerTiming.Outbound, operatorId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 自动触发失败不影响出库主流程
|
||||||
|
}
|
||||||
|
|
||||||
return ObjectMapper.Map<OutboundRecord, OutboundRecordOutput>(savedEntity);
|
return ObjectMapper.Map<OutboundRecord, OutboundRecordOutput>(savedEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,153 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Consts;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
|
||||||
|
namespace Oms.Application.Services.Orders.Issues;
|
||||||
|
|
||||||
|
public static class AutoTriggerConditionMatcher
|
||||||
|
{
|
||||||
|
public static bool Matches(Order order, string? triggerCondition)
|
||||||
|
{
|
||||||
|
var text = triggerCondition?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.StartsWith('{') && text.EndsWith('}'))
|
||||||
|
{
|
||||||
|
return MatchesJson(order, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lowered = text.ToLowerInvariant();
|
||||||
|
if (IssueTypeCodes.All.Contains(lowered))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ContainsKeyword(order, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolveIssueType(string? triggerCondition)
|
||||||
|
{
|
||||||
|
var text = triggerCondition?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return IssueTypeCodes.Other;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.StartsWith('{') && text.EndsWith('}'))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(text);
|
||||||
|
if (doc.RootElement.TryGetProperty("issueType", out var issueTypeElement))
|
||||||
|
{
|
||||||
|
var code = issueTypeElement.GetString()?.Trim().ToLowerInvariant();
|
||||||
|
if (!string.IsNullOrWhiteSpace(code) && IssueTypeCodes.All.Contains(code))
|
||||||
|
{
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lowered = text.ToLowerInvariant();
|
||||||
|
if (IssueTypeCodes.All.Contains(lowered))
|
||||||
|
{
|
||||||
|
return lowered;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IssueTypeCodes.Other;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<int> ParseNotifierUserIds(string? notifier)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(notifier))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return notifier
|
||||||
|
.Split([',', ';', ' ', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Select(x => int.TryParse(x, out var id) ? id : 0)
|
||||||
|
.Where(x => x > 0)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesJson(Order order, string json)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
if (root.TryGetProperty("orderStatus", out var statusElement))
|
||||||
|
{
|
||||||
|
var expected = statusElement.GetString()?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(expected)
|
||||||
|
&& !string.Equals(order.OrderStatus, expected, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("productCode", out var productElement))
|
||||||
|
{
|
||||||
|
var expected = productElement.GetString()?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(expected)
|
||||||
|
&& !string.Equals(order.ProductCode?.Trim(), expected, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("channelCode", out var channelElement))
|
||||||
|
{
|
||||||
|
var expected = channelElement.GetString()?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(expected)
|
||||||
|
&& !string.Equals(order.ChannelCode?.Trim(), expected, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("keyword", out var keywordElement))
|
||||||
|
{
|
||||||
|
var keyword = keywordElement.GetString()?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword) && !ContainsKeyword(order, keyword))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return ContainsKeyword(order, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ContainsKeyword(Order order, string keyword)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Contains(order.ServiceNo, keyword)
|
||||||
|
|| Contains(order.ClientNo, keyword)
|
||||||
|
|| Contains(order.TrackNo, keyword)
|
||||||
|
|| Contains(order.ProductCode, keyword)
|
||||||
|
|| Contains(order.ChannelCode, keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Contains(string? source, string keyword) =>
|
||||||
|
!string.IsNullOrWhiteSpace(source)
|
||||||
|
&& source.Contains(keyword, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
@ -0,0 +1,129 @@
|
|||||||
|
using Fake.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Consts;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
using Oms.Domain.OrdersAggregate.Repositories;
|
||||||
|
using Oms.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
namespace Oms.Application.Services.Orders.Issues;
|
||||||
|
|
||||||
|
public class AutoTriggerRuntimeService(
|
||||||
|
IAutoTriggerRepository autoTriggerRepository,
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
OrderExceptionWorkflowService exceptionWorkflowService,
|
||||||
|
OrderIssueNotificationService issueNotificationService,
|
||||||
|
ILogger<AutoTriggerRuntimeService> logger) : IScopedDependency
|
||||||
|
{
|
||||||
|
public async Task FireAsync(Order order, AutoTriggerTiming timing, int operatorUserId)
|
||||||
|
{
|
||||||
|
if (order.Id <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var triggers = await autoTriggerRepository.GetListAsync(x => x.Status == EnableStatus.Enabled);
|
||||||
|
foreach (var trigger in triggers)
|
||||||
|
{
|
||||||
|
if (!AutoTriggerTimingCodes.Matches(trigger.TriggerTiming, timing))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AutoTriggerConditionMatcher.Matches(order, trigger.TriggerCondition))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ExecuteTriggerAsync(order, trigger, operatorUserId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "自动触发失败 OrderId={OrderId} TriggerId={TriggerId}", order.Id, trigger.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task FireByOrderIdAsync(int orderId, AutoTriggerTiming timing, int operatorUserId)
|
||||||
|
{
|
||||||
|
var order = await orderRepository.FirstOrDefaultAsync(x => x.Id == orderId);
|
||||||
|
if (order == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await FireAsync(order, timing, operatorUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteTriggerAsync(
|
||||||
|
Order order,
|
||||||
|
AutoTrigger trigger,
|
||||||
|
int operatorUserId)
|
||||||
|
{
|
||||||
|
var operation = trigger.Operation?.Trim();
|
||||||
|
if (string.Equals(operation, "拦截", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (order.IsIntercept == 0)
|
||||||
|
{
|
||||||
|
order.IsIntercept = 1;
|
||||||
|
await orderRepository.UpdateAsync(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(operation, "警告", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var title = string.IsNullOrWhiteSpace(trigger.PromptMessage)
|
||||||
|
? "订单预警"
|
||||||
|
: trigger.PromptMessage.Trim();
|
||||||
|
var content = string.IsNullOrWhiteSpace(trigger.TriggerCondition)
|
||||||
|
? $"运单 {{serviceNo}} 触发自动警告规则"
|
||||||
|
: trigger.TriggerCondition.Trim();
|
||||||
|
var context = IssuePlaceholderContext.FromOrder(order, null, title);
|
||||||
|
await issueNotificationService.DispatchAsync(
|
||||||
|
order,
|
||||||
|
IssuePlaceholderRenderer.Render(title, context),
|
||||||
|
IssuePlaceholderRenderer.Render(content, context),
|
||||||
|
IssueNotifyMethodCodes.InApp,
|
||||||
|
operatorUserId,
|
||||||
|
AutoTriggerConditionMatcher.ParseNotifierUserIds(trigger.Notifier));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(operation, "问题件", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var issueType = AutoTriggerConditionMatcher.ResolveIssueType(trigger.TriggerCondition);
|
||||||
|
var request = await exceptionWorkflowService.BuildRequestFromTemplateAsync(
|
||||||
|
order.OrganizationId,
|
||||||
|
issueType,
|
||||||
|
trigger.PromptMessage,
|
||||||
|
null,
|
||||||
|
$"自动触发#{trigger.Id}");
|
||||||
|
if (request == null)
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"自动触发问题件跳过:未匹配问题模板 OrderId={OrderId} TriggerId={TriggerId}",
|
||||||
|
order.Id,
|
||||||
|
trigger.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
request.ExtraReceiverUserIds = AutoTriggerConditionMatcher.ParseNotifierUserIds(trigger.Notifier);
|
||||||
|
var reason = await exceptionWorkflowService.TryMarkProblematicAsync(order, request, operatorUserId, true);
|
||||||
|
if (!string.IsNullOrWhiteSpace(reason))
|
||||||
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"自动触发问题件未执行 OrderId={OrderId} TriggerId={TriggerId} Reason={Reason}",
|
||||||
|
order.Id,
|
||||||
|
trigger.Id,
|
||||||
|
reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
namespace Oms.Application.Services.Orders.Issues;
|
||||||
|
|
||||||
|
public enum AutoTriggerTiming
|
||||||
|
{
|
||||||
|
OrderCreate,
|
||||||
|
Inbound,
|
||||||
|
Outbound
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AutoTriggerTimingCodes
|
||||||
|
{
|
||||||
|
public const string OrderCreate = "order_create";
|
||||||
|
public const string Inbound = "inbound";
|
||||||
|
public const string Outbound = "outbound";
|
||||||
|
|
||||||
|
public static bool Matches(string? triggerTiming, AutoTriggerTiming timing)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(triggerTiming))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = triggerTiming.Trim();
|
||||||
|
return timing switch
|
||||||
|
{
|
||||||
|
AutoTriggerTiming.OrderCreate => ContainsAny(text, "下单", "order", "create", "parcel", "预报"),
|
||||||
|
AutoTriggerTiming.Inbound => ContainsAny(text, "入库", "inbound", "receive", "收货"),
|
||||||
|
AutoTriggerTiming.Outbound => ContainsAny(text, "出库", "outbound", "ship", "发货"),
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ContainsAny(string text, params string[] keywords)
|
||||||
|
{
|
||||||
|
foreach (var keyword in keywords)
|
||||||
|
{
|
||||||
|
if (text.Contains(keyword, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
using Oms.Domain.ChannelsAggregate.Consts;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
|
||||||
|
namespace Oms.Application.Services.Orders.Issues;
|
||||||
|
|
||||||
|
public sealed class IssuePlaceholderContext
|
||||||
|
{
|
||||||
|
public string? ServiceNo { get; init; }
|
||||||
|
|
||||||
|
public string? ClientNo { get; init; }
|
||||||
|
|
||||||
|
public string? TrackNo { get; init; }
|
||||||
|
|
||||||
|
public string? ProductCode { get; init; }
|
||||||
|
|
||||||
|
public string? ChannelCode { get; init; }
|
||||||
|
|
||||||
|
public string? IssueType { get; init; }
|
||||||
|
|
||||||
|
public string? IssueTypeName { get; init; }
|
||||||
|
|
||||||
|
public string? IssueTitle { get; init; }
|
||||||
|
|
||||||
|
public static IssuePlaceholderContext FromOrder(Order order, string? issueType = null, string? issueTitle = null) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ServiceNo = order.ServiceNo,
|
||||||
|
ClientNo = order.ClientNo,
|
||||||
|
TrackNo = order.TrackNo,
|
||||||
|
ProductCode = order.ProductCode,
|
||||||
|
ChannelCode = order.ChannelCode,
|
||||||
|
IssueType = issueType,
|
||||||
|
IssueTypeName = IssueTypeCodes.GetDisplayName(issueType),
|
||||||
|
IssueTitle = issueTitle
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class IssuePlaceholderRenderer
|
||||||
|
{
|
||||||
|
private static readonly (string Key, Func<IssuePlaceholderContext, string?> Resolver)[] Placeholders =
|
||||||
|
[
|
||||||
|
("serviceNo", ctx => ctx.ServiceNo),
|
||||||
|
("clientNo", ctx => ctx.ClientNo),
|
||||||
|
("trackNo", ctx => ctx.TrackNo),
|
||||||
|
("productCode", ctx => ctx.ProductCode),
|
||||||
|
("channelCode", ctx => ctx.ChannelCode),
|
||||||
|
("issueType", ctx => ctx.IssueType),
|
||||||
|
("issueTypeName", ctx => ctx.IssueTypeName),
|
||||||
|
("issueTitle", ctx => ctx.IssueTitle),
|
||||||
|
("运单号", ctx => ctx.ServiceNo),
|
||||||
|
("客户单号", ctx => ctx.ClientNo),
|
||||||
|
("跟踪号", ctx => ctx.TrackNo),
|
||||||
|
("产品编码", ctx => ctx.ProductCode),
|
||||||
|
("渠道编码", ctx => ctx.ChannelCode),
|
||||||
|
("问题类型", ctx => ctx.IssueTypeName ?? ctx.IssueType),
|
||||||
|
("问题标题", ctx => ctx.IssueTitle),
|
||||||
|
];
|
||||||
|
|
||||||
|
public static string Render(string? template, IssuePlaceholderContext context)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(template))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = template;
|
||||||
|
foreach (var (key, resolver) in Placeholders)
|
||||||
|
{
|
||||||
|
var token = "{" + key + "}";
|
||||||
|
if (!result.Contains(token, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.Replace(token, resolver(context) ?? string.Empty, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,154 @@
|
|||||||
|
using Fake.DependencyInjection;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Consts;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
using Oms.Domain.OrdersAggregate.Repositories;
|
||||||
|
using Oms.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
namespace Oms.Application.Services.Orders.Issues;
|
||||||
|
|
||||||
|
public sealed class ProblematicMarkRequest
|
||||||
|
{
|
||||||
|
public string IssueType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int? IssueTemplateId { get; set; }
|
||||||
|
|
||||||
|
public string? NotifyMethod { get; set; }
|
||||||
|
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<int>? ExtraReceiverUserIds { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderExceptionWorkflowService(
|
||||||
|
IOrderRepository orderRepository,
|
||||||
|
IOrderExceptionEventRepository orderExceptionEventRepository,
|
||||||
|
IIssueTemplateRepository issueTemplateRepository,
|
||||||
|
OrderIssueNotificationService issueNotificationService) : IScopedDependency
|
||||||
|
{
|
||||||
|
public async Task<string?> TryMarkProblematicAsync(
|
||||||
|
Order order,
|
||||||
|
ProblematicMarkRequest request,
|
||||||
|
int operatorUserId,
|
||||||
|
bool sendNotification)
|
||||||
|
{
|
||||||
|
if (!OrderExceptionStatusRules.CanMarkException(order.OrderStatus))
|
||||||
|
{
|
||||||
|
return OrderExceptionStatusRules.GetForbiddenReason(order.OrderStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = IssuePlaceholderContext.FromOrder(order, request.IssueType, request.Title);
|
||||||
|
var renderedTitle = IssuePlaceholderRenderer.Render(request.Title, context);
|
||||||
|
var renderedContent = IssuePlaceholderRenderer.Render(request.Content, context);
|
||||||
|
|
||||||
|
var fromStatus = order.OrderStatus;
|
||||||
|
order.PreviousOrderStatus = fromStatus;
|
||||||
|
order.OrderStatus = OrderStatusCodes.Problematic;
|
||||||
|
order.HandleStatus = OrderHandleStatusCodes.Pending;
|
||||||
|
order.IssueType = request.IssueType;
|
||||||
|
order.IssueTitle = renderedTitle;
|
||||||
|
order.IssueContent = renderedContent;
|
||||||
|
order.IssueTemplateId = request.IssueTemplateId;
|
||||||
|
order.IssueRemark = request.Remark;
|
||||||
|
order.IssueTime = DateTime.Now;
|
||||||
|
order.ClaimType = null;
|
||||||
|
order.ClaimAmount = null;
|
||||||
|
order.ClaimCurrency = null;
|
||||||
|
order.ClaimReason = null;
|
||||||
|
order.ClaimTime = null;
|
||||||
|
|
||||||
|
await orderRepository.UpdateAsync(order);
|
||||||
|
await orderExceptionEventRepository.InsertAsync(new OrderExceptionEvent
|
||||||
|
{
|
||||||
|
OrderId = order.Id,
|
||||||
|
EventType = OrderExceptionEventTypeCodes.MarkProblematic,
|
||||||
|
FromStatus = fromStatus,
|
||||||
|
ToStatus = OrderStatusCodes.Problematic,
|
||||||
|
FromHandleStatus = null,
|
||||||
|
ToHandleStatus = OrderHandleStatusCodes.Pending,
|
||||||
|
IssueType = request.IssueType,
|
||||||
|
Title = renderedTitle,
|
||||||
|
Content = renderedContent,
|
||||||
|
IssueTemplateId = request.IssueTemplateId,
|
||||||
|
NotifyMethod = request.NotifyMethod,
|
||||||
|
Remark = request.Remark
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sendNotification)
|
||||||
|
{
|
||||||
|
await issueNotificationService.DispatchAsync(
|
||||||
|
order,
|
||||||
|
renderedTitle,
|
||||||
|
renderedContent,
|
||||||
|
request.NotifyMethod,
|
||||||
|
operatorUserId,
|
||||||
|
request.ExtraReceiverUserIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ProblematicMarkRequest?> BuildRequestFromTemplateAsync(
|
||||||
|
string? organizationId,
|
||||||
|
string issueType,
|
||||||
|
string? titleOverride,
|
||||||
|
string? contentOverride,
|
||||||
|
string? remark)
|
||||||
|
{
|
||||||
|
var normalizedType = issueType.Trim().ToLowerInvariant();
|
||||||
|
if (!IssueTypeCodes.All.Contains(normalizedType))
|
||||||
|
{
|
||||||
|
normalizedType = IssueTypeCodes.Other;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedOrg = NormalizeOrganizationId(organizationId);
|
||||||
|
var hasOrgFilter = normalizedOrg != null;
|
||||||
|
var orgValue = normalizedOrg ?? string.Empty;
|
||||||
|
|
||||||
|
var templates = await issueTemplateRepository.GetListAsync(x =>
|
||||||
|
x.Status == EnableStatus.Enabled
|
||||||
|
&& x.IssueType == normalizedType
|
||||||
|
&& (!hasOrgFilter
|
||||||
|
|| x.OrganizationId == orgValue
|
||||||
|
|| x.OrganizationId == null
|
||||||
|
|| x.OrganizationId == string.Empty));
|
||||||
|
|
||||||
|
var template = templates.FirstOrDefault(x => x.OrganizationId == normalizedOrg)
|
||||||
|
?? templates.FirstOrDefault(x => string.IsNullOrWhiteSpace(x.OrganizationId))
|
||||||
|
?? templates.FirstOrDefault();
|
||||||
|
|
||||||
|
var title = !string.IsNullOrWhiteSpace(titleOverride)
|
||||||
|
? titleOverride.Trim()
|
||||||
|
: template?.Title?.Trim();
|
||||||
|
var content = !string.IsNullOrWhiteSpace(contentOverride)
|
||||||
|
? contentOverride.Trim()
|
||||||
|
: template?.Content?.Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(content))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ProblematicMarkRequest
|
||||||
|
{
|
||||||
|
IssueType = normalizedType,
|
||||||
|
Title = title,
|
||||||
|
Content = content,
|
||||||
|
IssueTemplateId = template?.Id,
|
||||||
|
NotifyMethod = template == null ? null : IssueNotifyMethodCodes.Normalize(template.NotifyMethod),
|
||||||
|
Remark = remark
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeOrganizationId(string? organizationId)
|
||||||
|
{
|
||||||
|
var text = organizationId?.Trim();
|
||||||
|
return string.IsNullOrWhiteSpace(text) ? null : text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,154 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Fake.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Oms.Application.Services.System;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Consts;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
|
using Oms.Domain.CustomersAggregate.Entities;
|
||||||
|
using Oms.Domain.CustomersAggregate.Repositories;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
using Oms.Domain.SystemAggregate.Entities;
|
||||||
|
using Oms.Domain.SystemAggregate.Repositories;
|
||||||
|
using Oms.Application.Shared;
|
||||||
|
|
||||||
|
namespace Oms.Application.Services.Orders.Issues;
|
||||||
|
|
||||||
|
public class OrderIssueNotificationService(
|
||||||
|
INoticeRepository noticeRepository,
|
||||||
|
ICustomerRepository customerRepository,
|
||||||
|
ISystemBatchEmailAccountRepository batchEmailAccountRepository,
|
||||||
|
ILogger<OrderIssueNotificationService> logger) : IScopedDependency
|
||||||
|
{
|
||||||
|
private const string NoticeTypeMessage = "message";
|
||||||
|
private const string ReceiverTypeUser = "user";
|
||||||
|
|
||||||
|
public async Task DispatchAsync(
|
||||||
|
Order order,
|
||||||
|
string title,
|
||||||
|
string content,
|
||||||
|
string? notifyMethodCsv,
|
||||||
|
int operatorUserId,
|
||||||
|
IEnumerable<int>? extraReceiverUserIds = null)
|
||||||
|
{
|
||||||
|
var methods = IssueNotifyMethodCodes.ParseList(notifyMethodCsv);
|
||||||
|
if (methods.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Customer? customer = null;
|
||||||
|
if (order.CustomerId is int customerId && customerId > 0)
|
||||||
|
{
|
||||||
|
customer = await customerRepository.FirstOrDefaultAsync(x => x.Id == customerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var method in methods)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (method)
|
||||||
|
{
|
||||||
|
case IssueNotifyMethodCodes.InApp:
|
||||||
|
await SendInAppAsync(order, customer, title, content, extraReceiverUserIds);
|
||||||
|
break;
|
||||||
|
case IssueNotifyMethodCodes.Email:
|
||||||
|
await SendEmailAsync(order, customer, title, content, operatorUserId);
|
||||||
|
break;
|
||||||
|
case IssueNotifyMethodCodes.Sms:
|
||||||
|
logger.LogInformation("订单 {OrderId} 问题件通知:SMS 通道未接入,已跳过", order.Id);
|
||||||
|
break;
|
||||||
|
case IssueNotifyMethodCodes.Feishu:
|
||||||
|
logger.LogInformation("订单 {OrderId} 问题件通知:飞书通道未接入,已跳过", order.Id);
|
||||||
|
break;
|
||||||
|
case IssueNotifyMethodCodes.Wechat:
|
||||||
|
logger.LogInformation("订单 {OrderId} 问题件通知:微信模板通道未接入,已跳过", order.Id);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.LogWarning("订单 {OrderId} 问题件通知:未知方式 {Method},已跳过", order.Id, method);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "订单 {OrderId} 问题件通知发送失败,方式={Method}", order.Id, method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendInAppAsync(
|
||||||
|
Order order,
|
||||||
|
Customer? customer,
|
||||||
|
string title,
|
||||||
|
string content,
|
||||||
|
IEnumerable<int>? extraReceiverUserIds)
|
||||||
|
{
|
||||||
|
var receiverIds = new HashSet<int>();
|
||||||
|
if (customer is { UserId: > 0 })
|
||||||
|
{
|
||||||
|
receiverIds.Add(customer.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var userId in extraReceiverUserIds ?? [])
|
||||||
|
{
|
||||||
|
if (userId > 0)
|
||||||
|
{
|
||||||
|
receiverIds.Add(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receiverIds.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogInformation("订单 {OrderId} 站内信跳过:无有效接收用户", order.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await noticeRepository.InsertAsync(new Notice
|
||||||
|
{
|
||||||
|
NoticeType = NoticeTypeMessage,
|
||||||
|
Title = title,
|
||||||
|
Content = content,
|
||||||
|
IsPopup = 0,
|
||||||
|
ReceiverType = ReceiverTypeUser,
|
||||||
|
ReceiverIds = OmsDocHelper.SerializeJson(receiverIds.OrderBy(x => x).ToList()) ?? "[]"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendEmailAsync(
|
||||||
|
Order order,
|
||||||
|
Customer? customer,
|
||||||
|
string title,
|
||||||
|
string content,
|
||||||
|
int operatorUserId)
|
||||||
|
{
|
||||||
|
var email = customer?.Email?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
|
||||||
|
{
|
||||||
|
logger.LogInformation("订单 {OrderId} 邮件跳过:客户邮箱无效", order.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operatorUserId <= 0)
|
||||||
|
{
|
||||||
|
logger.LogInformation("订单 {OrderId} 邮件跳过:操作人无效", order.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var account = await batchEmailAccountRepository.GetByOwnerAsync(operatorUserId);
|
||||||
|
if (account == null || string.IsNullOrWhiteSpace(account.SmtpPassword))
|
||||||
|
{
|
||||||
|
logger.LogInformation("订单 {OrderId} 邮件跳过:操作人未配置 SMTP", order.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await BatchEmailSmtpHelper.SendAsync(
|
||||||
|
account.SmtpUsername,
|
||||||
|
account.SmtpPassword,
|
||||||
|
account.ServerType,
|
||||||
|
email,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
"text",
|
||||||
|
account.FromName);
|
||||||
|
}
|
||||||
|
}
|
||||||
386
src/Oms.Application/Services/Orders/OrderService.Exception.cs
Normal file
386
src/Oms.Application/Services/Orders/OrderService.Exception.cs
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
using Fake.Application;
|
||||||
|
using Fake.Domain.Exceptions;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Oms.Application.Dtos.Orders;
|
||||||
|
using Oms.Application.Services.Orders.Issues;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Consts;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
using Oms.Domain.OrdersAggregate.Repositories;
|
||||||
|
using Oms.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
namespace Oms.Application.Services.Orders;
|
||||||
|
|
||||||
|
public partial class OrderService
|
||||||
|
{
|
||||||
|
private const int MaxExceptionBatchSize = 200;
|
||||||
|
|
||||||
|
private IIssueTemplateRepository IssueTemplateRepository =>
|
||||||
|
LazyServiceProvider.GetRequiredService<IIssueTemplateRepository>();
|
||||||
|
|
||||||
|
private IOrderExceptionEventRepository OrderExceptionEventRepository =>
|
||||||
|
LazyServiceProvider.GetRequiredService<IOrderExceptionEventRepository>();
|
||||||
|
|
||||||
|
private OrderExceptionWorkflowService ExceptionWorkflowService =>
|
||||||
|
LazyServiceProvider.GetRequiredService<OrderExceptionWorkflowService>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量创建问题件。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("/order/mark-problematic")]
|
||||||
|
public async Task<OrderBatchActionResultOutput> MarkProblematicAsync([FromBody] OrderMarkProblematicInput input)
|
||||||
|
{
|
||||||
|
var payload = await ResolveProblematicPayloadAsync(input);
|
||||||
|
var request = new ProblematicMarkRequest
|
||||||
|
{
|
||||||
|
IssueType = payload.IssueType,
|
||||||
|
Title = payload.Title,
|
||||||
|
Content = payload.Content,
|
||||||
|
IssueTemplateId = payload.IssueTemplateId,
|
||||||
|
NotifyMethod = payload.NotifyMethod,
|
||||||
|
Remark = payload.Remark
|
||||||
|
};
|
||||||
|
|
||||||
|
return await ExecuteProblematicBatchAsync(input.OrderIds, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量创建理赔件。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("/order/mark-claimed")]
|
||||||
|
public async Task<OrderBatchActionResultOutput> MarkClaimedAsync([FromBody] OrderMarkClaimedInput input)
|
||||||
|
{
|
||||||
|
var payload = NormalizeClaimedPayload(input);
|
||||||
|
return await ExecuteExceptionBatchAsync(
|
||||||
|
input.OrderIds,
|
||||||
|
order =>
|
||||||
|
{
|
||||||
|
if (!OrderExceptionStatusRules.CanMarkException(order.OrderStatus))
|
||||||
|
{
|
||||||
|
return OrderExceptionStatusRules.GetForbiddenReason(order.OrderStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fromStatus = order.OrderStatus;
|
||||||
|
order.PreviousOrderStatus = fromStatus;
|
||||||
|
order.OrderStatus = OrderStatusCodes.Claimed;
|
||||||
|
order.HandleStatus = OrderHandleStatusCodes.Pending;
|
||||||
|
order.ClaimType = payload.ClaimType;
|
||||||
|
order.ClaimAmount = payload.ClaimAmount;
|
||||||
|
order.ClaimCurrency = payload.ClaimCurrency;
|
||||||
|
order.ClaimReason = payload.ClaimReason;
|
||||||
|
order.ClaimTime = DateTime.Now;
|
||||||
|
order.IssueType = null;
|
||||||
|
order.IssueTitle = null;
|
||||||
|
order.IssueContent = null;
|
||||||
|
order.IssueTemplateId = null;
|
||||||
|
order.IssueRemark = null;
|
||||||
|
order.IssueTime = null;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
(order, remark) => new OrderExceptionEvent
|
||||||
|
{
|
||||||
|
OrderId = order.Id,
|
||||||
|
EventType = OrderExceptionEventTypeCodes.MarkClaimed,
|
||||||
|
FromStatus = order.PreviousOrderStatus,
|
||||||
|
ToStatus = OrderStatusCodes.Claimed,
|
||||||
|
FromHandleStatus = null,
|
||||||
|
ToHandleStatus = OrderHandleStatusCodes.Pending,
|
||||||
|
ClaimType = payload.ClaimType,
|
||||||
|
ClaimAmount = payload.ClaimAmount,
|
||||||
|
ClaimCurrency = payload.ClaimCurrency,
|
||||||
|
ClaimReason = payload.ClaimReason,
|
||||||
|
Remark = remark
|
||||||
|
},
|
||||||
|
payload.Remark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量更新处理子状态。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("/order/handle-status/update")]
|
||||||
|
public async Task<OrderBatchActionResultOutput> UpdateHandleStatusAsync([FromBody] OrderHandleStatusUpdateInput input)
|
||||||
|
{
|
||||||
|
var targetStatus = input.HandleStatus?.Trim().ToLowerInvariant();
|
||||||
|
if (!string.Equals(targetStatus, OrderHandleStatusCodes.Processed, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
throw new BusinessException("仅支持标记为已处理");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await ExecuteExceptionBatchAsync(
|
||||||
|
input.OrderIds,
|
||||||
|
order =>
|
||||||
|
{
|
||||||
|
if (order.OrderStatus is not (OrderStatusCodes.Problematic or OrderStatusCodes.Claimed))
|
||||||
|
{
|
||||||
|
return "仅问题件或理赔件可更新处理状态";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OrderHandleStatusCodes.IsProcessed(order.HandleStatus))
|
||||||
|
{
|
||||||
|
return "订单已处理";
|
||||||
|
}
|
||||||
|
|
||||||
|
order.HandleStatus = OrderHandleStatusCodes.Processed;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
(order, remark) => new OrderExceptionEvent
|
||||||
|
{
|
||||||
|
OrderId = order.Id,
|
||||||
|
EventType = OrderExceptionEventTypeCodes.HandleProcessed,
|
||||||
|
FromStatus = order.OrderStatus,
|
||||||
|
ToStatus = order.OrderStatus,
|
||||||
|
FromHandleStatus = OrderHandleStatusCodes.Pending,
|
||||||
|
ToHandleStatus = OrderHandleStatusCodes.Processed,
|
||||||
|
Remark = remark
|
||||||
|
},
|
||||||
|
input.Remark?.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<OrderBatchActionResultOutput> ExecuteProblematicBatchAsync(
|
||||||
|
IReadOnlyCollection<int> orderIds,
|
||||||
|
ProblematicMarkRequest request)
|
||||||
|
{
|
||||||
|
var ids = NormalizeOrderIds(orderIds);
|
||||||
|
var result = new OrderBatchActionResultOutput();
|
||||||
|
var operatorUserId = CurrentUser.GetId<int>();
|
||||||
|
var sendNotification = !string.IsNullOrWhiteSpace(request.NotifyMethod);
|
||||||
|
|
||||||
|
foreach (var orderId in ids)
|
||||||
|
{
|
||||||
|
Order? order;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
order = await GetOrderOrThrowAsync(orderId);
|
||||||
|
}
|
||||||
|
catch (BusinessException ex)
|
||||||
|
{
|
||||||
|
result.Failures.Add(new OrderBatchActionFailureOutput
|
||||||
|
{
|
||||||
|
OrderId = orderId,
|
||||||
|
Reason = ex.Message
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reason = await ExceptionWorkflowService.TryMarkProblematicAsync(
|
||||||
|
order,
|
||||||
|
request,
|
||||||
|
operatorUserId,
|
||||||
|
sendNotification);
|
||||||
|
if (!string.IsNullOrWhiteSpace(reason))
|
||||||
|
{
|
||||||
|
result.Failures.Add(new OrderBatchActionFailureOutput
|
||||||
|
{
|
||||||
|
OrderId = order.Id,
|
||||||
|
ServiceNo = order.ServiceNo,
|
||||||
|
Reason = reason
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.SuccessCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<OrderBatchActionResultOutput> ExecuteExceptionBatchAsync(
|
||||||
|
IReadOnlyCollection<int> orderIds,
|
||||||
|
Func<Order, string?> tryApply,
|
||||||
|
Func<Order, string?, OrderExceptionEvent> buildEvent,
|
||||||
|
string? remark)
|
||||||
|
{
|
||||||
|
var ids = NormalizeOrderIds(orderIds);
|
||||||
|
var result = new OrderBatchActionResultOutput();
|
||||||
|
foreach (var orderId in ids)
|
||||||
|
{
|
||||||
|
Order? order;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
order = await GetOrderOrThrowAsync(orderId);
|
||||||
|
}
|
||||||
|
catch (BusinessException ex)
|
||||||
|
{
|
||||||
|
result.Failures.Add(new OrderBatchActionFailureOutput
|
||||||
|
{
|
||||||
|
OrderId = orderId,
|
||||||
|
Reason = ex.Message
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reason = tryApply(order);
|
||||||
|
if (!string.IsNullOrWhiteSpace(reason))
|
||||||
|
{
|
||||||
|
result.Failures.Add(new OrderBatchActionFailureOutput
|
||||||
|
{
|
||||||
|
OrderId = order.Id,
|
||||||
|
ServiceNo = order.ServiceNo,
|
||||||
|
Reason = reason
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await OrderRepository.UpdateAsync(order);
|
||||||
|
await OrderExceptionEventRepository.InsertAsync(buildEvent(order, remark));
|
||||||
|
result.SuccessCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<int> NormalizeOrderIds(IReadOnlyCollection<int>? orderIds)
|
||||||
|
{
|
||||||
|
if (orderIds == null || orderIds.Count == 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("请至少选择一条订单");
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids = orderIds.Where(x => x > 0).Distinct().ToList();
|
||||||
|
if (ids.Count == 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("请至少选择一条订单");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ids.Count > MaxExceptionBatchSize)
|
||||||
|
{
|
||||||
|
throw new BusinessException($"单次最多处理 {MaxExceptionBatchSize} 条订单");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ResolvedProblematicPayload> ResolveProblematicPayloadAsync(OrderMarkProblematicInput input)
|
||||||
|
{
|
||||||
|
IssueTemplate? template = null;
|
||||||
|
if (input.IssueTemplateId is int templateId && templateId > 0)
|
||||||
|
{
|
||||||
|
template = await IssueTemplateRepository.FirstOrDefaultAsync(x =>
|
||||||
|
x.Id == templateId && x.Status == EnableStatus.Enabled);
|
||||||
|
if (template == null)
|
||||||
|
{
|
||||||
|
throw new BusinessException("问题模板不存在或未启用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var issueType = (template?.IssueType ?? input.IssueType)?.Trim().ToLowerInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(issueType) || !IssueTypeCodes.All.Contains(issueType))
|
||||||
|
{
|
||||||
|
throw new BusinessException("问题类型不合法");
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = (input.Title?.Trim().Length > 0 ? input.Title.Trim() : template?.Title)?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(title))
|
||||||
|
{
|
||||||
|
throw new BusinessException("问题标题不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title.Length > 128)
|
||||||
|
{
|
||||||
|
throw new BusinessException("问题标题长度不能超过128");
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = (input.Content?.Trim().Length > 0 ? input.Content.Trim() : template?.Content)?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
{
|
||||||
|
throw new BusinessException("问题内容不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
var remark = input.Remark?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(remark) && remark.Length > 512)
|
||||||
|
{
|
||||||
|
throw new BusinessException("备注长度不能超过512");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ResolvedProblematicPayload
|
||||||
|
{
|
||||||
|
IssueType = issueType,
|
||||||
|
Title = title,
|
||||||
|
Content = content,
|
||||||
|
IssueTemplateId = template?.Id,
|
||||||
|
NotifyMethod = template == null ? null : IssueNotifyMethodCodes.Normalize(template.NotifyMethod),
|
||||||
|
Remark = remark
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResolvedClaimedPayload NormalizeClaimedPayload(OrderMarkClaimedInput input)
|
||||||
|
{
|
||||||
|
var claimType = input.ClaimType?.Trim().ToLowerInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(claimType) || !ClaimTypeCodes.All.Contains(claimType))
|
||||||
|
{
|
||||||
|
throw new BusinessException("理赔类型不合法");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.ClaimAmount is not > 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("索赔金额必须大于0");
|
||||||
|
}
|
||||||
|
|
||||||
|
var currency = input.ClaimCurrency?.Trim().ToUpperInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(currency))
|
||||||
|
{
|
||||||
|
throw new BusinessException("索赔币种不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currency.Length > 16)
|
||||||
|
{
|
||||||
|
throw new BusinessException("索赔币种长度不能超过16");
|
||||||
|
}
|
||||||
|
|
||||||
|
var reason = input.ClaimReason?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(reason))
|
||||||
|
{
|
||||||
|
throw new BusinessException("理赔原因不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reason.Length > 512)
|
||||||
|
{
|
||||||
|
throw new BusinessException("理赔原因长度不能超过512");
|
||||||
|
}
|
||||||
|
|
||||||
|
var remark = input.Remark?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(remark) && remark.Length > 512)
|
||||||
|
{
|
||||||
|
throw new BusinessException("备注长度不能超过512");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ResolvedClaimedPayload
|
||||||
|
{
|
||||||
|
ClaimType = claimType,
|
||||||
|
ClaimAmount = input.ClaimAmount.Value,
|
||||||
|
ClaimCurrency = currency,
|
||||||
|
ClaimReason = reason,
|
||||||
|
Remark = remark
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ResolvedProblematicPayload
|
||||||
|
{
|
||||||
|
public string IssueType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int? IssueTemplateId { get; set; }
|
||||||
|
|
||||||
|
public string? NotifyMethod { get; set; }
|
||||||
|
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ResolvedClaimedPayload
|
||||||
|
{
|
||||||
|
public string ClaimType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public decimal ClaimAmount { get; set; }
|
||||||
|
|
||||||
|
public string ClaimCurrency { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string ClaimReason { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ using Oms.Application.Dtos.Orders;
|
|||||||
using Oms.Application.Dtos.Shared;
|
using Oms.Application.Dtos.Shared;
|
||||||
using Oms.Application.Shared;
|
using Oms.Application.Shared;
|
||||||
using Oms.Domain.OrdersAggregate;
|
using Oms.Domain.OrdersAggregate;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Consts;
|
||||||
using Oms.Domain.OrdersAggregate.Consts;
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
using Oms.Domain.OrdersAggregate.Entities;
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
using Oms.Domain.SystemAggregate.Entities;
|
using Oms.Domain.SystemAggregate.Entities;
|
||||||
@ -125,6 +126,24 @@ public partial class OrderService
|
|||||||
CodSum = order.CodSum,
|
CodSum = order.CodSum,
|
||||||
OrderStatus = OmsDocHelper.GetOrderStatusName(order.OrderStatus),
|
OrderStatus = OmsDocHelper.GetOrderStatusName(order.OrderStatus),
|
||||||
Status = order.OrderStatus,
|
Status = order.OrderStatus,
|
||||||
|
HandleStatus = OrderHandleStatusCodes.IsProcessed(order.HandleStatus)
|
||||||
|
? OrderHandleStatusCodes.Processed
|
||||||
|
: OrderHandleStatusCodes.Pending,
|
||||||
|
HandleStatusName = OrderHandleStatusCodes.GetDisplayName(order.HandleStatus),
|
||||||
|
IssueType = order.IssueType,
|
||||||
|
IssueTypeName = IssueTypeCodes.GetDisplayName(order.IssueType),
|
||||||
|
IssueTitle = order.IssueTitle,
|
||||||
|
IssueContent = order.IssueContent,
|
||||||
|
IssueTemplateId = order.IssueTemplateId,
|
||||||
|
IssueRemark = order.IssueRemark,
|
||||||
|
IssueTime = order.IssueTime,
|
||||||
|
ClaimType = order.ClaimType,
|
||||||
|
ClaimTypeName = ClaimTypeCodes.GetDisplayName(order.ClaimType),
|
||||||
|
ClaimAmount = order.ClaimAmount,
|
||||||
|
ClaimCurrency = order.ClaimCurrency,
|
||||||
|
ClaimReason = order.ClaimReason,
|
||||||
|
ClaimTime = order.ClaimTime,
|
||||||
|
PreviousOrderStatus = order.PreviousOrderStatus,
|
||||||
ReceiveTime = order.ReceiveTime,
|
ReceiveTime = order.ReceiveTime,
|
||||||
ShipTime = order.ShipTime,
|
ShipTime = order.ShipTime,
|
||||||
PayTime = order.PayTime,
|
PayTime = order.PayTime,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ using Oms.Application.Dtos.Channels;
|
|||||||
using Oms.Application.Dtos.Orders;
|
using Oms.Application.Dtos.Orders;
|
||||||
using Oms.Application.Integrations.SmallBagTrack;
|
using Oms.Application.Integrations.SmallBagTrack;
|
||||||
using Oms.Application.Shared;
|
using Oms.Application.Shared;
|
||||||
|
using Oms.Application.Services.Orders.Issues;
|
||||||
using Oms.Domain.ChannelsAggregate.Consts;
|
using Oms.Domain.ChannelsAggregate.Consts;
|
||||||
using Oms.Domain.ChannelsAggregate.Entities;
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
@ -33,6 +34,9 @@ public partial class OrderService
|
|||||||
private AllocationProviderSnapshotEnricher AllocationProviderSnapshotEnricher =>
|
private AllocationProviderSnapshotEnricher AllocationProviderSnapshotEnricher =>
|
||||||
LazyServiceProvider.GetRequiredService<AllocationProviderSnapshotEnricher>();
|
LazyServiceProvider.GetRequiredService<AllocationProviderSnapshotEnricher>();
|
||||||
|
|
||||||
|
private AutoTriggerRuntimeService AutoTriggerRuntimeService =>
|
||||||
|
LazyServiceProvider.GetRequiredService<AutoTriggerRuntimeService>();
|
||||||
|
|
||||||
private async Task<OrderParcelCreateOutput> CreateParcelCoreAsync(OrderParcelCreateInput input)
|
private async Task<OrderParcelCreateOutput> CreateParcelCoreAsync(OrderParcelCreateInput input)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(input.RequestId))
|
if (!string.IsNullOrWhiteSpace(input.RequestId))
|
||||||
@ -135,6 +139,15 @@ public partial class OrderService
|
|||||||
|
|
||||||
await EnqueueSmallBagTrackUpsertAsync(order, customer, input.Receiver);
|
await EnqueueSmallBagTrackUpsertAsync(order, customer, input.Receiver);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await AutoTriggerRuntimeService.FireAsync(order, AutoTriggerTiming.OrderCreate, CurrentUser.GetId<int>());
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 自动触发失败不影响预报主流程
|
||||||
|
}
|
||||||
|
|
||||||
return await BuildParcelCreateOutputAsync(order);
|
return await BuildParcelCreateOutputAsync(order);
|
||||||
}
|
}
|
||||||
catch (BusinessException ex)
|
catch (BusinessException ex)
|
||||||
|
|||||||
@ -70,10 +70,20 @@ public partial class OrderService : ApplicationService
|
|||||||
x => x.Status,
|
x => x.Status,
|
||||||
x => x.Count,
|
x => x.Count,
|
||||||
StringComparer.OrdinalIgnoreCase);
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
var handleRows = await OrderRepository.GetHandleStatusCountsByFilterAsync(filter);
|
||||||
|
var handleCounts = handleRows.ToDictionary(
|
||||||
|
x => x.Key,
|
||||||
|
x => new OrderHandleStatusCountsOutput
|
||||||
|
{
|
||||||
|
Pending = x.Value.Pending,
|
||||||
|
Processed = x.Value.Processed
|
||||||
|
},
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
return new OrderStatusCountsOutput
|
return new OrderStatusCountsOutput
|
||||||
{
|
{
|
||||||
Total = rows.Sum(x => x.Count),
|
Total = rows.Sum(x => x.Count),
|
||||||
Counts = counts
|
Counts = counts,
|
||||||
|
HandleCounts = handleCounts
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,6 +397,7 @@ public partial class OrderService : ApplicationService
|
|||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Status = input.Status,
|
Status = input.Status,
|
||||||
|
HandleStatus = input.HandleStatus,
|
||||||
Keyword = input.Keyword,
|
Keyword = input.Keyword,
|
||||||
CreateTimeStart = input.CreateTimeStart,
|
CreateTimeStart = input.CreateTimeStart,
|
||||||
CreateTimeEnd = input.CreateTimeEnd,
|
CreateTimeEnd = input.CreateTimeEnd,
|
||||||
|
|||||||
@ -168,7 +168,7 @@ public static class OmsDocHelper
|
|||||||
OrderStatusCodes.DeliveryFailed => "派送失败",
|
OrderStatusCodes.DeliveryFailed => "派送失败",
|
||||||
OrderStatusCodes.Problematic => "问题件",
|
OrderStatusCodes.Problematic => "问题件",
|
||||||
OrderStatusCodes.Deleted => "已删除",
|
OrderStatusCodes.Deleted => "已删除",
|
||||||
OrderStatusCodes.Claimed => "已认领",
|
OrderStatusCodes.Claimed => "理赔件",
|
||||||
_ => statusCode
|
_ => statusCode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/Oms.Domain/OrdersAggregate/Consts/ClaimTypeCodes.cs
Normal file
32
src/Oms.Domain/OrdersAggregate/Consts/ClaimTypeCodes.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
namespace Oms.Domain.OrdersAggregate.Consts;
|
||||||
|
|
||||||
|
public static class ClaimTypeCodes
|
||||||
|
{
|
||||||
|
public const string Lost = "lost";
|
||||||
|
public const string Damaged = "damaged";
|
||||||
|
public const string Delay = "delay";
|
||||||
|
public const string Customs = "customs";
|
||||||
|
public const string Fee = "fee";
|
||||||
|
public const string Other = "other";
|
||||||
|
|
||||||
|
public static readonly HashSet<string> All =
|
||||||
|
[
|
||||||
|
Lost,
|
||||||
|
Damaged,
|
||||||
|
Delay,
|
||||||
|
Customs,
|
||||||
|
Fee,
|
||||||
|
Other
|
||||||
|
];
|
||||||
|
|
||||||
|
public static string GetDisplayName(string? code) => code?.Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
Lost => "丢件",
|
||||||
|
Damaged => "破损",
|
||||||
|
Delay => "延误",
|
||||||
|
Customs => "清关理赔",
|
||||||
|
Fee => "费用争议",
|
||||||
|
Other => "其他",
|
||||||
|
_ => code ?? string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
namespace Oms.Domain.OrdersAggregate.Consts;
|
||||||
|
|
||||||
|
public static class OrderExceptionEventTypeCodes
|
||||||
|
{
|
||||||
|
public const string MarkProblematic = "mark_problematic";
|
||||||
|
public const string MarkClaimed = "mark_claimed";
|
||||||
|
public const string HandleProcessed = "handle_processed";
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
namespace Oms.Domain.OrdersAggregate.Consts;
|
||||||
|
|
||||||
|
public static class OrderExceptionStatusRules
|
||||||
|
{
|
||||||
|
public static readonly HashSet<string> ForbiddenSourceStatuses = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
OrderStatusCodes.Draft,
|
||||||
|
OrderStatusCodes.Deleted,
|
||||||
|
OrderStatusCodes.Problematic,
|
||||||
|
OrderStatusCodes.Claimed
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool CanMarkException(string? orderStatus) =>
|
||||||
|
!string.IsNullOrWhiteSpace(orderStatus)
|
||||||
|
&& !ForbiddenSourceStatuses.Contains(orderStatus.Trim());
|
||||||
|
|
||||||
|
public static string GetForbiddenReason(string? orderStatus) => orderStatus?.Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
OrderStatusCodes.Draft => "草稿订单不可标记",
|
||||||
|
OrderStatusCodes.Deleted => "已删除订单不可标记",
|
||||||
|
OrderStatusCodes.Problematic => "已是问题件",
|
||||||
|
OrderStatusCodes.Claimed => "已是理赔件",
|
||||||
|
_ => "订单状态不允许标记"
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
namespace Oms.Domain.OrdersAggregate.Consts;
|
||||||
|
|
||||||
|
public static class OrderHandleStatusCodes
|
||||||
|
{
|
||||||
|
public const string Pending = "pending";
|
||||||
|
public const string Processed = "processed";
|
||||||
|
|
||||||
|
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
Pending,
|
||||||
|
Processed
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string GetDisplayName(string? code) => code?.Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
Pending => "待处理",
|
||||||
|
Processed => "已处理",
|
||||||
|
_ => "待处理"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool IsPending(string? code) =>
|
||||||
|
string.IsNullOrWhiteSpace(code)
|
||||||
|
|| string.Equals(code.Trim(), Pending, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public static bool IsProcessed(string? code) =>
|
||||||
|
string.Equals(code?.Trim(), Processed, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
@ -54,6 +54,35 @@ public class Order : OmsAuditedAggregateRoot
|
|||||||
|
|
||||||
public string OrderStatus { get; set; } = string.Empty;
|
public string OrderStatus { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 问题件/已理赔子状态:pending-待处理,processed-已处理。
|
||||||
|
/// </summary>
|
||||||
|
public string? HandleStatus { get; set; }
|
||||||
|
|
||||||
|
public string? IssueType { get; set; }
|
||||||
|
|
||||||
|
public string? IssueTitle { get; set; }
|
||||||
|
|
||||||
|
public string? IssueContent { get; set; }
|
||||||
|
|
||||||
|
public int? IssueTemplateId { get; set; }
|
||||||
|
|
||||||
|
public string? IssueRemark { get; set; }
|
||||||
|
|
||||||
|
public DateTime? IssueTime { get; set; }
|
||||||
|
|
||||||
|
public string? ClaimType { get; set; }
|
||||||
|
|
||||||
|
public decimal? ClaimAmount { get; set; }
|
||||||
|
|
||||||
|
public string? ClaimCurrency { get; set; }
|
||||||
|
|
||||||
|
public string? ClaimReason { get; set; }
|
||||||
|
|
||||||
|
public DateTime? ClaimTime { get; set; }
|
||||||
|
|
||||||
|
public string? PreviousOrderStatus { get; set; }
|
||||||
|
|
||||||
public int? WarehouseId { get; set; }
|
public int? WarehouseId { get; set; }
|
||||||
|
|
||||||
public int? CustomerId { get; set; }
|
public int? CustomerId { get; set; }
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
using Oms.Domain.Shared;
|
||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
|
||||||
|
[SugarTable("t_order_exception_event")]
|
||||||
|
public class OrderExceptionEvent : OmsAuditedAggregateRoot
|
||||||
|
{
|
||||||
|
public int OrderId { get; set; }
|
||||||
|
|
||||||
|
public string EventType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? FromStatus { get; set; }
|
||||||
|
|
||||||
|
public string? ToStatus { get; set; }
|
||||||
|
|
||||||
|
public string? FromHandleStatus { get; set; }
|
||||||
|
|
||||||
|
public string? ToHandleStatus { get; set; }
|
||||||
|
|
||||||
|
public string? IssueType { get; set; }
|
||||||
|
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
public string? Content { get; set; }
|
||||||
|
|
||||||
|
public int? IssueTemplateId { get; set; }
|
||||||
|
|
||||||
|
public string? NotifyMethod { get; set; }
|
||||||
|
|
||||||
|
public string? ClaimType { get; set; }
|
||||||
|
|
||||||
|
public decimal? ClaimAmount { get; set; }
|
||||||
|
|
||||||
|
public string? ClaimCurrency { get; set; }
|
||||||
|
|
||||||
|
public string? ClaimReason { get; set; }
|
||||||
|
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
@ -7,6 +7,8 @@ public class OrderListFilter
|
|||||||
{
|
{
|
||||||
public string? Status { get; set; }
|
public string? Status { get; set; }
|
||||||
|
|
||||||
|
public string? HandleStatus { get; set; }
|
||||||
|
|
||||||
public string? Keyword { get; set; }
|
public string? Keyword { get; set; }
|
||||||
|
|
||||||
public DateTime? CreateTimeStart { get; set; }
|
public DateTime? CreateTimeStart { get; set; }
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
using Fake.Domain.Repositories;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
|
||||||
|
namespace Oms.Domain.OrdersAggregate.Repositories;
|
||||||
|
|
||||||
|
public interface IOrderExceptionEventRepository : IRepository<OrderExceptionEvent>
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -75,6 +75,10 @@ public interface IOrderRepository : IRepository<Order>
|
|||||||
OrderListFilter filter,
|
OrderListFilter filter,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<Dictionary<string, (int Pending, int Processed)>> GetHandleStatusCountsByFilterAsync(
|
||||||
|
OrderListFilter filter,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task<int> CountByFilterAndCreateTimeRangeAsync(
|
Task<int> CountByFilterAndCreateTimeRangeAsync(
|
||||||
OrderListFilter filter,
|
OrderListFilter filter,
|
||||||
DateTime startInclusive,
|
DateTime startInclusive,
|
||||||
|
|||||||
13
src/Oms.SqlSugarCore/Orders/OrderExceptionEventRepository.cs
Normal file
13
src/Oms.SqlSugarCore/Orders/OrderExceptionEventRepository.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Fake.DependencyInjection;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
using Oms.Domain.OrdersAggregate.Repositories;
|
||||||
|
using Oms.SqlSugarCore.Repositories;
|
||||||
|
|
||||||
|
namespace Oms.SqlSugarCore.Orders;
|
||||||
|
|
||||||
|
public class OrderExceptionEventRepository
|
||||||
|
: OmsSqlSugarRepository<OrderExceptionEvent>,
|
||||||
|
IOrderExceptionEventRepository,
|
||||||
|
IScopedDependency
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -73,6 +73,23 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
|||||||
query = query.Where(x => x.CreateTime <= end);
|
query = query.Where(x => x.CreateTime <= end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var handleStatus = filter.HandleStatus?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(handleStatus)
|
||||||
|
&& !string.Equals(handleStatus, "all", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (OrderHandleStatusCodes.IsProcessed(handleStatus))
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.HandleStatus == OrderHandleStatusCodes.Processed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
query = query.Where(x =>
|
||||||
|
x.HandleStatus == null
|
||||||
|
|| x.HandleStatus == string.Empty
|
||||||
|
|| x.HandleStatus == OrderHandleStatusCodes.Pending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
query = OrderListScopeFilterApplier.Apply(query, filter);
|
query = OrderListScopeFilterApplier.Apply(query, filter);
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
@ -425,6 +442,49 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
|||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, (int Pending, int Processed)>> GetHandleStatusCountsByFilterAsync(
|
||||||
|
OrderListFilter filter,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var db = await GetDbContextAsync(cancellationToken);
|
||||||
|
var baseFilter = new OrderListFilter
|
||||||
|
{
|
||||||
|
Keyword = filter.Keyword,
|
||||||
|
CreateTimeStart = filter.CreateTimeStart,
|
||||||
|
CreateTimeEnd = filter.CreateTimeEnd,
|
||||||
|
CustomerId = filter.CustomerId,
|
||||||
|
ScopeOrganizationIds = filter.ScopeOrganizationIds,
|
||||||
|
ScopeSecretLevels = filter.ScopeSecretLevels,
|
||||||
|
ScopeDataScope = filter.ScopeDataScope,
|
||||||
|
ScopePersonalOwnerMapId = filter.ScopePersonalOwnerMapId,
|
||||||
|
ScopePersonalOwnerAccount = filter.ScopePersonalOwnerAccount,
|
||||||
|
ScopeOwnerMapIds = filter.ScopeOwnerMapIds,
|
||||||
|
ScopeBizFilters = filter.ScopeBizFilters,
|
||||||
|
ScopeCustomFilters = filter.ScopeCustomFilters
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new Dictionary<string, (int Pending, int Processed)>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
[OrderStatusCodes.Problematic] = (0, 0),
|
||||||
|
[OrderStatusCodes.Claimed] = (0, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var status in new[] { OrderStatusCodes.Problematic, OrderStatusCodes.Claimed })
|
||||||
|
{
|
||||||
|
baseFilter.Status = status;
|
||||||
|
var query = ApplyListFilter(db.Queryable<Order>(), baseFilter);
|
||||||
|
var rows = await query
|
||||||
|
.Select(x => x.HandleStatus)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var pending = rows.Count(OrderHandleStatusCodes.IsPending);
|
||||||
|
var processed = rows.Count(OrderHandleStatusCodes.IsProcessed);
|
||||||
|
result[status] = (pending, processed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<int> CountByFilterAndCreateTimeRangeAsync(
|
public async Task<int> CountByFilterAndCreateTimeRangeAsync(
|
||||||
OrderListFilter filter,
|
OrderListFilter filter,
|
||||||
DateTime startInclusive,
|
DateTime startInclusive,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user