feat: 代发进度统计、申报HS分列与异常事件表迁移脚本

补充代发进度 Tab 计数、PrimarySkuCodes 与 Im/Ex HS 编码输出;新增 t_order_exception_event 幂等迁移(Handler*、ConcurrencyStamp)并同步 db.sql。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-08-20 17:06:16 +08:00
parent 9d813a5f89
commit 2018e45a7d
11 changed files with 274 additions and 10 deletions

1
db.sql
View File

@ -490,6 +490,7 @@ CREATE TABLE `t_order_exception_event` (
`ClaimReason` VARCHAR(512) NULL COMMENT '理赔原因', `ClaimReason` VARCHAR(512) NULL COMMENT '理赔原因',
`Remark` VARCHAR(512) NULL COMMENT '操作备注', `Remark` VARCHAR(512) NULL COMMENT '操作备注',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是', `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '操作人', `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '操作人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', `CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
`UpdateUserId` INT NULL COMMENT '更新人', `UpdateUserId` INT NULL COMMENT '更新人',

View File

@ -0,0 +1,118 @@
-- 订单异常事件表:补齐 Handler* / ConcurrencyStamp(幂等;列已存在则自动跳过)
-- 修复:Unknown column 'HandlerNames' / 'ConcurrencyStamp' in 'field list'
-- MySQL 5.7+ / 8.0
SET @db := DATABASE();
-- HandlerNames
SET @col := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_order_exception_event'
AND COLUMN_NAME = 'HandlerNames'
);
SET @sql := IF(
@col = 0,
'ALTER TABLE `t_order_exception_event` ADD COLUMN `HandlerNames` VARCHAR(256) NULL COMMENT ''处理人快照''',
'SELECT ''skip HandlerNames'' AS msg'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- HandlerType
SET @col := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_order_exception_event'
AND COLUMN_NAME = 'HandlerType'
);
SET @sql := IF(
@col = 0,
'ALTER TABLE `t_order_exception_event` ADD COLUMN `HandlerType` VARCHAR(16) NULL COMMENT ''处理人类型''',
'SELECT ''skip HandlerType'' AS msg'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- HandlerUserId
SET @col := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_order_exception_event'
AND COLUMN_NAME = 'HandlerUserId'
);
SET @sql := IF(
@col = 0,
'ALTER TABLE `t_order_exception_event` ADD COLUMN `HandlerUserId` INT NULL COMMENT ''处理人用户Id(customer 时为订单客户账号)''',
'SELECT ''skip HandlerUserId'' AS msg'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- ConcurrencyStamp(AggregateRoot 并发戳;避免 DEFAULT(UUID()) 触发 1674 复制不安全)
SET @col := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_order_exception_event'
AND COLUMN_NAME = 'ConcurrencyStamp'
);
SET @sql := IF(
@col = 0,
'ALTER TABLE `t_order_exception_event` ADD COLUMN `ConcurrencyStamp` CHAR(36) NULL COMMENT ''并发戳''',
'SELECT ''skip ConcurrencyStamp add'' AS msg'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @col := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_order_exception_event'
AND COLUMN_NAME = 'ConcurrencyStamp'
);
SET @sql := IF(
@col > 0,
'UPDATE `t_order_exception_event`
SET `ConcurrencyStamp` = LOWER(CONCAT(
LPAD(HEX(`Id`), 8, ''0''),
''-0000-4000-8000-'',
LPAD(HEX(`Id` * 9973), 12, ''0'')
))
WHERE `ConcurrencyStamp` IS NULL OR `ConcurrencyStamp` = ''''',
'SELECT ''skip ConcurrencyStamp backfill'' AS msg'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @col := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_order_exception_event'
AND COLUMN_NAME = 'ConcurrencyStamp'
);
SET @isNullable := (
SELECT IFNULL(MAX(CASE WHEN IS_NULLABLE = 'YES' THEN 1 ELSE 0 END), 0)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_order_exception_event'
AND COLUMN_NAME = 'ConcurrencyStamp'
);
SET @sql := IF(
@col > 0 AND @isNullable = 1,
'ALTER TABLE `t_order_exception_event` MODIFY COLUMN `ConcurrencyStamp` CHAR(36) NOT NULL COMMENT ''并发戳''',
'SELECT ''skip ConcurrencyStamp not null'' AS msg'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@ -1,14 +1,58 @@
-- 代发订单:t_order 履约类型字段(逐条执行;若列已存在请跳过对应 ALTER) -- 代发订单:t_order 履约类型字段(幂等;列/索引已存在则自动跳过)
-- MySQL 5.7+ / 8.0 -- MySQL 5.7+ / 8.0
ALTER TABLE `t_order` SET @db := DATABASE();
ADD COLUMN `FulfillmentType` VARCHAR(16) NULL DEFAULT 'direct' COMMENT '履约类型 direct/warehouse' AFTER `AdditionalServices`;
ALTER TABLE `t_order` -- FulfillmentType
ADD COLUMN `FulfillmentSource` VARCHAR(32) NULL COMMENT '代发来源 storeFulfill/postBind' AFTER `FulfillmentType`; SET @col := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_order'
AND COLUMN_NAME = 'FulfillmentType'
);
SET @sql := IF(
@col = 0,
'ALTER TABLE `t_order` ADD COLUMN `FulfillmentType` VARCHAR(16) NULL DEFAULT ''direct'' COMMENT ''履约类型 direct/warehouse'' AFTER `AdditionalServices`',
'SELECT ''skip FulfillmentType'' AS msg'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
ALTER TABLE `t_order` -- FulfillmentSource
ADD KEY `idx_ord_fulfillment_customer` (`FulfillmentType`, `CustomerId`, `OrderStatus`); SET @col := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_order'
AND COLUMN_NAME = 'FulfillmentSource'
);
SET @sql := IF(
@col = 0,
'ALTER TABLE `t_order` ADD COLUMN `FulfillmentSource` VARCHAR(32) NULL COMMENT ''代发来源 storeFulfill/postBind'' AFTER `FulfillmentType`',
'SELECT ''skip FulfillmentSource'' AS msg'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- idx_ord_fulfillment_customer
SET @idx := (
SELECT COUNT(*)
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_order'
AND INDEX_NAME = 'idx_ord_fulfillment_customer'
);
SET @sql := IF(
@idx = 0,
'ALTER TABLE `t_order` ADD KEY `idx_ord_fulfillment_customer` (`FulfillmentType`, `CustomerId`, `OrderStatus`)',
'SELECT ''skip idx_ord_fulfillment_customer'' AS msg'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 历史:已有绑定行的订单标记为 warehouse -- 历史:已有绑定行的订单标记为 warehouse
UPDATE `t_order` o UPDATE `t_order` o

View File

@ -23,6 +23,13 @@ public class OrderExportInput
[Newtonsoft.Json.JsonProperty("fulfillmentType")] [Newtonsoft.Json.JsonProperty("fulfillmentType")]
public string? FulfillmentType { get; set; } public string? FulfillmentType { get; set; }
/// <summary>
/// 代发进度筛选(pendingBind / domesticInTransit / arrivedPendingPick / outboundDone)。
/// </summary>
[Newtonsoft.Json.JsonProperty("fulfillProgress")]
[global::System.Text.Json.Serialization.JsonPropertyName("fulfillProgress")]
public string? FulfillProgress { get; set; }
/// <summary> /// <summary>
/// 关键词。 /// 关键词。
/// </summary> /// </summary>

View File

@ -51,10 +51,20 @@ public class OrderItemOutput
public decimal? Weight { get; set; } public decimal? Weight { get; set; }
/// <summary> /// <summary>
/// HS 编码。 /// HS 编码(兼容旧字段,优先出口编码)。
/// </summary> /// </summary>
public string? HsCode { get; set; } public string? HsCode { get; set; }
/// <summary>
/// 进口 HS 编码。
/// </summary>
public string? ImHsCode { get; set; }
/// <summary>
/// 出口 HS 编码。
/// </summary>
public string? ExHsCode { get; set; }
/// <summary> /// <summary>
/// SKU 图片。 /// SKU 图片。
/// </summary> /// </summary>

View File

@ -175,6 +175,11 @@ public class OrderOutput
/// </summary> /// </summary>
public string? PrimaryBinCode { get; set; } public string? PrimaryBinCode { get; set; }
/// <summary>
/// 绑定 SKU 编码汇总(逗号分隔,列表展示/拣货筛选用)。
/// </summary>
public string? PrimarySkuCodes { get; set; }
/// <summary> /// <summary>
/// 关联揽收单 Id。 /// 关联揽收单 Id。
/// </summary> /// </summary>

View File

@ -20,4 +20,10 @@ public class OrderStatusCountsOutput
/// </summary> /// </summary>
public Dictionary<string, OrderHandleStatusCountsOutput> HandleCounts { get; set; } = public Dictionary<string, OrderHandleStatusCountsOutput> HandleCounts { get; set; } =
new(StringComparer.OrdinalIgnoreCase); new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// 代发进度分组数量(fulfillmentType=warehouse 时返回)。
/// </summary>
public Dictionary<string, int> FulfillProgressCounts { get; set; } =
new(StringComparer.OrdinalIgnoreCase);
} }

