修复列表状态Tab将all误当状态筛选,并完善插件草稿TTL与客户上下文。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-15 12:46:05 +08:00
parent f0d0659750
commit 3d0db418d2
7 changed files with 182 additions and 36 deletions

View File

@ -96,6 +96,9 @@ public class PlatformStoreOrderListInput : PlatformStorePageInput
{
public int? StoreId { get; set; }
/// <summary>
/// 履约状态:pending / toShip / shipped / canceled / suspended / completed;空或 all 表示全部。
/// </summary>
public string? FulfillStatus { get; set; }
public string? PlatformOrderNo { get; set; }

View File

@ -152,6 +152,9 @@ public class WarehouseInboundListInput : WarehousePageInput
{
public int? WarehouseId { get; set; }
/// <summary>
/// 收货状态:forecasted / received / completed;空或 all 表示全部。
/// </summary>
public string? ReceiptStatus { get; set; }
public string? Keyword { get; set; }
@ -314,6 +317,9 @@ public class WarehouseParcelListInput : WarehousePageInput
{
public int? WarehouseId { get; set; }
/// <summary>
/// 生命周期状态:inTransit / arrived / stored;空或 all 表示全部。
/// </summary>
public string? LifecycleStatus { get; set; }
public string? Keyword { get; set; }

View File

@ -599,21 +599,73 @@ public class PlatformStoreOrderService : ApplicationService
private static Expression<Func<SalesStoreOrder, bool>> BuildListPredicate(int? customerId, PlatformStoreOrderListInput input)
{
var (hasCustomerFilter, customerIdValue) = WarehouseCustomerHelper.ToCustomerFilter(customerId);
var fulfillStatus = input.FulfillStatus?.Trim();
// 前端 Tab「全部」会传 all / 空;需忽略,不能按字面 FulfillStatus == "all" 过滤
var fulfillStatus = NormalizeFulfillStatusFilter(input.FulfillStatus);
var hasFulfillStatusFilter = !string.IsNullOrWhiteSpace(fulfillStatus);
var platformOrderNo = input.PlatformOrderNo?.Trim();
var hasStoreFilter = input.StoreId.HasValue;
var storeIdValue = input.StoreId.GetValueOrDefault();
var hasFulfillStatusFilter = !string.IsNullOrWhiteSpace(fulfillStatus);
var hasPlatformOrderNoFilter = !string.IsNullOrWhiteSpace(platformOrderNo);
var hasSuspendedFilter = input.IsSuspended.HasValue;
var isSuspendedValue = input.IsSuspended.GetValueOrDefault();
return x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasStoreFilter || x.StoreId == storeIdValue)
&& (!hasFulfillStatusFilter || x.FulfillStatus == fulfillStatus)
&& (!hasSuspendedFilter || x.IsSuspended == isSuspendedValue)
&& (!hasPlatformOrderNoFilter || x.PlatformOrderNo.Contains(platformOrderNo!));
return hasFulfillStatusFilter
? x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasStoreFilter || x.StoreId == storeIdValue)
&& x.FulfillStatus == fulfillStatus
&& (!hasSuspendedFilter || x.IsSuspended == isSuspendedValue)
&& (!hasPlatformOrderNoFilter || x.PlatformOrderNo.Contains(platformOrderNo!))
: x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasStoreFilter || x.StoreId == storeIdValue)
&& (!hasSuspendedFilter || x.IsSuspended == isSuspendedValue)
&& (!hasPlatformOrderNoFilter || x.PlatformOrderNo.Contains(platformOrderNo!));
}
/// <summary>
/// 规范化履约状态筛选:空 / all 表示不按状态过滤;已知编码统一为规范值。
/// </summary>
private static string? NormalizeFulfillStatusFilter(string? status)
{
var value = status?.Trim();
if (string.IsNullOrEmpty(value)
|| value.Equals("all", StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (value.Equals(SalesStoreFulfillStatusCodes.Pending, StringComparison.OrdinalIgnoreCase))
{
return SalesStoreFulfillStatusCodes.Pending;
}
if (value.Equals(SalesStoreFulfillStatusCodes.ToShip, StringComparison.OrdinalIgnoreCase))
{
return SalesStoreFulfillStatusCodes.ToShip;
}
if (value.Equals(SalesStoreFulfillStatusCodes.Canceled, StringComparison.OrdinalIgnoreCase))
{
return SalesStoreFulfillStatusCodes.Canceled;
}
if (value.Equals(SalesStoreFulfillStatusCodes.Suspended, StringComparison.OrdinalIgnoreCase))
{
return SalesStoreFulfillStatusCodes.Suspended;
}
if (value.Equals(SalesStoreFulfillStatusCodes.Shipped, StringComparison.OrdinalIgnoreCase))
{
return SalesStoreFulfillStatusCodes.Shipped;
}
if (value.Equals(SalesStoreFulfillStatusCodes.Completed, StringComparison.OrdinalIgnoreCase))
{
return SalesStoreFulfillStatusCodes.Completed;
}
return value;
}
private static PlatformStoreOrderOutput MapOrderOutput(

View File

@ -10,12 +10,13 @@ using Oms.Domain.WarehouseAggregate.Repositories;
namespace Oms.Application.Services.Plugin;
/// <summary>
/// 浏览器采集插件草稿 / 查重(对齐哪哪发 wepost API)。
/// 浏览器采集插件草稿 / 查重(对齐哪哪发插件 API)。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Plugin")]
public class PluginBrowserService : ApplicationService
{
private static readonly TimeSpan DraftTtl = TimeSpan.FromSeconds(90);
// 确认页打开 / SPA 冷启动可能超过 90s,适当放宽避免「草稿已过期」误判为解析失败
private static readonly TimeSpan DraftTtl = TimeSpan.FromMinutes(5);
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
private ICParcelRepository ParcelRepository => LazyServiceProvider.GetRequiredService<ICParcelRepository>();
@ -25,19 +26,20 @@ public class PluginBrowserService : ApplicationService
/// <summary>
/// 缓存采集草稿,返回 key(确认页 getPackage 使用)。
/// 管理端须传 customerId(或会话可解析客户)。
/// </summary>
[HttpPost("/plugin/browser/add")]
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
public async Task<object> AddDraftAsync([FromForm] string? item)
public async Task<object> AddDraftAsync([FromForm] string? item, [FromForm] int? customerId)
{
var customerId = await ResolveRequiredCustomerIdAsync();
var resolvedCustomerId = await ResolveRequiredCustomerIdAsync(customerId);
var payload = item;
if (string.IsNullOrWhiteSpace(payload))
{
throw new BusinessException("草稿内容不能为空");
}
var key = $"{customerId}{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}{Random.Shared.Next(100, 999)}";
var key = $"{resolvedCustomerId}{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}{Random.Shared.Next(100, 999)}";
await Cache.SetAsync(
CacheKey(key),
Encoding.UTF8.GetBytes(payload!),
@ -68,21 +70,28 @@ public class PluginBrowserService : ApplicationService
/// <summary>
/// 查重:快递单号是否已存在于当前客户的「我的包裹」。
/// 无客户上下文(如管理员未选客户)时不抛错,按「未添加」返回,避免插件按钮卡在「系统错误」。
/// </summary>
[HttpGet("/plugin/browser/selectPackage")]
public async Task<object> SelectPackageAsync([FromQuery] string? expressNum, [FromQuery] string? orderNo)
public async Task<object> SelectPackageAsync(
[FromQuery] string? expressNum,
[FromQuery] string? orderNo,
[FromQuery] int? customerId)
{
var customerId = await ResolveRequiredCustomerIdAsync();
var resolvedCustomerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
customerId);
var trackingNo = (expressNum ?? string.Empty).Trim();
var trackExists = false;
if (!string.IsNullOrWhiteSpace(trackingNo))
if (resolvedCustomerId is > 0 && !string.IsNullOrWhiteSpace(trackingNo))
{
var hit = await ParcelRepository.FirstOrDefaultAsync(x =>
x.CustomerId == customerId && x.TrackingNo == trackingNo);
x.CustomerId == resolvedCustomerId.Value && x.TrackingNo == trackingNo);
trackExists = hit != null;
}
// OMS 包裹表无独立订单号字段;orderNo 仅作兼容保留,默认同 track 不分别标记
// OMS 包裹表无独立订单号字段;orderNo 仅作兼容保留
return new
{
trackNo = trackExists,
@ -96,7 +105,11 @@ public class PluginBrowserService : ApplicationService
[HttpGet("/plugin/browser/getPluginWhseInfo")]
public async Task<object?> GetPluginWhseInfoAsync()
{
await ResolveRequiredCustomerIdAsync();
// 管理员无客户上下文时仍可取仓库列表,供确认页默认值
_ = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
var list = await WarehouseAddressRepository.GetPagedListAsync(
_ => true,
1,
@ -115,11 +128,11 @@ public class PluginBrowserService : ApplicationService
};
}
private async Task<int> ResolveRequiredCustomerIdAsync() =>
private async Task<int> ResolveRequiredCustomerIdAsync(int? requestedCustomerId) =>
await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
requestedCustomerId);
private static string CacheKey(string key) => $"oms:plugin:parcel:draft:{key}";
}

View File

@ -23,16 +23,22 @@ public partial class WarehouseService
var (hasCustomerFilter, customerIdValue) = CustomerFilter(customerId);
var keyword = input.Keyword?.Trim();
var hasKeyword = !string.IsNullOrWhiteSpace(keyword);
var receiptStatus = input.ReceiptStatus?.Trim();
// 前端 Tab「全部」会传 all / 空;需忽略,不能按字面 ReceiptStatus == "all" 过滤
var receiptStatus = NormalizeInboundReceiptStatusFilter(input.ReceiptStatus);
var hasReceiptStatusFilter = !string.IsNullOrWhiteSpace(receiptStatus);
var hasWarehouseFilter = input.WarehouseId.HasValue;
var warehouseIdValue = input.WarehouseId.GetValueOrDefault();
var hasReceiptStatusFilter = !string.IsNullOrWhiteSpace(receiptStatus);
Expression<Func<CInboundNotice, bool>> predicate = x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)
&& (!hasReceiptStatusFilter || x.ReceiptStatus == receiptStatus)
&& (!hasKeyword || x.NoticeNo.Contains(keyword!) || (x.MarkReference != null && x.MarkReference.Contains(keyword!)));
Expression<Func<CInboundNotice, bool>> predicate = hasReceiptStatusFilter
? x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)
&& x.ReceiptStatus == receiptStatus
&& (!hasKeyword || x.NoticeNo.Contains(keyword!) || (x.MarkReference != null && x.MarkReference.Contains(keyword!)))
: x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)
&& (!hasKeyword || x.NoticeNo.Contains(keyword!) || (x.MarkReference != null && x.MarkReference.Contains(keyword!)));
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
@ -59,6 +65,36 @@ public partial class WarehouseService
};
}
/// <summary>
/// 规范化入库状态筛选:空 / all 表示不按状态过滤;已知编码统一为规范值。
/// </summary>
private static string? NormalizeInboundReceiptStatusFilter(string? status)
{
var value = status?.Trim();
if (string.IsNullOrEmpty(value)
|| value.Equals("all", StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (value.Equals(WarehouseReceiptStatusCodes.Forecasted, StringComparison.OrdinalIgnoreCase))
{
return WarehouseReceiptStatusCodes.Forecasted;
}
if (value.Equals(WarehouseReceiptStatusCodes.Received, StringComparison.OrdinalIgnoreCase))
{
return WarehouseReceiptStatusCodes.Received;
}
if (value.Equals(WarehouseReceiptStatusCodes.Completed, StringComparison.OrdinalIgnoreCase))
{
return WarehouseReceiptStatusCodes.Completed;
}
return value;
}
/// <summary>
/// 查询入库预报详情。
/// </summary>

View File

@ -23,16 +23,22 @@ public partial class WarehouseService
var (hasCustomerFilter, customerIdValue) = CustomerFilter(customerId);
var keyword = input.Keyword?.Trim();
var hasKeyword = !string.IsNullOrWhiteSpace(keyword);
var lifecycleStatus = input.LifecycleStatus?.Trim();
// 前端 Tab「全部」会传 all / 空;需忽略,不能按字面 LifecycleStatus == "all" 过滤
var lifecycleStatus = NormalizeParcelLifecycleStatusFilter(input.LifecycleStatus);
var hasLifecycleStatusFilter = !string.IsNullOrWhiteSpace(lifecycleStatus);
var hasWarehouseFilter = input.WarehouseId.HasValue;
var warehouseIdValue = input.WarehouseId.GetValueOrDefault();
var hasLifecycleStatusFilter = !string.IsNullOrWhiteSpace(lifecycleStatus);
Expression<Func<CParcel, bool>> predicate = x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)
&& (!hasLifecycleStatusFilter || x.LifecycleStatus == lifecycleStatus)
&& (!hasKeyword || x.TrackingNo.Contains(keyword!) || (x.CarrierName != null && x.CarrierName.Contains(keyword!)));
Expression<Func<CParcel, bool>> predicate = hasLifecycleStatusFilter
? x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)
&& x.LifecycleStatus == lifecycleStatus
&& (!hasKeyword || x.TrackingNo.Contains(keyword!) || (x.CarrierName != null && x.CarrierName.Contains(keyword!)))
: x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)
&& (!hasKeyword || x.TrackingNo.Contains(keyword!) || (x.CarrierName != null && x.CarrierName.Contains(keyword!)));
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
@ -55,6 +61,36 @@ public partial class WarehouseService
};
}
/// <summary>
/// 规范化列表状态筛选:空 / all 表示不按状态过滤;已知编码统一为规范值。
/// </summary>
private static string? NormalizeParcelLifecycleStatusFilter(string? status)
{
var value = status?.Trim();
if (string.IsNullOrEmpty(value)
|| value.Equals("all", StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (value.Equals(WarehouseParcelLifecycleCodes.InTransit, StringComparison.OrdinalIgnoreCase))
{
return WarehouseParcelLifecycleCodes.InTransit;
}
if (value.Equals(WarehouseParcelLifecycleCodes.Arrived, StringComparison.OrdinalIgnoreCase))
{
return WarehouseParcelLifecycleCodes.Arrived;
}
if (value.Equals(WarehouseParcelLifecycleCodes.Stored, StringComparison.OrdinalIgnoreCase))
{
return WarehouseParcelLifecycleCodes.Stored;
}
return value;
}
/// <summary>
/// 保存包裹(新增或编辑)。
/// </summary>

View File

@ -35,7 +35,7 @@ public static class WarehouseCustomerHelper
var customerId = await ResolveListCustomerIdAsync(customerRepository, currentUser, requestedCustomerId);
if (!customerId.HasValue || customerId.Value <= 0)
{
throw new BusinessException("客户ID不能为空");
throw new BusinessException("客户ID不能为空(管理端请先在「我的包裹」选择客户后再使用采集插件)");
}
return customerId.Value;