Commit 5654bb5bc4dd16663254674c1c017b46ed57f44b

Authored by 杨鑫
2 parents 143afd59 536d25c4

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

Showing 37 changed files with 2235 additions and 64 deletions
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetListOutputDto.cs
... ... @@ -12,7 +12,10 @@ public class LabelGetListOutputDto
12 12  
13 13 public string ProductCategoryName { get; set; } = string.Empty;
14 14  
15   - public string ProductName { get; set; } = string.Empty;
  15 + /// <summary>
  16 + /// 同一个标签绑定的产品名称,用 “,” 分割
  17 + /// </summary>
  18 + public string Products { get; set; } = string.Empty;
16 19  
17 20 public string TemplateName { get; set; } = string.Empty;
18 21  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelPreviewResolveInputVo.cs
... ... @@ -16,9 +16,15 @@ public class LabelPreviewResolveInputVo
16 16 public string? ProductId { get; set; }
17 17  
18 18 /// <summary>
  19 + /// 业务基准时间(用于 DATE/TIME 元素的渲染计算)
  20 + /// 不传则默认使用服务器当前时间
  21 + /// </summary>
  22 + public DateTime? BaseTime { get; set; }
  23 +
  24 + /// <summary>
19 25 /// 打印输入(前端传,用于 PRINT_INPUT 元素)
20 26 /// key 建议使用模板元素的 InputKey
21 27 /// </summary>
22   - public Dictionary<string, object>? PrintInputJson { get; set; }
  28 + public Dictionary<string, object?>? PrintInputJson { get; set; }
23 29 }
24 30  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Picture/PictureUploadInputVo.cs 0 → 100644
  1 +using Microsoft.AspNetCore.Http;
  2 +using Microsoft.AspNetCore.Mvc;
  3 +
  4 +namespace FoodLabeling.Application.Contracts.Dtos.Picture;
  5 +
  6 +public class PictureUploadInputVo
  7 +{
  8 + [FromForm(Name = "file")]
  9 + public IFormFile File { get; set; } = default!;
  10 +
  11 + /// <summary>
  12 + /// 可选子目录(相对路径),例如:category、category/2026-03
  13 + /// </summary>
  14 + [FromForm(Name = "subDir")]
  15 + public string? SubDir { get; set; }
  16 +}
  17 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Picture/PictureUploadOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Picture;
  2 +
  3 +public class PictureUploadOutputDto
  4 +{
  5 + /// <summary>
  6 + /// 可直接保存到业务表的访问 URL(相对路径)
  7 + /// </summary>
  8 + public string Url { get; set; } = string.Empty;
  9 +
  10 + public string FileName { get; set; } = string.Empty;
  11 +
  12 + public long Size { get; set; }
  13 +}
  14 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs
... ... @@ -6,7 +6,7 @@ public class ProductCreateInputVo
6 6  
7 7 public string ProductName { get; set; } = string.Empty;
8 8  
9   - public string? CategoryName { get; set; }
  9 + public string? CategoryId { get; set; }
10 10  
11 11 public string? ProductImageUrl { get; set; }
12 12  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetListOutputDto.cs
... ... @@ -8,6 +8,8 @@ public class ProductGetListOutputDto
8 8  
9 9 public string ProductName { get; set; } = string.Empty;
10 10  
  11 + public string? CategoryId { get; set; }
  12 +
11 13 public string? CategoryName { get; set; }
12 14  
13 15 public string? ProductImageUrl { get; set; }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetOutputDto.cs
... ... @@ -10,6 +10,8 @@ public class ProductGetOutputDto
10 10  
11 11 public string ProductName { get; set; } = string.Empty;
12 12  
  13 + public string? CategoryId { get; set; }
  14 +
