Commit 7af9544770b459692bb647ca46635870d8777d92

Authored by 杨鑫
2 parents 43d16ca6 5cb14694

Merge branch 'main' of http://39.98.150.180/wangming/Food-Labeling-Management-Platform

美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogGetListInputVo.cs
  1 +using Volo.Abp.Application.Dtos;
  2 +
1 namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; 3 namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
2 4
3 /// <summary> 5 /// <summary>
4 -/// App 打印日志分页(接口 10 6 +/// App 打印日志分页查询入参(仅当前登录账号 + 当前门店
5 /// </summary> 7 /// </summary>
6 -public class PrintLogGetListInputVo 8 +public class PrintLogGetListInputVo : PagedAndSortedResultRequestDto
7 { 9 {
8 - /// <summary>当前门店 Id</summary> 10 + /// <summary>
  11 + /// 当前门店 Id(location.Id,Guid 字符串)
  12 + /// </summary>
9 public string LocationId { get; set; } = string.Empty; 13 public string LocationId { get; set; } = string.Empty;
10 -  
11 - /// <summary>页码,从 1 开始</summary>  
12 - public int SkipCount { get; set; } = 1;  
13 -  
14 - /// <summary>每页条数</summary>  
15 - public int MaxResultCount { get; set; } = 20;  
16 } 14 }
  15 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogItemDto.cs
1 -using System.Collections.Generic;  
2 -  
3 namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; 1 namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
4 2
5 /// <summary> 3 /// <summary>
6 -/// 单条打印日志(接口 10) 4 +/// 打印日志列表项
7 /// </summary> 5 /// </summary>
8 public class PrintLogItemDto 6 public class PrintLogItemDto
9 { 7 {
  8 + /// <summary>任务Id(fl_label_print_task.Id)</summary>
10 public string TaskId { get; set; } = string.Empty; 9 public string TaskId { get; set; } = string.Empty;
11 10
12 - public string BatchId { get; set; } = string.Empty; 11 + /// <summary>批次Id(同一次点击 Print 共享)</summary>
  12 + public string? BatchId { get; set; }
13 13
  14 + /// <summary>第几份(从 1 开始)</summary>
14 public int CopyIndex { get; set; } 15 public int CopyIndex { get; set; }
15 16
  17 + /// <summary>标签Id</summary>
16 public string LabelId { get; set; } = string.Empty; 18 public string LabelId { get; set; } = string.Empty;
17 19
  20 + /// <summary>标签编码</summary>
18 public string LabelCode { get; set; } = string.Empty; 21 public string LabelCode { get; set; } = string.Empty;
19 22
  23 + /// <summary>产品Id</summary>
20 public string? ProductId { get; set; } 24 public string? ProductId { get; set; }
21 25
  26 + /// <summary>产品名称</summary>
22 public string ProductName { get; set; } = "无"; 27 public string ProductName { get; set; } = "无";
23 28
24 - public string PrintedAt { get; set; } = string.Empty; 29 + /// <summary>标签类型名称(来自 fl_label_type.TypeName)</summary>
  30 + public string TypeName { get; set; } = string.Empty;
25 31
26 - public string OperatorName { get; set; } = string.Empty; 32 + /// <summary>模板尺寸(来自 fl_label_template.Width/Height/Unit)</summary>
  33 + public string? LabelSizeText { get; set; }
27 34
28 - public string LocationName { get; set; } = string.Empty; 35 + /// <summary>本次任务落库的渲染模板 JSON(fl_label_print_task.RenderTemplateJson)</summary>
  36 + public string? RenderTemplateJson { get; set; }
29 37
30 - /// <summary>标签分类名(展示用,可为空)</summary>  
31 - public string? LabelCategoryName { get; set; } 38 + /// <summary>
  39 + /// 本次打印的内容快照(来自 fl_label_print_data,按 PrintTaskId 关联)
  40 + /// </summary>
  41 + public List<PrintLogDataItemDto> PrintDataList { get; set; } = new();
32 42
33 - /// <summary>标签幅面/模板摘要(如 2"x2" Basic)</summary>  
34 - public string? LabelTemplateSummary { get; set; } 43 + /// <summary>打印时间(PrintedAt ?? CreationTime)</summary>
  44 + public DateTime PrintedAt { get; set; }
35 45
36 - /// <summary>标签幅面文案(与列表/预览 labelSizeText 一致,如 2.00x2.00inch)</summary>  
37 - public string? LabelSizeText { get; set; } 46 + /// <summary>操作人姓名(当前登录账号 Name)</summary>
  47 + public string OperatorName { get; set; } = string.Empty;
  48 +
  49 + /// <summary>门店名称</summary>
  50 + public string LocationName { get; set; } = "无";
  51 +}
38 52
39 - /// <summary>标签种类名称(fl_label_type.TypeName)</summary>  
40 - public string? TypeName { get; set; } 53 +/// <summary>
  54 +/// 打印内容快照项(fl_label_print_data)
  55 +/// </summary>
  56 +public class PrintLogDataItemDto
  57 +{
  58 + public string ElementId { get; set; } = string.Empty;
41 59
42 - /// <summary>本次份打印内容元素快照(由 RenderDataJson 解析)</summary>  
43 - public List<PrintLogDataItemDto> PrintDataList { get; set; } = new(); 60 + public string? RenderValue { get; set; }
  61 +
  62 + public object? RenderConfigJson { get; set; }
44 } 63 }
  64 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintInputVo.cs
@@ -31,6 +31,12 @@ public class UsAppLabelPrintInputVo @@ -31,6 +31,12 @@ public class UsAppLabelPrintInputVo
31 public int PrintQuantity { get; set; } = 1; 31 public int PrintQuantity { get; set; } = 1;
32 32
33 /// <summary> 33 /// <summary>
  34 + /// 客户端幂等请求Id(可选)。
  35 + /// 同一个 clientRequestId 重复调用 print 接口时,后端会直接返回首次创建的 batchId/taskIds,不会重复写库。
  36 + /// </summary>
  37 + public string? ClientRequestId { get; set; }
  38 +
  39 + /// <summary>
34 /// 业务基准时间(用于 DATE/TIME 等元素的计算) 40 /// 业务基准时间(用于 DATE/TIME 等元素的计算)
35 /// </summary> 41 /// </summary>
36 public DateTime? BaseTime { get; set; } 42 public DateTime? BaseTime { get; set; }
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintOutputDto.cs
  1 +using System.Collections.Generic;
  2 +
1 namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; 3 namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
2 4
3 /// <summary> 5 /// <summary>
@@ -12,10 +14,5 @@ public class UsAppLabelPrintOutputDto @@ -12,10 +14,5 @@ public class UsAppLabelPrintOutputDto
12 public string? BatchId { get; set; } 14 public string? BatchId { get; set; }
13 15
14 public List<string> TaskIds { get; set; } = new(); 16 public List<string> TaskIds { get; set; } = new();
15 -  
16 - /// <summary>  
17 - /// 供 App 本地 BLE 重打:合并模板 JSON 字符串(与接口 9 落库的 printInputJson 同构,含 elements[])。  
18 - /// </summary>  
19 - public string? MergedTemplateJson { get; set; }  
20 } 17 }
21 18
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelReprintInputVo.cs
  1 +using System;
  2 +using System.Collections.Generic;
  3 +
1 namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; 4 namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
2 5
3 /// <summary> 6 /// <summary>
4 -/// App 重新打印入参(接口 11 7 +/// App 重新打印入参(根据历史任务Id重打
5 /// </summary> 8 /// </summary>
6 public class UsAppLabelReprintInputVo 9 public class UsAppLabelReprintInputVo
7 { 10 {
  11 + /// <summary>
  12 + /// 当前门店Id(用于权限校验,必须与历史任务一致)
  13 + /// </summary>
8 public string LocationId { get; set; } = string.Empty; 14 public string LocationId { get; set; } = string.Empty;
9 15
  16 + /// <summary>
  17 + /// 历史打印任务Id(fl_label_print_task.Id)
  18 + /// </summary>
10 public string TaskId { get; set; } = string.Empty; 19 public string TaskId { get; set; } = string.Empty;
11 20
  21 + /// <summary>
  22 + /// 重新打印份数(&lt;=0 则按 1 处理;默认 1)
  23 + /// </summary>
12 public int PrintQuantity { get; set; } = 1; 24 public int PrintQuantity { get; set; } = 1;
13 25
  26 + /// <summary>
  27 + /// 客户端幂等请求Id(可选)。
  28 + /// 同一个 clientRequestId 重复调用 reprint 接口时,后端会直接返回首次创建的 batchId/taskIds,不会重复写库。
  29 + /// </summary>
14 public string? ClientRequestId { get; set; } 30 public string? ClientRequestId { get; set; }
15 31
  32 + /// <summary>
  33 + /// 重新打印时可覆盖打印机Id(可选)
  34 + /// </summary>
16 public string? PrinterId { get; set; } 35 public string? PrinterId { get; set; }
17 36
  37 + /// <summary>
  38 + /// 重新打印时可覆盖打印机蓝牙 MAC(可选)
  39 + /// </summary>
18 public string? PrinterMac { get; set; } 40 public string? PrinterMac { get; set; }
19 41
  42 + /// <summary>
  43 + /// 重新打印时可覆盖打印机地址(可选)
  44 + /// </summary>
20 public string? PrinterAddress { get; set; } 45 public string? PrinterAddress { get; set; }
21 } 46 }
  47 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs
