All checks were successful
Build and Deploy / deploy (push) Successful in 1m2s
- 新增 POST /order/customs-9610/create-from-orders:运单设为 9610Summary 并绑定出口资质备案(空则客户默认),按申报明细生成待申报单 - 逐单返回结果:已删除、已有 9610 单、无申报明细、无可用备案记为失败,不影响其它运单
1367 lines
52 KiB
C#
1367 lines
52 KiB
C#
using System.Text.Json;
|
||
using Fake.Application;
|
||
using Fake.Domain.Exceptions;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.Extensions.Options;
|
||
using Oms.Application.Dtos.Orders;
|
||
using Oms.Application.Dtos.Shared;
|
||
using Oms.Application.Integrations.Itfx;
|
||
using Oms.Application.Shared;
|
||
using Oms.Domain.CustomersAggregate.Repositories;
|
||
using Oms.Domain.OrdersAggregate.Consts;
|
||
using Oms.Domain.OrdersAggregate.Entities;
|
||
using Oms.Domain.OrdersAggregate.Repositories;
|
||
|
||
namespace Oms.Application.Services.Orders;
|
||
|
||
/// <summary>
|
||
/// 9610 汇总申报单管理。
|
||
/// </summary>
|
||
[ApiExplorerSettings(GroupName = "OMS-Order")]
|
||
public class Customs9610OrderService : ApplicationService
|
||
{
|
||
private ICustoms9610OrderRepository HeaderRepository =>
|
||
LazyServiceProvider.GetRequiredService<ICustoms9610OrderRepository>();
|
||
|
||
private ICustoms9610OrderItemRepository ItemRepository =>
|
||
LazyServiceProvider.GetRequiredService<ICustoms9610OrderItemRepository>();
|
||
|
||
private IOrderRepository OrderRepository =>
|
||
LazyServiceProvider.GetRequiredService<IOrderRepository>();
|
||
|
||
private ICustomerRepository CustomerRepository =>
|
||
LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||
|
||
private IItfxForwardClient ItfxForwardClient =>
|
||
LazyServiceProvider.GetRequiredService<IItfxForwardClient>();
|
||
|
||
private OrderDownstreamCredentialResolver CredentialResolver =>
|
||
LazyServiceProvider.GetRequiredService<OrderDownstreamCredentialResolver>();
|
||
|
||
private IOptions<ItfxGatewayOptions> ItfxOptions =>
|
||
LazyServiceProvider.GetRequiredService<IOptions<ItfxGatewayOptions>>();
|
||
|
||
private ICustomerExportQualificationRepository ExportQualificationRepository =>
|
||
LazyServiceProvider.GetRequiredService<ICustomerExportQualificationRepository>();
|
||
|
||
private Customs9610OrderWriter Customs9610Writer =>
|
||
LazyServiceProvider.GetRequiredService<Customs9610OrderWriter>();
|
||
|
||
[HttpPost("/order/customs-9610/list")]
|
||
public async Task<OmsPageResult<Customs9610OrderOutput>> ListAsync([FromBody] Customs9610ListInput input)
|
||
{
|
||
var page = input.Page <= 0 ? 1 : input.Page;
|
||
var pageSize = input.PageSize <= 0 ? 30 : Math.Min(input.PageSize, 200);
|
||
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
|
||
CustomerRepository,
|
||
CurrentUser,
|
||
input.CustomerId);
|
||
|
||
var keyword = Normalize(input.Keyword);
|
||
var status = Normalize(input.Status);
|
||
|
||
var all = await HeaderRepository.GetListAsync(x =>
|
||
(customerId == null || x.CustomerId == customerId.Value) &&
|
||
(status == null || x.Status == status) &&
|
||
(input.CreateTimeStart == null || x.CreateTime >= input.CreateTimeStart) &&
|
||
(input.CreateTimeEnd == null || x.CreateTime <= input.CreateTimeEnd) &&
|
||
(keyword == null ||
|
||
(x.ServiceNo != null && x.ServiceNo.Contains(keyword)) ||
|
||
(x.ClientNo != null && x.ClientNo.Contains(keyword)) ||
|
||
(x.CustomsOrderNo != null && x.CustomsOrderNo.Contains(keyword))));
|
||
|
||
var ordered = all.OrderByDescending(x => x.CreateTime).ThenByDescending(x => x.Id).ToList();
|
||
var total = ordered.Count;
|
||
var pageItems = ordered.Skip((page - 1) * pageSize).Take(pageSize).ToList();
|
||
var customerNames = await LoadCustomerNamesAsync(pageItems.Select(x => x.CustomerId));
|
||
|
||
return new OmsPageResult<Customs9610OrderOutput>
|
||
{
|
||
Total = total,
|
||
Items = pageItems.Select(x => MapOutput(x, customerNames, includeItems: false)).ToList()
|
||
};
|
||
}
|
||
|
||
[HttpGet("/order/customs-9610/status-counts")]
|
||
public async Task<Customs9610StatusCountsOutput> GetStatusCountsAsync([FromQuery] int? customerId)
|
||
{
|
||
var resolvedCustomerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
|
||
CustomerRepository,
|
||
CurrentUser,
|
||
customerId);
|
||
|
||
var all = await HeaderRepository.GetListAsync(x =>
|
||
resolvedCustomerId == null || x.CustomerId == resolvedCustomerId.Value);
|
||
|
||
return new Customs9610StatusCountsOutput
|
||
{
|
||
Total = all.Count,
|
||
Pending = all.Count(x =>
|
||
string.Equals(x.Status, Customs9610StatusCodes.Pending, StringComparison.OrdinalIgnoreCase)),
|
||
Declared = all.Count(x =>
|
||
string.Equals(x.Status, Customs9610StatusCodes.Declared, StringComparison.OrdinalIgnoreCase)),
|
||
};
|
||
}
|
||
|
||
[HttpGet("/order/customs-9610/detail")]
|
||
public async Task<Customs9610OrderOutput> GetDetailAsync([FromQuery] int id)
|
||
{
|
||
var header = await GetOwnedOrThrowAsync(id);
|
||
var items = await ItemRepository.GetByHeaderIdAsync(header.Id);
|
||
var customerNames = await LoadCustomerNamesAsync([header.CustomerId]);
|
||
var output = MapOutput(header, customerNames, includeItems: true);
|
||
output.Items = items
|
||
.OrderBy(x => x.SortNo)
|
||
.ThenBy(x => x.Id)
|
||
.Select(MapItem)
|
||
.ToList();
|
||
return output;
|
||
}
|
||
|
||
[HttpPost("/order/customs-9610/update")]
|
||
public async Task<Customs9610OrderOutput> UpdateAsync([FromBody] Customs9610UpdateInput input)
|
||
{
|
||
var header = await GetOwnedOrThrowAsync(input.Id);
|
||
if (!string.Equals(header.Status, Customs9610StatusCodes.Pending, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
throw new BusinessException("仅待申报单据可编辑");
|
||
}
|
||
|
||
ValidateFormFields(input);
|
||
|
||
header.PlatformCode = input.PlatformCode.Trim();
|
||
header.EcomCorpCode = Normalize(input.EcomCorpCode);
|
||
header.EcomCorpName = Normalize(input.EcomCorpName);
|
||
header.ProducerCode = Normalize(input.ProducerCode);
|
||
header.ProducerName = Normalize(input.ProducerName);
|
||
header.ClientNo = input.ClientNo.Trim();
|
||
header.ServiceNo = input.ServiceNo.Trim();
|
||
header.BusinessType = input.BusinessType.Trim().ToUpperInvariant();
|
||
header.Region = Normalize(input.Region);
|
||
header.AgentDeclare = Normalize(input.AgentDeclare);
|
||
header.ActualSellerCode = Normalize(input.ActualSellerCode);
|
||
header.PreRecordFlag = Normalize(input.PreRecordFlag);
|
||
header.IsSummarySimple = input.IsSummarySimple ? (byte)1 : (byte)0;
|
||
header.GrossWeight = input.GrossWeight;
|
||
header.Currency = input.Currency.Trim().ToUpperInvariant();
|
||
header.DestCountry = input.DestCountry.Trim().ToUpperInvariant();
|
||
header.Freight = input.Freight;
|
||
header.ShipmentNo = Normalize(input.ShipmentNo);
|
||
header.FailReason = null;
|
||
header.UpdateTime = DateTime.Now;
|
||
await HeaderRepository.UpdateAsync(header);
|
||
|
||
await HeaderRepository.HardDeleteItemsByHeaderIdAsync(header.Id);
|
||
var sort = 0;
|
||
foreach (var row in input.Items)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(row.Sku) || string.IsNullOrWhiteSpace(row.CnName))
|
||
{
|
||
throw new BusinessException("商品货号与品名不能为空");
|
||
}
|
||
|
||
await ItemRepository.InsertAsync(new Customs9610OrderItem
|
||
{
|
||
Customs9610OrderId = header.Id,
|
||
OrderDeclarationId = row.OrderDeclarationId,
|
||
Sku = row.Sku.Trim(),
|
||
CnName = row.CnName.Trim(),
|
||
ExHsCode = Normalize(row.ExHsCode),
|
||
DeclareElement = Normalize(row.DeclareElement),
|
||
UnitPrice = row.UnitPrice,
|
||
Quantity = row.Quantity,
|
||
Freight = row.Freight,
|
||
SortNo = row.SortNo > 0 ? row.SortNo : sort,
|
||
CreateTime = DateTime.Now,
|
||
});
|
||
sort++;
|
||
}
|
||
|
||
return await GetDetailAsync(header.Id);
|
||
}
|
||
|
||
[HttpPost("/order/customs-9610/declare")]
|
||
public async Task<Customs9610DeclareResult> DeclareAsync([FromBody] Customs9610IdInput input)
|
||
{
|
||
return await DeclareOneAsync(input.Id);
|
||
}
|
||
|
||
[HttpPost("/order/customs-9610/declare-batch")]
|
||
public async Task<Customs9610DeclareBatchOutput> DeclareBatchAsync([FromBody] Customs9610IdsInput input)
|
||
{
|
||
var results = new List<Customs9610DeclareResult>();
|
||
foreach (var id in input.Ids.Where(x => x > 0).Distinct())
|
||
{
|
||
try
|
||
{
|
||
results.Add(await DeclareOneAsync(id));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
results.Add(new Customs9610DeclareResult
|
||
{
|
||
Id = id,
|
||
Success = false,
|
||
Message = string.IsNullOrWhiteSpace(ex.Message) ? "申报失败" : ex.Message,
|
||
});
|
||
}
|
||
}
|
||
|
||
return new Customs9610DeclareBatchOutput
|
||
{
|
||
SuccessCount = results.Count(x => x.Success),
|
||
FailCount = results.Count(x => !x.Success),
|
||
Items = results,
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从已有运单批量创建 9610 汇总申报单:运单改为 9610Summary 并绑定出口资质备案,按运单申报明细生成待申报单。
|
||
/// </summary>
|
||
/// <param name="input">运单 Id 列表、出口资质备案 Id(空则用各客户默认备案)、电商平台编码。</param>
|
||
/// <returns>逐单结果;已删除、已有 9610 单、无申报明细、无可用备案的运单记为失败,不影响其它运单。</returns>
|
||
[HttpPost("/order/customs-9610/create-from-orders")]
|
||
public async Task<Customs9610CreateFromOrdersOutput> CreateFromOrdersAsync(
|
||
[FromBody] Customs9610CreateFromOrdersInput input)
|
||
{
|
||
var platformCode = input.PlatformCode?.Trim() ?? string.Empty;
|
||
if (string.IsNullOrWhiteSpace(platformCode))
|
||
{
|
||
throw new BusinessException("请选择电商平台");
|
||
}
|
||
|
||
if (platformCode.Length > 16)
|
||
{
|
||
throw new BusinessException("电商平台编码不能超过 16 个字符");
|
||
}
|
||
|
||
var orderIds = input.OrderIds.Where(x => x > 0).Distinct().ToList();
|
||
if (orderIds.Count == 0)
|
||
{
|
||
throw new BusinessException("请选择运单");
|
||
}
|
||
|
||
var sessionCustomerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
|
||
CustomerRepository,
|
||
CurrentUser,
|
||
null);
|
||
var orders = (await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id)))
|
||
.ToDictionary(x => x.Id);
|
||
var declarations = await OrderRepository.GetDeclarationsByOrderIdsAsync(orderIds);
|
||
var ordersWithItems = declarations.Select(x => x.OrderId).ToHashSet();
|
||
|
||
var results = new List<Customs9610CreateFromOrderResult>();
|
||
foreach (var orderId in orderIds)
|
||
{
|
||
var result = new Customs9610CreateFromOrderResult { OrderId = orderId };
|
||
results.Add(result);
|
||
try
|
||
{
|
||
if (!orders.TryGetValue(orderId, out var order)
|
||
|| (sessionCustomerId is > 0 && order.CustomerId != sessionCustomerId.Value))
|
||
{
|
||
throw new BusinessException("运单不存在或无权访问");
|
||
}
|
||
|
||
result.ServiceNo = order.ServiceNo;
|
||
if (string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
throw new BusinessException("运单已删除");
|
||
}
|
||
|
||
if (order.CustomerId is not > 0)
|
||
{
|
||
throw new BusinessException("运单未关联客户");
|
||
}
|
||
|
||
if (await HeaderRepository.GetByOrderIdAsync(order.Id) != null)
|
||
{
|
||
throw new BusinessException("该运单已存在 9610 申报单");
|
||
}
|
||
|
||
if (!ordersWithItems.Contains(order.Id))
|
||
{
|
||
throw new BusinessException("运单没有申报明细");
|
||
}
|
||
|
||
var qualification = input.ExportQualificationId is > 0
|
||
? await ExportQualificationRepository.GetOwnedAsync(
|
||
input.ExportQualificationId.Value,
|
||
order.CustomerId.Value)
|
||
: await ExportQualificationRepository.GetDefaultAsync(order.CustomerId.Value);
|
||
if (qualification == null)
|
||
{
|
||
throw new BusinessException(input.ExportQualificationId is > 0
|
||
? "所选出口资质备案不属于该运单客户"
|
||
: "运单客户未维护默认出口资质备案");
|
||
}
|
||
|
||
order.ExportCustomsMode = ExportCustomsModeCodes.Mode9610Summary;
|
||
order.ExportQualificationId = qualification.Id;
|
||
await Customs9610Writer.UpsertFromOrderAsync(order, platformCode);
|
||
order.UpdateTime = DateTime.Now;
|
||
await OrderRepository.UpdateAsync(order);
|
||
|
||
var header = await HeaderRepository.GetByOrderIdAsync(order.Id);
|
||
result.Customs9610OrderId = header?.Id;
|
||
result.Success = header != null;
|
||
result.Message = header == null ? "9610 申报单生成失败" : null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.Success = false;
|
||
result.Message = string.IsNullOrWhiteSpace(ex.Message) ? "创建失败" : ex.Message;
|
||
}
|
||
}
|
||
|
||
return new Customs9610CreateFromOrdersOutput
|
||
{
|
||
SuccessCount = results.Count(x => x.Success),
|
||
FailCount = results.Count(x => !x.Success),
|
||
Items = results,
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 批量获取清单数据(调 API 中心上游,标准码暂定),成功后回写本地字段。
|
||
/// </summary>
|
||
[HttpPost("/order/customs-9610/fetch-manifest-batch")]
|
||
public async Task<Customs9610DeclareBatchOutput> FetchManifestBatchAsync([FromBody] Customs9610IdsInput input)
|
||
{
|
||
var results = new List<Customs9610DeclareResult>();
|
||
foreach (var id in input.Ids.Where(x => x > 0).Distinct())
|
||
{
|
||
try
|
||
{
|
||
results.Add(await FetchManifestOneAsync(id));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
results.Add(new Customs9610DeclareResult
|
||
{
|
||
Id = id,
|
||
Success = false,
|
||
Message = string.IsNullOrWhiteSpace(ex.Message) ? "获取清单数据失败" : ex.Message,
|
||
});
|
||
}
|
||
}
|
||
|
||
return new Customs9610DeclareBatchOutput
|
||
{
|
||
SuccessCount = results.Count(x => x.Success),
|
||
FailCount = results.Count(x => !x.Success),
|
||
Items = results,
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 预报下游(create9610Order / BH;网关地址取自其他配置 api_center)。
|
||
/// </summary>
|
||
[HttpPost("/order/customs-9610/fetch-declaration-form-batch")]
|
||
public async Task<Customs9610DeclareBatchOutput> FetchDeclarationFormBatchAsync([FromBody] Customs9610IdsInput input)
|
||
{
|
||
var results = new List<Customs9610DeclareResult>();
|
||
foreach (var id in input.Ids.Where(x => x > 0).Distinct())
|
||
{
|
||
try
|
||
{
|
||
results.Add(await DownstreamForwardOneAsync(id));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
results.Add(new Customs9610DeclareResult
|
||
{
|
||
Id = id,
|
||
Success = false,
|
||
Message = string.IsNullOrWhiteSpace(ex.Message) ? "预报下游失败" : ex.Message,
|
||
});
|
||
}
|
||
}
|
||
|
||
return new Customs9610DeclareBatchOutput
|
||
{
|
||
SuccessCount = results.Count(x => x.Success),
|
||
FailCount = results.Count(x => !x.Success),
|
||
Items = results,
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 9610 预报下游标准码 / 供应商码(网关根地址由 api_center.BaseUrl 拼装)。
|
||
/// </summary>
|
||
private const string Create9610OrderStandardCode = "create9610Order";
|
||
private const string Create9610OrderSupplierCode = "BH";
|
||
|
||
private async Task<Customs9610DeclareResult> DownstreamForwardOneAsync(int id)
|
||
{
|
||
var header = await GetOwnedOrThrowAsync(id);
|
||
if (!string.Equals(header.Status, Customs9610StatusCodes.Pending, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
throw new BusinessException("仅待申报单据可预报下游");
|
||
}
|
||
|
||
var items = await ItemRepository.GetByHeaderIdAsync(header.Id);
|
||
if (items.Count == 0)
|
||
{
|
||
throw new BusinessException("申报商品不能为空");
|
||
}
|
||
|
||
ValidateHeaderForDeclare(header);
|
||
|
||
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == header.OrderId)
|
||
?? throw new BusinessException("关联运单不存在");
|
||
|
||
var creds = await CredentialResolver.ResolveAsync(order);
|
||
var body = BuildDeclareBody(header, items);
|
||
ItfxForwardResult forward;
|
||
try
|
||
{
|
||
// BaseUrl 来自其他配置 api_center(ItfxForwardClient 内部解析)
|
||
forward = await ItfxForwardClient.ForwardWithSubscribeAsync(
|
||
Create9610OrderStandardCode,
|
||
Create9610OrderSupplierCode,
|
||
creds.AppKey,
|
||
creds.AppSecret,
|
||
body);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
header.FailReason = Truncate(ex.Message, 512);
|
||
header.UpdateTime = DateTime.Now;
|
||
await HeaderRepository.UpdateAsync(header);
|
||
return new Customs9610DeclareResult
|
||
{
|
||
Id = header.Id,
|
||
Success = false,
|
||
Message = header.FailReason ?? "调用 API 中心失败",
|
||
Status = header.Status,
|
||
ServiceNo = header.ServiceNo,
|
||
ClientNo = header.ClientNo,
|
||
};
|
||
}
|
||
|
||
if (!forward.Success)
|
||
{
|
||
var reason = ItfxForwardResultEvaluator.BuildFailReason(
|
||
forward.StatusCode,
|
||
forward.ResponseBody,
|
||
null,
|
||
null);
|
||
header.FailReason = Truncate(reason, 512);
|
||
header.UpdateTime = DateTime.Now;
|
||
await HeaderRepository.UpdateAsync(header);
|
||
return new Customs9610DeclareResult
|
||
{
|
||
Id = header.Id,
|
||
Success = false,
|
||
Message = header.FailReason ?? "预报下游失败",
|
||
Status = header.Status,
|
||
ServiceNo = header.ServiceNo,
|
||
ClientNo = header.ClientNo,
|
||
HttpStatusCode = forward.StatusCode,
|
||
ResponseBody = forward.ResponseBody,
|
||
Url = forward.Url,
|
||
RequestJson = forward.RequestJson,
|
||
};
|
||
}
|
||
|
||
TryParseDeclareResponse(forward.ResponseBody, out var customsOrderNo, out var downstreamRef);
|
||
header.Status = Customs9610StatusCodes.Declared;
|
||
header.CustomsOrderNo = Truncate(customsOrderNo, 32) ?? header.CustomsOrderNo;
|
||
header.DownstreamRefNo = Truncate(downstreamRef, 32) ?? header.DownstreamRefNo;
|
||
header.DeclareTime = DateTime.Now;
|
||
header.FailReason = null;
|
||
header.UpdateTime = DateTime.Now;
|
||
await HeaderRepository.UpdateAsync(header);
|
||
|
||
return new Customs9610DeclareResult
|
||
{
|
||
Id = header.Id,
|
||
Success = true,
|
||
Message = "预报下游成功",
|
||
CustomsOrderNo = header.CustomsOrderNo,
|
||
Status = header.Status,
|
||
ServiceNo = header.ServiceNo,
|
||
ClientNo = header.ClientNo,
|
||
HttpStatusCode = forward.StatusCode,
|
||
ResponseBody = forward.ResponseBody,
|
||
Url = forward.Url,
|
||
RequestJson = forward.RequestJson,
|
||
};
|
||
}
|
||
|
||
private async Task<Customs9610DeclareResult> FetchManifestOneAsync(int id)
|
||
{
|
||
var forwardResult = await FetchDocumentOneAsync(
|
||
id,
|
||
settings => string.IsNullOrWhiteSpace(settings.Customs9610ManifestStandardCode)
|
||
? "customs9610Manifest"
|
||
: settings.Customs9610ManifestStandardCode.Trim(),
|
||
"获取清单数据");
|
||
|
||
if (!forwardResult.Success)
|
||
{
|
||
return forwardResult;
|
||
}
|
||
|
||
try
|
||
{
|
||
await ApplyManifestFromResponseAsync(id, forwardResult.ResponseBody);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new Customs9610DeclareResult
|
||
{
|
||
Id = id,
|
||
Success = false,
|
||
Message = Truncate(ex.Message, 512) ?? "清单数据解析/回写失败",
|
||
Status = forwardResult.Status,
|
||
CustomsOrderNo = forwardResult.CustomsOrderNo,
|
||
HttpStatusCode = forwardResult.HttpStatusCode,
|
||
ResponseBody = forwardResult.ResponseBody,
|
||
};
|
||
}
|
||
|
||
return forwardResult;
|
||
}
|
||
|
||
private async Task ApplyManifestFromResponseAsync(int headerId, string? responseBody)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(responseBody))
|
||
{
|
||
throw new BusinessException("清单响应为空");
|
||
}
|
||
|
||
using var doc = JsonDocument.Parse(responseBody);
|
||
var payload = ResolveManifestPayload(doc.RootElement);
|
||
if (payload.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null)
|
||
{
|
||
throw new BusinessException("清单响应缺少有效数据节点");
|
||
}
|
||
|
||
var header = await GetOwnedOrThrowAsync(headerId);
|
||
ApplyManifestHeader(header, payload);
|
||
header.ManifestSyncTime = DateTime.Now;
|
||
header.UpdateTime = DateTime.Now;
|
||
await HeaderRepository.UpdateAsync(header);
|
||
|
||
var itemArray = ResolveManifestItems(payload);
|
||
if (itemArray is null || itemArray.Value.ValueKind != JsonValueKind.Array)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var existing = await ItemRepository.GetByHeaderIdAsync(header.Id);
|
||
foreach (var el in itemArray.Value.EnumerateArray())
|
||
{
|
||
if (el.ValueKind != JsonValueKind.Object)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var sortNo = ReadInt(el, "gNo", "GNo", "gno") ?? 0;
|
||
var itemNo = Truncate(ReadString(el, "itemNo", "ItemNo", "sku", "Sku") ?? string.Empty, 32) ?? string.Empty;
|
||
var matched = existing.FirstOrDefault(x => sortNo > 0 && x.SortNo == sortNo)
|
||
?? existing.FirstOrDefault(x =>
|
||
!string.IsNullOrWhiteSpace(itemNo)
|
||
&& string.Equals(x.Sku, itemNo, StringComparison.OrdinalIgnoreCase));
|
||
|
||
if (matched is null)
|
||
{
|
||
matched = new Customs9610OrderItem
|
||
{
|
||
Customs9610OrderId = header.Id,
|
||
SortNo = sortNo,
|
||
Sku = itemNo,
|
||
CreateTime = DateTime.Now,
|
||
};
|
||
ApplyManifestItem(matched, el);
|
||
await ItemRepository.InsertAsync(matched);
|
||
existing.Add(matched);
|
||
}
|
||
else
|
||
{
|
||
ApplyManifestItem(matched, el);
|
||
matched.UpdateTime = DateTime.Now;
|
||
await ItemRepository.UpdateAsync(matched);
|
||
}
|
||
}
|
||
}
|
||
|
||
private static JsonElement ResolveManifestPayload(JsonElement root)
|
||
{
|
||
if (TryGetObject(root, out var data, "data", "Data"))
|
||
{
|
||
if (TryGetObject(data, out var nested, "cebItems", "CebItems"))
|
||
{
|
||
// 部分上游把头字段放 data,商品放 cebItems;头字段仍用 data
|
||
return data;
|
||
}
|
||
|
||
return data;
|
||
}
|
||
|
||
if (TryGetObject(root, out var ceb, "cebItems", "CebItems"))
|
||
{
|
||
return ceb;
|
||
}
|
||
|
||
return root;
|
||
}
|
||
|
||
private static JsonElement? ResolveManifestItems(JsonElement payload)
|
||
{
|
||
if (TryGetPropertyIgnoreCase(payload, out var items, "items", "Items")
|
||
&& items.ValueKind == JsonValueKind.Array)
|
||
{
|
||
return items;
|
||
}
|
||
|
||
if (TryGetObject(payload, out var ceb, "cebItems", "CebItems")
|
||
&& TryGetPropertyIgnoreCase(ceb, out var nested, "items", "Items")
|
||
&& nested.ValueKind == JsonValueKind.Array)
|
||
{
|
||
return nested;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static void ApplyManifestHeader(Customs9610Order header, JsonElement payload)
|
||
{
|
||
var customsOrderNo = Truncate(ReadString(payload, "customsOrderNo", "CustomsOrderNo"), 32);
|
||
if (!string.IsNullOrWhiteSpace(customsOrderNo))
|
||
{
|
||
header.CustomsOrderNo = customsOrderNo;
|
||
}
|
||
|
||
var logisticsNo = Truncate(ReadString(payload, "logisticsNo", "LogisticsNo", "serviceNo", "ServiceNo"), 48);
|
||
if (!string.IsNullOrWhiteSpace(logisticsNo))
|
||
{
|
||
header.ServiceNo = logisticsNo!;
|
||
}
|
||
|
||
var clientNo = Truncate(ReadString(payload, "referenceNo", "ReferenceNo", "clientNo", "ClientNo"), 48);
|
||
if (!string.IsNullOrWhiteSpace(clientNo))
|
||
{
|
||
header.ClientNo = clientNo!;
|
||
}
|
||
|
||
header.EnterpriseBizNo = Truncate(ReadString(payload, "copNo", "CopNo"), 48) ?? header.EnterpriseBizNo;
|
||
header.ManifestNo = Truncate(ReadString(payload, "invtNo", "InvtNo"), 48) ?? header.ManifestNo;
|
||
header.ClearanceTime = Truncate(ReadString(payload, "clearanceTime", "ClearanceTime"), 48) ?? header.ClearanceTime;
|
||
header.CustomsInboundTime =
|
||
Truncate(ReadString(payload, "warehouseTime", "WarehouseTime"), 48) ?? header.CustomsInboundTime;
|
||
header.CustomsState = Truncate(ReadString(payload, "customsStatus", "CustomsStatus"), 4) ?? header.CustomsState;
|
||
header.DeclareCustomsCode =
|
||
Truncate(ReadString(payload, "customsCode", "CustomsCode"), 4) ?? header.DeclareCustomsCode;
|
||
header.PlatformCorpCode = Truncate(ReadString(payload, "ebpCode", "EbpCode", "platformCorpCode", "PlatformCorpCode"), 18) ?? header.PlatformCorpCode;
|
||
header.PlatformName = Truncate(ReadString(payload, "ebpName", "EbpName", "platformName", "PlatformName"), 100) ?? header.PlatformName;
|
||
header.LogisticsCorpCode =
|
||
Truncate(ReadString(payload, "logisticsCode", "LogisticsCode"), 18) ?? header.LogisticsCorpCode;
|
||
header.LogisticsCorpName =
|
||
Truncate(ReadString(payload, "logisticsName", "LogisticsName"), 100) ?? header.LogisticsCorpName;
|
||
header.IeMark = Truncate(ReadString(payload, "ieFlag", "IeFlag"), 4) ?? header.IeMark;
|
||
header.ExportPortCode = Truncate(ReadString(payload, "portCode", "PortCode"), 4) ?? header.ExportPortCode;
|
||
header.ExportDate = Truncate(ReadString(payload, "ieDate", "IeDate"), 32) ?? header.ExportDate;
|
||
|
||
var businessType = Truncate(ReadString(payload, "statisticsFlag", "StatisticsFlag", "formType", "FormType", "businessType", "BusinessType"), 1);
|
||
if (businessType is "A" or "B" or "a" or "b")
|
||
{
|
||
header.BusinessType = businessType.ToUpperInvariant();
|
||
}
|
||
|
||
header.DeclareAgentCode = Truncate(ReadString(payload, "agentCode", "AgentCode"), 18) ?? header.DeclareAgentCode;
|
||
header.DeclareAgentName = Truncate(ReadString(payload, "agentName", "AgentName"), 100) ?? header.DeclareAgentName;
|
||
header.EcomCorpCode = Truncate(ReadString(payload, "ebcCode", "EbcCode", "ecomCorpCode", "EcomCorpCode"), 18) ?? header.EcomCorpCode;
|
||
header.EcomCorpName = Truncate(ReadString(payload, "ebcName", "EbcName", "ecomCorpName", "EcomCorpName"), 100) ?? header.EcomCorpName;
|
||
header.ProducerCode = Truncate(ReadString(payload, "ownerCode", "OwnerCode", "producerCode", "ProducerCode"), 18) ?? header.ProducerCode;
|
||
header.ProducerName = Truncate(ReadString(payload, "ownerName", "OwnerName", "producerName", "ProducerName"), 100) ?? header.ProducerName;
|
||
header.TradeModeCode = Truncate(ReadString(payload, "tradeMode", "TradeMode"), 4) ?? header.TradeModeCode;
|
||
header.TransportMode = Truncate(ReadString(payload, "trafMode", "TrafMode"), 4) ?? header.TransportMode;
|
||
|
||
var destCountry = Truncate(ReadString(payload, "countryCode", "CountryCode", "destCountry", "DestCountry"), 4);
|
||
if (!string.IsNullOrWhiteSpace(destCountry))
|
||
{
|
||
header.DestCountry = destCountry!;
|
||
}
|
||
|
||
var freight = ReadDecimal(payload, "freight", "Freight");
|
||
if (freight.HasValue)
|
||
{
|
||
header.Freight = freight;
|
||
}
|
||
|
||
header.FreightCurrency = Truncate(ReadString(payload, "fCurrency", "FCurrency"), 4) ?? header.FreightCurrency;
|
||
header.FreightMark = ReadInt(payload, "fFlag", "FFlag") ?? header.FreightMark;
|
||
header.PackageType = ReadInt(payload, "wrapType", "WrapType") ?? header.PackageType;
|
||
header.PackageQty = ReadInt(payload, "packNo", "PackNo") ?? header.PackageQty;
|
||
|
||
// 兼容:若平台短码仍空,用 platformCorpCode 回填 PlatformCode
|
||
if (string.IsNullOrWhiteSpace(header.PlatformCode) && !string.IsNullOrWhiteSpace(header.PlatformCorpCode))
|
||
{
|
||
header.PlatformCode = Truncate(header.PlatformCorpCode, 16)!;
|
||
}
|
||
}
|
||
|
||
private static void ApplyManifestItem(Customs9610OrderItem item, JsonElement el)
|
||
{
|
||
var sortNo = ReadInt(el, "gNo", "GNo", "gno");
|
||
if (sortNo.HasValue)
|
||
{
|
||
item.SortNo = sortNo.Value;
|
||
}
|
||
|
||
var itemNo = Truncate(ReadString(el, "itemNo", "ItemNo", "sku", "Sku"), 32);
|
||
if (!string.IsNullOrWhiteSpace(itemNo))
|
||
{
|
||
item.Sku = itemNo!;
|
||
}
|
||
|
||
var itemName = Truncate(ReadString(el, "itemName", "ItemName", "cnName", "CnName"), 250);
|
||
if (!string.IsNullOrWhiteSpace(itemName))
|
||
{
|
||
item.CnName = itemName!;
|
||
}
|
||
|
||
item.ExHsCode = Truncate(ReadString(el, "gcode", "Gcode", "gCode", "exHsCode", "ExHsCode"), 10) ?? item.ExHsCode;
|
||
item.DeclareElement = Truncate(ReadString(el, "gmodel", "Gmodel", "gModel", "declareElement", "DeclareElement"), 255) ?? item.DeclareElement;
|
||
item.Barcode = Truncate(ReadString(el, "barCode", "BarCode", "barcode"), 255) ?? item.Barcode;
|
||
item.DestCountry = Truncate(ReadString(el, "country", "Country", "destCountry", "DestCountry"), 255) ?? item.DestCountry;
|
||
item.ItemCurrency = Truncate(ReadString(el, "currency", "Currency"), 3) ?? item.ItemCurrency;
|
||
|
||
var qty = ReadDecimal(el, "qty", "Qty");
|
||
if (qty.HasValue)
|
||
{
|
||
item.Quantity = qty.Value;
|
||
}
|
||
|
||
item.LegalQty = ReadDecimal(el, "qty1", "Qty1") ?? item.LegalQty;
|
||
item.Unit = Truncate(ReadString(el, "unit", "Unit"), 3) ?? item.Unit;
|
||
item.LegalUnit = Truncate(ReadString(el, "unit1", "Unit1"), 3) ?? item.LegalUnit;
|
||
|
||
var price = ReadDecimal(el, "price", "Price");
|
||
if (price.HasValue)
|
||
{
|
||
item.UnitPrice = price.Value;
|
||
}
|
||
|
||
item.Amount = ReadDecimal(el, "totalPrice", "TotalPrice", "amount", "Amount") ?? item.Amount;
|
||
}
|
||
|
||
private static bool TryGetObject(JsonElement parent, out JsonElement child, params string[] names)
|
||
{
|
||
if (TryGetPropertyIgnoreCase(parent, out child, names) && child.ValueKind == JsonValueKind.Object)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
child = default;
|
||
return false;
|
||
}
|
||
|
||
private static bool TryGetPropertyIgnoreCase(JsonElement parent, out JsonElement child, params string[] names)
|
||
{
|
||
foreach (var name in names)
|
||
{
|
||
if (parent.TryGetProperty(name, out child))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
child = default;
|
||
return false;
|
||
}
|
||
|
||
private static int? ReadInt(JsonElement el, params string[] names)
|
||
{
|
||
foreach (var name in names)
|
||
{
|
||
if (!el.TryGetProperty(name, out var p))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (p.ValueKind == JsonValueKind.Number && p.TryGetInt32(out var n))
|
||
{
|
||
return n;
|
||
}
|
||
|
||
if (p.ValueKind == JsonValueKind.String
|
||
&& int.TryParse(p.GetString(), out var parsed))
|
||
{
|
||
return parsed;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static decimal? ReadDecimal(JsonElement el, params string[] names)
|
||
{
|
||
foreach (var name in names)
|
||
{
|
||
if (!el.TryGetProperty(name, out var p))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (p.ValueKind == JsonValueKind.Number && p.TryGetDecimal(out var d))
|
||
{
|
||
return d;
|
||
}
|
||
|
||
if (p.ValueKind == JsonValueKind.String
|
||
&& decimal.TryParse(p.GetString(), out var parsed))
|
||
{
|
||
return parsed;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private async Task<Customs9610DeclareBatchOutput> FetchDocumentBatchAsync(
|
||
Customs9610IdsInput input,
|
||
Func<ItfxGatewayOptions, string> resolveStandardCode,
|
||
string actionLabel)
|
||
{
|
||
var results = new List<Customs9610DeclareResult>();
|
||
foreach (var id in input.Ids.Where(x => x > 0).Distinct())
|
||
{
|
||
try
|
||
{
|
||
results.Add(await FetchDocumentOneAsync(id, resolveStandardCode, actionLabel));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
results.Add(new Customs9610DeclareResult
|
||
{
|
||
Id = id,
|
||
Success = false,
|
||
Message = string.IsNullOrWhiteSpace(ex.Message) ? $"{actionLabel}失败" : ex.Message,
|
||
});
|
||
}
|
||
}
|
||
|
||
return new Customs9610DeclareBatchOutput
|
||
{
|
||
SuccessCount = results.Count(x => x.Success),
|
||
FailCount = results.Count(x => !x.Success),
|
||
Items = results,
|
||
};
|
||
}
|
||
|
||
private async Task<Customs9610DeclareResult> FetchDocumentOneAsync(
|
||
int id,
|
||
Func<ItfxGatewayOptions, string> resolveStandardCode,
|
||
string actionLabel)
|
||
{
|
||
var header = await GetOwnedOrThrowAsync(id);
|
||
if (!string.Equals(header.Status, Customs9610StatusCodes.Declared, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
throw new BusinessException($"仅已申报单据可{actionLabel}");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(header.CustomsOrderNo)
|
||
&& string.IsNullOrWhiteSpace(header.ClientNo)
|
||
&& string.IsNullOrWhiteSpace(header.ServiceNo))
|
||
{
|
||
throw new BusinessException("缺少海关订单号/客户单号/运单号,无法获取");
|
||
}
|
||
|
||
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == header.OrderId)
|
||
?? throw new BusinessException("关联运单不存在");
|
||
|
||
var settings = ItfxOptions.Value;
|
||
var standardCode = resolveStandardCode(settings);
|
||
var creds = await CredentialResolver.ResolveAsync(order);
|
||
var supplierCode = !string.IsNullOrWhiteSpace(settings.Customs9610SupplierCode)
|
||
? settings.Customs9610SupplierCode.Trim()
|
||
: creds.SupplierCode;
|
||
|
||
var body = new
|
||
{
|
||
customsOrderNo = header.CustomsOrderNo,
|
||
clientNo = header.ClientNo,
|
||
serviceNo = header.ServiceNo,
|
||
businessType = header.BusinessType,
|
||
platformCode = header.PlatformCode,
|
||
};
|
||
|
||
ItfxForwardResult forward;
|
||
try
|
||
{
|
||
forward = await ItfxForwardClient.ForwardWithSubscribeAsync(
|
||
standardCode,
|
||
supplierCode,
|
||
creds.AppKey,
|
||
creds.AppSecret,
|
||
body);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new Customs9610DeclareResult
|
||
{
|
||
Id = header.Id,
|
||
Success = false,
|
||
Message = Truncate(ex.Message, 512) ?? $"调用 API 中心失败({actionLabel})",
|
||
Status = header.Status,
|
||
CustomsOrderNo = header.CustomsOrderNo,
|
||
};
|
||
}
|
||
|
||
if (!forward.Success)
|
||
{
|
||
var reason = ItfxForwardResultEvaluator.BuildFailReason(
|
||
forward.StatusCode,
|
||
forward.ResponseBody,
|
||
null,
|
||
null);
|
||
return new Customs9610DeclareResult
|
||
{
|
||
Id = header.Id,
|
||
Success = false,
|
||
Message = Truncate(reason, 512) ?? $"{actionLabel}失败",
|
||
Status = header.Status,
|
||
CustomsOrderNo = header.CustomsOrderNo,
|
||
HttpStatusCode = forward.StatusCode,
|
||
ResponseBody = forward.ResponseBody,
|
||
};
|
||
}
|
||
|
||
return new Customs9610DeclareResult
|
||
{
|
||
Id = header.Id,
|
||
Success = true,
|
||
Message = $"{actionLabel}成功",
|
||
Status = header.Status,
|
||
CustomsOrderNo = header.CustomsOrderNo,
|
||
HttpStatusCode = forward.StatusCode,
|
||
ResponseBody = forward.ResponseBody,
|
||
};
|
||
}
|
||
|
||
private async Task<Customs9610DeclareResult> DeclareOneAsync(int id)
|
||
{
|
||
var header = await GetOwnedOrThrowAsync(id);
|
||
if (!string.Equals(header.Status, Customs9610StatusCodes.Pending, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
throw new BusinessException("仅待申报单据可申报");
|
||
}
|
||
|
||
var items = await ItemRepository.GetByHeaderIdAsync(header.Id);
|
||
if (items.Count == 0)
|
||
{
|
||
throw new BusinessException("申报商品不能为空");
|
||
}
|
||
|
||
ValidateHeaderForDeclare(header);
|
||
|
||
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == header.OrderId)
|
||
?? throw new BusinessException("关联运单不存在");
|
||
|
||
var settings = ItfxOptions.Value;
|
||
var standardCode = string.IsNullOrWhiteSpace(settings.Customs9610StandardCode)
|
||
? "customs9610Declare"
|
||
: settings.Customs9610StandardCode.Trim();
|
||
|
||
var creds = await CredentialResolver.ResolveAsync(order);
|
||
var supplierCode = !string.IsNullOrWhiteSpace(settings.Customs9610SupplierCode)
|
||
? settings.Customs9610SupplierCode.Trim()
|
||
: creds.SupplierCode;
|
||
|
||
var body = BuildDeclareBody(header, items);
|
||
ItfxForwardResult forward;
|
||
try
|
||
{
|
||
forward = await ItfxForwardClient.ForwardWithSubscribeAsync(
|
||
standardCode,
|
||
supplierCode,
|
||
creds.AppKey,
|
||
creds.AppSecret,
|
||
body);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
header.FailReason = Truncate(ex.Message, 512);
|
||
header.UpdateTime = DateTime.Now;
|
||
await HeaderRepository.UpdateAsync(header);
|
||
return new Customs9610DeclareResult
|
||
{
|
||
Id = header.Id,
|
||
Success = false,
|
||
Message = header.FailReason ?? "调用 API 中心失败",
|
||
Status = header.Status,
|
||
};
|
||
}
|
||
|
||
if (!forward.Success)
|
||
{
|
||
var reason = ItfxForwardResultEvaluator.BuildFailReason(
|
||
forward.StatusCode,
|
||
forward.ResponseBody,
|
||
null,
|
||
null);
|
||
header.FailReason = Truncate(reason, 512);
|
||
header.UpdateTime = DateTime.Now;
|
||
await HeaderRepository.UpdateAsync(header);
|
||
return new Customs9610DeclareResult
|
||
{
|
||
Id = header.Id,
|
||
Success = false,
|
||
Message = header.FailReason ?? "申报失败",
|
||
Status = header.Status,
|
||
HttpStatusCode = forward.StatusCode,
|
||
ResponseBody = forward.ResponseBody,
|
||
};
|
||
}
|
||
|
||
TryParseDeclareResponse(forward.ResponseBody, out var customsOrderNo, out var downstreamRef);
|
||
header.Status = Customs9610StatusCodes.Declared;
|
||
header.CustomsOrderNo = Truncate(customsOrderNo, 32) ?? header.CustomsOrderNo;
|
||
header.DownstreamRefNo = Truncate(downstreamRef, 32) ?? header.DownstreamRefNo;
|
||
header.DeclareTime = DateTime.Now;
|
||
header.FailReason = null;
|
||
header.UpdateTime = DateTime.Now;
|
||
await HeaderRepository.UpdateAsync(header);
|
||
|
||
return new Customs9610DeclareResult
|
||
{
|
||
Id = header.Id,
|
||
Success = true,
|
||
Message = "申报成功",
|
||
CustomsOrderNo = header.CustomsOrderNo,
|
||
Status = header.Status,
|
||
HttpStatusCode = forward.StatusCode,
|
||
ResponseBody = forward.ResponseBody,
|
||
};
|
||
}
|
||
|
||
private static object BuildDeclareBody(Customs9610Order header, List<Customs9610OrderItem> items)
|
||
{
|
||
return new
|
||
{
|
||
platformCode = header.PlatformCode,
|
||
ecomCorpCode = header.EcomCorpCode,
|
||
ecomCorpName = header.EcomCorpName,
|
||
producerCode = header.ProducerCode,
|
||
producerName = header.ProducerName,
|
||
clientNo = header.ClientNo,
|
||
serviceNo = header.ServiceNo,
|
||
businessType = header.BusinessType,
|
||
region = header.Region,
|
||
agentDeclare = header.AgentDeclare,
|
||
actualSellerCode = header.ActualSellerCode,
|
||
preRecordFlag = header.PreRecordFlag,
|
||
isSummarySimple = header.IsSummarySimple == 1,
|
||
grossWeight = header.GrossWeight,
|
||
currency = header.Currency,
|
||
destCountry = header.DestCountry,
|
||
freight = header.Freight,
|
||
shipmentNo = header.ShipmentNo,
|
||
items = items
|
||
.OrderBy(x => x.SortNo)
|
||
.ThenBy(x => x.Id)
|
||
.Select(x => new
|
||
{
|
||
sku = x.Sku,
|
||
cnName = x.CnName,
|
||
exHsCode = x.ExHsCode,
|
||
declareElement = x.DeclareElement,
|
||
unitPrice = x.UnitPrice,
|
||
quantity = x.Quantity,
|
||
freight = x.Freight,
|
||
sortNo = x.SortNo,
|
||
})
|
||
.ToList()
|
||
};
|
||
}
|
||
|
||
private static void TryParseDeclareResponse(string? responseBody, out string? customsOrderNo, out string? referenceNo)
|
||
{
|
||
customsOrderNo = null;
|
||
referenceNo = null;
|
||
if (string.IsNullOrWhiteSpace(responseBody))
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
using var doc = JsonDocument.Parse(responseBody);
|
||
var root = doc.RootElement;
|
||
if (root.TryGetProperty("data", out var data) || root.TryGetProperty("Data", out data))
|
||
{
|
||
customsOrderNo = ReadString(data, "orderNo", "OrderNo", "customsOrderNo", "CustomsOrderNo");
|
||
// 下游单号:优先 serviceNo;兼容 referenceNo;BH create9610Order 常回 orderNo
|
||
referenceNo = ReadString(
|
||
data,
|
||
"serviceNo",
|
||
"ServiceNo",
|
||
"referenceNo",
|
||
"ReferenceNo",
|
||
"orderNo",
|
||
"OrderNo");
|
||
}
|
||
else
|
||
{
|
||
customsOrderNo = ReadString(root, "orderNo", "OrderNo", "customsOrderNo", "CustomsOrderNo");
|
||
referenceNo = ReadString(
|
||
root,
|
||
"serviceNo",
|
||
"ServiceNo",
|
||
"referenceNo",
|
||
"ReferenceNo",
|
||
"orderNo",
|
||
"OrderNo");
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore parse errors
|
||
}
|
||
}
|
||
|
||
private static string? ReadString(JsonElement el, params string[] names)
|
||
{
|
||
foreach (var name in names)
|
||
{
|
||
if (!el.TryGetProperty(name, out var p))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (p.ValueKind == JsonValueKind.String)
|
||
{
|
||
var s = p.GetString();
|
||
if (!string.IsNullOrWhiteSpace(s))
|
||
{
|
||
return s.Trim();
|
||
}
|
||
}
|
||
else if (p.ValueKind == JsonValueKind.Number)
|
||
{
|
||
return p.ToString();
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static void ValidateFormFields(Customs9610UpdateInput input)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(input.PlatformCode))
|
||
{
|
||
throw new BusinessException("电商平台 platformCode 不能为空");
|
||
}
|
||
|
||
var businessType = (input.BusinessType ?? string.Empty).Trim().ToUpperInvariant();
|
||
if (businessType is not (Customs9610FormTypeCodes.SimpleA or Customs9610FormTypeCodes.SummaryB))
|
||
{
|
||
throw new BusinessException("businessType 仅支持 A 或 B");
|
||
}
|
||
|
||
if (input.GrossWeight <= 0)
|
||
{
|
||
throw new BusinessException("毛重必须大于 0");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(input.Currency))
|
||
{
|
||
throw new BusinessException("币制不能为空");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(input.DestCountry))
|
||
{
|
||
throw new BusinessException("目的国不能为空");
|
||
}
|
||
|
||
if (businessType == Customs9610FormTypeCodes.SimpleA)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(input.Region))
|
||
{
|
||
throw new BusinessException("A 简化申报必须填写 region");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(input.AgentDeclare))
|
||
{
|
||
throw new BusinessException("A 简化申报必须填写 agentDeclare");
|
||
}
|
||
}
|
||
|
||
if (businessType == Customs9610FormTypeCodes.SummaryB
|
||
&& string.IsNullOrWhiteSpace(input.PreRecordFlag))
|
||
{
|
||
throw new BusinessException("B 汇总申报必须填写 preRecordFlag");
|
||
}
|
||
}
|
||
|
||
private static void ValidateHeaderForDeclare(Customs9610Order header)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(header.PlatformCode))
|
||
{
|
||
throw new BusinessException("请先填写电商平台 platformCode");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(header.ClientNo))
|
||
{
|
||
throw new BusinessException("客户单号不能为空");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(header.ServiceNo))
|
||
{
|
||
throw new BusinessException("运单号不能为空");
|
||
}
|
||
|
||
if (header.GrossWeight <= 0)
|
||
{
|
||
throw new BusinessException("毛重必须大于 0");
|
||
}
|
||
|
||
if (string.Equals(header.BusinessType, Customs9610FormTypeCodes.SummaryB, StringComparison.OrdinalIgnoreCase)
|
||
&& string.IsNullOrWhiteSpace(header.PreRecordFlag))
|
||
{
|
||
throw new BusinessException("B 汇总申报必须填写 preRecordFlag");
|
||
}
|
||
|
||
if (string.Equals(header.BusinessType, Customs9610FormTypeCodes.SimpleA, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
if (string.IsNullOrWhiteSpace(header.Region) || string.IsNullOrWhiteSpace(header.AgentDeclare))
|
||
{
|
||
throw new BusinessException("A 简化申报必须填写 region / agentDeclare");
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task<Customs9610Order> GetOwnedOrThrowAsync(int id)
|
||
{
|
||
var header = await HeaderRepository.FirstOrDefaultAsync(x => x.Id == id)
|
||
?? throw new BusinessException("9610 订单不存在");
|
||
|
||
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
|
||
CustomerRepository,
|
||
CurrentUser,
|
||
null);
|
||
if (customerId is > 0 && header.CustomerId != customerId.Value)
|
||
{
|
||
throw new BusinessException("无权访问该 9610 订单");
|
||
}
|
||
|
||
return header;
|
||
}
|
||
|
||
private async Task<Dictionary<int, string>> LoadCustomerNamesAsync(IEnumerable<int> customerIds)
|
||
{
|
||
var ids = customerIds.Where(x => x > 0).Distinct().ToList();
|
||
if (ids.Count == 0)
|
||
{
|
||
return new Dictionary<int, string>();
|
||
}
|
||
|
||
var customers = await CustomerRepository.GetListAsync(x => ids.Contains(x.Id));
|
||
return customers.ToDictionary(x => x.Id, x => x.CustomerName);
|
||
}
|
||
|
||
private static Customs9610OrderOutput MapOutput(
|
||
Customs9610Order header,
|
||
IReadOnlyDictionary<int, string> customerNames,
|
||
bool includeItems)
|
||
{
|
||
customerNames.TryGetValue(header.CustomerId, out var customerName);
|
||
return new Customs9610OrderOutput
|
||
{
|
||
Id = header.Id,
|
||
OrderId = header.OrderId,
|
||
ServiceNo = header.ServiceNo,
|
||
CustomerId = header.CustomerId,
|
||
CustomerName = customerName,
|
||
Status = header.Status,
|
||
PlatformCode = header.PlatformCode,
|
||
EcomCorpCode = header.EcomCorpCode,
|
||
EcomCorpName = header.EcomCorpName,
|
||
ProducerCode = header.ProducerCode,
|
||
ProducerName = header.ProducerName,
|
||
ClientNo = header.ClientNo,
|
||
BusinessType = header.BusinessType,
|
||
Region = header.Region,
|
||
AgentDeclare = header.AgentDeclare,
|
||
ActualSellerCode = header.ActualSellerCode,
|
||
PreRecordFlag = header.PreRecordFlag,
|
||
IsSummarySimple = header.IsSummarySimple == 1,
|
||
GrossWeight = header.GrossWeight,
|
||
Currency = header.Currency,
|
||
DestCountry = header.DestCountry,
|
||
Freight = header.Freight,
|
||
FreightCurrency = header.FreightCurrency,
|
||
FreightMark = header.FreightMark,
|
||
PackageType = header.PackageType,
|
||
PackageQty = header.PackageQty,
|
||
ShipmentNo = header.ShipmentNo,
|
||
CustomsOrderNo = header.CustomsOrderNo,
|
||
DownstreamRefNo = header.DownstreamRefNo,
|
||
EnterpriseBizNo = header.EnterpriseBizNo,
|
||
ManifestNo = header.ManifestNo,
|
||
ClearanceTime = header.ClearanceTime,
|
||
CustomsInboundTime = header.CustomsInboundTime,
|
||
CustomsState = header.CustomsState,
|
||
DeclareCustomsCode = header.DeclareCustomsCode,
|
||
PlatformCorpCode = header.PlatformCorpCode,
|
||
PlatformName = header.PlatformName,
|
||
LogisticsCorpCode = header.LogisticsCorpCode,
|
||
LogisticsCorpName = header.LogisticsCorpName,
|
||
IeMark = header.IeMark,
|
||
ExportPortCode = header.ExportPortCode,
|
||
ExportDate = header.ExportDate,
|
||
DeclareAgentCode = header.DeclareAgentCode,
|
||
DeclareAgentName = header.DeclareAgentName,
|
||
TradeModeCode = header.TradeModeCode,
|
||
TransportMode = header.TransportMode,
|
||
DeclareTime = header.DeclareTime,
|
||
ManifestSyncTime = header.ManifestSyncTime,
|
||
FailReason = header.FailReason,
|
||
CreateTime = header.CreateTime,
|
||
UpdateTime = header.UpdateTime == default ? null : header.UpdateTime,
|
||
Items = includeItems ? [] : null,
|
||
};
|
||
}
|
||
|
||
private static Customs9610ItemOutput MapItem(Customs9610OrderItem item) => new()
|
||
{
|
||
Id = item.Id,
|
||
OrderDeclarationId = item.OrderDeclarationId,
|
||
Sku = item.Sku,
|
||
CnName = item.CnName,
|
||
ExHsCode = item.ExHsCode,
|
||
DeclareElement = item.DeclareElement,
|
||
Barcode = item.Barcode,
|
||
DestCountry = item.DestCountry,
|
||
ItemCurrency = item.ItemCurrency,
|
||
UnitPrice = item.UnitPrice,
|
||
Amount = item.Amount,
|
||
Quantity = item.Quantity,
|
||
LegalQty = item.LegalQty,
|
||
Unit = item.Unit,
|
||
LegalUnit = item.LegalUnit,
|
||
Freight = item.Freight,
|
||
SortNo = item.SortNo,
|
||
};
|
||
|
||
private static string? Normalize(string? value)
|
||
{
|
||
var t = value?.Trim();
|
||
return string.IsNullOrEmpty(t) ? null : t;
|
||
}
|
||
|
||
private static string? Truncate(string? value, int max)
|
||
{
|
||
if (string.IsNullOrEmpty(value))
|
||
{
|
||
return value;
|
||
}
|
||
|
||
return value.Length <= max ? value : value[..max];
|
||
}
|
||
}
|