13 15 public string? CategoryName { get; set; }
14 16  
15 17 public string? ProductImageUrl { get; set; }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryCreateInputVo.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.ProductCategory;
  2 +
  3 +/// <summary>
  4 +/// 产品模块:新增类别入参
  5 +/// </summary>
  6 +public class ProductCategoryCreateInputVo
  7 +{
  8 + public string CategoryCode { get; set; } = string.Empty;
  9 +
  10 + public string CategoryName { get; set; } = string.Empty;
  11 +
  12 + public string? CategoryPhotoUrl { get; set; }
  13 +
  14 + public bool State { get; set; } = true;
  15 +
  16 + public int OrderNum { get; set; } = 0;
  17 +}
  18 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListInputVo.cs 0 → 100644
  1 +using Volo.Abp.Application.Dtos;
  2 +
  3 +namespace FoodLabeling.Application.Contracts.Dtos.ProductCategory;
  4 +
  5 +/// <summary>
  6 +/// 产品模块:类别分页列表入参
  7 +/// </summary>
  8 +public class ProductCategoryGetListInputVo : PagedAndSortedResultRequestDto
  9 +{
  10 + /// <summary>
  11 + /// 模糊搜索(CategoryCode/CategoryName)
  12 + /// </summary>
  13 + public string? Keyword { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 启用状态过滤
  17 + /// </summary>
  18 + public bool? State { get; set; }
  19 +}
  20 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.ProductCategory;
  2 +
  3 +/// <summary>
  4 +/// 产品模块:类别列表行
  5 +/// </summary>
  6 +public class ProductCategoryGetListOutputDto
  7 +{
  8 + public string Id { get; set; } = string.Empty;
  9 +
  10 + public string CategoryCode { get; set; } = string.Empty;
  11 +
  12 + public string CategoryName { get; set; } = string.Empty;
  13 +
  14 + public string? CategoryPhotoUrl { get; set; }
  15 +
  16 + public bool State { get; set; }
  17 +
  18 + public int OrderNum { get; set; }
  19 +
  20 + public DateTime LastEdited { get; set; }
  21 +}
  22 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.ProductCategory;
  2 +
  3 +/// <summary>
  4 +/// 产品模块:类别详情
  5 +/// </summary>
  6 +public class ProductCategoryGetOutputDto
  7 +{
  8 + public string Id { get; set; } = string.Empty;
  9 +
  10 + public string CategoryCode { get; set; } = string.Empty;
  11 +
  12 + public string CategoryName { get; set; } = string.Empty;
  13 +
  14 + public string? CategoryPhotoUrl { get; set; }
  15 +
  16 + public bool State { get; set; }
  17 +
  18 + public int OrderNum { get; set; }
  19 +}
  20 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryUpdateInputVo.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.ProductCategory;
  2 +
  3 +/// <summary>
  4 +/// 产品模块:编辑类别入参
  5 +/// </summary>
  6 +public class ProductCategoryUpdateInputVo : ProductCategoryCreateInputVo
  7 +{
  8 +}
  9 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelCategoryTreeNodeDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  2 +
  3 +/// <summary>
  4 +/// 第一级:标签分类(fl_label_category)
  5 +/// </summary>
  6 +public class UsAppLabelCategoryTreeNodeDto
  7 +{
  8 + public string Id { get; set; } = string.Empty;
  9 +
  10 + public string CategoryName { get; set; } = string.Empty;
  11 +
  12 + public string? CategoryPhotoUrl { get; set; }
  13 +
  14 + public int OrderNum { get; set; }
  15 +
  16 + public List<UsAppProductCategoryNodeDto> ProductCategories { get; set; } = new();
  17 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPreviewDto.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.Label;
  2 +
  3 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  4 +
  5 +/// <summary>
  6 +/// App 标签预览出参(顶部信息 + 预览模板结构)
  7 +/// </summary>
  8 +public class UsAppLabelPreviewDto
  9 +{
  10 + public string LocationId { get; set; } = string.Empty;
  11 +
  12 + public string LabelCode { get; set; } = string.Empty;
  13 +
  14 + public string? TemplateCode { get; set; }
  15 +
  16 + public string? LabelSizeText { get; set; }
  17 +
  18 + public string? TypeName { get; set; }
  19 +
  20 + public string? ProductName { get; set; }
  21 +
  22 + public string? ProductCategoryName { get; set; }
  23 +
  24 + public string? LabelCategoryName { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 预览图(base64 png,可空;若为空,客户端可用 Template 自行渲染)
  28 + /// </summary>
  29 + public string? PreviewImageBase64Png { get; set; }
  30 +
  31 + /// <summary>
  32 + /// 预览模板结构(与 LabelCanvas/LabelPreviewOnly 结构尽量一致)
  33 + /// </summary>
  34 + public LabelTemplatePreviewDto Template { get; set; } = new();
  35 +}
  36 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPreviewInputVo.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +
  4 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  5 +
  6 +/// <summary>
  7 +/// App 标签预览入参
  8 +/// </summary>
  9 +public class UsAppLabelPreviewInputVo
  10 +{
  11 + /// <summary>
  12 + /// 门店Id(fl_label.LocationId)
  13 + /// </summary>
  14 + public string LocationId { get; set; } = string.Empty;
  15 +
  16 + /// <summary>
  17 + /// 标签编码(fl_label.LabelCode)
  18 + /// </summary>
  19 + public string LabelCode { get; set; } = string.Empty;
  20 +
  21 + /// <summary>
  22 + /// 选择用于预览的产品Id(fl_product.Id)
  23 + /// 不传则默认取该标签绑定的第一个产品
  24 + /// </summary>
  25 + public string? ProductId { get; set; }
  26 +
  27 + /// <summary>
  28 + /// 业务基准时间(用于 DATE/TIME 等元素的计算)
  29 + /// </summary>
  30 + public DateTime? BaseTime { get; set; }
  31 +
  32 + /// <summary>
  33 + /// 打印输入(用于 PRINT_INPUT 元素)
  34 + /// </summary>
  35 + public Dictionary<string, object?>? PrintInputJson { get; set; }
  36 +}
  37 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintInputVo.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +
  4 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  5 +
  6 +/// <summary>
  7 +/// App 打印入参
  8 +/// </summary>
  9 +public class UsAppLabelPrintInputVo
  10 +{
  11 + /// <summary>
  12 + /// 门店Id(fl_label.LocationId)
  13 + /// </summary>
  14 + public string LocationId { get; set; } = string.Empty;
  15 +
  16 + /// <summary>
  17 + /// 标签编码(fl_label.LabelCode)
  18 + /// </summary>
  19 + public string LabelCode { get; set; } = string.Empty;
  20 +
  21 + /// <summary>
  22 + /// 选择用于打印的产品Id(fl_product.Id)
  23 + /// 不传则默认取该标签绑定的第一个产品
  24 + /// </summary>
  25 + public string? ProductId { get; set; }
  26 +
  27 + /// <summary>
  28 + /// 打印份数(&lt;=0 则按 1 处理)
  29 + /// </summary>
  30 + public int PrintQuantity { get; set; } = 1;
  31 +
  32 + /// <summary>
  33 + /// 业务基准时间(用于 DATE/TIME 等元素的计算)
  34 + /// </summary>
  35 + public DateTime? BaseTime { get; set; }
  36 +
  37 + /// <summary>
  38 + /// 打印输入(用于 PRINT_INPUT 元素)
  39 + /// </summary>
  40 + public Dictionary<string, object?>? PrintInputJson { get; set; }
  41 +
  42 + /// <summary>
  43 + /// 打印机Id(可选,若业务需要追踪)
  44 + /// </summary>
  45 + public string? PrinterId { get; set; }
  46 +
  47 + /// <summary>
  48 + /// 打印机蓝牙 MAC(可选)
  49 + /// </summary>
  50 + public string? PrinterMac { get; set; }
  51 +
  52 + /// <summary>
  53 + /// 打印机地址(可选)
  54 + /// </summary>
  55 + public string? PrinterAddress { get; set; }
  56 +}
  57 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  2 +
  3 +/// <summary>
  4 +/// App 打印出参
  5 +/// </summary>
  6 +public class UsAppLabelPrintOutputDto
  7 +{
  8 + public string TaskId { get; set; } = string.Empty;
  9 +
  10 + public int PrintQuantity { get; set; }
  11 +}
  12 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelTypeNodeDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  2 +
  3 +/// <summary>
  4 +/// 第四级:标签种类(对应一条可打印的标签实例)
  5 +/// </summary>
  6 +public class UsAppLabelTypeNodeDto
  7 +{
  8 + public string LabelTypeId { get; set; } = string.Empty;
  9 +
  10 + public string TypeName { get; set; } = string.Empty;
  11 +
  12 + public int OrderNum { get; set; }
  13 +
  14 + /// <summary>业务标签编码,预览/打印流程使用</summary>
  15 + public string LabelCode { get; set; } = string.Empty;
  16 +
  17 + public string? TemplateCode { get; set; }
  18 +
  19 + /// <summary>模板物理尺寸描述,如 2"x2"</summary>
  20 + public string? LabelSizeText { get; set; }
  21 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingProductNodeDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  2 +
  3 +/// <summary>
  4 +/// 第三级:产品
  5 +/// </summary>
  6 +public class UsAppLabelingProductNodeDto
  7 +{
  8 + public string ProductId { get; set; } = string.Empty;
  9 +
  10 + public string ProductName { get; set; } = string.Empty;
  11 +
  12 + public string ProductCode { get; set; } = string.Empty;
  13 +
  14 + public string? ProductImageUrl { get; set; }
  15 +
  16 + /// <summary>副标题(无独立业务字段时:有编码显示编码,否则「无」)</summary>
  17 + public string Subtitle { get; set; } = string.Empty;
  18 +
  19 + public int LabelTypeCount { get; set; }
  20 +
  21 + /// <summary>第四级:该产品在当前标签分类+门店下可选的标签种类</summary>
  22 + public List<UsAppLabelTypeNodeDto> LabelTypes { get; set; } = new();
  23 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelingTreeInputVo.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  2 +
  3 +/// <summary>
  4 +/// App Labeling 四级列表入参
  5 +/// </summary>
  6 +public class UsAppLabelingTreeInputVo
  7 +{
  8 + /// <summary>当前门店 Id(location.Id,Guid 字符串)</summary>
  9 + public string LocationId { get; set; } = string.Empty;
  10 +
  11 + /// <summary>关键词(匹配标签分类/产品分类/产品名/标签类型/标签名称)</summary>
  12 + public string? Keyword { get; set; }
  13 +
  14 + /// <summary>仅展示某一标签分类(侧边栏选中时传);不传则返回全部分类</summary>
  15 + public string? LabelCategoryId { get; set; }
  16 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppProductCategoryNodeDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  2 +
  3 +/// <summary>
  4 +/// 第二级:产品分类(fl_product.CategoryId join fl_product_category)
  5 +/// </summary>
  6 +public class UsAppProductCategoryNodeDto
  7 +{
  8 + /// <summary>产品分类Id;当产品未归类或分类不存在时为空</summary>
  9 + public string? CategoryId { get; set; }
  10 +
  11 + /// <summary>产品分类图片地址;当产品未归类或分类不存在时为空</summary>
  12 + public string? CategoryPhotoUrl { get; set; }
  13 +
  14 + /// <summary>分类显示名;空为「无」</summary>
  15 + public string Name { get; set; } = string.Empty;
  16 +
  17 + public int ItemCount { get; set; }
  18 +
  19 + public List<UsAppLabelingProductNodeDto> Products { get; set; } = new();
  20 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPictureAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.Picture;
  2 +
  3 +namespace FoodLabeling.Application.Contracts.IServices;
  4 +
  5 +public interface IPictureAppService
  6 +{
  7 + Task<PictureUploadOutputDto> UploadCategoryAsync(PictureUploadInputVo input);
  8 +}
  9 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductCategoryAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.Common;
  2 +using FoodLabeling.Application.Contracts.Dtos.ProductCategory;
  3 +using Volo.Abp.Application.Services;
  4 +
  5 +namespace FoodLabeling.Application.Contracts.IServices;
  6 +
  7 +/// <summary>
  8 +/// 产品模块:类别(Categories)接口
  9 +/// </summary>
  10 +public interface IProductCategoryAppService : IApplicationService
  11 +{
  12 + Task<PagedResultWithPageDto<ProductCategoryGetListOutputDto>> GetListAsync(ProductCategoryGetListInputVo input);
  13 +
  14 + Task<ProductCategoryGetOutputDto> GetAsync(string id);
  15 +
  16 + Task<ProductCategoryGetOutputDto> CreateAsync(ProductCategoryCreateInputVo input);
  17 +
  18 + Task<ProductCategoryGetOutputDto> UpdateAsync(string id, ProductCategoryUpdateInputVo input);
  19 +
  20 + Task DeleteAsync(string id);
  21 +}
  22 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  2 +using Volo.Abp.Application.Services;
  3 +
  4 +namespace FoodLabeling.Application.Contracts.IServices;
  5 +
  6 +/// <summary>
  7 +/// App Labeling:四级列表(标签分类 → 产品分类 → 产品 → 标签种类)
  8 +/// </summary>
  9 +public interface IUsAppLabelingAppService : IApplicationService
  10 +{
  11 + /// <summary>
  12 + /// 获取当前门店下四级嵌套树,供移动端 Labeling 首页使用
  13 + /// </summary>
  14 + Task<List<UsAppLabelCategoryTreeNodeDto>> GetLabelingTreeAsync(UsAppLabelingTreeInputVo input);
  15 +
  16 + /// <summary>
  17 + /// App 打印预览:按标签编码解析模板并返回顶部展示字段 + 预览模板结构
  18 + /// </summary>
  19 + Task<UsAppLabelPreviewDto> PreviewAsync(UsAppLabelPreviewInputVo input);
  20 +
  21 + /// <summary>
  22 + /// App 打印:创建打印任务并落库打印明细(fl_label_print_task / fl_label_print_data)
  23 + /// </summary>
  24 + Task<UsAppLabelPrintOutputDto> PrintAsync(UsAppLabelPrintInputVo input);
  25 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelPrintDataDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +/// <summary>
  6 +/// 标签打印数据明细(对应表:fl_label_print_data)
  7 +/// </summary>
  8 +[SugarTable("fl_label_print_data")]
  9 +public class FlLabelPrintDataDbEntity
  10 +{
  11 + [SugarColumn(IsPrimaryKey = true)]
  12 + public string Id { get; set; } = string.Empty;
  13 +
  14 + public bool IsDeleted { get; set; }
  15 +
  16 + public DateTime CreationTime { get; set; }
  17 +
  18 + public string? CreatorId { get; set; }
  19 +
  20 + public string? LastModifierId { get; set; }
  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; }
  39 +}
  40 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelPrintTaskDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +/// <summary>
  6 +/// 标签打印任务(对应表:fl_label_print_task)
  7 +/// </summary>
  8 +[SugarTable("fl_label_print_task")]
  9 +public class FlLabelPrintTaskDbEntity
  10 +{
  11 + [SugarColumn(IsPrimaryKey = true)]
  12 + public string Id { get; set; } = string.Empty;
  13 +
  14 + public bool IsDeleted { get; set; }
  15 +
  16 + public DateTime CreationTime { get; set; }
  17 +
  18 + public string? CreatorId { get; set; }
  19 +
  20 + public string? LastModifierId { get; set; }
  21 +
  22 + public DateTime? LastModificationTime { get; set; }
  23 +
  24 + public string ConcurrencyStamp { get; set; } = string.Empty;
  25 +
  26 + public string? LocationId { get; set; }
  27 +
  28 + public string? LabelCode { get; set; }
  29 +
  30 + public string? ProductId { get; set; }
  31 +
  32 + public string? LabelTypeId { get; set; }
  33 +
  34 + public string? TemplateCode { get; set; }
  35 +
  36 + public int PrintQuantity { get; set; }
  37 +
  38 + public DateTime? BaseTime { get; set; }
  39 +
  40 + public string? PrinterId { get; set; }
  41 +
  42 + public string? PrinterMac { get; set; }
  43 +
  44 + public string? PrinterAddress { get; set; }
  45 +}
  46 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +[SugarTable("fl_product_category")]
  6 +public class FlProductCategoryDbEntity
  7 +{
  8 + [SugarColumn(IsPrimaryKey = true)]
  9 + public string Id { get; set; } = string.Empty;
  10 +
  11 + public bool IsDeleted { get; set; }
  12 +
  13 + public DateTime CreationTime { get; set; }
  14 +
  15 + public string? CreatorId { get; set; }
  16 +
  17 + public string? LastModifierId { get; set; }
  18 +
  19 + public DateTime? LastModificationTime { get; set; }
  20 +
  21 + public string ConcurrencyStamp { get; set; } = string.Empty;
  22 +
  23 + public string CategoryCode { get; set; } = string.Empty;
  24 +
  25 + public string CategoryName { get; set; } = string.Empty;
  26 +
  27 + public string? CategoryPhotoUrl { get; set; }
  28 +
  29 + public bool State { get; set; }
  30 +
  31 + public int OrderNum { get; set; }
  32 +}
  33 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductDbEntity.cs
... ... @@ -14,7 +14,7 @@ public class FlProductDbEntity
14 14  
15 15 public string ProductName { get; set; } = string.Empty;
16 16  
17   - public string? CategoryName { get; set; }
  17 + public string? CategoryId { get; set; }
18 18  
19 19 public string? ProductImageUrl { get; set; }
20 20  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs
... ... @@ -40,55 +40,124 @@ public class LabelAppService : ApplicationService, ILabelAppService
40 40 var labelTypeId = input.LabelTypeId?.Trim();
41 41 var templateCode = input.TemplateCode?.Trim();
42 42  
43   - // 先查 label-product 映射(按产品)
44   - var query = _dbContext.SqlSugarClient.Queryable<FlLabelProductDbEntity, FlLabelDbEntity, FlProductDbEntity, FlLabelCategoryDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity>(
45   - (lp, l, p, c, t, tpl) =>
46   - lp.LabelId == l.Id &&
47   - lp.ProductId == p.Id &&
48   - l.LabelCategoryId == c.Id &&
49   - l.LabelTypeId == t.Id &&
50   - l.TemplateId == tpl.Id)
51   - .Where((lp, l, p, c, t, tpl) => l.IsDeleted == false)
52   - .Where((lp, l, p, c, t, tpl) => !c.IsDeleted)
53   - .Where((lp, l, p, c, t, tpl) => !t.IsDeleted)
54   - .Where((lp, l, p, c, t, tpl) => !tpl.IsDeleted)
55   - .Where((lp, l, p, c, t, tpl) => !p.IsDeleted)
56   - .WhereIF(!string.IsNullOrWhiteSpace(productId), (lp, l, p, c, t, tpl) => lp.ProductId == productId)
57   - .WhereIF(!string.IsNullOrWhiteSpace(locationId), (lp, l, p, c, t, tpl) => l.LocationId == locationId)
58   - .WhereIF(!string.IsNullOrWhiteSpace(labelCategoryId), (lp, l, p, c, t, tpl) => l.LabelCategoryId == labelCategoryId)
59   - .WhereIF(!string.IsNullOrWhiteSpace(labelTypeId), (lp, l, p, c, t, tpl) => l.LabelTypeId == labelTypeId)
60   - .WhereIF(!string.IsNullOrWhiteSpace(templateCode), (lp, l, p, c, t, tpl) => tpl.TemplateCode == templateCode)
61   - .WhereIF(!string.IsNullOrWhiteSpace(keyword),
62   - (lp, l, p, c, t, tpl) =>
  43 + // 目标:列表每行是“标签”,同一个标签下的 products 以 “,” 拼接展示
  44 + // 因此需要按 label 维度分页(避免 label-product join 导致重复行与分页错乱)。
  45 +
  46 + var labelIdsQuery = _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>()
  47 + .Where(l => !l.IsDeleted)
  48 + .WhereIF(!string.IsNullOrWhiteSpace(locationId), l => l.LocationId == locationId)
  49 + .WhereIF(!string.IsNullOrWhiteSpace(labelCategoryId), l => l.LabelCategoryId == labelCategoryId)
  50 + .WhereIF(!string.IsNullOrWhiteSpace(labelTypeId), l => l.LabelTypeId == labelTypeId)
  51 + .WhereIF(input.State != null, l => l.State == input.State);
  52 +
  53 + if (!string.IsNullOrWhiteSpace(templateCode))
  54 + {
  55 + labelIdsQuery = labelIdsQuery
  56 + .InnerJoin<FlLabelTemplateDbEntity>((l, tpl) => l.TemplateId == tpl.Id)
  57 + .Where((l, tpl) => !tpl.IsDeleted && tpl.TemplateCode == templateCode)
  58 + .Select((l, tpl) => l);
  59 + }
  60 +
  61 + // 按产品筛选:存在 label-product 关联即可
  62 + if (!string.IsNullOrWhiteSpace(productId))
  63 + {
  64 + labelIdsQuery = labelIdsQuery
  65 + .InnerJoin<FlLabelProductDbEntity>((l, lp) => lp.LabelId == l.Id)
  66 + .Where((l, lp) => lp.ProductId == productId)
  67 + .Select((l, lp) => l);
  68 + }
  69 +
  70 + // 关键字:匹配 labelName/categoryName/typeName/templateName/productName
  71 + if (!string.IsNullOrWhiteSpace(keyword))
  72 + {
  73 + labelIdsQuery = labelIdsQuery
  74 + .LeftJoin<FlLabelCategoryDbEntity>((l, c) => l.LabelCategoryId == c.Id)
  75 + .LeftJoin<FlLabelTypeDbEntity>((l, c, t) => l.LabelTypeId == t.Id)
  76 + .LeftJoin<FlLabelTemplateDbEntity>((l, c, t, tpl) => l.TemplateId == tpl.Id)
  77 + .LeftJoin<FlLabelProductDbEntity>((l, c, t, tpl, lp) => lp.LabelId == l.Id)
  78 + .LeftJoin<FlProductDbEntity>((l, c, t, tpl, lp, p) => lp.ProductId == p.Id)
  79 + .Where((l, c, t, tpl, lp, p) =>
63 80 l.LabelName.Contains(keyword!) ||
64   - p.ProductName.Contains(keyword!) ||
65   - c.CategoryName.Contains(keyword!) ||
66   - t.TypeName.Contains(keyword!))
67   - .WhereIF(input.State != null, (lp, l, p, c, t, tpl) => l.State == input.State)
68   - .OrderByDescending((lp, l, p, c, t, tpl) => l.LastModificationTime ?? l.CreationTime);
  81 + (c.CategoryName != null && c.CategoryName.Contains(keyword!)) ||
  82 + (t.TypeName != null && t.TypeName.Contains(keyword!)) ||
  83 + (tpl.TemplateName != null && tpl.TemplateName.Contains(keyword!)) ||
  84 + (p.ProductName != null && p.ProductName.Contains(keyword!)))
  85 + .Select((l, c, t, tpl, lp, p) => l);
  86 + }
69 87  
  88 + // 排序(优先外部 Sorting,否则按最后编辑倒序)
70 89 if (!string.IsNullOrWhiteSpace(input.Sorting))
71 90 {
72   - query = query.OrderBy(input.Sorting);
  91 + labelIdsQuery = labelIdsQuery.OrderBy(input.Sorting);
  92 + }
  93 + else
  94 + {
  95 + labelIdsQuery = labelIdsQuery.OrderByDescending(l => l.LastModificationTime ?? l.CreationTime);
73 96 }
74 97  
75   - var entities = await query.Select((lp, l, p, c, t, tpl) => new
  98 + var pageLabelIds = await labelIdsQuery
  99 + .Select(l => l.Id)
  100 + .Distinct()
  101 + .ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
  102 +
  103 + if (pageLabelIds.Count == 0)
  104 + {
  105 + return new PagedResultWithPageDto<LabelGetListOutputDto>
76 106 {
77   - LabelCode = l.LabelCode,
78   - LabelName = l.LabelName,
79   - LocationId = l.LocationId,
80   - LocationName = (string?)null, // later fill
  107 + PageIndex = 1,
  108 + PageSize = input.MaxResultCount,
  109 + TotalCount = total,
  110 + TotalPages = 0,
  111 + Items = new List<LabelGetListOutputDto>()
  112 + };
  113 + }
  114 +
  115 + // 查询标签基础信息(分类/类型/模板)
  116 + var labelRows = await _dbContext.SqlSugarClient
  117 + .Queryable<FlLabelDbEntity, FlLabelCategoryDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity>(
  118 + (l, c, t, tpl) => l.LabelCategoryId == c.Id && l.LabelTypeId == t.Id && l.TemplateId == tpl.Id)
  119 + .Where((l, c, t, tpl) => pageLabelIds.Contains(l.Id))
  120 + .Where((l, c, t, tpl) => !l.IsDeleted && !c.IsDeleted && !t.IsDeleted && !tpl.IsDeleted)
  121 + .Select((l, c, t, tpl) => new
  122 + {
  123 + l.Id,
  124 + l.LabelCode,
  125 + l.LabelName,
  126 + l.LocationId,
81 127 LabelCategoryName = c.CategoryName,
82   - ProductCategoryName = p.CategoryName,
83   - ProductName = p.ProductName,
84   - TemplateName = tpl.TemplateName,
85 128 LabelTypeName = t.TypeName,
86   - State = l.State,
  129 + TemplateName = tpl.TemplateName,
  130 + l.State,
87 131 LastEdited = l.LastModificationTime ?? l.CreationTime
88 132 })
89   - .ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
  133 + .ToListAsync();
  134 +
  135 + // 按分页顺序输出
  136 + var labelMap = labelRows.ToDictionary(x => x.Id, x => x);
  137 + var orderedLabels = pageLabelIds.Where(id => labelMap.ContainsKey(id)).Select(id => labelMap[id]).ToList();
  138 +
  139 + // 查询 products 并拼接
  140 + var productRows = await _dbContext.SqlSugarClient
  141 + .Queryable<FlLabelProductDbEntity>()
  142 + .InnerJoin<FlLabelDbEntity>((lp, l) => lp.LabelId == l.Id)
  143 + .InnerJoin<FlProductDbEntity>((lp, l, p) => lp.ProductId == p.Id)
  144 + .LeftJoin<FlProductCategoryDbEntity>((lp, l, p, pc) => p.CategoryId == pc.Id)
  145 + .Where((lp, l, p, pc) => pageLabelIds.Contains(lp.LabelId))
  146 + .Where((lp, l, p, pc) => !l.IsDeleted && !p.IsDeleted)
  147 + .Select((lp, l, p, pc) => new { lp.LabelId, p.ProductName, ProductCategoryName = pc.CategoryName })
  148 + .ToListAsync();
90 149  
91   - var locationIds = entities
  150 + var productsMap = productRows
  151 + .GroupBy(x => x.LabelId)
  152 + .ToDictionary(
  153 + g => g.Key,
  154 + g => new
  155 + {
  156 + Products = string.Join(",", g.Select(x => x.ProductName ?? string.Empty).Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()),
  157 + ProductCategoryName = string.Join(",", g.Select(x => x.ProductCategoryName ?? string.Empty).Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct())
  158 + });
  159 +
  160 + var locationIds = orderedLabels
92 161 .Select(x => x.LocationId)
93 162 .Where(x => !string.IsNullOrWhiteSpace(x))
94 163 .Select(x => x!.Trim())
... ... @@ -109,21 +178,23 @@ public class LabelAppService : ApplicationService, ILabelAppService
109 178 }
110 179 }
111 180  
112   - var items = entities.Select(x =>
  181 + var items = orderedLabels.Select(x =>
113 182 {
114 183 var locationName = string.Empty;
115 184 if (!string.IsNullOrWhiteSpace(x.LocationId) && locationMap.TryGetValue(x.LocationId!, out var loc))
116 185 {
117 186 locationName = loc.LocationName ?? loc.LocationCode;
118 187 }
  188 + var products = productsMap.TryGetValue(x.Id, out var prod) ? prod.Products : string.Empty;
  189 + var productCategoryNameValue = productsMap.TryGetValue(x.Id, out var prod2) ? prod2.ProductCategoryName : string.Empty;
119 190 return new LabelGetListOutputDto
120 191 {
121 192 Id = x.LabelCode ?? string.Empty,
122 193 LabelName = x.LabelName ?? string.Empty,
123 194 LocationName = string.IsNullOrWhiteSpace(locationName) ? "无" : locationName,
124 195 LabelCategoryName = x.LabelCategoryName ?? string.Empty,
125   - ProductCategoryName = x.ProductCategoryName ?? string.Empty,
126   - ProductName = x.ProductName ?? string.Empty,
  196 + ProductCategoryName = string.IsNullOrWhiteSpace(productCategoryNameValue) ? "无" : productCategoryNameValue,
  197 + Products = products,
127 198 TemplateName = x.TemplateName ?? string.Empty,
128 199 LabelTypeName = x.LabelTypeName ?? string.Empty,
129 200 State = x.State,
... ... @@ -406,6 +477,8 @@ public class LabelAppService : ApplicationService, ILabelAppService
406 477 throw new UserFriendlyException("labelCode不能为空");
407 478 }
408 479  
  480 + var baseTime = input?.BaseTime ?? DateTime.Now;
  481 +
409 482 var label = await _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>()
410 483 .FirstAsync(x => !x.IsDeleted && x.LabelCode == labelCode);
411 484 if (label is null)
... ... @@ -572,14 +645,14 @@ public class LabelAppService : ApplicationService, ILabelAppService
572 645 case "DATE":
573 646 {
574 647 var offsetDays = cfg.TryGetValue("offsetDays", out var od) ? TryGetInt(od, 0) : 0;
575   - var dt = DateTime.Today.AddDays(offsetDays);
  648 + var dt = baseTime.Date.AddDays(offsetDays);
576 649 UpsertConfigValue(cfg, "format", dt.ToString("yyyy-MM-dd"));
577 650 cfg.Remove("inputType");
578 651 }
579 652 break;
580 653 case "TIME":
581 654 {
582   - var dt = DateTime.Now;
  655 + var dt = baseTime;
583 656 UpsertConfigValue(cfg, "format", dt.ToString("HH:mm"));
584 657 cfg.Remove("inputType");
585 658 }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PictureAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.Picture;
  2 +using FoodLabeling.Application.Contracts.IServices;
  3 +using Microsoft.AspNetCore.Http;
  4 +using Microsoft.AspNetCore.Mvc;
  5 +using Microsoft.Extensions.Hosting;
  6 +using Volo.Abp;
  7 +using Volo.Abp.Application.Services;
  8 +using Volo.Abp.Guids;
  9 +
  10 +namespace FoodLabeling.Application.Services;
  11 +
  12 +public class PictureAppService : ApplicationService, IPictureAppService
  13 +{
  14 + private const long MaxSizeBytes = 5 * 1024 * 1024;
  15 + private static readonly HashSet<string> AllowedExtensions = new(StringComparer.OrdinalIgnoreCase)
  16 + {
  17 + ".jpg",
  18 + ".jpeg",
  19 + ".png",
  20 + ".webp",
  21 + ".gif"
  22 + };
  23 +
  24 + private readonly IGuidGenerator _guidGenerator;
  25 + private readonly IHostEnvironment _hostEnvironment;
  26 +
  27 + public PictureAppService(IGuidGenerator guidGenerator, IHostEnvironment hostEnvironment)
  28 + {
  29 + _guidGenerator = guidGenerator;
  30 + _hostEnvironment = hostEnvironment;
  31 + }
  32 +
  33 + /// <summary>
  34 + /// 上传类别图片(保存到 /www/wwwroot/FoodLabelingManagementUs/picture)
  35 + /// </summary>
  36 + /// <remarks>返回的 Url 可直接保存到 CategoryPhotoUrl。</remarks>
  37 + [HttpPost]
  38 + [Consumes("multipart/form-data")]
  39 + [Route("/api/app/picture/category/upload")]
  40 + public async Task<PictureUploadOutputDto> UploadCategoryAsync([FromForm] PictureUploadInputVo input)
  41 + {
  42 + if (input.File is null || input.File.Length <= 0)
  43 + {
  44 + throw new UserFriendlyException("请选择要上传的图片文件");
  45 + }
  46 +
  47 + if (input.File.Length > MaxSizeBytes)
  48 + {
  49 + throw new UserFriendlyException("图片大小不能超过5MB");
  50 + }
  51 +
  52 + var ext = Path.GetExtension(input.File.FileName ?? string.Empty);
  53 + if (string.IsNullOrWhiteSpace(ext) || !AllowedExtensions.Contains(ext))
  54 + {
  55 + throw new UserFriendlyException("仅支持上传 jpg/jpeg/png/webp/gif 格式图片");
  56 + }
  57 +
  58 + var subDir = NormalizeSubDir(input.SubDir);
  59 + var saveRoot = ResolvePictureRoot();
  60 + var saveDir = string.IsNullOrWhiteSpace(subDir) ? saveRoot : Path.Combine(saveRoot, subDir);
  61 + Directory.CreateDirectory(saveDir);
  62 +
  63 + var fileName = $"{DateTime.Now:yyyyMMddHHmmss}_{_guidGenerator.Create():N}{ext.ToLowerInvariant()}";
  64 + var savePath = Path.Combine(saveDir, fileName);
  65 +
  66 + await using (var stream = new FileStream(savePath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
  67 + {
  68 + await input.File.CopyToAsync(stream);
  69 + }
  70 +
  71 + var url = BuildPictureUrl(subDir, fileName);
  72 + return new PictureUploadOutputDto
  73 + {
  74 + Url = url,
  75 + FileName = fileName,
  76 + Size = input.File.Length
  77 + };
  78 + }
  79 +
  80 + private string ResolvePictureRoot()
  81 + {
  82 + var linuxPictureRoot = "/www/wwwroot/FoodLabelingManagementUs/picture";
  83 + var webRootPicture = Path.Combine(_hostEnvironment.ContentRootPath, "wwwroot", "FoodLabelingManagementUs", "picture");
  84 + return Directory.Exists(linuxPictureRoot) ? linuxPictureRoot : webRootPicture;
  85 + }
  86 +
  87 + private static string NormalizeSubDir(string? subDir)
  88 + {
  89 + if (string.IsNullOrWhiteSpace(subDir))
  90 + {
  91 + return string.Empty;
  92 + }
  93 +
  94 + var s = subDir.Trim().Replace('\\', '/');
  95 + while (s.StartsWith('/'))
  96 + {
  97 + s = s[1..];
  98 + }
  99 +
  100 + if (s.Contains("..", StringComparison.Ordinal))
  101 + {
  102 + throw new UserFriendlyException("subDir 不能包含 ..");
  103 + }
  104 +
  105 + return s;
  106 + }
  107 +
  108 + private static string BuildPictureUrl(string? subDir, string fileName)
  109 + {
  110 + var s = string.IsNullOrWhiteSpace(subDir) ? string.Empty : $"/{subDir.Trim().Replace('\\', '/').Trim('/')}";
  111 + return $"/picture{s}/{fileName}";
  112 + }
  113 +}
  114 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs
... ... @@ -31,14 +31,22 @@ public class ProductAppService : ApplicationService, IProductAppService
31 31 RefAsync<int> total = 0;
32 32 var keyword = input.Keyword?.Trim();
33 33  
34   - var query = _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>()
  34 + var query = _dbContext.SqlSugarClient
  35 + .Queryable<FlProductDbEntity>()
35 36 .Where(x => !x.IsDeleted)
36   - .WhereIF(!string.IsNullOrWhiteSpace(keyword), x =>
37   - x.ProductCode.Contains(keyword!) ||
38   - x.ProductName.Contains(keyword!) ||
39   - (x.CategoryName != null && x.CategoryName.Contains(keyword!)))
40 37 .WhereIF(input.State != null, x => x.State == input.State);
41 38  
  39 + if (!string.IsNullOrWhiteSpace(keyword))
  40 + {
  41 + query = query
  42 + .LeftJoin<FlProductCategoryDbEntity>((p, c) => p.CategoryId == c.Id)
  43 + .Where((p, c) =>
  44 + p.ProductCode.Contains(keyword!) ||
  45 + p.ProductName.Contains(keyword!) ||
  46 + (c.CategoryName != null && c.CategoryName.Contains(keyword!)))
  47 + .Select((p, c) => p);
  48 + }
  49 +
42 50 if (!string.IsNullOrWhiteSpace(input.Sorting))
43 51 {
44 52 query = query.OrderBy(input.Sorting);
... ... @@ -65,15 +73,41 @@ public class ProductAppService : ApplicationService, IProductAppService
65 73 countMap = countRows.ToDictionary(x => x.ProductId, x => (long)x.Count);
66 74 }
67 75  
68   - var items = entities.Select(x => new ProductGetListOutputDto
  76 + var categoryIds = entities
  77 + .Select(x => x.CategoryId)
  78 + .Where(x => !string.IsNullOrWhiteSpace(x))
  79 + .Select(x => x!.Trim())
  80 + .Distinct()
  81 + .ToList();
  82 +
  83 + var categoryMap = new Dictionary<string, FlProductCategoryDbEntity>();
  84 + if (categoryIds.Count > 0)
69 85 {
70   - Id = x.Id,
71   - ProductCode = x.ProductCode,
72   - ProductName = x.ProductName,
73   - CategoryName = x.CategoryName,
74   - ProductImageUrl = x.ProductImageUrl,
75   - State = x.State,
76   - NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0
  86 + var categories = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
  87 + .Where(x => !x.IsDeleted && categoryIds.Contains(x.Id))
  88 + .ToListAsync();
  89 + categoryMap = categories.ToDictionary(x => x.Id, x => x);
  90 + }
  91 +
  92 + var items = entities.Select(x =>
  93 + {
  94 + var categoryName = "无";
  95 + if (!string.IsNullOrWhiteSpace(x.CategoryId) && categoryMap.TryGetValue(x.CategoryId.Trim(), out var c))
  96 + {
  97 + categoryName = string.IsNullOrWhiteSpace(c.CategoryName) ? "无" : c.CategoryName.Trim();
  98 + }
  99 +
  100 + return new ProductGetListOutputDto
  101 + {
  102 + Id = x.Id,
  103 + ProductCode = x.ProductCode,
  104 + ProductName = x.ProductName,
  105 + CategoryId = x.CategoryId,
  106 + CategoryName = categoryName,
  107 + ProductImageUrl = x.ProductImageUrl,
  108 + State = x.State,
  109 + NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0
  110 + };
77 111 }).ToList();
78 112  
79 113 return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items);
... ... @@ -95,6 +129,17 @@ public class ProductAppService : ApplicationService, IProductAppService
95 129 throw new UserFriendlyException("产品不存在");
96 130 }
97 131  
  132 + string? categoryName = "无";
  133 + if (!string.IsNullOrWhiteSpace(entity.CategoryId))
  134 + {
  135 + var c = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
  136 + .FirstAsync(x => !x.IsDeleted && x.Id == entity.CategoryId);
  137 + if (c is not null && !string.IsNullOrWhiteSpace(c.CategoryName))
  138 + {
  139 + categoryName = c.CategoryName.Trim();
  140 + }
  141 + }
  142 +
98 143 var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLocationProductDbEntity>()
99 144 .Where(x => x.ProductId == productId)
100 145 .Select(x => x.LocationId)
... ... @@ -106,7 +151,8 @@ public class ProductAppService : ApplicationService, IProductAppService
106 151 Id = entity.Id,
107 152 ProductCode = entity.ProductCode,
108 153 ProductName = entity.ProductName,
109   - CategoryName = entity.CategoryName,
  154 + CategoryId = entity.CategoryId,
  155 + CategoryName = categoryName,
110 156 ProductImageUrl = entity.ProductImageUrl,
111 157 State = entity.State,
112 158 LocationIds = locationIds
... ... @@ -136,7 +182,7 @@ public class ProductAppService : ApplicationService, IProductAppService
136 182 IsDeleted = false,
137 183 ProductCode = code,
138 184 ProductName = name,
139   - CategoryName = input.CategoryName?.Trim(),
  185 + CategoryId = input.CategoryId?.Trim(),
140 186 ProductImageUrl = input.ProductImageUrl?.Trim(),
141 187 State = input.State
142 188 };
... ... @@ -177,7 +223,7 @@ public class ProductAppService : ApplicationService, IProductAppService
177 223  
178 224 entity.ProductCode = code;
179 225 entity.ProductName = name;
180   - entity.CategoryName = input.CategoryName?.Trim();
  226 + entity.CategoryId = input.CategoryId?.Trim();
181 227 entity.ProductImageUrl = input.ProductImageUrl?.Trim();
182 228 entity.State = input.State;
183 229  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.Common;
  2 +using FoodLabeling.Application.Contracts.Dtos.ProductCategory;
  3 +using FoodLabeling.Application.Contracts.IServices;
  4 +using FoodLabeling.Application.Services.DbModels;
  5 +using SqlSugar;
  6 +using Volo.Abp;
  7 +using Volo.Abp.Application.Services;
  8 +using Volo.Abp.Guids;
  9 +using Yi.Framework.SqlSugarCore.Abstractions;
  10 +
  11 +namespace FoodLabeling.Application.Services;
  12 +
  13 +/// <summary>
  14 +/// 产品模块:类别(Categories)服务,对外仅在 food-labeling-us 暴露
  15 +/// </summary>
  16 +public class ProductCategoryAppService : ApplicationService, IProductCategoryAppService
  17 +{
  18 + private readonly ISqlSugarDbContext _dbContext;
  19 + private readonly IGuidGenerator _guidGenerator;
  20 +
  21 + public ProductCategoryAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator)
  22 + {
  23 + _dbContext = dbContext;
  24 + _guidGenerator = guidGenerator;
  25 + }
  26 +
  27 + /// <summary>
  28 + /// 类别分页列表
  29 + /// </summary>
  30 + public async Task<PagedResultWithPageDto<ProductCategoryGetListOutputDto>> GetListAsync(ProductCategoryGetListInputVo input)
  31 + {
  32 + RefAsync<int> total = 0;
  33 + var keyword = input.Keyword?.Trim();
  34 +
  35 + var query = _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
  36 + .Where(x => !x.IsDeleted)
  37 + .WhereIF(!string.IsNullOrWhiteSpace(keyword),
  38 + x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!))
  39 + .WhereIF(input.State != null, x => x.State == input.State);
  40 +
  41 + // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column
  42 + // 同时避免将 input.Sorting 原样拼接到 SQL(存在注入风险)
  43 + if (!string.IsNullOrWhiteSpace(input.Sorting))
  44 + {
  45 + var sorting = input.Sorting.Trim();
  46 + if (sorting.Equals("OrderNum desc", StringComparison.OrdinalIgnoreCase))
  47 + {
  48 + query = query.OrderByDescending(x => x.OrderNum);
  49 + }
  50 + else if (sorting.Equals("OrderNum asc", StringComparison.OrdinalIgnoreCase))
  51 + {
  52 + query = query.OrderBy(x => x.OrderNum);
  53 + }
  54 + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase))
  55 + {
  56 + query = query.OrderByDescending(x => x.CreationTime);
  57 + }
  58 + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase))
  59 + {
  60 + query = query.OrderBy(x => x.CreationTime);
  61 + }
  62 + else
  63 + {
  64 + // 不识别的排序统一走默认
  65 + query = query.OrderByDescending(x => x.OrderNum).OrderByDescending(x => x.CreationTime);
  66 + }
  67 + }
  68 + else
  69 + {
  70 + query = query.OrderByDescending(x => x.OrderNum).OrderByDescending(x => x.CreationTime);
  71 + }
  72 +
  73 + var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
  74 +
  75 + var items = entities.Select(x => new ProductCategoryGetListOutputDto
  76 + {
  77 + Id = x.Id,
  78 + CategoryCode = x.CategoryCode,
  79 + CategoryName = x.CategoryName,
  80 + CategoryPhotoUrl = x.CategoryPhotoUrl,
  81 + State = x.State,
  82 + OrderNum = x.OrderNum,
  83 + LastEdited = x.LastModificationTime ?? x.CreationTime
  84 + }).ToList();
  85 +
  86 + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items);
  87 + }
  88 +
  89 + /// <summary>
  90 + /// 类别详情
  91 + /// </summary>
  92 + public async Task<ProductCategoryGetOutputDto> GetAsync(string id)
  93 + {
  94 + var entity = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
  95 + .FirstAsync(x => x.Id == id && !x.IsDeleted);
  96 + if (entity is null)
  97 + {
  98 + throw new UserFriendlyException("类别不存在");
  99 + }
  100 +
  101 + return MapToGetOutput(entity);
  102 + }
  103 +
  104 + /// <summary>
  105 + /// 新增类别
  106 + /// </summary>
  107 + public async Task<ProductCategoryGetOutputDto> CreateAsync(ProductCategoryCreateInputVo input)
  108 + {
  109 + var code = input.CategoryCode?.Trim();
  110 + var name = input.CategoryName?.Trim();
  111 + if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(name))
  112 + {
  113 + throw new UserFriendlyException("类别编码和名称不能为空");
  114 + }
  115 +
  116 + var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
  117 + .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name));
  118 + if (duplicated)
  119 + {
  120 + throw new UserFriendlyException("类别编码或名称已存在");
  121 + }
  122 +
  123 + var now = DateTime.Now;
  124 + var currentUserId = CurrentUser?.Id?.ToString();
  125 + var entity = new FlProductCategoryDbEntity
  126 + {
  127 + Id = _guidGenerator.Create().ToString(),
  128 + IsDeleted = false,
  129 + CreationTime = now,
  130 + CreatorId = currentUserId,
  131 + LastModifierId = currentUserId,
  132 + LastModificationTime = now,
  133 + ConcurrencyStamp = _guidGenerator.Create().ToString("N"),
  134 + CategoryCode = code,
  135 + CategoryName = name,
  136 + CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(),
  137 + State = input.State,
  138 + OrderNum = input.OrderNum
  139 + };
  140 +
  141 + await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  142 + return await GetAsync(entity.Id);
  143 + }
  144 +
  145 + /// <summary>
  146 + /// 编辑类别
  147 + /// </summary>
  148 + public async Task<ProductCategoryGetOutputDto> UpdateAsync(string id, ProductCategoryUpdateInputVo input)
  149 + {
  150 + var entity = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
  151 + .FirstAsync(x => x.Id == id && !x.IsDeleted);
  152 + if (entity is null)
  153 + {
  154 + throw new UserFriendlyException("类别不存在");
  155 + }
  156 +
  157 + var code = input.CategoryCode?.Trim();
  158 + var name = input.CategoryName?.Trim();
  159 + if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(name))
  160 + {
  161 + throw new UserFriendlyException("类别编码和名称不能为空");
  162 + }
  163 +
  164 + var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
  165 + .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name));
  166 + if (duplicated)
  167 + {
  168 + throw new UserFriendlyException("类别编码或名称已存在");
  169 + }
  170 +
  171 + entity.CategoryCode = code;
  172 + entity.CategoryName = name;
  173 + entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim();
  174 + entity.State = input.State;
  175 + entity.OrderNum = input.OrderNum;
  176 + entity.LastModificationTime = DateTime.Now;
  177 + entity.LastModifierId = CurrentUser?.Id?.ToString();
  178 +
  179 + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  180 + return await GetAsync(id);
  181 + }
  182 +
  183 + /// <summary>
  184 + /// 删除类别(逻辑删除)
  185 + /// </summary>
  186 + public async Task DeleteAsync(string id)
  187 + {
  188 + var entity = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
  189 + .FirstAsync(x => x.Id == id && !x.IsDeleted);
  190 + if (entity is null)
  191 + {
  192 + return;
  193 + }
  194 +
  195 + // 若被产品引用则不允许删除
  196 + var usedByProduct = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>()
  197 + .AnyAsync(x => !x.IsDeleted && x.CategoryId == id);
  198 + if (usedByProduct)
  199 + {
  200 + throw new UserFriendlyException("该类别已被产品引用,无法删除");
  201 + }
  202 +
  203 + entity.IsDeleted = true;
  204 + entity.LastModificationTime = DateTime.Now;
  205 + entity.LastModifierId = CurrentUser?.Id?.ToString();
  206 + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  207 + }
  208 +
  209 + private static ProductCategoryGetOutputDto MapToGetOutput(FlProductCategoryDbEntity x)
  210 + {
  211 + return new ProductCategoryGetOutputDto
  212 + {
  213 + Id = x.Id,
  214 + CategoryCode = x.CategoryCode,
  215 + CategoryName = x.CategoryName,
  216 + CategoryPhotoUrl = x.CategoryPhotoUrl,
  217 + State = x.State,
  218 + OrderNum = x.OrderNum
  219 + };
  220 + }
  221 +
  222 + private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items)
  223 + {
  224 + var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount;
  225 + var pageIndex = pageSize <= 0 ? 1 : (skipCount / pageSize) + 1;
  226 + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize);
  227 + return new PagedResultWithPageDto<T>
  228 + {
  229 + PageIndex = pageIndex,
  230 + PageSize = pageSize,
  231 + TotalCount = total,
  232 + TotalPages = totalPages,
  233 + Items = items
  234 + };
  235 + }
  236 +}
  237 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs 0 → 100644
  1 +using System;
  2 +using System.Collections.Generic;
  3 +using System.Globalization;
  4 +using System.Linq;
  5 +using System.Text.Json;
  6 +using System.Threading.Tasks;
  7 +using FoodLabeling.Application.Contracts.Dtos.Label;
  8 +using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  9 +using FoodLabeling.Application.Contracts.IServices;
  10 +using FoodLabeling.Application.Services.DbModels;
  11 +using Microsoft.AspNetCore.Authorization;
  12 +using SqlSugar;
  13 +using Volo.Abp;
  14 +using Volo.Abp.Application.Services;
  15 +using Volo.Abp.Guids;
  16 +using Volo.Abp.Uow;
  17 +using Yi.Framework.SqlSugarCore.Abstractions;
  18 +
  19 +namespace FoodLabeling.Application.Services;
  20 +
  21 +/// <summary>
  22 +/// App Labeling:四级列表(标签分类 → 产品分类 → 产品 → 标签种类)
  23 +/// </summary>
  24 +public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppService
  25 +{
  26 + private readonly ISqlSugarDbContext _dbContext;
  27 + private readonly ILabelAppService _labelAppService;
  28 + private readonly IGuidGenerator _guidGenerator;
  29 +
  30 + public UsAppLabelingAppService(ISqlSugarDbContext dbContext, ILabelAppService labelAppService, IGuidGenerator guidGenerator)
  31 + {
  32 + _dbContext = dbContext;
  33 + _labelAppService = labelAppService;
  34 + _guidGenerator = guidGenerator;
  35 + }
  36 +
  37 + /// <summary>
  38 + /// 获取当前门店下四级嵌套数据
  39 + /// </summary>
  40 + /// <remarks>
  41 + /// L1 标签分类 fl_label_category;L2 产品分类 fl_product.CategoryId join fl_product_category;
  42 + /// L3 产品;L4 与该门店、该标签分类、该产品关联的标签实例(fl_label + fl_label_type)。
  43 + /// </remarks>
  44 + [Authorize]
  45 + public virtual async Task<List<UsAppLabelCategoryTreeNodeDto>> GetLabelingTreeAsync(UsAppLabelingTreeInputVo input)
  46 + {
  47 + if (string.IsNullOrWhiteSpace(input.LocationId))
  48 + {
  49 + throw new UserFriendlyException("门店Id不能为空");
  50 + }
  51 +
  52 + var locationId = input.LocationId.Trim();
  53 + var keyword = input.Keyword?.Trim();
  54 + var filterCategoryId = input.LabelCategoryId?.Trim();
  55 +
  56 + var productIds = await _dbContext.SqlSugarClient.Queryable<FlLocationProductDbEntity>()
  57 + .Where(x => x.LocationId == locationId)
  58 + .Select(x => x.ProductId)
  59 + .ToListAsync();
  60 +
  61 + if (productIds.Count == 0)
  62 + {
  63 + return new List<UsAppLabelCategoryTreeNodeDto>();
  64 + }
  65 +
  66 + var query = BuildLabelingJoinQuery(locationId, productIds, filterCategoryId, keyword);
  67 +
  68 + var raw = await query
  69 + .Select((lp, l, p, c, t, tpl, pc) => new LabelingTreeRow
  70 + {
  71 + LabelCategoryId = c.Id,
  72 + LabelCategoryName = c.CategoryName,
  73 + LabelCategoryPhotoUrl = c.CategoryPhotoUrl,
  74 + LabelCategoryOrderNum = c.OrderNum,
  75 + ProductCategoryId = p.CategoryId,
  76 + ProductCategoryName = pc.CategoryName,
  77 + ProductCategoryPhotoUrl = pc.CategoryPhotoUrl,
  78 + ProductId = p.Id,
  79 + ProductName = p.ProductName,
  80 + ProductCode = p.ProductCode,
  81 + ProductImageUrl = p.ProductImageUrl,
  82 + LabelTypeId = t.Id,
  83 + TypeName = t.TypeName,
  84 + TypeOrderNum = t.OrderNum,
  85 + LabelCode = l.LabelCode,
  86 + TemplateCode = tpl.TemplateCode,
  87 + TemplateWidth = tpl.Width,
  88 + TemplateHeight = tpl.Height,
  89 + TemplateUnit = tpl.Unit
  90 + })
  91 + .ToListAsync();
  92 +
  93 + if (raw.Count == 0)
  94 + {
  95 + return new List<UsAppLabelCategoryTreeNodeDto>();
  96 + }
  97 +
  98 + var byL1 = raw.GroupBy(x => new
  99 + {
  100 + x.LabelCategoryId,
  101 + x.LabelCategoryName,
  102 + x.LabelCategoryPhotoUrl,
  103 + x.LabelCategoryOrderNum
  104 + }).OrderBy(g => g.Key.LabelCategoryOrderNum).ThenBy(g => g.Key.LabelCategoryName);
  105 +
  106 + var result = new List<UsAppLabelCategoryTreeNodeDto>();
  107 + foreach (var g1 in byL1)
  108 + {
  109 + var l1 = new UsAppLabelCategoryTreeNodeDto
  110 + {
  111 + Id = g1.Key.LabelCategoryId,
  112 + CategoryName = g1.Key.LabelCategoryName ?? string.Empty,
  113 + CategoryPhotoUrl = g1.Key.LabelCategoryPhotoUrl,
  114 + OrderNum = g1.Key.LabelCategoryOrderNum,
  115 + ProductCategories = new List<UsAppProductCategoryNodeDto>()
  116 + };
  117 +
  118 + var byL2 = g1.GroupBy(x =>
  119 + {
  120 + var categoryId = NormalizeNullableId(x.ProductCategoryId);
  121 + if (categoryId is null)
  122 + {
  123 + return new
  124 + {
  125 + CategoryId = (string?)null,
  126 + CategoryName = "无",
  127 + CategoryPhotoUrl = (string?)null
  128 + };
  129 + }
  130 +
  131 + var categoryName = NormalizeCategoryName(x.ProductCategoryName);
  132 + var categoryPhotoUrl = NormalizeNullableUrl(x.ProductCategoryPhotoUrl);
  133 + return new
  134 + {
  135 + CategoryId = (string?)categoryId,
  136 + CategoryName = categoryName,
  137 + CategoryPhotoUrl = categoryPhotoUrl
  138 + };
  139 + })
  140 + .OrderBy(g => g.Key.CategoryName);
  141 +
  142 + foreach (var g2 in byL2)
  143 + {
  144 + var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName);
  145 + var l2 = new UsAppProductCategoryNodeDto
  146 + {
  147 + CategoryId = g2.Key.CategoryId,
  148 + CategoryPhotoUrl = g2.Key.CategoryPhotoUrl,
  149 + Name = g2.Key.CategoryName,
  150 + ItemCount = productsGrouped.Count(),
  151 + Products = new List<UsAppLabelingProductNodeDto>()
  152 + };
  153 +
  154 + foreach (var g3 in productsGrouped)
  155 + {
  156 + var first = g3.First();
  157 + var typeNodes = g3
  158 + .GroupBy(r => r.LabelCode)
  159 + .Select(gr => BuildLabelTypeNode(gr.First()))
  160 + .OrderBy(t => t.OrderNum)
  161 + .ThenBy(t => t.TypeName)
  162 + .ToList();
  163 +
  164 + var subtitle = string.IsNullOrWhiteSpace(first.ProductCode?.Trim())
  165 + ? "无"
  166 + : first.ProductCode!.Trim();
  167 +
  168 + l2.Products.Add(new UsAppLabelingProductNodeDto
  169 + {
  170 + ProductId = first.ProductId,
  171 + ProductName = first.ProductName ?? string.Empty,
  172 + ProductCode = first.ProductCode ?? string.Empty,
  173 + ProductImageUrl = first.ProductImageUrl,
  174 + Subtitle = subtitle,
  175 + LabelTypeCount = typeNodes.Count,
  176 + LabelTypes = typeNodes
  177 + });
  178 + }
  179 +
  180 + l1.ProductCategories.Add(l2);
  181 + }
  182 +
  183 + result.Add(l1);
  184 + }
  185 +
  186 + return result;
  187 + }
  188 +
  189 + /// <summary>
  190 + /// App 打印预览:按标签编码解析模板并返回顶部展示字段 + 预览模板结构
  191 + /// </summary>
  192 + /// <remarks>
  193 + /// 示例请求:
  194 + /// ```json
  195 + /// {
  196 + /// "locationId": "LOC001",
  197 + /// "labelCode": "LBL0001",
  198 + /// "productId": "PROD001",
  199 + /// "baseTime": "2026-03-26T10:30:00",
  200 + /// "printInputJson": {
  201 + /// "price": "12.99"
  202 + /// }
  203 + /// }
  204 + /// ```
  205 + /// </remarks>
  206 + /// <param name="input">预览入参</param>
  207 + /// <returns>顶部字段 + 预览模板结构</returns>
  208 + /// <response code="200">成功</response>
  209 + /// <response code="400">参数错误/数据不存在</response>
  210 + /// <response code="500">服务器错误</response>
  211 + [Authorize]
  212 + public virtual async Task<UsAppLabelPreviewDto> PreviewAsync(UsAppLabelPreviewInputVo input)
  213 + {
  214 + if (input is null)
  215 + {
  216 + throw new UserFriendlyException("入参不能为空");
  217 + }
  218 +
  219 + var locationId = input.LocationId?.Trim();
  220 + if (string.IsNullOrWhiteSpace(locationId))
  221 + {
  222 + throw new UserFriendlyException("门店Id不能为空");
  223 + }
  224 +
  225 + var labelCode = input.LabelCode?.Trim();
  226 + if (string.IsNullOrWhiteSpace(labelCode))
  227 + {
  228 + throw new UserFriendlyException("labelCode不能为空");
  229 + }
  230 +
  231 + var labelRow = await _dbContext.SqlSugarClient
  232 + .Queryable<FlLabelDbEntity, FlLabelCategoryDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity>(
  233 + (l, c, t, tpl) => l.LabelCategoryId == c.Id && l.LabelTypeId == t.Id && l.TemplateId == tpl.Id)
  234 + .Where((l, c, t, tpl) => !l.IsDeleted && l.State)
  235 + .Where((l, c, t, tpl) => !c.IsDeleted && c.State)
  236 + .Where((l, c, t, tpl) => !t.IsDeleted && t.State)
  237 + .Where((l, c, t, tpl) => !tpl.IsDeleted)
  238 + .Where((l, c, t, tpl) => l.LabelCode == labelCode)
  239 + .Select((l, c, t, tpl) => new
  240 + {
  241 + l.LabelCode,
  242 + l.LocationId,
  243 + LabelCategoryName = c.CategoryName,
  244 + TypeName = t.TypeName,
  245 + TemplateCode = tpl.TemplateCode,
  246 + TemplateWidth = tpl.Width,
  247 + TemplateHeight = tpl.Height,
  248 + TemplateUnit = tpl.Unit
  249 + })
  250 + .FirstAsync();
  251 +
  252 + if (labelRow is null)
  253 + {
  254 + throw new UserFriendlyException("标签不存在或不可用");
  255 + }
  256 +
  257 + if (!string.Equals(labelRow.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase))
  258 + {
  259 + throw new UserFriendlyException("该标签不属于当前门店");
  260 + }
  261 +
  262 + var template = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo
  263 + {
  264 + LabelCode = labelCode,
  265 + ProductId = input.ProductId?.Trim(),
  266 + BaseTime = input.BaseTime,
  267 + PrintInputJson = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value)
  268 + });
  269 +
  270 + var productName = string.Empty;
  271 + var productCategoryName = "无";
  272 + if (!string.IsNullOrWhiteSpace(input.ProductId))
  273 + {
  274 + var pid = input.ProductId.Trim();
  275 + var p = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>()
  276 + .FirstAsync(x => !x.IsDeleted && x.State && x.Id == pid);
  277 + if (p is not null)
  278 + {
  279 + productName = p.ProductName ?? string.Empty;
  280 + if (!string.IsNullOrWhiteSpace(p.CategoryId))
  281 + {
  282 + var pc = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
  283 + .FirstAsync(x => !x.IsDeleted && x.State && x.Id == p.CategoryId);
  284 + productCategoryName = NormalizeCategoryName(pc?.CategoryName);
  285 + }
  286 + }
  287 + }
  288 +
  289 + return new UsAppLabelPreviewDto
  290 + {
  291 + LocationId = locationId,
  292 + LabelCode = labelCode,
  293 + TemplateCode = labelRow.TemplateCode,
  294 + LabelSizeText = FormatLabelSize(labelRow.TemplateWidth, labelRow.TemplateHeight, labelRow.TemplateUnit),
  295 + TypeName = labelRow.TypeName,
  296 + ProductName = string.IsNullOrWhiteSpace(productName) ? null : productName,
  297 + ProductCategoryName = productCategoryName,
  298 + LabelCategoryName = labelRow.LabelCategoryName,
  299 + PreviewImageBase64Png = null,
  300 + Template = template
  301 + };
  302 + }
  303 +
  304 + /// <summary>
  305 + /// App 打印:创建打印任务并落库打印明细(fl_label_print_task / fl_label_print_data)
  306 + /// </summary>
  307 + /// <param name="input">打印入参</param>
  308 + /// <returns>任务Id</returns>
  309 + [Authorize]
  310 + [UnitOfWork]
  311 + public virtual async Task<UsAppLabelPrintOutputDto> PrintAsync(UsAppLabelPrintInputVo input)
  312 + {
  313 + if (input is null)
  314 + {
  315 + throw new UserFriendlyException("入参不能为空");
  316 + }
  317 +
  318 + var locationId = input.LocationId?.Trim();
  319 + if (string.IsNullOrWhiteSpace(locationId))
  320 + {
  321 + throw new UserFriendlyException("门店Id不能为空");
  322 + }
  323 +
  324 + var labelCode = input.LabelCode?.Trim();
  325 + if (string.IsNullOrWhiteSpace(labelCode))
  326 + {
  327 + throw new UserFriendlyException("labelCode不能为空");
  328 + }
  329 +
  330 + var quantity = input.PrintQuantity <= 0 ? 1 : input.PrintQuantity;
  331 +
  332 + // 校验 label + location,并补齐一些顶部字段用于任务表落库
  333 + var labelRow = await _dbContext.SqlSugarClient
  334 + .Queryable<FlLabelDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity>(
  335 + (l, t, tpl) => l.LabelTypeId == t.Id && l.TemplateId == tpl.Id)
  336 + .Where((l, t, tpl) => !l.IsDeleted && l.State)
  337 + .Where((l, t, tpl) => !t.IsDeleted && t.State)
  338 + .Where((l, t, tpl) => !tpl.IsDeleted)
  339 + .Where((l, t, tpl) => l.LabelCode == labelCode)
  340 + .Select((l, t, tpl) => new
  341 + {
  342 + l.LocationId,
  343 + l.LabelTypeId,
  344 + TemplateCode = tpl.TemplateCode
  345 + })
  346 + .FirstAsync();
  347 +
  348 + if (labelRow is null)
  349 + {
  350 + throw new UserFriendlyException("标签不存在或不可用");
  351 + }
  352 +
  353 + if (!string.Equals(labelRow.LocationId?.Trim(), locationId, StringComparison.OrdinalIgnoreCase))
  354 + {
  355 + throw new UserFriendlyException("该标签不属于当前门店");
  356 + }
  357 +
  358 + // 解析模板 elements(与预览一致的渲染数据)
  359 + var resolvedTemplate = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo
  360 + {
  361 + LabelCode = labelCode,
  362 + ProductId = input.ProductId?.Trim(),
  363 + BaseTime = input.BaseTime,
  364 + PrintInputJson = input.PrintInputJson
  365 + });
  366 +
  367 + var printInputJsonStr = input.PrintInputJson is null
  368 + ? null
  369 + : JsonSerializer.Serialize(input.PrintInputJson);
  370 + var renderDataJsonStr = JsonSerializer.Serialize(resolvedTemplate);
  371 +
  372 + var now = DateTime.Now;
  373 + var currentUserId = CurrentUser?.Id?.ToString();
  374 + var taskId = _guidGenerator.Create().ToString();
  375 +
  376 + var task = new FlLabelPrintTaskDbEntity
  377 + {
  378 + Id = taskId,
  379 + IsDeleted = false,
  380 + CreationTime = now,
  381 + CreatorId = currentUserId,
  382 + ConcurrencyStamp = _guidGenerator.Create().ToString("N"),
  383 + LocationId = locationId,
  384 + LabelCode = labelCode,
  385 + ProductId = input.ProductId?.Trim(),
  386 + LabelTypeId = labelRow.LabelTypeId,
  387 + TemplateCode = labelRow.TemplateCode,
  388 + PrintQuantity = quantity,
  389 + BaseTime = input.BaseTime,
  390 + PrinterId = input.PrinterId?.Trim(),
  391 + PrinterMac = input.PrinterMac?.Trim(),
  392 + PrinterAddress = input.PrinterAddress?.Trim()
  393 + };
  394 +
  395 + await _dbContext.SqlSugarClient.Insertable(task).ExecuteCommandAsync();
  396 +
  397 + var dataRows = Enumerable.Range(1, quantity).Select(i => new FlLabelPrintDataDbEntity
  398 + {
  399 + Id = _guidGenerator.Create().ToString(),
  400 + IsDeleted = false,
  401 + CreationTime = now,
  402 + CreatorId = currentUserId,
  403 + ConcurrencyStamp = _guidGenerator.Create().ToString("N"),
  404 + TaskId = taskId,
  405 + CopyIndex = i,
  406 + PrintInputJson = printInputJsonStr,
  407 + RenderDataJson = renderDataJsonStr
  408 + }).ToList();
  409 +
  410 + await _dbContext.SqlSugarClient.Insertable(dataRows).ExecuteCommandAsync();
  411 +
  412 + return new UsAppLabelPrintOutputDto
  413 + {
  414 + TaskId = taskId,
  415 + PrintQuantity = quantity
  416 + };
  417 + }
  418 +
  419 + private ISugarQueryable<FlLabelProductDbEntity, FlLabelDbEntity, FlProductDbEntity, FlLabelCategoryDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity, FlProductCategoryDbEntity> BuildLabelingJoinQuery(
  420 + string locationId,
  421 + List<string> productIds,
  422 + string? filterCategoryId,
  423 + string? keyword)
  424 + {
  425 + var q = _dbContext.SqlSugarClient
  426 + .Queryable<FlLabelProductDbEntity>()
  427 + .InnerJoin<FlLabelDbEntity>((lp, l) => lp.LabelId == l.Id)
  428 + .InnerJoin<FlProductDbEntity>((lp, l, p) => lp.ProductId == p.Id)
  429 + .InnerJoin<FlLabelCategoryDbEntity>((lp, l, p, c) => l.LabelCategoryId == c.Id)
  430 + .InnerJoin<FlLabelTypeDbEntity>((lp, l, p, c, t) => l.LabelTypeId == t.Id)
  431 + .InnerJoin<FlLabelTemplateDbEntity>((lp, l, p, c, t, tpl) => l.TemplateId == tpl.Id)
  432 + .LeftJoin<FlProductCategoryDbEntity>((lp, l, p, c, t, tpl, pc) => p.CategoryId == pc.Id)
  433 + .Where((lp, l, p, c, t, tpl, pc) => productIds.Contains(p.Id))
  434 + .Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId)
  435 + .Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State)
  436 + .Where((lp, l, p, c, t, tpl, pc) => !p.IsDeleted && p.State)
  437 + .Where((lp, l, p, c, t, tpl, pc) => !c.IsDeleted && c.State)
  438 + .Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State)
  439 + .Where((lp, l, p, c, t, tpl, pc) => !tpl.IsDeleted)
  440 + .WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId)
  441 + .WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) =>
  442 + (l.LabelName != null && l.LabelName.Contains(keyword!)) ||
  443 + (p.ProductName != null && p.ProductName.Contains(keyword!)) ||
  444 + (pc.CategoryName != null && pc.CategoryName.Contains(keyword!)) ||
  445 + (c.CategoryName != null && c.CategoryName.Contains(keyword!)) ||
  446 + (t.TypeName != null && t.TypeName.Contains(keyword!)) ||
  447 + (l.LabelCode != null && l.LabelCode.Contains(keyword!)));
  448 +
  449 + return q;
  450 + }
  451 +
  452 + private sealed class LabelingTreeRow
  453 + {
  454 + public string LabelCategoryId { get; set; } = string.Empty;
  455 +
  456 + public string? LabelCategoryName { get; set; }
  457 +
  458 + public string? LabelCategoryPhotoUrl { get; set; }
  459 +
  460 + public int LabelCategoryOrderNum { get; set; }
  461 +
  462 + public string? ProductCategoryId { get; set; }
  463 +
  464 + public string? ProductCategoryName { get; set; }
  465 +
  466 + public string? ProductCategoryPhotoUrl { get; set; }
  467 +
  468 + public string ProductId { get; set; } = string.Empty;
  469 +
  470 + public string? ProductName { get; set; }
  471 +
  472 + public string? ProductCode { get; set; }
  473 +
  474 + public string? ProductImageUrl { get; set; }
  475 +
  476 + public string LabelTypeId { get; set; } = string.Empty;
  477 +
  478 + public string? TypeName { get; set; }
  479 +
  480 + public int TypeOrderNum { get; set; }
  481 +
  482 + public string LabelCode { get; set; } = string.Empty;
  483 +
  484 + public string? TemplateCode { get; set; }
  485 +
  486 + public decimal TemplateWidth { get; set; }
  487 +
  488 + public decimal TemplateHeight { get; set; }
  489 +
  490 + public string TemplateUnit { get; set; } = "inch";
  491 + }
  492 +
  493 + private static string NormalizeCategoryName(string? categoryName)
  494 + {
  495 + var s = categoryName?.Trim();
  496 + return string.IsNullOrWhiteSpace(s) ? "无" : s;
  497 + }
  498 +
  499 + private static string? NormalizeNullableId(string? id)
  500 + {
  501 + var s = id?.Trim();
  502 + return string.IsNullOrWhiteSpace(s) ? null : s;
  503 + }
  504 +
  505 + private static string? NormalizeNullableUrl(string? url)
  506 + {
  507 + var s = url?.Trim();
  508 + return string.IsNullOrWhiteSpace(s) ? null : s;
  509 + }
  510 +
  511 + private static UsAppLabelTypeNodeDto BuildLabelTypeNode(LabelingTreeRow r)
  512 + {
  513 + return new UsAppLabelTypeNodeDto
  514 + {
  515 + LabelTypeId = r.LabelTypeId,
  516 + TypeName = r.TypeName ?? string.Empty,
  517 + OrderNum = r.TypeOrderNum,
  518 + LabelCode = r.LabelCode ?? string.Empty,
  519 + TemplateCode = r.TemplateCode,
  520 + LabelSizeText = FormatLabelSize(r.TemplateWidth, r.TemplateHeight, r.TemplateUnit)
  521 + };
  522 + }
  523 +
  524 + private static string? FormatLabelSize(decimal w, decimal h, string unit)
  525 + {
  526 + var u = (unit ?? "inch").Trim().ToLowerInvariant();
  527 + var ws = w.ToString(CultureInfo.InvariantCulture);
  528 + var hs = h.ToString(CultureInfo.InvariantCulture);
  529 + return u is "inch" or "in"
  530 + ? $"{ws}\"x{hs}\""
  531 + : $"{ws}x{hs}{u}";
  532 + }
  533 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs
