feat: 客户出口资质备案与申报要素,9610 预报直连下游
All checks were successful
Build and Deploy / deploy (push) Successful in 1m49s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-09-22 18:13:55 +08:00
parent e5c9364434
commit 8ce8b82f48
24 changed files with 834 additions and 9 deletions

28
db.sql
View File

@ -261,6 +261,30 @@ CREATE TABLE `t_customer_sender_address` (
KEY `idx_csa_customer_default` (`CustomerId`, `AddressType`, `IsDefault`)
) COMMENT='客户地址簿(发件/退件/收件)';
CREATE TABLE `t_customer_export_qualification` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`CustomerId` INT NOT NULL COMMENT '归属客户 t_customer.Id',
`CompanyName` VARCHAR(200) NOT NULL COMMENT '企业名称',
`SocialCreditCode` VARCHAR(64) NOT NULL COMMENT '企业社会信用代码',
`DomesticConsignor` VARCHAR(200) NOT NULL COMMENT '境内发货人',
`DomesticConsignorCustomsCode` VARCHAR(64) NULL COMMENT '境内发货人海关代码',
`ProducerName` VARCHAR(200) NOT NULL COMMENT '生产销售单位',
`ProducerCustomsCode` VARCHAR(64) NULL COMMENT '生产销售单位海关代码',
`CustomsPort` VARCHAR(100) NULL COMMENT '备案海关港口地',
`IsDefault` TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认:0-否,1-是(同客户仅一条)',
`Remark` VARCHAR(512) NULL COMMENT '备注',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`UpdateUserId` INT NULL COMMENT '更新人',
`UpdateTime` DATETIME NULL COMMENT '更新时间',
PRIMARY KEY (`Id`),
KEY `idx_ceq_customer` (`CustomerId`, `IsDeleted`, `Id`),
KEY `idx_ceq_credit` (`SocialCreditCode`, `IsDeleted`),
KEY `idx_ceq_customer_default` (`CustomerId`, `IsDefault`)
) COMMENT='客户出口资质备案';
CREATE TABLE `t_order_template` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1',
`Name` VARCHAR(64) NOT NULL COMMENT '模板名称',
@ -403,6 +427,7 @@ CREATE TABLE `t_order_declaration` (
`HsCode` VARCHAR(50) NULL COMMENT 'HsCode',
`ImHsCode` VARCHAR(50) NULL COMMENT '进口HsCode',
`ExHsCode` VARCHAR(50) NULL COMMENT '出口HsCode',
`DeclareElement` VARCHAR(255) NULL COMMENT '出口申报要素',
`SkuImage` VARCHAR(512) NULL COMMENT '图片(存 storage key 或短 URL)',
`Category` VARCHAR(20) NULL COMMENT '类别',
`Brand` VARCHAR(50) NULL COMMENT '品牌',
@ -1351,6 +1376,7 @@ CREATE TABLE `c_sku` (
`UnitPrice` DECIMAL(12,4) NULL COMMENT '单价',
`ExHsCode` VARCHAR(64) NULL COMMENT '出口HSCode',
`ImHsCode` VARCHAR(64) NULL COMMENT '进口HSCode',
`DeclareElement` VARCHAR(255) NULL COMMENT '出口申报要素',
`WeightG` DECIMAL(10,2) NULL COMMENT '重量(g)',
`LengthCm` DECIMAL(10,2) NULL COMMENT '长(cm)',
`WidthCm` DECIMAL(10,2) NULL COMMENT '宽(cm)',
@ -1998,7 +2024,7 @@ CREATE TABLE `t_9610_order_item` (
`Sku` VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'SKU(对齐 t_order_declaration.Sku)',
`CnName` VARCHAR(250) NOT NULL DEFAULT '' COMMENT '中文品名(对齐 t_order_declaration.CnName)',
`ExHsCode` VARCHAR(10) NULL COMMENT '出口海关编码(对齐 t_order_declaration.ExHsCode)',
`DeclareElement` VARCHAR(255) NULL COMMENT '申报要素',
`DeclareElement` VARCHAR(255) NULL COMMENT '出口申报要素',
`Barcode` VARCHAR(255) NULL COMMENT '条形码',
`DestCountry` VARCHAR(255) NULL COMMENT '目的国(地区)代码',
`ItemCurrency` VARCHAR(3) NULL COMMENT '币制',

View File

@ -0,0 +1,98 @@
-- 存量库:客户出口资质备案表(含 IsDefault / Remark)
-- 可重复执行。
-- 目标表:t_customer_export_qualification
SET @db := DATABASE();
-- 1) 表不存在则整表创建
SET @exists := (
SELECT COUNT(1)
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_customer_export_qualification'
);
SET @sql := IF(
@exists = 0,
'CREATE TABLE `t_customer_export_qualification` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT ''主键'',
`CustomerId` INT NOT NULL COMMENT ''归属客户 t_customer.Id'',
`CompanyName` VARCHAR(200) NOT NULL COMMENT ''企业名称'',
`SocialCreditCode` VARCHAR(64) NOT NULL COMMENT ''企业社会信用代码'',
`DomesticConsignor` VARCHAR(200) NOT NULL COMMENT ''境内发货人'',
`DomesticConsignorCustomsCode` VARCHAR(64) NULL COMMENT ''境内发货人海关代码'',
`ProducerName` VARCHAR(200) NOT NULL COMMENT ''生产销售单位'',
`ProducerCustomsCode` VARCHAR(64) NULL COMMENT ''生产销售单位海关代码'',
`CustomsPort` VARCHAR(100) NULL COMMENT ''备案海关港口地'',
`IsDefault` TINYINT NOT NULL DEFAULT 0 COMMENT ''是否默认:0-否,1-是(同客户仅一条)'',
`Remark` VARCHAR(512) NULL COMMENT ''备注'',
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT ''软删除:0-否,1-是'',
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT ''并发戳'',
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT ''创建人'',
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ''创建时间'',
`UpdateUserId` INT NULL COMMENT ''更新人'',
`UpdateTime` DATETIME NULL COMMENT ''更新时间'',
PRIMARY KEY (`Id`),
KEY `idx_ceq_customer` (`CustomerId`, `IsDeleted`, `Id`),
KEY `idx_ceq_credit` (`SocialCreditCode`, `IsDeleted`),
KEY `idx_ceq_customer_default` (`CustomerId`, `IsDefault`)
) COMMENT=''客户出口资质备案''',
'SELECT ''t_customer_export_qualification already exists'' AS info'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 2) 已建表但缺 IsDefault 时补列
SET @col := (
SELECT COUNT(1)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_customer_export_qualification'
AND COLUMN_NAME = 'IsDefault'
);
SET @sql := IF(
@col = 0,
'ALTER TABLE `t_customer_export_qualification`
ADD COLUMN `IsDefault` TINYINT NOT NULL DEFAULT 0 COMMENT ''是否默认:0-否,1-是(同客户仅一条)'' AFTER `CustomsPort`',
'SELECT ''IsDefault already exists'' AS info'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 3) 已建表但缺 Remark 时补列
SET @col := (
SELECT COUNT(1)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_customer_export_qualification'
AND COLUMN_NAME = 'Remark'
);
SET @sql := IF(
@col = 0,
'ALTER TABLE `t_customer_export_qualification`
ADD COLUMN `Remark` VARCHAR(512) NULL COMMENT ''备注'' AFTER `IsDefault`',
'SELECT ''Remark already exists'' AS info'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 4) 默认索引
SET @idx := (
SELECT COUNT(1)
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_customer_export_qualification'
AND INDEX_NAME = 'idx_ceq_customer_default'
);
SET @sql := IF(
@idx = 0,
'ALTER TABLE `t_customer_export_qualification` ADD KEY `idx_ceq_customer_default` (`CustomerId`, `IsDefault`)',
'SELECT ''idx_ceq_customer_default already exists'' AS info'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@ -0,0 +1,38 @@
-- 存量库:SKU / 订单申报增加「出口申报要素」DeclareElement
-- 可重复执行;仅在列不存在时 ADD。
SET @db := DATABASE();
-- c_sku.DeclareElement
SET @exists := (
SELECT COUNT(1)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 'c_sku'
AND COLUMN_NAME = 'DeclareElement'
);
SET @sql := IF(
@exists = 0,
'ALTER TABLE `c_sku` ADD COLUMN `DeclareElement` VARCHAR(255) NULL COMMENT ''出口申报要素'' AFTER `ImHsCode`',
'SELECT ''c_sku.DeclareElement already exists'' AS info'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- t_order_declaration.DeclareElement
SET @exists := (
SELECT COUNT(1)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_order_declaration'
AND COLUMN_NAME = 'DeclareElement'
);
SET @sql := IF(
@exists = 0,
'ALTER TABLE `t_order_declaration` ADD COLUMN `DeclareElement` VARCHAR(255) NULL COMMENT ''出口申报要素'' AFTER `ExHsCode`',
'SELECT ''t_order_declaration.DeclareElement already exists'' AS info'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@ -0,0 +1,109 @@
using System.ComponentModel.DataAnnotations;
namespace Oms.Application.Dtos.Customers;
/// <summary>
/// 出口资质备案分页查询。
/// </summary>
public class CustomerExportQualificationPaginatedInput
{
[Newtonsoft.Json.JsonProperty("page")]
public int Page { get; set; } = 1;
[Newtonsoft.Json.JsonProperty("pageSize")]
public int PageSize { get; set; } = 50;
/// <summary>
/// 客户 ID(管理端可选;不传则查全部)。
/// </summary>
[Newtonsoft.Json.JsonProperty("customerId")]
public int? CustomerId { get; set; }
[Newtonsoft.Json.JsonProperty("keyword")]
public string? Keyword { get; set; }
[Newtonsoft.Json.JsonProperty("onlyDefault")]
public bool OnlyDefault { get; set; }
}
/// <summary>
/// 新增出口资质备案。
/// </summary>
public class CreateCustomerExportQualificationInput
{
/// <summary>
/// 客户 ID(管理端代操作必传;客户门户可省略)。
/// </summary>
public int? CustomerId { get; set; }
[Required]
[StringLength(200)]
public string CompanyName { get; set; } = string.Empty;
[Required]
[StringLength(64)]
public string SocialCreditCode { get; set; } = string.Empty;
[Required]
[StringLength(200)]
public string DomesticConsignor { get; set; } = string.Empty;
[StringLength(64)]
public string? DomesticConsignorCustomsCode { get; set; }
[Required]
[StringLength(200)]
public string ProducerName { get; set; } = string.Empty;
[StringLength(64)]
public string? ProducerCustomsCode { get; set; }
[StringLength(100)]
public string? CustomsPort { get; set; }
public bool IsDefault { get; set; }
[StringLength(512)]
public string? Remark { get; set; }
}
/// <summary>
/// 更新出口资质备案。
/// </summary>
public class UpdateCustomerExportQualificationInput : CreateCustomerExportQualificationInput
{
[Required]
public int Id { get; set; }
}
/// <summary>
/// 出口资质备案出参。
/// </summary>
public class CustomerExportQualificationOutput
{
public int Id { get; set; }
public int CustomerId { get; set; }
public string CompanyName { get; set; } = string.Empty;
public string SocialCreditCode { get; set; } = string.Empty;
public string DomesticConsignor { get; set; } = string.Empty;
public string? DomesticConsignorCustomsCode { get; set; }
public string ProducerName { get; set; } = string.Empty;
public string? ProducerCustomsCode { get; set; }
public string? CustomsPort { get; set; }
public bool IsDefault { get; set; }
public string? Remark { get; set; }
public DateTime? CreateTime { get; set; }
public DateTime? UpdateTime { get; set; }
}

View File

@ -301,6 +301,14 @@ public class Customs9610DeclareResult
public int? HttpStatusCode { get; set; }
public string? ResponseBody { get; set; }
public string? Url { get; set; }
public string? RequestJson { get; set; }
public string? ServiceNo { get; set; }
public string? ClientNo { get; set; }
}
public class Customs9610DeclareBatchOutput

View File

@ -65,6 +65,11 @@ public class OrderItemOutput
/// </summary>
public string? ExHsCode { get; set; }
/// <summary>
/// 出口申报要素。
/// </summary>
public string? DeclareElement { get; set; }
/// <summary>
/// SKU 图片。
/// </summary>

View File

@ -60,6 +60,11 @@ public class OrderParcelItemInput
/// </summary>
public string? ExHsCode { get; set; }
/// <summary>
/// 出口申报要素。
/// </summary>
public string? DeclareElement { get; set; }
/// <summary>
/// SKU 图片。
/// </summary>

View File

@ -42,6 +42,11 @@ public class WarehouseSkuOutput
public string? ImHsCode { get; set; }
/// <summary>
/// 出口申报要素。
/// </summary>
public string? DeclareElement { get; set; }
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }
@ -81,6 +86,11 @@ public class WarehouseSkuSaveInput
public string? ImHsCode { get; set; }
/// <summary>
/// 出口申报要素。
/// </summary>
public string? DeclareElement { get; set; }
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }

View File

@ -32,4 +32,14 @@ public interface IItfxForwardClient
string appSecret,
object body,
CancellationToken cancellationToken = default);
/// <summary>
/// 向指定绝对 URL 发起 ForwardWithSubscribe(用于写死网关地址的场景)。
/// </summary>
Task<ItfxForwardResult> ForwardToAbsoluteUrlAsync(
string absoluteUrl,
string appKey,
string appSecret,
object body,
CancellationToken cancellationToken = default);
}

View File

@ -64,6 +64,23 @@ internal sealed class ItfxForwardClient(
var root = $"{settings.BaseUrl.Trim().TrimEnd('/')}/InterfaceForward/ForwardWithSubscribe";
var url =
$"{root}?standardCode={Uri.EscapeDataString(standardCode.Trim())}&supplierCode={Uri.EscapeDataString(supplierCode.Trim())}";
return await ForwardToAbsoluteUrlAsync(url, appKey, appSecret, body, cancellationToken);
}
public async Task<ItfxForwardResult> ForwardToAbsoluteUrlAsync(
string absoluteUrl,
string appKey,
string appSecret,
object body,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(absoluteUrl))
{
throw new InvalidOperationException("Itfx absoluteUrl is empty");
}
var settings = await configProvider.GetAsync(cancellationToken);
var url = absoluteUrl.Trim();
var requestJson = JsonSerializer.Serialize(body, JsonOptions);
using var request = new HttpRequestMessage(HttpMethod.Post, url);

View File

@ -0,0 +1,239 @@
using System.Linq.Expressions;
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Customers;
using Oms.Application.Dtos.Shared;
using Oms.Application.Shared;
using Oms.Domain.CustomersAggregate.Entities;
using Oms.Domain.CustomersAggregate.Repositories;
namespace Oms.Application.Services.Customers;
/// <summary>
/// 客户出口资质备案 API。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Customer")]
public class CustomerExportQualificationService : ApplicationService
{
private ICustomerRepository CustomerRepository =>
LazyServiceProvider.GetRequiredService<ICustomerRepository>();
private ICustomerExportQualificationRepository ExportQualificationRepository =>
LazyServiceProvider.GetRequiredService<ICustomerExportQualificationRepository>();
/// <summary>
/// 出口资质备案分页列表。
/// 管理端未传 customerId 时查询全部;客户门户仅查自身。
/// </summary>
[HttpPost("/customer/export-qualification/list")]
public async Task<OmsPageResult<CustomerExportQualificationOutput>> GetListAsync(
[FromBody] CustomerExportQualificationPaginatedInput input)
{
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var predicate = BuildPredicate(customerId, input);
var (entities, total) = await RepositoryPageHelper.GetPageAsync(
ExportQualificationRepository,
predicate,
pageIndex,
pageSize,
q => q.OrderByDescending(x => x.IsDefault).ThenByDescending(x => x.Id));
return new OmsPageResult<CustomerExportQualificationOutput>
{
Items = entities.Select(MapOutput).ToList(),
Total = total
};
}
/// <summary>
/// 出口资质备案详情。
/// </summary>
[HttpGet("/customer/export-qualification/detail")]
public async Task<CustomerExportQualificationOutput> GetDetailAsync(
[FromQuery] int id,
[FromQuery] int? customerId = null)
{
var entity = await GetOwnedOrThrowAsync(id, customerId);
return MapOutput(entity);
}
/// <summary>
/// 新增出口资质备案。
/// </summary>
[HttpPost("/customer/export-qualification/create")]
public async Task<object> CreateAsync([FromBody] CreateCustomerExportQualificationInput input)
{
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
ValidateInput(input);
if (input.IsDefault)
{
await ExportQualificationRepository.ClearDefaultAsync(customerId);
}
var entity = new CustomerExportQualification
{
CustomerId = customerId,
CompanyName = input.CompanyName.Trim(),
SocialCreditCode = input.SocialCreditCode.Trim(),
DomesticConsignor = input.DomesticConsignor.Trim(),
DomesticConsignorCustomsCode = TrimOrNull(input.DomesticConsignorCustomsCode),
ProducerName = input.ProducerName.Trim(),
ProducerCustomsCode = TrimOrNull(input.ProducerCustomsCode),
CustomsPort = TrimOrNull(input.CustomsPort),
IsDefault = input.IsDefault ? (byte)1 : (byte)0,
Remark = TrimOrNull(input.Remark)
};
entity = await ExportQualificationRepository.InsertAsync(entity);
return new { id = entity.Id };
}
/// <summary>
/// 更新出口资质备案。
/// </summary>
[HttpPost("/customer/export-qualification/update")]
public async Task UpdateAsync([FromBody] UpdateCustomerExportQualificationInput input)
{
var entity = await GetOwnedOrThrowAsync(input.Id, input.CustomerId);
ValidateInput(input);
if (input.IsDefault)
{
await ExportQualificationRepository.ClearDefaultAsync(entity.CustomerId, entity.Id);
}
entity.CompanyName = input.CompanyName.Trim();
entity.SocialCreditCode = input.SocialCreditCode.Trim();
entity.DomesticConsignor = input.DomesticConsignor.Trim();
entity.DomesticConsignorCustomsCode = TrimOrNull(input.DomesticConsignorCustomsCode);
entity.ProducerName = input.ProducerName.Trim();
entity.ProducerCustomsCode = TrimOrNull(input.ProducerCustomsCode);
entity.CustomsPort = TrimOrNull(input.CustomsPort);
entity.IsDefault = input.IsDefault ? (byte)1 : (byte)0;
entity.Remark = TrimOrNull(input.Remark);
await ExportQualificationRepository.UpdateAsync(entity);
}
/// <summary>
/// 删除出口资质备案(软删)。
/// </summary>
[HttpPost("/customer/export-qualification/delete")]
public async Task DeleteAsync([FromBody] IdInput input)
{
var entity = await GetOwnedOrThrowAsync(input.Id, null);
await ExportQualificationRepository.DeleteAsync(entity);
}
/// <summary>
/// 设为默认出口资质备案(同客户仅一条)。
/// </summary>
[HttpPost("/customer/export-qualification/set-default")]
public async Task SetDefaultAsync([FromBody] IdInput input)
{
var entity = await GetOwnedOrThrowAsync(input.Id, null);
await ExportQualificationRepository.ClearDefaultAsync(entity.CustomerId, entity.Id);
entity.IsDefault = 1;
await ExportQualificationRepository.UpdateAsync(entity);
}
private async Task<CustomerExportQualification> GetOwnedOrThrowAsync(int id, int? requestedCustomerId)
{
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
requestedCustomerId);
var (hasCustomerFilter, customerIdValue) = WarehouseCustomerHelper.ToCustomerFilter(customerId);
var entity = hasCustomerFilter
? await ExportQualificationRepository.GetOwnedAsync(id, customerIdValue)
: await ExportQualificationRepository.FirstOrDefaultAsync(x => x.Id == id);
if (entity == null)
{
throw new BusinessException("出口资质备案不存在");
}
return entity;
}
private static void ValidateInput(CreateCustomerExportQualificationInput input)
{
if (string.IsNullOrWhiteSpace(input.CompanyName))
{
throw new BusinessException("企业名称不能为空");
}
if (string.IsNullOrWhiteSpace(input.SocialCreditCode))
{
throw new BusinessException("企业社会信用代码不能为空");
}
if (string.IsNullOrWhiteSpace(input.DomesticConsignor))
{
throw new BusinessException("境内发货人不能为空");
}
if (string.IsNullOrWhiteSpace(input.ProducerName))
{
throw new BusinessException("生产销售单位不能为空");
}
}
private static Expression<Func<CustomerExportQualification, bool>> BuildPredicate(
int? customerId,
CustomerExportQualificationPaginatedInput input)
{
var (hasCustomerFilter, customerIdValue) = WarehouseCustomerHelper.ToCustomerFilter(customerId);
var keyword = input.Keyword?.Trim();
var hasKeyword = !string.IsNullOrWhiteSpace(keyword);
var onlyDefault = input.OnlyDefault;
return x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!onlyDefault || x.IsDefault == 1)
&& (!hasKeyword
|| x.CompanyName.Contains(keyword!)
|| x.SocialCreditCode.Contains(keyword!)
|| x.DomesticConsignor.Contains(keyword!)
|| (x.DomesticConsignorCustomsCode != null && x.DomesticConsignorCustomsCode.Contains(keyword!))
|| x.ProducerName.Contains(keyword!)
|| (x.ProducerCustomsCode != null && x.ProducerCustomsCode.Contains(keyword!))
|| (x.CustomsPort != null && x.CustomsPort.Contains(keyword!))
|| (x.Remark != null && x.Remark.Contains(keyword!)));
}
private static CustomerExportQualificationOutput MapOutput(CustomerExportQualification entity)
{
return new CustomerExportQualificationOutput
{
Id = entity.Id,
CustomerId = entity.CustomerId,
CompanyName = entity.CompanyName,
SocialCreditCode = entity.SocialCreditCode,
DomesticConsignor = entity.DomesticConsignor,
DomesticConsignorCustomsCode = entity.DomesticConsignorCustomsCode,
ProducerName = entity.ProducerName,
ProducerCustomsCode = entity.ProducerCustomsCode,
CustomsPort = entity.CustomsPort,
IsDefault = entity.IsDefault > 0,
Remark = entity.Remark,
CreateTime = entity.CreateTime,
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
};
}
private static string? TrimOrNull(string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
}

View File

@ -242,17 +242,138 @@ public class Customs9610OrderService : ApplicationService
}
/// <summary>
/// 批量获取报关单(调 API 中心上游,标准码暂定)。
/// 预报下游(写死 create9610Order / BH 网关地址)。
/// </summary>
[HttpPost("/order/customs-9610/fetch-declaration-form-batch")]
public async Task<Customs9610DeclareBatchOutput> FetchDeclarationFormBatchAsync([FromBody] Customs9610IdsInput input)
{
return await FetchDocumentBatchAsync(
input,
settings => string.IsNullOrWhiteSpace(settings.Customs9610DeclarationFormStandardCode)
? "customs9610DeclarationForm"
: settings.Customs9610DeclarationFormStandardCode.Trim(),
"获取报关单");
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 预报下游地址:create9610Order + supplierCode=BH。
/// </summary>
private const string Create9610OrderForwardUrl =
"https://api.ideship.com/itfx/InterfaceForward/ForwardWithSubscribe?standardCode=create9610Order&supplierCode=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
{
forward = await ItfxForwardClient.ForwardToAbsoluteUrlAsync(
Create9610OrderForwardUrl,
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,
Url = Create9610OrderForwardUrl,
};
}
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)

View File

@ -119,7 +119,9 @@ public class Customs9610OrderWriter : IScopedDependency
{
var itemName = FirstNonEmpty(d.CnName, d.EnName) ?? string.Empty;
var sku = FirstNonEmpty(d.Sku, $"ITEM{sort + 1}")!;
var declareElement = JoinNonEmpty(" ", d.Model, d.Specifications, d.Attributes, d.Material);
var declareElement = FirstNonEmpty(
d.DeclareElement,
JoinNonEmpty(" ", d.Model, d.Specifications, d.Attributes, d.Material));
await _itemRepository.InsertAsync(new Customs9610OrderItem
{
Customs9610OrderId = header.Id,

View File

@ -198,6 +198,7 @@ public static class AutoTriggerFormulaMeta
yield return ("HS编码", "t_order_declaration", "HsCode", FormulaValueType.String, false);
yield return ("进口HS编码", "t_order_declaration", "ImHsCode", FormulaValueType.String, false);
yield return ("出口HS编码", "t_order_declaration", "ExHsCode", FormulaValueType.String, false);
yield return ("出口申报要素", "t_order_declaration", "DeclareElement", FormulaValueType.String, false);
yield return ("申报类别", "t_order_declaration", "Category", FormulaValueType.String, false);
yield return ("品牌", "t_order_declaration", "Brand", FormulaValueType.String, false);
yield return ("品牌类型", "t_order_declaration", "BrandType", FormulaValueType.String, false);

View File

@ -759,6 +759,7 @@ public partial class OrderService
HsCode = x.ExHsCode ?? x.ImHsCode ?? x.HsCode,
ImHsCode = x.ImHsCode ?? x.HsCode,
ExHsCode = x.ExHsCode ?? x.HsCode,
DeclareElement = x.DeclareElement,
SkuImage = FileStorage.GetPublicUrl(x.SkuImage),
Category = x.Category,
Brand = x.Brand,

View File

@ -1128,6 +1128,7 @@ public partial class OrderService
HsCode = x.ExHsCode ?? x.ImHsCode,
ImHsCode = x.ImHsCode,
ExHsCode = x.ExHsCode ?? x.ImHsCode,
DeclareElement = x.DeclareElement,
SkuImage = FileStorage.ToStorageKey(x.SkuImage),
Category = x.Category,
Brand = x.Brand,

View File

@ -2012,6 +2012,7 @@ public partial class OrderService
HsCode = x.ExHsCode ?? x.ImHsCode,
ImHsCode = x.ImHsCode,
ExHsCode = x.ExHsCode,
DeclareElement = x.DeclareElement,
SkuImage = FileStorage.ToStorageKey(x.SkuImage),
Category = x.Category,
Brand = x.Brand,

View File

@ -121,6 +121,7 @@ public partial class WarehouseService
entity.UnitPrice = input.UnitPrice;
entity.ExHsCode = string.IsNullOrWhiteSpace(input.ExHsCode) ? null : input.ExHsCode.Trim();
entity.ImHsCode = string.IsNullOrWhiteSpace(input.ImHsCode) ? null : input.ImHsCode.Trim();
entity.DeclareElement = string.IsNullOrWhiteSpace(input.DeclareElement) ? null : input.DeclareElement.Trim();
entity.WeightG = input.WeightG;
entity.LengthCm = input.LengthCm;
entity.WidthCm = input.WidthCm;
@ -230,6 +231,7 @@ public partial class WarehouseService
UnitPrice = entity.UnitPrice,
ExHsCode = entity.ExHsCode,
ImHsCode = entity.ImHsCode,
DeclareElement = entity.DeclareElement,
WeightG = entity.WeightG,
LengthCm = entity.LengthCm,
WidthCm = entity.WidthCm,
@ -251,6 +253,7 @@ public partial class WarehouseService
UnitPrice = entity.UnitPrice,
ExHsCode = entity.ExHsCode,
ImHsCode = entity.ImHsCode,
DeclareElement = entity.DeclareElement,
WeightG = entity.WeightG,
LengthCm = entity.LengthCm,
WidthCm = entity.WidthCm,

View File

@ -199,6 +199,7 @@ public static class OrderDownstreamCreateOneBodyBuilder
Weight = x.Weight,
ImHsCode = x.ImHsCode,
ExHsCode = x.ExHsCode ?? x.HsCode,
DeclareElement = x.DeclareElement,
SkuImage = fileStorage?.GetPublicUrl(x.SkuImage) ?? x.SkuImage,
Category = x.Category,
Brand = x.Brand,

View File

@ -0,0 +1,58 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.CustomersAggregate.Entities;
/// <summary>
/// 客户出口资质备案。
/// </summary>
[SugarTable("t_customer_export_qualification")]
public class CustomerExportQualification : OmsAuditedAggregateRoot
{
public int CustomerId { get; set; }
/// <summary>
/// 企业名称。
/// </summary>
public string CompanyName { get; set; } = string.Empty;
/// <summary>
/// 企业社会信用代码。
/// </summary>
public string SocialCreditCode { get; set; } = string.Empty;
/// <summary>
/// 境内发货人。
/// </summary>
public string DomesticConsignor { get; set; } = string.Empty;
/// <summary>
/// 境内发货人海关代码。
/// </summary>
public string? DomesticConsignorCustomsCode { get; set; }
/// <summary>
/// 生产销售单位。
/// </summary>
public string ProducerName { get; set; } = string.Empty;
/// <summary>
/// 生产销售单位海关代码。
/// </summary>
public string? ProducerCustomsCode { get; set; }
/// <summary>
/// 备案海关港口地。
/// </summary>
public string? CustomsPort { get; set; }
/// <summary>
/// 是否默认:同客户仅一条为默认。
/// </summary>
public byte IsDefault { get; set; }
/// <summary>
/// 备注。
/// </summary>
public string? Remark { get; set; }
}

View File

@ -0,0 +1,17 @@
using Fake.Domain.Repositories;
using Oms.Domain.CustomersAggregate.Entities;
namespace Oms.Domain.CustomersAggregate.Repositories;
public interface ICustomerExportQualificationRepository : IRepository<CustomerExportQualification>
{
Task<CustomerExportQualification?> GetOwnedAsync(
int id,
int customerId,
CancellationToken cancellationToken = default);
Task ClearDefaultAsync(
int customerId,
int? excludeId = null,
CancellationToken cancellationToken = default);
}

View File

@ -32,6 +32,11 @@ public class OrderDeclaration : OmsAuditedEntity
public string? ExHsCode { get; set; }
/// <summary>
/// 出口申报要素。
/// </summary>
public string? DeclareElement { get; set; }
public string? SkuImage { get; set; }
public string? Category { get; set; }

View File

@ -22,6 +22,11 @@ public class CSku : OmsAuditedAggregateRoot
public string? ImHsCode { get; set; }
/// <summary>
/// 出口申报要素。
/// </summary>
public string? DeclareElement { get; set; }
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }

View File

@ -0,0 +1,44 @@
using Fake.DependencyInjection;
using Oms.Domain.CustomersAggregate.Entities;
using Oms.Domain.CustomersAggregate.Repositories;
using Oms.SqlSugarCore.Repositories;
namespace Oms.SqlSugarCore.Customers;
public class CustomerExportQualificationRepository
: OmsSqlSugarRepository<CustomerExportQualification>,
ICustomerExportQualificationRepository,
IScopedDependency
{
public Task<CustomerExportQualification?> GetOwnedAsync(
int id,
int customerId,
CancellationToken cancellationToken = default)
{
return FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId, cancellationToken);
}
public async Task ClearDefaultAsync(
int customerId,
int? excludeId = null,
CancellationToken cancellationToken = default)
{
var hasExcludeId = excludeId.HasValue;
var excludeIdValue = excludeId.GetValueOrDefault();
var list = await GetListAsync(
x => x.CustomerId == customerId
&& x.IsDefault == 1
&& (!hasExcludeId || x.Id != excludeIdValue),
cancellationToken: cancellationToken);
if (list.Count == 0)
{
return;
}
foreach (var item in list)
{
item.IsDefault = 0;
await UpdateAsync(item);
}
}
}