1 using FoodLabeling.Application.Contracts.Dtos.Common; 1 using FoodLabeling.Application.Contracts.Dtos.Common;
2 using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; 2 using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  3 +using FoodLabeling.Application.Contracts.Dtos.Common;
3 using Volo.Abp.Application.Services; 4 using Volo.Abp.Application.Services;
4 5
5 namespace FoodLabeling.Application.Contracts.IServices; 6 namespace FoodLabeling.Application.Contracts.IServices;
@@ -25,12 +26,12 @@ public interface IUsAppLabelingAppService : IApplicationService @@ -25,12 +26,12 @@ public interface IUsAppLabelingAppService : IApplicationService
25 Task<UsAppLabelPrintOutputDto> PrintAsync(UsAppLabelPrintInputVo input); 26 Task<UsAppLabelPrintOutputDto> PrintAsync(UsAppLabelPrintInputVo input);
26 27
27 /// <summary> 28 /// <summary>
28 - /// 接口 10:当前账号在当前门店的打印日志分页 29 + /// App 重新打印:根据历史任务Id重打(创建新任务与明细)
29 /// </summary> 30 /// </summary>
30 - Task<PagedResultWithPageDto<PrintLogItemDto>> GetPrintLogListAsync(PrintLogGetListInputVo input); 31 + Task<UsAppLabelPrintOutputDto> ReprintAsync(UsAppLabelReprintInputVo input);
31 32
32 /// <summary> 33 /// <summary>
33 - /// 接口 11:按历史任务重打并落库,返回新任务信息及可本地打印的模板 JSON 34 + /// App 打印日志:获取当前登录账号在当前门店打印的记录(分页,时间倒序)
34 /// </summary> 35 /// </summary>
35 - Task<UsAppLabelPrintOutputDto> ReprintAsync(UsAppLabelReprintInputVo input); 36 + Task<PagedResultWithPageDto<PrintLogItemDto>> GetPrintLogListAsync(PrintLogGetListInputVo input);
36 } 37 }
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelPrintDataDbEntity.cs
@@ -11,30 +11,14 @@ public class FlLabelPrintDataDbEntity @@ -11,30 +11,14 @@ public class FlLabelPrintDataDbEntity
11 [SugarColumn(IsPrimaryKey = true)] 11 [SugarColumn(IsPrimaryKey = true)]
12 public string Id { get; set; } = string.Empty; 12 public string Id { get; set; } = string.Empty;
13 13
14 - public bool IsDeleted { get; set; } 14 + public string PrintTaskId { get; set; } = string.Empty;
15 15
16 - public DateTime CreationTime { get; set; } 16 + public string ElementId { get; set; } = string.Empty;
17 17
18 - public string? CreatorId { get; set; } 18 + public string? ElementName { get; set; }
19 19
20 - public string? LastModifierId { get; set; } 20 + public string? RenderValue { get; set; }
21 21
22 - public DateTime? LastModificationTime { get; set; }  
23 -  
24 - public string ConcurrencyStamp { get; set; } = string.Empty;  
25 -  
26 - public string TaskId { get; set; } = string.Empty;  
27 -  
28 - public int? CopyIndex { get; set; }  
29 -  
30 - /// <summary>  
31 - /// 原始打印输入(json 字段,直接保存为字符串)  
32 - /// </summary>  
33 - public string? PrintInputJson { get; set; }  
34 -  
35 - /// <summary>  
36 - /// 解析后的可打印数据(建议保存为 json 字符串)  
37 - /// </summary>  
38 - public string? RenderDataJson { get; set; } 22 + public string? RenderConfigJson { get; set; }
39 } 23 }
40 24
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelPrintTaskDbEntity.cs
@@ -11,36 +11,44 @@ public class FlLabelPrintTaskDbEntity @@ -11,36 +11,44 @@ public class FlLabelPrintTaskDbEntity
11 [SugarColumn(IsPrimaryKey = true)] 11 [SugarColumn(IsPrimaryKey = true)]
12 public string Id { get; set; } = string.Empty; 12 public string Id { get; set; } = string.Empty;
13 13
14 - public bool IsDeleted { get; set; } 14 + public string? BatchId { get; set; }
15 15
16 - public DateTime CreationTime { get; set; }  
17 -  
18 - public string? CreatorId { get; set; } 16 + public int CopyIndex { get; set; } = 1;
19 17
20 - public string? LastModifierId { get; set; } 18 + public string? ClientRequestId { get; set; }
21 19
22 - public DateTime? LastModificationTime { get; set; } 20 + public string LabelId { get; set; } = string.Empty;
23 21
24 - public string ConcurrencyStamp { get; set; } = string.Empty;  
25 -  
26 - public string? LocationId { get; set; } 22 + public string TemplateId { get; set; } = string.Empty;
27 23
28 - public string? LabelCode { get; set; } 24 + public string? LabelTypeId { get; set; }
29 25
30 public string? ProductId { get; set; } 26 public string? ProductId { get; set; }
31 27
32 - public string? LabelTypeId { get; set; } 28 + public string? LocationId { get; set; }
  29 +
  30 + public DateTime? BaseTime { get; set; }
33 31
34 - public string? TemplateCode { get; set; } 32 + public string? PrintInputJson { get; set; }
35 33
36 - public int PrintQuantity { get; set; } 34 + public string? TemplateProductDefaultValuesJson { get; set; }
37 35
38 - public DateTime? BaseTime { get; set; } 36 + public string RenderTemplateJson { get; set; } = string.Empty;
39 37
40 public string? PrinterId { get; set; } 38 public string? PrinterId { get; set; }
41 39
42 public string? PrinterMac { get; set; } 40 public string? PrinterMac { get; set; }
43 41
44 public string? PrinterAddress { get; set; } 42 public string? PrinterAddress { get; set; }
  43 +
  44 + public string Status { get; set; } = "CREATED";
  45 +
  46 + public DateTime? PrintedAt { get; set; }
  47 +
  48 + public string? ErrorMessage { get; set; }
  49 +
  50 + public string? CreatedBy { get; set; }
  51 +
  52 + public DateTime CreationTime { get; set; }
45 } 53 }
46 54
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs
@@ -19,6 +19,7 @@ using Volo.Abp; @@ -19,6 +19,7 @@ using Volo.Abp;
19 using Volo.Abp.Application.Services; 19 using Volo.Abp.Application.Services;
20 using Volo.Abp.Guids; 20 using Volo.Abp.Guids;
21 using Volo.Abp.Uow; 21 using Volo.Abp.Uow;
  22 +using Yi.Framework.Rbac.Domain.Entities;
22 using Yi.Framework.SqlSugarCore.Abstractions; 23 using Yi.Framework.SqlSugarCore.Abstractions;
23 24
24 namespace FoodLabeling.Application.Services; 25 namespace FoodLabeling.Application.Services;
@@ -31,12 +32,18 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -31,12 +32,18 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
31 private readonly ISqlSugarDbContext _dbContext; 32 private readonly ISqlSugarDbContext _dbContext;
32 private readonly ILabelAppService _labelAppService; 33 private readonly ILabelAppService _labelAppService;
33 private readonly IGuidGenerator _guidGenerator; 34 private readonly IGuidGenerator _guidGenerator;
  35 + private readonly ISqlSugarRepository<UserAggregateRoot, Guid> _userRepository;
34 36
35 - public UsAppLabelingAppService(ISqlSugarDbContext dbContext, ILabelAppService labelAppService, IGuidGenerator guidGenerator) 37 + public UsAppLabelingAppService(
  38 + ISqlSugarDbContext dbContext,
  39 + ILabelAppService labelAppService,
  40 + IGuidGenerator guidGenerator,
  41 + ISqlSugarRepository<UserAggregateRoot, Guid> userRepository)
36 { 42 {
37 _dbContext = dbContext; 43 _dbContext = dbContext;
38 _labelAppService = labelAppService; 44 _labelAppService = labelAppService;
39 _guidGenerator = guidGenerator; 45 _guidGenerator = guidGenerator;
  46 + _userRepository = userRepository;
40 } 47 }
41 48
42 /// <summary> 49 /// <summary>
@@ -366,6 +373,28 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -366,6 +373,28 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
366 } 373 }
367 374
368 var quantity = input.PrintQuantity <= 0 ? 1 : input.PrintQuantity; 375 var quantity = input.PrintQuantity <= 0 ? 1 : input.PrintQuantity;
  376 + var clientRequestId = input.ClientRequestId?.Trim();
  377 + if (!string.IsNullOrWhiteSpace(clientRequestId))
  378 + {
  379 + // 幂等:同一个 clientRequestId 重复调用,直接返回首次创建的任务集合
  380 + var existed = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>()
  381 + .Where(x => x.ClientRequestId == clientRequestId)
  382 + .OrderBy(x => x.CopyIndex)
  383 + .ToListAsync();
  384 +
  385 + if (existed is not null && existed.Count > 0)
  386 + {
  387 + var existedBatchId = existed.First().BatchId;
  388 + var existedTaskIds = existed.Select(x => x.Id).ToList();
  389 + return new UsAppLabelPrintOutputDto
  390 + {
  391 + TaskId = existedTaskIds.FirstOrDefault() ?? string.Empty,
  392 + PrintQuantity = existedTaskIds.Count,
  393 + BatchId = existedBatchId,
  394 + TaskIds = existedTaskIds
  395 + };
  396 + }
  397 + }