... ... @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc;
12 12 using Microsoft.AspNetCore.StaticFiles;
13 13 using Microsoft.IdentityModel.Tokens;
14 14 using Microsoft.OpenApi.Models;
  15 +using Microsoft.Extensions.FileProviders;
15 16 using StackExchange.Redis;
16 17 using Volo.Abp.AspNetCore.Auditing;
17 18 using Volo.Abp.AspNetCore.Authentication.JwtBearer;
... ... @@ -397,6 +398,19 @@ namespace Yi.Abp.Web
397 398 });
398 399 app.UseDirectoryBrowser("/api/app/wwwroot");
399 400  
  401 + // 类别图片静态资源(物理目录 /www/wwwroot/FoodLabelingManagementUs/picture)
  402 + // - Linux:通常可直接写入与访问
  403 + // - Windows:如该目录不存在,则使用项目 wwwroot/FoodLabelingManagementUs/picture 作为落盘目录
  404 + var linuxPictureRoot = "/www/wwwroot/FoodLabelingManagementUs/picture";
  405 + var webRootPicture = Path.Combine(env.WebRootPath ?? string.Empty, "FoodLabelingManagementUs", "picture");
  406 + var pictureRoot = Directory.Exists(linuxPictureRoot) ? linuxPictureRoot : webRootPicture;
  407 + Directory.CreateDirectory(pictureRoot);
  408 + app.UseStaticFiles(new StaticFileOptions
  409 + {
  410 + FileProvider = new PhysicalFileProvider(pictureRoot),
  411 + RequestPath = "/picture"
  412 + });
  413 +
