Commit ca114eb993958d591582b9c6952660a8f559e752

Authored by 李曜臣
1 parent c304e27c

打印日志接口实现

美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogGetListInputVo.cs 0 → 100644
  1 +using Volo.Abp.Application.Dtos;
  2 +
  3 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  4 +
  5 +/// <summary>
  6 +/// App 打印日志分页查询入参(仅当前登录账号 + 当前门店)
  7 +/// </summary>
  8 +public class PrintLogGetListInputVo : PagedAndSortedResultRequestDto
  9 +{
  10 + /// <summary>
  11 + /// 当前门店 Id(location.Id,Guid 字符串)
  12 + /// </summary>
  13 + public string LocationId { get; set; } = string.Empty;
  14 +}
  15 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogItemDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  2 +
  3 +/// <summary>
  4 +/// 打印日志列表项
  5 +/// </summary>
  6 +public class PrintLogItemDto
  7 +{
  8 + /// <summary>任务Id(fl_label_print_task.Id)</summary>
  9 + public string TaskId { get; set; } = string.Empty;
  10 +
  11 + /// <summary>批次Id(同一次点击 Print 共享)</summary>
  12 + public string? BatchId { get; set; }
  13 +
  14 + /// <summary>第几份(从 1 开始)</summary>
  15 + public int CopyIndex { get; set; }
  16 +
  17 + /// <summary>标签Id</summary>
  18 + public string LabelId { get; set; } = string.Empty;
  19 +
  20 + /// <summary>标签编码</summary>
  21 + public string LabelCode { get; set; } = string.Empty;
  22 +
  23 + /// <summary>产品Id</summary>
  24 + public string? ProductId { get; set; }
  25 +
  26 + /// <summary>产品名称</summary>
  27 + public string ProductName { get; set; } = "无";
  28 +
  29 + /// <summary>标签类型名称(来自 fl_label_type.TypeName)</summary>
  30 + public string TypeName { get; set; } = string.Empty;
  31 +
  32 + /// <summary>模板尺寸(来自 fl_label_template.Width/Height/Unit)</summary>
  33 + public string? LabelSizeText { get; set; }
  34 +
  35 + /// <summary>打印时间(PrintedAt ?? CreationTime)</summary>
  36 + public DateTime PrintedAt { get; set; }
  37 +
  38 + /// <summary>操作人姓名(当前登录账号 Name)</summary>
  39 + public string OperatorName { get; set; } = string.Empty;
  40 +
  41 + /// <summary>门店名称</summary>
  42 + public string LocationName { get; set; } = "无";
  43 +}
  44 +
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelReprintInputVo.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +
  4 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  5 +
  6 +/// <summary>
  7 +/// App 重新打印入参(根据历史任务Id重打)
  8 +/// </summary>
  9 +public class UsAppLabelReprintInputVo
  10 +{
  11 + /// <summary>
  12 + /// 当前门店Id(用于权限校验,必须与历史任务一致)
  13 + /// </summary>
  14 + public string LocationId { get; set; } = string.Empty;
  15 +
  16 + /// <summary>
  17 + /// 历史打印任务Id(fl_label_print_task.Id)
  18 + /// </summary>
  19 + public string TaskId { get; set; } = string.Empty;
  20 +
  21 + /// <summary>
  22 + /// 重新打印份数(&lt;=0 则按 1 处理;默认 1)
  23 + /// </summary>
  24 + public int PrintQuantity { get; set; } = 1;
  25 +
  26 + /// <summary>
  27 + /// 客户端幂等请求Id(可选)。
  28 + /// 同一个 clientRequestId 重复调用 reprint 接口时,后端会直接返回首次创建的 batchId/taskIds,不会重复写库。
  29 + /// </summary>
  30 + public string? ClientRequestId { get; set; }
  31 +
  32 + /// <summary>
  33 + /// 重新打印时可覆盖打印机Id(可选)
  34 + /// </summary>
  35 + public string? PrinterId { get; set; }
  36 +
  37 + /// <summary>
  38 + /// 重新打印时可覆盖打印机蓝牙 MAC(可选)
  39 + /// </summary>
  40 + public string? PrinterMac { get; set; }
  41 +
  42 + /// <summary>
  43 + /// 重新打印时可覆盖打印机地址(可选)
  44 + /// </summary>
  45 + public string? PrinterAddress { get; set; }
  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.UsAppLabeling; 1 using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  2 +using FoodLabeling.Application.Contracts.Dtos.Common;
2 using Volo.Abp.Application.Services; 3 using Volo.Abp.Application.Services;
3 4
4 namespace FoodLabeling.Application.Contracts.IServices; 5 namespace FoodLabeling.Application.Contracts.IServices;
@@ -22,4 +23,14 @@ public interface IUsAppLabelingAppService : IApplicationService @@ -22,4 +23,14 @@ public interface IUsAppLabelingAppService : IApplicationService
22 /// App 打印:创建打印任务并落库打印明细(fl_label_print_task / fl_label_print_data) 23 /// App 打印:创建打印任务并落库打印明细(fl_label_print_task / fl_label_print_data)
23 /// </summary> 24 /// </summary>
24 Task<UsAppLabelPrintOutputDto> PrintAsync(UsAppLabelPrintInputVo input); 25 Task<UsAppLabelPrintOutputDto> PrintAsync(UsAppLabelPrintInputVo input);
  26 +
  27 + /// <summary>
  28 + /// App 重新打印:根据历史任务Id重打(创建新任务与明细)
  29 + /// </summary>
  30 + Task<UsAppLabelPrintOutputDto> ReprintAsync(UsAppLabelReprintInputVo input);
  31 +
  32 + /// <summary>
  33 + /// App 打印日志:获取当前登录账号在当前门店打印的记录(分页,时间倒序)
  34 + /// </summary>
  35 + Task<PagedResultWithPageDto<PrintLogItemDto>> GetPrintLogListAsync(PrintLogGetListInputVo input);
25 } 36 }
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs
@@ -4,16 +4,21 @@ using System.Globalization; @@ -4,16 +4,21 @@ using System.Globalization;
4 using System.Linq; 4 using System.Linq;
5 using System.Text.Json; 5 using System.Text.Json;
6 using System.Threading.Tasks; 6 using System.Threading.Tasks;
  7 +using FoodLabeling.Application.Contracts.Dtos.Common;
