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 3 namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
2 4  
3 5 /// <summary>
4   -/// App 打印日志分页(接口 10
  6 +/// App 打印日志分页查询入参(仅当前登录账号 + 当前门店
5 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 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 1 namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
4 2  
5 3 /// <summary>
6   -/// 单条打印日志(接口 10)
  4 +/// 打印日志列表项
7 5 /// </summary>
8 6 public class PrintLogItemDto
9 7 {
  8 + /// <summary>任务Id(fl_label_print_task.Id)</summary>
10 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 15 public int CopyIndex { get; set; }
15 16  
  17 + /// <summary>标签Id</summary>
16 18 public string LabelId { get; set; } = string.Empty;
17 19  
  20 + /// <summary>标签编码</summary>
18 21 public string LabelCode { get; set; } = string.Empty;
19 22  
  23 + /// <summary>产品Id</summary>
20 24 public string? ProductId { get; set; }
21 25  
  26 + /// <summary>产品名称</summary>
22 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 31 public int PrintQuantity { get; set; } = 1;
32 32  
33 33 /// <summary>
  34 + /// 客户端幂等请求Id(可选)。
  35 + /// 同一个 clientRequestId 重复调用 print 接口时,后端会直接返回首次创建的 batchId/taskIds,不会重复写库。
  36 + /// </summary>
  37 + public string? ClientRequestId { get; set; }
  38 +
  39 + /// <summary>
34 40 /// 业务基准时间(用于 DATE/TIME 等元素的计算)
35 41 /// </summary>
36 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 3 namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
2 4  
3 5 /// <summary>
... ... @@ -12,10 +14,5 @@ public class UsAppLabelPrintOutputDto
12 14 public string? BatchId { get; set; }
13 15  
14 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 4 namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
2 5  
3 6 /// <summary>
4   -/// App 重新打印入参(接口 11
  7 +/// App 重新打印入参(根据历史任务Id重打
5 8 /// </summary>
6 9 public class UsAppLabelReprintInputVo
7 10 {
  11 + /// <summary>
  12 + /// 当前门店Id(用于权限校验,必须与历史任务一致)
  13 + /// </summary>
8 14 public string LocationId { get; set; } = string.Empty;
9 15  
  16 + /// <summary>
  17 + /// 历史打印任务Id(fl_label_print_task.Id)
  18 + /// </summary>
10 19 public string TaskId { get; set; } = string.Empty;
11 20  
  21 + /// <summary>
  22 + /// 重新打印份数(&lt;=0 则按 1 处理;默认 1)
  23 + /// </summary>
12 24 public int PrintQuantity { get; set; } = 1;
13 25  
  26 + /// <summary>
  27 + /// 客户端幂等请求Id(可选)。
  28 + /// 同一个 clientRequestId 重复调用 reprint 接口时,后端会直接返回首次创建的 batchId/taskIds,不会重复写库。
  29 + /// </summary>
14 30 public string? ClientRequestId { get; set; }
15 31  
  32 + /// <summary>
  33 + /// 重新打印时可覆盖打印机Id(可选)
  34 + /// </summary>
16 35 public string? PrinterId { get; set; }
17 36  
  37 + /// <summary>
  38 + /// 重新打印时可覆盖打印机蓝牙 MAC(可选)
  39 + /// </summary>
18 40 public string? PrinterMac { get; set; }
19 41  
  42 + /// <summary>
  43 + /// 重新打印时可覆盖打印机地址(可选)
  44 + /// </summary>
20 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 1 using FoodLabeling.Application.Contracts.Dtos.Common;
2 2 using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  3 +using FoodLabeling.Application.Contracts.Dtos.Common;
3 4 using Volo.Abp.Application.Services;
4 5  
5 6 namespace FoodLabeling.Application.Contracts.IServices;
... ... @@ -25,12 +26,12 @@ public interface IUsAppLabelingAppService : IApplicationService
25 26 Task<UsAppLabelPrintOutputDto> PrintAsync(UsAppLabelPrintInputVo input);
26 27  
27 28 /// <summary>
28   - /// 接口 10:当前账号在当前门店的打印日志分页
  29 + /// App 重新打印:根据历史任务Id重打(创建新任务与明细)
29 30 /// </summary>
30   - Task<PagedResultWithPageDto<PrintLogItemDto>> GetPrintLogListAsync(PrintLogGetListInputVo input);
  31 + Task<UsAppLabelPrintOutputDto> ReprintAsync(UsAppLabelReprintInputVo input);
31 32  
32 33 /// <summary>
33   - /// 接口 11:按历史任务重打并落库,返回新任务信息及可本地打印的模板 JSON
  34 + /// App 打印日志:获取当前登录账号在当前门店打印的记录(分页,时间倒序)
34 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 11 [SugarColumn(IsPrimaryKey = true)]
12 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 11 [SugarColumn(IsPrimaryKey = true)]
12 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 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 38 public string? PrinterId { get; set; }
41 39  
42 40 public string? PrinterMac { get; set; }
43 41  
44 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 19 using Volo.Abp.Application.Services;
20 20 using Volo.Abp.Guids;
21 21 using Volo.Abp.Uow;
  22 +using Yi.Framework.Rbac.Domain.Entities;
22 23 using Yi.Framework.SqlSugarCore.Abstractions;
23 24  
24 25 namespace FoodLabeling.Application.Services;
... ... @@ -31,12 +32,18 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
31 32 private readonly ISqlSugarDbContext _dbContext;
32 33 private readonly ILabelAppService _labelAppService;
33 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 43 _dbContext = dbContext;
38 44 _labelAppService = labelAppService;
39 45 _guidGenerator = guidGenerator;
  46 + _userRepository = userRepository;
40 47 }
41 48  
42 49 /// <summary>
... ... @@ -366,6 +373,28 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
366 373 }
367 374  
368 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 399 // 校验 label + location,并补齐一些顶部字段用于任务表落库
371 400 var labelRow = await _dbContext.SqlSugarClient
... ... @@ -377,9 +406,10 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
377 406 .Where((l, t, tpl) => l.LabelCode == labelCode)
378 407 .Select((l, t, tpl) => new
379 408 {
  409 + l.Id,
380 410 l.LocationId,
381 411 l.LabelTypeId,
382   - TemplateCode = tpl.TemplateCode
  412 + l.TemplateId
383 413 })
384 414 .FirstAsync();
385 415  
... ... @@ -393,114 +423,112 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
393 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 448 var now = DateTime.Now;
447 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 515 return new UsAppLabelPrintOutputDto
487 516 {
488   - TaskId = taskId,
  517 + TaskId = taskIds.FirstOrDefault() ?? string.Empty,
489 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 524 /// <summary>
497   - /// 接口 10:分页打印日志(当前用户 + 当前门店
  525 + /// App 重新打印:根据历史任务Id重打(创建新任务与明细
498 526 /// </summary>
499 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 533 throw new UserFriendlyException("入参不能为空");
506 534 }
... ... @@ -511,399 +539,337 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
511 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 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 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 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 684 /// <summary>
789   - /// 接口 11:按历史任务重打并落库,返回可本地打印的合并模板 JSON
  685 + /// App 打印日志:获取当前登录账号在当前门店打印的记录(分页,时间倒序)
