diff --git a/db.sql b/db.sql index 64f875d..b18ad53 100644 --- a/db.sql +++ b/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', diff --git a/sql/C_parcel_arrival.sql b/sql/C_parcel_arrival.sql new file mode 100644 index 0000000..e4e3513 --- /dev/null +++ b/sql/C_parcel_arrival.sql @@ -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='仓储-快递到仓记录'; diff --git a/sql/README.md b/sql/README.md index 41907d1..60067fe 100644 --- a/sql/README.md +++ b/sql/README.md @@ -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` 末尾对应段落 diff --git a/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs b/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs index edf6583..833f177 100644 --- a/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs +++ b/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs @@ -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; } +} diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.ParcelArrival.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.ParcelArrival.cs new file mode 100644 index 0000000..a19a8fb --- /dev/null +++ b/src/Oms.Application/Services/Warehouse/WarehouseService.ParcelArrival.cs @@ -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(); + + private IWarehouseAddressRepository WarehouseAddressRepository => + LazyServiceProvider.GetRequiredService(); + + private OmsRbacUserDisplayNameResolver UserDisplayNameResolver => + LazyServiceProvider.GetRequiredService(); + + /// + /// 快递到仓记录列表。 + /// + [HttpPost("/warehouse/parcel-arrival/list")] + public async Task> 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> 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 + { + [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 + { + 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 + }; + } + + /// + /// 快递到仓:写入到仓记录,并将对应「我的包裹」状态改为 arrived。 + /// + [HttpPost("/warehouse/parcel-arrival/create")] + public async Task 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) + }; + } + + /// + /// 删除快递到仓记录(不回退包裹状态)。 + /// + [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); + } +} diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs index 6a2a5b9..c785b9a 100644 --- a/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs +++ b/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs @@ -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(); + /// /// 查询 SKU 列表。 /// @@ -142,6 +147,18 @@ public partial class WarehouseService await SkuRepository.DeleteAsync(entity); } + /// + /// 上传 SKU 图片,返回可访问相对路径。 + /// + [HttpPost("/warehouse/sku/upload-image")] + [Consumes("multipart/form-data")] + public async Task UploadSkuImageAsync(IFormFile file) + { + await ResolveMutationCustomerIdAsync(null); + var path = await OmsFileUploadHelper.SaveWarehouseSkuImageAsync(file, WebHostEnvironment); + return new { url = path, fileUrl = path }; + } + /// /// 绑定平台 SKU 与库内 SKU 映射。 /// diff --git a/src/Oms.Application/Shared/OmsFileUploadHelper.cs b/src/Oms.Application/Shared/OmsFileUploadHelper.cs index a086a92..bc9b366 100644 --- a/src/Oms.Application/Shared/OmsFileUploadHelper.cs +++ b/src/Oms.Application/Shared/OmsFileUploadHelper.cs @@ -17,16 +17,34 @@ public static class OmsFileUploadHelper /// /// 保存充值截图,返回以 /uploads 开头的相对访问路径。 /// - public static async Task SaveRechargeScreenshotAsync(IFormFile? file, IWebHostEnvironment env) + public static Task SaveRechargeScreenshotAsync(IFormFile? file, IWebHostEnvironment env) => + SaveImageAsync(file, env, "recharge", "充值截图"); + + /// + /// 保存仓储 SKU 图片。 + /// + public static Task SaveWarehouseSkuImageAsync(IFormFile file, IWebHostEnvironment env) => + SaveImageAsync(file, env, "sku", "SKU 图片"); + + private static async Task 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 ExcelExtensions = diff --git a/src/Oms.Domain/WarehouseAggregate/Entities/CParcelArrival.cs b/src/Oms.Domain/WarehouseAggregate/Entities/CParcelArrival.cs new file mode 100644 index 0000000..d4e22aa --- /dev/null +++ b/src/Oms.Domain/WarehouseAggregate/Entities/CParcelArrival.cs @@ -0,0 +1,21 @@ +using Oms.Domain.Shared; +using SqlSugar; + +namespace Oms.Domain.WarehouseAggregate.Entities; + +/// +/// 快递到仓扫描记录。 +/// +[SugarTable("C_parcel_arrival")] +public class CParcelArrival : OmsAuditedAggregateRoot +{ + public int CustomerId { get; set; } + + public int WarehouseId { get; set; } + + /// 关联我的包裹(可为空,兼容历史)。 + public int? ParcelId { get; set; } + + /// 国内快递单号。 + public string TrackingNo { get; set; } = string.Empty; +} diff --git a/src/Oms.Domain/WarehouseAggregate/Repositories/IWarehouseRepositories.cs b/src/Oms.Domain/WarehouseAggregate/Repositories/IWarehouseRepositories.cs index 527e50d..f2c0fb8 100644 --- a/src/Oms.Domain/WarehouseAggregate/Repositories/IWarehouseRepositories.cs +++ b/src/Oms.Domain/WarehouseAggregate/Repositories/IWarehouseRepositories.cs @@ -19,4 +19,6 @@ public interface ICParcelRepository : IRepository; public interface ICParcelItemRepository : IRepository; +public interface ICParcelArrivalRepository : IRepository; + public interface ICSkuPlatformMapRepository : IRepository; diff --git a/src/Oms.SqlSugarCore/Warehouse/WarehouseRepositories.cs b/src/Oms.SqlSugarCore/Warehouse/WarehouseRepositories.cs index 1e5688f..c9e6193 100644 --- a/src/Oms.SqlSugarCore/Warehouse/WarehouseRepositories.cs +++ b/src/Oms.SqlSugarCore/Warehouse/WarehouseRepositories.cs @@ -21,4 +21,6 @@ public class CParcelRepository : OmsSqlSugarRepository, ICParcelReposit public class CParcelItemRepository : OmsSqlSugarRepository, ICParcelItemRepository, IScopedDependency; +public class CParcelArrivalRepository : OmsSqlSugarRepository, ICParcelArrivalRepository, IScopedDependency; + public class CSkuPlatformMapRepository : OmsSqlSugarRepository, ICSkuPlatformMapRepository, IScopedDependency;