7 using FoodLabeling.Application.Contracts.Dtos.Label; 8 using FoodLabeling.Application.Contracts.Dtos.Label;
8 using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; 9 using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
9 using FoodLabeling.Application.Contracts.IServices; 10 using FoodLabeling.Application.Contracts.IServices;
  11 +using FoodLabeling.Application.Helpers;
10 using FoodLabeling.Application.Services.DbModels; 12 using FoodLabeling.Application.Services.DbModels;
  13 +using FoodLabeling.Domain.Entities;
11 using Microsoft.AspNetCore.Authorization; 14 using Microsoft.AspNetCore.Authorization;
  15 +using Microsoft.AspNetCore.Mvc;
12 using SqlSugar; 16 using SqlSugar;
13 using Volo.Abp; 17 using Volo.Abp;
14 using Volo.Abp.Application.Services; 18 using Volo.Abp.Application.Services;
15 using Volo.Abp.Guids; 19 using Volo.Abp.Guids;
16 using Volo.Abp.Uow; 20 using Volo.Abp.Uow;
  21 +using Yi.Framework.Rbac.Domain.Entities;
17 using Yi.Framework.SqlSugarCore.Abstractions; 22 using Yi.Framework.SqlSugarCore.Abstractions;
18 23
19 namespace FoodLabeling.Application.Services; 24 namespace FoodLabeling.Application.Services;
@@ -26,12 +31,18 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -26,12 +31,18 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
26 private readonly ISqlSugarDbContext _dbContext; 31 private readonly ISqlSugarDbContext _dbContext;
27 private readonly ILabelAppService _labelAppService; 32 private readonly ILabelAppService _labelAppService;
28 private readonly IGuidGenerator _guidGenerator; 33 private readonly IGuidGenerator _guidGenerator;
  34 + private readonly ISqlSugarRepository<UserAggregateRoot, Guid> _userRepository;
29 35
30 - public UsAppLabelingAppService(ISqlSugarDbContext dbContext, ILabelAppService labelAppService, IGuidGenerator guidGenerator) 36 + public UsAppLabelingAppService(
  37 + ISqlSugarDbContext dbContext,
  38 + ILabelAppService labelAppService,
  39 + IGuidGenerator guidGenerator,
  40 + ISqlSugarRepository<UserAggregateRoot, Guid> userRepository)
