采用年码+月码+序号编码(5/6位,单月最多10000);summary 接口改用 hasStoreFilter 避免 SqlSugar 生成非法 SQL。 Co-authored-by: Cursor <cursoragent@cursor.com>
192 lines
5.4 KiB
C#
192 lines
5.4 KiB
C#
using Fake.Application;
|
||
using Oms.Domain.CustomersAggregate.Repositories;
|
||
|
||
namespace Oms.Application.Shared;
|
||
|
||
/// <summary>
|
||
/// 客户编号生成:年码(A=2026)+月码(A=1月) + 序号编码。
|
||
/// 1~1000 为 5 位(如 AF0A1);1001~10000 为 6 位(如 AF10A1)。
|
||
/// </summary>
|
||
public static class CustomerCodeGenerator
|
||
{
|
||
private const int BaseYear = 2026;
|
||
private const int MaxSequence = 10000;
|
||
private const int MaxRetries = 5;
|
||
|
||
public static async Task<string> GenerateNextAsync(
|
||
ICustomerRepository repository,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
for (var attempt = 0; attempt < MaxRetries; attempt++)
|
||
{
|
||
var code = await BuildNextCodeAsync(repository, cancellationToken);
|
||
if (await repository.GetByCustomerCodeAsync(code, cancellationToken) == null)
|
||
{
|
||
return code;
|
||
}
|
||
}
|
||
|
||
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 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($"当月客户编号已达上限({monthPrefix},最多 {MaxSequence} 条)");
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|