对接 Lazada/Shopee/TikTok/Temu OAuth 与回调落库,移除手工店;地址管理支持按类型默认与揽收校验。 Co-authored-by: Cursor <cursoragent@cursor.com>
285 lines
11 KiB
C#
285 lines
11 KiB
C#
using System.Linq.Expressions;
|
|
using Fake.Application;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Oms.Application.Dtos.Orders;
|
|
using Oms.Application.Dtos.Shared;
|
|
using Oms.Application.Shared;
|
|
using Oms.Domain.OrdersAggregate.Consts;
|
|
using Oms.Domain.OrdersAggregate.Entities;
|
|
using Oms.Domain.OrdersAggregate.Repositories;
|
|
|
|
namespace Oms.Application.Services.Orders;
|
|
|
|
public partial class OrderService
|
|
{
|
|
private ICustomerSenderAddressRepository CustomerSenderAddressRepository =>
|
|
LazyServiceProvider.GetRequiredService<ICustomerSenderAddressRepository>();
|
|
|
|
/// <summary>
|
|
/// 客户地址分页列表(发件/退件/收件)。
|
|
/// </summary>
|
|
[HttpPost("/order/sender-address/list")]
|
|
public async Task<OmsPageResult<CustomerSenderAddressOutput>> GetSenderAddressListAsync(
|
|
[FromBody] CustomerSenderAddressPaginatedInput input)
|
|
{
|
|
var customer = await OmsSessionHelper.ResolveSessionCustomerAsync(CustomerRepository, CurrentUser);
|
|
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
|
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
|
var predicate = BuildSenderAddressPredicate(customer.Id, input);
|
|
|
|
var entities = await CustomerSenderAddressRepository.GetPagedListAsync(
|
|
predicate,
|
|
pageIndex,
|
|
pageSize,
|
|
new Dictionary<string, bool>
|
|
{
|
|
[nameof(CustomerSenderAddress.IsDefault)] = false,
|
|
[nameof(CustomerSenderAddress.Id)] = false
|
|
});
|
|
var total = await CustomerSenderAddressRepository.CountAsync(predicate);
|
|
var countryMap = await LoadCountryCodeMapAsync(entities.Select(x => x.Country).Where(x => x.HasValue).Select(x => x!.Value));
|
|
|
|
return new OmsPageResult<CustomerSenderAddressOutput>
|
|
{
|
|
Items = entities.Select(x => MapSenderAddressOutput(x, countryMap)).ToList(),
|
|
Total = total
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 客户地址详情。
|
|
/// </summary>
|
|
[HttpGet("/order/sender-address/detail")]
|
|
public async Task<CustomerSenderAddressOutput> GetSenderAddressDetailAsync([FromQuery] int id)
|
|
{
|
|
var entity = await GetOwnedSenderAddressOrThrowAsync(id);
|
|
var countryMap = await LoadCountryCodeMapAsync(entity.Country.HasValue ? [entity.Country.Value] : []);
|
|
return MapSenderAddressOutput(entity, countryMap);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 新增客户地址。
|
|
/// </summary>
|
|
[HttpPost("/order/sender-address/create")]
|
|
public async Task<object> CreateSenderAddressAsync([FromBody] CreateCustomerSenderAddressInput input)
|
|
{
|
|
var customer = await OmsSessionHelper.ResolveSessionCustomerAsync(CustomerRepository, CurrentUser);
|
|
var addressType = NormalizeAddressTypeOrThrow(input.AddressType);
|
|
ValidateSenderAddressInput(input);
|
|
var countryId = await ResolveSenderCountryIdAsync(input.CountryCode);
|
|
|
|
if (input.IsDefault)
|
|
{
|
|
await CustomerSenderAddressRepository.ClearDefaultAsync(customer.Id, addressType);
|
|
}
|
|
|
|
var entity = new CustomerSenderAddress
|
|
{
|
|
CustomerId = customer.Id,
|
|
AddressType = addressType,
|
|
AddressName = TrimOrNull(input.AddressName),
|
|
SenderName = input.SenderName.Trim(),
|
|
Company = TrimOrNull(input.Company),
|
|
Country = countryId,
|
|
Province = TrimOrNull(input.Province),
|
|
City = TrimOrNull(input.City),
|
|
ZipCode = TrimOrNull(input.ZipCode),
|
|
StreetAddress1 = input.StreetAddress1.Trim(),
|
|
StreetAddress2 = TrimOrNull(input.StreetAddress2),
|
|
StreetAddress3 = TrimOrNull(input.StreetAddress3),
|
|
HouseNumber = TrimOrNull(input.HouseNumber),
|
|
Phone = TrimOrNull(input.Phone),
|
|
TelePhone = TrimOrNull(input.TelePhone),
|
|
Email = TrimOrNull(input.Email),
|
|
IsDefault = input.IsDefault ? (byte)1 : (byte)0
|
|
};
|
|
|
|
entity = await CustomerSenderAddressRepository.InsertAsync(entity);
|
|
return new { id = entity.Id };
|
|
}
|
|
|
|
/// <summary>
|
|
/// 更新客户地址。
|
|
/// </summary>
|
|
[HttpPost("/order/sender-address/update")]
|
|
public async Task UpdateSenderAddressAsync([FromBody] UpdateCustomerSenderAddressInput input)
|
|
{
|
|
var entity = await GetOwnedSenderAddressOrThrowAsync(input.Id);
|
|
var addressType = NormalizeAddressTypeOrThrow(input.AddressType);
|
|
ValidateSenderAddressInput(input);
|
|
var countryId = await ResolveSenderCountryIdAsync(input.CountryCode);
|
|
|
|
if (input.IsDefault)
|
|
{
|
|
await CustomerSenderAddressRepository.ClearDefaultAsync(entity.CustomerId, addressType, entity.Id);
|
|
}
|
|
|
|
entity.AddressType = addressType;
|
|
entity.AddressName = TrimOrNull(input.AddressName);
|
|
entity.SenderName = input.SenderName.Trim();
|
|
entity.Company = TrimOrNull(input.Company);
|
|
entity.Country = countryId;
|
|
entity.Province = TrimOrNull(input.Province);
|
|
entity.City = TrimOrNull(input.City);
|
|
entity.ZipCode = TrimOrNull(input.ZipCode);
|
|
entity.StreetAddress1 = input.StreetAddress1.Trim();
|
|
entity.StreetAddress2 = TrimOrNull(input.StreetAddress2);
|
|
entity.StreetAddress3 = TrimOrNull(input.StreetAddress3);
|
|
entity.HouseNumber = TrimOrNull(input.HouseNumber);
|
|
entity.Phone = TrimOrNull(input.Phone);
|
|
entity.TelePhone = TrimOrNull(input.TelePhone);
|
|
entity.Email = TrimOrNull(input.Email);
|
|
entity.IsDefault = input.IsDefault ? (byte)1 : (byte)0;
|
|
|
|
await CustomerSenderAddressRepository.UpdateAsync(entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 删除客户地址(软删)。
|
|
/// </summary>
|
|
[HttpPost("/order/sender-address/delete")]
|
|
public async Task DeleteSenderAddressAsync([FromBody] IdInput input)
|
|
{
|
|
var entity = await GetOwnedSenderAddressOrThrowAsync(input.Id);
|
|
await CustomerSenderAddressRepository.DeleteAsync(entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设为同类型默认地址。
|
|
/// </summary>
|
|
[HttpPost("/order/sender-address/set-default")]
|
|
public async Task SetDefaultSenderAddressAsync([FromBody] IdInput input)
|
|
{
|
|
var entity = await GetOwnedSenderAddressOrThrowAsync(input.Id);
|
|
var addressType = string.IsNullOrWhiteSpace(entity.AddressType)
|
|
? CustomerAddressTypeCodes.Sender
|
|
: entity.AddressType;
|
|
await CustomerSenderAddressRepository.ClearDefaultAsync(entity.CustomerId, addressType, entity.Id);
|
|
entity.IsDefault = 1;
|
|
await CustomerSenderAddressRepository.UpdateAsync(entity);
|
|
}
|
|
|
|
private async Task<CustomerSenderAddress> GetOwnedSenderAddressOrThrowAsync(int id)
|
|
{
|
|
var customer = await OmsSessionHelper.ResolveSessionCustomerAsync(CustomerRepository, CurrentUser);
|
|
var entity = await CustomerSenderAddressRepository.GetOwnedAsync(id, customer.Id);
|
|
if (entity == null)
|
|
{
|
|
throw new BusinessException("地址不存在");
|
|
}
|
|
|
|
return entity;
|
|
}
|
|
|
|
private static string NormalizeAddressTypeOrThrow(string? addressType)
|
|
{
|
|
try
|
|
{
|
|
return CustomerAddressTypeCodes.NormalizeOrThrow(addressType);
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
throw new BusinessException("地址类型不合法,仅支持发件/退件/收件");
|
|
}
|
|
}
|
|
|
|
private static void ValidateSenderAddressInput(CreateCustomerSenderAddressInput input)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(input.SenderName))
|
|
{
|
|
throw new BusinessException("联系人姓名不能为空");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(input.StreetAddress1))
|
|
{
|
|
throw new BusinessException("详细地址不能为空");
|
|
}
|
|
}
|
|
|
|
private async Task<int?> ResolveSenderCountryIdAsync(string? countryCode)
|
|
{
|
|
var code = string.IsNullOrWhiteSpace(countryCode) ? "CN" : countryCode.Trim();
|
|
var country = await GetCountryOrThrowAsync(code, "国家不存在");
|
|
return country.Id;
|
|
}
|
|
|
|
private static Expression<Func<CustomerSenderAddress, bool>> BuildSenderAddressPredicate(
|
|
int customerId,
|
|
CustomerSenderAddressPaginatedInput input)
|
|
{
|
|
var keyword = input.Keyword?.Trim();
|
|
var onlyDefault = input.OnlyDefault;
|
|
var addressType = input.AddressType?.Trim();
|
|
var filterByType = !string.IsNullOrWhiteSpace(addressType)
|
|
&& !string.Equals(addressType, "all", StringComparison.OrdinalIgnoreCase);
|
|
|
|
return x =>
|
|
x.CustomerId == customerId
|
|
&& (!filterByType || x.AddressType == addressType)
|
|
&& (!onlyDefault || x.IsDefault == 1)
|
|
&& (string.IsNullOrWhiteSpace(keyword)
|
|
|| (x.AddressName ?? string.Empty).Contains(keyword)
|
|
|| x.SenderName.Contains(keyword)
|
|
|| (x.Phone ?? string.Empty).Contains(keyword)
|
|
|| x.StreetAddress1.Contains(keyword));
|
|
}
|
|
|
|
private async Task<Dictionary<int, string>> LoadCountryCodeMapAsync(IEnumerable<int> countryIds)
|
|
{
|
|
var ids = countryIds.Distinct().ToArray();
|
|
if (ids.Length == 0)
|
|
{
|
|
return new Dictionary<int, string>();
|
|
}
|
|
|
|
var countries = await CountryRepository.GetListAsync(x => ids.Contains(x.Id));
|
|
return countries.ToDictionary(x => x.Id, x => x.CountryCode ?? string.Empty);
|
|
}
|
|
|
|
private static CustomerSenderAddressOutput MapSenderAddressOutput(
|
|
CustomerSenderAddress entity,
|
|
IReadOnlyDictionary<int, string> countryMap)
|
|
{
|
|
string? countryCode = null;
|
|
if (entity.Country.HasValue && countryMap.TryGetValue(entity.Country.Value, out var code))
|
|
{
|
|
countryCode = code;
|
|
}
|
|
|
|
var addressType = string.IsNullOrWhiteSpace(entity.AddressType)
|
|
? CustomerAddressTypeCodes.Sender
|
|
: entity.AddressType;
|
|
|
|
return new CustomerSenderAddressOutput
|
|
{
|
|
Id = entity.Id,
|
|
CustomerId = entity.CustomerId,
|
|
AddressType = addressType,
|
|
AddressTypeName = CustomerAddressTypeCodes.GetDisplayName(addressType),
|
|
AddressName = entity.AddressName,
|
|
SenderName = entity.SenderName,
|
|
Company = entity.Company,
|
|
Country = entity.Country,
|
|
CountryCode = countryCode,
|
|
Province = entity.Province,
|
|
City = entity.City,
|
|
ZipCode = entity.ZipCode,
|
|
StreetAddress1 = entity.StreetAddress1,
|
|
StreetAddress2 = entity.StreetAddress2,
|
|
StreetAddress3 = entity.StreetAddress3,
|
|
HouseNumber = entity.HouseNumber,
|
|
Phone = entity.Phone,
|
|
TelePhone = entity.TelePhone,
|
|
Email = entity.Email,
|
|
IsDefault = entity.IsDefault > 0,
|
|
CreateTime = entity.CreateTime,
|
|
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
|
|
};
|
|
}
|
|
|
|
private static string? TrimOrNull(string? value)
|
|
{
|
|
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
|
}
|
|
}
|