Compare commits
No commits in common. "ae78834d7c0763f280b9b88eb6a2db3be3d566c9" and "f8cabe19096ae61caca094e99d29c13fafde165e" have entirely different histories.
ae78834d7c
...
f8cabe1909
3
db.sql
3
db.sql
@ -1413,9 +1413,6 @@ CREATE TABLE `c_sku` (
|
|||||||
`ExHsCode` VARCHAR(64) NULL COMMENT '出口HSCode',
|
`ExHsCode` VARCHAR(64) NULL COMMENT '出口HSCode',
|
||||||
`ImHsCode` VARCHAR(64) NULL COMMENT '进口HSCode',
|
`ImHsCode` VARCHAR(64) NULL COMMENT '进口HSCode',
|
||||||
`DeclareElement` VARCHAR(255) NULL COMMENT '出口申报要素',
|
`DeclareElement` VARCHAR(255) NULL COMMENT '出口申报要素',
|
||||||
`ProductId` VARCHAR(100) NULL COMMENT '产品标识编码',
|
|
||||||
`NonStdProductId` VARCHAR(100) NULL COMMENT '非标产品标识编码',
|
|
||||||
`StdProductId` VARCHAR(100) NULL COMMENT '标准产品标识编码',
|
|
||||||
`WeightG` DECIMAL(10,2) NULL COMMENT '重量(g)',
|
`WeightG` DECIMAL(10,2) NULL COMMENT '重量(g)',
|
||||||
`LengthCm` DECIMAL(10,2) NULL COMMENT '长(cm)',
|
`LengthCm` DECIMAL(10,2) NULL COMMENT '长(cm)',
|
||||||
`WidthCm` DECIMAL(10,2) NULL COMMENT '宽(cm)',
|
`WidthCm` DECIMAL(10,2) NULL COMMENT '宽(cm)',
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
-- c_sku:新增产品标识编码 / 非标产品标识编码 / 标准产品标识编码(对齐订单申报明细)。
|
|
||||||
-- 可重复执行。
|
|
||||||
|
|
||||||
SET @db := DATABASE();
|
|
||||||
|
|
||||||
-- c_sku.ProductId
|
|
||||||
SET @exists := (
|
|
||||||
SELECT COUNT(1)
|
|
||||||
FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = @db
|
|
||||||
AND TABLE_NAME = 'c_sku'
|
|
||||||
AND COLUMN_NAME = 'ProductId'
|
|
||||||
);
|
|
||||||
SET @sql := IF(
|
|
||||||
@exists = 0,
|
|
||||||
'ALTER TABLE `c_sku` ADD COLUMN `ProductId` VARCHAR(100) NULL COMMENT ''产品标识编码'' AFTER `DeclareElement`',
|
|
||||||
'SELECT ''c_sku.ProductId already exists'' AS info'
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
-- c_sku.NonStdProductId
|
|
||||||
SET @exists := (
|
|
||||||
SELECT COUNT(1)
|
|
||||||
FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = @db
|
|
||||||
AND TABLE_NAME = 'c_sku'
|
|
||||||
AND COLUMN_NAME = 'NonStdProductId'
|
|
||||||
);
|
|
||||||
SET @sql := IF(
|
|
||||||
@exists = 0,
|
|
||||||
'ALTER TABLE `c_sku` ADD COLUMN `NonStdProductId` VARCHAR(100) NULL COMMENT ''非标产品标识编码'' AFTER `ProductId`',
|
|
||||||
'SELECT ''c_sku.NonStdProductId already exists'' AS info'
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
-- c_sku.StdProductId
|
|
||||||
SET @exists := (
|
|
||||||
SELECT COUNT(1)
|
|
||||||
FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = @db
|
|
||||||
AND TABLE_NAME = 'c_sku'
|
|
||||||
AND COLUMN_NAME = 'StdProductId'
|
|
||||||
);
|
|
||||||
SET @sql := IF(
|
|
||||||
@exists = 0,
|
|
||||||
'ALTER TABLE `c_sku` ADD COLUMN `StdProductId` VARCHAR(100) NULL COMMENT ''标准产品标识编码'' AFTER `NonStdProductId`',
|
|
||||||
'SELECT ''c_sku.StdProductId already exists'' AS info'
|
|
||||||
);
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Oms.Application.Dtos.Orders;
|
namespace Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
@ -69,24 +68,21 @@ public class OrderParcelItemInput
|
|||||||
public string? DeclareElement { get; set; }
|
public string? DeclareElement { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 产品标识编码(选填)。预报下游始终序列化,无值时为 null。
|
/// 产品标识编码(选填)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[StringLength(100)]
|
[StringLength(100)]
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
|
||||||
public string? ProductId { get; set; }
|
public string? ProductId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 非标产品标识编码(选填)。预报下游始终序列化,无值时为 null。
|
/// 非标产品标识编码(选填)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[StringLength(100)]
|
[StringLength(100)]
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
|
||||||
public string? NonStdProductId { get; set; }
|
public string? NonStdProductId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 标准产品标识编码(选填)。预报下游始终序列化,无值时为 null。
|
/// 标准产品标识编码(选填)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[StringLength(100)]
|
[StringLength(100)]
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
|
||||||
public string? StdProductId { get; set; }
|
public string? StdProductId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -47,21 +47,6 @@ public class WarehouseSkuOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? DeclareElement { get; set; }
|
public string? DeclareElement { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 产品标识编码。
|
|
||||||
/// </summary>
|
|
||||||
public string? ProductId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 非标产品标识编码。
|
|
||||||
/// </summary>
|
|
||||||
public string? NonStdProductId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 标准产品标识编码(如 GTIN / UPC / EAN)。
|
|
||||||
/// </summary>
|
|
||||||
public string? StdProductId { get; set; }
|
|
||||||
|
|
||||||
public decimal? WeightG { get; set; }
|
public decimal? WeightG { get; set; }
|
||||||
|
|
||||||
public decimal? LengthCm { get; set; }
|
public decimal? LengthCm { get; set; }
|
||||||
@ -106,21 +91,6 @@ public class WarehouseSkuSaveInput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? DeclareElement { get; set; }
|
public string? DeclareElement { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 产品标识编码。
|
|
||||||
/// </summary>
|
|
||||||
public string? ProductId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 非标产品标识编码。
|
|
||||||
/// </summary>
|
|
||||||
public string? NonStdProductId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 标准产品标识编码(如 GTIN / UPC / EAN)。
|
|
||||||
/// </summary>
|
|
||||||
public string? StdProductId { get; set; }
|
|
||||||
|
|
||||||
public decimal? WeightG { get; set; }
|
public decimal? WeightG { get; set; }
|
||||||
|
|
||||||
public decimal? LengthCm { get; set; }
|
public decimal? LengthCm { get; set; }
|
||||||
|
|||||||
Binary file not shown.
@ -1,5 +0,0 @@
|
|||||||
# 面单 PDF 中文字体
|
|
||||||
|
|
||||||
`OmsLabelCjk.ttf`:随程序嵌入,供 QuestPDF 渲染箱唛/SKU 中文(避免默认字体缺字形显示 `?`/`□`)。
|
|
||||||
|
|
||||||
部署镜像无需再依赖系统 Noto;仍可通过环境变量 `OMS_LABEL_FONT` 覆盖为其它 ttf/otf。
|
|
||||||
@ -63,10 +63,6 @@ public sealed class HiprintElementOptions
|
|||||||
[JsonPropertyName("title")]
|
[JsonPropertyName("title")]
|
||||||
public string? Title { get; set; }
|
public string? Title { get; set; }
|
||||||
|
|
||||||
/// <summary>设计器「测试数据」;无 field 的纯文本有时只把文案写在这里。</summary>
|
|
||||||
[JsonPropertyName("testData")]
|
|
||||||
public string? TestData { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("field")]
|
[JsonPropertyName("field")]
|
||||||
public string? Field { get; set; }
|
public string? Field { get; set; }
|
||||||
|
|
||||||
|
|||||||
@ -105,15 +105,14 @@ public sealed class HiprintTemplateParser
|
|||||||
Src = ResolveValue(opt.Field, opt.Src ?? opt.Title, printData)
|
Src = ResolveValue(opt.Field, opt.Src ?? opt.Title, printData)
|
||||||
};
|
};
|
||||||
case "longtext":
|
case "longtext":
|
||||||
return BuildTextOp(
|
return BuildTextOp(opt, left, top, width, height, printData, wrap: true);
|
||||||
opt, left, top, width, height, printData, wrap: true, element.PrintElementType?.Title);
|
|
||||||
case "text":
|
case "text":
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
var textType = opt.TextType?.Trim().ToLowerInvariant();
|
var textType = opt.TextType?.Trim().ToLowerInvariant();
|
||||||
if (textType is "barcode" or "qrcode")
|
if (textType is "barcode" or "qrcode")
|
||||||
{
|
{
|
||||||
var content = ResolveValue(opt.Field, FirstNonEmpty(opt.Title, opt.TestData), printData);
|
var content = ResolveValue(opt.Field, opt.Title, printData);
|
||||||
if (string.IsNullOrWhiteSpace(content))
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@ -130,8 +129,7 @@ public sealed class HiprintTemplateParser
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return BuildTextOp(
|
return BuildTextOp(opt, left, top, width, height, printData, wrap: false);
|
||||||
opt, left, top, width, height, printData, wrap: false, element.PrintElementType?.Title);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,38 +141,25 @@ public sealed class HiprintTemplateParser
|
|||||||
float width,
|
float width,
|
||||||
float height,
|
float height,
|
||||||
IReadOnlyDictionary<string, object?> printData,
|
IReadOnlyDictionary<string, object?> printData,
|
||||||
bool wrap,
|
bool wrap)
|
||||||
string? elementTypeTitle = null)
|
|
||||||
{
|
{
|
||||||
var value = ResolveValue(opt.Field, null, printData);
|
var value = ResolveValue(opt.Field, null, printData);
|
||||||
// 与 vg-print 一致:无 field 时打印 title;有 field 时按 hideTitle 决定是否带标题
|
|
||||||
var title = FirstNonEmpty(opt.Title, elementTypeTitle);
|
|
||||||
string text;
|
string text;
|
||||||
if (!string.IsNullOrWhiteSpace(opt.Field))
|
if (!string.IsNullOrWhiteSpace(opt.Field))
|
||||||
{
|
{
|
||||||
var hideTitle = opt.HideTitle != false; // null/true → 隐藏标题(OMS 字段默认 hideTitle:true)
|
// 有 field:默认只打字段值;hideTitle=false 且有 title 时拼「标题:值」
|
||||||
if (!hideTitle && !string.IsNullOrWhiteSpace(title) && !string.IsNullOrWhiteSpace(value))
|
if (opt.HideTitle == false && !string.IsNullOrWhiteSpace(opt.Title) && !string.IsNullOrWhiteSpace(value))
|
||||||
{
|
{
|
||||||
text = $"{title}:{value}";
|
text = $"{opt.Title}:{value}";
|
||||||
}
|
|
||||||
else if (!string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
text = value!;
|
|
||||||
}
|
|
||||||
else if (!hideTitle && !string.IsNullOrWhiteSpace(title))
|
|
||||||
{
|
|
||||||
// 仅标题(值为空时仍保留自定义标题文案)
|
|
||||||
text = title!;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
text = string.Empty;
|
text = value ?? string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 纯自定义文本:title → testData → 元素类型名
|
text = opt.Title ?? string.Empty;
|
||||||
text = FirstNonEmpty(opt.Title, opt.TestData, elementTypeTitle) ?? string.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
@ -184,18 +169,15 @@ public sealed class HiprintTemplateParser
|
|||||||
|
|
||||||
var bold = string.Equals(opt.FontWeight, "bold", StringComparison.OrdinalIgnoreCase)
|
var bold = string.Equals(opt.FontWeight, "bold", StringComparison.OrdinalIgnoreCase)
|
||||||
|| opt.FontWeight is "700" or "600";
|
|| opt.FontWeight is "700" or "600";
|
||||||
var fontSize = opt.FontSize is > 0 ? opt.FontSize.Value : 10f;
|
|
||||||
// 设计器文本框常只有十余 pt 高,行高略大于字号时会被裁成「空白」
|
|
||||||
var boxHeight = Math.Max(height, fontSize * 1.4f);
|
|
||||||
|
|
||||||
return new LabelTextOp
|
return new LabelTextOp
|
||||||
{
|
{
|
||||||
LeftPt = left,
|
LeftPt = left,
|
||||||
TopPt = top,
|
TopPt = top,
|
||||||
WidthPt = width,
|
WidthPt = width,
|
||||||
HeightPt = boxHeight,
|
HeightPt = height,
|
||||||
Text = text,
|
Text = text,
|
||||||
FontSizePt = fontSize,
|
FontSizePt = opt.FontSize is > 0 ? opt.FontSize.Value : 10f,
|
||||||
Bold = bold,
|
Bold = bold,
|
||||||
ColorHex = opt.Color,
|
ColorHex = opt.Color,
|
||||||
Align = (opt.TextAlign ?? "left").ToLowerInvariant(),
|
Align = (opt.TextAlign ?? "left").ToLowerInvariant(),
|
||||||
@ -203,9 +185,6 @@ public sealed class HiprintTemplateParser
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? FirstNonEmpty(params string?[] values) =>
|
|
||||||
values.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x));
|
|
||||||
|
|
||||||
private static string? ResolveValue(
|
private static string? ResolveValue(
|
||||||
string? field,
|
string? field,
|
||||||
string? fallback,
|
string? fallback,
|
||||||
|
|||||||
@ -1,120 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using QuestPDF.Drawing;
|
|
||||||
|
|
||||||
namespace Oms.Application.Labeling.Hiprint;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 面单 PDF 中文字体:QuestPDF 默认字体无 CJK;优先嵌入字体,避免依赖容器内 TTC。
|
|
||||||
/// </summary>
|
|
||||||
public static class LabelPdfFontRegistrar
|
|
||||||
{
|
|
||||||
public const string FamilyName = "OmsLabelSans";
|
|
||||||
|
|
||||||
private const string EmbeddedName = "Oms.Application.Fonts.OmsLabelCjk.ttf";
|
|
||||||
|
|
||||||
private static int _state; // 0=未试 1=成功 2=失败
|
|
||||||
private static string? _source;
|
|
||||||
private static string? _lastError;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 确保已注册可用中文字体;成功返回 <see cref="FamilyName"/>,否则 null。
|
|
||||||
/// </summary>
|
|
||||||
public static string? EnsureRegistered()
|
|
||||||
{
|
|
||||||
var state = Volatile.Read(ref _state);
|
|
||||||
if (state == 1)
|
|
||||||
{
|
|
||||||
return FamilyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == 2)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var attempt in EnumerateAttempts())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var stream = attempt.Open();
|
|
||||||
if (stream == null || stream.Length == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读入内存:Register 后不可依赖外部流生命周期;并规避部分 TTC 流问题
|
|
||||||
using var ms = new MemoryStream();
|
|
||||||
stream.CopyTo(ms);
|
|
||||||
ms.Position = 0;
|
|
||||||
FontManager.RegisterFontWithCustomName(FamilyName, ms);
|
|
||||||
_source = attempt.Description;
|
|
||||||
Interlocked.Exchange(ref _state, 1);
|
|
||||||
return FamilyName;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_lastError = $"{attempt.Description}: {ex.Message}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Interlocked.Exchange(ref _state, 2);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string? RegisteredSource => _source;
|
|
||||||
|
|
||||||
public static string? LastError => _lastError;
|
|
||||||
|
|
||||||
private static IEnumerable<FontAttempt> EnumerateAttempts()
|
|
||||||
{
|
|
||||||
yield return new FontAttempt(
|
|
||||||
"embedded:" + EmbeddedName,
|
|
||||||
() => Assembly.GetExecutingAssembly().GetManifestResourceStream(EmbeddedName));
|
|
||||||
|
|
||||||
var fromEnv = Environment.GetEnvironmentVariable("OMS_LABEL_FONT");
|
|
||||||
if (!string.IsNullOrWhiteSpace(fromEnv))
|
|
||||||
{
|
|
||||||
var path = fromEnv.Trim();
|
|
||||||
yield return new FontAttempt("env:" + path, () => File.Exists(path) ? File.OpenRead(path) : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
var baseDir = AppContext.BaseDirectory;
|
|
||||||
foreach (var name in new[]
|
|
||||||
{
|
|
||||||
"OmsLabelCjk.ttf",
|
|
||||||
"NotoSansSC-Regular.otf",
|
|
||||||
"NotoSansSC-Regular.ttf",
|
|
||||||
"NotoSansCJKsc-Regular.otf",
|
|
||||||
"simhei.ttf"
|
|
||||||
})
|
|
||||||
{
|
|
||||||
var path = Path.Combine(baseDir, "Fonts", name);
|
|
||||||
yield return new FontAttempt("file:" + path, () => File.Exists(path) ? File.OpenRead(path) : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
var winFonts = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
|
|
||||||
if (!string.IsNullOrWhiteSpace(winFonts))
|
|
||||||
{
|
|
||||||
// 单文件 TTF 优先于 TTC(集合字体在部分 Skia 版本上注册失败/缺简体)
|
|
||||||
foreach (var name in new[] { "simhei.ttf", "msyh.ttc", "msyhbd.ttc", "simsun.ttc" })
|
|
||||||
{
|
|
||||||
var path = Path.Combine(winFonts, name);
|
|
||||||
yield return new FontAttempt("win:" + path, () => File.Exists(path) ? File.OpenRead(path) : null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var path in new[]
|
|
||||||
{
|
|
||||||
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
|
|
||||||
"/usr/share/fonts/truetype/arphic/uming.ttc",
|
|
||||||
"/usr/share/fonts/opentype/noto/NotoSansCJKsc-Regular.otf",
|
|
||||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
|
||||||
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc"
|
|
||||||
})
|
|
||||||
{
|
|
||||||
yield return new FontAttempt("linux:" + path, () => File.Exists(path) ? File.OpenRead(path) : null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct FontAttempt(string Description, Func<Stream?> Open);
|
|
||||||
}
|
|
||||||
@ -23,9 +23,6 @@ public sealed class QuestPdfLabelRenderer(HiprintTemplateParser parser) : ILabel
|
|||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
EnsureLicense();
|
EnsureLicense();
|
||||||
var fontFamily = LabelPdfFontRegistrar.EnsureRegistered()
|
|
||||||
?? throw new InvalidOperationException(
|
|
||||||
"面单中文字体未就绪:" + (LabelPdfFontRegistrar.LastError ?? "未找到可用 CJK 字体"));
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (printDataPages == null || printDataPages.Count == 0)
|
if (printDataPages == null || printDataPages.Count == 0)
|
||||||
@ -51,15 +48,13 @@ public sealed class QuestPdfLabelRenderer(HiprintTemplateParser parser) : ILabel
|
|||||||
{
|
{
|
||||||
page.Size(pageWidthPt, pageHeightPt);
|
page.Size(pageWidthPt, pageHeightPt);
|
||||||
page.Margin(0);
|
page.Margin(0);
|
||||||
// 页级默认中文字体;单元素再设字号/颜色。勿对嵌入 Regular 调 Bold(会回退缺字西文字体 → ???)
|
|
||||||
page.DefaultTextStyle(x => x.FontFamily(fontFamily).FontColor(Colors.Black));
|
|
||||||
page.Content().Layers(layers =>
|
page.Content().Layers(layers =>
|
||||||
{
|
{
|
||||||
layers.PrimaryLayer().Background(Colors.White);
|
layers.PrimaryLayer().Background(Colors.White);
|
||||||
|
|
||||||
foreach (var op in pageIr.Ops)
|
foreach (var op in pageIr.Ops)
|
||||||
{
|
{
|
||||||
DrawOp(layers, op, fontFamily);
|
DrawOp(layers, op);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -69,7 +64,7 @@ public sealed class QuestPdfLabelRenderer(HiprintTemplateParser parser) : ILabel
|
|||||||
return Task.FromResult(bytes);
|
return Task.FromResult(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DrawOp(LayersDescriptor layers, LabelDrawOp op, string fontFamily)
|
private static void DrawOp(LayersDescriptor layers, LabelDrawOp op)
|
||||||
{
|
{
|
||||||
switch (op)
|
switch (op)
|
||||||
{
|
{
|
||||||
@ -89,16 +84,16 @@ public sealed class QuestPdfLabelRenderer(HiprintTemplateParser parser) : ILabel
|
|||||||
_ => box.AlignLeft()
|
_ => box.AlignLeft()
|
||||||
};
|
};
|
||||||
|
|
||||||
aligned.AlignTop().Text(t =>
|
var span = aligned
|
||||||
|
.AlignTop()
|
||||||
|
.Text(text.Text)
|
||||||
|
.FontSize(text.FontSizePt)
|
||||||
|
.FontColor(ParseColor(text.ColorHex));
|
||||||
|
|
||||||
|
if (text.Bold)
|
||||||
{
|
{
|
||||||
// 勿对嵌入 Regular 调用 Bold():会回退缺字西文字体,中文变成 ???
|
span.Bold();
|
||||||
var size = text.Bold ? text.FontSizePt * 1.02f : text.FontSizePt;
|
}
|
||||||
t.DefaultTextStyle(style => style
|
|
||||||
.FontFamily(fontFamily)
|
|
||||||
.FontSize(size)
|
|
||||||
.FontColor(ParseColor(text.ColorHex)));
|
|
||||||
t.Span(text.Text ?? string.Empty);
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -165,8 +160,6 @@ public sealed class QuestPdfLabelRenderer(HiprintTemplateParser parser) : ILabel
|
|||||||
}
|
}
|
||||||
|
|
||||||
QuestPDF.Settings.License = LicenseType.Community;
|
QuestPDF.Settings.License = LicenseType.Community;
|
||||||
// 缺字形时不抛,但仍应依赖嵌入 CJK 字体
|
|
||||||
QuestPDF.Settings.CheckIfAllTextGlyphsAreAvailable = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float MmToPt(float mm) => mm * 72f / 25.4f;
|
private static float MmToPt(float mm) => mm * 72f / 25.4f;
|
||||||
|
|||||||
@ -61,7 +61,6 @@ public static class InboundBoxLabelPrintDataBuilder
|
|||||||
|
|
||||||
["boxId"] = box.Id,
|
["boxId"] = box.Id,
|
||||||
["boxSeqNo"] = box.BoxSeqNo,
|
["boxSeqNo"] = box.BoxSeqNo,
|
||||||
["boxCount"] = pageCount,
|
|
||||||
["markBoxNo"] = box.MarkBoxNo,
|
["markBoxNo"] = box.MarkBoxNo,
|
||||||
["barcode"] = barcode,
|
["barcode"] = barcode,
|
||||||
|
|
||||||
@ -104,7 +103,6 @@ public static class InboundBoxLabelPrintDataBuilder
|
|||||||
["remark"] = null,
|
["remark"] = null,
|
||||||
["boxId"] = 1,
|
["boxId"] = 1,
|
||||||
["boxSeqNo"] = "1",
|
["boxSeqNo"] = "1",
|
||||||
["boxCount"] = 2,
|
|
||||||
["markBoxNo"] = "BOX-001",
|
["markBoxNo"] = "BOX-001",
|
||||||
["barcode"] = "BOX-001",
|
["barcode"] = "BOX-001",
|
||||||
["warehouseName"] = "SZ-01",
|
["warehouseName"] = "SZ-01",
|
||||||
|
|||||||
@ -85,7 +85,6 @@ public static class LabelFieldDictionary
|
|||||||
Field("markReference", "唛头参考"),
|
Field("markReference", "唛头参考"),
|
||||||
Field("markBoxNo", "箱唛号"),
|
Field("markBoxNo", "箱唛号"),
|
||||||
Field("boxSeqNo", "箱序号"),
|
Field("boxSeqNo", "箱序号"),
|
||||||
Field("boxCount", "箱子总数"),
|
|
||||||
Field("barcode", "条码内容"),
|
Field("barcode", "条码内容"),
|
||||||
Field("boxIndexText", "箱序页码"),
|
Field("boxIndexText", "箱序页码"),
|
||||||
Field("skuSummary", "SKU 摘要"),
|
Field("skuSummary", "SKU 摘要"),
|
||||||
|
|||||||
@ -29,9 +29,6 @@ public static class SkuLabelPrintDataBuilder
|
|||||||
["exHsCode"] = sku.ExHsCode,
|
["exHsCode"] = sku.ExHsCode,
|
||||||
["imHsCode"] = sku.ImHsCode,
|
["imHsCode"] = sku.ImHsCode,
|
||||||
["declareElement"] = sku.DeclareElement,
|
["declareElement"] = sku.DeclareElement,
|
||||||
["productId"] = sku.ProductId,
|
|
||||||
["nonStdProductId"] = sku.NonStdProductId,
|
|
||||||
["stdProductId"] = sku.StdProductId,
|
|
||||||
["weightG"] = sku.WeightG,
|
["weightG"] = sku.WeightG,
|
||||||
["lengthCm"] = sku.LengthCm,
|
["lengthCm"] = sku.LengthCm,
|
||||||
["widthCm"] = sku.WidthCm,
|
["widthCm"] = sku.WidthCm,
|
||||||
@ -64,9 +61,6 @@ public static class SkuLabelPrintDataBuilder
|
|||||||
["exHsCode"] = "610910",
|
["exHsCode"] = "610910",
|
||||||
["imHsCode"] = "610910",
|
["imHsCode"] = "610910",
|
||||||
["declareElement"] = "棉质 T 恤",
|
["declareElement"] = "棉质 T 恤",
|
||||||
["productId"] = null,
|
|
||||||
["nonStdProductId"] = null,
|
|
||||||
["stdProductId"] = "06901234567890",
|
|
||||||
["weightG"] = 200m,
|
["weightG"] = 200m,
|
||||||
["lengthCm"] = 30m,
|
["lengthCm"] = 30m,
|
||||||
["widthCm"] = 20m,
|
["widthCm"] = 20m,
|
||||||
|
|||||||
@ -41,13 +41,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Services\Ai\AiSkillSeedContent.json" />
|
<EmbeddedResource Include="Services\Ai\AiSkillSeedContent.json" />
|
||||||
<!-- 面单 QuestPDF 中文:嵌入单文件 TTF,避免容器仅有 TTC/未装字体时出 ??? -->
|
|
||||||
<EmbeddedResource Include="Fonts\OmsLabelCjk.ttf" LogicalName="Oms.Application.Fonts.OmsLabelCjk.ttf" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Include="Fonts\OmsLabelCjk.ttf" CopyToOutputDirectory="PreserveNewest" Link="Fonts\OmsLabelCjk.ttf" />
|
|
||||||
<None Include="Fonts\README.md" CopyToOutputDirectory="Never" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -16,7 +16,6 @@ using Oms.Application.Storage;
|
|||||||
using Oms.Domain.OrdersAggregate;
|
using Oms.Domain.OrdersAggregate;
|
||||||
using Oms.Domain.ChannelsAggregate.Consts;
|
using Oms.Domain.ChannelsAggregate.Consts;
|
||||||
using Oms.Domain.ChannelsAggregate.Entities;
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
using Oms.Domain.CustomersAggregate.Entities;
|
|
||||||
using Oms.Domain.OrdersAggregate.Consts;
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
using Oms.Domain.OrdersAggregate.Entities;
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
using Oms.Domain.SystemAggregate.Entities;
|
using Oms.Domain.SystemAggregate.Entities;
|
||||||
@ -297,272 +296,6 @@ public partial class OrderService
|
|||||||
return outputs;
|
return outputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 列表页专用映射:不加载申报明细/费用明细,批量 enrichment,减小 JSON 与 DB 开销。
|
|
||||||
/// </summary>
|
|
||||||
private async Task<List<OrderOutput>> MapOrderListOutputsAsync(IReadOnlyCollection<Order> orders)
|
|
||||||
{
|
|
||||||
if (orders.Count == 0)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var orderIds = orders.Select(x => x.Id).ToArray();
|
|
||||||
var warehouseIds = orders.Where(x => x.WarehouseId.HasValue).Select(x => x.WarehouseId!.Value).Distinct().ToArray();
|
|
||||||
var productCodes = orders
|
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.ProductCode))
|
|
||||||
.Select(x => x.ProductCode!.Trim())
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToArray();
|
|
||||||
var channelCodes = orders
|
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.ChannelCode))
|
|
||||||
.Select(x => x.ChannelCode!.Trim())
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToArray();
|
|
||||||
var countryCodes = orders
|
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.ConsigneeCountry))
|
|
||||||
.Select(x => x.ConsigneeCountry!.Trim())
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToArray();
|
|
||||||
var customerIds = orders
|
|
||||||
.Where(x => x.CustomerId is > 0)
|
|
||||||
.Select(x => x.CustomerId!.Value)
|
|
||||||
.Distinct()
|
|
||||||
.ToArray();
|
|
||||||
var pickupOrderIds = orders
|
|
||||||
.Where(x => x.PickupOrderId is > 0)
|
|
||||||
.Select(x => x.PickupOrderId!.Value)
|
|
||||||
.Distinct()
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
// SqlSugarScope 共用同一 MySqlConnection,不能 Task.WhenAll 并发查库(会报 already in use)
|
|
||||||
var senders = await OrderRepository.GetSendersByOrderIdsAsync(orderIds);
|
|
||||||
var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync(orderIds);
|
|
||||||
var weights = await OrderRepository.GetWeightsByOrderIdsAsync(orderIds);
|
|
||||||
var receivableSumMap = await OrderRepository.GetReceivableSumByOrderIdsAsync(orderIds);
|
|
||||||
var warehouses = warehouseIds.Length == 0
|
|
||||||
? []
|
|
||||||
: await WarehouseAddressRepository.GetListAsync(x => warehouseIds.Contains(x.Id));
|
|
||||||
var products = productCodes.Length == 0
|
|
||||||
? []
|
|
||||||
: await SalesProductRepository.GetListAsync(x => productCodes.Contains(x.ProductCode));
|
|
||||||
var channels = channelCodes.Length == 0
|
|
||||||
? []
|
|
||||||
: await ServiceChannelRepository.GetListAsync(x => channelCodes.Contains(x.ChannelCode));
|
|
||||||
var countries = countryCodes.Length == 0
|
|
||||||
? []
|
|
||||||
: await CountryRepository.GetListAsync(x => countryCodes.Contains(x.CountryCode));
|
|
||||||
var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync(
|
|
||||||
orders.SelectMany(x => new[] { x.CreateUserId, x.UpdateUserId }));
|
|
||||||
var customers = customerIds.Length == 0
|
|
||||||
? []
|
|
||||||
: await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
|
|
||||||
var pickupOrders = pickupOrderIds.Length == 0
|
|
||||||
? []
|
|
||||||
: await PickupOrderRepository.GetListAsync(x => pickupOrderIds.Contains(x.Id));
|
|
||||||
var bindSummaryMap = await LoadStockBindListSummariesAsync(orderIds);
|
|
||||||
|
|
||||||
var senderMap = senders.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.First());
|
|
||||||
var recipientMap = recipients.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.First());
|
|
||||||
var weightMap = weights.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.ToList());
|
|
||||||
var warehouseMap = warehouses.ToDictionary(x => x.Id);
|
|
||||||
var productMap = products.ToDictionary(x => x.ProductCode, StringComparer.OrdinalIgnoreCase);
|
|
||||||
var channelMap = channels.ToDictionary(x => x.ChannelCode, StringComparer.OrdinalIgnoreCase);
|
|
||||||
var countryMap = countries.ToDictionary(x => x.CountryCode, StringComparer.OrdinalIgnoreCase);
|
|
||||||
var customerMap = customers.ToDictionary(x => x.Id);
|
|
||||||
var pickupNoMap = pickupOrders.ToDictionary(x => x.Id, x => x.PickupNo);
|
|
||||||
var carrierWaybillNoMap = pickupOrders
|
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.CarrierWaybillNo))
|
|
||||||
.ToDictionary(x => x.Id, x => x.CarrierWaybillNo!);
|
|
||||||
|
|
||||||
var addressCountryIds = senders
|
|
||||||
.Select(x => x.Country)
|
|
||||||
.Concat(recipients.Select(x => x.Country))
|
|
||||||
.Where(x => x.HasValue)
|
|
||||||
.Select(x => x!.Value)
|
|
||||||
.Distinct()
|
|
||||||
.ToArray();
|
|
||||||
var addressCountries = addressCountryIds.Length == 0
|
|
||||||
? []
|
|
||||||
: await CountryRepository.GetListAsync(x => addressCountryIds.Contains(x.Id));
|
|
||||||
var countryIdCodeMap = addressCountries.ToDictionary(
|
|
||||||
x => x.Id,
|
|
||||||
x => x.CountryCode ?? string.Empty);
|
|
||||||
|
|
||||||
var outputs = orders.Select(order =>
|
|
||||||
{
|
|
||||||
senderMap.TryGetValue(order.Id, out var sender);
|
|
||||||
recipientMap.TryGetValue(order.Id, out var recipient);
|
|
||||||
weightMap.TryGetValue(order.Id, out var orderWeights);
|
|
||||||
var chargeableWeight = ResolveDisplayChargeableWeight(orderWeights);
|
|
||||||
decimal? receivableAmount = receivableSumMap.TryGetValue(order.Id, out var sum)
|
|
||||||
? sum
|
|
||||||
: null;
|
|
||||||
var additionalServices = NormalizeAdditionalServiceCodes(
|
|
||||||
OmsDocHelper.DeserializeJson<List<string>>(order.AdditionalServices));
|
|
||||||
var hasInsurance = additionalServices.Any(x =>
|
|
||||||
string.Equals(x, "insurance", StringComparison.OrdinalIgnoreCase));
|
|
||||||
string? customerName = null;
|
|
||||||
string? customerCode = null;
|
|
||||||
if (order.CustomerId is > 0 && customerMap.TryGetValue(order.CustomerId.Value, out var customer))
|
|
||||||
{
|
|
||||||
customerName = customer.CustomerName;
|
|
||||||
customerCode = customer.CustomerCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
SalesProduct? product = null;
|
|
||||||
if (!string.IsNullOrWhiteSpace(order.ProductCode))
|
|
||||||
{
|
|
||||||
productMap.TryGetValue(order.ProductCode.Trim(), out product);
|
|
||||||
}
|
|
||||||
|
|
||||||
bindSummaryMap.TryGetValue(order.Id, out var bindSummary);
|
|
||||||
|
|
||||||
return new OrderOutput
|
|
||||||
{
|
|
||||||
Id = order.Id,
|
|
||||||
ClientNo = order.ClientNo,
|
|
||||||
TrackNo = order.TrackNo,
|
|
||||||
DownstreamServiceNo = order.DownstreamServiceNo,
|
|
||||||
ServiceNo = order.ServiceNo,
|
|
||||||
ProductCode = order.ProductCode,
|
|
||||||
ProductName = product?.ProductName,
|
|
||||||
MaxTimeliness = product?.MaxTimeliness,
|
|
||||||
ChannelCode = order.ChannelCode,
|
|
||||||
DownstreamSupplierCode = order.DownstreamSupplierCode,
|
|
||||||
DownstreamSupplierName = order.DownstreamSupplierName,
|
|
||||||
DownstreamAppKey = order.DownstreamAppKey,
|
|
||||||
DownstreamAppId = order.DownstreamAppId,
|
|
||||||
ChannelForecastCode = order.ChannelForecastCode,
|
|
||||||
ChannelName = !string.IsNullOrWhiteSpace(order.ChannelCode)
|
|
||||||
&& channelMap.TryGetValue(order.ChannelCode.Trim(), out var channel)
|
|
||||||
? channel.ChannelName
|
|
||||||
: null,
|
|
||||||
ConsigneeCountry = order.ConsigneeCountry,
|
|
||||||
ConsigneeCountryName = ResolveConsigneeCountryName(order, countryMap),
|
|
||||||
CargoAttribute = order.CargoAttribute,
|
|
||||||
WarehouseId = order.WarehouseId,
|
|
||||||
Warehouse = order.WarehouseId.HasValue && warehouseMap.TryGetValue(order.WarehouseId.Value, out var warehouse)
|
|
||||||
? warehouse.WarehouseName
|
|
||||||
: null,
|
|
||||||
CustomerId = order.CustomerId,
|
|
||||||
CustomerName = customerName,
|
|
||||||
CustomerCode = customerCode,
|
|
||||||
OrderSource = string.IsNullOrWhiteSpace(order.OrderSource)
|
|
||||||
? OrderSourceCodes.Manual
|
|
||||||
: order.OrderSource,
|
|
||||||
PickupTime = order.PickupTime,
|
|
||||||
CustomerSenderAddressId = order.CustomerSenderAddressId,
|
|
||||||
InboundMethod = order.InboundMethod,
|
|
||||||
FulfillmentType = string.IsNullOrWhiteSpace(order.FulfillmentType)
|
|
||||||
? FulfillmentTypeCodes.Direct
|
|
||||||
: order.FulfillmentType,
|
|
||||||
FulfillmentSource = order.FulfillmentSource,
|
|
||||||
StockBindStatus = bindSummary?.BindStatus ?? "none",
|
|
||||||
StockBindLineCount = bindSummary?.LineCount ?? 0,
|
|
||||||
StockBindQtyTotal = bindSummary?.QtyTotal ?? 0,
|
|
||||||
DomesticTrackingNos = bindSummary?.TrackingNosText,
|
|
||||||
FulfillProgress = FulfillmentProgressCodes.Resolve(
|
|
||||||
FulfillmentTypeCodes.IsWarehouse(order.FulfillmentType),
|
|
||||||
bindSummary?.BindStatus ?? "none",
|
|
||||||
bindSummary?.LineCount ?? 0,
|
|
||||||
bindSummary?.HasInTransitParcel ?? false),
|
|
||||||
PrimaryBinCode = bindSummary?.PrimaryBinCode,
|
|
||||||
PrimarySkuCodes = bindSummary?.PrimarySkuCodes,
|
|
||||||
PickupOrderId = order.PickupOrderId,
|
|
||||||
PickupNo = order.PickupOrderId is > 0
|
|
||||||
&& pickupNoMap.TryGetValue(order.PickupOrderId.Value, out var pickupNo)
|
|
||||||
? pickupNo
|
|
||||||
: null,
|
|
||||||
CarrierWaybillNo = order.PickupOrderId is > 0
|
|
||||||
&& carrierWaybillNoMap.TryGetValue(order.PickupOrderId.Value, out var carrierWaybillNo)
|
|
||||||
? carrierWaybillNo
|
|
||||||
: null,
|
|
||||||
RequestId = order.RequestId,
|
|
||||||
ExportCustomsMode = order.ExportCustomsMode,
|
|
||||||
ExportQualificationId = order.ExportQualificationId,
|
|
||||||
PredeclareFailReason = string.IsNullOrWhiteSpace(order.PredeclareFailReason)
|
|
||||||
? null
|
|
||||||
: order.PredeclareFailReason.Trim(),
|
|
||||||
AllocationPendingReason = string.IsNullOrWhiteSpace(order.AllocationPendingReason)
|
|
||||||
? null
|
|
||||||
: order.AllocationPendingReason.Trim(),
|
|
||||||
AllocationScheme = string.IsNullOrWhiteSpace(order.AllocationScheme)
|
|
||||||
? null
|
|
||||||
: order.AllocationScheme.Trim(),
|
|
||||||
// 分页已 IgnoreColumns(AllocationTraceJson);与写入路径一并落库的 TraceTime 作为布尔依据
|
|
||||||
HasAllocationTrace = order.AllocationTraceTime.HasValue
|
|
||||||
|| !string.IsNullOrWhiteSpace(order.AllocationTraceJson),
|
|
||||||
DeclaredCurrency = order.DeclaredCurrency,
|
|
||||||
DeclaredSum = order.DeclaredSum,
|
|
||||||
Quantity = order.Quantity,
|
|
||||||
IsCod = order.IsCod > 0,
|
|
||||||
CodCurrency = order.CodCurrency,
|
|
||||||
CodSum = order.CodSum,
|
|
||||||
OrderStatus = OmsDocHelper.GetOrderStatusName(order.OrderStatus),
|
|
||||||
Status = order.OrderStatus,
|
|
||||||
HandleStatus = OrderHandleStatusCodes.IsProcessed(order.HandleStatus)
|
|
||||||
? OrderHandleStatusCodes.Processed
|
|
||||||
: OrderHandleStatusCodes.Pending,
|
|
||||||
HandleStatusName = OrderHandleStatusCodes.GetDisplayName(order.HandleStatus),
|
|
||||||
IssueType = order.IssueType,
|
|
||||||
IssueTypeName = IssueTypeCodes.GetDisplayName(order.IssueType),
|
|
||||||
IssueTitle = order.IssueTitle,
|
|
||||||
IssueContent = order.IssueContent,
|
|
||||||
IssueTemplateId = order.IssueTemplateId,
|
|
||||||
IssueRemark = order.IssueRemark,
|
|
||||||
IssueTime = order.IssueTime,
|
|
||||||
ClaimType = order.ClaimType,
|
|
||||||
ClaimTypeName = ClaimTypeCodes.GetDisplayName(order.ClaimType),
|
|
||||||
ClaimAmount = order.ClaimAmount,
|
|
||||||
ClaimCurrency = order.ClaimCurrency,
|
|
||||||
ClaimReason = order.ClaimReason,
|
|
||||||
ClaimTime = order.ClaimTime,
|
|
||||||
PreviousOrderStatus = order.PreviousOrderStatus,
|
|
||||||
ReceiveTime = order.ReceiveTime,
|
|
||||||
ShipTime = order.ShipTime,
|
|
||||||
PayTime = order.PayTime,
|
|
||||||
IsPaid = order.IsPaid > 0,
|
|
||||||
SignTime = order.SignTime,
|
|
||||||
IsIntercept = order.IsIntercept > 0,
|
|
||||||
IsRemote = order.IsRemote > 0,
|
|
||||||
AdditionalServices = additionalServices,
|
|
||||||
HasInsurance = hasInsurance,
|
|
||||||
ForecastReceivableAmount = order.ForecastReceivableAmount,
|
|
||||||
ForecastPayableAmount = order.ForecastPayableAmount,
|
|
||||||
ReceivableSubtotal = order.ReceivableSubtotal,
|
|
||||||
LevelDiscountAmount = order.LevelDiscountAmount,
|
|
||||||
CouponDiscountAmount = order.CouponDiscountAmount,
|
|
||||||
ActualPayAmount = order.PayableAmount,
|
|
||||||
CouponInstanceId = order.CouponInstanceId,
|
|
||||||
SalesProductRulesVersion = string.IsNullOrWhiteSpace(order.SalesProductRulesVersion)
|
|
||||||
? null
|
|
||||||
: order.SalesProductRulesVersion.Trim(),
|
|
||||||
SalesProductPriceVersion = string.IsNullOrWhiteSpace(order.SalesProductPriceVersion)
|
|
||||||
? null
|
|
||||||
: order.SalesProductPriceVersion.Trim(),
|
|
||||||
CreateTime = order.CreateTime,
|
|
||||||
CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(order.CreateUserId, auditUserNameMap),
|
|
||||||
UpdateTime = OmsDocHelper.NullIfDefault(order.UpdateTime),
|
|
||||||
UpdateBy = OmsRbacUserDisplayNameResolver.FormatUserId(order.UpdateUserId, auditUserNameMap),
|
|
||||||
Sender = MapSender(sender, countryIdCodeMap),
|
|
||||||
Receiver = MapReceiver(recipient, countryIdCodeMap),
|
|
||||||
Items = [],
|
|
||||||
WeightList = [],
|
|
||||||
FeeList = [],
|
|
||||||
ChargeableWeight = chargeableWeight,
|
|
||||||
ReceivableAmount = receivableAmount,
|
|
||||||
};
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
if (OmsSessionHelper.IsCustomerLogin(CurrentUser))
|
|
||||||
{
|
|
||||||
RedactChannelFieldsForCustomer(outputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
return outputs;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class StockBindListSummary
|
private sealed class StockBindListSummary
|
||||||
{
|
{
|
||||||
public string BindStatus { get; init; } = "none";
|
public string BindStatus { get; init; } = "none";
|
||||||
|
|||||||
@ -70,11 +70,7 @@ public partial class OrderService : ApplicationService
|
|||||||
pageIndex,
|
pageIndex,
|
||||||
pageSize,
|
pageSize,
|
||||||
filter);
|
filter);
|
||||||
return new OmsPageResult<OrderOutput>
|
return new OmsPageResult<OrderOutput> { Items = await MapOrderOutputsAsync(orders), Total = total };
|
||||||
{
|
|
||||||
Items = await MapOrderListOutputsAsync(orders),
|
|
||||||
Total = total,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -267,9 +267,7 @@ public static class PlatformStoreOrderParcelBuilder
|
|||||||
var unitWeightKg = sku.WeightG is > 0 ? Math.Max(sku.WeightG.Value / 1000m, MinActualWeightKg) : MinActualWeightKg;
|
var unitWeightKg = sku.WeightG is > 0 ? Math.Max(sku.WeightG.Value / 1000m, MinActualWeightKg) : MinActualWeightKg;
|
||||||
var unitPrice = line.UnitPrice is > 0 ? line.UnitPrice.Value : 1m;
|
var unitPrice = line.UnitPrice is > 0 ? line.UnitPrice.Value : 1m;
|
||||||
var cnName = string.IsNullOrWhiteSpace(line.ProductName) ? sku.SkuName : line.ProductName.Trim();
|
var cnName = string.IsNullOrWhiteSpace(line.ProductName) ? sku.SkuName : line.ProductName.Trim();
|
||||||
var enName = string.IsNullOrWhiteSpace(sku.SkuNameEn)
|
var enName = string.IsNullOrWhiteSpace(sku.SkuName) ? cnName : sku.SkuName;
|
||||||
? (string.IsNullOrWhiteSpace(sku.SkuName) ? cnName : sku.SkuName)
|
|
||||||
: sku.SkuNameEn.Trim();
|
|
||||||
|
|
||||||
items.Add(new OrderParcelItemInput
|
items.Add(new OrderParcelItemInput
|
||||||
{
|
{
|
||||||
@ -281,13 +279,7 @@ public static class PlatformStoreOrderParcelBuilder
|
|||||||
UnitWeight = unitWeightKg,
|
UnitWeight = unitWeightKg,
|
||||||
Quantity = line.Quantity,
|
Quantity = line.Quantity,
|
||||||
Amount = unitPrice * line.Quantity,
|
Amount = unitPrice * line.Quantity,
|
||||||
Weight = unitWeightKg * line.Quantity,
|
Weight = unitWeightKg * line.Quantity
|
||||||
ExHsCode = sku.ExHsCode,
|
|
||||||
ImHsCode = sku.ImHsCode,
|
|
||||||
DeclareElement = sku.DeclareElement,
|
|
||||||
ProductId = sku.ProductId,
|
|
||||||
NonStdProductId = sku.NonStdProductId,
|
|
||||||
StdProductId = sku.StdProductId,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -139,11 +139,6 @@ public partial class WarehouseService
|
|||||||
entity.ExHsCode = string.IsNullOrWhiteSpace(input.ExHsCode) ? null : input.ExHsCode.Trim();
|
entity.ExHsCode = string.IsNullOrWhiteSpace(input.ExHsCode) ? null : input.ExHsCode.Trim();
|
||||||
entity.ImHsCode = string.IsNullOrWhiteSpace(input.ImHsCode) ? null : input.ImHsCode.Trim();
|
entity.ImHsCode = string.IsNullOrWhiteSpace(input.ImHsCode) ? null : input.ImHsCode.Trim();
|
||||||
entity.DeclareElement = string.IsNullOrWhiteSpace(input.DeclareElement) ? null : input.DeclareElement.Trim();
|
entity.DeclareElement = string.IsNullOrWhiteSpace(input.DeclareElement) ? null : input.DeclareElement.Trim();
|
||||||
entity.ProductId = string.IsNullOrWhiteSpace(input.ProductId) ? null : input.ProductId.Trim();
|
|
||||||
entity.NonStdProductId = string.IsNullOrWhiteSpace(input.NonStdProductId)
|
|
||||||
? null
|
|
||||||
: input.NonStdProductId.Trim();
|
|
||||||
entity.StdProductId = string.IsNullOrWhiteSpace(input.StdProductId) ? null : input.StdProductId.Trim();
|
|
||||||
entity.WeightG = input.WeightG;
|
entity.WeightG = input.WeightG;
|
||||||
entity.LengthCm = input.LengthCm;
|
entity.LengthCm = input.LengthCm;
|
||||||
entity.WidthCm = input.WidthCm;
|
entity.WidthCm = input.WidthCm;
|
||||||
@ -314,9 +309,6 @@ public partial class WarehouseService
|
|||||||
ExHsCode = entity.ExHsCode,
|
ExHsCode = entity.ExHsCode,
|
||||||
ImHsCode = entity.ImHsCode,
|
ImHsCode = entity.ImHsCode,
|
||||||
DeclareElement = entity.DeclareElement,
|
DeclareElement = entity.DeclareElement,
|
||||||
ProductId = entity.ProductId,
|
|
||||||
NonStdProductId = entity.NonStdProductId,
|
|
||||||
StdProductId = entity.StdProductId,
|
|
||||||
WeightG = entity.WeightG,
|
WeightG = entity.WeightG,
|
||||||
LengthCm = entity.LengthCm,
|
LengthCm = entity.LengthCm,
|
||||||
WidthCm = entity.WidthCm,
|
WidthCm = entity.WidthCm,
|
||||||
@ -339,9 +331,6 @@ public partial class WarehouseService
|
|||||||
ExHsCode = entity.ExHsCode,
|
ExHsCode = entity.ExHsCode,
|
||||||
ImHsCode = entity.ImHsCode,
|
ImHsCode = entity.ImHsCode,
|
||||||
DeclareElement = entity.DeclareElement,
|
DeclareElement = entity.DeclareElement,
|
||||||
ProductId = entity.ProductId,
|
|
||||||
NonStdProductId = entity.NonStdProductId,
|
|
||||||
StdProductId = entity.StdProductId,
|
|
||||||
WeightG = entity.WeightG,
|
WeightG = entity.WeightG,
|
||||||
LengthCm = entity.LengthCm,
|
LengthCm = entity.LengthCm,
|
||||||
WidthCm = entity.WidthCm,
|
WidthCm = entity.WidthCm,
|
||||||
|
|||||||
@ -201,10 +201,9 @@ public static class OrderDownstreamCreateOneBodyBuilder
|
|||||||
ImHsCode = x.ImHsCode,
|
ImHsCode = x.ImHsCode,
|
||||||
ExHsCode = x.ExHsCode ?? x.HsCode,
|
ExHsCode = x.ExHsCode ?? x.HsCode,
|
||||||
DeclareElement = x.DeclareElement,
|
DeclareElement = x.DeclareElement,
|
||||||
// 始终赋值;空白→null。配合 OrderParcelItemInput 上 JsonIgnore(Never),请求体固定出现 "xxx": null。
|
ProductId = x.ProductId,
|
||||||
ProductId = NullIfWhiteSpace(x.ProductId),
|
NonStdProductId = x.NonStdProductId,
|
||||||
NonStdProductId = NullIfWhiteSpace(x.NonStdProductId),
|
StdProductId = x.StdProductId,
|
||||||
StdProductId = NullIfWhiteSpace(x.StdProductId),
|
|
||||||
SkuImage = fileStorage?.GetPublicUrl(x.SkuImage) ?? x.SkuImage,
|
SkuImage = fileStorage?.GetPublicUrl(x.SkuImage) ?? x.SkuImage,
|
||||||
Category = x.Category,
|
Category = x.Category,
|
||||||
Brand = x.Brand,
|
Brand = x.Brand,
|
||||||
@ -221,7 +220,4 @@ public static class OrderDownstreamCreateOneBodyBuilder
|
|||||||
Specifications = x.Specifications,
|
Specifications = x.Specifications,
|
||||||
Comment = x.Comment
|
Comment = x.Comment
|
||||||
};
|
};
|
||||||
|
|
||||||
private static string? NullIfWhiteSpace(string? value) =>
|
|
||||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,32 +28,7 @@ public static class FileStoragePath
|
|||||||
throw new ArgumentException("object key 不能为空", nameof(pathOrUrl));
|
throw new ArgumentException("object key 不能为空", nameof(pathOrUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义域公开 URL 常带 PathBase「admin/」;库内/COS 逻辑 key 为 uploads/...。
|
return key;
|
||||||
// 若不剥离,GetUrl 再拼 KeyPrefix/PathBase 会变成 admin/admin/uploads → NoSuchKey。
|
|
||||||
return StripPublicPathBase(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 去掉公开 URL 上的 PathBase(可重复的 admin/),保留 uploads/... 等逻辑 key。
|
|
||||||
/// </summary>
|
|
||||||
public static string StripPublicPathBase(string key)
|
|
||||||
{
|
|
||||||
var value = key.Replace('\\', '/').Trim().TrimStart('/');
|
|
||||||
while (value.StartsWith("admin/", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var rest = value["admin/".Length..];
|
|
||||||
// 仅剥「PathBase + 业务前缀」;避免误伤 admin 业务目录(若有)
|
|
||||||
if (rest.StartsWith("uploads/", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| rest.StartsWith("admin/", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
value = rest;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -70,13 +70,6 @@ public interface IOrderRepository : IRepository<Order>
|
|||||||
|
|
||||||
Task<List<OrderFee>> GetFeesByOrderIdsAsync(IEnumerable<int> orderIds, CancellationToken cancellationToken = default);
|
Task<List<OrderFee>> GetFeesByOrderIdsAsync(IEnumerable<int> orderIds, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 列表页:按订单聚合应收金额合计(不加载费用明细行)。
|
|
||||||
/// </summary>
|
|
||||||
Task<Dictionary<int, decimal>> GetReceivableSumByOrderIdsAsync(
|
|
||||||
IEnumerable<int> orderIds,
|
|
||||||
CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
Task<(List<OrderReceivablePayableRow> Items, int Total)> GetReceivablePayablePagedListAsync(
|
Task<(List<OrderReceivablePayableRow> Items, int Total)> GetReceivablePayablePagedListAsync(
|
||||||
int pageIndex,
|
int pageIndex,
|
||||||
int pageSize,
|
int pageSize,
|
||||||
|
|||||||
@ -27,21 +27,6 @@ public class CSku : OmsAuditedAggregateRoot
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? DeclareElement { get; set; }
|
public string? DeclareElement { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 产品标识编码。
|
|
||||||
/// </summary>
|
|
||||||
public string? ProductId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 非标产品标识编码。
|
|
||||||
/// </summary>
|
|
||||||
public string? NonStdProductId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 标准产品标识编码(如 GTIN / UPC / EAN)。
|
|
||||||
/// </summary>
|
|
||||||
public string? StdProductId { get; set; }
|
|
||||||
|
|
||||||
public decimal? WeightG { get; set; }
|
public decimal? WeightG { get; set; }
|
||||||
|
|
||||||
public decimal? LengthCm { get; set; }
|
public decimal? LengthCm { get; set; }
|
||||||
|
|||||||
@ -23,9 +23,7 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
|||||||
var query = ApplyListFilter(db.Queryable<Order>(), filter);
|
var query = ApplyListFilter(db.Queryable<Order>(), filter);
|
||||||
var total = await query.CountAsync(cancellationToken);
|
var total = await query.CountAsync(cancellationToken);
|
||||||
query = ApplyListSort(query, filter);
|
query = ApplyListSort(query, filter);
|
||||||
// 列表仅需 HasAllocationTrace 布尔语义,勿拉取大字段 AllocationTraceJson
|
|
||||||
var items = await query
|
var items = await query
|
||||||
.IgnoreColumns(x => x.AllocationTraceJson)
|
|
||||||
.ToPageListAsync(pageIndex, pageSize, cancellationToken);
|
.ToPageListAsync(pageIndex, pageSize, cancellationToken);
|
||||||
return (items, total);
|
return (items, total);
|
||||||
}
|
}
|
||||||
@ -1106,34 +1104,6 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
|||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 列表页:按订单聚合应收金额合计(不加载费用明细行)。
|
|
||||||
/// </summary>
|
|
||||||
public async Task<Dictionary<int, decimal>> GetReceivableSumByOrderIdsAsync(
|
|
||||||
IEnumerable<int> orderIds,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var ids = orderIds.Distinct().ToArray();
|
|
||||||
if (ids.Length == 0)
|
|
||||||
{
|
|
||||||
return new Dictionary<int, decimal>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var db = await GetDbContextAsync(cancellationToken);
|
|
||||||
var rows = await db.Queryable<OrderFee>()
|
|
||||||
.Where(x => ids.Contains(x.OrderId)
|
|
||||||
&& !x.IsDeleted
|
|
||||||
&& x.Type == OrderFeeTypeCodes.Receivable)
|
|
||||||
.GroupBy(x => x.OrderId)
|
|
||||||
.Select(x => new
|
|
||||||
{
|
|
||||||
OrderId = x.OrderId,
|
|
||||||
Amount = SqlFunc.AggregateSum(x.Amount),
|
|
||||||
})
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
return rows.ToDictionary(x => x.OrderId, x => x.Amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(List<OrderReceivablePayableRow> Items, int Total)> GetReceivablePayablePagedListAsync(
|
public async Task<(List<OrderReceivablePayableRow> Items, int Total)> GetReceivablePayablePagedListAsync(
|
||||||
int pageIndex,
|
int pageIndex,
|
||||||
int pageSize,
|
int pageSize,
|
||||||
|
|||||||
@ -15,9 +15,8 @@ public class FileStoragePathTests
|
|||||||
|
|
||||||
var keys = FileStoragePath.ExpandReadKeys(url);
|
var keys = FileStoragePath.ExpandReadKeys(url);
|
||||||
|
|
||||||
// NormalizeKey 已剥 PathBase,主 key 为 uploads/...;Expand 仍保留 admin/ 候选以兼容历史对象
|
|
||||||
keys.Should().Contain("uploads/prints/orders/IDE2610061059117673-20261006105912.pdf");
|
|
||||||
keys.Should().Contain("admin/uploads/prints/orders/IDE2610061059117673-20261006105912.pdf");
|
keys.Should().Contain("admin/uploads/prints/orders/IDE2610061059117673-20261006105912.pdf");
|
||||||
|
keys.Should().Contain("uploads/prints/orders/IDE2610061059117673-20261006105912.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -42,47 +41,6 @@ public class FileStoragePathTests
|
|||||||
.Be("uploads/prints/orders/IDE2610081704036507-20261008170404.pdf");
|
.Be("uploads/prints/orders/IDE2610081704036507-20261008170404.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ToStorageKey_ShouldStripPathBaseAdminFromCustomDomainUrl()
|
|
||||||
{
|
|
||||||
var url =
|
|
||||||
"https://cos.test.ideship.com/admin/uploads/prints/orders/IDE2610231052073356-20261023105208.pdf?q-sign-algorithm=sha1";
|
|
||||||
|
|
||||||
OmsObjectStorageExtensions.ToStorageKey(url)
|
|
||||||
.Should()
|
|
||||||
.Be("uploads/prints/orders/IDE2610231052073356-20261023105208.pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void NormalizeKey_ShouldCollapseDoubleAdminPathBase()
|
|
||||||
{
|
|
||||||
FileStoragePath.NormalizeKey("admin/admin/uploads/prints/orders/a.pdf")
|
|
||||||
.Should()
|
|
||||||
.Be("uploads/prints/orders/a.pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task GetPublicUrl_ShouldResignCustomDomainUrlWithoutDoubleAdmin()
|
|
||||||
{
|
|
||||||
var expired =
|
|
||||||
"https://cos.test.ideship.com/admin/uploads/prints/orders/IDE2610231052073356-20261023105208.pdf?q-sign-algorithm=sha1&q-sign-time=1;2";
|
|
||||||
const string fresh =
|
|
||||||
"https://cos.test.ideship.com/admin/uploads/prints/orders/IDE2610231052073356-20261023105208.pdf?q-sign-algorithm=sha1&fresh=1";
|
|
||||||
|
|
||||||
var storage = new Mock<IObjectStorage>(MockBehavior.Strict);
|
|
||||||
storage
|
|
||||||
.Setup(x => x.GetUrlAsync(
|
|
||||||
"uploads/prints/orders/IDE2610231052073356-20261023105208.pdf",
|
|
||||||
It.IsAny<TimeSpan?>(),
|
|
||||||
It.IsAny<CancellationToken>()))
|
|
||||||
.ReturnsAsync(fresh);
|
|
||||||
|
|
||||||
var url = await storage.Object.GetPublicUrlAsync(expired);
|
|
||||||
|
|
||||||
url.Should().Be(fresh);
|
|
||||||
storage.VerifyAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToStorageKey_ShouldKeepExternalAbsoluteUrl()
|
public void ToStorageKey_ShouldKeepExternalAbsoluteUrl()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,56 +0,0 @@
|
|||||||
using FluentAssertions;
|
|
||||||
using Oms.Application.Labeling.Hiprint;
|
|
||||||
|
|
||||||
namespace Oms.Tests;
|
|
||||||
|
|
||||||
public class LabelPdfFontTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void EnsureRegistered_ShouldLoadEmbeddedCjkFont()
|
|
||||||
{
|
|
||||||
var family = LabelPdfFontRegistrar.EnsureRegistered();
|
|
||||||
|
|
||||||
family.Should().Be(LabelPdfFontRegistrar.FamilyName);
|
|
||||||
LabelPdfFontRegistrar.RegisteredSource.Should().NotBeNullOrWhiteSpace();
|
|
||||||
LabelPdfFontRegistrar.RegisteredSource.Should().StartWith("embedded:");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Render_ShouldEmbedChineseWithoutThrowing()
|
|
||||||
{
|
|
||||||
LabelPdfFontRegistrar.EnsureRegistered().Should().NotBeNull();
|
|
||||||
|
|
||||||
const string templateJson = """
|
|
||||||
{
|
|
||||||
"panels": [{
|
|
||||||
"width": 100,
|
|
||||||
"height": 50,
|
|
||||||
"printElements": [{
|
|
||||||
"options": {
|
|
||||||
"left": 2,
|
|
||||||
"top": 2,
|
|
||||||
"width": 90,
|
|
||||||
"height": 20,
|
|
||||||
"title": "红米手机 摘要×25",
|
|
||||||
"fontSize": 12
|
|
||||||
},
|
|
||||||
"printElementType": { "type": "text" }
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var renderer = new QuestPdfLabelRenderer(new HiprintTemplateParser());
|
|
||||||
var pdf = await renderer.RenderAsync(
|
|
||||||
templateJson,
|
|
||||||
new Dictionary<string, object?> { ["skuSummary"] = "红米手机 512g×25" });
|
|
||||||
|
|
||||||
pdf.Should().NotBeNull();
|
|
||||||
pdf.Length.Should().BeGreaterThan(500);
|
|
||||||
// RegisterFontWithCustomName 后 PDF 仍写入真实 PostScript 名(如 SimHei)
|
|
||||||
var ascii = System.Text.Encoding.ASCII.GetString(pdf);
|
|
||||||
ascii.Should().Match(s => s.Contains("SimHei", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| s.Contains("OmsLabelSans", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| s.Contains("Noto", StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -49,59 +49,6 @@ public class HiprintTemplateParserTests
|
|||||||
act.Should().Throw<Exception>();
|
act.Should().Throw<Exception>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Parse_StaticCustomText_ShouldKeepTitleAndNotClipHeight()
|
|
||||||
{
|
|
||||||
const string json = """
|
|
||||||
{
|
|
||||||
"panels": [{
|
|
||||||
"width": 100,
|
|
||||||
"height": 50,
|
|
||||||
"printElements": [{
|
|
||||||
"options": {
|
|
||||||
"left": 10,
|
|
||||||
"top": 10,
|
|
||||||
"width": 80,
|
|
||||||
"height": 13,
|
|
||||||
"title": "箱子数量",
|
|
||||||
"fontSize": 10,
|
|
||||||
"hideTitle": true
|
|
||||||
},
|
|
||||||
"printElementType": { "type": "text", "title": "文本" }
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var page = new HiprintTemplateParser().Parse(json, new Dictionary<string, object?>());
|
|
||||||
var text = page.Ops.OfType<LabelTextOp>().Should().ContainSingle().Subject;
|
|
||||||
text.Text.Should().Be("箱子数量");
|
|
||||||
text.HeightPt.Should().BeGreaterThanOrEqualTo(14f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Parse_StaticTextFromTestData_WhenTitleEmpty()
|
|
||||||
{
|
|
||||||
const string json = """
|
|
||||||
{
|
|
||||||
"panels": [{
|
|
||||||
"width": 100,
|
|
||||||
"height": 50,
|
|
||||||
"printElements": [{
|
|
||||||
"options": {
|
|
||||||
"left": 1, "top": 1, "width": 50, "height": 16,
|
|
||||||
"testData": "仅测试数据文案"
|
|
||||||
},
|
|
||||||
"printElementType": { "type": "text" }
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var page = new HiprintTemplateParser().Parse(json, new Dictionary<string, object?>());
|
|
||||||
page.Ops.OfType<LabelTextOp>().Should().ContainSingle(x => x.Text == "仅测试数据文案");
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static string LoadFixture(string fileName)
|
internal static string LoadFixture(string fileName)
|
||||||
{
|
{
|
||||||
var dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
var dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
||||||
|
|||||||
@ -1,67 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using FluentAssertions;
|
|
||||||
using Moq;
|
|
||||||
using Oms.Application.Shared;
|
|
||||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
|
||||||
using Oms.Domain.OrdersAggregate.Entities;
|
|
||||||
using Oms.Domain.SystemAggregate.Repositories;
|
|
||||||
|
|
||||||
namespace Oms.Tests;
|
|
||||||
|
|
||||||
public class OrderDownstreamCreateOneBodyBuilderTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task BuildAsync_ShouldAlwaysSerializeProductIdFieldsIncludingNull()
|
|
||||||
{
|
|
||||||
var order = new Order
|
|
||||||
{
|
|
||||||
Id = 1,
|
|
||||||
ClientNo = "C1",
|
|
||||||
ServiceNo = "S1",
|
|
||||||
ChannelCode = "CH1",
|
|
||||||
ChannelForecastCode = "FC1",
|
|
||||||
};
|
|
||||||
var declarations = new List<OrderDeclaration>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
OrderId = 1,
|
|
||||||
CnName = "衬衫",
|
|
||||||
EnName = "Shirt",
|
|
||||||
UnitPrice = 1,
|
|
||||||
Quantity = 1,
|
|
||||||
ProductId = "P-1",
|
|
||||||
NonStdProductId = " ",
|
|
||||||
StdProductId = " 06901234567890 ",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var body = await OrderDownstreamCreateOneBodyBuilder.BuildAsync(
|
|
||||||
order,
|
|
||||||
sender: null,
|
|
||||||
recipient: null,
|
|
||||||
declarations,
|
|
||||||
weights: [],
|
|
||||||
new Mock<ICountryRepository>(MockBehavior.Loose).Object,
|
|
||||||
new Mock<IServiceChannelRepository>(MockBehavior.Loose).Object);
|
|
||||||
|
|
||||||
body.Items.Should().ContainSingle();
|
|
||||||
body.Items[0].ProductId.Should().Be("P-1");
|
|
||||||
body.Items[0].NonStdProductId.Should().BeNull();
|
|
||||||
body.Items[0].StdProductId.Should().Be("06901234567890");
|
|
||||||
|
|
||||||
// 与 ItfxForwardClient 一致:全局 WhenWritingNull,但三字段 JsonIgnore(Never) 仍写出 null
|
|
||||||
var json = JsonSerializer.Serialize(
|
|
||||||
body,
|
|
||||||
new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
||||||
});
|
|
||||||
|
|
||||||
json.Should().Contain("\"productId\":\"P-1\"");
|
|
||||||
json.Should().Contain("\"nonStdProductId\":null");
|
|
||||||
json.Should().Contain("\"stdProductId\":\"06901234567890\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -24,10 +24,6 @@ RUN dotnet publish "SimpleAdmin.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publ
|
|||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# 面单 QuestPDF 需 CJK 字体,否则中文 SKU 等渲染为 □
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends fonts-noto-cjk \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
COPY --from=build /app/publish .
|
COPY --from=build /app/publish .
|
||||||
# Local 存储落盘 wwwroot;非 root 用户需可写 uploads(Provider=Cos 时可不依赖此目录)
|
# Local 存储落盘 wwwroot;非 root 用户需可写 uploads(Provider=Cos 时可不依赖此目录)
|
||||||
RUN mkdir -p /app/wwwroot/uploads \
|
RUN mkdir -p /app/wwwroot/uploads \
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user