diff --git a/sql/migrate_t_order_parcel_columns.sql b/sql/migrate_t_order_parcel_columns.sql new file mode 100644 index 0000000..fa45b98 --- /dev/null +++ b/sql/migrate_t_order_parcel_columns.sql @@ -0,0 +1,44 @@ +-- t_order / t_order_fee 小包预报计费字段补齐(可重复执行) +-- 在 OMS 业务库执行,例如:mysql -h -u -p < migrate_t_order_parcel_columns.sql + +SET @db = DATABASE(); + +-- t_order.ExportCustomsMode +SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_order' AND COLUMN_NAME = 'ExportCustomsMode'); +SET @sql := IF(@exist = 0, + 'ALTER TABLE `t_order` ADD COLUMN `ExportCustomsMode` VARCHAR(32) NULL COMMENT ''出口报关方式'' AFTER `CargoAttribute`', + 'SELECT ''skip ExportCustomsMode'''); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- t_order.PredeclareFailReason +SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_order' AND COLUMN_NAME = 'PredeclareFailReason'); +SET @sql := IF(@exist = 0, + 'ALTER TABLE `t_order` ADD COLUMN `PredeclareFailReason` VARCHAR(512) NULL COMMENT ''预报失败原因'' AFTER `IsRemote`', + 'SELECT ''skip PredeclareFailReason'''); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- t_order.SalesProductRulesVersion +SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_order' AND COLUMN_NAME = 'SalesProductRulesVersion'); +SET @sql := IF(@exist = 0, + 'ALTER TABLE `t_order` ADD COLUMN `SalesProductRulesVersion` VARCHAR(64) NULL COMMENT ''销售产品规则快照版本'' AFTER `PredeclareFailReason`', + 'SELECT ''skip SalesProductRulesVersion'''); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- t_order.SalesProductPriceVersion +SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_order' AND COLUMN_NAME = 'SalesProductPriceVersion'); +SET @sql := IF(@exist = 0, + 'ALTER TABLE `t_order` ADD COLUMN `SalesProductPriceVersion` VARCHAR(64) NULL COMMENT ''销售产品价格快照版本'' AFTER `SalesProductRulesVersion`', + 'SELECT ''skip SalesProductPriceVersion'''); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- t_order_fee.ChargingStage +SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_order_fee' AND COLUMN_NAME = 'ChargingStage'); +SET @sql := IF(@exist = 0, + 'ALTER TABLE `t_order_fee` ADD COLUMN `ChargingStage` VARCHAR(16) NULL COMMENT ''计费阶段:forecast/inbound/outbound'' AFTER `PriceEffectiveTime`', + 'SELECT ''skip ChargingStage'''); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/sql/parcel_billing.sql b/sql/parcel_billing.sql index d4a3717..90017c2 100644 --- a/sql/parcel_billing.sql +++ b/sql/parcel_billing.sql @@ -1,11 +1,39 @@ --- 小包预报计费:订单版本字段 + 费用计费阶段 --- 若列已存在可跳过对应语句 +-- 与 migrate_t_order_parcel_columns.sql 相同,保留此文件名便于按文档引用 +-- t_order / t_order_fee 小包预报计费字段补齐(可重复执行) -ALTER TABLE `t_order` - ADD COLUMN IF NOT EXISTS `ExportCustomsMode` VARCHAR(32) NULL COMMENT '出口报关方式' AFTER `CargoAttribute`, - ADD COLUMN IF NOT EXISTS `PredeclareFailReason` VARCHAR(512) NULL COMMENT '预报失败原因' AFTER `IsRemote`, - ADD COLUMN IF NOT EXISTS `SalesProductRulesVersion` VARCHAR(64) NULL COMMENT '销售产品规则快照版本' AFTER `PredeclareFailReason`, - ADD COLUMN IF NOT EXISTS `SalesProductPriceVersion` VARCHAR(64) NULL COMMENT '销售产品价格快照版本' AFTER `SalesProductRulesVersion`; +SET @db = DATABASE(); -ALTER TABLE `t_order_fee` - ADD COLUMN IF NOT EXISTS `ChargingStage` VARCHAR(16) NULL COMMENT '计费阶段:forecast/inbound/outbound' AFTER `PriceEffectiveTime`; +SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_order' AND COLUMN_NAME = 'ExportCustomsMode'); +SET @sql := IF(@exist = 0, + 'ALTER TABLE `t_order` ADD COLUMN `ExportCustomsMode` VARCHAR(32) NULL COMMENT ''出口报关方式'' AFTER `CargoAttribute`', + 'SELECT ''skip ExportCustomsMode'''); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_order' AND COLUMN_NAME = 'PredeclareFailReason'); +SET @sql := IF(@exist = 0, + 'ALTER TABLE `t_order` ADD COLUMN `PredeclareFailReason` VARCHAR(512) NULL COMMENT ''预报失败原因'' AFTER `IsRemote`', + 'SELECT ''skip PredeclareFailReason'''); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_order' AND COLUMN_NAME = 'SalesProductRulesVersion'); +SET @sql := IF(@exist = 0, + 'ALTER TABLE `t_order` ADD COLUMN `SalesProductRulesVersion` VARCHAR(64) NULL COMMENT ''销售产品规则快照版本'' AFTER `PredeclareFailReason`', + 'SELECT ''skip SalesProductRulesVersion'''); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_order' AND COLUMN_NAME = 'SalesProductPriceVersion'); +SET @sql := IF(@exist = 0, + 'ALTER TABLE `t_order` ADD COLUMN `SalesProductPriceVersion` VARCHAR(64) NULL COMMENT ''销售产品价格快照版本'' AFTER `SalesProductRulesVersion`', + 'SELECT ''skip SalesProductPriceVersion'''); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_order_fee' AND COLUMN_NAME = 'ChargingStage'); +SET @sql := IF(@exist = 0, + 'ALTER TABLE `t_order_fee` ADD COLUMN `ChargingStage` VARCHAR(16) NULL COMMENT ''计费阶段:forecast/inbound/outbound'' AFTER `PriceEffectiveTime`', + 'SELECT ''skip ChargingStage'''); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/src/Oms.Application/Dtos/Recharge/SubmitRechargeFormInput.cs b/src/Oms.Application/Dtos/Recharge/SubmitRechargeFormInput.cs index d5fc9dc..f9195a5 100644 --- a/src/Oms.Application/Dtos/Recharge/SubmitRechargeFormInput.cs +++ b/src/Oms.Application/Dtos/Recharge/SubmitRechargeFormInput.cs @@ -24,8 +24,7 @@ public class SubmitRechargeFormInput public string? Remark { get; set; } /// - /// 充值截图文件。 + /// 充值截图文件(可选;未上传时存 "0")。 /// - [Required] - public IFormFile RechargeScreenshot { get; set; } = null!; + public IFormFile? RechargeScreenshot { get; set; } } diff --git a/src/Oms.Application/Services/Orders/OrderService.cs b/src/Oms.Application/Services/Orders/OrderService.cs index ecc9276..d306110 100644 --- a/src/Oms.Application/Services/Orders/OrderService.cs +++ b/src/Oms.Application/Services/Orders/OrderService.cs @@ -413,7 +413,12 @@ public partial class OrderService : ApplicationService /// 出参:订单查询表达式。 private Expression> BuildPaginatedPredicate(OrderPaginatedInput input) { - return BuildOrderPredicate(input.Status, input.Keyword, input.CreateTimeStart, input.CreateTimeEnd); + return BuildOrderPredicate( + input.Status, + input.Keyword, + input.CreateTimeStart, + input.CreateTimeEnd, + OmsSessionHelper.TryGetCustomerCreateUserId(CurrentUser)); } /// @@ -423,7 +428,12 @@ public partial class OrderService : ApplicationService /// 出参:订单查询表达式。 private Expression> BuildExportPredicate(OrderExportInput input) { - return BuildOrderPredicate(input.Status, input.Keyword, input.CreateTimeStart, input.CreateTimeEnd); + return BuildOrderPredicate( + input.Status, + input.Keyword, + input.CreateTimeStart, + input.CreateTimeEnd, + OmsSessionHelper.TryGetCustomerCreateUserId(CurrentUser)); } /// @@ -445,12 +455,15 @@ public partial class OrderService : ApplicationService string? status, string? keyword, DateTime? createTimeStart, - DateTime? createTimeEnd) + DateTime? createTimeEnd, + int? createUserId = null) { status = status?.Trim(); keyword = keyword?.Trim(); var hasStatusFilter = !string.IsNullOrWhiteSpace(status) && status != "all"; var hasKeywordFilter = !string.IsNullOrWhiteSpace(keyword); + var hasCreateUserFilter = createUserId.HasValue; + var createUserIdValue = createUserId.GetValueOrDefault(); // 显式拆开时间分支,避免 SqlSugar 在翻译 Nullable 判空时生成异常 SQL。 if (createTimeStart.HasValue && createTimeEnd.HasValue) @@ -458,6 +471,7 @@ public partial class OrderService : ApplicationService var start = createTimeStart.Value; var end = createTimeEnd.Value; return order => + (!hasCreateUserFilter || order.CreateUserId == createUserIdValue) && (!hasStatusFilter || order.OrderStatus == status) && (!hasKeywordFilter || order.ServiceNo.Contains(keyword!) || @@ -471,6 +485,7 @@ public partial class OrderService : ApplicationService { var start = createTimeStart.Value; return order => + (!hasCreateUserFilter || order.CreateUserId == createUserIdValue) && (!hasStatusFilter || order.OrderStatus == status) && (!hasKeywordFilter || order.ServiceNo.Contains(keyword!) || @@ -483,6 +498,7 @@ public partial class OrderService : ApplicationService { var end = createTimeEnd.Value; return order => + (!hasCreateUserFilter || order.CreateUserId == createUserIdValue) && (!hasStatusFilter || order.OrderStatus == status) && (!hasKeywordFilter || order.ServiceNo.Contains(keyword!) || @@ -492,6 +508,7 @@ public partial class OrderService : ApplicationService } return order => + (!hasCreateUserFilter || order.CreateUserId == createUserIdValue) && (!hasStatusFilter || order.OrderStatus == status) && (!hasKeywordFilter || order.ServiceNo.Contains(keyword!) || @@ -499,6 +516,20 @@ public partial class OrderService : ApplicationService (order.TrackNo ?? string.Empty).Contains(keyword!)); } + private void EnsureCustomerCanAccessOrder(Order order) + { + var createUserId = OmsSessionHelper.TryGetCustomerCreateUserId(CurrentUser); + if (createUserId is null) + { + return; + } + + if (order.CreateUserId != createUserId.Value) + { + throw new BusinessException("无权访问该订单"); + } + } + private async Task GetOrderOrThrowAsync(int id) { var entity = await OrderRepository.FirstOrDefaultAsync(x => x.Id == id); @@ -507,6 +538,7 @@ public partial class OrderService : ApplicationService throw new BusinessException("订单不存在"); } + EnsureCustomerCanAccessOrder(entity); return entity; } diff --git a/src/Oms.Application/Services/Recharge/RechargeService.cs b/src/Oms.Application/Services/Recharge/RechargeService.cs index e7382a9..a3769f8 100644 --- a/src/Oms.Application/Services/Recharge/RechargeService.cs +++ b/src/Oms.Application/Services/Recharge/RechargeService.cs @@ -30,17 +30,10 @@ public class RechargeService : ApplicationService [Consumes("multipart/form-data")] public async Task SubmitAsync([FromForm] SubmitRechargeFormInput input) { - var customerId = input.CustomerId ?? CurrentUser.GetId(); - if (customerId <= 0) - { - throw new BusinessException("客户ID不能为空"); - } - - var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == customerId); - if (customer == null) - { - throw new BusinessException("客户不存在"); - } + var customer = await OmsSessionHelper.ResolveSessionCustomerAsync( + CustomerRepository, + CurrentUser, + input.CustomerId); if (input.RechargeAmount < 0.01M) { @@ -148,7 +141,11 @@ public class RechargeService : ApplicationService [HttpGet("/recharge/recharge-record/list")] public async Task> GetPaginatedListAsync([FromQuery] RechargeRecordPaginatedInput input) { - var predicate = BuildPaginatedPredicate(input); + var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync( + CustomerRepository, + CurrentUser, + input.CustomerId); + var predicate = BuildPaginatedPredicate(input, customerId); var pageIndex = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); @@ -203,7 +200,11 @@ public class RechargeService : ApplicationService [HttpGet("/recharge/export")] public async Task ExportAsync([FromQuery] RechargeExportInput input) { - var records = await RechargeRecordRepository.GetListAsync(BuildExportPredicate(input)); + var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync( + CustomerRepository, + CurrentUser, + input.CustomerId); + var records = await RechargeRecordRepository.GetListAsync(BuildExportPredicate(input, customerId)); var customerIds = records.Select(x => x.CustomerId).Distinct().ToArray(); var customers = customerIds.Length == 0 ? [] @@ -245,10 +246,12 @@ public class RechargeService : ApplicationService /// /// 入参:充值分页查询条件对象。 /// 出参:充值记录查询表达式。 - private Expression> BuildPaginatedPredicate(RechargeRecordPaginatedInput input) + private Expression> BuildPaginatedPredicate( + RechargeRecordPaginatedInput input, + int? customerId) { var serialNumber = input.SerialNumber?.Trim(); - return BuildRechargePredicate(input.CustomerId, input.RechargeStatus, serialNumber, input.StartTime, input.EndTime); + return BuildRechargePredicate(customerId, input.RechargeStatus, serialNumber, input.StartTime, input.EndTime); } private async Task GetRechargeRecordOrThrowAsync(int id) @@ -302,9 +305,9 @@ public class RechargeService : ApplicationService /// /// 入参:充值导出筛选条件对象。 /// 出参:充值记录查询表达式。 - private Expression> BuildExportPredicate(RechargeExportInput input) + private Expression> BuildExportPredicate(RechargeExportInput input, int? customerId) { - return BuildRechargePredicate(input.CustomerId, null, null, input.StartTime, input.EndTime); + return BuildRechargePredicate(customerId, null, null, input.StartTime, input.EndTime); } /// diff --git a/src/Oms.Application/Shared/OmsDocHelper.cs b/src/Oms.Application/Shared/OmsDocHelper.cs index 65091f1..cd29d3a 100644 --- a/src/Oms.Application/Shared/OmsDocHelper.cs +++ b/src/Oms.Application/Shared/OmsDocHelper.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Nodes; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.SystemAggregate.Consts; @@ -6,12 +7,20 @@ namespace Oms.Application.Shared; public static class OmsDocHelper { + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + public static int NormalizePage(int page) => page <= 0 ? 1 : page; public static int NormalizePageSize(int pageSize) => pageSize <= 0 ? 20 : Math.Min(pageSize, 200); public static DateTime? NullIfDefault(DateTime value) => value == default ? null : value; + /// + /// 将库内 JSON 字符串转为 API 可输出的 CLR 对象(避免 JsonElement 被 Newtonsoft 序列化成 valueKind)。 + /// public static object? DeserializeJsonObject(string? json) { if (string.IsNullOrWhiteSpace(json)) @@ -19,7 +28,8 @@ public static class OmsDocHelper return null; } - return JsonSerializer.Deserialize(json); + using var document = JsonDocument.Parse(json); + return ConvertJsonElement(document.RootElement); } public static T? DeserializeJson(string? json) @@ -29,19 +39,62 @@ public static class OmsDocHelper return default; } - return JsonSerializer.Deserialize(json); + return JsonSerializer.Deserialize(json, JsonOptions); } + /// + /// 将请求体中的动态 JSON(JsonElement / JToken / Dictionary 等)规范为可落库的 JSON 字符串。 + /// public static string? SerializeJson(T? value) { - if (value == null) + if (value is null) { return null; } - return JsonSerializer.Serialize(value); + if (value is JsonElement element) + { + return element.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined + ? null + : element.GetRawText(); + } + + if (value is JsonNode node) + { + return node.ToJsonString(); + } + + if (value is string text) + { + return string.IsNullOrWhiteSpace(text) ? null : text; + } + + var typeName = value.GetType().FullName; + if (typeName is not null && typeName.StartsWith("Newtonsoft.Json.Linq.", StringComparison.Ordinal)) + { + return value.ToString(); + } + + return JsonSerializer.Serialize(value, JsonOptions); } + private static object? ConvertJsonElement(JsonElement element) => element.ValueKind switch + { + JsonValueKind.Object => element.EnumerateObject() + .ToDictionary(property => property.Name, property => ConvertJsonElement(property.Value)), + JsonValueKind.Array => element.EnumerateArray() + .Select(ConvertJsonElement) + .ToList(), + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.TryGetInt64(out var integer) + ? integer + : element.GetDecimal(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null or JsonValueKind.Undefined => null, + _ => element.GetRawText() + }; + public static List DeserializeStringList(string? json) => JsonStringArrayHelper.Deserialize(json); public static string? GetOrderStatusName(string statusCode) diff --git a/src/Oms.Application/Shared/OmsFileUploadHelper.cs b/src/Oms.Application/Shared/OmsFileUploadHelper.cs index 600cf05..1ea6de3 100644 --- a/src/Oms.Application/Shared/OmsFileUploadHelper.cs +++ b/src/Oms.Application/Shared/OmsFileUploadHelper.cs @@ -17,11 +17,11 @@ public static class OmsFileUploadHelper /// /// 保存充值截图,返回以 /uploads 开头的相对访问路径。 /// - public static async Task SaveRechargeScreenshotAsync(IFormFile file, IWebHostEnvironment env) + public static async Task SaveRechargeScreenshotAsync(IFormFile? file, IWebHostEnvironment env) { if (file == null || file.Length <= 0) { - throw new BusinessException("请上传充值截图"); + return "0"; } if (file.Length > MaxImageBytes) diff --git a/src/Oms.Application/Shared/OmsSessionHelper.cs b/src/Oms.Application/Shared/OmsSessionHelper.cs new file mode 100644 index 0000000..45f840d --- /dev/null +++ b/src/Oms.Application/Shared/OmsSessionHelper.cs @@ -0,0 +1,105 @@ +using Fake.Application; +using Fake.Users; +using Oms.Domain.CustomersAggregate.Entities; +using Oms.Domain.CustomersAggregate.Repositories; + +namespace Oms.Application.Shared; + +/// +/// 从 JWT Claims 解析当前会话类型(管理员 / 客户)。 +/// +public static class OmsSessionHelper +{ + public const string LoginTypeClaim = "loginType"; + + private const string CustomerLoginType = "1"; + + public static bool IsCustomerLogin(ICurrentUser currentUser) => + string.Equals( + currentUser.FindClaimOrNull(LoginTypeClaim)?.Value, + CustomerLoginType, + StringComparison.Ordinal); + + /// + /// 客户登录时返回 mapId(对应 t_customer.UserId / 订单 CreateUserId);管理员返回 null。 + /// + public static int? TryGetCustomerMapId(ICurrentUser currentUser) + { + if (!IsCustomerLogin(currentUser)) + { + return null; + } + + var userId = currentUser.GetId(); + return userId > 0 ? userId : null; + } + + /// + /// 客户登录时返回 mapId(对应订单 CreateUserId);管理员返回 null(不过滤)。 + /// + public static int? TryGetCustomerCreateUserId(ICurrentUser currentUser) => + TryGetCustomerMapId(currentUser); + + /// + /// 解析当前会话对应的 t_customer 记录(客户按 UserId=mapId;管理员可按 CustomerId 代提交)。 + /// + public static async Task ResolveSessionCustomerAsync( + ICustomerRepository customerRepository, + ICurrentUser currentUser, + int? requestedCustomerId = null) + { + var mapId = TryGetCustomerMapId(currentUser); + if (mapId.HasValue) + { + var sessionCustomer = await customerRepository.FirstOrDefaultAsync(x => x.UserId == mapId.Value); + if (sessionCustomer == null) + { + throw new BusinessException("未找到客户信息"); + } + + if (requestedCustomerId is > 0 && requestedCustomerId.Value != sessionCustomer.Id) + { + throw new BusinessException("无权为该客户提交充值"); + } + + return sessionCustomer; + } + + var customerId = requestedCustomerId ?? currentUser.GetId(); + if (customerId <= 0) + { + throw new BusinessException("客户ID不能为空"); + } + + var customer = await customerRepository.FirstOrDefaultAsync(x => x.Id == customerId); + if (customer == null) + { + throw new BusinessException("客户不存在"); + } + + return customer; + } + + /// + /// 客户登录时强制返回 t_customer.Id;管理员返回入参 customerId。 + /// + public static async Task ResolveListCustomerIdAsync( + ICustomerRepository customerRepository, + ICurrentUser currentUser, + int? requestedCustomerId) + { + var mapId = TryGetCustomerMapId(currentUser); + if (!mapId.HasValue) + { + return requestedCustomerId; + } + + var customer = await customerRepository.FirstOrDefaultAsync(x => x.UserId == mapId.Value); + if (customer == null) + { + throw new BusinessException("未找到客户信息"); + } + + return customer.Id; + } +} diff --git a/src/Oms.Tests/OmsDocHelperTests.cs b/src/Oms.Tests/OmsDocHelperTests.cs index c74b36a..7ad1d41 100644 --- a/src/Oms.Tests/OmsDocHelperTests.cs +++ b/src/Oms.Tests/OmsDocHelperTests.cs @@ -56,6 +56,26 @@ public class OmsDocHelperTests result["amount"].GetDecimal().Should().Be(128.5m); } + [Fact] + public void SerializeJson_ShouldPreserveJsonElementObjectInsteadOfValueKind() + { + using var document = JsonDocument.Parse("""{"分区":"3","重量":"1.5"}"""); + var json = OmsDocHelper.SerializeJson(document.RootElement); + + json.Should().Be("""{"分区":"3","重量":"1.5"}"""); + } + + [Fact] + public void DeserializeJsonObject_ShouldReturnDictionaryNotJsonElement() + { + var result = OmsDocHelper.DeserializeJsonObject("""{"普通客户":1,"VIP1":2}"""); + + result.Should().BeOfType>(); + var map = (Dictionary)result!; + map["普通客户"].Should().Be(1L); + map["VIP1"].Should().Be(2L); + } + [Fact] public void DeserializeStringList_ShouldReturnEmptyListWhenJsonMissing() {