fix: 批量任务详情查询不再写库,修复 ConcurrencyStamp 冲突

- MapJobOutputAsync 改为只读聚合,GET /jobs/{id} 不再 UpdateJob
- /jobs/{id}/items 仅校验存在,避免并行查询双写
- 重写 migrate_rename_warehouse_C_to_c.sql,支持 Navicat 与大小写两步改名

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-22 16:51:13 +08:00
parent a1daea6b35
commit c2d854b83f
3 changed files with 137 additions and 45 deletions

View File

@ -1,34 +1,123 @@
-- 仓储表 C_* → c_*(小写前缀,对齐 t_* 命名风格) -- 仓储表 C_* → c_*(小写 c 前缀)
-- 可重复执行:源表不存在或目标表已存在则跳过 -- 不依赖 DELIMITER / 存储过程,Navicat、DMS、mysql 命令行均可整段执行
-- 可重复执行
--
-- 说明:
-- 1) 若库已开启 lower_case_table_names(Windows/部分云库),建表时 C_sku 磁盘上已是 c_sku,
-- 无需 RENAME,部署新代码即可;下方「诊断」应只看到 c_*。
-- 2) Linux 且区分大小写时,须经临时表名两步改名,否则可能报 Table 'c_sku' already exists。
-- 3) 执行前请确认已 USE 到业务库(DATABASE() 不能为 NULL)。
DROP PROCEDURE IF EXISTS sp_rename_warehouse_table_if_needed; -- ========== 诊断(先看当前表名)==========
DELIMITER $$ SELECT TABLE_NAME AS warehouse_table
CREATE PROCEDURE sp_rename_warehouse_table_if_needed(IN old_name VARCHAR(128), IN new_name VARCHAR(128)) FROM information_schema.TABLES
BEGIN WHERE TABLE_SCHEMA = DATABASE()
IF EXISTS ( AND (
SELECT 1 FROM information_schema.TABLES TABLE_NAME LIKE 'C\_%' ESCAPE '\\'
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = old_name OR TABLE_NAME LIKE 'c\_%' ESCAPE '\\'
) AND NOT EXISTS ( OR TABLE_NAME LIKE '\_\_oms\_ren\_%' ESCAPE '\\'
SELECT 1 FROM information_schema.TABLES )
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = new_name ORDER BY TABLE_NAME;
) THEN
SET @ddl := CONCAT('RENAME TABLE `', old_name, '` TO `', new_name, '`');
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
END$$
DELIMITER ;
CALL sp_rename_warehouse_table_if_needed('C_sku', 'c_sku'); -- ========== 单表改名:大写 C_ 存在 → 临时名 → c_* ==========
CALL sp_rename_warehouse_table_if_needed('C_bin_location', 'c_bin_location'); -- c_sku
CALL sp_rename_warehouse_table_if_needed('C_inventory', 'c_inventory'); SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND BINARY TABLE_NAME = 'C_sku');
CALL sp_rename_warehouse_table_if_needed('C_inbound_notice', 'c_inbound_notice'); SET @sql := IF(@need > 0, 'RENAME TABLE `C_sku` TO `__oms_ren_sku`', 'SELECT ''skip: C_sku'' AS info');
CALL sp_rename_warehouse_table_if_needed('C_inbound_box', 'c_inbound_box'); PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
CALL sp_rename_warehouse_table_if_needed('C_inbound_box_line', 'c_inbound_box_line'); SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '__oms_ren_sku');
CALL sp_rename_warehouse_table_if_needed('C_parcel', 'c_parcel'); SET @sql := IF(@need > 0, 'RENAME TABLE `__oms_ren_sku` TO `c_sku`', 'SELECT ''skip: __oms_ren_sku'' AS info');
CALL sp_rename_warehouse_table_if_needed('C_parcel_arrival', 'c_parcel_arrival'); PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
CALL sp_rename_warehouse_table_if_needed('C_parcel_item', 'c_parcel_item');
CALL sp_rename_warehouse_table_if_needed('C_sku_platform_map', 'c_sku_platform_map');
DROP PROCEDURE IF EXISTS sp_rename_warehouse_table_if_needed; -- c_bin_location
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND BINARY TABLE_NAME = 'C_bin_location');
SET @sql := IF(@need > 0, 'RENAME TABLE `C_bin_location` TO `__oms_ren_bin_location`', 'SELECT ''skip: C_bin_location'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '__oms_ren_bin_location');
SET @sql := IF(@need > 0, 'RENAME TABLE `__oms_ren_bin_location` TO `c_bin_location`', 'SELECT ''skip: __oms_ren_bin_location'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
-- c_inventory
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND BINARY TABLE_NAME = 'C_inventory');
SET @sql := IF(@need > 0, 'RENAME TABLE `C_inventory` TO `__oms_ren_inventory`', 'SELECT ''skip: C_inventory'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '__oms_ren_inventory');
SET @sql := IF(@need > 0, 'RENAME TABLE `__oms_ren_inventory` TO `c_inventory`', 'SELECT ''skip: __oms_ren_inventory'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
-- c_inbound_notice
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND BINARY TABLE_NAME = 'C_inbound_notice');
SET @sql := IF(@need > 0, 'RENAME TABLE `C_inbound_notice` TO `__oms_ren_inbound_notice`', 'SELECT ''skip: C_inbound_notice'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '__oms_ren_inbound_notice');
SET @sql := IF(@need > 0, 'RENAME TABLE `__oms_ren_inbound_notice` TO `c_inbound_notice`', 'SELECT ''skip: __oms_ren_inbound_notice'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
-- c_inbound_box
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND BINARY TABLE_NAME = 'C_inbound_box');
SET @sql := IF(@need > 0, 'RENAME TABLE `C_inbound_box` TO `__oms_ren_inbound_box`', 'SELECT ''skip: C_inbound_box'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '__oms_ren_inbound_box');
SET @sql := IF(@need > 0, 'RENAME TABLE `__oms_ren_inbound_box` TO `c_inbound_box`', 'SELECT ''skip: __oms_ren_inbound_box'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
-- c_inbound_box_line
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND BINARY TABLE_NAME = 'C_inbound_box_line');
SET @sql := IF(@need > 0, 'RENAME TABLE `C_inbound_box_line` TO `__oms_ren_inbound_box_line`', 'SELECT ''skip: C_inbound_box_line'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '__oms_ren_inbound_box_line');
SET @sql := IF(@need > 0, 'RENAME TABLE `__oms_ren_inbound_box_line` TO `c_inbound_box_line`', 'SELECT ''skip: __oms_ren_inbound_box_line'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
-- c_parcel
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND BINARY TABLE_NAME = 'C_parcel');
SET @sql := IF(@need > 0, 'RENAME TABLE `C_parcel` TO `__oms_ren_parcel`', 'SELECT ''skip: C_parcel'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '__oms_ren_parcel');
SET @sql := IF(@need > 0, 'RENAME TABLE `__oms_ren_parcel` TO `c_parcel`', 'SELECT ''skip: __oms_ren_parcel'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
-- c_parcel_arrival
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND BINARY TABLE_NAME = 'C_parcel_arrival');
SET @sql := IF(@need > 0, 'RENAME TABLE `C_parcel_arrival` TO `__oms_ren_parcel_arrival`', 'SELECT ''skip: C_parcel_arrival'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '__oms_ren_parcel_arrival');
SET @sql := IF(@need > 0, 'RENAME TABLE `__oms_ren_parcel_arrival` TO `c_parcel_arrival`', 'SELECT ''skip: __oms_ren_parcel_arrival'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
-- c_parcel_item
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND BINARY TABLE_NAME = 'C_parcel_item');
SET @sql := IF(@need > 0, 'RENAME TABLE `C_parcel_item` TO `__oms_ren_parcel_item`', 'SELECT ''skip: C_parcel_item'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '__oms_ren_parcel_item');
SET @sql := IF(@need > 0, 'RENAME TABLE `__oms_ren_parcel_item` TO `c_parcel_item`', 'SELECT ''skip: __oms_ren_parcel_item'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
-- c_sku_platform_map
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND BINARY TABLE_NAME = 'C_sku_platform_map');
SET @sql := IF(@need > 0, 'RENAME TABLE `C_sku_platform_map` TO `__oms_ren_sku_platform_map`', 'SELECT ''skip: C_sku_platform_map'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
SET @need := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '__oms_ren_sku_platform_map');
SET @sql := IF(@need > 0, 'RENAME TABLE `__oms_ren_sku_platform_map` TO `c_sku_platform_map`', 'SELECT ''skip: __oms_ren_sku_platform_map'' AS info');
PREPARE _s FROM @sql; EXECUTE _s; DEALLOCATE PREPARE _s;
-- ========== 执行后复查 ==========
SELECT TABLE_NAME AS warehouse_table_after
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME LIKE 'c\_%' ESCAPE '\\'
ORDER BY TABLE_NAME;
-- ========== 若上面全是 skip 且已是 c_*:无需再改,直接部署后端即可 ==========
-- 若仍残留 C_*(区分大小写 Linux),可手工一次性执行(会失败则勿重复跑):
-- RENAME TABLE
-- `C_sku` TO `__oms_ren_sku`, `C_bin_location` TO `__oms_ren_bin_location`,
-- `C_inventory` TO `__oms_ren_inventory`, `C_inbound_notice` TO `__oms_ren_inbound_notice`,
-- `C_inbound_box` TO `__oms_ren_inbound_box`, `C_inbound_box_line` TO `__oms_ren_inbound_box_line`,
-- `C_parcel` TO `__oms_ren_parcel`, `C_parcel_arrival` TO `__oms_ren_parcel_arrival`,
-- `C_parcel_item` TO `__oms_ren_parcel_item`, `C_sku_platform_map` TO `__oms_ren_sku_platform_map`;
-- RENAME TABLE
-- `__oms_ren_sku` TO `c_sku`, `__oms_ren_bin_location` TO `c_bin_location`,
-- `__oms_ren_inventory` TO `c_inventory`, `__oms_ren_inbound_notice` TO `c_inbound_notice`,
-- `__oms_ren_inbound_box` TO `c_inbound_box`, `__oms_ren_inbound_box_line` TO `c_inbound_box_line`,
-- `__oms_ren_parcel` TO `c_parcel`, `__oms_ren_parcel_arrival` TO `c_parcel_arrival`,
-- `__oms_ren_parcel_item` TO `c_parcel_item`, `__oms_ren_sku_platform_map` TO `c_sku_platform_map`;