400 414 app.Properties.Add("_AbpExceptionHandlingMiddleware_Added", false);
401 415 //工作单元
402 416 app.UseUnitOfWork();
... ...
项目相关文档/产品模块Categories接口对接说明.md 0 → 100644
  1 +# 产品模块 Categories(类别)接口对接说明(美国版)
  2 +
  3 +## 概述
  4 +
  5 +本模块用于平台端(H5)Products → **Categories** 页签的数据对接。
  6 +
  7 +- **模块**:`food-labeling-us`
  8 +- **接口前缀**:宿主统一前缀为 `/api/app`
  9 +- **分类表**:`fl_product_category`
  10 +- **关联字段**:`fl_product.category_id` → `fl_product_category.id`
  11 +- **图片字段**:`CategoryPhotoUrl`(前端字段:`categoryPhotoUrl`)
  12 +
  13 +> 说明:本文以 Swagger 为准(本地示例:`http://localhost:19001/swagger`,搜索 `ProductCategory`)。
  14 +
  15 +---
  16 +
  17 +## 接口 1:类别分页列表
  18 +
  19 +### HTTP
  20 +
  21 +- **方法**:`GET`
  22 +- **路径**:`/api/app/product-category`
  23 +- **鉴权**:需要登录(Header:`Authorization: Bearer {token}`)
  24 +
  25 +### 入参(Query 参数)
  26 +
  27 +| 参数名 | 类型 | 必填 | 说明 |
  28 +|------|------|------|------|
  29 +| `skipCount` | number | 是 | 跳过条数(分页) |
  30 +| `maxResultCount` | number | 是 | 每页条数(分页) |
  31 +| `sorting` | string | 否 | 排序字段(如 `OrderNum desc`),不传则按 `OrderNum desc, CreationTime desc` |
  32 +| `keyword` | string | 否 | 模糊搜索(匹配 `CategoryCode/CategoryName`) |
  33 +| `state` | boolean | 否 | 启用状态过滤 |
  34 +
  35 +### 请求示例
  36 +
  37 +```http
  38 +GET /api/app/product-category?skipCount=0&maxResultCount=10&keyword=Prep HTTP/1.1
  39 +Host: localhost:19001
  40 +Authorization: Bearer eyJhbGciOi...
  41 +```
  42 +
  43 +### 出参(PagedResultWithPageDto<ProductCategoryGetListOutputDto>)
  44 +
  45 +| 字段 | 类型 | 说明 |
  46 +|------|------|------|
  47 +| `pageIndex` | number | 当前页(从 1 开始) |
  48 +| `pageSize` | number | 每页条数 |
  49 +| `totalCount` | number | 总数 |
  50 +| `totalPages` | number | 总页数 |
  51 +| `items` | array | 当前页数据 |
  52 +
  53 +`items[]` 字段:
  54 +
  55 +| 字段 | 类型 | 说明 |
  56 +|------|------|------|
  57 +| `id` | string | 主键 |
  58 +| `categoryCode` | string | 类别编码 |
  59 +| `categoryName` | string | 类别名称 |
  60 +| `categoryPhotoUrl` | string \| null | 类别图片 URL(建议用 `/picture/...`) |
  61 +| `state` | boolean | 是否启用 |
  62 +| `orderNum` | number | 排序 |
  63 +| `lastEdited` | string | 最后编辑时间 |
  64 +
  65 +### 响应示例
  66 +
  67 +```json
  68 +{
  69 + "pageIndex": 1,
  70 + "pageSize": 10,
  71 + "totalCount": 1,
  72 + "totalPages": 1,
  73 + "items": [
  74 + {
  75 + "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
  76 + "categoryCode": "CAT_PREP",
  77 + "categoryName": "Prep",
  78 + "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png",
  79 + "state": true,
  80 + "orderNum": 100,
  81 + "lastEdited": "2026-03-25 12:30:10"
  82 + }
  83 + ]
  84 +}
  85 +```
  86 +
  87 +---
  88 +
  89 +## 接口 2:类别详情
  90 +
  91 +### HTTP
  92 +
  93 +- **方法**:`GET`
  94 +- **路径**:`/api/app/product-category/{id}`
  95 +
  96 +### 请求示例
  97 +
  98 +```http
  99 +GET /api/app/product-category/a2696b9e-2277-11f1-b4c6-00163e0c7c4f HTTP/1.1
  100 +Host: localhost:19001
  101 +Authorization: Bearer eyJhbGciOi...
  102 +```
  103 +
  104 +### 响应示例(ProductCategoryGetOutputDto)
  105 +
  106 +```json
  107 +{
  108 + "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
  109 + "categoryCode": "CAT_PREP",
  110 + "categoryName": "Prep",
  111 + "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png",
  112 + "state": true,
  113 + "orderNum": 100
  114 +}
  115 +```
  116 +
  117 +---
  118 +
  119 +## 接口 3:新增类别
  120 +
  121 +### HTTP
  122 +
  123 +- **方法**:`POST`
  124 +- **路径**:`/api/app/product-category`
  125 +- **Content-Type**:`application/json`
  126 +
  127 +### 入参(Body JSON:ProductCategoryCreateInputVo)
  128 +
  129 +| 字段 | 类型 | 必填 | 说明 |
  130 +|------|------|------|------|
  131 +| `categoryCode` | string | 是 | 类别编码(唯一) |
  132 +| `categoryName` | string | 是 | 类别名称(唯一) |
  133 +| `categoryPhotoUrl` | string \| null | 否 | 图片 URL(建议先上传图片拿到 `/picture/...` 再保存) |
  134 +| `state` | boolean | 否 | 是否启用(默认 true) |
  135 +| `orderNum` | number | 否 | 排序(默认 0) |
  136 +
  137 +### 请求示例
  138 +
  139 +```json
  140 +{
  141 + "categoryCode": "CAT_PREP",
  142 + "categoryName": "Prep",
  143 + "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png",
  144 + "state": true,
  145 + "orderNum": 100
  146 +}
  147 +```
  148 +
  149 +---
  150 +
  151 +## 接口 4:编辑类别
  152 +
  153 +### HTTP
  154 +
  155 +- **方法**:`PUT`
  156 +- **路径**:`/api/app/product-category/{id}`
  157 +- **Content-Type**:`application/json`
  158 +
  159 +### 请求示例
  160 +
  161 +```json
  162 +{
  163 + "categoryCode": "CAT_PREP",
  164 + "categoryName": "Prep",
  165 + "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png",
  166 + "state": true,
  167 + "orderNum": 100
  168 +}
  169 +```
  170 +
  171 +---
  172 +
  173 +## 接口 5:删除类别(逻辑删除)
  174 +
  175 +### HTTP
  176 +
  177 +- **方法**:`DELETE`
  178 +- **路径**:`/api/app/product-category/{id}`
  179 +
  180 +### 约束
  181 +
  182 +- 若该类别已被 `fl_label` 引用(`fl_label.LabelCategoryId = id`),删除会失败并返回友好提示:`该类别已被标签引用,无法删除`。
  183 +
  184 +### 请求示例
  185 +
  186 +```http
  187 +DELETE /api/app/product-category/a2696b9e-2277-11f1-b4c6-00163e0c7c4f HTTP/1.1
  188 +Host: localhost:19001
  189 +Authorization: Bearer eyJhbGciOi...
  190 +```
  191 +
  192 +---
  193 +
  194 +## 配套:类别图片上传接口
  195 +
  196 +类别图片上传接口见文档:
  197 +
  198 +- `项目相关文档/平台端Categories图片上传接口说明.md`
  199 +
  200 +推荐前端流程:
  201 +
  202 +1. 调用上传接口 `POST /api/app/picture/category/upload` 拿到响应 `url`
  203 +2. 新增/编辑类别时把 `categoryPhotoUrl` 设为该 `url`
  204 +