790 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 712 [Authorize]
792   - [UnitOfWork]
793 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 718 throw new UserFriendlyException("入参不能为空");
799 719 }
800 720  
  721 + if (!CurrentUser.Id.HasValue)
  722 + {
  723 + throw new UserFriendlyException("用户未登录");
  724 + }
  725 +
801 726 var locationId = input.LocationId?.Trim();
802 727 if (string.IsNullOrWhiteSpace(locationId))
803 728 {
804 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 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 873 private ISugarQueryable<FlLabelProductDbEntity, FlLabelDbEntity, FlProductDbEntity, FlLabelCategoryDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity, FlProductCategoryDbEntity> BuildLabelingJoinQuery(
908 874 string locationId,
909 875 List<string> productIds,
... ... @@ -1032,4 +998,13 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
1032 998 ? $"{ws}\"x{hs}\""
1033 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 907 | `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) |
908 908 | `productId` | string | 否 | 打印用产品Id;不传则默认取该标签绑定的第一个产品(用于模板解析) |
909 909 | `printQuantity` | number | 否 | 打印份数;`<=0` 按 1 处理 |
  910 +| `clientRequestId` | string | 否 | 客户端幂等请求Id;同一个值重复调用会直接返回首次创建的 `batchId/taskIds`,避免重复写库 用法:前端/客户端每次点击 Print 生成一个稳定的 clientRequestId(比如 uuid) |
910 911 | `baseTime` | string | 否 | 业务基准时间(用于 DATE/TIME 元素计算) |
911 912 | `printInputJson` | object | 否 | 打印输入(用于模板 PRINT_INPUT 元素),key 建议与模板元素 `inputKey` 对齐 |
912 913 | `printerId` | string | 否 | 打印机Id(可选,用于追踪) |
... ... @@ -916,11 +917,11 @@ curl -X POST &quot;http://localhost:19001/api/app/us-app-labeling/preview&quot; \
916 917 #### 数据落库说明
917 918  
918 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 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 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 929  
929 930 | 字段 | 类型 | 说明 |
930 931 |---|---|---|
931   -| `taskId` | string | 打印任务Id(用于后续查询/重打/统计) |
  932 +| `taskId` | string | 第 1 份打印任务Id(兼容旧逻辑) |
932 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 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 +
... ...