diff --git a/src/Oms.Application/Dtos/Customers/CreateCustomerInput.cs b/src/Oms.Application/Dtos/Customers/CreateCustomerInput.cs index 3edfcf0..6aa6712 100644 --- a/src/Oms.Application/Dtos/Customers/CreateCustomerInput.cs +++ b/src/Oms.Application/Dtos/Customers/CreateCustomerInput.cs @@ -9,7 +9,7 @@ namespace Oms.Application.Dtos.Customers; public class CreateCustomerInput { /// - /// 客户编号(留空时系统自动生成,规则 W+年份后2位+月份2位+3位顺序)。 + /// 客户编号(留空时系统自动生成,规则:年码+月码+序号,如 AF0A1)。 /// [StringLength(32)] public string? CustomerCode { get; set; } diff --git a/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs b/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs index 1e0d1d7..eef3c11 100644 --- a/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs +++ b/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs @@ -126,9 +126,11 @@ public class PlatformStoreOrderService : ApplicationService CurrentUser, customerId); var (hasCustomerFilter, customerIdValue) = WarehouseCustomerHelper.ToCustomerFilter(resolvedCustomerId); + var hasStoreFilter = storeId.HasValue; + var storeIdValue = storeId.GetValueOrDefault(); var orders = await StoreOrderRepository.GetListAsync(x => (!hasCustomerFilter || x.CustomerId == customerIdValue) - && (!storeId.HasValue || x.StoreId == storeId.Value)); + && (!hasStoreFilter || x.StoreId == storeIdValue)); return new PlatformStoreOrderSummaryOutput { diff --git a/src/Oms.Application/Shared/CustomerCodeGenerator.cs b/src/Oms.Application/Shared/CustomerCodeGenerator.cs index 9f4fff3..e872d46 100644 --- a/src/Oms.Application/Shared/CustomerCodeGenerator.cs +++ b/src/Oms.Application/Shared/CustomerCodeGenerator.cs @@ -4,11 +4,13 @@ using Oms.Domain.CustomersAggregate.Repositories; namespace Oms.Application.Shared; /// -/// 客户编号生成:W + 年份后2位 + 月份2位 + 3位顺序号,如 W2606001。 +/// 客户编号生成:年码(A=2026)+月码(A=1月) + 序号编码。 +/// 1~1000 为 5 位(如 AF0A1);1001~10000 为 6 位(如 AF10A1)。 /// public static class CustomerCodeGenerator { - private const int MaxSequence = 999; + private const int BaseYear = 2026; + private const int MaxSequence = 10000; private const int MaxRetries = 5; public static async Task GenerateNextAsync( @@ -27,18 +29,163 @@ public static class CustomerCodeGenerator throw new BusinessException("客户编号生成失败,请重试"); } + internal static string BuildMonthPrefix(DateTime time) => + $"{ToYearCode(time.Year)}{ToMonthCode(time.Month)}"; + + internal static string FormatSequence(int sequence, string monthPrefix) + { + if (sequence is < 1 or > MaxSequence) + { + throw new BusinessException($"客户序号超出范围(1~{MaxSequence})"); + } + + if (sequence <= 1000) + { + return monthPrefix + EncodeSuffix(sequence); + } + + return EncodeExpanded(sequence, monthPrefix); + } + + internal static bool TryParseSequence(string customerCode, string monthPrefix, out int sequence) + { + sequence = 0; + if (string.IsNullOrWhiteSpace(customerCode) + || !customerCode.StartsWith(monthPrefix, StringComparison.Ordinal)) + { + return false; + } + + var body = customerCode[monthPrefix.Length..]; + return body.Length switch + { + 3 => TryDecodeSuffix(body, out sequence), + 4 when char.IsDigit(body[0]) && body[0] != '0' + => TryDecodeExpanded(body[0] - '0', body[1..], out sequence), + _ => false + }; + } + private static async Task BuildNextCodeAsync( ICustomerRepository repository, CancellationToken cancellationToken) { - var prefix = $"W{DateTime.Now:yyMM}"; - var maxSeq = await repository.GetMaxCustomerCodeSequenceForPrefixAsync(prefix, cancellationToken); + var now = DateTime.Now; + var monthPrefix = BuildMonthPrefix(now); + var codes = await repository.ListCustomerCodesByPrefixAsync(monthPrefix, cancellationToken); + var maxSeq = 0; + foreach (var code in codes) + { + if (TryParseSequence(code, monthPrefix, out var sequence) && sequence > maxSeq) + { + maxSeq = sequence; + } + } + var next = maxSeq + 1; if (next > MaxSequence) { - throw new BusinessException($"当月客户编号已达上限({prefix}xxx)"); + throw new BusinessException($"当月客户编号已达上限({monthPrefix},最多 {MaxSequence} 条)"); } - return $"{prefix}{next:D3}"; + return FormatSequence(next, monthPrefix); + } + + private static char ToYearCode(int year) + { + if (year < BaseYear) + { + throw new BusinessException($"客户编号暂不支持 {BaseYear} 年之前的年份"); + } + + var offset = year - BaseYear; + if (offset > 25) + { + throw new BusinessException($"客户编号年份编码超出范围({BaseYear}~{BaseYear + 25})"); + } + + return (char)('A' + offset); + } + + private static char ToMonthCode(int month) + { + if (month is < 1 or > 12) + { + throw new BusinessException("月份无效"); + } + + return (char)('A' + month - 1); + } + + private static string EncodeSuffix(int sequence) + { + if (sequence == 1000) + { + return "0A0"; + } + + var hundreds = sequence / 100; + var tens = (sequence / 10) % 10; + var ones = sequence % 10; + return $"{hundreds}{(char)('A' + tens)}{ones}"; + } + + private static bool TryDecodeSuffix(string suffix, out int sequence) + { + sequence = 0; + if (suffix.Length != 3 + || !char.IsDigit(suffix[0]) + || !char.IsDigit(suffix[2]) + || suffix[1] is < 'A' or > 'J') + { + return false; + } + + if (suffix == "0A0") + { + sequence = 1000; + return true; + } + + var hundreds = suffix[0] - '0'; + var tens = suffix[1] - 'A'; + var ones = suffix[2] - '0'; + sequence = hundreds * 100 + tens * 10 + ones; + return sequence is >= 1 and <= 999; + } + + private static string EncodeExpanded(int sequence, string monthPrefix) + { + var expansion = Math.Min(sequence / 1000, 9); + var local = sequence - expansion * 1000; + if (local == 0) + { + return monthPrefix + expansion + "0A0"; + } + + return monthPrefix + expansion + EncodeSuffix(local); + } + + private static bool TryDecodeExpanded(int expansion, string suffix, out int sequence) + { + sequence = 0; + if (expansion is < 1 or > 9) + { + return false; + } + + if (suffix == "0A0") + { + sequence = expansion == 9 ? 10000 : expansion * 1000; + return true; + } + + if (!TryDecodeSuffix(suffix, out var local)) + { + return false; + } + + sequence = expansion * 1000 + local; + return sequence is >= 1001 and <= MaxSequence; } } diff --git a/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerRepository.cs b/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerRepository.cs index 62b07a5..6d1ed8d 100644 --- a/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerRepository.cs +++ b/src/Oms.Domain/CustomersAggregate/Repositories/ICustomerRepository.cs @@ -22,7 +22,9 @@ public interface ICustomerRepository : IRepository Task ExistsWechatOpenidAnyAsync(string wechatOpenid, CancellationToken cancellationToken = default); /// - /// 查询指定前缀(WyyMM)下已用顺序号的最大值。 + /// 查询指定年月前缀下的客户编号列表(5 位或 6 位序号体)。 /// - Task GetMaxCustomerCodeSequenceForPrefixAsync(string prefix, CancellationToken cancellationToken = default); + Task> ListCustomerCodesByPrefixAsync( + string prefix, + CancellationToken cancellationToken = default); } diff --git a/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs b/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs index 60fe0aa..775e299 100644 --- a/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs +++ b/src/Oms.SqlSugarCore/Customers/CustomerRepository.cs @@ -59,32 +59,23 @@ public class CustomerRepository : OmsSqlSugarRepository, ICustomerRepo return hit.HasValue; } - public async Task GetMaxCustomerCodeSequenceForPrefixAsync( + public async Task> ListCustomerCodesByPrefixAsync( string prefix, CancellationToken cancellationToken = default) { - var expectedLength = prefix.Length + 3; - var db = await GetDbContextAsync(cancellationToken); - var codes = await db.Queryable() - .Where(x => x.CustomerCode.StartsWith(prefix) && x.CustomerCode.Length == expectedLength) - .Select(x => x.CustomerCode) - .ToListAsync(cancellationToken); - - var max = 0; - foreach (var code in codes) + if (string.IsNullOrWhiteSpace(prefix)) { - if (code.Length <= prefix.Length) - { - continue; - } - - var suffix = code[prefix.Length..]; - if (int.TryParse(suffix, out var seq) && seq > max) - { - max = seq; - } + return []; } - return max; + var minLength = prefix.Length + 3; + var maxLength = prefix.Length + 4; + var db = await GetDbContextAsync(cancellationToken); + return await db.Queryable() + .Where(x => x.CustomerCode.StartsWith(prefix) + && x.CustomerCode.Length >= minLength + && x.CustomerCode.Length <= maxLength) + .Select(x => x.CustomerCode) + .ToListAsync(cancellationToken); } } diff --git a/src/Oms.Tests/CustomerCodeGeneratorTests.cs b/src/Oms.Tests/CustomerCodeGeneratorTests.cs new file mode 100644 index 0000000..940171b --- /dev/null +++ b/src/Oms.Tests/CustomerCodeGeneratorTests.cs @@ -0,0 +1,47 @@ +using FluentAssertions; +using Oms.Application.Shared; + +namespace Oms.Tests; + +public class CustomerCodeGeneratorTests +{ + private const string June2026Prefix = "AF"; + + [Theory] + [InlineData(1, "AF0A1")] + [InlineData(12, "AF0B2")] + [InlineData(100, "AF1A0")] + [InlineData(999, "AF9J9")] + [InlineData(1000, "AF0A0")] + [InlineData(1001, "AF10A1")] + [InlineData(1999, "AF19J9")] + [InlineData(2000, "AF20A0")] + [InlineData(10000, "AF90A0")] + public void FormatSequence_ShouldMatchExpectedCode(int sequence, string expectedCode) + { + CustomerCodeGenerator.FormatSequence(sequence, June2026Prefix).Should().Be(expectedCode); + } + + [Theory] + [InlineData("AF0A1", 1)] + [InlineData("AF0B2", 12)] + [InlineData("AF1A0", 100)] + [InlineData("AF9J9", 999)] + [InlineData("AF0A0", 1000)] + [InlineData("AF10A1", 1001)] + [InlineData("AF19J9", 1999)] + [InlineData("AF20A0", 2000)] + [InlineData("AF90A0", 10000)] + public void TryParseSequence_ShouldRoundTripFormattedCodes(string code, int expectedSequence) + { + CustomerCodeGenerator.TryParseSequence(code, June2026Prefix, out var sequence).Should().BeTrue(); + sequence.Should().Be(expectedSequence); + } + + [Fact] + public void BuildMonthPrefix_ShouldUseYearAndMonthLetters() + { + CustomerCodeGenerator.BuildMonthPrefix(new DateTime(2026, 6, 1)).Should().Be("AF"); + CustomerCodeGenerator.BuildMonthPrefix(new DateTime(2027, 1, 1)).Should().Be("BA"); + } +}