369 398
370 // 校验 label + location,并补齐一些顶部字段用于任务表落库 399 // 校验 label + location,并补齐一些顶部字段用于任务表落库
371 var labelRow = await _dbContext.SqlSugarClient 400 var labelRow = await _dbContext.SqlSugarClient
@@ -377,9 +406,10 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -377,9 +406,10 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
377 .Where((l, t, tpl) => l.LabelCode == labelCode) 406 .Where((l, t, tpl) => l.LabelCode == labelCode)
378 .Select((l, t, tpl) => new 407 .Select((l, t, tpl) => new
379 { 408 {
  409 + l.Id,
380 l.LocationId, 410 l.LocationId,
381 l.LabelTypeId, 411 l.LabelTypeId,
382 - TemplateCode = tpl.TemplateCode 412 + l.TemplateId
383 }) 413 })
384 .FirstAsync(); 414 .FirstAsync();
385 415
@@ -393,114 +423,112 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -393,114 +423,112 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
393 throw new UserFriendlyException("该标签不属于当前门店"); 423 throw new UserFriendlyException("该标签不属于当前门店");
394 } 424 }
395 425
396 - string? printInputJsonStr = null;  
397 - string renderDataJsonStr;  
398 - var templateSnapshotOk = false; 426 + var previewProductId = await ResolvePreviewProductIdAsync(labelRow.Id, input.ProductId);
  427 + var normalizedPrintInput = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value);
399 428
400 - if (input.PrintInputJson.HasValue) 429 + // 解析模板 elements(与预览一致的渲染数据)
  430 + var resolvedTemplate = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo
401 { 431 {
402 - var piRoot = input.PrintInputJson.Value;  
403 - if (piRoot.ValueKind == JsonValueKind.Object  
404 - && piRoot.TryGetProperty("elements", out var elArr)  
405 - && elArr.ValueKind == JsonValueKind.Array)  
406 - {  
407 - // App 传入整份合并模板(与 label-template JSON 同构):落库 printInputJson / renderDataJson 均存同一份,供重打  
408 - printInputJsonStr = piRoot.GetRawText();  
409 - renderDataJsonStr = printInputJsonStr;  
410 - templateSnapshotOk = true;  
411 - }  
412 - } 432 + LabelCode = labelCode,
  433 + ProductId = previewProductId,
  434 + BaseTime = input.BaseTime,
  435 + PrintInputJson = normalizedPrintInput
  436 + });
413 437
414 - Dictionary<string, object?>? flatPrintInput = null;  
415 - if (!templateSnapshotOk && input.PrintInputJson.HasValue)  
416 - {  
417 - var piFlat = input.PrintInputJson.Value;  
418 - if (piFlat.ValueKind == JsonValueKind.Object)  
419 - {  
420 - try  
421 - {  
422 - flatPrintInput = JsonSerializer.Deserialize<Dictionary<string, object?>>(piFlat.GetRawText());  
423 - }  
424 - catch  
425 - {  
426 - flatPrintInput = null;  
427 - }  
428 - }  
429 - } 438 + var templateProductDefaultValuesJson = await ResolveTemplateProductDefaultValuesJsonAsync(
  439 + labelRow.TemplateId,
  440 + previewProductId,
  441 + labelRow.LabelTypeId);
430 442
431 - if (!templateSnapshotOk)  
432 - {  
433 - var resolvedTemplate = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo  
434 - {  
435 - LabelCode = labelCode,  
436 - ProductId = input.ProductId?.Trim(),  
437 - BaseTime = input.BaseTime,  
438 - PrintInputJson = flatPrintInput  
439 - });  
440 - renderDataJsonStr = JsonSerializer.Serialize(resolvedTemplate);  
441 - printInputJsonStr = input.PrintInputJson.HasValue  
442 - ? input.PrintInputJson.Value.GetRawText()  
443 - : null;  
444 - } 443 + var printInputJsonStr = input.PrintInputJson is null
  444 + ? null
  445 + : JsonSerializer.Serialize(input.PrintInputJson);
  446 + var renderTemplateJsonStr = JsonSerializer.Serialize(resolvedTemplate);
445 447
446 var now = DateTime.Now; 448 var now = DateTime.Now;
447 var currentUserId = CurrentUser?.Id?.ToString(); 449 var currentUserId = CurrentUser?.Id?.ToString();
448 - var taskId = _guidGenerator.Create().ToString(); 450 + var batchId = _guidGenerator.Create().ToString();
  451 + var taskIds = new List<string>();
449 452
450 - var task = new FlLabelPrintTaskDbEntity 453 + for (var i = 1; i <= quantity; i++)
451 { 454 {
452 - Id = taskId,  
453 - IsDeleted = false,  
454 - CreationTime = now,  
455 - CreatorId = currentUserId,  
456 - ConcurrencyStamp = _guidGenerator.Create().ToString("N"),  
457 - LocationId = locationId,  
458 - LabelCode = labelCode,  
459 - ProductId = input.ProductId?.Trim(),  
460 - LabelTypeId = labelRow.LabelTypeId,  
461 - TemplateCode = labelRow.TemplateCode,  
462 - PrintQuantity = quantity,  
463 - BaseTime = input.BaseTime,  
464 - PrinterId = input.PrinterId?.Trim(),  
465 - PrinterMac = input.PrinterMac?.Trim(),  
466 - PrinterAddress = input.PrinterAddress?.Trim()  
467 - }; 455 + var taskId = _guidGenerator.Create().ToString();
  456 + taskIds.Add(taskId);
468 457
469 - await _dbContext.SqlSugarClient.Insertable(task).ExecuteCommandAsync(); 458 + var task = new FlLabelPrintTaskDbEntity
  459 + {
  460 + Id = taskId,
  461 + BatchId = batchId,
  462 + CopyIndex = i,
  463 + ClientRequestId = string.IsNullOrWhiteSpace(clientRequestId) ? null : clientRequestId,
  464 + LabelId = labelRow.Id,
  465 + TemplateId = labelRow.TemplateId,
  466 + LabelTypeId = labelRow.LabelTypeId,
  467 + ProductId = previewProductId,
  468 + LocationId = locationId,
  469 + BaseTime = input.BaseTime,
  470 + PrintInputJson = printInputJsonStr,
  471 + TemplateProductDefaultValuesJson = templateProductDefaultValuesJson,
  472 + RenderTemplateJson = renderTemplateJsonStr,
  473 + PrinterId = input.PrinterId?.Trim(),
  474 + PrinterMac = input.PrinterMac?.Trim(),
  475 + PrinterAddress = input.PrinterAddress?.Trim(),
  476 + Status = "CREATED",
  477 + PrintedAt = null,
  478 + ErrorMessage = null,
  479 + CreatedBy = currentUserId,
  480 + CreationTime = now
  481 + };
470 482
471 - var dataRows = Enumerable.Range(1, quantity).Select(i => new FlLabelPrintDataDbEntity  
472 - {  
473 - Id = _guidGenerator.Create().ToString(),  
474 - IsDeleted = false,  
475 - CreationTime = now,  
476 - CreatorId = currentUserId,  
477 - ConcurrencyStamp = _guidGenerator.Create().ToString("N"),  
478 - TaskId = taskId,  
479 - CopyIndex = i,  
480 - PrintInputJson = printInputJsonStr,  
481 - RenderDataJson = renderDataJsonStr  
482 - }).ToList(); 483 + await _dbContext.SqlSugarClient.Insertable(task).ExecuteCommandAsync();
  484 +
  485 + var rows = resolvedTemplate.Elements.Select(e =>
  486 + {
  487 + var cfgJson = e.ConfigJson is null ? null : JsonSerializer.Serialize(e.ConfigJson);
  488 + string? renderValue = null;
  489 + if (e.ConfigJson is JsonElement je && je.ValueKind == JsonValueKind.Object && je.TryGetProperty("text", out var tv))
  490 + {
  491 + renderValue = tv.ValueKind == JsonValueKind.String ? tv.GetString() : tv.ToString();
  492 + }
  493 + else if (e.ConfigJson is Dictionary<string, object?> dict && dict.TryGetValue("text", out var v))
  494 + {
  495 + renderValue = v?.ToString();
  496 + }
483 497
484 - await _dbContext.SqlSugarClient.Insertable(dataRows).ExecuteCommandAsync(); 498 + return new FlLabelPrintDataDbEntity
  499 + {
  500 + Id = _guidGenerator.Create().ToString(),
  501 + PrintTaskId = taskId,
  502 + ElementId = e.Id?.Trim() ?? string.Empty,
  503 + ElementName = e.ElementName?.Trim(),
  504 + RenderValue = renderValue,
  505 + RenderConfigJson = cfgJson
  506 + };
  507 + }).Where(x => !string.IsNullOrWhiteSpace(x.ElementId)).ToList();
  508 +
  509 + if (rows.Count > 0)
  510 + {
  511 + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
  512 + }
  513 + }
