feat: 9610汇总下单必填电商平台写入预报,并增强官网产品目录筛选项
All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ddb8efba15
commit
6c8f7cb781
@ -6,10 +6,15 @@ namespace Oms.Application.Dtos.Channels;
|
||||
public class SalesProductPortalCatalogOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 分组列表(已排除国内揽收,且仅含有产品的分组)。
|
||||
/// 分组列表(字典 sales_product_group,已排除国内揽收,且仅含有产品的分组)。
|
||||
/// </summary>
|
||||
public List<SalesProductPortalGroupOutput> Groups { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 货型筛选项(字典 cargo_attribute)。
|
||||
/// </summary>
|
||||
public List<SalesProductPortalCargoOutput> Cargos { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 启用中的销售产品(已排除国内揽收)。
|
||||
/// </summary>
|
||||
@ -22,12 +27,28 @@ public class SalesProductPortalCatalogOutput
|
||||
public class SalesProductPortalGroupOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 分组编码(字典 code)。
|
||||
/// 分组编码(字典 sales_product_group.code)。
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 分组名称。
|
||||
/// 分组名称(字典 sales_product_group.name)。
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 官网货型筛选项。
|
||||
/// </summary>
|
||||
public class SalesProductPortalCargoOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 货型编码(字典 cargo_attribute.code)。
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 货型名称(字典 cargo_attribute.name)。
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
@ -81,4 +102,20 @@ public class SalesProductPortalItemOutput
|
||||
/// 产品说明。
|
||||
/// </summary>
|
||||
public string? ProductDescription { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 官网目的地区域编码:us / eu / sea / ca_mx / au / global。
|
||||
/// 优先由价格分区国家推导;国际快递跨区时含 global。
|
||||
/// </summary>
|
||||
public List<string> DestRegions { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 覆盖国家二字码(来自价格分区明细,去重后截断)。
|
||||
/// </summary>
|
||||
public List<string> CoverageCountries { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 货型标签(字典 cargo_attribute.code,如 general / electronics / sensitive)。
|
||||
/// </summary>
|
||||
public List<string> CargoTags { get; set; } = [];
|
||||
}
|
||||
|
||||
@ -50,6 +50,11 @@ public class OrderParcelCreateInput
|
||||
/// </summary>
|
||||
public int? ExportQualificationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 电商平台(ExportCustomsMode=9610Summary 时必填,写入 t_9610_order.PlatformCode)。
|
||||
/// </summary>
|
||||
public string? PlatformCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 仓库主键。
|
||||
/// </summary>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
@ -43,7 +42,6 @@ public interface IPickupBigBagTrackSyncer
|
||||
|
||||
internal sealed class PickupBigBagTrackSyncer(
|
||||
ISmallBagTrackOrderClient trackClient,
|
||||
ICustomerRepository customerRepository,
|
||||
IPickupOrderLineRepository pickupOrderLineRepository,
|
||||
ISalesProductRepository salesProductRepository,
|
||||
IServiceChannelRepository serviceChannelRepository,
|
||||
@ -85,16 +83,14 @@ internal sealed class PickupBigBagTrackSyncer(
|
||||
|
||||
try
|
||||
{
|
||||
var customer = await customerRepository.FirstOrDefaultAsync(x => x.Id == pickup.CustomerId, cancellationToken);
|
||||
var clientCode = customer?.CustomerCode?.Trim() ?? string.Empty;
|
||||
var (channel, product) = await ResolveChannelProductAsync(pickup.CarrierService, cancellationToken);
|
||||
var country = ResolveCountry(pickup);
|
||||
|
||||
// 1) UpsertBigBag
|
||||
// 1) UpsertBigBag:client 取 product(销售产品编码)
|
||||
var upsertResult = await trackClient.UpsertBigBagAsync(
|
||||
new SmallBagTrackUpsertBigBagRequest
|
||||
{
|
||||
Client = clientCode,
|
||||
Client = product ?? string.Empty,
|
||||
BagNo = bagNo,
|
||||
OtherNo = pickup.CarrierWaybillNo,
|
||||
Channel = channel,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Channels;
|
||||
using Oms.Domain.ChannelsAggregate.Entities;
|
||||
using Oms.Domain.Shared.Enums;
|
||||
using Oms.Domain.SystemAggregate.Repositories;
|
||||
|
||||
@ -9,8 +10,22 @@ namespace Oms.Application.Services.Channels;
|
||||
public partial class SalesProductService
|
||||
{
|
||||
private const string SalesProductGroupDictType = "sales_product_group";
|
||||
private const string CargoAttributeDictType = "cargo_attribute";
|
||||
private const string UngroupedProductGroupCode = "_ungrouped";
|
||||
private const string DomesticPickupGroupNameKeyword = "国内揽收";
|
||||
private const string ExpressProductGroupCode = "express";
|
||||
private const int PortalCoverageCountryLimit = 40;
|
||||
|
||||
private static readonly HashSet<string> PortalEuCountryCodes = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT",
|
||||
"LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "GB", "UK"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> PortalSeaCountryCodes = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"MY", "SG", "PH", "TH", "VN", "ID"
|
||||
};
|
||||
|
||||
private IDictRepository DictRepository =>
|
||||
LazyServiceProvider.GetRequiredService<IDictRepository>();
|
||||
@ -24,6 +39,7 @@ public partial class SalesProductService
|
||||
{
|
||||
var products = await SalesProductRepository.GetListAsync(x => x.IsActive == EnableStatus.Enabled);
|
||||
var dictRows = await DictRepository.GetListAsync(x => x.DictType == SalesProductGroupDictType);
|
||||
var cargoDictRows = await DictRepository.GetListAsync(x => x.DictType == CargoAttributeDictType);
|
||||
|
||||
var domesticPickupGroupCodes = dictRows
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.DictName)
|
||||
@ -59,6 +75,26 @@ public partial class SalesProductService
|
||||
.ThenBy(p => p.Id)
|
||||
.ToList();
|
||||
|
||||
var areaIds = filtered
|
||||
.Select(p => p.PriceAreaId)
|
||||
.Where(id => id is > 0)
|
||||
.Select(id => id!.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var countriesByAreaId = new Dictionary<int, List<string>>();
|
||||
foreach (var areaId in areaIds)
|
||||
{
|
||||
var infos = await KpiAreaRepository.GetAllInfosByAreaIdAsync(areaId);
|
||||
var codes = infos
|
||||
.Select(x => (x.CountryCode ?? string.Empty).Trim().ToUpperInvariant())
|
||||
.Where(code => code.Length == 2)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(code => code, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
countriesByAreaId[areaId] = codes;
|
||||
}
|
||||
|
||||
var usedGroupCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var items = filtered.Select(p =>
|
||||
{
|
||||
@ -67,6 +103,11 @@ public partial class SalesProductService
|
||||
: p.ProductGroup.Trim();
|
||||
usedGroupCodes.Add(groupCode);
|
||||
groupNameMap.TryGetValue(groupCode, out var groupName);
|
||||
|
||||
var coverageCountries = ResolvePortalCoverageCountries(p, countriesByAreaId);
|
||||
var destRegions = ResolvePortalDestRegions(p, groupCode, coverageCountries);
|
||||
var cargoTags = ResolvePortalCargoTags(p);
|
||||
|
||||
return new SalesProductPortalItemOutput
|
||||
{
|
||||
Id = p.Id,
|
||||
@ -77,7 +118,10 @@ public partial class SalesProductService
|
||||
MaxTimeliness = p.MaxTimeliness,
|
||||
TradeTerms = string.IsNullOrWhiteSpace(p.TradeTerms) ? null : p.TradeTerms.Trim().ToUpperInvariant(),
|
||||
RouteSort = p.RouteSort,
|
||||
ProductDescription = p.ProductDescription
|
||||
ProductDescription = p.ProductDescription,
|
||||
DestRegions = destRegions,
|
||||
CoverageCountries = coverageCountries.Take(PortalCoverageCountryLimit).ToList(),
|
||||
CargoTags = cargoTags
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
@ -121,10 +165,156 @@ public partial class SalesProductService
|
||||
});
|
||||
}
|
||||
|
||||
var cargos = cargoDictRows
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.DictCode))
|
||||
.GroupBy(x => x.DictCode!.Trim(), StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => g.OrderBy(x => x.DictSort).ThenBy(x => x.DictCode).First())
|
||||
.OrderBy(x => x.DictSort)
|
||||
.ThenBy(x => x.DictCode)
|
||||
.Select(x => new SalesProductPortalCargoOutput
|
||||
{
|
||||
Code = x.DictCode!.Trim(),
|
||||
Name = string.IsNullOrWhiteSpace(x.DictName) ? x.DictCode!.Trim() : x.DictName.Trim()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new SalesProductPortalCatalogOutput
|
||||
{
|
||||
Groups = groups,
|
||||
Cargos = cargos,
|
||||
Items = items
|
||||
};
|
||||
}
|
||||
|
||||
private static List<string> ResolvePortalCoverageCountries(
|
||||
SalesProduct product,
|
||||
IReadOnlyDictionary<int, List<string>> countriesByAreaId)
|
||||
{
|
||||
if (product.PriceAreaId is not > 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return countriesByAreaId.TryGetValue(product.PriceAreaId.Value, out var codes)
|
||||
? codes
|
||||
: [];
|
||||
}
|
||||
|
||||
private static List<string> ResolvePortalDestRegions(
|
||||
SalesProduct product,
|
||||
string groupCode,
|
||||
IReadOnlyCollection<string> coverageCountries)
|
||||
{
|
||||
var regions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var code in coverageCountries)
|
||||
{
|
||||
if (string.Equals(code, "US", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
regions.Add("us");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(code, "CA", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(code, "MX", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
regions.Add("ca_mx");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(code, "AU", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(code, "NZ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
regions.Add("au");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (PortalSeaCountryCodes.Contains(code))
|
||||
{
|
||||
regions.Add("sea");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (PortalEuCountryCodes.Contains(code))
|
||||
{
|
||||
regions.Add("eu");
|
||||
}
|
||||
}
|
||||
|
||||
// 国际快递:价格分区跨多区,或分区为空时仍作为全球快递备选
|
||||
var isExpress = string.Equals(groupCode, ExpressProductGroupCode, StringComparison.OrdinalIgnoreCase)
|
||||
|| ContainsAnyIgnoreCase(product.ProductName, "DHL", "UPS", "FedEx", "FEDEX", "3PE");
|
||||
if (isExpress && (regions.Count >= 2 || coverageCountries.Count == 0 || coverageCountries.Count >= 12))
|
||||
{
|
||||
regions.Add("global");
|
||||
}
|
||||
|
||||
// 无价格分区时,用产品名做轻量兜底(与官网前端关键词对齐)
|
||||
if (regions.Count == 0)
|
||||
{
|
||||
var blob = $"{product.ProductName} {product.ProductDescription}";
|
||||
if (ContainsAnyIgnoreCase(blob, "美国", "USA", "United States", "美东", "美西")) regions.Add("us");
|
||||
if (ContainsAnyIgnoreCase(blob, "东南", "越南", "马来", "新加坡", "泰国", "菲律宾", "印尼")) regions.Add("sea");
|
||||
if (ContainsAnyIgnoreCase(blob, "欧盟", "欧洲", "英国", "德国", "法国")) regions.Add("eu");
|
||||
if (ContainsAnyIgnoreCase(blob, "加拿大", "墨西哥")) regions.Add("ca_mx");
|
||||
if (ContainsAnyIgnoreCase(blob, "澳洲", "澳大利亚", "新西兰")) regions.Add("au");
|
||||
if (isExpress) regions.Add("global");
|
||||
}
|
||||
|
||||
return regions
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将产品说明映射到字典 cargo_attribute 编码(general / electronics / sensitive)。
|
||||
/// 销售产品本身暂无货型字段,官网先按说明关键词归类,便于筛选项与字典一致。
|
||||
/// </summary>
|
||||
private static List<string> ResolvePortalCargoTags(SalesProduct product)
|
||||
{
|
||||
var blob = $"{product.ProductName} {product.ProductDescription}";
|
||||
var tags = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (ContainsAnyIgnoreCase(blob, "带电", "电池", "锂电", "3C", "lithium", "battery", "electronics"))
|
||||
{
|
||||
tags.Add("electronics");
|
||||
}
|
||||
|
||||
if (ContainsAnyIgnoreCase(
|
||||
blob,
|
||||
"化妆品",
|
||||
"cosmetic",
|
||||
"敏感",
|
||||
"特殊",
|
||||
"疑难",
|
||||
"超长",
|
||||
"超重",
|
||||
"超大",
|
||||
"oversize",
|
||||
"overweight",
|
||||
"special",
|
||||
"sensitive"))
|
||||
{
|
||||
tags.Add("sensitive");
|
||||
}
|
||||
|
||||
if (ContainsAnyIgnoreCase(blob, "普货", "general cargo", "general"))
|
||||
{
|
||||
tags.Add("general");
|
||||
}
|
||||
|
||||
return tags
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static bool ContainsAnyIgnoreCase(string? text, params string[] needles)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) || needles.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return needles.Any(n => text.Contains(n, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,10 @@ public class Customs9610OrderWriter : IScopedDependency
|
||||
_exportQualificationRepository = exportQualificationRepository;
|
||||
}
|
||||
|
||||
public async Task UpsertFromOrderAsync(Order order, CancellationToken cancellationToken = default)
|
||||
public async Task UpsertFromOrderAsync(
|
||||
Order order,
|
||||
string? platformCode = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (order.Id <= 0)
|
||||
{
|
||||
@ -106,6 +109,13 @@ public class Customs9610OrderWriter : IScopedDependency
|
||||
|
||||
ApplyQualification(header, qualification);
|
||||
|
||||
// 下单传入的电商平台优先;刷新时若未传则保留已有值
|
||||
var normalizedPlatform = Truncate(FirstNonEmpty(platformCode, header.PlatformCode), 16);
|
||||
if (!string.IsNullOrWhiteSpace(normalizedPlatform))
|
||||
{
|
||||
header.PlatformCode = normalizedPlatform;
|
||||
}
|
||||
|
||||
header.GrossWeight = grossWeight;
|
||||
header.Currency = FirstNonEmpty(order.DeclaredCurrency, header.Currency, "CNY")!;
|
||||
header.DestCountry = FirstNonEmpty(order.ConsigneeCountry, header.DestCountry) ?? string.Empty;
|
||||
|
||||
@ -142,7 +142,7 @@ public partial class OrderService
|
||||
// 客户主动「先保存为草稿」:落库草稿后直接返回,不跑预报计费。
|
||||
if (input.SaveAsDraft)
|
||||
{
|
||||
await TryUpsertCustoms9610Async(order);
|
||||
await TryUpsertCustoms9610Async(order, input.PlatformCode);
|
||||
return await BuildParcelCreateOutputAsync(order);
|
||||
}
|
||||
|
||||
@ -158,7 +158,7 @@ public partial class OrderService
|
||||
await ApplyChargeableWeightAsync(product, snapshotBinding.Rules, forecast);
|
||||
SyncWeightRows(weights, forecast);
|
||||
await OrderRepository.ReplaceWeightsAsync(order.Id, weights);
|
||||
await TryUpsertCustoms9610Async(order);
|
||||
await TryUpsertCustoms9610Async(order, input.PlatformCode);
|
||||
|
||||
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId);
|
||||
var priceTierKey = await ResolvePriceTierKeyAsync(order.CustomerId);
|
||||
@ -261,12 +261,12 @@ public partial class OrderService
|
||||
await OrderRepository.DeleteAsync(order);
|
||||
}
|
||||
|
||||
private async Task TryUpsertCustoms9610Async(Order order)
|
||||
private async Task TryUpsertCustoms9610Async(Order order, string? platformCode = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await LazyServiceProvider.GetRequiredService<Customs9610OrderWriter>()
|
||||
.UpsertFromOrderAsync(order);
|
||||
.UpsertFromOrderAsync(order, platformCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -438,10 +438,22 @@ public partial class OrderService
|
||||
|
||||
input.ExportQualificationId = qualification.Id;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.PlatformCode))
|
||||
{
|
||||
throw new BusinessException("9610汇总申报必须填写电商平台");
|
||||
}
|
||||
|
||||
input.PlatformCode = input.PlatformCode.Trim();
|
||||
if (input.PlatformCode.Length > 16)
|
||||
{
|
||||
throw new BusinessException("电商平台长度不能超过 16");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
input.ExportQualificationId = null;
|
||||
input.PlatformCode = null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.DeclaredCurrency))
|
||||
|
||||
@ -113,8 +113,9 @@ public static class OrderTemplateFieldRegistry
|
||||
Add("length", "长(cm)", true, "number", example: "10", hint: "必填,用于材积计费");
|
||||
Add("width", "宽(cm)", true, "number", example: "10", hint: "必填,用于材积计费");
|
||||
Add("height", "高(cm)", true, "number", example: "10", hint: "必填,用于材积计费");
|
||||
Add("exportCustomsMode", "出口报关", false, "select", "export_customs_mode", "basic", "9610Simple", hint: "不选则按简易申报;填 9610Summary 时需有默认出口资质或填备案Id");
|
||||
Add("exportCustomsMode", "出口报关", false, "select", "export_customs_mode", "basic", "9610Simple", hint: "不选则按简易申报;填 9610Summary 时需有默认出口资质或填备案Id,并填电商平台");
|
||||
Add("exportQualificationId", "出口资质备案Id", false, "number", example: "1", hint: "9610汇总申报时填写;留空则用客户默认备案");
|
||||
Add("platformCode", "电商平台", false, example: "Amazon", hint: "9610汇总申报时必填,写入海关申报电商平台");
|
||||
|
||||
Add("receiver.recipientsName", "收件人姓名", true, group: "receiver", example: "John Smith", hint: "必填");
|
||||
Add("receiver.phone", "收件人手机", true, group: "receiver", example: "13900139000", hint: "必填");
|
||||
@ -168,8 +169,9 @@ public static class OrderTemplateFieldRegistry
|
||||
Add("clientNo", "客户单号", true, example: "C202604230001", hint: "您自己的订单编号,同一客户不可重复");
|
||||
Add("productCode", "运输方式", true, "select", "sales_product", "basic", "PROD001", hint: "从下拉选择产品编码");
|
||||
Add("cargoAttribute", "货物属性", true, "select", "cargo_attribute", "basic", "general", hint: "普货选 general");
|
||||
Add("exportCustomsMode", "出口报关方式", false, "select", "export_customs_mode", "basic", "9610Simple", hint: "不选则简易申报;填 9610Summary 时需有默认出口资质或填备案Id");
|
||||
Add("exportCustomsMode", "出口报关方式", false, "select", "export_customs_mode", "basic", "9610Simple", hint: "不选则简易申报;填 9610Summary 时需有默认出口资质或填备案Id,并填电商平台");
|
||||
Add("exportQualificationId", "出口资质备案Id", false, "number", example: "1", hint: "9610汇总申报时填写;留空则用客户默认备案");
|
||||
Add("platformCode", "电商平台", false, example: "Amazon", hint: "9610汇总申报时必填,写入海关申报电商平台");
|
||||
Add("warehouseId", "发货仓库", true, "select", "warehouse", "basic", "1", hint: "从下拉选择仓库 ID");
|
||||
Add("length", "长(cm)", true, "number", example: "10", hint: "必填,材积计费用");
|
||||
Add("width", "宽(cm)", true, "number", example: "10", hint: "必填,材积计费用");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user