重构客户编号生成规则,并修复店铺订单 summary SQL 翻译错误。
重构客户编号生成规则,并修复店铺订单 summary SQL 翻译错误。
This commit is contained in:
commit
bb3881b163
@ -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; }
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
var codes = await db.Queryable<Customer>()
|
||||
.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<Customer>()
|
||||
.Where(x => x.CustomerCode.StartsWith(prefix)
|
||||
&& x.CustomerCode.Length >= minLength
|
||||
&& x.CustomerCode.Length <= maxLength)
|
||||
.Select(x => x.CustomerCode)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
47
src/Oms.Tests/CustomerCodeGeneratorTests.cs
Normal file
47
src/Oms.Tests/CustomerCodeGeneratorTests.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user