... ...
项目相关文档/平台端Categories图片上传接口说明.md 0 → 100644
  1 +# 平台端 Categories 图片上传接口说明
  2 +
  3 +## 概述
  4 +
  5 +平台端(H5)Products 模块的 **Categories** 页面,类别图片字段为 **`CategoryPhotoUrl`**(后端已在 `fl_label_category` 与分类 CRUD 接口中贯通)。
  6 +
  7 +图片上传由 `food-labeling-us` 模块提供上传接口,文件会保存到服务器目录,并通过静态资源路径 `/picture/...` 直接访问。
  8 +
  9 +---
  10 +
  11 +## 接口:上传类别图片
  12 +
  13 +### HTTP
  14 +
  15 +- **方法**:`POST`
  16 +- **路径**:`/api/app/picture/category/upload`
  17 +- **Content-Type**:`multipart/form-data`
  18 +- **鉴权**:需要登录(Header:`Authorization: Bearer {token}`)
  19 +
  20 +### 表单参数(multipart/form-data)
  21 +
  22 +| 参数名 | 类型 | 必填 | 说明 |
  23 +|------|------|------|------|
  24 +| `file` | file | 是 | 图片文件 |
  25 +| `subDir` | string | 否 | 可选子目录(相对路径),例如 `category`、`category/2026-03`;**禁止包含 `..`** |
  26 +
  27 +### 限制
  28 +
  29 +- **大小**:最大 5MB
  30 +- **格式**:仅支持 `jpg/jpeg/png/webp/gif`
  31 +- **文件名策略**:后端自动生成唯一文件名(避免覆盖)
  32 +
  33 +### 请求示例(curl)
  34 +
  35 +Windows(PowerShell/命令行注意路径转义):
  36 +
  37 +```bash
  38 +curl -X POST "http://localhost:19001/api/app/picture/category/upload" ^
  39 + -H "Authorization: Bearer <token>" ^
  40 + -F "file=@C:\\tmp\\category.png" ^
  41 + -F "subDir=category"
  42 +```
  43 +
  44 +### 请求示例(Postman/Apifox)
  45 +
  46 +- 选择 `POST`
  47 +- URL:`http://localhost:19001/api/app/picture/category/upload`
  48 +- Headers:`Authorization: Bearer <token>`
  49 +- Body:`form-data`
  50 + - Key=`file`,类型选 `File`,选择图片文件
  51 + - Key=`subDir`,类型 `Text`,填 `category`(可选)
  52 +
  53 +### 响应体(PictureUploadOutputDto)
  54 +
  55 +| 字段 | 类型 | 说明 |
  56 +|------|------|------|
  57 +| `url` | string | 图片访问的相对路径,可直接保存到 `CategoryPhotoUrl`(例如:`/picture/category/xxx.png`) |
  58 +| `fileName` | string | 服务器保存的文件名 |
  59 +| `size` | number | 文件大小(字节) |
  60 +
  61 +### 响应示例
  62 +
  63 +```json
  64 +{
  65 + "url": "/picture/category/20260325123010_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.png",
  66 + "fileName": "20260325123010_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.png",
  67 + "size": 123456
  68 +}
  69 +```
  70 +
  71 +---
  72 +
  73 +## 文件保存位置与访问方式
  74 +
  75 +### 落盘目录
  76 +
  77 +后端会按环境自动选择落盘目录(不存在会自动创建):
  78 +
  79 +- 优先:`/www/wwwroot/FoodLabelingManagementUs/picture`
  80 +- 否则(Windows 本地开发):`<项目根>/wwwroot/FoodLabelingManagementUs/picture`
  81 +
  82 +### 访问 URL
  83 +
  84 +静态资源映射为:
  85 +
  86 +- `GET /picture/{subDir}/{fileName}`
  87 +
  88 +举例:
  89 +
  90 +- `http://localhost:19001/picture/category/20260325123010_xxx.png`
  91 +
  92 +---
  93 +
  94 +## 如何写入 Categories(CategoryPhotoUrl)
  95 +
  96 +推荐前端流程:
  97 +
  98 +1. 调用本上传接口,拿到返回的 `url`
  99 +2. 再调用分类新增/编辑接口,把 `categoryPhotoUrl` 设置为该 `url`
  100 +
  101 +> 说明:分类 CRUD 已支持 `CategoryPhotoUrl` 字段;你只需要在页面表单里新增该字段即可。
  102 +