31 { 41 {
32 _dbContext = dbContext; 42 _dbContext = dbContext;
33 _labelAppService = labelAppService; 43 _labelAppService = labelAppService;
34 _guidGenerator = guidGenerator; 44 _guidGenerator = guidGenerator;
  45 + _userRepository = userRepository;
35 } 46 }
36 47
37 /// <summary> 48 /// <summary>
@@ -509,6 +520,297 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -509,6 +520,297 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
509 }; 520 };
510 } 521 }
511 522
  523 + /// <summary>
  524 + /// App 重新打印:根据历史任务Id重打(创建新任务与明细)
  525 + /// </summary>
  526 + [Authorize]
  527 + [UnitOfWork]
  528 + public virtual async Task<UsAppLabelPrintOutputDto> ReprintAsync(UsAppLabelReprintInputVo input)
  529 + {
  530 + if (input is null)
  531 + {
  532 + throw new UserFriendlyException("入参不能为空");
  533 + }
  534 +
  535 + var locationId = input.LocationId?.Trim();
  536 + if (string.IsNullOrWhiteSpace(locationId))
  537 + {
  538 + throw new UserFriendlyException("门店Id不能为空");
  539 + }
  540 +
  541 + var taskId = input.TaskId?.Trim();
  542 + if (string.IsNullOrWhiteSpace(taskId))
  543 + {
  544 + throw new UserFriendlyException("taskId不能为空");
  545 + }
  546 +
  547 + var quantity = input.PrintQuantity <= 0 ? 1 : input.PrintQuantity;
  548 + var clientRequestId = input.ClientRequestId?.Trim();
  549 + if (!string.IsNullOrWhiteSpace(clientRequestId))
  550 + {
  551 + var existed = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>()
  552 + .Where(x => x.ClientRequestId == clientRequestId)
  553 + .OrderBy(x => x.CopyIndex)
  554 + .ToListAsync();
  555 +
  556 + if (existed is not null && existed.Count > 0)
  557 + {
  558 + var existedBatchId = existed.First().BatchId;
  559 + var existedTaskIds = existed.Select(x => x.Id).ToList();
  560 + return new UsAppLabelPrintOutputDto
  561 + {
  562 + TaskId = existedTaskIds.FirstOrDefault() ?? string.Empty,
  563 + PrintQuantity = existedTaskIds.Count,
  564 + BatchId = existedBatchId,
  565 + TaskIds = existedTaskIds
  566 + };
  567 + }
  568 + }
  569 +
  570 + var currentUserId = CurrentUser?.Id?.ToString();
  571 + if (string.IsNullOrWhiteSpace(currentUserId))
  572 + {
  573 + throw new UserFriendlyException("未登录");
  574 + }
  575 +
  576 + var old = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>()
  577 + .FirstAsync(x => x.Id == taskId);
  578 + if (old is null)
  579 + {
  580 + throw new UserFriendlyException("打印任务不存在");
  581 + }
  582 +
  583 + // 仅允许重打自己在当前门店的任务
  584 + if (!string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase))
  585 + {
  586 + throw new UserFriendlyException("无权限重打该任务");
  587 + }
  588 + if (!string.Equals(old.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase))
  589 + {
  590 + throw new UserFriendlyException("该任务不属于当前门店");
  591 + }
  592 +
  593 + LabelTemplatePreviewDto? resolvedTemplate = null;
  594 + try
  595 + {
  596 + resolvedTemplate = JsonSerializer.Deserialize<LabelTemplatePreviewDto>(old.RenderTemplateJson);
  597 + }
  598 + catch
  599 + {
  600 + resolvedTemplate = null;
  601 + }
  602 +
  603 + if (resolvedTemplate is null)
  604 + {
  605 + throw new UserFriendlyException("历史任务渲染快照解析失败,无法重打");
  606 + }
  607 +
  608 + var now = DateTime.Now;
  609 + var batchId = _guidGenerator.Create().ToString();
  610 + var taskIds = new List<string>();
  611 +
  612 + for (var i = 1; i <= quantity; i++)
  613 + {
  614 + var newTaskId = _guidGenerator.Create().ToString();
  615 + taskIds.Add(newTaskId);
  616 +
  617 + var newTask = new FlLabelPrintTaskDbEntity
  618 + {
  619 + Id = newTaskId,
  620 + BatchId = batchId,
  621 + CopyIndex = i,
  622 + ClientRequestId = string.IsNullOrWhiteSpace(clientRequestId) ? null : clientRequestId,
  623 + LabelId = old.LabelId,
  624 + TemplateId = old.TemplateId,
  625 + LabelTypeId = old.LabelTypeId,
  626 + ProductId = old.ProductId,
  627 + LocationId = old.LocationId,
  628 + BaseTime = old.BaseTime,
  629 + PrintInputJson = old.PrintInputJson,
  630 + TemplateProductDefaultValuesJson = old.TemplateProductDefaultValuesJson,
  631 + RenderTemplateJson = old.RenderTemplateJson,
  632 + PrinterId = string.IsNullOrWhiteSpace(input.PrinterId) ? old.PrinterId : input.PrinterId.Trim(),
  633 + PrinterMac = string.IsNullOrWhiteSpace(input.PrinterMac) ? old.PrinterMac : input.PrinterMac.Trim(),
  634 + PrinterAddress = string.IsNullOrWhiteSpace(input.PrinterAddress) ? old.PrinterAddress : input.PrinterAddress.Trim(),
  635 + Status = "CREATED",
  636 + PrintedAt = null,
  637 + ErrorMessage = null,
  638 + CreatedBy = currentUserId,
  639 + CreationTime = now
  640 + };
  641 +
  642 + await _dbContext.SqlSugarClient.Insertable(newTask).ExecuteCommandAsync();
  643 +
  644 + var rows = resolvedTemplate.Elements.Select(e =>
  645 + {
  646 + var cfgJson = e.ConfigJson is null ? null : JsonSerializer.Serialize(e.ConfigJson);
  647 + string? renderValue = null;
  648 + if (e.ConfigJson is JsonElement je && je.ValueKind == JsonValueKind.Object && je.TryGetProperty("text", out var tv))
  649 + {
  650 + renderValue = tv.ValueKind == JsonValueKind.String ? tv.GetString() : tv.ToString();
  651 + }
  652 + else if (e.ConfigJson is Dictionary<string, object?> dict && dict.TryGetValue("text", out var v))
  653 + {
  654 + renderValue = v?.ToString();
  655 + }
  656 +
  657 + return new FlLabelPrintDataDbEntity
  658 + {
  659 + Id = _guidGenerator.Create().ToString(),
  660 + PrintTaskId = newTaskId,
  661 + ElementId = e.Id?.Trim() ?? string.Empty,
  662 + ElementName = e.ElementName?.Trim(),
  663 + RenderValue = renderValue,
  664 + RenderConfigJson = cfgJson
  665 + };
  666 + }).Where(x => !string.IsNullOrWhiteSpace(x.ElementId)).ToList();
  667 +
  668 + if (rows.Count > 0)
  669 + {
  670 + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
  671 + }
  672 + }
  673 +
  674 + return new UsAppLabelPrintOutputDto
  675 + {
  676 + TaskId = taskIds.FirstOrDefault() ?? string.Empty,
  677 + PrintQuantity = quantity,
  678 + BatchId = batchId,
  679 + TaskIds = taskIds
  680 + };
  681 + }
  682 +
  683 + /// <summary>
  684 + /// App 打印日志:获取当前登录账号在当前门店打印的记录(分页,时间倒序)
  685 + /// </summary>
  686 + /// <remarks>
  687 + /// 仅返回满足:
  688 + /// - CreatedBy == CurrentUser.Id
  689 + /// - LocationId == input.LocationId
  690 + /// 的打印任务记录(fl_label_print_task)。
  691 + ///
  692 + /// 示例请求:
  693 + /// ```json
  694 + /// {
  695 + /// "locationId": "11111111-1111-1111-1111-111111111111",
  696 + /// "skipCount": 1,
  697 + /// "maxResultCount": 20
  698 + /// }
  699 + /// ```
  700 + ///
  701 + /// 参数说明:
  702 + /// - locationId: 当前门店 Id(必填)
  703 + /// - skipCount: 页码(从 1 开始,遵循本项目约定)
  704 + /// - maxResultCount: 每页条数
  705 + /// </remarks>
  706 + /// <param name="input">分页查询入参</param>
  707 + /// <returns>分页打印日志</returns>
  708 + /// <response code="200">成功</response>
  709 + /// <response code="400">参数错误/未登录</response>
  710 + /// <response code="500">服务器错误</response>
  711 + [Authorize]
  712 + [HttpPost]
  713 + public virtual async Task<PagedResultWithPageDto<PrintLogItemDto>> GetPrintLogListAsync(PrintLogGetListInputVo input)
  714 + {
  715 + if (input is null)
  716 + {
  717 + throw new UserFriendlyException("入参不能为空");
  718 + }
  719 +
  720 + if (!CurrentUser.Id.HasValue)
  721 + {
  722 + throw new UserFriendlyException("用户未登录");
  723 + }
  724 +
  725 + var locationId = input.LocationId?.Trim();
  726 + if (string.IsNullOrWhiteSpace(locationId))
  727 + {
  728 + throw new UserFriendlyException("门店Id不能为空");
  729 + }
  730 +
  731 + var currentUserIdStr = CurrentUser.Id.Value.ToString();
  732 +
  733 + var currentUser = await _userRepository.GetByIdAsync(CurrentUser.Id.Value);
  734 + var operatorName = currentUser?.Name?.Trim() ?? string.Empty;
  735 +
  736 + var locationName = "无";
  737 + if (Guid.TryParse(locationId, out var locationGuid))
  738 + {
  739 + var loc = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>()
  740 + .Where(x => !x.IsDeleted && x.Id == locationGuid)
  741 + .Select(x => new { x.LocationCode, x.LocationName })
  742 + .FirstAsync();
  743 + if (loc is not null)
  744 + {
  745 + var name = loc.LocationName?.Trim();
  746 + if (!string.IsNullOrWhiteSpace(name))
  747 + {
  748 + locationName = name;
  749 + }
  750 + }
  751 + }
  752 +
  753 + RefAsync<int> total = 0;
  754 +
  755 + var query = _dbContext.SqlSugarClient
  756 + .Queryable<FlLabelPrintTaskDbEntity>()
  757 + .LeftJoin<FlLabelDbEntity>((t, l) => t.LabelId == l.Id)
  758 + .LeftJoin<FlProductDbEntity>((t, l, p) => t.ProductId == p.Id)
  759 + .LeftJoin<FlLabelTypeDbEntity>((t, l, p, lt) => t.LabelTypeId == lt.Id)
  760 + .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lt, tpl) => t.TemplateId == tpl.Id)
  761 + .Where((t, l, p, lt, tpl) => t.CreatedBy == currentUserIdStr && t.LocationId == locationId)
  762 + .OrderBy((t, l, p, lt, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc)
  763 + .OrderBy((t, l, p, lt, tpl) => t.CreationTime, OrderByType.Desc)
  764 + .Select((t, l, p, lt, tpl) => new
  765 + {
  766 + t.Id,
  767 + t.BatchId,
  768 + t.CopyIndex,
  769 + t.LabelId,
  770 + LabelCode = l.LabelCode,
  771 + t.ProductId,
  772 + ProductName = p.ProductName,
  773 + TypeName = lt.TypeName,
  774 + TemplateWidth = tpl.Width,
  775 + TemplateHeight = tpl.Height,
  776 + TemplateUnit = tpl.Unit,
  777 + t.PrintedAt,
  778 + t.CreationTime
  779 + });
  780 +
  781 + var pageRows = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
  782 +
  783 + var items = pageRows.Select(x => new PrintLogItemDto
  784 + {
  785 + TaskId = x.Id,
  786 + BatchId = x.BatchId,
  787 + CopyIndex = x.CopyIndex,
  788 + LabelId = x.LabelId,
  789 + LabelCode = x.LabelCode ?? string.Empty,
  790 + ProductId = x.ProductId,
  791 + ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(),
  792 + TypeName = x.TypeName ?? string.Empty,
  793 + LabelSizeText = FormatLabelSizeWithUnit(x.TemplateWidth, x.TemplateHeight, x.TemplateUnit),
  794 + PrintedAt = x.PrintedAt ?? x.CreationTime,
  795 + OperatorName = operatorName,
  796 + LocationName = locationName
  797 + }).ToList();
  798 +
  799 + var pageSize = input.MaxResultCount <= 0 ? items.Count : input.MaxResultCount;
  800 + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount);
  801 + var totalCount = (long)total;
  802 + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize);
  803 +
  804 + return new PagedResultWithPageDto<PrintLogItemDto>
  805 + {
  806 + PageIndex = pageIndex,
  807 + PageSize = pageSize,
  808 + TotalCount = totalCount,
  809 + TotalPages = totalPages,
  810 + Items = items
  811 + };
  812 + }
  813 +