485 514
486 return new UsAppLabelPrintOutputDto 515 return new UsAppLabelPrintOutputDto
487 { 516 {
488 - TaskId = taskId, 517 + TaskId = taskIds.FirstOrDefault() ?? string.Empty,
489 PrintQuantity = quantity, 518 PrintQuantity = quantity,
490 - BatchId = taskId,  
491 - TaskIds = new List<string> { taskId },  
492 - MergedTemplateJson = null 519 + BatchId = batchId,
  520 + TaskIds = taskIds
493 }; 521 };
494 } 522 }
495 523
496 /// <summary> 524 /// <summary>
497 - /// 接口 10:分页打印日志(当前用户 + 当前门店 525 + /// App 重新打印:根据历史任务Id重打(创建新任务与明细
498 /// </summary> 526 /// </summary>
499 [Authorize] 527 [Authorize]
500 - [HttpPost]  
501 - public virtual async Task<PagedResultWithPageDto<PrintLogItemDto>> GetPrintLogListAsync(PrintLogGetListInputVo input) 528 + [UnitOfWork]
  529 + public virtual async Task<UsAppLabelPrintOutputDto> ReprintAsync(UsAppLabelReprintInputVo input)
502 { 530 {
503 - if (input == null) 531 + if (input is null)
504 { 532 {
505 throw new UserFriendlyException("入参不能为空"); 533 throw new UserFriendlyException("入参不能为空");
506 } 534 }
@@ -511,399 +539,337 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -511,399 +539,337 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
511 throw new UserFriendlyException("门店Id不能为空"); 539 throw new UserFriendlyException("门店Id不能为空");
512 } 540 }
513 541
514 - var userId = CurrentUser.Id?.ToString();  
515 - if (string.IsNullOrWhiteSpace(userId)) 542 + var taskId = input.TaskId?.Trim();
  543 + if (string.IsNullOrWhiteSpace(taskId))
516 { 544 {
517 - throw new UserFriendlyException("未登录");  
518 - }  
519 -  
520 - var pageIndex = input.SkipCount <= 0 ? 1 : input.SkipCount;  
521 - var pageSize = input.MaxResultCount <= 0 ? 20 : Math.Min(input.MaxResultCount, 200);  
522 -  
523 - RefAsync<int> total = 0;  
524 - var dataRows = await _dbContext.SqlSugarClient  
525 - .Queryable<FlLabelPrintDataDbEntity, FlLabelPrintTaskDbEntity>((d, t) => d.TaskId == t.Id)  
526 - .Where((d, t) => !d.IsDeleted && !t.IsDeleted)  
527 - .Where((d, t) => t.CreatorId == userId && t.LocationId == locationId)  
528 - .OrderBy((d, t) => d.CreationTime, OrderByType.Desc)  
529 - .Select((d, t) => d)  
530 - .ToPageListAsync(pageIndex, pageSize, total);  
531 -  
532 - string? locationDisplayName = null;  
533 - if (Guid.TryParse(locationId, out var locGuid))  
534 - {  
535 - var locRows = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>()  
536 - .Where(x => x.Id == locGuid && !x.IsDeleted)  
537 - .Select(x => x.LocationName)  
538 - .Take(1)  
539 - .ToListAsync();  
540 - locationDisplayName = locRows.FirstOrDefault(); 545 + throw new UserFriendlyException("taskId不能为空");
541 } 546 }
542 547
543 - var operatorName = CurrentUser.Name?.Trim();  
544 - if (string.IsNullOrWhiteSpace(operatorName))  
545 - {  
546 - operatorName = CurrentUser.UserName?.Trim();  
547 - }  
548 - if (string.IsNullOrWhiteSpace(operatorName)) 548 + var quantity = input.PrintQuantity <= 0 ? 1 : input.PrintQuantity;
  549 + var clientRequestId = input.ClientRequestId?.Trim();
  550 + if (!string.IsNullOrWhiteSpace(clientRequestId))
549 { 551 {
550 - operatorName = "无";  
551 - }  
552 -  
553 - var taskIds = dataRows.Select(x => x.TaskId).Distinct().ToList();  
554 - var tasks = taskIds.Count == 0  
555 - ? new List<FlLabelPrintTaskDbEntity>()  
556 - : await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>()  
557 - .Where(t => taskIds.Contains(t.Id))  
558 - .ToListAsync();  
559 - var taskMap = tasks.ToDictionary(x => x.Id, x => x);  
560 -  
561 - var labelCodes = tasks.Select(t => t.LabelCode).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList();  
562 - var labels = labelCodes.Count == 0  
563 - ? new List<FlLabelDbEntity>()  
564 - : await _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>()  
565 - .Where(l => !l.IsDeleted && labelCodes.Contains(l.LabelCode))  
566 - .ToListAsync();  
567 - var labelByCode = labels.GroupBy(x => x.LabelCode).ToDictionary(g => g.Key, g => g.First());  
568 -  
569 - var categoryIds = labels  
570 - .Select(x => x.LabelCategoryId)  
571 - .Where(x => !string.IsNullOrWhiteSpace(x))  
572 - .Select(x => x!.Trim())  
573 - .Distinct()  
574 - .ToList();  
575 - var categories = categoryIds.Count == 0  
576 - ? new List<FlLabelCategoryDbEntity>()  
577 - : await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()  
578 - .Where(c => !c.IsDeleted && categoryIds.Contains(c.Id)) 552 + var existed = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>()
  553 + .Where(x => x.ClientRequestId == clientRequestId)
  554 + .OrderBy(x => x.CopyIndex)
579 .ToListAsync(); 555 .ToListAsync();
580 - var catMap = categories.ToDictionary(x => x.Id, x => x);  
581 -  
582 - var templateIds = labels  
583 - .Select(x => x.TemplateId)  
584 - .Where(x => !string.IsNullOrWhiteSpace(x))  
585 - .Select(x => x!.Trim())  
586 - .Distinct()  
587 - .ToList();  
588 - var templates = templateIds.Count == 0  
589 - ? new List<FlLabelTemplateDbEntity>()  
590 - : await _dbContext.SqlSugarClient.Queryable<FlLabelTemplateDbEntity>()  
591 - .Where(tpl => !tpl.IsDeleted && templateIds.Contains(tpl.Id))  
592 - .ToListAsync();  
593 - var tplMap = templates.ToDictionary(x => x.Id, x => x);  
594 -  
595 - var labelTypeIds = labels  
596 - .Select(x => x.LabelTypeId)  
597 - .Where(x => !string.IsNullOrWhiteSpace(x))  
598 - .Select(x => x!.Trim())  
599 - .Distinct()  
600 - .ToList();  
601 - var labelTypes = labelTypeIds.Count == 0  
602 - ? new List<FlLabelTypeDbEntity>()  
603 - : await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>()  
604 - .Where(lt => !lt.IsDeleted && labelTypeIds.Contains(lt.Id))  
605 - .ToListAsync();  
606 - var typeMap = labelTypes.ToDictionary(x => x.Id, x => x);  
607 -  
608 - var productIds = tasks  
609 - .Select(t => t.ProductId)  
610 - .Where(x => !string.IsNullOrWhiteSpace(x))  
611 - .Select(x => x!.Trim())  
612 - .Distinct()  
613 - .ToList();  
614 - var products = productIds.Count == 0  
615 - ? new List<FlProductDbEntity>()  
616 - : await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>()  
617 - .Where(p => !p.IsDeleted && productIds.Contains(p.Id))  
618 - .ToListAsync();  
619 - var prodMap = products.ToDictionary(x => x.Id, x => x);  
620 556
621 - var items = dataRows.Select(d =>  
622 - {  
623 - taskMap.TryGetValue(d.TaskId, out var t);  
624 - var lblId = "";  
625 - string? catName = null;  
626 - string? tplSummary = null;  
627 - string? labelSizeText = null;  
628 - string? typeName = null;  
629 - if (t != null && !string.IsNullOrWhiteSpace(t.LabelCode) && labelByCode.TryGetValue(t.LabelCode.Trim(), out var lbl)) 557 + if (existed is not null && existed.Count > 0)
630 { 558 {
631 - lblId = lbl.Id;  
632 - if (!string.IsNullOrWhiteSpace(lbl.LabelCategoryId) && catMap.TryGetValue(lbl.LabelCategoryId.Trim(), out var c))  
633 - {  
634 - catName = string.IsNullOrWhiteSpace(c.CategoryName) ? "无" : c.CategoryName.Trim();  
635 - }  
636 - if (!string.IsNullOrWhiteSpace(lbl.LabelTypeId) && typeMap.TryGetValue(lbl.LabelTypeId.Trim(), out var lt))  
637 - {  
638 - typeName = string.IsNullOrWhiteSpace(lt.TypeName) ? null : lt.TypeName.Trim();  
639 - }  
640 - if (!string.IsNullOrWhiteSpace(lbl.TemplateId) && tplMap.TryGetValue(lbl.TemplateId.Trim(), out var tpl)) 559 + var existedBatchId = existed.First().BatchId;
  560 + var existedTaskIds = existed.Select(x => x.Id).ToList();
  561 + return new UsAppLabelPrintOutputDto
641 { 562 {
642 - tplSummary = $"{tpl.Width}x{tpl.Height}{tpl.Unit} {tpl.TemplateName}".Trim();  
643 - labelSizeText = string.Format(  
644 - CultureInfo.InvariantCulture,  
645 - "{0:0.00}x{1:0.00}{2}",  
646 - tpl.Width,  
647 - tpl.Height,  
648 - tpl.Unit);  
649 - }  
650 - }  
651 -  
652 - var productName = "无";  
653 - if (t != null && !string.IsNullOrWhiteSpace(t.ProductId) && prodMap.TryGetValue(t.ProductId.Trim(), out var p))  
654 - {  
655 - productName = string.IsNullOrWhiteSpace(p.ProductName) ? "无" : p.ProductName.Trim(); 563 + TaskId = existedTaskIds.FirstOrDefault() ?? string.Empty,
  564 + PrintQuantity = existedTaskIds.Count,
  565 + BatchId = existedBatchId,
  566 + TaskIds = existedTaskIds
  567 + };
656 } 568 }
  569 + }
657 570
658 - return new PrintLogItemDto  
659 - {  
660 - TaskId = d.TaskId,  
661 - BatchId = d.TaskId,  
662 - CopyIndex = d.CopyIndex ?? 1,  
663 - LabelId = string.IsNullOrWhiteSpace(lblId) ? (t?.LabelCode ?? "") : lblId,  
664 - LabelCode = t?.LabelCode ?? "",  
665 - ProductId = t?.ProductId,  
666 - ProductName = productName,  
667 - PrintedAt = d.CreationTime.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),  
668 - OperatorName = operatorName,  
669 - LocationName = string.IsNullOrWhiteSpace(locationDisplayName) ? "无" : locationDisplayName!,  
670 - LabelCategoryName = catName,  
671 - LabelTemplateSummary = tplSummary,  
672 - LabelSizeText = labelSizeText,  
673 - TypeName = typeName,  
674 - PrintDataList = BuildPrintDataListFromRenderJson(d.RenderDataJson)  
675 - };  
676 - }).ToList();  
677 -  
678 - var totalCount = total.Value;  
679 - var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize);  
680 -  
681 - return new PagedResultWithPageDto<PrintLogItemDto> 571 + var currentUserId = CurrentUser?.Id?.ToString();
  572 + if (string.IsNullOrWhiteSpace(currentUserId))