... ...
项目相关文档/标签模块接口对接说明.md
... ... @@ -6,7 +6,7 @@ Swagger 地址:
6 6 - `http://localhost:19001/swagger`
7 7  
8 8 说明:
9   -- 接口最终 URL 以 Swagger 展示为准(可在 Swagger 里搜索 `LabelCategory / LabelType / LabelMultipleOption / LabelTemplate / Label`)。
  9 +- 接口最终 URL 以 Swagger 展示为准(可在 Swagger 里搜索 `LabelCategory / LabelType / LabelMultipleOption / LabelTemplate / Label / UsAppLabeling`)。
10 10 - 本模块后端接口以各 AppService 的方法签名自动暴露。
11 11 - 返回分页统一包含 `PageIndex / PageSize / TotalCount / TotalPages / Items`。
12 12  
... ... @@ -22,6 +22,7 @@ Swagger 地址:
22 22 - `label-multiple-option`
23 23 - `label-template`
24 24 - `label`
  25 + - `us-app-labeling`(App 端 Labeling 四级树)
25 26  
26 27 ---
27 28  
... ... @@ -355,7 +356,8 @@ Swagger 地址:
355 356 ## 接口 5:Labels(按产品展示多个标签)
356 357  
357 358 说明:
358   -- 列表接口按 `ProductId` 查询,一个产品会对应多条标签记录。
  359 +- 列表接口以“标签”为维度分页展示(同一个标签会绑定多个产品)。
  360 +- 列表支持按 `ProductId` 过滤:仅返回“绑定了该产品”的标签。