View File

@ -187,6 +187,7 @@ public partial class OrderService
bindSummary?.LineCount ?? 0, bindSummary?.LineCount ?? 0,
bindSummary?.HasInTransitParcel ?? false), bindSummary?.HasInTransitParcel ?? false),
PrimaryBinCode = bindSummary?.PrimaryBinCode, PrimaryBinCode = bindSummary?.PrimaryBinCode,
PrimarySkuCodes = bindSummary?.PrimarySkuCodes,
PickupOrderId = order.PickupOrderId, PickupOrderId = order.PickupOrderId,
PickupNo = order.PickupOrderId is > 0 PickupNo = order.PickupOrderId is > 0
&& pickupNoMap.TryGetValue(order.PickupOrderId.Value, out var pickupNo) && pickupNoMap.TryGetValue(order.PickupOrderId.Value, out var pickupNo)
@ -280,6 +281,8 @@ public partial class OrderService
public bool HasInTransitParcel { get; init; } public bool HasInTransitParcel { get; init; }
public string? PrimaryBinCode { get; init; } public string? PrimaryBinCode { get; init; }
public string? PrimarySkuCodes { get; init; }
} }
private async Task<Dictionary<int, StockBindListSummary>> LoadStockBindListSummariesAsync(int[] orderIds) private async Task<Dictionary<int, StockBindListSummary>> LoadStockBindListSummariesAsync(int[] orderIds)
@ -342,6 +345,12 @@ public partial class OrderService
.Where(code => !string.IsNullOrWhiteSpace(code)) .Where(code => !string.IsNullOrWhiteSpace(code))
.OrderBy(code => code, StringComparer.OrdinalIgnoreCase) .OrderBy(code => code, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(); .FirstOrDefault();
var skuCodes = rows
.Select(row => row.SkuCode?.Trim())
.Where(code => !string.IsNullOrWhiteSpace(code))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(code => code, StringComparer.OrdinalIgnoreCase)
.ToArray();
return new StockBindListSummary return new StockBindListSummary
{ {
BindStatus = bindStatus, BindStatus = bindStatus,
@ -349,7 +358,8 @@ public partial class OrderService
QtyTotal = rows.Sum(x => x.BindQty), QtyTotal = rows.Sum(x => x.BindQty),
TrackingNosText = trackingNos.Length == 0 ? null : string.Join(", ", trackingNos), TrackingNosText = trackingNos.Length == 0 ? null : string.Join(", ", trackingNos),
HasInTransitParcel = hasInTransitParcel, HasInTransitParcel = hasInTransitParcel,
PrimaryBinCode = primaryBinCode PrimaryBinCode = primaryBinCode,
PrimarySkuCodes = skuCodes.Length == 0 ? null : string.Join(", ", skuCodes),
}; };
}); });
} }
@ -732,6 +742,8 @@ public partial class OrderService
Amount = x.Amount, Amount = x.Amount,
Weight = x.Weight, Weight = x.Weight,
HsCode = x.ExHsCode ?? x.ImHsCode ?? x.HsCode, HsCode = x.ExHsCode ?? x.ImHsCode ?? x.HsCode,
ImHsCode = x.ImHsCode ?? x.HsCode,
ExHsCode = x.ExHsCode ?? x.HsCode,
SkuImage = x.SkuImage, SkuImage = x.SkuImage,
Category = x.Category, Category = x.Category,
Brand = x.Brand, Brand = x.Brand,