682 { 573 {
683 - PageIndex = pageIndex,  
684 - PageSize = pageSize,  
685 - TotalCount = totalCount,  
686 - TotalPages = totalPages,  
687 - Items = items  
688 - };  
689 - } 574 + throw new UserFriendlyException("未登录");
  575 + }
690 576
691 - private static List<PrintLogDataItemDto> BuildPrintDataListFromRenderJson(string? renderDataJson)  
692 - {  
693 - var list = new List<PrintLogDataItemDto>();  
694 - if (string.IsNullOrWhiteSpace(renderDataJson)) 577 + var old = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>()
  578 + .FirstAsync(x => x.Id == taskId);
  579 + if (old is null)
695 { 580 {
696 - return list; 581 + throw new UserFriendlyException("打印任务不存在");
697 } 582 }
698 583
699 - try 584 + // 仅允许重打自己在当前门店的任务
  585 + if (!string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase))
700 { 586 {
701 - using var doc = JsonDocument.Parse(renderDataJson);  
702 - var root = doc.RootElement;  
703 - if (root.ValueKind == JsonValueKind.Object  
704 - && root.TryGetProperty("elements", out var elArr)  
705 - && elArr.ValueKind == JsonValueKind.Array)  
706 - {  
707 - foreach (var el in elArr.EnumerateArray())  
708 - {  
709 - if (el.ValueKind != JsonValueKind.Object)  
710 - {  
711 - continue;  
712 - }  
713 -  
714 - var id = el.TryGetProperty("id", out var idEl) ? idEl.GetString() ?? string.Empty : string.Empty;  
715 - list.Add(new PrintLogDataItemDto  
716 - {  
717 - ElementId = id,  
718 - RenderValue = string.Empty,  
719 - RenderConfigJson = el.Clone()  
720 - });  
721 - }  
722 -  
723 - if (list.Count > 0)  
724 - {  
725 - return list;  
726 - }  
727 - } 587 + throw new UserFriendlyException("无权限重打该任务");
728 } 588 }
729 - catch 589 + if (!string.Equals(old.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase))
730 { 590 {
731 - // 继续尝试强类型解析 591 + throw new UserFriendlyException("该任务不属于当前门店");
732 } 592 }
733 593
  594 + LabelTemplatePreviewDto? resolvedTemplate = null;
734 try 595 try
735 { 596 {
736 - var preview = JsonSerializer.Deserialize<LabelTemplatePreviewDto>(renderDataJson);  
737 - if (preview?.Elements == null || preview.Elements.Count == 0)  
738 - {  
739 - return list;  
740 - }  
741 -  
742 - foreach (var el in preview.Elements)  
743 - {  
744 - var elJson = JsonSerializer.SerializeToElement(el);  
745 - list.Add(new PrintLogDataItemDto  
746 - {  
747 - ElementId = el.Id ?? string.Empty,  
748 - RenderValue = GetElementRenderValue(el),  
749 - RenderConfigJson = elJson  
750 - });  
751 - } 597 + resolvedTemplate = JsonSerializer.Deserialize<LabelTemplatePreviewDto>(old.RenderTemplateJson);
752 } 598 }
753 catch 599 catch
754 { 600 {
755 - // 历史数据或非预览结构时忽略 601 + resolvedTemplate = null;
756 } 602 }
757 603
758 - return list;  
759 - } 604 + if (resolvedTemplate is null)
  605 + {
  606 + throw new UserFriendlyException("历史任务渲染快照解析失败,无法重打");
  607 + }
760 608
761 - private static string GetElementRenderValue(LabelTemplateElementDto el)  
762 - {  
763 - try 609 + var now = DateTime.Now;
  610 + var batchId = _guidGenerator.Create().ToString();
  611 + var taskIds = new List<string>();
  612 +
  613 + for (var i = 1; i <= quantity; i++)
764 { 614 {
765 - if (el.ConfigJson is JsonElement je) 615 + var newTaskId = _guidGenerator.Create().ToString();
  616 + taskIds.Add(newTaskId);
  617 +
  618 + var newTask = new FlLabelPrintTaskDbEntity
  619 + {
  620 + Id = newTaskId,
  621 + BatchId = batchId,
  622 + CopyIndex = i,
  623 + ClientRequestId = string.IsNullOrWhiteSpace(clientRequestId) ? null : clientRequestId,
  624 + LabelId = old.LabelId,
  625 + TemplateId = old.TemplateId,
  626 + LabelTypeId = old.LabelTypeId,
  627 + ProductId = old.ProductId,
  628 + LocationId = old.LocationId,
  629 + BaseTime = old.BaseTime,
  630 + PrintInputJson = old.PrintInputJson,
  631 + TemplateProductDefaultValuesJson = old.TemplateProductDefaultValuesJson,
  632 + RenderTemplateJson = old.RenderTemplateJson,
  633 + PrinterId = string.IsNullOrWhiteSpace(input.PrinterId) ? old.PrinterId : input.PrinterId.Trim(),
  634 + PrinterMac = string.IsNullOrWhiteSpace(input.PrinterMac) ? old.PrinterMac : input.PrinterMac.Trim(),
  635 + PrinterAddress = string.IsNullOrWhiteSpace(input.PrinterAddress) ? old.PrinterAddress : input.PrinterAddress.Trim(),
  636 + Status = "CREATED",
  637 + PrintedAt = null,
  638 + ErrorMessage = null,
  639 + CreatedBy = currentUserId,
  640 + CreationTime = now
  641 + };
  642 +
  643 + await _dbContext.SqlSugarClient.Insertable(newTask).ExecuteCommandAsync();
  644 +
  645 + var rows = resolvedTemplate.Elements.Select(e =>
766 { 646 {
767 - if (je.ValueKind == JsonValueKind.Object) 647 + var cfgJson = e.ConfigJson is null ? null : JsonSerializer.Serialize(e.ConfigJson);
  648 + string? renderValue = null;
  649 + if (e.ConfigJson is JsonElement je && je.ValueKind == JsonValueKind.Object && je.TryGetProperty("text", out var tv))
768 { 650 {
769 - if (je.TryGetProperty("text", out var t))  
770 - {  
771 - return t.GetString() ?? string.Empty;  
772 - }  
773 - if (je.TryGetProperty("Text", out var t2))  
774 - {  
775 - return t2.GetString() ?? string.Empty;  
776 - } 651 + renderValue = tv.ValueKind == JsonValueKind.String ? tv.GetString() : tv.ToString();
  652 + }
  653 + else if (e.ConfigJson is Dictionary<string, object?> dict && dict.TryGetValue("text", out var v))
  654 + {
  655 + renderValue = v?.ToString();
777 } 656 }
  657 +
  658 + return new FlLabelPrintDataDbEntity
  659 + {
  660 + Id = _guidGenerator.Create().ToString(),
  661 + PrintTaskId = newTaskId,
  662 + ElementId = e.Id?.Trim() ?? string.Empty,
  663 + ElementName = e.ElementName?.Trim(),
  664 + RenderValue = renderValue,
  665 + RenderConfigJson = cfgJson
  666 + };
  667 + }).Where(x => !string.IsNullOrWhiteSpace(x.ElementId)).ToList();
  668 +
  669 + if (rows.Count > 0)
  670 + {
  671 + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
778 } 672 }
779 } 673 }
780 - catch  
781 - {  
782 - // ignore  
783 - }  
784 674
785 - return string.Empty; 675 + return new UsAppLabelPrintOutputDto
  676 + {
  677 + TaskId = taskIds.FirstOrDefault() ?? string.Empty,
  678 + PrintQuantity = quantity,
  679 + BatchId = batchId,
  680 + TaskIds = taskIds
  681 + };
786 } 682 }
787 683
788 /// <summary> 684 /// <summary>
789 - /// 接口 11:按历史任务重打并落库,返回可本地打印的合并模板 JSON 685 + /// App 打印日志:获取当前登录账号在当前门店打印的记录(分页,时间倒序)
790 /// </summary> 686 /// </summary>
  687 + /// <remarks>
  688 + /// 仅返回满足:
  689 + /// - CreatedBy == CurrentUser.Id
  690 + /// - LocationId == input.LocationId
  691 + /// 的打印任务记录(fl_label_print_task)。
  692 + ///
  693 + /// 示例请求:
  694 + /// ```json
  695 + /// {
  696 + /// "locationId": "11111111-1111-1111-1111-111111111111",
  697 + /// "skipCount": 1,
  698 + /// "maxResultCount": 20
  699 + /// }
  700 + /// ```
  701 + ///
  702 + /// 参数说明:
  703 + /// - locationId: 当前门店 Id(必填)
  704 + /// - skipCount: 页码(从 1 开始,遵循本项目约定)
  705 + /// - maxResultCount: 每页条数
  706 + /// </remarks>
  707 + /// <param name="input">分页查询入参</param>
  708 + /// <returns>分页打印日志</returns>
  709 + /// <response code="200">成功</response>
  710 + /// <response code="400">参数错误/未登录</response>
  711 + /// <response code="500">服务器错误</response>