View File

@ -400,6 +400,9 @@ public sealed class OrderBatchJobCoordinator(
return await MapJobOutputAsync(job, cancellationToken); return await MapJobOutputAsync(job, cancellationToken);
} }
public Task<OrderBatchJob?> GetJobEntityAsync(int jobId, CancellationToken cancellationToken = default) =>
batchJobRepository.GetByIdAsync(jobId, cancellationToken);
public async Task<List<OrderBatchJobItemOutput>> GetJobItemsAsync(int jobId, CancellationToken cancellationToken = default) public async Task<List<OrderBatchJobItemOutput>> GetJobItemsAsync(int jobId, CancellationToken cancellationToken = default)
{ {
var job = await batchJobRepository.GetByIdAsync(jobId, cancellationToken); var job = await batchJobRepository.GetByIdAsync(jobId, cancellationToken);
@ -541,39 +544,39 @@ public sealed class OrderBatchJobCoordinator(
var runningCount = items.Count(x => x.Status == OrderBatchJobItemStatusCodes.Running); var runningCount = items.Count(x => x.Status == OrderBatchJobItemStatusCodes.Running);
var cancelledCount = items.Count(x => x.Status == OrderBatchJobItemStatusCodes.Cancelled); var cancelledCount = items.Count(x => x.Status == OrderBatchJobItemStatusCodes.Cancelled);
if (job.SuccessCount != successCount || job.FailCount != failCount) // 只读聚合:查询接口不得写库,避免与 MQ 消费者并发更新 ConcurrencyStamp 冲突。
{ var status = job.Status;
job.SuccessCount = successCount; var finishedAt = job.FinishedAt;
job.FailCount = failCount;
}
if (items.Count > 0 && items.All(x => if (items.Count > 0 && items.All(x =>
x.Status is OrderBatchJobItemStatusCodes.Success x.Status is OrderBatchJobItemStatusCodes.Success
or OrderBatchJobItemStatusCodes.Failed or OrderBatchJobItemStatusCodes.Failed
or OrderBatchJobItemStatusCodes.Cancelled)) or OrderBatchJobItemStatusCodes.Cancelled))
{ {
job.Status = cancelledCount == items.Count status = cancelledCount == items.Count
? OrderBatchJobStatusCodes.Cancelled ? OrderBatchJobStatusCodes.Cancelled
: failCount > 0 && successCount == 0 : failCount > 0 && successCount == 0
? OrderBatchJobStatusCodes.Failed ? OrderBatchJobStatusCodes.Failed
: OrderBatchJobStatusCodes.Completed; : OrderBatchJobStatusCodes.Completed;
job.FinishedAt ??= DateTime.Now; finishedAt ??= items
await batchJobRepository.UpdateJobAsync(job, cancellationToken); .Where(x => x.FinishedAt.HasValue)
.Select(x => x.FinishedAt)
.OrderByDescending(x => x)
.FirstOrDefault();
} }
var output = new OrderBatchJobOutput var output = new OrderBatchJobOutput
{ {
JobId = job.Id, JobId = job.Id,
Action = job.Action, Action = job.Action,
Status = job.Status, Status = status,
Total = job.Total, Total = job.Total,
SuccessCount = job.SuccessCount, SuccessCount = successCount,
FailCount = job.FailCount, FailCount = failCount,
PendingCount = pendingCount, PendingCount = pendingCount,
RunningCount = runningCount, RunningCount = runningCount,
CancelledCount = cancelledCount, CancelledCount = cancelledCount,
StartedAt = job.StartedAt, StartedAt = job.StartedAt,
FinishedAt = job.FinishedAt, FinishedAt = finishedAt,
CreateTime = job.CreateTime, CreateTime = job.CreateTime,
}; };
ApplyImportMetadata(output, job); ApplyImportMetadata(output, job);

View File

@ -59,7 +59,7 @@ public partial class OrderService
[HttpGet("/order/batch/jobs/{id:int}/items")] [HttpGet("/order/batch/jobs/{id:int}/items")]
public async Task<List<OrderBatchJobItemOutput>> GetBatchJobItemsAsync([FromRoute] int id) public async Task<List<OrderBatchJobItemOutput>> GetBatchJobItemsAsync([FromRoute] int id)
{ {
var job = await BatchJobCoordinator.GetJobAsync(id); var job = await BatchJobCoordinator.GetJobEntityAsync(id);
if (job == null) if (job == null)
{ {
throw new BusinessException("批量任务不存在"); throw new BusinessException("批量任务不存在");