diff --git a/db.sql b/db.sql
index 2f18829..72e747b 100644
--- a/db.sql
+++ b/db.sql
@@ -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 '币制',
diff --git a/sql/migrate_customer_export_qualification.sql b/sql/migrate_customer_export_qualification.sql
new file mode 100644
index 0000000..efd88fe
--- /dev/null
+++ b/sql/migrate_customer_export_qualification.sql
@@ -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;
diff --git a/sql/migrate_declare_element.sql b/sql/migrate_declare_element.sql
new file mode 100644
index 0000000..62f59fd
--- /dev/null
+++ b/sql/migrate_declare_element.sql
@@ -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;
diff --git a/src/Oms.Application/Dtos/Customers/CustomerExportQualificationDtos.cs b/src/Oms.Application/Dtos/Customers/CustomerExportQualificationDtos.cs
new file mode 100644
index 0000000..d068309
--- /dev/null
+++ b/src/Oms.Application/Dtos/Customers/CustomerExportQualificationDtos.cs
@@ -0,0 +1,109 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Oms.Application.Dtos.Customers;
+
+///
+/// 出口资质备案分页查询。
+///
+public class CustomerExportQualificationPaginatedInput
+{
+ [Newtonsoft.Json.JsonProperty("page")]
+ public int Page { get; set; } = 1;
+
+ [Newtonsoft.Json.JsonProperty("pageSize")]
+ public int PageSize { get; set; } = 50;
+
+ ///
+ /// 客户 ID(管理端可选;不传则查全部)。
+ ///
+ [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; }
+}
+
+///
+/// 新增出口资质备案。
+///
+public class CreateCustomerExportQualificationInput
+{
+ ///
+ /// 客户 ID(管理端代操作必传;客户门户可省略)。
+ ///
+ 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; }
+}
+
+///
+/// 更新出口资质备案。
+///
+public class UpdateCustomerExportQualificationInput : CreateCustomerExportQualificationInput
+{
+ [Required]
+ public int Id { get; set; }
+}
+
+///
+/// 出口资质备案出参。
+///
+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; }
+}
diff --git a/src/Oms.Application/Dtos/Orders/Customs9610Dtos.cs b/src/Oms.Application/Dtos/Orders/Customs9610Dtos.cs
index c8bce36..5d7f66b 100644
--- a/src/Oms.Application/Dtos/Orders/Customs9610Dtos.cs
+++ b/src/Oms.Application/Dtos/Orders/Customs9610Dtos.cs
@@ -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
diff --git a/src/Oms.Application/Dtos/Orders/OrderItemOutput.cs b/src/Oms.Application/Dtos/Orders/OrderItemOutput.cs
index c7dba00..6797c7f 100644
--- a/src/Oms.Application/Dtos/Orders/OrderItemOutput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderItemOutput.cs
@@ -65,6 +65,11 @@ public class OrderItemOutput
///
public string? ExHsCode { get; set; }
+ ///
+ /// 出口申报要素。
+ ///
+ public string? DeclareElement { get; set; }
+
///
/// SKU 图片。
///
diff --git a/src/Oms.Application/Dtos/Orders/OrderParcelItemInput.cs b/src/Oms.Application/Dtos/Orders/OrderParcelItemInput.cs
index e108084..7f29d6f 100644
--- a/src/Oms.Application/Dtos/Orders/OrderParcelItemInput.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderParcelItemInput.cs
@@ -60,6 +60,11 @@ public class OrderParcelItemInput
///
public string? ExHsCode { get; set; }
+ ///
+ /// 出口申报要素。
+ ///
+ public string? DeclareElement { get; set; }
+
///
/// SKU 图片。
///
diff --git a/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs b/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs
index ebdd332..ed1c63d 100644
--- a/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs
+++ b/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs
@@ -42,6 +42,11 @@ public class WarehouseSkuOutput
public string? ImHsCode { get; set; }
+ ///
+ /// 出口申报要素。
+ ///
+ 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; }
+ ///
+ /// 出口申报要素。
+ ///
+ public string? DeclareElement { get; set; }
+
public decimal? WeightG { get; set; }
public decimal? LengthCm { get; set; }
diff --git a/src/Oms.Application/Integrations/Itfx/IItfxForwardClient.cs b/src/Oms.Application/Integrations/Itfx/IItfxForwardClient.cs
index cdeee86..c64c164 100644
--- a/src/Oms.Application/Integrations/Itfx/IItfxForwardClient.cs
+++ b/src/Oms.Application/Integrations/Itfx/IItfxForwardClient.cs
@@ -32,4 +32,14 @@ public interface IItfxForwardClient
string appSecret,
object body,
CancellationToken cancellationToken = default);
+
+ ///
+ /// 向指定绝对 URL 发起 ForwardWithSubscribe(用于写死网关地址的场景)。
+ ///
+ Task ForwardToAbsoluteUrlAsync(
+ string absoluteUrl,
+ string appKey,
+ string appSecret,
+ object body,
+ CancellationToken cancellationToken = default);
}
diff --git a/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs b/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs
index c61ad95..32f8943 100644
--- a/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs
+++ b/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs
@@ -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 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);
diff --git a/src/Oms.Application/Services/Customers/CustomerExportQualificationService.cs b/src/Oms.Application/Services/Customers/CustomerExportQualificationService.cs
new file mode 100644
index 0000000..4818a7b
--- /dev/null
+++ b/src/Oms.Application/Services/Customers/CustomerExportQualificationService.cs
@@ -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;
+
+///
+/// 客户出口资质备案 API。
+///
+[ApiExplorerSettings(GroupName = "OMS-Customer")]
+public class CustomerExportQualificationService : ApplicationService
+{
+ private ICustomerRepository CustomerRepository =>
+ LazyServiceProvider.GetRequiredService();
+
+ private ICustomerExportQualificationRepository ExportQualificationRepository =>
+ LazyServiceProvider.GetRequiredService();
+
+ ///
+ /// 出口资质备案分页列表。
+ /// 管理端未传 customerId 时查询全部;客户门户仅查自身。
+ ///
+ [HttpPost("/customer/export-qualification/list")]
+ public async Task> 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
+ {
+ Items = entities.Select(MapOutput).ToList(),
+ Total = total
+ };
+ }
+
+ ///
+ /// 出口资质备案详情。
+ ///
+ [HttpGet("/customer/export-qualification/detail")]
+ public async Task GetDetailAsync(
+ [FromQuery] int id,
+ [FromQuery] int? customerId = null)
+ {
+ var entity = await GetOwnedOrThrowAsync(id, customerId);
+ return MapOutput(entity);
+ }
+
+ ///
+ /// 新增出口资质备案。
+ ///
+ [HttpPost("/customer/export-qualification/create")]
+ public async Task