admin/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs
zzh d1d31fee8a
All checks were successful
Build and Deploy / deploy (push) Successful in 1m43s
fix: 仓储图片上传支持 customerId,9610 明细补并发戳且主从安全
上传接口接受 query customerId;ConcurrencyStamp 默认改为空串并用主键确定性回填,避免 Galera UUID unsafe。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 12:09:36 +08:00

264 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Linq.Expressions;
using Fake.Application;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.Warehouse;
using Oms.Application.Shared;
using Oms.Application.Storage;
namespace Oms.Application.Services.Warehouse;
public partial class WarehouseService
{
private IFileStorage FileStorage =>
LazyServiceProvider.GetRequiredService<IFileStorage>();
/// <summary>
/// 查询 SKU 列表。
/// </summary>
/// <param name="input">入参:SKU 分页查询条件。</param>
/// <returns>SKU 分页结果。</returns>
[HttpPost("/warehouse/sku/list")]
public async Task<OmsPageResult<WarehouseSkuOutput>> GetSkuListAsync(WarehouseSkuListInput input)
{
var customerId = await ResolveListCustomerIdAsync(input.CustomerId);
var (hasCustomerFilter, customerIdValue) = CustomerFilter(customerId);
var keyword = input.Keyword?.Trim();
var hasKeyword = !string.IsNullOrWhiteSpace(keyword);
Expression<Func<Domain.WarehouseAggregate.Entities.CSku, bool>> predicate = x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasKeyword || x.SkuCode.Contains(keyword!) || x.SkuName.Contains(keyword!));
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var entities = await SkuRepository.GetPagedListAsync(
predicate,
pageIndex,
pageSize,
new Dictionary<string, bool> { [nameof(Domain.WarehouseAggregate.Entities.CSku.UpdateTime)] = false, [nameof(Domain.WarehouseAggregate.Entities.CSku.Id)] = false });
var total = await SkuRepository.CountAsync(predicate);
var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray();
var customers = customerIds.Length == 0
? []
: await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
var customerMap = customers.ToDictionary(x => x.Id);
return new OmsPageResult<WarehouseSkuOutput>
{
Items = entities.Select(x =>
{
var output = MapSkuListOutput(x);
if (customerMap.TryGetValue(x.CustomerId, out var customer))
{
output.CustomerName = customer.CustomerName;
output.CustomerCode = customer.CustomerCode;
}
return output;
}).ToList(),
Total = total
};
}
/// <summary>
/// 查询 SKU 详情。
/// </summary>
/// <param name="id">SKU ID。</param>
/// <param name="customerId">客户 ID(可选)。</param>
/// <returns>SKU 详情。</returns>
[HttpGet("/warehouse/sku/detail")]
public async Task<WarehouseSkuOutput> GetSkuDetailAsync([FromQuery] int id, [FromQuery] int? customerId = null)
{
var resolvedCustomerId = await ResolveListCustomerIdAsync(customerId);
var (hasCustomerFilter, customerIdValue) = CustomerFilter(resolvedCustomerId);
var entity = await GetSkuOrThrowAsync(resolvedCustomerId, id);
var inventories = await InventoryRepository.GetListAsync(x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& x.SkuId == id);
return MapSkuOutput(entity, inventories.Sum(x => x.Qty));
}
/// <summary>
/// 保存 SKU(新增或编辑)。
/// </summary>
/// <param name="input">入参:SKU 保存数据。</param>
/// <returns>保存后的 SKU。</returns>
[HttpPost("/warehouse/sku/save")]
public async Task<WarehouseSkuOutput> SaveSkuAsync(WarehouseSkuSaveInput input)
{
var customerId = await ResolveMutationCustomerIdAsync(input.CustomerId);
var skuCode = input.SkuCode.Trim();
var skuName = input.SkuName.Trim();
if (string.IsNullOrWhiteSpace(skuCode) || string.IsNullOrWhiteSpace(skuName))
{
throw new BusinessException("SKU 编码和名称不能为空");
}
Domain.WarehouseAggregate.Entities.CSku entity;
if (input.Id > 0)
{
entity = await GetSkuOrThrowAsync(customerId, input.Id);
}
else
{
entity = new Domain.WarehouseAggregate.Entities.CSku { CustomerId = customerId };
}
var duplicate = await SkuRepository.FirstOrDefaultAsync(x =>
x.CustomerId == customerId && x.SkuCode == skuCode && x.Id != entity.Id);
if (duplicate != null)
{
throw new BusinessException("SKU 编码已存在");
}
entity.SkuCode = skuCode;
entity.SkuName = skuName;
entity.SkuNameEn = string.IsNullOrWhiteSpace(input.SkuNameEn) ? null : input.SkuNameEn.Trim();
entity.SkuImageUrl = FileStorage.ToStorageKey(input.SkuImageUrl);
entity.UnitPrice = input.UnitPrice;
entity.ExHsCode = string.IsNullOrWhiteSpace(input.ExHsCode) ? null : input.ExHsCode.Trim();
entity.ImHsCode = string.IsNullOrWhiteSpace(input.ImHsCode) ? null : input.ImHsCode.Trim();
entity.WeightG = input.WeightG;
entity.LengthCm = input.LengthCm;
entity.WidthCm = input.WidthCm;
entity.HeightCm = input.HeightCm;
entity.Remark = string.IsNullOrWhiteSpace(input.Remark) ? null : input.Remark.Trim();
if (input.Id > 0)
{
await SkuRepository.UpdateAsync(entity);
}
else
{
await SkuRepository.InsertAsync(entity);
}
return MapSkuOutput(entity, null);
}
/// <summary>
/// 删除 SKU。
/// </summary>
/// <param name="input">入参:SKU ID。</param>
[HttpPost("/warehouse/sku/delete")]
public async Task DeleteSkuAsync(IdInput input)
{
var customerId = await ResolveListCustomerIdAsync(null);
var entity = await GetSkuOrThrowAsync(customerId, input.Id);
var hasInventory = await InventoryRepository.CountAsync(x =>
x.SkuId == entity.Id && x.Qty > 0);
if (hasInventory > 0)
{
throw new BusinessException("SKU 仍有库存,不能删除");
}
await SkuRepository.DeleteAsync(entity);
}
/// <summary>
/// 上传 SKU 图片,返回可访问相对路径。
/// </summary>
[HttpPost("/warehouse/sku/upload-image")]
[Consumes("multipart/form-data")]
public async Task<object> UploadSkuImageAsync(IFormFile file, [FromQuery] int? customerId = null)
{
await ResolveMutationCustomerIdAsync(customerId);
var key = await OmsFileUploadHelper.SaveWarehouseSkuImageAsync(file, FileStorage);
var url = FileStorage.GetPublicUrl(key);
return new { key, url, fileUrl = key };
}
/// <summary>
/// 绑定平台 SKU 与库内 SKU 映射。
/// </summary>
/// <param name="input">入参:映射关系。</param>
/// <returns>映射结果。</returns>
[HttpPost("/warehouse/sku/bind-mapping")]
public async Task<WarehouseSkuMappingOutput> BindSkuMappingAsync(WarehouseSkuBindMappingInput input)
{
var customerId = await ResolveMutationCustomerIdAsync(input.CustomerId);
var platformSku = input.PlatformSku.Trim();
if (string.IsNullOrWhiteSpace(platformSku))
{
throw new BusinessException("平台 SKU 不能为空");
}
var sku = await GetSkuOrThrowAsync(customerId, input.SkuId);
var existing = await SkuPlatformMapRepository.FirstOrDefaultAsync(x =>
x.CustomerId == customerId && x.StoreId == input.StoreId && x.PlatformSku == platformSku);
if (existing == null)
{
existing = new Domain.WarehouseAggregate.Entities.CSkuPlatformMap
{
CustomerId = customerId,
StoreId = input.StoreId,
PlatformSku = platformSku,
SkuId = sku.Id,
SkuCode = sku.SkuCode
};
await SkuPlatformMapRepository.InsertAsync(existing);
}
else
{
existing.SkuId = sku.Id;
existing.SkuCode = sku.SkuCode;
await SkuPlatformMapRepository.UpdateAsync(existing);
}
return new WarehouseSkuMappingOutput
{
Id = existing.Id,
StoreId = existing.StoreId,
PlatformSku = existing.PlatformSku,
SkuId = existing.SkuId,
SkuCode = existing.SkuCode
};
}
private WarehouseSkuOutput MapSkuListOutput(Domain.WarehouseAggregate.Entities.CSku entity) =>
new()
{
Id = entity.Id,
CustomerId = entity.CustomerId,
SkuCode = entity.SkuCode,
SkuName = entity.SkuName,
SkuNameEn = entity.SkuNameEn,
SkuImageUrl = FileStorage.GetPublicUrl(entity.SkuImageUrl),
UnitPrice = entity.UnitPrice,
ExHsCode = entity.ExHsCode,
ImHsCode = entity.ImHsCode,
WeightG = entity.WeightG,
LengthCm = entity.LengthCm,
WidthCm = entity.WidthCm,
HeightCm = entity.HeightCm,
Remark = entity.Remark,
CreateTime = entity.CreateTime,
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
};
private WarehouseSkuOutput MapSkuOutput(Domain.WarehouseAggregate.Entities.CSku entity, int? totalQty) =>
new()
{
Id = entity.Id,
CustomerId = entity.CustomerId,
SkuCode = entity.SkuCode,
SkuName = entity.SkuName,
SkuNameEn = entity.SkuNameEn,
SkuImageUrl = FileStorage.GetPublicUrl(entity.SkuImageUrl),
UnitPrice = entity.UnitPrice,
ExHsCode = entity.ExHsCode,
ImHsCode = entity.ImHsCode,
WeightG = entity.WeightG,
LengthCm = entity.LengthCm,
WidthCm = entity.WidthCm,
HeightCm = entity.HeightCm,
Remark = entity.Remark,
TotalQty = totalQty,
CreateTime = entity.CreateTime,
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
};
}