791 [Authorize] 712 [Authorize]
792 - [UnitOfWork]  
793 [HttpPost] 713 [HttpPost]
794 - public virtual async Task<UsAppLabelPrintOutputDto> ReprintAsync(UsAppLabelReprintInputVo input) 714 + public virtual async Task<PagedResultWithPageDto<PrintLogItemDto>> GetPrintLogListAsync(PrintLogGetListInputVo input)
795 { 715 {
796 - if (input == null) 716 + if (input is null)
797 { 717 {
798 throw new UserFriendlyException("入参不能为空"); 718 throw new UserFriendlyException("入参不能为空");
799 } 719 }
800 720
  721 + if (!CurrentUser.Id.HasValue)
  722 + {
  723 + throw new UserFriendlyException("用户未登录");
  724 + }
  725 +
801 var locationId = input.LocationId?.Trim(); 726 var locationId = input.LocationId?.Trim();
802 if (string.IsNullOrWhiteSpace(locationId)) 727 if (string.IsNullOrWhiteSpace(locationId))
803 { 728 {
804 throw new UserFriendlyException("门店Id不能为空"); 729 throw new UserFriendlyException("门店Id不能为空");
805 } 730 }
806 731
807 - var histTaskId = input.TaskId?.Trim();  
808 - if (string.IsNullOrWhiteSpace(histTaskId))  
809 - {  
810 - throw new UserFriendlyException("taskId不能为空");  
811 - } 732 + var currentUserIdStr = CurrentUser.Id.Value.ToString();
812 733
813 - var userId = CurrentUser.Id?.ToString();  
814 - if (string.IsNullOrWhiteSpace(userId))  
815 - {  
816 - throw new UserFriendlyException("未登录");  
817 - } 734 + var currentUser = await _userRepository.GetByIdAsync(CurrentUser.Id.Value);
  735 + var operatorName = currentUser?.Name?.Trim() ?? string.Empty;
818 736
819 - var taskRows = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>()  
820 - .Where(t => t.Id == histTaskId && !t.IsDeleted)  
821 - .Take(1)  
822 - .ToListAsync();  
823 - var task = taskRows.FirstOrDefault();  
824 - if (task == null) 737 + var locationName = "无";
  738 + if (Guid.TryParse(locationId, out var locationGuid))
825 { 739 {
826 - throw new UserFriendlyException("打印任务不存在"); 740 + var loc = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>()
  741 + .Where(x => !x.IsDeleted && x.Id == locationGuid)
  742 + .Select(x => new { x.LocationCode, x.LocationName })
  743 + .FirstAsync();
  744 + if (loc is not null)
  745 + {
  746 + var name = loc.LocationName?.Trim();
  747 + if (!string.IsNullOrWhiteSpace(name))
  748 + {
  749 + locationName = name;
  750 + }
  751 + }
827 } 752 }
828 753
829 - if (!string.Equals(task.CreatorId?.Trim(), userId, StringComparison.Ordinal))  
830 - {  
831 - throw new UserFriendlyException("无权操作该打印任务");  
832 - } 754 + RefAsync<int> total = 0;
833 755
834 - if (!string.Equals(task.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase))  
835 - {  
836 - throw new UserFriendlyException("该任务不属于当前门店");  
837 - } 756 + var query = _dbContext.SqlSugarClient
  757 + .Queryable<FlLabelPrintTaskDbEntity>()
  758 + .LeftJoin<FlLabelDbEntity>((t, l) => t.LabelId == l.Id)
  759 + .LeftJoin<FlProductDbEntity>((t, l, p) => t.ProductId == p.Id)
  760 + .LeftJoin<FlLabelTypeDbEntity>((t, l, p, lt) => t.LabelTypeId == lt.Id)
  761 + .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lt, tpl) => t.TemplateId == tpl.Id)
  762 + .Where((t, l, p, lt, tpl) => t.CreatedBy == currentUserIdStr && t.LocationId == locationId)
  763 + .OrderBy((t, l, p, lt, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc)
  764 + .OrderBy((t, l, p, lt, tpl) => t.CreationTime, OrderByType.Desc)
  765 + .Select((t, l, p, lt, tpl) => new
  766 + {
  767 + t.Id,
  768 + t.BatchId,
  769 + t.CopyIndex,
  770 + t.LabelId,
  771 + LabelCode = l.LabelCode,
  772 + t.ProductId,
  773 + ProductName = p.ProductName,
  774 + TypeName = lt.TypeName,
  775 + TemplateWidth = tpl.Width,
  776 + TemplateHeight = tpl.Height,
  777 + TemplateUnit = tpl.Unit,
  778 + t.RenderTemplateJson,
  779 + t.PrintedAt,
  780 + t.CreationTime
  781 + });
838 782
839 - var histDataRows = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintDataDbEntity>()  
840 - .Where(d => d.TaskId == histTaskId && !d.IsDeleted)  
841 - .OrderBy(d => d.CopyIndex)  
842 - .Take(1)  
843 - .ToListAsync();  
844 - var histData = histDataRows.FirstOrDefault();  
845 - if (histData == null)  
846 - {  
847 - throw new UserFriendlyException("打印明细不存在");  
848 - } 783 + var pageRows = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
849 784
850 - var mergedJson = !string.IsNullOrWhiteSpace(histData.PrintInputJson)  
851 - ? histData.PrintInputJson!  
852 - : histData.RenderDataJson;  
853 - if (string.IsNullOrWhiteSpace(mergedJson))  
854 - {  
855 - throw new UserFriendlyException("无法重打:历史打印数据为空");  
856 - } 785 + var taskIds = pageRows.Select(x => x.Id).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList();
857 786
858 - var qty = input.PrintQuantity <= 0 ? 1 : input.PrintQuantity; 787 + var dataRows = taskIds.Count == 0
  788 + ? new List<FlLabelPrintDataDbEntity>()
  789 + : await _dbContext.SqlSugarClient.Queryable<FlLabelPrintDataDbEntity>()
  790 + .Where(x => taskIds.Contains(x.PrintTaskId))
  791 + .ToListAsync();
859 792
860 - var now = DateTime.Now;  
861 - var newTaskId = _guidGenerator.Create().ToString();  
862 - var newTask = new FlLabelPrintTaskDbEntity  
863 - {  
864 - Id = newTaskId,  
865 - IsDeleted = false,  
866 - CreationTime = now,  
867 - CreatorId = userId,  
868 - ConcurrencyStamp = _guidGenerator.Create().ToString("N"),  
869 - LocationId = locationId,  
870 - LabelCode = task.LabelCode,  
871 - ProductId = task.ProductId,  
872 - LabelTypeId = task.LabelTypeId,  
873 - TemplateCode = task.TemplateCode,  
874 - PrintQuantity = qty,  
875 - BaseTime = task.BaseTime,  
876 - PrinterId = !string.IsNullOrWhiteSpace(input.PrinterId) ? input.PrinterId.Trim() : task.PrinterId,  
877 - PrinterMac = !string.IsNullOrWhiteSpace(input.PrinterMac) ? input.PrinterMac.Trim() : task.PrinterMac,  
878 - PrinterAddress = !string.IsNullOrWhiteSpace(input.PrinterAddress) ? input.PrinterAddress.Trim() : task.PrinterAddress  
879 - };  
880 - await _dbContext.SqlSugarClient.Insertable(newTask).ExecuteCommandAsync(); 793 + var dataMap = dataRows
  794 + .GroupBy(x => x.PrintTaskId)
  795 + .ToDictionary(
  796 + g => g.Key,
  797 + g => g.Select(d =>
  798 + {
  799 + object? cfg = null;
  800 + if (!string.IsNullOrWhiteSpace(d.RenderConfigJson))
  801 + {
  802 + try
  803 + {
  804 + cfg = JsonSerializer.Deserialize<object>(d.RenderConfigJson);
  805 + }
  806 + catch
  807 + {
  808 + cfg = null;
  809 + }
  810 + }
881 811
882 - var dataRows = Enumerable.Range(1, qty).Select(i => new FlLabelPrintDataDbEntity  
883 - {  
884 - Id = _guidGenerator.Create().ToString(),  
885 - IsDeleted = false,  
886 - CreationTime = now,  
887 - CreatorId = userId,  
888 - ConcurrencyStamp = _guidGenerator.Create().ToString("N"),  
889 - TaskId = newTaskId,  
890 - CopyIndex = i,  
891 - PrintInputJson = histData.PrintInputJson,  
892 - RenderDataJson = histData.RenderDataJson 812 + return new PrintLogDataItemDto
  813 + {
  814 + ElementId = d.ElementId,
  815 + RenderValue = d.RenderValue,
  816 + RenderConfigJson = cfg
  817 + };
  818 + }).ToList());
  819 +
  820 + var items = pageRows.Select(x => new PrintLogItemDto
  821 + {
  822 + TaskId = x.Id,
  823 + BatchId = x.BatchId,
  824 + CopyIndex = x.CopyIndex,
  825 + LabelId = x.LabelId,
  826 + LabelCode = x.LabelCode ?? string.Empty,
  827 + ProductId = x.ProductId,
  828 + ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(),
  829 + TypeName = x.TypeName ?? string.Empty,
  830 + LabelSizeText = FormatLabelSizeWithUnit(x.TemplateWidth, x.TemplateHeight, x.TemplateUnit),
  831 + RenderTemplateJson = x.RenderTemplateJson,
  832 + PrintDataList = dataMap.TryGetValue(x.Id, out var list) ? list : new List<PrintLogDataItemDto>(),
  833 + PrintedAt = x.PrintedAt ?? x.CreationTime,
  834 + OperatorName = operatorName,
  835 + LocationName = locationName
893 }).ToList(); 836 }).ToList();
894 837
895 - await _dbContext.SqlSugarClient.Insertable(dataRows).ExecuteCommandAsync(); 838 + var pageSize = input.MaxResultCount <= 0 ? items.Count : input.MaxResultCount;
  839 + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount);
  840 + var totalCount = (long)total;
  841 + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize);
