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");
+ }
+}