重构客户编号生成规则,并修复店铺订单 summary SQL 翻译错误。

重构客户编号生成规则,并修复店铺订单 summary SQL 翻译错误。
This commit is contained in:
biybuy 2026-07-11 17:55:18 +08:00 committed by GitHub
commit bb3881b163
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 220 additions and 31 deletions

View File

@ -9,7 +9,7 @@ namespace Oms.Application.Dtos.Customers;
public class CreateCustomerInput
{
/// <summary>
/// 客户编号(留空时系统自动生成,规则 W+年份后2位+月份2位+3位顺序)。
/// 客户编号(留空时系统自动生成,规则:年码+月码+序号,如 AF0A1)。
/// </summary>
[StringLength(32)]
public string? CustomerCode { get; set; }

View File

@ -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
{

View File

@ -4,11 +4,13 @@ using Oms.Domain.CustomersAggregate.Repositories;
namespace Oms.Application.Shared;
/// <summary>
/// 客户编号生成:W + 年份后2位 + 月份2位 + 3位顺序号,如 W2606001。
/// 客户编号生成:年码(A=2026)+月码(A=1月) + 序号编码。
/// 1~1000 为 5 位(如 AF0A1);1001~10000 为 6 位(如 AF10A1)。
/// </summary>
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<string> 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<string> 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;
}
}

View File

@ -22,7 +22,9 @@ public interface ICustomerRepository : IRepository<Customer>
Task<bool> ExistsWechatOpenidAnyAsync(string wechatOpenid, CancellationToken cancellationToken = default);
/// <summary>
/// 查询指定前缀(WyyMM)下已用顺序号的最大值。
/// 查询指定年月前缀下的客户编号列表(5 位或 6 位序号体)。
/// </summary>
Task<int> GetMaxCustomerCodeSequenceForPrefixAsync(string prefix, CancellationToken cancellationToken = default);
Task<IReadOnlyList<string>> ListCustomerCodesByPrefixAsync(
string prefix,
CancellationToken cancellationToken = default);
}

View File

@ -59,32 +59,23 @@ public class CustomerRepository : OmsSqlSugarRepository<Customer>, ICustomerRepo
return hit.HasValue;
}
public async Task<int> GetMaxCustomerCodeSequenceForPrefixAsync(
public async Task<IReadOnlyList<string>> ListCustomerCodesByPrefixAsync(
string prefix,
CancellationToken cancellationToken = default)
{
var expectedLength = prefix.Length + 3;
if (string.IsNullOrWhiteSpace(prefix))
{
return [];
}
var minLength = prefix.Length + 3;
var maxLength = prefix.Length + 4;
var db = await GetDbContextAsync(cancellationToken);
var codes = await db.Queryable<Customer>()
.Where(x => x.CustomerCode.StartsWith(prefix) && x.CustomerCode.Length == expectedLength)
return await db.Queryable<Customer>()
.Where(x => x.CustomerCode.StartsWith(prefix)
&& x.CustomerCode.Length >= minLength
&& x.CustomerCode.Length <= maxLength)
.Select(x => x.CustomerCode)
.ToListAsync(cancellationToken);
var max = 0;
foreach (var code in codes)
{
if (code.Length <= prefix.Length)
{
continue;
}
var suffix = code[prefix.Length..];
if (int.TryParse(suffix, out var seq) && seq > max)
{
max = seq;
}
}
return max;
}
}

View File

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