View File

@ -93,12 +93,18 @@ public partial class OrderService : ApplicationService
Processed = x.Value.Processed Processed = x.Value.Processed
}, },
StringComparer.OrdinalIgnoreCase); StringComparer.OrdinalIgnoreCase);
return new OrderStatusCountsOutput var output = new OrderStatusCountsOutput
{ {
Total = rows.Sum(x => x.Count), Total = rows.Sum(x => x.Count),
Counts = counts, Counts = counts,
HandleCounts = handleCounts HandleCounts = handleCounts
}; };
if (string.Equals(filter.FulfillmentType, FulfillmentTypeCodes.Warehouse, StringComparison.OrdinalIgnoreCase))
{
output.FulfillProgressCounts = await OrderRepository.GetFulfillProgressCountsByFilterAsync(filter);
}
return output;
} }
/// <summary> /// <summary>
@ -626,6 +632,7 @@ public partial class OrderService : ApplicationService
Status = input.Status, Status = input.Status,
OrderSource = input.OrderSource, OrderSource = input.OrderSource,
FulfillmentType = NormalizeFulfillmentTypeFilter(input.FulfillmentType), FulfillmentType = NormalizeFulfillmentTypeFilter(input.FulfillmentType),
FulfillProgress = NormalizeFulfillProgressFilter(input.FulfillProgress),
Keyword = NormalizeOrderKeyword(input.Keyword), Keyword = NormalizeOrderKeyword(input.Keyword),
CreateTimeStart = input.CreateTimeStart, CreateTimeStart = input.CreateTimeStart,
CreateTimeEnd = input.CreateTimeEnd, CreateTimeEnd = input.CreateTimeEnd,

