diff --git a/sql/migrate_sales_product_group_wms.sql b/sql/migrate_sales_product_group_wms.sql new file mode 100644 index 0000000..08470da --- /dev/null +++ b/sql/migrate_sales_product_group_wms.sql @@ -0,0 +1,24 @@ +-- 销售产品分组字典:仓储代发(wms)。可重复执行。 +-- 用于店铺订单「创建代发订单」选品过滤;销售产品 ProductGroup 填 wms。 + +SET @db := DATABASE(); + +-- 确保字典类型存在 +INSERT INTO `t_dict_type` (`DictType`, `Remark`, `TypeSort`, `IsDeleted`, `CreateUserId`) +SELECT 'sales_product_group', '销售产品分组', 0, 0, 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `t_dict_type` + WHERE `DictType` = 'sales_product_group' AND `IsDeleted` = 0 +); + +-- 写入 wms 字典项 +INSERT INTO `t_dict` (`DictType`, `DictSort`, `DictCode`, `DictName`, `IsDeleted`, `CreateUserId`) +SELECT 'sales_product_group', 90, 'wms', '仓储代发', 0, 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `t_dict` + WHERE `DictType` = 'sales_product_group' + AND `DictCode` = 'wms' + AND `IsDeleted` = 0 +); diff --git a/src/Oms.Application/DataPerm/DataPermStore.cs b/src/Oms.Application/DataPerm/DataPermStore.cs index 1022508..fce007d 100644 --- a/src/Oms.Application/DataPerm/DataPermStore.cs +++ b/src/Oms.Application/DataPerm/DataPermStore.cs @@ -16,12 +16,12 @@ public class DataPermStore(FakeRbacDbContext dbContext, DataPermContextCache cac PropertyNameCaseInsensitive = true }; - // EF Core 原始 SQL 参数不接受 DBNull,空值直接传 null - private static object? SqlParam(object? value) => value; + // EF Core 原始 SQL 参数不接受 DBNull;object[] 元素类型为 object,运行时仍可传 null + private static object SqlParam(object? value) => value ?? null!; public async Task GetUserDataPermAsync(Guid userId, CancellationToken cancellationToken = default) { - var user = await dbContext.Database.SqlQueryRaw($@" + var userSql = $@" SELECT u.`Id` AS `UserId`, u.`Name` AS `UserName`, @@ -31,11 +31,13 @@ public class DataPermStore(FakeRbacDbContext dbContext, DataPermContextCache cac LEFT JOIN `{FakeTables.Organization}` o ON o.`Id` = u.`OrganizationId` AND o.`IsDeleted` = 0 WHERE u.`Id` = {{0}} AND u.`IsDeleted` = 0 LIMIT 1 - ", userId.ToString()).FirstOrDefaultAsync(cancellationToken) + "; + var user = await dbContext.Database.SqlQueryRaw(userSql, userId.ToString()) + .FirstOrDefaultAsync(cancellationToken) ?? throw new BusinessException("用户不存在"); var now = DateTime.UtcNow; - var orgRows = await dbContext.Database.SqlQueryRaw($@" + var orgSql = $@" SELECT p.`Id` AS `Id`, p.`OrganizationId` AS `OrganizationId`, @@ -47,7 +49,9 @@ public class DataPermStore(FakeRbacDbContext dbContext, DataPermContextCache cac FROM `user_org_perm` p LEFT JOIN `{FakeTables.Organization}` o ON o.`Id` = p.`OrganizationId` AND o.`IsDeleted` = 0 WHERE p.`IsDeleted` = 0 AND p.`UserId` = {{0}} - ", userId.ToString()).ToListAsync(cancellationToken); + "; + var orgRows = await dbContext.Database.SqlQueryRaw(orgSql, userId.ToString()) + .ToListAsync(cancellationToken); var scopeRows = await dbContext.Database.SqlQueryRaw(""" SELECT p.`Id` AS `Id`, p.`DataScope` AS `DataScope`, p.`DataResourceCode` AS `DataResourceCode`, p.`ExpireTime` AS `ExpireTime` @@ -128,20 +132,24 @@ public class DataPermStore(FakeRbacDbContext dbContext, DataPermContextCache cac public async Task GetRoleDataPermAsync(Guid roleId, CancellationToken cancellationToken = default) { - var role = await dbContext.Database.SqlQueryRaw($@" + var roleSql = $@" SELECT r.`Id` AS `RoleId`, r.`Name` AS `RoleName` FROM `{FakeTables.Role}` r WHERE r.`Id` = {{0}} AND r.`IsDeleted` = 0 LIMIT 1 - ", roleId.ToString()).FirstOrDefaultAsync(cancellationToken) + "; + var role = await dbContext.Database.SqlQueryRaw(roleSql, roleId.ToString()) + .FirstOrDefaultAsync(cancellationToken) ?? throw new BusinessException("角色不存在"); - var orgRows = await dbContext.Database.SqlQueryRaw($@" + var orgSql = $@" SELECT p.`Id` AS `Id`, p.`OrganizationId` AS `OrganizationId`, o.`Name` AS `OrganizationName`, p.`OrgScope` AS `OrgScope` FROM `role_org_perm` p LEFT JOIN `{FakeTables.Organization}` o ON o.`Id` = p.`OrganizationId` AND o.`IsDeleted` = 0 WHERE p.`IsDeleted` = 0 AND p.`RoleId` = {{0}} - ", roleId.ToString()).ToListAsync(cancellationToken); + "; + var orgRows = await dbContext.Database.SqlQueryRaw(orgSql, roleId.ToString()) + .ToListAsync(cancellationToken); var scopeRows = await dbContext.Database.SqlQueryRaw(""" SELECT p.`Id` AS `Id`, p.`DataScope` AS `DataScope`, p.`DataResourceCode` AS `DataResourceCode` diff --git a/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs b/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs index cdab609..92ab5b9 100644 --- a/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs +++ b/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs @@ -11,6 +11,9 @@ public class PlatformStorePageInput public int? CustomerId { get; set; } + /// + /// 平台编码:tiktok / temu / shopee / lazada;空或 all 表示全部。 + /// public string? PlatformCode { get; set; } public string? StoreNickname { get; set; } @@ -147,11 +150,6 @@ public class PlatformStoreOrderListInput : PlatformStorePageInput { public int? StoreId { get; set; } - /// - /// 平台编码:tiktok / temu / shopee / lazada;空或 all 表示全部。 - /// - public string? PlatformCode { get; set; } - /// /// 履约状态:pending / toShip / shipped / canceled / suspended / completed;空或 all 表示全部。 /// diff --git a/src/Oms.Application/Rbac/UserProfileRepository.cs b/src/Oms.Application/Rbac/UserProfileRepository.cs index 817d8b1..a02fa3f 100644 --- a/src/Oms.Application/Rbac/UserProfileRepository.cs +++ b/src/Oms.Application/Rbac/UserProfileRepository.cs @@ -104,12 +104,12 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende sql, [ userId.ToString(), - // EF Core 参数不接受 DBNull,空值直接传 null 即可映射为 SQL NULL - (object?)NormalizeOptional(profile.Phone), - (object?)NormalizeOptional(profile.EmployeeNo), - (object?)NormalizeOptional(profile.WechatId), - (object?)NormalizeOptional(profile.WechatQrCode), - (object?)NormalizeOptional(profile.WecomKfUrl), + // EF Core 参数不接受 DBNull;object[] 元素类型为 object,运行时仍可传 null + SqlParam(NormalizeOptional(profile.Phone)), + SqlParam(NormalizeOptional(profile.EmployeeNo)), + SqlParam(NormalizeOptional(profile.WechatId)), + SqlParam(NormalizeOptional(profile.WechatQrCode)), + SqlParam(NormalizeOptional(profile.WecomKfUrl)), profile.IsEnabled ? 1 : 0, ], cancellationToken); @@ -175,10 +175,10 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende sql, [ userId.ToString(), - (object?)NormalizeOptional(profile.Phone), - (object?)NormalizeOptional(profile.WechatId), - (object?)NormalizeOptional(profile.WechatQrCode), - (object?)NormalizeOptional(profile.WecomKfUrl), + SqlParam(NormalizeOptional(profile.Phone)), + SqlParam(NormalizeOptional(profile.WechatId)), + SqlParam(NormalizeOptional(profile.WechatQrCode)), + SqlParam(NormalizeOptional(profile.WecomKfUrl)), ], cancellationToken); } @@ -255,6 +255,8 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende return value.Trim(); } + private static object SqlParam(object? value) => value ?? null!; + private sealed class UserProfileRow { public string? Phone { get; set; } diff --git a/src/Oms.Application/Services/Customers/CustomerService.cs b/src/Oms.Application/Services/Customers/CustomerService.cs index e58bc83..32cd9e8 100644 --- a/src/Oms.Application/Services/Customers/CustomerService.cs +++ b/src/Oms.Application/Services/Customers/CustomerService.cs @@ -204,9 +204,9 @@ public class CustomerService : ApplicationService // 同步 RBAC 登录邮箱,覆盖注册时的 @noemail.local 占位 try { - await RbacDbContext.Database.ExecuteSqlRawAsync( - $"UPDATE `{FakeTables.User}` SET `Email` = {{0}} WHERE `userid` = {{1}} AND `IsDeleted` = 0", - email, customer.UserId); + var syncEmailSql = + $"UPDATE `{FakeTables.User}` SET `Email` = {{0}} WHERE `userid` = {{1}} AND `IsDeleted` = 0"; + await RbacDbContext.Database.ExecuteSqlRawAsync(syncEmailSql, email, customer.UserId); } catch { @@ -392,8 +392,8 @@ public class CustomerService : ApplicationService private async Task GetRbacUserGuidByMapIdAsync(int mapId) { - return await RbacDbContext.Database.SqlQueryRaw( - $"select `Id` as `Value` from `{FakeTables.User}` where `userid` = {{0}} and `IsDeleted` = 0", mapId) + var sql = $"select `Id` as `Value` from `{FakeTables.User}` where `userid` = {{0}} and `IsDeleted` = 0"; + return await RbacDbContext.Database.SqlQueryRaw(sql, mapId) .FirstOrDefaultAsync(); } @@ -442,7 +442,7 @@ public class CustomerService : ApplicationService (string.IsNullOrWhiteSpace(keyword) || customer.CustomerCode.Contains(keyword) || customer.CustomerName.Contains(keyword) || - customer.Phone.Contains(keyword) || + (customer.Phone ?? string.Empty).Contains(keyword) || (customer.Email ?? string.Empty).Contains(keyword)) && (!hasStatusFilter || customer.Status == statusValue) && (!hasCustomerGroupFilter || customer.CustomerGroup == customerGroupValue) && @@ -497,8 +497,8 @@ public class CustomerService : ApplicationService private async Task GetRbacUserMapIdOrThrowAsync(Guid id) { - var userId = await RbacDbContext.Database.SqlQueryRaw( - $"select `userid` as `Value` from `{FakeTables.User}` where `Id` = {{0}}", id) + var sql = $"select `userid` as `Value` from `{FakeTables.User}` where `Id` = {{0}}"; + var userId = await RbacDbContext.Database.SqlQueryRaw(sql, id) .FirstOrDefaultAsync(); if (!userId.HasValue || userId.Value <= 0) { diff --git a/src/Oms.Application/Services/OpenApi/WarehouseOpenApiService.cs b/src/Oms.Application/Services/OpenApi/WarehouseOpenApiService.cs index 35a81be..3788f08 100644 --- a/src/Oms.Application/Services/OpenApi/WarehouseOpenApiService.cs +++ b/src/Oms.Application/Services/OpenApi/WarehouseOpenApiService.cs @@ -17,7 +17,7 @@ namespace Oms.Application.Services.OpenApi; /// /// 仓配对外开放 API(Swagger 文档分组:warehouseOpenApi)。 -/// 鉴权:Authorization: Bearer {token}(账号密码换取,有效期 48 小时)。 +/// 鉴权:Authorization: {token}(账号密码换取,有效期 48 小时;无需 Bearer 前缀)。 /// 与客户 HMAC 分组 openApi 相互独立。 /// [ApiExplorerSettings(GroupName = "warehouseOpenApi")] @@ -45,7 +45,7 @@ public class WarehouseOpenApiService : ApplicationService LazyServiceProvider.GetRequiredService(); /// - /// 获取 Token(有效期 48 小时)。后续请求 Header:Authorization: Bearer {token}。 + /// 获取 Token(有效期 48 小时)。后续请求 Header:Authorization: {token}(无需 Bearer)。 /// [AllowAnonymous] [HttpPost("/openapi/warehouse/token")] @@ -75,7 +75,7 @@ public class WarehouseOpenApiService : ApplicationService /// /// 入库上传件重尺。与操作台「入库扫描」页「入库」按钮同源(POST /operation/inbound/create)。 /// 写入收货件重尺、入库记录,并触发入库计费/自动触发。重量默认 kg,长度默认 cm。 - /// Header:Authorization: Bearer {token}。 + /// Header:Authorization: {token}(无需 Bearer)。 /// [Authorize] [HttpPost("/openapi/warehouse/inbound")] @@ -86,7 +86,7 @@ public class WarehouseOpenApiService : ApplicationService /// /// 上传件重尺。mode:0=入库(同「入库」按钮),1=出库(同「出库」按钮)。 - /// Header:Authorization: Bearer {token}。 + /// Header:Authorization: {token}(无需 Bearer)。 /// [Authorize] [HttpPost("/openapi/warehouse/weight/upload")] @@ -164,7 +164,7 @@ public class WarehouseOpenApiService : ApplicationService /// /// 上传红外图片。Base64 解码后转存 COS,写入订单附件 fileType=infraredImage(Source=upload), /// 与订单详情「附件」Tab →「红外图片」→「点击上传」(POST /order/files/upload)同源展示。 - /// 成功无响应体。Header:Authorization: Bearer {token}。 + /// 成功无响应体。Header:Authorization: {token}(无需 Bearer)。 /// [Authorize] [HttpPost("/openapi/warehouse/image/upload")] diff --git a/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs b/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs index e73d78d..1f836a7 100644 --- a/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs +++ b/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs @@ -525,12 +525,12 @@ internal static class ParcelBillingCalculator priceSnapshot.Prices.Select(p => (p.Status, p.Details))); private static IReadOnlyList CollectDistinctDetailZoneKeys( - IEnumerable<(byte Status, List? Details)> prices) + IEnumerable<(byte Status, List Details)> prices) { var zones = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var price in prices.Where(x => EnableStatusRules.IsEnabled(x.Status))) { - foreach (var detail in price.Details ?? []) + foreach (var detail in price.Details) { var factors = ParseFactorDictionary(detail.FactorValuesJson); if (TryGetZoneFactorText(factors, out var zone)) diff --git a/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs b/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs index d812708..e949a82 100644 --- a/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs +++ b/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs @@ -5,6 +5,7 @@ using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.PlatformStore; using Oms.Application.Dtos.Shared; using Oms.Application.Shared; +using Oms.Domain.ChannelsAggregate.Consts; using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.CustomersAggregate.Repositories; using Oms.Domain.OrdersAggregate.Consts; @@ -409,7 +410,8 @@ public class PlatformStoreOrderService : ApplicationService input.ProductCode, input.WarehouseId, input.Items, - requireBind: false); + requireBind: false, + requireWmsProductGroup: false); } /// @@ -429,7 +431,30 @@ public class PlatformStoreOrderService : ApplicationService input.ProductCode, input.WarehouseId, input.Items, - requireBind: true); + requireBind: true, + requireWmsProductGroup: false); + } + + /// + /// 批量创建仓储代发订单:与代发运单同链路(建单+绑定+产品计费), + /// 但产品必须属于分组 wms(仓储代发)。 + /// + [HttpPost("/order/platform-store-order/batch-create-fulfill-wms")] + public async Task BatchCreateFulfillWmsAsync( + PlatformStoreOrderBatchCreateFulfillInput input) + { + if (input.WarehouseId <= 0) + { + throw new BusinessException("请指定发货仓库"); + } + + return await BatchCreateLogisticsInternalAsync( + input.CustomerId, + input.ProductCode, + input.WarehouseId, + input.Items, + requireBind: true, + requireWmsProductGroup: true); } private async Task BatchCreateLogisticsInternalAsync( @@ -437,7 +462,8 @@ public class PlatformStoreOrderService : ApplicationService string? productCode, int? warehouseId, List items, - bool requireBind) + bool requireBind, + bool requireWmsProductGroup = false) { if (items == null || items.Count == 0) { @@ -451,14 +477,14 @@ public class PlatformStoreOrderService : ApplicationService if (string.IsNullOrWhiteSpace(productCode)) { - throw new BusinessException("请选择运输方式"); + throw new BusinessException(requireWmsProductGroup ? "请选择仓储代发产品" : "请选择运输方式"); } var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync( CustomerRepository, CurrentUser, customerIdInput); - var resolvedProductCode = await ResolveProductCodeRequiredAsync(productCode); + var resolvedProductCode = await ResolveProductCodeRequiredAsync(productCode, requireWmsProductGroup); var batchNo = $"PSL{DateTime.Now:yyyyMMddHHmmss}"; var output = new PlatformStoreOrderBatchCreateLogisticsOutput { BatchNo = batchNo }; @@ -488,9 +514,11 @@ public class PlatformStoreOrderService : ApplicationService itemInput.Receiver, requireBind ? itemInput.BindItems : null); item.Success = true; - item.Message = requireBind - ? $"已创建物流单 {result.ServiceNo} 并绑定" - : $"已创建物流单 {result.ServiceNo}"; + item.Message = requireWmsProductGroup + ? $"已创建代发订单 {result.ServiceNo} 并绑定" + : requireBind + ? $"已创建物流单 {result.ServiceNo} 并绑定" + : $"已创建物流单 {result.ServiceNo}"; item.LinkedOrderId = result.OrderId; item.ServiceNo = result.ServiceNo; output.SuccessCount++; @@ -523,7 +551,7 @@ public class PlatformStoreOrderService : ApplicationService List? bindItems) { var order = await GetOrderOrThrowAsync(customerId, storeOrderId); - await EnsureMatchableAsync(order); + await EnsureCanCreateLogisticsAsync(order); var lines = await StoreOrderLineRepository.GetListAsync(x => x.StoreOrderId == order.Id && !x.IsShipExcluded); if (lines.Count == 0) @@ -639,7 +667,9 @@ public class PlatformStoreOrderService : ApplicationService await OrderRepository.UpdateAsync(logisticsOrder); } - private async Task ResolveProductCodeRequiredAsync(string productCode) + private async Task ResolveProductCodeRequiredAsync( + string productCode, + bool requireWmsProductGroup = false) { var normalized = productCode.Trim(); var product = await SalesProductRepository.GetByProductCodeAsync(normalized); @@ -648,6 +678,15 @@ public class PlatformStoreOrderService : ApplicationService throw new BusinessException("销售产品不存在或未启用"); } + if (requireWmsProductGroup + && !string.Equals( + product.ProductGroup?.Trim(), + SalesProductGroupCodes.Wms, + StringComparison.OrdinalIgnoreCase)) + { + throw new BusinessException("仓储代发订单仅可选择产品分组为「仓储代发(wms)」的销售产品"); + } + return normalized; } @@ -734,6 +773,26 @@ public class PlatformStoreOrderService : ApplicationService } } + /// + /// 创建运单/代发:不限制履约状态;拦截搁置与未删除的已关联物流单。 + /// + private async Task EnsureCanCreateLogisticsAsync(SalesStoreOrder order) + { + if (order.IsSuspended) + { + throw new BusinessException("订单已搁置,不能创建"); + } + + if (order.LinkedOrderId is > 0) + { + var linked = await OrderRepository.FirstOrDefaultAsync(x => x.Id == order.LinkedOrderId.Value); + if (linked != null && linked.OrderStatus != OrderStatusCodes.Deleted) + { + throw new BusinessException($"已关联小包订单 {linked.ServiceNo},不能重复创建"); + } + } + } + private async Task EnsureMatchableAsync(SalesStoreOrder order) { if (order.IsSuspended) diff --git a/src/Oms.Application/Services/System/NoticeService.cs b/src/Oms.Application/Services/System/NoticeService.cs index f0620b9..ca82fe3 100644 --- a/src/Oms.Application/Services/System/NoticeService.cs +++ b/src/Oms.Application/Services/System/NoticeService.cs @@ -277,7 +277,7 @@ public class NoticeService : ApplicationService x.ParentCustomerId == null && (x.CustomerName.Contains(pattern) || x.CustomerCode.Contains(pattern) || - x.Phone.Contains(pattern)); + (x.Phone != null && x.Phone.Contains(pattern))); } var total = await CustomerRepository.CountAsync(predicate); diff --git a/src/Oms.Application/Shared/OmsRbacRegistrationGuard.cs b/src/Oms.Application/Shared/OmsRbacRegistrationGuard.cs index fa1f7f1..0971ad4 100644 --- a/src/Oms.Application/Shared/OmsRbacRegistrationGuard.cs +++ b/src/Oms.Application/Shared/OmsRbacRegistrationGuard.cs @@ -167,14 +167,14 @@ public static class OmsRbacRegistrationGuard return; } - var hit = await rbacDbContext.Database - .SqlQueryRaw( - $@" + var sql = $@" SELECT 1 AS Value FROM `{FakeTables.Organization}` WHERE `Id` = {{0}} AND `IsDeleted` = 0 LIMIT 1 - ", organizationId.Value.ToString()) + "; + var hit = await rbacDbContext.Database + .SqlQueryRaw(sql, organizationId.Value.ToString()) .FirstOrDefaultAsync(cancellationToken); if (!hit.HasValue) @@ -201,14 +201,14 @@ public static class OmsRbacRegistrationGuard foreach (var roleId in ids) { - var hit = await rbacDbContext.Database - .SqlQueryRaw( - $@" + var sql = $@" SELECT 1 AS Value FROM `{FakeTables.Role}` WHERE `Id` = {{0}} AND `IsDeleted` = 0 LIMIT 1 - ", roleId.ToString()) + "; + var hit = await rbacDbContext.Database + .SqlQueryRaw(sql, roleId.ToString()) .FirstOrDefaultAsync(cancellationToken); if (!hit.HasValue) @@ -492,19 +492,20 @@ public static class OmsRbacRegistrationGuard // 唯一索引不按 IsDeleted 区分,软删除账号仍会触发 Duplicate entry。 if (excludeUserId is Guid id && id != Guid.Empty) { + var sql = $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `Account` = {{0}} AND `Id` <> {{1}} LIMIT 1"; var hit = await rbacDbContext.Database - .SqlQueryRaw( - $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `Account` = {{0}} AND `Id` <> {{1}} LIMIT 1", - account, id.ToString()) + .SqlQueryRaw(sql, account, id.ToString()) .FirstOrDefaultAsync(cancellationToken); return hit.HasValue; } - var any = await rbacDbContext.Database - .SqlQueryRaw( - $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `Account` = {{0}} LIMIT 1", account) - .FirstOrDefaultAsync(cancellationToken); - return any.HasValue; + { + var sql = $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `Account` = {{0}} LIMIT 1"; + var any = await rbacDbContext.Database + .SqlQueryRaw(sql, account) + .FirstOrDefaultAsync(cancellationToken); + return any.HasValue; + } } private static async Task ExistsUserByEmailAsync( @@ -515,19 +516,20 @@ public static class OmsRbacRegistrationGuard { if (excludeUserId is Guid id && id != Guid.Empty) { + var sql = $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `Email` = {{0}} AND `Id` <> {{1}} LIMIT 1"; var hit = await rbacDbContext.Database - .SqlQueryRaw( - $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `Email` = {{0}} AND `Id` <> {{1}} LIMIT 1", - email, id.ToString()) + .SqlQueryRaw(sql, email, id.ToString()) .FirstOrDefaultAsync(cancellationToken); return hit.HasValue; } - var any = await rbacDbContext.Database - .SqlQueryRaw( - $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `Email` = {{0}} LIMIT 1", email) - .FirstOrDefaultAsync(cancellationToken); - return any.HasValue; + { + var sql = $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `Email` = {{0}} LIMIT 1"; + var any = await rbacDbContext.Database + .SqlQueryRaw(sql, email) + .FirstOrDefaultAsync(cancellationToken); + return any.HasValue; + } } private static async Task ExistsUserByPhoneAsync( @@ -538,19 +540,20 @@ public static class OmsRbacRegistrationGuard { if (excludeUserId is Guid id && id != Guid.Empty) { + var sql = $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `Phone` = {{0}} AND `Id` <> {{1}} LIMIT 1"; var hit = await rbacDbContext.Database - .SqlQueryRaw( - $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `Phone` = {{0}} AND `Id` <> {{1}} LIMIT 1", - phone, id.ToString()) + .SqlQueryRaw(sql, phone, id.ToString()) .FirstOrDefaultAsync(cancellationToken); return hit.HasValue; } - var any = await rbacDbContext.Database - .SqlQueryRaw( - $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `Phone` = {{0}} LIMIT 1", phone) - .FirstOrDefaultAsync(cancellationToken); - return any.HasValue; + { + var sql = $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `Phone` = {{0}} LIMIT 1"; + var any = await rbacDbContext.Database + .SqlQueryRaw(sql, phone) + .FirstOrDefaultAsync(cancellationToken); + return any.HasValue; + } } private static async Task ExistsUserByWechatIdAsync( @@ -561,19 +564,20 @@ public static class OmsRbacRegistrationGuard { if (excludeUserId is Guid id && id != Guid.Empty) { + var sql = $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `WechatId` = {{0}} AND `Id` <> {{1}} LIMIT 1"; var hit = await rbacDbContext.Database - .SqlQueryRaw( - $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `WechatId` = {{0}} AND `Id` <> {{1}} LIMIT 1", - wechatId, id.ToString()) + .SqlQueryRaw(sql, wechatId, id.ToString()) .FirstOrDefaultAsync(cancellationToken); return hit.HasValue; } - var any = await rbacDbContext.Database - .SqlQueryRaw( - $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `WechatId` = {{0}} LIMIT 1", wechatId) - .FirstOrDefaultAsync(cancellationToken); - return any.HasValue; + { + var sql = $"SELECT 1 AS Value FROM `{FakeTables.User}` WHERE `WechatId` = {{0}} LIMIT 1"; + var any = await rbacDbContext.Database + .SqlQueryRaw(sql, wechatId) + .FirstOrDefaultAsync(cancellationToken); + return any.HasValue; + } } private static string ExtractDeepMessage(Exception ex) diff --git a/src/Oms.Domain/ChannelsAggregate/Consts/SalesProductGroupCodes.cs b/src/Oms.Domain/ChannelsAggregate/Consts/SalesProductGroupCodes.cs new file mode 100644 index 0000000..ff419d1 --- /dev/null +++ b/src/Oms.Domain/ChannelsAggregate/Consts/SalesProductGroupCodes.cs @@ -0,0 +1,10 @@ +namespace Oms.Domain.ChannelsAggregate.Consts; + +/// +/// 销售产品分组编码(字典 sales_product_group.DictCode)。 +/// +public static class SalesProductGroupCodes +{ + /// 仓储代发(贴单出库 / 仓内履约计费)。 + public const string Wms = "wms"; +} diff --git a/src/Oms.Domain/OrdersAggregate/Consts/OrderFileTypeCodes.cs b/src/Oms.Domain/OrdersAggregate/Consts/OrderFileTypeCodes.cs index 986fd4c..9507109 100644 --- a/src/Oms.Domain/OrdersAggregate/Consts/OrderFileTypeCodes.cs +++ b/src/Oms.Domain/OrdersAggregate/Consts/OrderFileTypeCodes.cs @@ -43,7 +43,7 @@ public static class OrderFileTypeCodes return false; } - return !string.Equals(fileType.Trim(), DownstreamLabel, StringComparison.OrdinalIgnoreCase); + return !string.Equals(fileType!.Trim(), DownstreamLabel, StringComparison.OrdinalIgnoreCase); } public static string Normalize(string fileType) => fileType.Trim(); diff --git a/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs b/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs index 6215bd9..34baceb 100644 --- a/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs +++ b/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs @@ -140,7 +140,7 @@ public class CustomerRepository : OmsSqlSugarRepository, ICustomerRepo query = query.Where((g, c) => c.CustomerCode.Contains(kw) || c.CustomerName.Contains(kw) || - c.Phone.Contains(kw)); + (c.Phone != null && c.Phone.Contains(kw))); } var total = await query.CountAsync(cancellationToken); diff --git a/src/SimpleAdmin.Api/Auth/AuthService.cs b/src/SimpleAdmin.Api/Auth/AuthService.cs index 449f59f..5bef285 100644 --- a/src/SimpleAdmin.Api/Auth/AuthService.cs +++ b/src/SimpleAdmin.Api/Auth/AuthService.cs @@ -908,15 +908,15 @@ public partial class AuthService( private async Task GetRbacUserGuidByMapIdAsync(int userId, CancellationToken cancellationToken) { - return await rbacDbContext.Database.SqlQueryRaw( - $"select `Id` as `Value` from `{FakeTables.User}` where `IsDeleted` = 0 and `userid` = {{0}}", userId) + var sql = $"select `Id` as `Value` from `{FakeTables.User}` where `IsDeleted` = 0 and `userid` = {{0}}"; + return await rbacDbContext.Database.SqlQueryRaw(sql, userId) .FirstOrDefaultAsync(cancellationToken); } private async Task GetMapIdByUserGuidAsync(Guid userId, CancellationToken cancellationToken) { - return await rbacDbContext.Database.SqlQueryRaw( - $"select `userid` as `Value` from `{FakeTables.User}` where `Id` = {{0}}", userId) + var sql = $"select `userid` as `Value` from `{FakeTables.User}` where `Id` = {{0}}"; + return await rbacDbContext.Database.SqlQueryRaw(sql, userId) .FirstOrDefaultAsync(cancellationToken); } diff --git a/src/SimpleAdmin.Api/Auth/WarehouseOpenApiAuthHeaderMiddleware.cs b/src/SimpleAdmin.Api/Auth/WarehouseOpenApiAuthHeaderMiddleware.cs new file mode 100644 index 0000000..f418a24 --- /dev/null +++ b/src/SimpleAdmin.Api/Auth/WarehouseOpenApiAuthHeaderMiddleware.cs @@ -0,0 +1,36 @@ +namespace SimpleAdmin.Api.Auth; + +/// +/// 仓配开放 API:Authorization 直接传 JWT(不要求 Bearer 前缀)。 +/// 在进入 JwtBearer 前补上 Bearer,以便复用现有鉴权。 +/// +public sealed class WarehouseOpenApiAuthHeaderMiddleware(RequestDelegate next) +{ + public Task InvokeAsync(HttpContext context) + { + if (context.Request.Path.StartsWithSegments("/openapi/warehouse")) + { + NormalizeAuthorizationHeader(context.Request.Headers); + } + + return next(context); + } + + private static void NormalizeAuthorizationHeader(IHeaderDictionary headers) + { + var auth = headers.Authorization.ToString(); + if (string.IsNullOrWhiteSpace(auth)) + { + return; + } + + auth = auth.Trim(); + if (auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + // 无 scheme:Authorization: {token} + headers.Authorization = "Bearer " + auth; + } +} diff --git a/src/SimpleAdmin.Api/SimpleAdminApiModule.cs b/src/SimpleAdmin.Api/SimpleAdminApiModule.cs index d995727..ab4606f 100644 --- a/src/SimpleAdmin.Api/SimpleAdminApiModule.cs +++ b/src/SimpleAdmin.Api/SimpleAdminApiModule.cs @@ -198,6 +198,9 @@ public class SimpleAdminApiModule : FakeModule await next(); }); + // 仓配开放 API:Authorization 直接传 token(可不带 Bearer) + app.UseMiddleware(); + // 认证和授权 app.UseAuthentication(); app.UseAuthorization(); diff --git a/src/SimpleAdmin.Api/SimpleJwtService.cs b/src/SimpleAdmin.Api/SimpleJwtService.cs index f2aad1a..09a7ad9 100644 --- a/src/SimpleAdmin.Api/SimpleJwtService.cs +++ b/src/SimpleAdmin.Api/SimpleJwtService.cs @@ -22,8 +22,8 @@ public class SimpleJwtService(IOptions jwtOptions, UserService u CancellationToken cancellationToken) { var claims = await base.GenerateClaimsByUserIdAsync(userId, cancellationToken); - var mapId = await rbacDbContext.Database.SqlQueryRaw( - $"select `userid` as `Value` from `{FakeTables.User}` where `Id` = {{0}}", userId) + var sql = $"select `userid` as `Value` from `{FakeTables.User}` where `Id` = {{0}}"; + var mapId = await rbacDbContext.Database.SqlQueryRaw(sql, userId) .FirstOrDefaultAsync(cancellationToken); if (mapId.HasValue) { diff --git a/src/SimpleAdmin.Api/WeChat/WeChatCustomerService.cs b/src/SimpleAdmin.Api/WeChat/WeChatCustomerService.cs index 4911344..e5c6ec2 100644 --- a/src/SimpleAdmin.Api/WeChat/WeChatCustomerService.cs +++ b/src/SimpleAdmin.Api/WeChat/WeChatCustomerService.cs @@ -325,8 +325,8 @@ public class WeChatCustomerService( private async Task GetRbacUserMapIdOrThrowAsync(Guid id, CancellationToken cancellationToken) { - var userId = await rbacDbContext.Database.SqlQueryRaw( - $"select `userid` as `Value` from `{FakeTables.User}` where `Id` = {{0}}", id) + var sql = $"select `userid` as `Value` from `{FakeTables.User}` where `Id` = {{0}}"; + var userId = await rbacDbContext.Database.SqlQueryRaw(sql, id) .FirstOrDefaultAsync(cancellationToken); if (!userId.HasValue || userId.Value <= 0) {