新增快递到仓 API 与表结构,并支持 SKU 图片上传。
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
600ab37594
commit
d0df7e864c
18
db.sql
18
db.sql
@ -40,6 +40,7 @@ DROP TABLE IF EXISTS `C_inbound_box_line`;
|
||||
DROP TABLE IF EXISTS `C_inbound_box`;
|
||||
DROP TABLE IF EXISTS `C_inbound_notice`;
|
||||
DROP TABLE IF EXISTS `C_parcel_item`;
|
||||
DROP TABLE IF EXISTS `C_parcel_arrival`;
|
||||
DROP TABLE IF EXISTS `C_parcel`;
|
||||
DROP TABLE IF EXISTS `C_inventory`;
|
||||
DROP TABLE IF EXISTS `C_sku_platform_map`;
|
||||
@ -1357,6 +1358,23 @@ CREATE TABLE `C_parcel` (
|
||||
KEY `idx_c_parcel_status` (`CustomerId`, `LifecycleStatus`)
|
||||
) COMMENT='仓储-客户包裹';
|
||||
|
||||
CREATE TABLE `C_parcel_arrival` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`WarehouseId` INT NOT NULL COMMENT '仓库ID',
|
||||
`ParcelId` INT NULL COMMENT '关联包裹 C_parcel.Id',
|
||||
`TrackingNo` VARCHAR(64) NOT NULL COMMENT '国内快递单号',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_c_parcel_arrival_tracking` (`CustomerId`, `TrackingNo`, `WarehouseId`),
|
||||
KEY `idx_c_parcel_arrival_ctime` (`CreateTime`)
|
||||
) COMMENT='仓储-快递到仓记录';
|
||||
|
||||
CREATE TABLE `C_parcel_item` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`ParcelId` INT NOT NULL COMMENT '包裹ID → C_parcel.Id',
|
||||
|
||||
20
sql/C_parcel_arrival.sql
Normal file
20
sql/C_parcel_arrival.sql
Normal file
@ -0,0 +1,20 @@
|
||||
-- 已有库增量:快递到仓表 C_parcel_arrival
|
||||
-- 对应功能:操作 → 快递到仓;全量结构见根目录 db.sql
|
||||
-- 可重复执行(IF NOT EXISTS)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `C_parcel_arrival` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '客户ID',
|
||||
`WarehouseId` INT NOT NULL COMMENT '仓库ID',
|
||||
`ParcelId` INT NULL COMMENT '关联包裹 C_parcel.Id',
|
||||
`TrackingNo` VARCHAR(64) NOT NULL COMMENT '国内快递单号',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_c_parcel_arrival_tracking` (`CustomerId`, `TrackingNo`, `WarehouseId`),
|
||||
KEY `idx_c_parcel_arrival_ctime` (`CreateTime`)
|
||||
) COMMENT='仓储-快递到仓记录';
|
||||
@ -3,5 +3,5 @@
|
||||
OMS / 共用库的建表脚本已统一到仓库根目录 `db.sql`。
|
||||
|
||||
- 新环境:直接执行 `db.sql`(会 DROP 后重建,勿在有生产数据的库上执行)
|
||||
- 已有库:用 diff 对比 `db.sql` 与现库结构后手工 ALTER,不再维护 `migrate_*.sql`
|
||||
- 已有库:用 diff 对比 `db.sql` 与现库结构后手工 ALTER;本目录下单个表脚本(如 `C_parcel_arrival.sql`)可直接在已有库执行增量建表
|
||||
- 数据权限表(`user_org_perm` 等)若 RBAC 与 OMS 分库,请只在 RBAC 库执行 `db.sql` 末尾对应段落
|
||||
|
||||
@ -466,3 +466,44 @@ public class WarehouseInventoryRelocateInput
|
||||
[Range(1, int.MaxValue)]
|
||||
public int Qty { get; set; }
|
||||
}
|
||||
|
||||
// --- Parcel arrival(快递到仓)---
|
||||
|
||||
public class WarehouseParcelArrivalListInput : WarehousePageInput
|
||||
{
|
||||
public int? WarehouseId { get; set; }
|
||||
|
||||
public string? Keyword { get; set; }
|
||||
}
|
||||
|
||||
public class WarehouseParcelArrivalCreateInput
|
||||
{
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
[Required]
|
||||
public int WarehouseId { get; set; }
|
||||
|
||||
[Required]
|
||||
public string TrackingNo { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class WarehouseParcelArrivalOutput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
public int WarehouseId { get; set; }
|
||||
|
||||
public string? WarehouseName { get; set; }
|
||||
|
||||
public int? ParcelId { get; set; }
|
||||
|
||||
public string TrackingNo { get; set; } = string.Empty;
|
||||
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
public int CreateUserId { get; set; }
|
||||
|
||||
public string? CreateBy { get; set; }
|
||||
}
|
||||
|
||||
@ -0,0 +1,165 @@
|
||||
using System.Linq.Expressions;
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Dtos.Warehouse;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.SystemAggregate.Repositories;
|
||||
using Oms.Domain.WarehouseAggregate.Consts;
|
||||
using Oms.Domain.WarehouseAggregate.Entities;
|
||||
using Oms.Domain.WarehouseAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Services.Warehouse;
|
||||
|
||||
public partial class WarehouseService
|
||||
{
|
||||
private ICParcelArrivalRepository ParcelArrivalRepository =>
|
||||
LazyServiceProvider.GetRequiredService<ICParcelArrivalRepository>();
|
||||
|
||||
private IWarehouseAddressRepository WarehouseAddressRepository =>
|
||||
LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>();
|
||||
|
||||
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
|
||||
LazyServiceProvider.GetRequiredService<OmsRbacUserDisplayNameResolver>();
|
||||
|
||||
/// <summary>
|
||||
/// 快递到仓记录列表。
|
||||
/// </summary>
|
||||
[HttpPost("/warehouse/parcel-arrival/list")]
|
||||
public async Task<OmsPageResult<WarehouseParcelArrivalOutput>> GetParcelArrivalListAsync(
|
||||
WarehouseParcelArrivalListInput input)
|
||||
{
|
||||
var customerId = await ResolveListCustomerIdAsync(input.CustomerId);
|
||||
var (hasCustomerFilter, customerIdValue) = CustomerFilter(customerId);
|
||||
var keyword = input.Keyword?.Trim();
|
||||
var hasKeyword = !string.IsNullOrWhiteSpace(keyword);
|
||||
var hasWarehouseFilter = input.WarehouseId is > 0;
|
||||
var warehouseIdValue = input.WarehouseId.GetValueOrDefault();
|
||||
|
||||
Expression<Func<CParcelArrival, bool>> predicate = x =>
|
||||
(!hasCustomerFilter || x.CustomerId == customerIdValue)
|
||||
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)
|
||||
&& (!hasKeyword || x.TrackingNo.Contains(keyword!));
|
||||
|
||||
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
||||
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
||||
var entities = await ParcelArrivalRepository.GetPagedListAsync(
|
||||
predicate,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
new Dictionary<string, bool>
|
||||
{
|
||||
[nameof(CParcelArrival.CreateTime)] = false,
|
||||
[nameof(CParcelArrival.Id)] = false
|
||||
});
|
||||
var total = await ParcelArrivalRepository.CountAsync(predicate);
|
||||
|
||||
var warehouseIds = entities.Select(x => x.WarehouseId).Distinct().ToArray();
|
||||
var warehouses = warehouseIds.Length == 0
|
||||
? []
|
||||
: await WarehouseAddressRepository.GetListAsync(x => warehouseIds.Contains(x.Id));
|
||||
var warehouseNameMap = warehouses.ToDictionary(x => x.Id, x => x.WarehouseName);
|
||||
|
||||
var userIds = entities.Select(x => x.CreateUserId).Distinct().ToArray();
|
||||
var userNameMap = await UserDisplayNameResolver.ResolveManyAsync(userIds);
|
||||
|
||||
return new OmsPageResult<WarehouseParcelArrivalOutput>
|
||||
{
|
||||
Items = entities.Select(x => new WarehouseParcelArrivalOutput
|
||||
{
|
||||
Id = x.Id,
|
||||
CustomerId = x.CustomerId,
|
||||
WarehouseId = x.WarehouseId,
|
||||
WarehouseName = warehouseNameMap.GetValueOrDefault(x.WarehouseId),
|
||||
ParcelId = x.ParcelId,
|
||||
TrackingNo = x.TrackingNo,
|
||||
CreateTime = x.CreateTime,
|
||||
CreateUserId = x.CreateUserId,
|
||||
CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(x.CreateUserId, userNameMap)
|
||||
}).ToList(),
|
||||
Total = total
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 快递到仓:写入到仓记录,并将对应「我的包裹」状态改为 arrived。
|
||||
/// </summary>
|
||||
[HttpPost("/warehouse/parcel-arrival/create")]
|
||||
public async Task<WarehouseParcelArrivalOutput> CreateParcelArrivalAsync(WarehouseParcelArrivalCreateInput input)
|
||||
{
|
||||
var customerId = await ResolveMutationCustomerIdAsync(input.CustomerId);
|
||||
if (input.WarehouseId <= 0)
|
||||
{
|
||||
throw new BusinessException("仓库ID不能为空");
|
||||
}
|
||||
|
||||
var trackingNo = input.TrackingNo.Trim();
|
||||
if (string.IsNullOrWhiteSpace(trackingNo))
|
||||
{
|
||||
throw new BusinessException("国内快递单号不能为空");
|
||||
}
|
||||
|
||||
var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == input.WarehouseId)
|
||||
?? throw new BusinessException("仓库不存在");
|
||||
|
||||
var parcel = await ParcelRepository.FirstOrDefaultAsync(x =>
|
||||
x.CustomerId == customerId
|
||||
&& x.WarehouseId == input.WarehouseId
|
||||
&& x.TrackingNo == trackingNo);
|
||||
if (parcel == null)
|
||||
{
|
||||
throw new BusinessException("未找到对应包裹,请确认快递单号与仓库");
|
||||
}
|
||||
|
||||
if (parcel.LifecycleStatus == WarehouseParcelLifecycleCodes.Stored)
|
||||
{
|
||||
throw new BusinessException("包裹已上架,无法再到仓");
|
||||
}
|
||||
|
||||
parcel.LifecycleStatus = WarehouseParcelLifecycleCodes.Arrived;
|
||||
parcel.ArrivalTime ??= DateTime.Now;
|
||||
await ParcelRepository.UpdateAsync(parcel);
|
||||
|
||||
var entity = new CParcelArrival
|
||||
{
|
||||
CustomerId = customerId,
|
||||
WarehouseId = input.WarehouseId,
|
||||
ParcelId = parcel.Id,
|
||||
TrackingNo = trackingNo
|
||||
};
|
||||
await ParcelArrivalRepository.InsertAsync(entity);
|
||||
|
||||
var userNameMap = await UserDisplayNameResolver.ResolveManyAsync([entity.CreateUserId]);
|
||||
return new WarehouseParcelArrivalOutput
|
||||
{
|
||||
Id = entity.Id,
|
||||
CustomerId = entity.CustomerId,
|
||||
WarehouseId = entity.WarehouseId,
|
||||
WarehouseName = warehouse.WarehouseName,
|
||||
ParcelId = entity.ParcelId,
|
||||
TrackingNo = entity.TrackingNo,
|
||||
CreateTime = entity.CreateTime,
|
||||
CreateUserId = entity.CreateUserId,
|
||||
CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(entity.CreateUserId, userNameMap)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除快递到仓记录(不回退包裹状态)。
|
||||
/// </summary>
|
||||
[HttpPost("/warehouse/parcel-arrival/delete")]
|
||||
public async Task DeleteParcelArrivalAsync(IdInput input)
|
||||
{
|
||||
var customerId = await ResolveListCustomerIdAsync(null);
|
||||
var entity = customerId.HasValue
|
||||
? await ParcelArrivalRepository.FirstOrDefaultAsync(x =>
|
||||
x.Id == input.Id && x.CustomerId == customerId.Value)
|
||||
: await ParcelArrivalRepository.FirstOrDefaultAsync(x => x.Id == input.Id);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new BusinessException("到仓记录不存在");
|
||||
}
|
||||
|
||||
await ParcelArrivalRepository.DeleteAsync(entity);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
using System.Linq.Expressions;
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Dtos.Warehouse;
|
||||
@ -9,6 +11,9 @@ namespace Oms.Application.Services.Warehouse;
|
||||
|
||||
public partial class WarehouseService
|
||||
{
|
||||
private IWebHostEnvironment WebHostEnvironment =>
|
||||
LazyServiceProvider.GetRequiredService<IWebHostEnvironment>();
|
||||
|
||||
/// <summary>
|
||||
/// 查询 SKU 列表。
|
||||
/// </summary>
|
||||
@ -142,6 +147,18 @@ public partial class WarehouseService
|
||||
await SkuRepository.DeleteAsync(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传 SKU 图片,返回可访问相对路径。
|
||||
/// </summary>
|
||||
[HttpPost("/warehouse/sku/upload-image")]
|
||||
[Consumes("multipart/form-data")]
|
||||
public async Task<object> UploadSkuImageAsync(IFormFile file)
|
||||
{
|
||||
await ResolveMutationCustomerIdAsync(null);
|
||||
var path = await OmsFileUploadHelper.SaveWarehouseSkuImageAsync(file, WebHostEnvironment);
|
||||
return new { url = path, fileUrl = path };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绑定平台 SKU 与库内 SKU 映射。
|
||||
/// </summary>
|
||||
|
||||
@ -17,16 +17,34 @@ public static class OmsFileUploadHelper
|
||||
/// <summary>
|
||||
/// 保存充值截图,返回以 /uploads 开头的相对访问路径。
|
||||
/// </summary>
|
||||
public static async Task<string> SaveRechargeScreenshotAsync(IFormFile? file, IWebHostEnvironment env)
|
||||
public static Task<string> SaveRechargeScreenshotAsync(IFormFile? file, IWebHostEnvironment env) =>
|
||||
SaveImageAsync(file, env, "recharge", "充值截图");
|
||||
|
||||
/// <summary>
|
||||
/// 保存仓储 SKU 图片。
|
||||
/// </summary>
|
||||
public static Task<string> SaveWarehouseSkuImageAsync(IFormFile file, IWebHostEnvironment env) =>
|
||||
SaveImageAsync(file, env, "sku", "SKU 图片");
|
||||
|
||||
private static async Task<string> SaveImageAsync(
|
||||
IFormFile? file,
|
||||
IWebHostEnvironment env,
|
||||
string folder,
|
||||
string label)
|
||||
{
|
||||
if (file == null || file.Length <= 0)
|
||||
{
|
||||
return "0";
|
||||
if (folder == "recharge")
|
||||
{
|
||||
return "0";
|
||||
}
|
||||
|
||||
throw new BusinessException($"{label}不能为空");
|
||||
}
|
||||
|
||||
if (file.Length > MaxImageBytes)
|
||||
{
|
||||
throw new BusinessException("充值截图大小不能超过 5MB");
|
||||
throw new BusinessException($"{label}大小不能超过 5MB");
|
||||
}
|
||||
|
||||
var ext = Path.GetExtension(file.FileName);
|
||||
@ -37,7 +55,7 @@ public static class OmsFileUploadHelper
|
||||
|
||||
if (!ImageExtensions.Contains(ext))
|
||||
{
|
||||
throw new BusinessException("充值截图仅支持 jpg、png、webp、gif、bmp 格式");
|
||||
throw new BusinessException($"{label}仅支持 jpg、png、webp、gif、bmp 格式");
|
||||
}
|
||||
|
||||
var webRoot = env.WebRootPath;
|
||||
@ -47,7 +65,7 @@ public static class OmsFileUploadHelper
|
||||
}
|
||||
|
||||
var monthFolder = DateTime.Now.ToString("yyyyMM");
|
||||
var relativeDir = Path.Combine("uploads", "recharge", monthFolder);
|
||||
var relativeDir = Path.Combine("uploads", folder, monthFolder);
|
||||
var absoluteDir = Path.Combine(webRoot, relativeDir);
|
||||
Directory.CreateDirectory(absoluteDir);
|
||||
|
||||
@ -58,7 +76,7 @@ public static class OmsFileUploadHelper
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
return $"/uploads/recharge/{monthFolder}/{fileName}".Replace('\\', '/');
|
||||
return $"/uploads/{folder}/{monthFolder}/{fileName}".Replace('\\', '/');
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> ExcelExtensions =
|
||||
|
||||
21
src/Oms.Domain/WarehouseAggregate/Entities/CParcelArrival.cs
Normal file
21
src/Oms.Domain/WarehouseAggregate/Entities/CParcelArrival.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using Oms.Domain.Shared;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.Domain.WarehouseAggregate.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 快递到仓扫描记录。
|
||||
/// </summary>
|
||||
[SugarTable("C_parcel_arrival")]
|
||||
public class CParcelArrival : OmsAuditedAggregateRoot
|
||||
{
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
public int WarehouseId { get; set; }
|
||||
|
||||
/// <summary>关联我的包裹(可为空,兼容历史)。</summary>
|
||||
public int? ParcelId { get; set; }
|
||||
|
||||
/// <summary>国内快递单号。</summary>
|
||||
public string TrackingNo { get; set; } = string.Empty;
|
||||
}
|
||||
@ -19,4 +19,6 @@ public interface ICParcelRepository : IRepository<CParcel>;
|
||||
|
||||
public interface ICParcelItemRepository : IRepository<CParcelItem>;
|
||||
|
||||
public interface ICParcelArrivalRepository : IRepository<CParcelArrival>;
|
||||
|
||||
public interface ICSkuPlatformMapRepository : IRepository<CSkuPlatformMap>;
|
||||
|
||||
@ -21,4 +21,6 @@ public class CParcelRepository : OmsSqlSugarRepository<CParcel>, ICParcelReposit
|
||||
|
||||
public class CParcelItemRepository : OmsSqlSugarRepository<CParcelItem>, ICParcelItemRepository, IScopedDependency;
|
||||
|
||||
public class CParcelArrivalRepository : OmsSqlSugarRepository<CParcelArrival>, ICParcelArrivalRepository, IScopedDependency;
|
||||
|
||||
public class CSkuPlatformMapRepository : OmsSqlSugarRepository<CSkuPlatformMap>, ICSkuPlatformMapRepository, IScopedDependency;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user