View File

@ -86,6 +86,10 @@ public interface IOrderRepository : IRepository<Order>
OrderListFilter filter, OrderListFilter filter,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
Task<Dictionary<string, int>> GetFulfillProgressCountsByFilterAsync(
OrderListFilter filter,
CancellationToken cancellationToken = default);
Task<int> CountByFilterAndCreateTimeRangeAsync( Task<int> CountByFilterAndCreateTimeRangeAsync(
OrderListFilter filter, OrderListFilter filter,
DateTime startInclusive, DateTime startInclusive,

View File

@ -1212,6 +1212,56 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
return result; return result;
} }
public async Task<Dictionary<string, int>> GetFulfillProgressCountsByFilterAsync(
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,
WarehouseId = filter.WarehouseId,
FulfillmentType = filter.FulfillmentType,
StockBindStatus = filter.StockBindStatus,
OrderSource = filter.OrderSource,
FieldFilters = filter.FieldFilters,
SortField = filter.SortField,
SortOrder = filter.SortOrder,
ScopeOrganizationIds = filter.ScopeOrganizationIds,
ScopeSecretLevels = filter.ScopeSecretLevels,
ScopeDataScope = filter.ScopeDataScope,
ScopePersonalOwnerMapId = filter.ScopePersonalOwnerMapId,
ScopePersonalOwnerAccount = filter.ScopePersonalOwnerAccount,
ScopeOwnerMapIds = filter.ScopeOwnerMapIds,
ScopeBizFilters = filter.ScopeBizFilters,
ScopeCustomFilters = filter.ScopeCustomFilters,
};
async Task<int> CountAsync(string? fulfillProgress)
{
var query = ApplyListFilter(db.Queryable<Order>(), baseFilter);
if (!string.IsNullOrWhiteSpace(fulfillProgress))
{
query = ApplyFulfillProgressFilter(query, fulfillProgress);
}
return await query.CountAsync(cancellationToken);
}
var result = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["all"] = await CountAsync(null),
[FulfillmentProgressCodes.PendingBind] = await CountAsync(FulfillmentProgressCodes.PendingBind),
[FulfillmentProgressCodes.DomesticInTransit] = await CountAsync(FulfillmentProgressCodes.DomesticInTransit),
[FulfillmentProgressCodes.ArrivedPendingPick] = await CountAsync(FulfillmentProgressCodes.ArrivedPendingPick),
[FulfillmentProgressCodes.OutboundDone] = await CountAsync(FulfillmentProgressCodes.OutboundDone),
};
return result;
}
public async Task<int> CountByFilterAndCreateTimeRangeAsync( public async Task<int> CountByFilterAndCreateTimeRangeAsync(
OrderListFilter filter, OrderListFilter filter,
DateTime startInclusive, DateTime startInclusive,