using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using FoodLabeling.Application.Contracts.Dtos.Common;
using FoodLabeling.Application.Contracts.Dtos.Label;
using FoodLabeling.Application.Contracts.Dtos.LabelTemplate;
using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
using FoodLabeling.Application.Contracts.IServices;
using FoodLabeling.Application.Helpers;
using FoodLabeling.Application.Services.DbModels;
using FoodLabeling.Domain.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Guids;
using Volo.Abp.Uow;
using Yi.Framework.Rbac.Domain.Entities;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace FoodLabeling.Application.Services;
///
/// App Labeling:四级列表(标签分类 → 产品分类 → 产品 → 标签种类)
///
public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppService
{
private readonly ISqlSugarDbContext _dbContext;
private readonly ILabelAppService _labelAppService;
private readonly IGuidGenerator _guidGenerator;
private readonly ISqlSugarRepository _userRepository;
public UsAppLabelingAppService(
ISqlSugarDbContext dbContext,
ILabelAppService labelAppService,
IGuidGenerator guidGenerator,
ISqlSugarRepository userRepository)
{
_dbContext = dbContext;
_labelAppService = labelAppService;
_guidGenerator = guidGenerator;
_userRepository = userRepository;
}
///
/// 获取当前门店下四级嵌套数据
///
///
/// L1 标签分类 fl_label_category;L2 产品分类 fl_product.CategoryId join fl_product_category;
/// L3 产品;L4 与该门店、该标签分类、该产品关联的标签实例(fl_label + fl_label_type)。
///
[Authorize]
public virtual async Task> GetLabelingTreeAsync(UsAppLabelingTreeInputVo input)
{
if (string.IsNullOrWhiteSpace(input.LocationId))
{
throw new UserFriendlyException("门店Id不能为空");
}
var locationId = input.LocationId.Trim();
var keyword = input.Keyword?.Trim();
var filterCategoryId = input.LabelCategoryId?.Trim();
var productIds = await _dbContext.SqlSugarClient.Queryable()
.Where(x => x.LocationId == locationId)
.Select(x => x.ProductId)
.ToListAsync();
if (productIds.Count == 0)
{
return new List();
}
var query = BuildLabelingJoinQuery(locationId, productIds, filterCategoryId, keyword);
var raw = await query
.Select((lp, l, p, c, t, tpl, pc) => new LabelingTreeRow
{
LabelCategoryId = c.Id,
LabelCategoryName = c.CategoryName,
LabelCategoryPhotoUrl = c.CategoryPhotoUrl,
LabelCategoryOrderNum = c.OrderNum,
ProductCategoryId = p.CategoryId,
ProductCategoryName = pc.CategoryName,
ProductCategoryPhotoUrl = pc.CategoryPhotoUrl,
ProductId = p.Id,
ProductName = p.ProductName,
ProductCode = p.ProductCode,
ProductImageUrl = p.ProductImageUrl,
LabelTypeId = t.Id,
TypeName = t.TypeName,
TypeOrderNum = t.OrderNum,
LabelCode = l.LabelCode,
TemplateCode = tpl.TemplateCode,
TemplateWidth = tpl.Width,
TemplateHeight = tpl.Height,
TemplateUnit = tpl.Unit
})
.ToListAsync();
if (raw.Count == 0)
{
return new List();
}
var byL1 = raw.GroupBy(x => new
{
x.LabelCategoryId,
x.LabelCategoryName,
x.LabelCategoryPhotoUrl,
x.LabelCategoryOrderNum
}).OrderBy(g => g.Key.LabelCategoryOrderNum).ThenBy(g => g.Key.LabelCategoryName);
var result = new List();
foreach (var g1 in byL1)
{
var l1 = new UsAppLabelCategoryTreeNodeDto
{
Id = g1.Key.LabelCategoryId,
CategoryName = g1.Key.LabelCategoryName ?? string.Empty,
CategoryPhotoUrl = g1.Key.LabelCategoryPhotoUrl,
OrderNum = g1.Key.LabelCategoryOrderNum,
ProductCategories = new List()
};
var byL2 = g1.GroupBy(x =>
{
var categoryId = NormalizeNullableId(x.ProductCategoryId);
if (categoryId is null)
{
return new
{
CategoryId = (string?)null,
CategoryName = "无",
CategoryPhotoUrl = (string?)null
};
}
var categoryName = NormalizeCategoryName(x.ProductCategoryName);
var categoryPhotoUrl = NormalizeNullableUrl(x.ProductCategoryPhotoUrl);
return new
{
CategoryId = (string?)categoryId,
CategoryName = categoryName,
CategoryPhotoUrl = categoryPhotoUrl
};
})
.OrderBy(g => g.Key.CategoryName);
foreach (var g2 in byL2)
{
var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName);
var l2 = new UsAppProductCategoryNodeDto
{
CategoryId = g2.Key.CategoryId,
CategoryPhotoUrl = g2.Key.CategoryPhotoUrl,
Name = g2.Key.CategoryName,
ItemCount = productsGrouped.Count(),
Products = new List()
};
foreach (var g3 in productsGrouped)
{
var first = g3.First();
var typeNodes = g3
.GroupBy(r => r.LabelCode)
.Select(gr => BuildLabelTypeNode(gr.First()))
.OrderBy(t => t.OrderNum)
.ThenBy(t => t.TypeName)
.ToList();
var subtitle = string.IsNullOrWhiteSpace(first.ProductCode?.Trim())
? "无"
: first.ProductCode!.Trim();
l2.Products.Add(new UsAppLabelingProductNodeDto
{
ProductId = first.ProductId,
ProductName = first.ProductName ?? string.Empty,
ProductCode = first.ProductCode ?? string.Empty,
ProductImageUrl = first.ProductImageUrl,
Subtitle = subtitle,
LabelTypeCount = typeNodes.Count,
LabelTypes = typeNodes
});
}
l1.ProductCategories.Add(l2);
}
result.Add(l1);
}
return result;
}
///
/// App 打印预览:按标签编码解析模板并返回顶部展示字段 + 预览模板结构
///
///
/// 示例请求:
/// ```json
/// {
/// "locationId": "LOC001",
/// "labelCode": "LBL0001",
/// "productId": "PROD001",
/// "baseTime": "2026-03-26T10:30:00",
/// "printInputJson": {
/// "price": "12.99"
/// }
/// }
/// ```
///
/// 预览入参
/// 顶部字段 + 预览模板结构
/// 成功
/// 参数错误/数据不存在
/// 服务器错误
[Authorize]
public virtual async Task PreviewAsync(UsAppLabelPreviewInputVo input)
{
if (input is null)
{
throw new UserFriendlyException("入参不能为空");
}
var locationId = input.LocationId?.Trim();
if (string.IsNullOrWhiteSpace(locationId))
{
throw new UserFriendlyException("门店Id不能为空");
}
var labelCode = input.LabelCode?.Trim();
if (string.IsNullOrWhiteSpace(labelCode))
{
throw new UserFriendlyException("labelCode不能为空");
}
var labelRow = await _dbContext.SqlSugarClient
.Queryable(
(l, c, t, tpl) => l.LabelCategoryId == c.Id && l.LabelTypeId == t.Id && l.TemplateId == tpl.Id)
.Where((l, c, t, tpl) => !l.IsDeleted && l.State)
.Where((l, c, t, tpl) => !c.IsDeleted && c.State)
.Where((l, c, t, tpl) => !t.IsDeleted && t.State)
.Where((l, c, t, tpl) => !tpl.IsDeleted)
.Where((l, c, t, tpl) => l.LabelCode == labelCode)
.Select((l, c, t, tpl) => new
{
l.Id,
l.LabelCode,
l.LocationId,
l.LabelTypeId,
l.TemplateId,
l.LastModificationTime,
l.CreationTime,
LabelCategoryName = c.CategoryName,
TypeName = t.TypeName,
TemplateCode = tpl.TemplateCode,
TemplateWidth = tpl.Width,
TemplateHeight = tpl.Height,
TemplateUnit = tpl.Unit
})
.FirstAsync();
if (labelRow is null)
{
throw new UserFriendlyException("标签不存在或不可用");
}
if (!string.Equals(labelRow.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase))
{
throw new UserFriendlyException("该标签不属于当前门店");
}
var previewProductId = await ResolvePreviewProductIdAsync(labelRow.Id, input.ProductId);
var template = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo
{
LabelCode = labelCode,
ProductId = previewProductId,
BaseTime = input.BaseTime,
PrintInputJson = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value)
});
Dictionary? templateProductDefaultValues = null;
if (!string.IsNullOrWhiteSpace(previewProductId))
{
var productDefault = await _dbContext.SqlSugarClient.Queryable()
.Where(x => x.TemplateId == labelRow.TemplateId)
.Where(x => x.ProductId == previewProductId)
.Where(x => x.LabelTypeId == labelRow.LabelTypeId)
.OrderBy(x => x.OrderNum)
.FirstAsync();
if (!string.IsNullOrWhiteSpace(productDefault?.DefaultValuesJson))
{
try
{
templateProductDefaultValues =
JsonSerializer.Deserialize>(productDefault.DefaultValuesJson!);
}
catch
{
templateProductDefaultValues = null;
}
}
}
var productName = string.Empty;
var productCategoryName = "无";
if (!string.IsNullOrWhiteSpace(previewProductId))
{
var p = await _dbContext.SqlSugarClient.Queryable()
.FirstAsync(x => !x.IsDeleted && x.State && x.Id == previewProductId);
if (p is not null)
{
productName = p.ProductName ?? string.Empty;
if (!string.IsNullOrWhiteSpace(p.CategoryId))
{
var pc = await _dbContext.SqlSugarClient.Queryable()
.FirstAsync(x => !x.IsDeleted && x.State && x.Id == p.CategoryId);
productCategoryName = NormalizeCategoryName(pc?.CategoryName);
}
}
}
return new UsAppLabelPreviewDto
{
LabelId = labelRow.Id,
LocationId = locationId,
LabelCode = labelCode,
TemplateCode = labelRow.TemplateCode,
LabelSizeText = FormatLabelSize(labelRow.TemplateWidth, labelRow.TemplateHeight, labelRow.TemplateUnit),
TypeName = labelRow.TypeName,
ProductName = string.IsNullOrWhiteSpace(productName) ? null : productName,
ProductCategoryName = productCategoryName,
LabelCategoryName = labelRow.LabelCategoryName,
LabelLastEdited = labelRow.LastModificationTime ?? labelRow.CreationTime,
PreviewImageBase64Png = null,
Template = template,
TemplateProductDefaultValues = templateProductDefaultValues
};
}
///
/// App 打印:创建打印任务并落库打印明细(fl_label_print_task / fl_label_print_data)
///
/// 打印入参
/// 任务Id
[Authorize]
[UnitOfWork]
public virtual async Task PrintAsync(UsAppLabelPrintInputVo input)
{
if (input is null)
{
throw new UserFriendlyException("入参不能为空");
}
var locationId = input.LocationId?.Trim();
if (string.IsNullOrWhiteSpace(locationId))
{
throw new UserFriendlyException("门店Id不能为空");
}
var labelCode = input.LabelCode?.Trim();
if (string.IsNullOrWhiteSpace(labelCode))
{
throw new UserFriendlyException("labelCode不能为空");
}
var quantity = input.PrintQuantity <= 0 ? 1 : input.PrintQuantity;
var clientRequestId = input.ClientRequestId?.Trim();
if (!string.IsNullOrWhiteSpace(clientRequestId))
{
// 幂等:同一个 clientRequestId 重复调用,直接返回首次创建的任务集合
var existed = await _dbContext.SqlSugarClient.Queryable()
.Where(x => x.ClientRequestId == clientRequestId)
.OrderBy(x => x.CopyIndex)
.ToListAsync();
if (existed is not null && existed.Count > 0)
{
var existedBatchId = existed.First().BatchId;
var existedTaskIds = existed.Select(x => x.Id).ToList();
return new UsAppLabelPrintOutputDto
{
TaskId = existedTaskIds.FirstOrDefault() ?? string.Empty,
PrintQuantity = existedTaskIds.Count,
BatchId = existedBatchId,
TaskIds = existedTaskIds
};
}
}
// 校验 label + location,并补齐一些顶部字段用于任务表落库
var labelRow = await _dbContext.SqlSugarClient
.Queryable(
(l, t, tpl) => l.LabelTypeId == t.Id && l.TemplateId == tpl.Id)
.Where((l, t, tpl) => !l.IsDeleted && l.State)
.Where((l, t, tpl) => !t.IsDeleted && t.State)
.Where((l, t, tpl) => !tpl.IsDeleted)
.Where((l, t, tpl) => l.LabelCode == labelCode)
.Select((l, t, tpl) => new
{
l.Id,
l.LocationId,
l.LabelTypeId,
l.TemplateId
})
.FirstAsync();
if (labelRow is null)
{
throw new UserFriendlyException("标签不存在或不可用");
}
if (!string.Equals(labelRow.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase))
{
throw new UserFriendlyException("该标签不属于当前门店");
}
var previewProductId = await ResolvePreviewProductIdAsync(labelRow.Id, input.ProductId);
var normalizedPrintInput = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value);
// 解析模板 elements(与预览一致的渲染数据)
var resolvedTemplate = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo
{
LabelCode = labelCode,
ProductId = previewProductId,
BaseTime = input.BaseTime,
PrintInputJson = normalizedPrintInput
});
var templateProductDefaultValuesJson = await ResolveTemplateProductDefaultValuesJsonAsync(
labelRow.TemplateId,
previewProductId,
labelRow.LabelTypeId);
var printInputJsonStr = input.PrintInputJson is null
? null
: JsonSerializer.Serialize(input.PrintInputJson);
var renderTemplateJsonStr = JsonSerializer.Serialize(resolvedTemplate);
var now = DateTime.Now;
var currentUserId = CurrentUser?.Id?.ToString();
var batchId = _guidGenerator.Create().ToString();
var taskIds = new List();
for (var i = 1; i <= quantity; i++)
{
var taskId = _guidGenerator.Create().ToString();
taskIds.Add(taskId);
var task = new FlLabelPrintTaskDbEntity
{
Id = taskId,
BatchId = batchId,
CopyIndex = i,
ClientRequestId = string.IsNullOrWhiteSpace(clientRequestId) ? null : clientRequestId,
LabelId = labelRow.Id,
TemplateId = labelRow.TemplateId,
LabelTypeId = labelRow.LabelTypeId,
ProductId = previewProductId,
LocationId = locationId,
BaseTime = input.BaseTime,
PrintInputJson = printInputJsonStr,
TemplateProductDefaultValuesJson = templateProductDefaultValuesJson,
RenderTemplateJson = renderTemplateJsonStr,
PrinterId = input.PrinterId?.Trim(),
PrinterMac = input.PrinterMac?.Trim(),
PrinterAddress = input.PrinterAddress?.Trim(),
Status = "CREATED",
PrintedAt = null,
ErrorMessage = null,
CreatedBy = currentUserId,
CreationTime = now
};
await _dbContext.SqlSugarClient.Insertable(task).ExecuteCommandAsync();
var rows = resolvedTemplate.Elements.Select(e =>
{
var cfgJson = e.ConfigJson is null ? null : JsonSerializer.Serialize(e.ConfigJson);
string? renderValue = null;
if (e.ConfigJson is JsonElement je && je.ValueKind == JsonValueKind.Object && je.TryGetProperty("text", out var tv))
{
renderValue = tv.ValueKind == JsonValueKind.String ? tv.GetString() : tv.ToString();
}
else if (e.ConfigJson is Dictionary dict && dict.TryGetValue("text", out var v))
{
renderValue = v?.ToString();
}
return new FlLabelPrintDataDbEntity
{
Id = _guidGenerator.Create().ToString(),
PrintTaskId = taskId,
ElementId = e.Id?.Trim() ?? string.Empty,
ElementName = e.ElementName?.Trim(),
RenderValue = renderValue,
RenderConfigJson = cfgJson
};
}).Where(x => !string.IsNullOrWhiteSpace(x.ElementId)).ToList();
if (rows.Count > 0)
{
await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
}
}
return new UsAppLabelPrintOutputDto
{
TaskId = taskIds.FirstOrDefault() ?? string.Empty,
PrintQuantity = quantity,
BatchId = batchId,
TaskIds = taskIds
};
}
///
/// App 重新打印:根据历史任务Id重打(创建新任务与明细)
///
[Authorize]
[UnitOfWork]
public virtual async Task ReprintAsync(UsAppLabelReprintInputVo input)
{
if (input is null)
{
throw new UserFriendlyException("入参不能为空");
}
var locationId = input.LocationId?.Trim();
if (string.IsNullOrWhiteSpace(locationId))
{
throw new UserFriendlyException("门店Id不能为空");
}
var taskId = input.TaskId?.Trim();
if (string.IsNullOrWhiteSpace(taskId))
{
throw new UserFriendlyException("taskId不能为空");
}
var quantity = input.PrintQuantity <= 0 ? 1 : input.PrintQuantity;
var clientRequestId = input.ClientRequestId?.Trim();
if (!string.IsNullOrWhiteSpace(clientRequestId))
{
var existed = await _dbContext.SqlSugarClient.Queryable()
.Where(x => x.ClientRequestId == clientRequestId)
.OrderBy(x => x.CopyIndex)
.ToListAsync();
if (existed is not null && existed.Count > 0)
{
var existedBatchId = existed.First().BatchId;
var existedTaskIds = existed.Select(x => x.Id).ToList();
return new UsAppLabelPrintOutputDto
{
TaskId = existedTaskIds.FirstOrDefault() ?? string.Empty,
PrintQuantity = existedTaskIds.Count,
BatchId = existedBatchId,
TaskIds = existedTaskIds
};
}
}
var currentUserId = CurrentUser?.Id?.ToString();
if (string.IsNullOrWhiteSpace(currentUserId))
{
throw new UserFriendlyException("未登录");
}
var old = await _dbContext.SqlSugarClient.Queryable()
.FirstAsync(x => x.Id == taskId);
if (old is null)
{
throw new UserFriendlyException("打印任务不存在");
}
// 仅允许重打自己在当前门店的任务
if (!string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase))
{
throw new UserFriendlyException("无权限重打该任务");
}
if (!string.Equals(old.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase))
{
throw new UserFriendlyException("该任务不属于当前门店");
}
LabelTemplatePreviewDto? resolvedTemplate = null;
try
{
resolvedTemplate = JsonSerializer.Deserialize(old.RenderTemplateJson);
}
catch
{
resolvedTemplate = null;
}
if (resolvedTemplate is null)
{
throw new UserFriendlyException("历史任务渲染快照解析失败,无法重打");
}
var now = DateTime.Now;
var batchId = _guidGenerator.Create().ToString();
var taskIds = new List();
for (var i = 1; i <= quantity; i++)
{
var newTaskId = _guidGenerator.Create().ToString();
taskIds.Add(newTaskId);
var newTask = new FlLabelPrintTaskDbEntity
{
Id = newTaskId,
BatchId = batchId,
CopyIndex = i,
ClientRequestId = string.IsNullOrWhiteSpace(clientRequestId) ? null : clientRequestId,
LabelId = old.LabelId,
TemplateId = old.TemplateId,
LabelTypeId = old.LabelTypeId,
ProductId = old.ProductId,
LocationId = old.LocationId,
BaseTime = old.BaseTime,
PrintInputJson = old.PrintInputJson,
TemplateProductDefaultValuesJson = old.TemplateProductDefaultValuesJson,
RenderTemplateJson = old.RenderTemplateJson,
PrinterId = string.IsNullOrWhiteSpace(input.PrinterId) ? old.PrinterId : input.PrinterId.Trim(),
PrinterMac = string.IsNullOrWhiteSpace(input.PrinterMac) ? old.PrinterMac : input.PrinterMac.Trim(),
PrinterAddress = string.IsNullOrWhiteSpace(input.PrinterAddress) ? old.PrinterAddress : input.PrinterAddress.Trim(),
Status = "CREATED",
PrintedAt = null,
ErrorMessage = null,
CreatedBy = currentUserId,
CreationTime = now
};
await _dbContext.SqlSugarClient.Insertable(newTask).ExecuteCommandAsync();
var rows = resolvedTemplate.Elements.Select(e =>
{
var cfgJson = e.ConfigJson is null ? null : JsonSerializer.Serialize(e.ConfigJson);
string? renderValue = null;
if (e.ConfigJson is JsonElement je && je.ValueKind == JsonValueKind.Object && je.TryGetProperty("text", out var tv))
{
renderValue = tv.ValueKind == JsonValueKind.String ? tv.GetString() : tv.ToString();
}
else if (e.ConfigJson is Dictionary dict && dict.TryGetValue("text", out var v))
{
renderValue = v?.ToString();
}
return new FlLabelPrintDataDbEntity
{
Id = _guidGenerator.Create().ToString(),
PrintTaskId = newTaskId,
ElementId = e.Id?.Trim() ?? string.Empty,
ElementName = e.ElementName?.Trim(),
RenderValue = renderValue,
RenderConfigJson = cfgJson
};
}).Where(x => !string.IsNullOrWhiteSpace(x.ElementId)).ToList();
if (rows.Count > 0)
{
await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
}
}
return new UsAppLabelPrintOutputDto
{
TaskId = taskIds.FirstOrDefault() ?? string.Empty,
PrintQuantity = quantity,
BatchId = batchId,
TaskIds = taskIds
};
}
///
/// App 打印日志:获取当前登录账号在当前门店打印的记录(分页,时间倒序)
///
///
/// 仅返回满足:
/// - CreatedBy == CurrentUser.Id
/// - LocationId == input.LocationId
/// 的打印任务记录(fl_label_print_task)。
///
/// 示例请求:
/// ```json
/// {
/// "locationId": "11111111-1111-1111-1111-111111111111",
/// "skipCount": 1,
/// "maxResultCount": 20
/// }
/// ```
///
/// 参数说明:
/// - locationId: 当前门店 Id(必填)
/// - skipCount: 页码(从 1 开始,遵循本项目约定)
/// - maxResultCount: 每页条数
///
/// 分页查询入参
/// 分页打印日志
/// 成功
/// 参数错误/未登录
/// 服务器错误
[Authorize]
[HttpPost]
public virtual async Task> GetPrintLogListAsync(PrintLogGetListInputVo input)
{
if (input is null)
{
throw new UserFriendlyException("入参不能为空");
}
if (!CurrentUser.Id.HasValue)
{
throw new UserFriendlyException("用户未登录");
}
var locationId = input.LocationId?.Trim();
if (string.IsNullOrWhiteSpace(locationId))
{
throw new UserFriendlyException("门店Id不能为空");
}
var currentUserIdStr = CurrentUser.Id.Value.ToString();
var currentUser = await _userRepository.GetByIdAsync(CurrentUser.Id.Value);
var operatorName = currentUser?.Name?.Trim() ?? string.Empty;
var locationName = "无";
if (Guid.TryParse(locationId, out var locationGuid))
{
var loc = await _dbContext.SqlSugarClient.Queryable()
.Where(x => !x.IsDeleted && x.Id == locationGuid)
.Select(x => new { x.LocationCode, x.LocationName })
.FirstAsync();
if (loc is not null)
{
var name = loc.LocationName?.Trim();
if (!string.IsNullOrWhiteSpace(name))
{
locationName = name;
}
}
}
RefAsync total = 0;
var query = _dbContext.SqlSugarClient
.Queryable()
.LeftJoin((t, l) => t.LabelId == l.Id)
.LeftJoin((t, l, p) => t.ProductId == p.Id)
.LeftJoin((t, l, p, lt) => t.LabelTypeId == lt.Id)
.LeftJoin((t, l, p, lt, tpl) => t.TemplateId == tpl.Id)
.Where((t, l, p, lt, tpl) => t.CreatedBy == currentUserIdStr && t.LocationId == locationId)
.OrderBy((t, l, p, lt, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc)
.OrderBy((t, l, p, lt, tpl) => t.CreationTime, OrderByType.Desc)
.Select((t, l, p, lt, tpl) => new
{
t.Id,
t.BatchId,
t.CopyIndex,
t.LabelId,
LabelCode = l.LabelCode,
t.ProductId,
ProductName = p.ProductName,
TypeName = lt.TypeName,
TemplateWidth = tpl.Width,
TemplateHeight = tpl.Height,
TemplateUnit = tpl.Unit,
t.PrintInputJson,
t.PrintedAt,
t.CreationTime
});
var pageRows = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
var items = pageRows.Select(x => new PrintLogItemDto
{
TaskId = x.Id,
BatchId = x.BatchId,
CopyIndex = x.CopyIndex,
LabelId = x.LabelId,
LabelCode = x.LabelCode ?? string.Empty,
ProductId = x.ProductId,
ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(),
TypeName = x.TypeName ?? string.Empty,
LabelSizeText = FormatLabelSizeWithUnit(x.TemplateWidth, x.TemplateHeight, x.TemplateUnit),
PrintInputJson = x.PrintInputJson,
PrintedAt = x.PrintedAt ?? x.CreationTime,
OperatorName = operatorName,
LocationName = locationName
}).ToList();
var pageSize = input.MaxResultCount <= 0 ? items.Count : input.MaxResultCount;
var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount);
var totalCount = (long)total;
var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize);
return new PagedResultWithPageDto
{
PageIndex = pageIndex,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = totalPages,
Items = items
};
}
private async Task ResolveTemplateProductDefaultValuesJsonAsync(
string templateId,
string? productId,
string labelTypeId)
{
if (string.IsNullOrWhiteSpace(templateId) || string.IsNullOrWhiteSpace(productId) || string.IsNullOrWhiteSpace(labelTypeId))
{
return null;
}
var productDefault = await _dbContext.SqlSugarClient.Queryable()
.Where(x => x.TemplateId == templateId)
.Where(x => x.ProductId == productId)
.Where(x => x.LabelTypeId == labelTypeId)
.OrderBy(x => x.OrderNum)
.FirstAsync();
return string.IsNullOrWhiteSpace(productDefault?.DefaultValuesJson) ? null : productDefault!.DefaultValuesJson;
}
private ISugarQueryable BuildLabelingJoinQuery(
string locationId,
List productIds,
string? filterCategoryId,
string? keyword)
{
var q = _dbContext.SqlSugarClient
.Queryable()
.InnerJoin((lp, l) => lp.LabelId == l.Id)
.InnerJoin((lp, l, p) => lp.ProductId == p.Id)
.InnerJoin((lp, l, p, c) => l.LabelCategoryId == c.Id)
.InnerJoin((lp, l, p, c, t) => l.LabelTypeId == t.Id)
.InnerJoin((lp, l, p, c, t, tpl) => l.TemplateId == tpl.Id)
.LeftJoin((lp, l, p, c, t, tpl, pc) => p.CategoryId == pc.Id)
.Where((lp, l, p, c, t, tpl, pc) => productIds.Contains(p.Id))
.Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId)
.Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State)
.Where((lp, l, p, c, t, tpl, pc) => !p.IsDeleted && p.State)
.Where((lp, l, p, c, t, tpl, pc) => !c.IsDeleted && c.State)
.Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State)
.Where((lp, l, p, c, t, tpl, pc) => !tpl.IsDeleted)
.WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId)
.WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) =>
(l.LabelName != null && l.LabelName.Contains(keyword!)) ||
(p.ProductName != null && p.ProductName.Contains(keyword!)) ||
(pc.CategoryName != null && pc.CategoryName.Contains(keyword!)) ||
(c.CategoryName != null && c.CategoryName.Contains(keyword!)) ||
(t.TypeName != null && t.TypeName.Contains(keyword!)) ||
(l.LabelCode != null && l.LabelCode.Contains(keyword!)));
return q;
}
private sealed class LabelingTreeRow
{
public string LabelCategoryId { get; set; } = string.Empty;
public string? LabelCategoryName { get; set; }
public string? LabelCategoryPhotoUrl { get; set; }
public int LabelCategoryOrderNum { get; set; }
public string? ProductCategoryId { get; set; }
public string? ProductCategoryName { get; set; }
public string? ProductCategoryPhotoUrl { get; set; }
public string ProductId { get; set; } = string.Empty;
public string? ProductName { get; set; }
public string? ProductCode { get; set; }
public string? ProductImageUrl { get; set; }
public string LabelTypeId { get; set; } = string.Empty;
public string? TypeName { get; set; }
public int TypeOrderNum { get; set; }
public string LabelCode { get; set; } = string.Empty;
public string? TemplateCode { get; set; }
public decimal TemplateWidth { get; set; }
public decimal TemplateHeight { get; set; }
public string TemplateUnit { get; set; } = "inch";
}
private static string NormalizeCategoryName(string? categoryName)
{
var s = categoryName?.Trim();
return string.IsNullOrWhiteSpace(s) ? "无" : s;
}
private static string? NormalizeNullableId(string? id)
{
var s = id?.Trim();
return string.IsNullOrWhiteSpace(s) ? null : s;
}
private static string? NormalizeNullableUrl(string? url)
{
var s = url?.Trim();
return string.IsNullOrWhiteSpace(s) ? null : s;
}
private async Task ResolvePreviewProductIdAsync(string labelId, string? productId)
{
var resolvedProductId = productId?.Trim();
if (!string.IsNullOrWhiteSpace(resolvedProductId))
{
return resolvedProductId;
}
return await _dbContext.SqlSugarClient.Queryable()
.Where(x => x.LabelId == labelId)
.Select(x => x.ProductId)
.FirstAsync();
}
private static UsAppLabelTypeNodeDto BuildLabelTypeNode(LabelingTreeRow r)
{
return new UsAppLabelTypeNodeDto
{
LabelTypeId = r.LabelTypeId,
TypeName = r.TypeName ?? string.Empty,
OrderNum = r.TypeOrderNum,
LabelCode = r.LabelCode ?? string.Empty,
TemplateCode = r.TemplateCode,
LabelSizeText = FormatLabelSize(r.TemplateWidth, r.TemplateHeight, r.TemplateUnit)
};
}
private static string? FormatLabelSize(decimal w, decimal h, string unit)
{
var u = (unit ?? "inch").Trim().ToLowerInvariant();
var ws = w.ToString(CultureInfo.InvariantCulture);
var hs = h.ToString(CultureInfo.InvariantCulture);
return u is "inch" or "in"
? $"{ws}\"x{hs}\""
: $"{ws}x{hs}{u}";
}
private static string? FormatLabelSizeWithUnit(decimal w, decimal h, string unit)
{
var u = (unit ?? "inch").Trim().ToLowerInvariant();
var ws = w.ToString(CultureInfo.InvariantCulture);
var hs = h.ToString(CultureInfo.InvariantCulture);
var normalizedUnit = u is "in" ? "inch" : u;
return $"{ws}x{hs}{normalizedUnit}";
}
}