359 361 - 标签详情/编辑/删除的 `id` 使用 `fl_label.LabelCode`。
360 362  
361 363 ### 5.1 分页列表(按产品)
... ... @@ -379,6 +381,11 @@ Swagger 地址:
379 381 }
380 382 ```
381 383  
  384 +列表出参要点(`LabelGetListOutputDto`):
  385 +
  386 +- `products`:同一个标签下绑定的产品名称,用 `,` 分割(例如:`Chicken,Sandwich`)
  387 +- 其他字段与之前一致:`labelName/locationName/category/type/template/state/lastEdited...`
  388 +
382 389 ### 5.2 详情
383 390  
384 391 方法:`GET /api/app/label/{id}`
... ... @@ -582,3 +589,297 @@ Swagger 地址:
582 589 入参:
583 590 - `id`:门店Id
584 591  
  592 +---
  593 +
  594 +## 接口 8:App Labeling 四级列表(门店打标页)
  595 +
  596 +**场景**:美国版 UniApp「Labeling」页:左侧 **标签分类(Label Category)** → 主区域按 **产品分类(Product Category)** 折叠分组 → **产品(Product)** 卡片 → 点选后底部弹层展示 **标签种类(Label Type)**。
  597 +
  598 +**实现**:`UsAppLabelingAppService.GetLabelingTreeAsync`,约定式 API 控制器名 **`us-app-labeling`**(与 `UsAppAuth` → `us-app-auth` 同规则)。
  599 +
  600 +### 8.1 获取四级嵌套树
  601 +
  602 +#### HTTP
  603 +
  604 +- **方法**:`GET`
  605 +- **路径**:`/api/app/us-app-labeling/labeling-tree`(若与 Swagger 不一致,**以 Swagger 为准**)
  606 +- **鉴权**:需要登录(`Authorization: Bearer ...`);可使用 App 登录或 Web 账号 Token,需能通过 `[Authorize]`。当前用户可选门店列表见 **`/api/app/us-app-auth/my-locations`**(说明见 `美国版App登录接口说明.md`)。
  607 +
  608 +#### 入参(Query:`UsAppLabelingTreeInputVo`)
  609 +
  610 +| 参数名 | 类型 | 必填 | 说明 |
  611 +|--------|------|------|------|
  612 +| `locationId` | string | 是 | 当前门店 Id(`location.Id`,与 `fl_location_product.LocationId`、`fl_label.LocationId` 一致) |
  613 +| `keyword` | string | 否 | 模糊过滤:标签名、产品名、产品分类、**标签分类**名、标签类型名、`labelCode` 等(实现见服务内 `WhereIF`) |
  614 +| `labelCategoryId` | string | 否 | 侧边栏只展示某一 **标签分类** 时传入;不传则返回当前门店下出现的全部标签分类节点 |
  615 +
  616 +#### 数据范围与表关联(便于联调对照)
  617 +
  618 +- **门店内商品**:`fl_location_product`(先有 `locationId` 下的 `productId` 集合)。
  619 +- **参与连接的表**:`fl_label_product`(标签-产品)、`fl_label`(`LocationId` 须为当前门店且未删除、启用)、`fl_product`、`fl_label_category`、`fl_label_type`、`fl_label_template`。
  620 +- **第二级「产品分类」**:来自 `fl_product.CategoryName`,trim 后为空则归并为显示名 **`无`**。
  621 +- **第四级去重**:同一产品在同一标签分类、同一门店下,多条 `fl_label` 若 **`labelCode` 相同**,只保留一条用于列表(预览/打印仍用返回的 `labelCode` 等业务字段)。
  622 +
  623 +#### 出参(`List<UsAppLabelCategoryTreeNodeDto>`)
  624 +
  625 +若宿主对成功结果有统一包装,业务数组一般在 **`data`** 中;下列为 **解包后的数组项** 结构。
  626 +
  627 +**L1 `UsAppLabelCategoryTreeNodeDto`(标签分类)**
  628 +
  629 +| 字段 | 类型 | 说明 |
  630 +|------|------|------|
  631 +| `id` | string | `fl_label_category.Id` |
  632 +| `categoryName` | string | 分类名称 |
  633 +| `categoryPhotoUrl` | string \| null | 分类图标/图 |
  634 +| `orderNum` | number | 排序 |
  635 +| `productCategories` | array | 第二级列表(见下表) |
  636 +
  637 +**L2 `UsAppProductCategoryNodeDto`(产品分类)**
  638 +
  639 +| 字段 | 类型 | 说明 |
  640 +|------|------|------|
  641 +| `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 |
  642 +| `categoryPhotoUrl` | string \| null | 产品分类图片地址;产品未归类或分类不存在时为空 |
  643 +| `name` | string | 产品分类显示名;空源数据为 **`无`** |
  644 +| `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) |
  645 +| `products` | array | 第三级产品列表(见下表) |
  646 +
  647 +**L3 `UsAppLabelingProductNodeDto`(产品)**
  648 +
  649 +| 字段 | 类型 | 说明 |
  650 +|------|------|------|
  651 +| `productId` | string | `fl_product.Id` |
  652 +| `productName` | string | 产品名称 |
  653 +| `productCode` | string | 产品编码 |
  654 +| `productImageUrl` | string \| null | 主图 |
  655 +| `subtitle` | string | 卡片副标题:**有 `productCode` 则显示编码,否则「无」**(与原型「Basic」等独立文案不同,需另行扩展字段时再对齐) |
  656 +| `labelTypeCount` | number | 第四级条数,可用于角标「N Types」 |
  657 +| `labelTypes` | array | 第四级(见下表) |
  658 +
  659 +**L4 `UsAppLabelTypeNodeDto`(标签种类 / 可选项)**
  660 +
  661 +| 字段 | 类型 | 说明 |
  662 +|------|------|------|
  663 +| `labelTypeId` | string | `fl_label_type.Id` |
  664 +| `typeName` | string | 类型名称(如 Defrost) |
  665 +| `orderNum` | number | 排序 |
  666 +| `labelCode` | string | 业务标签编码,后续预览、打印流程使用 |
  667 +| `templateCode` | string \| null | 关联模板编码 |
  668 +| `labelSizeText` | string \| null | 尺寸文案;`inch` 常用格式如 `2"x2"` |
  669 +
  670 +#### 错误与边界
  671 +
  672 +- `locationId` 为空:返回友好错误 **「门店Id不能为空」**。
  673 +- 门店下无关联产品:返回 **空数组** `[]`。
  674 +- 有产品但无任何符合条件的标签关联:返回 **空数组** `[]`。
  675 +
  676 +#### 请求示例
  677 +
  678 +```http
  679 +GET /api/app/us-app-labeling/labeling-tree?locationId=11111111-1111-1111-1111-111111111111&labelCategoryId=a2696b9e-2277-11f1-b4c6-00163e0c7c4f&keyword=Chicken HTTP/1.1
  680 +Host: localhost:19001
  681 +Authorization: Bearer eyJhbGciOi...
  682 +```
  683 +
  684 +**curl**(Token 取自登录响应的 `data.token` 整段,已含 `Bearer ` 前缀时直接放入 Header):
  685 +
  686 +```bash
  687 +curl -X GET "http://localhost:19001/api/app/us-app-labeling/labeling-tree?locationId=11111111-1111-1111-1111-111111111111" \
  688 + -H "Authorization: <data.token>"
  689 +```
  690 +
  691 +#### 响应结构示例(解包后)
  692 +
  693 +```json
  694 +[
  695 + {
  696 + "id": "cat-prep-id",
  697 + "categoryName": "Prep",
  698 + "categoryPhotoUrl": "/picture/...",
  699 + "orderNum": 1,
  700 + "productCategories": [
  701 + {
  702 + "categoryId": "pc-meat-id",
  703 + "categoryPhotoUrl": "/picture/product-category/20260325123010_xxx.png",
  704 + "name": "Meat",
  705 + "itemCount": 1,
  706 + "products": [
  707 + {
  708 + "productId": "prod-chicken-id",
  709 + "productName": "Chicken",
  710 + "productCode": "CHK-001",
  711 + "productImageUrl": "/picture/...",
  712 + "subtitle": "CHK-001",
  713 + "labelTypeCount": 3,
  714 + "labelTypes": [
  715 + {
  716 + "labelTypeId": "lt-defrost",
  717 + "typeName": "Defrost",
  718 + "orderNum": 1,
  719 + "labelCode": "LBL_CHICKEN_DEFROST",
  720 + "templateCode": "TPL_2X2",
  721 + "labelSizeText": "2\"x2\""
  722 + }
  723 + ]
  724 + }
  725 + ]
  726 + }
  727 + ]
  728 + }
  729 +]
  730 +```
  731 +
  732 +> 前端 Axios 若项目约定 **GET 使用 `data` 配置对象** 传参,请仍绑定到与上述 Query 同名的字段(`locationId`、`keyword`、`labelCategoryId`),与 URL Query 等价即可。
  733 +
  734 +### 8.2 App 打印预览(elements 渲染结构)
  735 +
  736 +**场景**:用户选择某个 Product + Label Type 进入「Label Preview」页面,需要把模板预览区域渲染出来。
  737 +后端根据 `labelCode` 读取模板(`fl_label_template` + `fl_label_template_element`),并将 AUTO_DB / PRINT_INPUT 的值渲染回每个 element 的 `config`,前端按 `elements` 自行绘制预览。
  738 +
  739 +#### HTTP
  740 +
  741 +- **方法**:`POST`
  742 +- **路径**:`/api/app/us-app-labeling/preview`(若与 Swagger 不一致,**以 Swagger 为准**)
  743 +- **鉴权**:需要登录(`Authorization: Bearer ...`)
  744 +
  745 +#### 入参(Body:`UsAppLabelPreviewInputVo`)
  746 +
  747 +| 参数名(JSON) | 类型 | 必填 | 说明 |
  748 +|---|---|---|---|
  749 +| `locationId` | string | 是 | 门店Id(校验 `fl_label.LocationId` 必须一致) |
  750 +| `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) |
  751 +| `productId` | string | 否 | 预览用产品Id;不传则默认取该标签绑定的第一个产品(用于 AUTO_DB 数据填充) |
  752 +| `baseTime` | string | 否 | 业务基准时间(用于 DATE/TIME 元素计算;不传则用服务器当前时间) |
  753 +| `printInputJson` | object | 否 | 打印输入(用于 PRINT_INPUT 元素),key 建议与模板元素 `inputKey` 对齐 |
  754 +
  755 +#### 出参(`UsAppLabelPreviewDto`)
  756 +
  757 +除顶部信息外,核心是 `template`(供前端画布渲染):
  758 +
  759 +- `template`:`LabelTemplatePreviewDto`
  760 + - `width` / `height` / `unit`:模板物理尺寸
  761 + - `elements[]`:元素数组(对齐前端 editor JSON:`id/type/x/y/width/height/rotation/border/zIndex/orderNum/config`)
  762 +
  763 +`elements[].config` 内常用字段(示例):
  764 +
  765 +- 文本类(如 `TEXT_PRODUCT` / `TEXT_STATIC` / `TEXT_PRICE`):`config.text`
  766 +- 条码/二维码(`BARCODE` / `QRCODE`):`config.data`
  767 +- 日期/时间(`DATE` / `TIME`):`config.format`(后端已计算并写回)
  768 +
  769 +#### 数据来源说明
  770 +
  771 +- 模板头:`fl_label_template`
  772 +- 模板元素:`fl_label_template_element`(按 `OrderNum` + `ZIndex` 排序)
  773 +- 标签归属:`fl_label`(校验 `labelCode` 存在且 `LocationId == locationId`)
  774 +
  775 +#### 错误与边界
  776 +
  777 +- `locationId` 为空:友好错误 **「门店Id不能为空」**。
  778 +- `labelCode` 为空:友好错误 **「labelCode不能为空」**。
  779 +- 标签不存在:友好错误 **「标签不存在」**。
  780 +- 模板不存在:友好错误 **「模板不存在」**。
  781 +- 标签不属于当前门店:友好错误 **「该标签不属于当前门店」**。
  782 +- 标签未绑定产品且未传 `productId`:友好错误 **「该标签未绑定产品,无法预览」**。
  783 +
  784 +#### 请求示例
  785 +
  786 +```json
  787 +{
  788 + "locationId": "11111111-1111-1111-1111-111111111111",
  789 + "labelCode": "LBL_CHICKEN_DEFROST",
  790 + "productId": "22222222-2222-2222-2222-222222222222",
  791 + "baseTime": "2026-03-26T10:30:00",
  792 + "printInputJson": {
  793 + "price": "12.99"
  794 + }
  795 +}
  796 +```
  797 +
  798 +**curl:**
  799 +
  800 +```bash
  801 +curl -X POST "http://localhost:19001/api/app/us-app-labeling/preview" \
  802 + -H "Authorization: <data.token>" \
  803 + -H "Content-Type: application/json" \
  804 + -d '{"locationId":"11111111-1111-1111-1111-111111111111","labelCode":"LBL_CHICKEN_DEFROST","productId":"22222222-2222-2222-2222-222222222222","baseTime":"2026-03-26T10:30:00","printInputJson":{"price":"12.99"}}'
  805 +```
  806 +
  807 +---
  808 +
  809 +## 接口 9:App 打印(落库打印任务与明细)
  810 +
  811 +**场景**:移动端预览确认后点击 **Print**。后端负责把“本次打印”写入数据库,方便追溯/统计/重打。
  812 +
  813 +### 9.1 创建打印任务并写入明细
  814 +
  815 +#### HTTP
  816 +
  817 +- **方法**:`POST`
  818 +- **路径**:`/api/app/us-app-labeling/print`(若与 Swagger 不一致,**以 Swagger 为准**)
  819 +- **鉴权**:需要登录(`Authorization: Bearer ...`)
  820 +
  821 +#### 入参(Body:`UsAppLabelPrintInputVo`)
  822 +
  823 +| 参数名(JSON) | 类型 | 必填 | 说明 |
  824 +|---|---|---|---|
  825 +| `locationId` | string | 是 | 门店Id(校验 `fl_label.LocationId` 必须一致) |
  826 +| `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) |
  827 +| `productId` | string | 否 | 打印用产品Id;不传则默认取该标签绑定的第一个产品(用于模板解析) |
  828 +| `printQuantity` | number | 否 | 打印份数;`<=0` 按 1 处理 |
  829 +| `baseTime` | string | 否 | 业务基准时间(用于 DATE/TIME 元素计算) |
  830 +| `printInputJson` | object | 否 | 打印输入(用于模板 PRINT_INPUT 元素),key 建议与模板元素 `inputKey` 对齐 |
  831 +| `printerId` | string | 否 | 打印机Id(可选,用于追踪) |
  832 +| `printerMac` | string | 否 | 打印机蓝牙 MAC(可选) |
  833 +| `printerAddress` | string | 否 | 打印机地址(可选) |
  834 +
  835 +#### 数据落库说明
  836 +
  837 +- **任务表**:`fl_label_print_task`
  838 + - 插入 1 条任务记录(`locationId / labelCode / productId / labelTypeId / templateCode / printQuantity / baseTime / printer...` 等)。
  839 +- **明细表**:`fl_label_print_data`
  840 + - 按 `printQuantity` 插入 N 条明细记录(`copyIndex = 1..N`)。
  841 + - `printInputJson`:保存本次打印的原始输入(JSON 字符串)。
  842 + - `renderDataJson`:保存本次解析后的模板预览结构(`LabelTemplatePreviewDto`,包含 resolved 后的 `elements[].config`),供追溯/重打使用。
  843 +
  844 +> 模板解析的数据源来自 `fl_label_template` + `fl_label_template_element`,与预览接口一致。
  845 +
  846 +#### 出参(`UsAppLabelPrintOutputDto`)
  847 +
  848 +| 字段 | 类型 | 说明 |
  849 +|---|---|---|
  850 +| `taskId` | string | 打印任务Id(用于后续查询/重打/统计) |
  851 +| `printQuantity` | number | 实际写入的份数 |
  852 +
  853 +#### 错误与边界
  854 +
  855 +- `locationId` 为空:友好错误 **「门店Id不能为空」**。
  856 +- `labelCode` 为空:友好错误 **「labelCode不能为空」**。
  857 +- 标签不存在/不可用:友好错误 **「标签不存在或不可用」**。
  858 +- 标签不属于当前门店:友好错误 **「该标签不属于当前门店」**。
  859 +- 标签未绑定产品且未传 `productId`:友好错误 **「该标签未绑定产品,无法预览」**(模板解析阶段抛出)。
  860 +
  861 +#### 请求示例
  862 +
  863 +```json
  864 +{
  865 + "locationId": "11111111-1111-1111-1111-111111111111",
  866 + "labelCode": "LBL_CHICKEN_DEFROST",
  867 + "productId": "22222222-2222-2222-2222-222222222222",
  868 + "printQuantity": 2,
  869 + "baseTime": "2026-03-26T10:30:00",
  870 + "printInputJson": {
  871 + "price": "12.99"
  872 + },
  873 + "printerMac": "AA:BB:CC:DD:EE:FF"
  874 +}
  875 +```
  876 +
  877 +**curl:**
  878 +
  879 +```bash
  880 +curl -X POST "http://localhost:19001/api/app/us-app-labeling/print" \
  881 + -H "Authorization: <data.token>" \
  882 + -H "Content-Type: application/json" \
  883 + -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"}}'
  884 +```
  885 +
... ...