896 842
897 - return new UsAppLabelPrintOutputDto 843 + return new PagedResultWithPageDto<PrintLogItemDto>
898 { 844 {
899 - TaskId = newTaskId,  
900 - PrintQuantity = qty,  
901 - BatchId = newTaskId,  
902 - TaskIds = new List<string> { newTaskId },  
903 - MergedTemplateJson = mergedJson 845 + PageIndex = pageIndex,
  846 + PageSize = pageSize,
  847 + TotalCount = totalCount,
  848 + TotalPages = totalPages,
  849 + Items = items
904 }; 850 };
905 } 851 }
906 852
  853 + private async Task<string?> ResolveTemplateProductDefaultValuesJsonAsync(
  854 + string templateId,
  855 + string? productId,
  856 + string labelTypeId)
  857 + {
  858 + if (string.IsNullOrWhiteSpace(templateId) || string.IsNullOrWhiteSpace(productId) || string.IsNullOrWhiteSpace(labelTypeId))
  859 + {
  860 + return null;
  861 + }
  862 +
  863 + var productDefault = await _dbContext.SqlSugarClient.Queryable<FlLabelTemplateProductDefaultDbEntity>()
  864 + .Where(x => x.TemplateId == templateId)
  865 + .Where(x => x.ProductId == productId)
  866 + .Where(x => x.LabelTypeId == labelTypeId)
  867 + .OrderBy(x => x.OrderNum)
  868 + .FirstAsync();
  869 +
  870 + return string.IsNullOrWhiteSpace(productDefault?.DefaultValuesJson) ? null : productDefault!.DefaultValuesJson;
  871 + }
  872 +
907 private ISugarQueryable<FlLabelProductDbEntity, FlLabelDbEntity, FlProductDbEntity, FlLabelCategoryDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity, FlProductCategoryDbEntity> BuildLabelingJoinQuery( 873 private ISugarQueryable<FlLabelProductDbEntity, FlLabelDbEntity, FlProductDbEntity, FlLabelCategoryDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity, FlProductCategoryDbEntity> BuildLabelingJoinQuery(
908 string locationId, 874 string locationId,
909 List<string> productIds, 875 List<string> productIds,
@@ -1032,4 +998,13 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -1032,4 +998,13 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
1032 ? $"{ws}\"x{hs}\"" 998 ? $"{ws}\"x{hs}\""
1033 : $"{ws}x{hs}{u}"; 999 : $"{ws}x{hs}{u}";
1034 } 1000 }
  1001 +
  1002 + private static string? FormatLabelSizeWithUnit(decimal w, decimal h, string unit)
  1003 + {
  1004 + var u = (unit ?? "inch").Trim().ToLowerInvariant();
  1005 + var ws = w.ToString(CultureInfo.InvariantCulture);
  1006 + var hs = h.ToString(CultureInfo.InvariantCulture);
  1007 + var normalizedUnit = u is "in" ? "inch" : u;
  1008 + return $"{ws}x{hs}{normalizedUnit}";
  1009 + }
1035 } 1010 }
项目相关文档/标签模块接口对接说明.md
@@ -907,6 +907,7 @@ curl -X POST &quot;http://localhost:19001/api/app/us-app-labeling/preview&quot; \ @@ -907,6 +907,7 @@ curl -X POST &quot;http://localhost:19001/api/app/us-app-labeling/preview&quot; \
907 | `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) | 907 | `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) |
908 | `productId` | string | 否 | 打印用产品Id;不传则默认取该标签绑定的第一个产品(用于模板解析) | 908 | `productId` | string | 否 | 打印用产品Id;不传则默认取该标签绑定的第一个产品(用于模板解析) |
909 | `printQuantity` | number | 否 | 打印份数;`<=0` 按 1 处理 | 909 | `printQuantity` | number | 否 | 打印份数;`<=0` 按 1 处理 |
  910 +| `clientRequestId` | string | 否 | 客户端幂等请求Id;同一个值重复调用会直接返回首次创建的 `batchId/taskIds`,避免重复写库 用法:前端/客户端每次点击 Print 生成一个稳定的 clientRequestId(比如 uuid) |
910 | `baseTime` | string | 否 | 业务基准时间(用于 DATE/TIME 元素计算) | 911 | `baseTime` | string | 否 | 业务基准时间(用于 DATE/TIME 元素计算) |
911 | `printInputJson` | object | 否 | 打印输入(用于模板 PRINT_INPUT 元素),key 建议与模板元素 `inputKey` 对齐 | 912 | `printInputJson` | object | 否 | 打印输入(用于模板 PRINT_INPUT 元素),key 建议与模板元素 `inputKey` 对齐 |
912 | `printerId` | string | 否 | 打印机Id(可选,用于追踪) | 913 | `printerId` | string | 否 | 打印机Id(可选,用于追踪) |
@@ -916,11 +917,11 @@ curl -X POST &quot;http://localhost:19001/api/app/us-app-labeling/preview&quot; \ @@ -916,11 +917,11 @@ curl -X POST &quot;http://localhost:19001/api/app/us-app-labeling/preview&quot; \
916 #### 数据落库说明 917 #### 数据落库说明
917 918
918 - **任务表**:`fl_label_print_task` 919 - **任务表**:`fl_label_print_task`
919 - - 插入 1 条任务记录(`locationId / labelCode / productId / labelTypeId / templateCode / printQuantity / baseTime / printer...` 等)。 920 + - **一份打印 = 一条任务**:当 `printQuantity = N` 时,后端会插入 **N 条任务记录**(同一次点击 Print 共享一个 `BatchId`,并记录 `CopyIndex=1..N`)。
  921 + - 任务表会保存:本次打印的输入、命中的模板默认值、以及整份 resolved 后的模板快照 JSON,便于追溯/重打。
920 - **明细表**:`fl_label_print_data` 922 - **明细表**:`fl_label_print_data`
921 - - 按 `printQuantity` 插入 N 条明细记录(`copyIndex = 1..N`)。  
922 - - `printInputJson`:保存本次打印的原始输入(JSON 字符串)。  
923 - - `renderDataJson`:保存本次解析后的模板预览结构(`LabelTemplatePreviewDto`,包含 resolved 后的 `elements[].config`),供追溯/重打使用。 923 + - **按组件写快照**:每个任务会按模板 `elements[]` 逐个插入明细记录(`ElementId/ElementName/RenderValue/RenderConfigJson`)。
  924 + - 适用于按组件维度审计/统计/追溯。
