Add order attachment files table and move system labels off Order.
Persist generated/printed/downstream labels in t_order_file, drop Order.LabelUrl generate-status fields, and expose file list/upload/delete APIs with customer read-only systemLabel access. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e2443ed6f8
commit
03f11a276f
22
db.sql
22
db.sql
@ -156,6 +156,7 @@ CREATE TABLE `t_order` (
|
||||
`Quantity` INT NULL COMMENT '包裹数量',
|
||||
`IsIntercept` TINYINT NOT NULL DEFAULT 0 COMMENT '是否拦截:0-否,1-是',
|
||||
`IsRemote` TINYINT NOT NULL DEFAULT 0 COMMENT '是否偏远:0-否,1-是',
|
||||
`LabelTemplateId` INT NULL COMMENT '出单标签模板Id',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
@ -177,6 +178,27 @@ CREATE TABLE `t_order` (
|
||||
KEY `idx_ord_source_ctime` (`OrderSource`, `CreateTime`)
|
||||
) COMMENT='订单表';
|
||||
|
||||
CREATE TABLE `t_order_file` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT,
|
||||
`OrderId` INT NOT NULL COMMENT '订单Id',
|
||||
`FileType` VARCHAR(32) NOT NULL COMMENT 'tradeProof/systemLabel/downstreamLabel/lastMileLabel/infraredImage',
|
||||
`FileName` VARCHAR(255) NULL COMMENT '原始文件名',
|
||||
`FileUrl` VARCHAR(512) NOT NULL COMMENT '相对路径或外链',
|
||||
`ContentType` VARCHAR(128) NULL,
|
||||
`FileSize` BIGINT NULL,
|
||||
`Sort` INT NOT NULL DEFAULT 0,
|
||||
`Source` VARCHAR(32) NOT NULL DEFAULT 'upload' COMMENT 'upload/systemGenerate/downstream/migrate',
|
||||
`Remark` VARCHAR(512) NULL,
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_ord_file_order_type` (`OrderId`, `FileType`, `IsDeleted`)
|
||||
) COMMENT='订单附件表';
|
||||
|
||||
CREATE TABLE `t_customer_sender_address` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '归属客户 t_customer.Id',
|
||||
|
||||
50
sql/20260728_order_file.sql
Normal file
50
sql/20260728_order_file.sql
Normal file
@ -0,0 +1,50 @@
|
||||
-- Order files table + migrate LabelUrl into systemLabel, then drop legacy columns.
|
||||
-- Run on MySQL. Backup first.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_order_file` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT,
|
||||
`OrderId` INT NOT NULL COMMENT 'Order Id',
|
||||
`FileType` VARCHAR(32) NOT NULL COMMENT 'tradeProof/systemLabel/downstreamLabel/lastMileLabel/infraredImage',
|
||||
`FileName` VARCHAR(255) NULL COMMENT 'Original file name',
|
||||
`FileUrl` VARCHAR(512) NOT NULL COMMENT 'Relative or absolute URL',
|
||||
`ContentType` VARCHAR(128) NULL,
|
||||
`FileSize` BIGINT NULL,
|
||||
`Sort` INT NOT NULL DEFAULT 0,
|
||||
`Source` VARCHAR(32) NOT NULL DEFAULT 'upload' COMMENT 'upload/systemGenerate/downstream/migrate',
|
||||
`Remark` VARCHAR(512) NULL,
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()),
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NULL,
|
||||
`UpdateTime` DATETIME NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_ord_file_order_type` (`OrderId`, `FileType`, `IsDeleted`)
|
||||
) COMMENT='Order attachment files';
|
||||
|
||||
-- Migrate existing system label URL (ignore if column already dropped)
|
||||
INSERT INTO `t_order_file` (`OrderId`, `FileType`, `FileName`, `FileUrl`, `ContentType`, `Sort`, `Source`, `CreateUserId`, `CreateTime`)
|
||||
SELECT
|
||||
`Id`,
|
||||
'systemLabel',
|
||||
SUBSTRING_INDEX(`LabelUrl`, '/', -1),
|
||||
`LabelUrl`,
|
||||
'application/pdf',
|
||||
0,
|
||||
'migrate',
|
||||
0,
|
||||
NOW()
|
||||
FROM `t_order`
|
||||
WHERE `LabelUrl` IS NOT NULL
|
||||
AND TRIM(`LabelUrl`) <> ''
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `t_order_file` f
|
||||
WHERE f.`OrderId` = `t_order`.`Id`
|
||||
AND f.`FileType` = 'systemLabel'
|
||||
AND f.`IsDeleted` = 0
|
||||
);
|
||||
|
||||
ALTER TABLE `t_order`
|
||||
DROP COLUMN `LabelUrl`,
|
||||
DROP COLUMN `LabelGenerateStatus`,
|
||||
DROP COLUMN `LabelGenerateError`;
|
||||
@ -36,12 +36,12 @@ public class SalesProductDetailOutput
|
||||
public int? MaxTimeliness { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户面单类型。
|
||||
/// 系统面单类型。
|
||||
/// </summary>
|
||||
public string? CustomerWaybill { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户面单绑定的标签模板 Id。
|
||||
/// 系统面单绑定的标签模板 Id。
|
||||
/// </summary>
|
||||
public int? LabelTemplateId { get; set; }
|
||||
|
||||
|
||||
@ -36,12 +36,12 @@ public class SalesProductListOutput
|
||||
public int? MaxTimeliness { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户面单类型。
|
||||
/// 系统面单类型。
|
||||
/// </summary>
|
||||
public string? CustomerWaybill { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户面单绑定的标签模板 Id。
|
||||
/// 系统面单绑定的标签模板 Id。
|
||||
/// </summary>
|
||||
public int? LabelTemplateId { get; set; }
|
||||
|
||||
|
||||
@ -38,12 +38,12 @@ public class SalesProductSaveInput
|
||||
public int? MaxTimeliness { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户面单类型(旧枚举,兼容展示)。
|
||||
/// 系统面单类型(旧枚举字段,兼容展示)。
|
||||
/// </summary>
|
||||
public string? CustomerWaybill { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户面单绑定的标签模板 Id。
|
||||
/// 系统面单绑定的标签模板 Id。
|
||||
/// </summary>
|
||||
public int? LabelTemplateId { get; set; }
|
||||
|
||||
|
||||
41
src/Oms.Application/Dtos/Orders/OrderFileDtos.cs
Normal file
41
src/Oms.Application/Dtos/Orders/OrderFileDtos.cs
Normal file
@ -0,0 +1,41 @@
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
public class OrderFileItemOutput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int OrderId { get; set; }
|
||||
|
||||
public string FileType { get; set; } = string.Empty;
|
||||
|
||||
public string? FileName { get; set; }
|
||||
|
||||
public string FileUrl { get; set; } = string.Empty;
|
||||
|
||||
public string? ContentType { get; set; }
|
||||
|
||||
public long? FileSize { get; set; }
|
||||
|
||||
public int Sort { get; set; }
|
||||
|
||||
public string Source { get; set; } = string.Empty;
|
||||
|
||||
public string? Remark { get; set; }
|
||||
|
||||
public DateTime CreateTime { get; set; }
|
||||
}
|
||||
|
||||
public class OrderFileGroupedOutput
|
||||
{
|
||||
public int OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户登录时仅含 systemLabel;内部含全部类型。
|
||||
/// </summary>
|
||||
public Dictionary<string, List<OrderFileItemOutput>> Groups { get; set; } = new();
|
||||
}
|
||||
|
||||
public class OrderFileDeleteInput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
@ -62,7 +62,7 @@ internal static class ItfxCreateOrderOneDataParser
|
||||
|
||||
/// <summary>
|
||||
/// 将下游返回写入订单:服务商单号 ← Data.serviceNo,跟踪号 ← Data.trackNo。
|
||||
/// 空值不覆盖已有字段。
|
||||
/// 空值不覆盖已有字段。面单 URL 不写订单表,由调用方写入 t_order_file.downstreamLabel。
|
||||
/// </summary>
|
||||
public static bool ApplyToOrder(Order order, CreateOrderOneData data)
|
||||
{
|
||||
@ -88,16 +88,6 @@ internal static class ItfxCreateOrderOneDataParser
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(data.LabelUrl))
|
||||
{
|
||||
var value = data.LabelUrl.Trim();
|
||||
if (!string.Equals(order.LabelUrl, value, StringComparison.Ordinal))
|
||||
{
|
||||
order.LabelUrl = value;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ using Fake.Application;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Application.Services.Orders;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
@ -12,7 +13,7 @@ using Oms.Domain.SystemAggregate.Repositories;
|
||||
namespace Oms.Application.Labeling;
|
||||
|
||||
/// <summary>
|
||||
/// 面单 PDF 生成:解析模板 → 渲染 → 落盘 → 回写订单。
|
||||
/// 面单 PDF 生成:解析模板 → 渲染 → 落盘 → 写入 t_order_file(systemLabel)。
|
||||
/// </summary>
|
||||
public sealed class LabelPdfGenerationService(
|
||||
ILabelPdfRenderer renderer,
|
||||
@ -20,6 +21,7 @@ public sealed class LabelPdfGenerationService(
|
||||
ISalesProductRepository salesProductRepository,
|
||||
ISystemOtherConfigRepository systemOtherConfigRepository,
|
||||
IOrderRepository orderRepository,
|
||||
OrderFileManager orderFileManager,
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
ILogger<LabelPdfGenerationService> logger)
|
||||
{
|
||||
@ -28,6 +30,7 @@ public sealed class LabelPdfGenerationService(
|
||||
OrderOutput orderOutput,
|
||||
int? templateIdOverride = null,
|
||||
string? operatorName = null,
|
||||
int operatorUserId = 0,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
@ -35,13 +38,13 @@ public sealed class LabelPdfGenerationService(
|
||||
var templateId = await ResolveTemplateIdAsync(order, templateIdOverride, cancellationToken);
|
||||
if (!templateId.HasValue)
|
||||
{
|
||||
return await MarkFailedAsync(order, "未配置标签模板(产品未绑且无全局默认)", cancellationToken);
|
||||
return MarkFailed("未配置标签模板(产品未绑且无全局默认)", order.LabelTemplateId);
|
||||
}
|
||||
|
||||
var template = await labelTemplateRepository.FirstOrDefaultAsync(x => x.Id == templateId.Value);
|
||||
if (template == null || string.IsNullOrWhiteSpace(template.TemplateJson))
|
||||
{
|
||||
return await MarkFailedAsync(order, $"标签模板不存在或 JSON 为空: {templateId}", cancellationToken);
|
||||
return MarkFailed($"标签模板不存在或 JSON 为空: {templateId}", order.LabelTemplateId);
|
||||
}
|
||||
|
||||
var printData = LabelPrintDataBuilder.Build(orderOutput, operatorName);
|
||||
@ -49,12 +52,19 @@ public sealed class LabelPdfGenerationService(
|
||||
|
||||
var relativeUrl = await SavePdfAsync(order.ServiceNo, pdfBytes, cancellationToken);
|
||||
order.LabelTemplateId = templateId;
|
||||
order.LabelUrl = relativeUrl;
|
||||
order.LabelGenerateStatus = LabelGenerateStatusCodes.Ready;
|
||||
order.LabelGenerateError = null;
|
||||
order.UpdateTime = DateTime.Now;
|
||||
await orderRepository.UpdateAsync(order);
|
||||
|
||||
await orderFileManager.ReplaceSystemLabelAsync(
|
||||
order.Id,
|
||||
relativeUrl,
|
||||
fileName: Path.GetFileName(relativeUrl),
|
||||
contentType: "application/pdf",
|
||||
fileSize: pdfBytes.LongLength,
|
||||
source: OrderFileSourceCodes.SystemGenerate,
|
||||
operatorUserId: operatorUserId,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
return new LabelPdfGenerateResult
|
||||
{
|
||||
Success = true,
|
||||
@ -66,7 +76,7 @@ public sealed class LabelPdfGenerationService(
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Generate label PDF failed for order {OrderId}", order.Id);
|
||||
return await MarkFailedAsync(order, ex.Message, cancellationToken);
|
||||
return MarkFailed(ex.Message, order.LabelTemplateId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,21 +144,15 @@ public sealed class LabelPdfGenerationService(
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<LabelPdfGenerateResult> MarkFailedAsync(
|
||||
Order order,
|
||||
string error,
|
||||
CancellationToken cancellationToken)
|
||||
private static LabelPdfGenerateResult MarkFailed(string error, int? labelTemplateId)
|
||||
{
|
||||
order.LabelGenerateStatus = LabelGenerateStatusCodes.Failed;
|
||||
order.LabelGenerateError = error.Length > 500 ? error[..500] : error;
|
||||
order.UpdateTime = DateTime.Now;
|
||||
await orderRepository.UpdateAsync(order);
|
||||
var msg = error.Length > 500 ? error[..500] : error;
|
||||
return new LabelPdfGenerateResult
|
||||
{
|
||||
Success = false,
|
||||
Status = LabelGenerateStatusCodes.Failed,
|
||||
Error = order.LabelGenerateError,
|
||||
LabelTemplateId = order.LabelTemplateId
|
||||
Error = msg,
|
||||
LabelTemplateId = labelTemplateId
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -130,7 +130,6 @@ public class LabelPrintService : ApplicationService
|
||||
if (labelTemplate != null)
|
||||
{
|
||||
order.LabelTemplateId = labelTemplate.Id;
|
||||
order.LabelGenerateStatus = LabelGenerateStatusCodes.Pending;
|
||||
await OrderRepository.UpdateAsync(order);
|
||||
}
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ public sealed class OrderBatchActionExecutor(
|
||||
ISmallBagTrackUpsertRequestBuilder smallBagTrackUpsertRequestBuilder,
|
||||
OrderDownstreamCredentialResolver downstreamCredentialResolver,
|
||||
IItfxForwardClient itfxForwardClient,
|
||||
OrderFileManager orderFileManager,
|
||||
OrderBatchCreateActionHandler createActionHandler,
|
||||
OrderBatchOrderActionHandler orderActionHandler) : IOrderBatchActionExecutor, IScopedDependency
|
||||
{
|
||||
@ -215,10 +216,21 @@ public sealed class OrderBatchActionExecutor(
|
||||
}
|
||||
|
||||
if (ItfxCreateOrderOneDataParser.TryParse(forward.ResponseBody, out var data)
|
||||
&& data != null
|
||||
&& ItfxCreateOrderOneDataParser.ApplyToOrder(order, data))
|
||||
&& data != null)
|
||||
{
|
||||
await orderRepository.UpdateAsync(order, cancellationToken);
|
||||
if (ItfxCreateOrderOneDataParser.ApplyToOrder(order, data))
|
||||
{
|
||||
await orderRepository.UpdateAsync(order, cancellationToken);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(data.LabelUrl))
|
||||
{
|
||||
await orderFileManager.AddOrReplaceDownstreamLabelAsync(
|
||||
order.Id,
|
||||
data.LabelUrl!,
|
||||
message.OperatorUserId,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
return OrderBatchActionExecuteResult.Ok(
|
||||
|
||||
355
src/Oms.Application/Services/Orders/OrderFileManager.cs
Normal file
355
src/Oms.Application/Services/Orders/OrderFileManager.cs
Normal file
@ -0,0 +1,355 @@
|
||||
using Fake.Application;
|
||||
using Fake.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Services.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 订单附件读写(系统面单仅 1 份,其它类型最多 20)。
|
||||
/// </summary>
|
||||
public sealed class OrderFileManager(
|
||||
IOrderFileRepository orderFileRepository,
|
||||
IWebHostEnvironment webHostEnvironment) : IScopedDependency
|
||||
{
|
||||
private static readonly HashSet<string> ImageExt =
|
||||
new(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp" };
|
||||
|
||||
private static readonly HashSet<string> LabelExt =
|
||||
new(StringComparer.OrdinalIgnoreCase) { ".pdf", ".jpg", ".jpeg", ".png" };
|
||||
|
||||
private const long MaxImageBytes = 5 * 1024 * 1024;
|
||||
private const long MaxPdfBytes = 10 * 1024 * 1024;
|
||||
|
||||
public async Task<string?> GetSystemLabelUrlAsync(int orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var file = await orderFileRepository.GetByOrderAndTypeAsync(
|
||||
orderId,
|
||||
OrderFileTypeCodes.SystemLabel,
|
||||
cancellationToken);
|
||||
return string.IsNullOrWhiteSpace(file?.FileUrl) ? null : file.FileUrl.Trim();
|
||||
}
|
||||
|
||||
public async Task<OrderFileGroupedOutput> GetGroupedAsync(
|
||||
int orderId,
|
||||
bool customerView,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var list = await orderFileRepository.GetByOrderIdAsync(orderId, cancellationToken);
|
||||
if (customerView)
|
||||
{
|
||||
list = list
|
||||
.Where(x => string.Equals(x.FileType, OrderFileTypeCodes.SystemLabel, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var groups = list
|
||||
.OrderBy(x => x.Sort)
|
||||
.ThenBy(x => x.Id)
|
||||
.GroupBy(x => x.FileType, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => g.Select(MapItem).ToList(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Ensure known keys present for UI
|
||||
foreach (var type in OrderFileTypeCodes.All)
|
||||
{
|
||||
if (customerView && !string.Equals(type, OrderFileTypeCodes.SystemLabel, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!groups.ContainsKey(type))
|
||||
{
|
||||
groups[type] = [];
|
||||
}
|
||||
}
|
||||
|
||||
return new OrderFileGroupedOutput { OrderId = orderId, Groups = groups };
|
||||
}
|
||||
|
||||
public async Task<OrderFileItemOutput> ReplaceSystemLabelAsync(
|
||||
int orderId,
|
||||
string fileUrl,
|
||||
string? fileName,
|
||||
string? contentType,
|
||||
long? fileSize,
|
||||
string source,
|
||||
int operatorUserId,
|
||||
string? remark = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await orderFileRepository.SoftDeleteByOrderAndTypeAsync(
|
||||
orderId,
|
||||
OrderFileTypeCodes.SystemLabel,
|
||||
cancellationToken);
|
||||
|
||||
var entity = new OrderFile
|
||||
{
|
||||
OrderId = orderId,
|
||||
FileType = OrderFileTypeCodes.SystemLabel,
|
||||
FileName = fileName,
|
||||
FileUrl = fileUrl.Trim(),
|
||||
ContentType = contentType,
|
||||
FileSize = fileSize,
|
||||
Sort = 0,
|
||||
Source = source,
|
||||
Remark = remark,
|
||||
CreateUserId = operatorUserId,
|
||||
CreateTime = DateTime.Now
|
||||
};
|
||||
var saved = await orderFileRepository.InsertAsync(entity, cancellationToken);
|
||||
return MapItem(saved);
|
||||
}
|
||||
|
||||
public async Task<OrderFileItemOutput> AddOrReplaceDownstreamLabelAsync(
|
||||
int orderId,
|
||||
string fileUrl,
|
||||
int operatorUserId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileUrl))
|
||||
{
|
||||
throw new BusinessException("下游面单地址为空");
|
||||
}
|
||||
|
||||
var existing = await orderFileRepository.GetByOrderAndTypeListAsync(
|
||||
orderId,
|
||||
OrderFileTypeCodes.DownstreamLabel,
|
||||
cancellationToken);
|
||||
var dup = existing.FirstOrDefault(x =>
|
||||
string.Equals(x.FileUrl, fileUrl.Trim(), StringComparison.OrdinalIgnoreCase));
|
||||
if (dup != null)
|
||||
{
|
||||
return MapItem(dup);
|
||||
}
|
||||
|
||||
if (existing.Count >= OrderFileTypeCodes.GetMaxCount(OrderFileTypeCodes.DownstreamLabel))
|
||||
{
|
||||
// 超额时软删最早一条再追加
|
||||
var oldest = existing.OrderBy(x => x.Id).First();
|
||||
oldest.IsDeleted = true;
|
||||
oldest.UpdateTime = DateTime.Now;
|
||||
await orderFileRepository.UpdateAsync(oldest, cancellationToken);
|
||||
}
|
||||
|
||||
var entity = new OrderFile
|
||||
{
|
||||
OrderId = orderId,
|
||||
FileType = OrderFileTypeCodes.DownstreamLabel,
|
||||
FileName = TryFileNameFromUrl(fileUrl),
|
||||
FileUrl = fileUrl.Trim(),
|
||||
ContentType = null,
|
||||
Sort = existing.Count,
|
||||
Source = OrderFileSourceCodes.Downstream,
|
||||
CreateUserId = operatorUserId,
|
||||
CreateTime = DateTime.Now
|
||||
};
|
||||
var saved = await orderFileRepository.InsertAsync(entity, cancellationToken);
|
||||
return MapItem(saved);
|
||||
}
|
||||
|
||||
public async Task<List<OrderFileItemOutput>> UploadAsync(
|
||||
int orderId,
|
||||
string fileType,
|
||||
IFormFileCollection files,
|
||||
int operatorUserId,
|
||||
bool isCustomer,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (isCustomer)
|
||||
{
|
||||
throw new BusinessException("客户账号无权上传订单附件");
|
||||
}
|
||||
|
||||
if (!OrderFileTypeCodes.IsValid(fileType))
|
||||
{
|
||||
throw new BusinessException("不支持的附件类型");
|
||||
}
|
||||
|
||||
var type = OrderFileTypeCodes.Normalize(fileType);
|
||||
if (files == null || files.Count == 0)
|
||||
{
|
||||
throw new BusinessException("请选择文件");
|
||||
}
|
||||
|
||||
var existing = await orderFileRepository.GetByOrderAndTypeListAsync(orderId, type, cancellationToken);
|
||||
var max = OrderFileTypeCodes.GetMaxCount(type);
|
||||
var results = new List<OrderFileItemOutput>();
|
||||
|
||||
if (string.Equals(type, OrderFileTypeCodes.SystemLabel, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (files.Count != 1)
|
||||
{
|
||||
throw new BusinessException("系统面单仅允许上传 1 个文件");
|
||||
}
|
||||
|
||||
var savedPath = await SaveUploadFileAsync(files[0], orderId, type);
|
||||
results.Add(await ReplaceSystemLabelAsync(
|
||||
orderId,
|
||||
savedPath.Url,
|
||||
savedPath.FileName,
|
||||
savedPath.ContentType,
|
||||
savedPath.Size,
|
||||
OrderFileSourceCodes.Upload,
|
||||
operatorUserId,
|
||||
cancellationToken: cancellationToken));
|
||||
return results;
|
||||
}
|
||||
|
||||
if (existing.Count + files.Count > max)
|
||||
{
|
||||
throw new BusinessException($"类型 {type} 最多 {max} 个文件");
|
||||
}
|
||||
|
||||
var sortBase = existing.Count == 0 ? 0 : existing.Max(x => x.Sort) + 1;
|
||||
var index = 0;
|
||||
foreach (var file in files)
|
||||
{
|
||||
var savedPath = await SaveUploadFileAsync(file, orderId, type);
|
||||
var entity = new OrderFile
|
||||
{
|
||||
OrderId = orderId,
|
||||
FileType = type,
|
||||
FileName = savedPath.FileName,
|
||||
FileUrl = savedPath.Url,
|
||||
ContentType = savedPath.ContentType,
|
||||
FileSize = savedPath.Size,
|
||||
Sort = sortBase + index,
|
||||
Source = OrderFileSourceCodes.Upload,
|
||||
CreateUserId = operatorUserId,
|
||||
CreateTime = DateTime.Now
|
||||
};
|
||||
var saved = await orderFileRepository.InsertAsync(entity, cancellationToken);
|
||||
results.Add(MapItem(saved));
|
||||
index++;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(
|
||||
int fileId,
|
||||
bool isCustomer,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (isCustomer)
|
||||
{
|
||||
throw new BusinessException("客户账号无权删除订单附件");
|
||||
}
|
||||
|
||||
var entity = await orderFileRepository.FirstOrDefaultAsync(x => x.Id == fileId, cancellationToken)
|
||||
?? throw new BusinessException("附件不存在");
|
||||
entity.IsDeleted = true;
|
||||
entity.UpdateTime = DateTime.Now;
|
||||
await orderFileRepository.UpdateAsync(entity, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<(string Url, string FileName, string? ContentType, long Size)> SaveUploadFileAsync(
|
||||
IFormFile file,
|
||||
int orderId,
|
||||
string fileType)
|
||||
{
|
||||
if (file == null || file.Length <= 0)
|
||||
{
|
||||
throw new BusinessException("上传文件不能为空");
|
||||
}
|
||||
|
||||
var ext = Path.GetExtension(file.FileName);
|
||||
if (string.IsNullOrWhiteSpace(ext))
|
||||
{
|
||||
ext = ".bin";
|
||||
}
|
||||
|
||||
var isLabelType = fileType is OrderFileTypeCodes.SystemLabel
|
||||
or OrderFileTypeCodes.DownstreamLabel
|
||||
or OrderFileTypeCodes.LastMileLabel;
|
||||
var isInfraredOrProof = fileType is OrderFileTypeCodes.InfraredImage
|
||||
or OrderFileTypeCodes.TradeProof;
|
||||
|
||||
if (isLabelType)
|
||||
{
|
||||
if (!LabelExt.Contains(ext))
|
||||
{
|
||||
throw new BusinessException("面单仅支持 pdf / jpg / png");
|
||||
}
|
||||
|
||||
if (string.Equals(ext, ".pdf", StringComparison.OrdinalIgnoreCase) && file.Length > MaxPdfBytes)
|
||||
{
|
||||
throw new BusinessException("PDF 不能超过 10MB");
|
||||
}
|
||||
|
||||
if (!string.Equals(ext, ".pdf", StringComparison.OrdinalIgnoreCase) && file.Length > MaxImageBytes)
|
||||
{
|
||||
throw new BusinessException("图片不能超过 5MB");
|
||||
}
|
||||
}
|
||||
else if (isInfraredOrProof)
|
||||
{
|
||||
if (!ImageExt.Contains(ext) && !string.Equals(ext, ".pdf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new BusinessException("交易证明/红外图片仅支持图片或 pdf");
|
||||
}
|
||||
|
||||
if (file.Length > MaxPdfBytes)
|
||||
{
|
||||
throw new BusinessException("文件不能超过 10MB");
|
||||
}
|
||||
}
|
||||
|
||||
var webRoot = string.IsNullOrWhiteSpace(webHostEnvironment.WebRootPath)
|
||||
? Path.Combine(webHostEnvironment.ContentRootPath, "wwwroot")
|
||||
: webHostEnvironment.WebRootPath;
|
||||
var relativeDir = Path.Combine("uploads", "order-files", orderId.ToString(), fileType);
|
||||
var absoluteDir = Path.Combine(webRoot, relativeDir);
|
||||
Directory.CreateDirectory(absoluteDir);
|
||||
|
||||
var storedName = $"{Guid.NewGuid():N}{ext.ToLowerInvariant()}";
|
||||
var absolutePath = Path.Combine(absoluteDir, storedName);
|
||||
await using (var stream = File.Create(absolutePath))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
var url = $"/uploads/order-files/{orderId}/{fileType}/{storedName}".Replace('\\', '/');
|
||||
return (url, Path.GetFileName(file.FileName), file.ContentType, file.Length);
|
||||
}
|
||||
|
||||
private static OrderFileItemOutput MapItem(OrderFile x) =>
|
||||
new()
|
||||
{
|
||||
Id = x.Id,
|
||||
OrderId = x.OrderId,
|
||||
FileType = x.FileType,
|
||||
FileName = x.FileName,
|
||||
FileUrl = x.FileUrl,
|
||||
ContentType = x.ContentType,
|
||||
FileSize = x.FileSize,
|
||||
Sort = x.Sort,
|
||||
Source = x.Source,
|
||||
Remark = x.Remark,
|
||||
CreateTime = x.CreateTime
|
||||
};
|
||||
|
||||
private static string? TryFileNameFromUrl(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = url.Contains('?', StringComparison.Ordinal)
|
||||
? url[..url.IndexOf('?', StringComparison.Ordinal)]
|
||||
: url;
|
||||
var name = Path.GetFileName(path);
|
||||
return string.IsNullOrWhiteSpace(name) ? null : name;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -39,6 +39,7 @@ public partial class OrderService
|
||||
mapped,
|
||||
templateIdOverride: null,
|
||||
operatorName: null,
|
||||
operatorUserId: 0,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@ -53,10 +53,20 @@ public partial class OrderService
|
||||
|
||||
if (forward.Success
|
||||
&& ItfxCreateOrderOneDataParser.TryParse(forward.ResponseBody, out var data)
|
||||
&& data != null
|
||||
&& ItfxCreateOrderOneDataParser.ApplyToOrder(order, data))
|
||||
&& data != null)
|
||||
{
|
||||
await OrderRepository.UpdateAsync(order);
|
||||
if (ItfxCreateOrderOneDataParser.ApplyToOrder(order, data))
|
||||
{
|
||||
await OrderRepository.UpdateAsync(order);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(data.LabelUrl))
|
||||
{
|
||||
await OrderFileManager.AddOrReplaceDownstreamLabelAsync(
|
||||
order.Id,
|
||||
data.LabelUrl!,
|
||||
CurrentUser.IsAuthenticated ? CurrentUser.GetId<int>() : 0);
|
||||
}
|
||||
}
|
||||
|
||||
return new OrderDownstreamForwardResult
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Application.Shared;
|
||||
|
||||
namespace Oms.Application.Services.Orders;
|
||||
|
||||
public partial class OrderService
|
||||
{
|
||||
private OrderFileManager OrderFileManager =>
|
||||
LazyServiceProvider.GetRequiredService<OrderFileManager>();
|
||||
|
||||
/// <summary>
|
||||
/// 查询订单附件(按类型分组)。客户登录仅返回系统面单。
|
||||
/// </summary>
|
||||
[HttpGet("/order/files")]
|
||||
public async Task<OrderFileGroupedOutput> GetOrderFilesAsync([FromQuery] int orderId)
|
||||
{
|
||||
await GetOrderOrThrowAsync(orderId);
|
||||
var customerView = OmsSessionHelper.IsCustomerLogin(CurrentUser);
|
||||
return await OrderFileManager.GetGroupedAsync(orderId, customerView);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传订单附件。客户账号禁止上传。
|
||||
/// </summary>
|
||||
[HttpPost("/order/files/upload")]
|
||||
[Consumes("multipart/form-data")]
|
||||
public async Task<List<OrderFileItemOutput>> UploadOrderFilesAsync(
|
||||
[FromForm] int orderId,
|
||||
[FromForm] string fileType,
|
||||
[FromForm] List<IFormFile> files)
|
||||
{
|
||||
await GetOrderOrThrowAsync(orderId);
|
||||
var collection = new FormFileCollection();
|
||||
foreach (var file in files ?? [])
|
||||
{
|
||||
collection.Add(file);
|
||||
}
|
||||
|
||||
return await OrderFileManager.UploadAsync(
|
||||
orderId,
|
||||
fileType,
|
||||
collection,
|
||||
CurrentUser.GetId<int>(),
|
||||
OmsSessionHelper.IsCustomerLogin(CurrentUser));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除订单附件。客户账号禁止删除。
|
||||
/// </summary>
|
||||
[HttpPost("/order/files/delete")]
|
||||
public async Task DeleteOrderFileAsync([FromBody] OrderFileDeleteInput input)
|
||||
{
|
||||
if (input == null || input.Id <= 0)
|
||||
{
|
||||
throw new BusinessException("附件 Id 无效");
|
||||
}
|
||||
|
||||
var entity = await LazyServiceProvider
|
||||
.GetRequiredService<Domain.OrdersAggregate.Repositories.IOrderFileRepository>()
|
||||
.FirstOrDefaultAsync(x => x.Id == input.Id)
|
||||
?? throw new BusinessException("附件不存在");
|
||||
|
||||
await GetOrderOrThrowAsync(entity.OrderId);
|
||||
await OrderFileManager.DeleteAsync(
|
||||
input.Id,
|
||||
OmsSessionHelper.IsCustomerLogin(CurrentUser));
|
||||
}
|
||||
}
|
||||
@ -1416,7 +1416,6 @@ public partial class OrderService
|
||||
SalesProductRulesVersion = snapshotBinding.RulesVersion,
|
||||
SalesProductPriceVersion = snapshotBinding.PriceVersion,
|
||||
LabelTemplateId = product.LabelTemplateId,
|
||||
LabelGenerateStatus = LabelGenerateStatusCodes.Pending,
|
||||
OrderStatus = OrderStatusCodes.Draft
|
||||
};
|
||||
}
|
||||
@ -1720,6 +1719,7 @@ public partial class OrderService
|
||||
private async Task<OrderParcelCreateOutput> BuildParcelCreateOutputAsync(Order order)
|
||||
{
|
||||
var mapped = (await MapOrderOutputsAsync([order])).First();
|
||||
var systemLabelUrl = await OrderFileManager.GetSystemLabelUrlAsync(order.Id);
|
||||
return new OrderParcelCreateOutput
|
||||
{
|
||||
OrderId = order.Id,
|
||||
@ -1728,8 +1728,8 @@ public partial class OrderService
|
||||
TrackNo = order.TrackNo,
|
||||
ConsigneeCountry = order.ConsigneeCountry,
|
||||
ConsigneeCountryName = order.ConsigneeCountryName,
|
||||
LabelUrl = !string.IsNullOrWhiteSpace(order.LabelUrl)
|
||||
? order.LabelUrl!
|
||||
LabelUrl = !string.IsNullOrWhiteSpace(systemLabelUrl)
|
||||
? systemLabelUrl!
|
||||
: order.OrderStatus == OrderStatusCodes.Predeclared
|
||||
? await CreateOrderPrintFileAsync(mapped)
|
||||
: string.Empty
|
||||
@ -1748,7 +1748,10 @@ public partial class OrderService
|
||||
{
|
||||
// 本地未开队列:同步生成,避免 Pending 悬挂
|
||||
var mapped = (await MapOrderOutputsAsync([order])).First();
|
||||
await LabelPdfGenerationService.GenerateAndPersistAsync(order, mapped);
|
||||
await LabelPdfGenerationService.GenerateAndPersistAsync(
|
||||
order,
|
||||
mapped,
|
||||
operatorUserId: CurrentUser.IsAuthenticated ? CurrentUser.GetId<int>() : 0);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -253,23 +253,23 @@ public partial class OrderService : ApplicationService
|
||||
var order = await GetOrderOrThrowAsync(input.Id);
|
||||
var mapped = (await MapOrderOutputsAsync([order])).First();
|
||||
|
||||
if (string.Equals(order.LabelGenerateStatus, LabelGenerateStatusCodes.Ready, StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.IsNullOrWhiteSpace(order.LabelUrl))
|
||||
var existingUrl = await OrderFileManager.GetSystemLabelUrlAsync(order.Id);
|
||||
if (!string.IsNullOrWhiteSpace(existingUrl))
|
||||
{
|
||||
return new OrderPrintOutput
|
||||
{
|
||||
PrintUrl = order.LabelUrl!,
|
||||
LabelGenerateStatus = order.LabelGenerateStatus,
|
||||
LabelGenerateError = order.LabelGenerateError
|
||||
PrintUrl = existingUrl!,
|
||||
LabelGenerateStatus = LabelGenerateStatusCodes.Ready
|
||||
};
|
||||
}
|
||||
|
||||
// Pending / Failed / 无状态:即时补生成(打印场景优先出 PDF)
|
||||
// 无系统面单:即时补生成(打印场景优先出 PDF)
|
||||
var result = await LabelPdfGenerationService.GenerateAndPersistAsync(
|
||||
order,
|
||||
mapped,
|
||||
templateIdOverride: null,
|
||||
operatorName: CurrentUser.UserName);
|
||||
operatorName: CurrentUser.UserName,
|
||||
operatorUserId: CurrentUser.IsAuthenticated ? CurrentUser.GetId<int>() : 0);
|
||||
|
||||
if (!result.Success || string.IsNullOrWhiteSpace(result.LabelUrl))
|
||||
{
|
||||
|
||||
@ -20,7 +20,7 @@ public class SalesProduct : OmsAuditedAggregateRoot
|
||||
public string? CustomerWaybill { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户面单绑定的标签模板 Id。
|
||||
/// 系统面单绑定的标签模板 Id。
|
||||
/// </summary>
|
||||
public int? LabelTemplateId { get; set; }
|
||||
|
||||
|
||||
46
src/Oms.Domain/OrdersAggregate/Consts/OrderFileTypeCodes.cs
Normal file
46
src/Oms.Domain/OrdersAggregate/Consts/OrderFileTypeCodes.cs
Normal file
@ -0,0 +1,46 @@
|
||||
namespace Oms.Domain.OrdersAggregate.Consts;
|
||||
|
||||
/// <summary>
|
||||
/// 订单附件类型。
|
||||
/// </summary>
|
||||
public static class OrderFileTypeCodes
|
||||
{
|
||||
public const string TradeProof = "tradeProof";
|
||||
public const string SystemLabel = "systemLabel";
|
||||
public const string DownstreamLabel = "downstreamLabel";
|
||||
public const string LastMileLabel = "lastMileLabel";
|
||||
public const string InfraredImage = "infraredImage";
|
||||
|
||||
public const int MaxFilesPerType = 20;
|
||||
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
TradeProof,
|
||||
SystemLabel,
|
||||
DownstreamLabel,
|
||||
LastMileLabel,
|
||||
InfraredImage
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 系统面单仅允许 1 份。
|
||||
/// </summary>
|
||||
public static int GetMaxCount(string fileType) =>
|
||||
string.Equals(fileType, SystemLabel, StringComparison.OrdinalIgnoreCase) ? 1 : MaxFilesPerType;
|
||||
|
||||
public static bool IsValid(string? fileType) =>
|
||||
!string.IsNullOrWhiteSpace(fileType) && All.Contains(fileType.Trim());
|
||||
|
||||
public static string Normalize(string fileType) => fileType.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订单附件来源。
|
||||
/// </summary>
|
||||
public static class OrderFileSourceCodes
|
||||
{
|
||||
public const string Upload = "upload";
|
||||
public const string SystemGenerate = "systemGenerate";
|
||||
public const string Downstream = "downstream";
|
||||
public const string Migrate = "migrate";
|
||||
}
|
||||
@ -168,25 +168,7 @@ public class Order : OmsAuditedAggregateRoot
|
||||
public byte IsRemote { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 面单 PDF 相对路径(如 /uploads/prints/orders/xxx.pdf)。
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 500, IsNullable = true)]
|
||||
public string? LabelUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 出单时实际使用的标签模板 Id。
|
||||
/// 出单时实际使用的标签模板 Id(面单文件见 t_order_file.systemLabel)。
|
||||
/// </summary>
|
||||
public int? LabelTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 面单生成状态:Pending / Ready / Failed。
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 32, IsNullable = true)]
|
||||
public string? LabelGenerateStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 面单生成失败原因。
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 500, IsNullable = true)]
|
||||
public string? LabelGenerateError { get; set; }
|
||||
}
|
||||
|
||||
41
src/Oms.Domain/OrdersAggregate/Entities/OrderFile.cs
Normal file
41
src/Oms.Domain/OrdersAggregate/Entities/OrderFile.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using Oms.Domain.Shared;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.Domain.OrdersAggregate.Entities;
|
||||
|
||||
[SugarTable("t_order_file")]
|
||||
public class OrderFile : OmsAuditedAggregateRoot
|
||||
{
|
||||
public int OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 附件类型:tradeProof / systemLabel / downstreamLabel / lastMileLabel / infraredImage。
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 32)]
|
||||
public string FileType { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(Length = 255, IsNullable = true)]
|
||||
public string? FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 相对路径或外链 URL。
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 512)]
|
||||
public string FileUrl { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(Length = 128, IsNullable = true)]
|
||||
public string? ContentType { get; set; }
|
||||
|
||||
public long? FileSize { get; set; }
|
||||
|
||||
public int Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// upload / systemGenerate / downstream / migrate。
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 32)]
|
||||
public string Source { get; set; } = "upload";
|
||||
|
||||
[SugarColumn(Length = 512, IsNullable = true)]
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
using Fake.Domain.Repositories;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
|
||||
namespace Oms.Domain.OrdersAggregate.Repositories;
|
||||
|
||||
public interface IOrderFileRepository : IRepository<OrderFile>
|
||||
{
|
||||
Task<List<OrderFile>> GetByOrderIdAsync(int orderId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<OrderFile?> GetByOrderAndTypeAsync(
|
||||
int orderId,
|
||||
string fileType,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<List<OrderFile>> GetByOrderAndTypeListAsync(
|
||||
int orderId,
|
||||
string fileType,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task SoftDeleteByOrderAndTypeAsync(
|
||||
int orderId,
|
||||
string fileType,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
47
src/Oms.SqlSugarCore/Orders/OrderFileRepository.cs
Normal file
47
src/Oms.SqlSugarCore/Orders/OrderFileRepository.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
using Oms.SqlSugarCore.Repositories;
|
||||
|
||||
namespace Oms.SqlSugarCore.Orders;
|
||||
|
||||
public class OrderFileRepository
|
||||
: OmsSqlSugarRepository<OrderFile>,
|
||||
IOrderFileRepository,
|
||||
IScopedDependency
|
||||
{
|
||||
public Task<List<OrderFile>> GetByOrderIdAsync(int orderId, CancellationToken cancellationToken = default) =>
|
||||
GetListAsync(
|
||||
x => x.OrderId == orderId,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
public Task<OrderFile?> GetByOrderAndTypeAsync(
|
||||
int orderId,
|
||||
string fileType,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
FirstOrDefaultAsync(
|
||||
x => x.OrderId == orderId && x.FileType == fileType,
|
||||
cancellationToken);
|
||||
|
||||
public Task<List<OrderFile>> GetByOrderAndTypeListAsync(
|
||||
int orderId,
|
||||
string fileType,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
GetListAsync(
|
||||
x => x.OrderId == orderId && x.FileType == fileType,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
public async Task SoftDeleteByOrderAndTypeAsync(
|
||||
int orderId,
|
||||
string fileType,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var list = await GetByOrderAndTypeListAsync(orderId, fileType, cancellationToken);
|
||||
foreach (var item in list)
|
||||
{
|
||||
item.IsDeleted = true;
|
||||
item.UpdateTime = DateTime.Now;
|
||||
await UpdateAsync(item, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -49,7 +49,8 @@ public class ItfxCreateOrderOneDataParserTests
|
||||
Assert.Equal("OMS001", order.ServiceNo);
|
||||
Assert.Equal("1ZK6B4420326915547", order.DownstreamServiceNo);
|
||||
Assert.Equal("TRACK123", order.TrackNo);
|
||||
Assert.Equal("https://label.example/a.pdf", order.LabelUrl);
|
||||
// LabelUrl 由调用方写入 t_order_file,不再回写订单表
|
||||
Assert.Equal("https://label.example/a.pdf", data.LabelUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user