512 private async Task<string?> ResolveTemplateProductDefaultValuesJsonAsync( 814 private async Task<string?> ResolveTemplateProductDefaultValuesJsonAsync(
513 string templateId, 815 string templateId,
514 string? productId, 816 string? productId,
@@ -657,4 +959,13 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -657,4 +959,13 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
657 ? $"{ws}\"x{hs}\"" 959 ? $"{ws}\"x{hs}\""
658 : $"{ws}x{hs}{u}"; 960 : $"{ws}x{hs}{u}";
659 } 961 }
  962 +
  963 + private static string? FormatLabelSizeWithUnit(decimal w, decimal h, string unit)
  964 + {
  965 + var u = (unit ?? "inch").Trim().ToLowerInvariant();
  966 + var ws = w.ToString(CultureInfo.InvariantCulture);
  967 + var hs = h.ToString(CultureInfo.InvariantCulture);
  968 + var normalizedUnit = u is "in" ? "inch" : u;
  969 + return $"{ws}x{hs}{normalizedUnit}";
  970 + }
660 } 971 }
项目相关文档/标签模块接口对接说明.md
@@ -964,3 +964,113 @@ curl -X POST &quot;http://localhost:19001/api/app/us-app-labeling/print&quot; \ @@ -964,3 +964,113 @@ 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"}}' 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"}}'
965 ``` 965 ```
966 966
  967 +---
  968 +
  969 +## 接口 10:App 打印日志(当前登录账号 + 当前门店)
  970 +
  971 +**场景**:移动端“打印记录/历史”页面。只展示**当前登录账号**在**当前门店**打印的记录,便于追溯/重打。
  972 +
  973 +### 10.1 分页获取打印日志
  974 +
  975 +#### HTTP
  976 +
  977 +- **方法**:`POST`(与本模块其它复杂入参接口一致;若与 Swagger 不一致,**以 Swagger 为准**)
  978 +- **路径**:`/api/app/us-app-labeling/get-print-log-list`
  979 +- **鉴权**:需要登录(`Authorization: Bearer ...`)
  980 +
  981 +#### 入参(Body:`PrintLogGetListInputVo`)
  982 +
  983 +> 本项目分页约定:`skipCount` 表示 **页码(从 1 开始)**,不是 0 基 offset。
  984 +
  985 +| 参数名(JSON) | 类型 | 必填 | 说明 |
  986 +|---|---|---|---|
  987 +| `locationId` | string | 是 | 当前门店Id(仅返回该门店记录) |
  988 +| `skipCount` | number | 否 | 页码,从 1 开始;默认 1 |
  989 +| `maxResultCount` | number | 否 | 每页条数;默认按后端/ABP 默认 |
  990 +
  991 +#### 过滤条件(后端固定逻辑)
  992 +
  993 +- `fl_label_print_task.CreatedBy == CurrentUser.Id`
  994 +- `fl_label_print_task.LocationId == locationId`
  995 +- 按时间倒序:`PrintedAt ?? CreationTime`(越新的越靠前)
  996 +
  997 +#### 出参(`PagedResultWithPageDto<PrintLogItemDto>`)
  998 +
  999 +| 字段 | 类型 | 说明 |
  1000 +|---|---|---|
  1001 +| `pageIndex` | number | 当前页码(从 1 开始) |
  1002 +| `pageSize` | number | 每页条数 |
  1003 +| `totalCount` | number | 总条数 |
  1004 +| `totalPages` | number | 总页数 |
  1005 +| `items` | PrintLogItemDto[] | 列表 |
  1006 +
  1007 +`PrintLogItemDto`:
  1008 +
  1009 +| 字段 | 类型 | 说明 |
  1010 +|---|---|---|
  1011 +| `taskId` | string | 任务Id(fl_label_print_task.Id) |
  1012 +| `batchId` | string | 批次Id(同一次点击 Print 共享) |
  1013 +| `copyIndex` | number | 第几份(从 1 开始) |
  1014 +| `labelId` | string | 标签Id |
  1015 +| `labelCode` | string | 标签编码(来自 fl_label.LabelCode) |
  1016 +| `productId` | string | 产品Id |
  1017 +| `productName` | string | 产品名(来自 fl_product.ProductName;无则 “无”) |
  1018 +| `typeName` | string | 标签类型名称(来自 fl_label_type.TypeName) |
  1019 +| `labelSizeText` | string | 模板尺寸(宽高+单位,如 `2.00x2.00inch` / `6.00x4.00cm`) |
  1020 +| `printedAt` | string | 打印时间(PrintedAt ?? CreationTime) |
  1021 +| `operatorName` | string | 操作人姓名(当前登录账号 Name) |
  1022 +| `locationName` | string | 门店名称 |
  1023 +
  1024 +#### curl
  1025 +
  1026 +```bash
  1027 +curl -X POST "http://localhost:19001/api/app/us-app-labeling/get-print-log-list" \
  1028 + -H "Authorization: <data.token>" \
  1029 + -H "Content-Type: application/json" \
  1030 + -d '{"locationId":"11111111-1111-1111-1111-111111111111","skipCount":1,"maxResultCount":20}'
  1031 +```
  1032 +
  1033 +---
  1034 +
  1035 +## 接口 11:App 重新打印(根据任务Id重打)
  1036 +
  1037 +**场景**:移动端“打印记录/历史”页面点击 **Reprint**。后端根据历史任务 `taskId` 创建一批新的打印任务与明细。
  1038 +
  1039 +### 11.1 重打
  1040 +
  1041 +#### HTTP
  1042 +
  1043 +- **方法**:`POST`
  1044 +- **路径**:`/api/app/us-app-labeling/reprint`(若与 Swagger 不一致,**以 Swagger 为准**)
  1045 +- **鉴权**:需要登录(`Authorization: Bearer ...`)
  1046 +
  1047 +#### 入参(Body:`UsAppLabelReprintInputVo`)
  1048 +
  1049 +| 参数名(JSON) | 类型 | 必填 | 说明 |
  1050 +|---|---|---|---|
  1051 +| `locationId` | string | 是 | 当前门店Id(后端校验历史任务必须属于该门店) |
  1052 +| `taskId` | string | 是 | 历史打印任务Id(`fl_label_print_task.Id`) |
  1053 +| `printQuantity` | number | 否 | 重新打印份数;`<=0` 按 1 处理;默认 1 |
  1054 +| `clientRequestId` | string | 否 | 客户端幂等请求Id;同一个值重复调用会直接返回首次创建的 `batchId/taskIds`,避免重复写库 |
  1055 +| `printerId` | string | 否 | 可选,覆盖历史任务的打印机Id |
  1056 +| `printerMac` | string | 否 | 可选,覆盖历史任务的打印机MAC |
  1057 +| `printerAddress` | string | 否 | 可选,覆盖历史任务的打印机地址 |
  1058 +
  1059 +#### 权限校验(后端固定逻辑)
  1060 +
  1061 +- 历史任务必须满足:`CreatedBy == CurrentUser.Id`
  1062 +- 且 `LocationId == locationId`
  1063 +
  1064 +#### 出参(`UsAppLabelPrintOutputDto`)
  1065 +
  1066 +字段与接口 9 一致:`taskId / printQuantity / batchId / taskIds`
  1067 +
  1068 +#### curl
  1069 +
  1070 +```bash
  1071 +curl -X POST "http://localhost:19001/api/app/us-app-labeling/reprint" \
  1072 + -H "Authorization: <data.token>" \
  1073 + -H "Content-Type: application/json" \
  1074 + -d '{"locationId":"11111111-1111-1111-1111-111111111111","taskId":"3a205389-78dd-4750-51ab-720344c9f607","printQuantity":1}'
  1075 +```
  1076 +