924 925
925 > 模板解析的数据源来自 `fl_label_template` + `fl_label_template_element`,与预览接口一致。 926 > 模板解析的数据源来自 `fl_label_template` + `fl_label_template_element`,与预览接口一致。
926 927
@@ -928,8 +929,11 @@ curl -X POST &quot;http://localhost:19001/api/app/us-app-labeling/preview&quot; \ @@ -928,8 +929,11 @@ curl -X POST &quot;http://localhost:19001/api/app/us-app-labeling/preview&quot; \
928 929
929 | 字段 | 类型 | 说明 | 930 | 字段 | 类型 | 说明 |
930 |---|---|---| 931 |---|---|---|
931 -| `taskId` | string | 打印任务Id(用于后续查询/重打/统计) | 932 +| `taskId` | string | 第 1 份打印任务Id(兼容旧逻辑) |
932 | `printQuantity` | number | 实际写入的份数 | 933 | `printQuantity` | number | 实际写入的份数 |
  934 +| `batchId` | string | 本次点击 Print 的批次Id |
  935 +| `taskIds` | string[] | 本次生成的所有任务Id(长度=printQuantity) |
  936 +
933 937
934 #### 错误与边界 938 #### 错误与边界
935 939
@@ -964,3 +968,123 @@ curl -X POST &quot;http://localhost:19001/api/app/us-app-labeling/print&quot; \ @@ -964,3 +968,123 @@ curl -X POST &quot;http://localhost:19001/api/app/us-app-labeling/print&quot; \
964 -d '{"locationId":"11111111-1111-1111-1111-111111111111","labelCode":"LBL_CHICKEN_DEFROST","productId":"22222222-2222-2222-2222-222222222222","printQuantity":2,"baseTime":"2026-03-26T10:30:00","printInputJson":{"price":"12.99"}}' 968 -d '{"locationId":"11111111-1111-1111-1111-111111111111","labelCode":"LBL_CHICKEN_DEFROST","productId":"22222222-2222-2222-2222-222222222222","printQuantity":2,"baseTime":"2026-03-26T10:30:00","printInputJson":{"price":"12.99"}}'
965 ``` 969 ```
966 970
  971 +---
  972 +
  973 +## 接口 10:App 打印日志(当前登录账号 + 当前门店)
  974 +
  975 +**场景**:移动端“打印记录/历史”页面。只展示**当前登录账号**在**当前门店**打印的记录,便于追溯/重打。
  976 +
  977 +### 10.1 分页获取打印日志
  978 +
  979 +#### HTTP
  980 +
  981 +- **方法**:`POST`(与本模块其它复杂入参接口一致;若与 Swagger 不一致,**以 Swagger 为准**)
  982 +- **路径**:`/api/app/us-app-labeling/get-print-log-list`
  983 +- **鉴权**:需要登录(`Authorization: Bearer ...`)
  984 +
  985 +#### 入参(Body:`PrintLogGetListInputVo`)
  986 +
  987 +> 本项目分页约定:`skipCount` 表示 **页码(从 1 开始)**,不是 0 基 offset。
  988 +
  989 +| 参数名(JSON) | 类型 | 必填 | 说明 |
  990 +|---|---|---|---|
  991 +| `locationId` | string | 是 | 当前门店Id(仅返回该门店记录) |
  992 +| `skipCount` | number | 否 | 页码,从 1 开始;默认 1 |
  993 +| `maxResultCount` | number | 否 | 每页条数;默认按后端/ABP 默认 |
  994 +
  995 +#### 过滤条件(后端固定逻辑)
  996 +
  997 +- `fl_label_print_task.CreatedBy == CurrentUser.Id`
  998 +- `fl_label_print_task.LocationId == locationId`
  999 +- 按时间倒序:`PrintedAt ?? CreationTime`(越新的越靠前)
  1000 +
  1001 +#### 出参(`PagedResultWithPageDto<PrintLogItemDto>`)
  1002 +
  1003 +| 字段 | 类型 | 说明 |
  1004 +|---|---|---|
  1005 +| `pageIndex` | number | 当前页码(从 1 开始) |
  1006 +| `pageSize` | number | 每页条数 |
  1007 +| `totalCount` | number | 总条数 |
  1008 +| `totalPages` | number | 总页数 |
  1009 +| `items` | PrintLogItemDto[] | 列表 |
  1010 +
  1011 +`PrintLogItemDto`:
  1012 +
  1013 +| 字段 | 类型 | 说明 |
  1014 +|---|---|---|
  1015 +| `taskId` | string | 任务Id(fl_label_print_task.Id) |
  1016 +| `batchId` | string | 批次Id(同一次点击 Print 共享) |
  1017 +| `copyIndex` | number | 第几份(从 1 开始) |
  1018 +| `labelId` | string | 标签Id |
  1019 +| `labelCode` | string | 标签编码(来自 fl_label.LabelCode) |
  1020 +| `productId` | string | 产品Id |
  1021 +| `productName` | string | 产品名(来自 fl_product.ProductName;无则 “无”) |
  1022 +| `typeName` | string | 标签类型名称(来自 fl_label_type.TypeName) |
  1023 +| `labelSizeText` | string | 模板尺寸(宽高+单位,如 `2.00x2.00inch` / `6.00x4.00cm`) |
  1024 +| `renderTemplateJson` | string | 本次任务落库的渲染模板 JSON(`fl_label_print_task.RenderTemplateJson`) |
  1025 +| `printDataList` | PrintLogDataItemDto[] | 本次打印内容快照(来自 fl_label_print_data,按 taskId 关联) |
  1026 +| `printedAt` | string | 打印时间(PrintedAt ?? CreationTime) |
  1027 +| `operatorName` | string | 操作人姓名(当前登录账号 Name) |
  1028 +| `locationName` | string | 门店名称 |
  1029 +
  1030 +`PrintLogDataItemDto`:
  1031 +
  1032 +| 字段 | 类型 | 说明 |
  1033 +|---|---|---|
  1034 +| `elementId` | string | 模板组件Id(fl_label_print_data.ElementId) |
  1035 +| `renderValue` | string | 最终渲染值(fl_label_print_data.RenderValue) |
  1036 +| `renderConfigJson` | object | 最终渲染配置(fl_label_print_data.RenderConfigJson 反序列化) |
  1037 +
  1038 +#### curl
  1039 +
  1040 +```bash
  1041 +curl -X POST "http://localhost:19001/api/app/us-app-labeling/get-print-log-list" \
  1042 + -H "Authorization: <data.token>" \
  1043 + -H "Content-Type: application/json" \
  1044 + -d '{"locationId":"11111111-1111-1111-1111-111111111111","skipCount":1,"maxResultCount":20}'
  1045 +```
  1046 +
  1047 +---
  1048 +
  1049 +## 接口 11:App 重新打印(根据任务Id重打)
  1050 +
  1051 +**场景**:移动端“打印记录/历史”页面点击 **Reprint**。后端根据历史任务 `taskId` 创建一批新的打印任务与明细。
  1052 +
  1053 +### 11.1 重打
  1054 +
  1055 +#### HTTP
  1056 +
  1057 +- **方法**:`POST`
  1058 +- **路径**:`/api/app/us-app-labeling/reprint`(若与 Swagger 不一致,**以 Swagger 为准**)
  1059 +- **鉴权**:需要登录(`Authorization: Bearer ...`)
  1060 +
  1061 +#### 入参(Body:`UsAppLabelReprintInputVo`)
  1062 +
  1063 +| 参数名(JSON) | 类型 | 必填 | 说明 |
  1064 +|---|---|---|---|
  1065 +| `locationId` | string | 是 | 当前门店Id(后端校验历史任务必须属于该门店) |
  1066 +| `taskId` | string | 是 | 历史打印任务Id(`fl_label_print_task.Id`) |
  1067 +| `printQuantity` | number | 否 | 重新打印份数;`<=0` 按 1 处理;默认 1 |
  1068 +| `clientRequestId` | string | 否 | 客户端幂等请求Id;同一个值重复调用会直接返回首次创建的 `batchId/taskIds`,避免重复写库 |
  1069 +| `printerId` | string | 否 | 可选,覆盖历史任务的打印机Id |
  1070 +| `printerMac` | string | 否 | 可选,覆盖历史任务的打印机MAC |
  1071 +| `printerAddress` | string | 否 | 可选,覆盖历史任务的打印机地址 |
  1072 +
  1073 +#### 权限校验(后端固定逻辑)
  1074 +
  1075 +- 历史任务必须满足:`CreatedBy == CurrentUser.Id`
  1076 +- 且 `LocationId == locationId`
  1077 +
  1078 +#### 出参(`UsAppLabelPrintOutputDto`)
  1079 +
  1080 +字段与接口 9 一致:`taskId / printQuantity / batchId / taskIds`
  1081 +
  1082 +#### curl
  1083 +
  1084 +```bash
  1085 +curl -X POST "http://localhost:19001/api/app/us-app-labeling/reprint" \
  1086 + -H "Authorization: <data.token>" \
  1087 + -H "Content-Type: application/json" \
  1088 + -d '{"locationId":"11111111-1111-1111-1111-111111111111","taskId":"3a205389-78dd-4750-51ab-720344c9f607","printQuantity":1}'
  1089 +```
  1090 +