Commit 28dc179d4556d684c1b73699886f150a5e2c18e2

Authored by 李曜臣
1 parent 13b6d2e3

产品标签关联接口实现

Showing 17 changed files with 910 additions and 42 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/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/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/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/Services/LabelAppService.cs
... ... @@ -39,55 +39,122 @@ public class LabelAppService : ApplicationService, ILabelAppService
39 39 var labelTypeId = input.LabelTypeId?.Trim();
40 40 var templateCode = input.TemplateCode?.Trim();
41 41  
42   - // 先查 label-product 映射(按产品)
43   - var query = _dbContext.SqlSugarClient.Queryable<FlLabelProductDbEntity, FlLabelDbEntity, FlProductDbEntity, FlLabelCategoryDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity>(
44   - (lp, l, p, c, t, tpl) =>
45   - lp.LabelId == l.Id &&
46   - lp.ProductId == p.Id &&
47   - l.LabelCategoryId == c.Id &&
48   - l.LabelTypeId == t.Id &&
49   - l.TemplateId == tpl.Id)
50   - .Where((lp, l, p, c, t, tpl) => l.IsDeleted == false)
51   - .Where((lp, l, p, c, t, tpl) => !c.IsDeleted)
52   - .Where((lp, l, p, c, t, tpl) => !t.IsDeleted)
53   - .Where((lp, l, p, c, t, tpl) => !tpl.IsDeleted)
54   - .Where((lp, l, p, c, t, tpl) => !p.IsDeleted)
55   - .WhereIF(!string.IsNullOrWhiteSpace(productId), (lp, l, p, c, t, tpl) => lp.ProductId == productId)
56   - .WhereIF(!string.IsNullOrWhiteSpace(locationId), (lp, l, p, c, t, tpl) => l.LocationId == locationId)
57   - .WhereIF(!string.IsNullOrWhiteSpace(labelCategoryId), (lp, l, p, c, t, tpl) => l.LabelCategoryId == labelCategoryId)
58   - .WhereIF(!string.IsNullOrWhiteSpace(labelTypeId), (lp, l, p, c, t, tpl) => l.LabelTypeId == labelTypeId)
59   - .WhereIF(!string.IsNullOrWhiteSpace(templateCode), (lp, l, p, c, t, tpl) => tpl.TemplateCode == templateCode)
60   - .WhereIF(!string.IsNullOrWhiteSpace(keyword),
61   - (lp, l, p, c, t, tpl) =>
  42 + // 目标:列表每行是“标签”,同一个标签下的 products 以 “,” 拼接展示
  43 + // 因此需要按 label 维度分页(避免 label-product join 导致重复行与分页错乱)。
  44 +
  45 + var labelIdsQuery = _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>()
  46 + .Where(l => !l.IsDeleted)
  47 + .WhereIF(!string.IsNullOrWhiteSpace(locationId), l => l.LocationId == locationId)
  48 + .WhereIF(!string.IsNullOrWhiteSpace(labelCategoryId), l => l.LabelCategoryId == labelCategoryId)
  49 + .WhereIF(!string.IsNullOrWhiteSpace(labelTypeId), l => l.LabelTypeId == labelTypeId)
  50 + .WhereIF(input.State != null, l => l.State == input.State);
  51 +
  52 + if (!string.IsNullOrWhiteSpace(templateCode))
  53 + {
  54 + labelIdsQuery = labelIdsQuery
  55 + .InnerJoin<FlLabelTemplateDbEntity>((l, tpl) => l.TemplateId == tpl.Id)
  56 + .Where((l, tpl) => !tpl.IsDeleted && tpl.TemplateCode == templateCode)
  57 + .Select((l, tpl) => l);
  58 + }
  59 +
  60 + // 按产品筛选:存在 label-product 关联即可
  61 + if (!string.IsNullOrWhiteSpace(productId))
  62 + {
  63 + labelIdsQuery = labelIdsQuery
  64 + .InnerJoin<FlLabelProductDbEntity>((l, lp) => lp.LabelId == l.Id)
  65 + .Where((l, lp) => lp.ProductId == productId)
  66 + .Select((l, lp) => l);
  67 + }
  68 +
  69 + // 关键字:匹配 labelName/categoryName/typeName/templateName/productName
  70 + if (!string.IsNullOrWhiteSpace(keyword))
  71 + {
  72 + labelIdsQuery = labelIdsQuery
  73 + .LeftJoin<FlLabelCategoryDbEntity>((l, c) => l.LabelCategoryId == c.Id)
  74 + .LeftJoin<FlLabelTypeDbEntity>((l, c, t) => l.LabelTypeId == t.Id)
  75 + .LeftJoin<FlLabelTemplateDbEntity>((l, c, t, tpl) => l.TemplateId == tpl.Id)
  76 + .LeftJoin<FlLabelProductDbEntity>((l, c, t, tpl, lp) => lp.LabelId == l.Id)
  77 + .LeftJoin<FlProductDbEntity>((l, c, t, tpl, lp, p) => lp.ProductId == p.Id)
  78 + .Where((l, c, t, tpl, lp, p) =>
62 79 l.LabelName.Contains(keyword!) ||
63   - p.ProductName.Contains(keyword!) ||
64   - c.CategoryName.Contains(keyword!) ||
65   - t.TypeName.Contains(keyword!))
66   - .WhereIF(input.State != null, (lp, l, p, c, t, tpl) => l.State == input.State)
67   - .OrderByDescending((lp, l, p, c, t, tpl) => l.LastModificationTime ?? l.CreationTime);
  80 + (c.CategoryName != null && c.CategoryName.Contains(keyword!)) ||
  81 + (t.TypeName != null && t.TypeName.Contains(keyword!)) ||
  82 + (tpl.TemplateName != null && tpl.TemplateName.Contains(keyword!)) ||
  83 + (p.ProductName != null && p.ProductName.Contains(keyword!)))
  84 + .Select((l, c, t, tpl, lp, p) => l);
  85 + }
68 86  
  87 + // 排序(优先外部 Sorting,否则按最后编辑倒序)
69 88 if (!string.IsNullOrWhiteSpace(input.Sorting))
70 89 {
71   - query = query.OrderBy(input.Sorting);
  90 + labelIdsQuery = labelIdsQuery.OrderBy(input.Sorting);
  91 + }
  92 + else
  93 + {
  94 + labelIdsQuery = labelIdsQuery.OrderByDescending(l => l.LastModificationTime ?? l.CreationTime);
  95 + }
  96 +
  97 + var pageLabelIds = await labelIdsQuery
  98 + .Select(l => l.Id)
  99 + .Distinct()
  100 + .ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
  101 +
  102 + if (pageLabelIds.Count == 0)
  103 + {
  104 + return new PagedResultWithPageDto<LabelGetListOutputDto>
  105 + {
  106 + PageIndex = 1,
  107 + PageSize = input.MaxResultCount,
  108 + TotalCount = total,
  109 + TotalPages = 0,
  110 + Items = new List<LabelGetListOutputDto>()
  111 + };
72 112 }
73 113  
74   - var entities = await query.Select((lp, l, p, c, t, tpl) => new
  114 + // 查询标签基础信息(分类/类型/模板)
  115 + var labelRows = await _dbContext.SqlSugarClient
  116 + .Queryable<FlLabelDbEntity, FlLabelCategoryDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity>(
  117 + (l, c, t, tpl) => l.LabelCategoryId == c.Id && l.LabelTypeId == t.Id && l.TemplateId == tpl.Id)
  118 + .Where((l, c, t, tpl) => pageLabelIds.Contains(l.Id))
  119 + .Where((l, c, t, tpl) => !l.IsDeleted && !c.IsDeleted && !t.IsDeleted && !tpl.IsDeleted)
  120 + .Select((l, c, t, tpl) => new
75 121 {
76   - LabelCode = l.LabelCode,
77   - LabelName = l.LabelName,
78   - LocationId = l.LocationId,
79   - LocationName = (string?)null, // later fill
  122 + l.Id,
  123 + l.LabelCode,
  124 + l.LabelName,
  125 + l.LocationId,
80 126 LabelCategoryName = c.CategoryName,
81   - ProductCategoryName = p.CategoryName,
82   - ProductName = p.ProductName,
83   - TemplateName = tpl.TemplateName,
84 127 LabelTypeName = t.TypeName,
85   - State = l.State,
  128 + TemplateName = tpl.TemplateName,
  129 + l.State,
86 130 LastEdited = l.LastModificationTime ?? l.CreationTime
87 131 })
88   - .ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
  132 + .ToListAsync();
  133 +
  134 + // 按分页顺序输出
  135 + var labelMap = labelRows.ToDictionary(x => x.Id, x => x);
  136 + var orderedLabels = pageLabelIds.Where(id => labelMap.ContainsKey(id)).Select(id => labelMap[id]).ToList();
  137 +
  138 + // 查询 products 并拼接
  139 + var productRows = await _dbContext.SqlSugarClient
  140 + .Queryable<FlLabelProductDbEntity, FlLabelDbEntity, FlProductDbEntity>(
  141 + (lp, l, p) => lp.LabelId == l.Id && lp.ProductId == p.Id)
  142 + .Where((lp, l, p) => pageLabelIds.Contains(lp.LabelId))
  143 + .Where((lp, l, p) => !l.IsDeleted && !p.IsDeleted)
  144 + .Select((lp, l, p) => new { lp.LabelId, p.ProductName, p.CategoryName })
  145 + .ToListAsync();
  146 +
  147 + var productsMap = productRows
  148 + .GroupBy(x => x.LabelId)
  149 + .ToDictionary(
  150 + g => g.Key,
  151 + g => new
  152 + {
  153 + Products = string.Join(",", g.Select(x => x.ProductName ?? string.Empty).Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()),
  154 + ProductCategoryName = string.Join(",", g.Select(x => x.CategoryName ?? string.Empty).Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct())
  155 + });
89 156  
90   - var locationIds = entities
  157 + var locationIds = orderedLabels
91 158 .Select(x => x.LocationId)
92 159 .Where(x => !string.IsNullOrWhiteSpace(x))
93 160 .Select(x => x!.Trim())
... ... @@ -108,21 +175,23 @@ public class LabelAppService : ApplicationService, ILabelAppService
108 175 }
109 176 }
110 177  
111   - var items = entities.Select(x =>
  178 + var items = orderedLabels.Select(x =>
112 179 {
113 180 var locationName = string.Empty;
114 181 if (!string.IsNullOrWhiteSpace(x.LocationId) && locationMap.TryGetValue(x.LocationId!, out var loc))
115 182 {
116 183 locationName = loc.LocationName ?? loc.LocationCode;
117 184 }
  185 + var products = productsMap.TryGetValue(x.Id, out var prod) ? prod.Products : string.Empty;
  186 + var productCategoryNameValue = productsMap.TryGetValue(x.Id, out var prod2) ? prod2.ProductCategoryName : string.Empty;
118 187 return new LabelGetListOutputDto
119 188 {
120 189 Id = x.LabelCode ?? string.Empty,
121 190 LabelName = x.LabelName ?? string.Empty,
122 191 LocationName = string.IsNullOrWhiteSpace(locationName) ? "无" : locationName,
123 192 LabelCategoryName = x.LabelCategoryName ?? string.Empty,
124   - ProductCategoryName = x.ProductCategoryName ?? string.Empty,
125   - ProductName = x.ProductName ?? string.Empty,
  193 + ProductCategoryName = string.IsNullOrWhiteSpace(productCategoryNameValue) ? "无" : productCategoryNameValue,
  194 + Products = products,
126 195 TemplateName = x.TemplateName ?? string.Empty,
127 196 LabelTypeName = x.LabelTypeName ?? string.Empty,
128 197 State = x.State,
... ...
美国版/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/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<FlLabelCategoryDbEntity>()
  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 + if (!string.IsNullOrWhiteSpace(input.Sorting))
  42 + {
  43 + query = query.OrderBy(input.Sorting);
  44 + }
  45 + else
  46 + {
  47 + query = query.OrderByDescending(x => x.OrderNum).OrderByDescending(x => x.CreationTime);
  48 + }
  49 +
  50 + var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
  51 +
  52 + var items = entities.Select(x => new ProductCategoryGetListOutputDto
  53 + {
  54 + Id = x.Id,
  55 + CategoryCode = x.CategoryCode,
  56 + CategoryName = x.CategoryName,
  57 + CategoryPhotoUrl = x.CategoryPhotoUrl,
  58 + State = x.State,
  59 + OrderNum = x.OrderNum,
  60 + LastEdited = x.LastModificationTime ?? x.CreationTime
  61 + }).ToList();
  62 +
  63 + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items);
  64 + }
  65 +
  66 + /// <summary>
  67 + /// 类别详情
  68 + /// </summary>
  69 + public async Task<ProductCategoryGetOutputDto> GetAsync(string id)
  70 + {
  71 + var entity = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
  72 + .FirstAsync(x => x.Id == id && !x.IsDeleted);
  73 + if (entity is null)
  74 + {
  75 + throw new UserFriendlyException("类别不存在");
  76 + }
  77 +
  78 + return MapToGetOutput(entity);
  79 + }
  80 +
  81 + /// <summary>
  82 + /// 新增类别
  83 + /// </summary>
  84 + public async Task<ProductCategoryGetOutputDto> CreateAsync(ProductCategoryCreateInputVo input)
  85 + {
  86 + var code = input.CategoryCode?.Trim();
  87 + var name = input.CategoryName?.Trim();
  88 + if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(name))
  89 + {
  90 + throw new UserFriendlyException("类别编码和名称不能为空");
  91 + }
  92 +
  93 + var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
  94 + .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name));
  95 + if (duplicated)
  96 + {
  97 + throw new UserFriendlyException("类别编码或名称已存在");
  98 + }
  99 +
  100 + var entity = new FlLabelCategoryDbEntity
  101 + {
  102 + Id = _guidGenerator.Create().ToString(),
  103 + CategoryCode = code,
  104 + CategoryName = name,
  105 + CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(),
  106 + State = input.State,
  107 + OrderNum = input.OrderNum
  108 + };
  109 +
  110 + await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  111 + return await GetAsync(entity.Id);
  112 + }
  113 +
  114 + /// <summary>
  115 + /// 编辑类别
  116 + /// </summary>
  117 + public async Task<ProductCategoryGetOutputDto> UpdateAsync(string id, ProductCategoryUpdateInputVo input)
  118 + {
  119 + var entity = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
  120 + .FirstAsync(x => x.Id == id && !x.IsDeleted);
  121 + if (entity is null)
  122 + {
  123 + throw new UserFriendlyException("类别不存在");
  124 + }
  125 +
  126 + var code = input.CategoryCode?.Trim();
  127 + var name = input.CategoryName?.Trim();
  128 + if (string.IsNullOrWhiteSpace(code) || string.IsNullOrWhiteSpace(name))
  129 + {
  130 + throw new UserFriendlyException("类别编码和名称不能为空");
  131 + }
  132 +
  133 + var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
  134 + .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name));
  135 + if (duplicated)
  136 + {
  137 + throw new UserFriendlyException("类别编码或名称已存在");
  138 + }
  139 +
  140 + entity.CategoryCode = code;
  141 + entity.CategoryName = name;
  142 + entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim();
  143 + entity.State = input.State;
  144 + entity.OrderNum = input.OrderNum;
  145 + entity.LastModificationTime = DateTime.Now;
  146 + entity.LastModifierId = CurrentUser?.Id?.ToString();
  147 +
  148 + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  149 + return await GetAsync(id);
  150 + }
  151 +
  152 + /// <summary>
  153 + /// 删除类别(逻辑删除)
  154 + /// </summary>
  155 + public async Task DeleteAsync(string id)
  156 + {
  157 + var entity = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
  158 + .FirstAsync(x => x.Id == id && !x.IsDeleted);
  159 + if (entity is null)
  160 + {
  161 + return;
  162 + }
  163 +
  164 + // 若被标签引用则不允许删除(保持与 LabelCategory 一致的约束)
  165 + var used = await _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>()
  166 + .AnyAsync(x => !x.IsDeleted && x.LabelCategoryId == id);
  167 + if (used)
  168 + {
  169 + throw new UserFriendlyException("该类别已被标签引用,无法删除");
  170 + }
  171 +
  172 + entity.IsDeleted = true;
  173 + entity.LastModificationTime = DateTime.Now;
  174 + entity.LastModifierId = CurrentUser?.Id?.ToString();
  175 + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  176 + }
  177 +
  178 + private static ProductCategoryGetOutputDto MapToGetOutput(FlLabelCategoryDbEntity x)
  179 + {
  180 + return new ProductCategoryGetOutputDto
  181 + {
  182 + Id = x.Id,
  183 + CategoryCode = x.CategoryCode,
  184 + CategoryName = x.CategoryName,
  185 + CategoryPhotoUrl = x.CategoryPhotoUrl,
  186 + State = x.State,
  187 + OrderNum = x.OrderNum
  188 + };
  189 + }
  190 +
  191 + private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items)
  192 + {
  193 + var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount;
  194 + var pageIndex = pageSize <= 0 ? 1 : (skipCount / pageSize) + 1;
  195 + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize);
  196 + return new PagedResultWithPageDto<T>
  197 + {
  198 + PageIndex = pageIndex,
  199 + PageSize = pageSize,
  200 + TotalCount = total,
  201 + TotalPages = totalPages,
  202 + Items = items
  203 + };
  204 + }
  205 +}
  206 +
... ...
美国版/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_label_category`
  10 +- **图片字段**:`CategoryPhotoUrl`(前端字段:`categoryPhotoUrl`)
  11 +
  12 +> 说明:本文以 Swagger 为准(本地示例:`http://localhost:19001/swagger`,搜索 `ProductCategory`)。
  13 +
  14 +---
  15 +
  16 +## 接口 1:类别分页列表
  17 +
  18 +### HTTP
  19 +
  20 +- **方法**:`GET`
  21 +- **路径**:`/api/app/product-category`
  22 +- **鉴权**:需要登录(Header:`Authorization: Bearer {token}`)
  23 +
  24 +### 入参(Query 参数)
  25 +
  26 +| 参数名 | 类型 | 必填 | 说明 |
  27 +|------|------|------|------|
  28 +| `skipCount` | number | 是 | 跳过条数(分页) |
  29 +| `maxResultCount` | number | 是 | 每页条数(分页) |
  30 +| `sorting` | string | 否 | 排序字段(如 `OrderNum desc`),不传则按 `OrderNum desc, CreationTime desc` |
  31 +| `keyword` | string | 否 | 模糊搜索(匹配 `CategoryCode/CategoryName`) |
  32 +| `state` | boolean | 否 | 启用状态过滤 |
  33 +
  34 +### 请求示例
  35 +
  36 +```http
  37 +GET /api/app/product-category?skipCount=0&maxResultCount=10&keyword=Prep HTTP/1.1
  38 +Host: localhost:19001
  39 +Authorization: Bearer eyJhbGciOi...
  40 +```
  41 +
  42 +### 出参(PagedResultWithPageDto<ProductCategoryGetListOutputDto>)
  43 +
  44 +| 字段 | 类型 | 说明 |
  45 +|------|------|------|
  46 +| `pageIndex` | number | 当前页(从 1 开始) |
  47 +| `pageSize` | number | 每页条数 |
  48 +| `totalCount` | number | 总数 |
  49 +| `totalPages` | number | 总页数 |
  50 +| `items` | array | 当前页数据 |
  51 +
  52 +`items[]` 字段:
  53 +
  54 +| 字段 | 类型 | 说明 |
  55 +|------|------|------|
  56 +| `id` | string | 主键 |
  57 +| `categoryCode` | string | 类别编码 |
  58 +| `categoryName` | string | 类别名称 |
  59 +| `categoryPhotoUrl` | string \| null | 类别图片 URL(建议用 `/picture/...`) |
  60 +| `state` | boolean | 是否启用 |
  61 +| `orderNum` | number | 排序 |
  62 +| `lastEdited` | string | 最后编辑时间 |
  63 +
  64 +### 响应示例
  65 +
  66 +```json
  67 +{
  68 + "pageIndex": 1,
  69 + "pageSize": 10,
  70 + "totalCount": 1,
  71 + "totalPages": 1,
  72 + "items": [
  73 + {
  74 + "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
  75 + "categoryCode": "CAT_PREP",
  76 + "categoryName": "Prep",
  77 + "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png",
  78 + "state": true,
  79 + "orderNum": 100,
  80 + "lastEdited": "2026-03-25 12:30:10"
  81 + }
  82 + ]
  83 +}
  84 +```
  85 +
  86 +---
  87 +
  88 +## 接口 2:类别详情
  89 +
  90 +### HTTP
  91 +
  92 +- **方法**:`GET`
  93 +- **路径**:`/api/app/product-category/{id}`
  94 +
  95 +### 请求示例
  96 +
  97 +```http
  98 +GET /api/app/product-category/a2696b9e-2277-11f1-b4c6-00163e0c7c4f HTTP/1.1
  99 +Host: localhost:19001
  100 +Authorization: Bearer eyJhbGciOi...
  101 +```
  102 +
  103 +### 响应示例(ProductCategoryGetOutputDto)
  104 +
  105 +```json
  106 +{
  107 + "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
  108 + "categoryCode": "CAT_PREP",
  109 + "categoryName": "Prep",
  110 + "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png",
  111 + "state": true,
  112 + "orderNum": 100
  113 +}
  114 +```
  115 +
  116 +---
  117 +
  118 +## 接口 3:新增类别
  119 +
  120 +### HTTP
  121 +
  122 +- **方法**:`POST`
  123 +- **路径**:`/api/app/product-category`
  124 +- **Content-Type**:`application/json`
  125 +
  126 +### 入参(Body JSON:ProductCategoryCreateInputVo)
  127 +
  128 +| 字段 | 类型 | 必填 | 说明 |
  129 +|------|------|------|------|
  130 +| `categoryCode` | string | 是 | 类别编码(唯一) |
  131 +| `categoryName` | string | 是 | 类别名称(唯一) |
  132 +| `categoryPhotoUrl` | string \| null | 否 | 图片 URL(建议先上传图片拿到 `/picture/...` 再保存) |
  133 +| `state` | boolean | 否 | 是否启用(默认 true) |
  134 +| `orderNum` | number | 否 | 排序(默认 0) |
  135 +
  136 +### 请求示例
  137 +
  138 +```json
  139 +{
  140 + "categoryCode": "CAT_PREP",
  141 + "categoryName": "Prep",
  142 + "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png",
  143 + "state": true,
  144 + "orderNum": 100
  145 +}
  146 +```
  147 +
  148 +---
  149 +
  150 +## 接口 4:编辑类别
  151 +
  152 +### HTTP
  153 +
  154 +- **方法**:`PUT`
  155 +- **路径**:`/api/app/product-category/{id}`
  156 +- **Content-Type**:`application/json`
  157 +
  158 +### 请求示例
  159 +
  160 +```json
  161 +{
  162 + "categoryCode": "CAT_PREP",
  163 + "categoryName": "Prep",
  164 + "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png",
  165 + "state": true,
  166 + "orderNum": 100
  167 +}
  168 +```
  169 +
  170 +---
  171 +
  172 +## 接口 5:删除类别(逻辑删除)
  173 +
  174 +### HTTP
  175 +
  176 +- **方法**:`DELETE`
  177 +- **路径**:`/api/app/product-category/{id}`
  178 +
  179 +### 约束
  180 +
  181 +- 若该类别已被 `fl_label` 引用(`fl_label.LabelCategoryId = id`),删除会失败并返回友好提示:`该类别已被标签引用,无法删除`。
  182 +
  183 +### 请求示例
  184 +
  185 +```http
  186 +DELETE /api/app/product-category/a2696b9e-2277-11f1-b4c6-00163e0c7c4f HTTP/1.1
  187 +Host: localhost:19001
  188 +Authorization: Bearer eyJhbGciOi...
  189 +```
  190 +
  191 +---
  192 +
  193 +## 配套:类别图片上传接口
  194 +
  195 +类别图片上传接口见文档:
  196 +
  197 +- `项目相关文档/平台端Categories图片上传接口说明.md`
  198 +
  199 +推荐前端流程:
  200 +
  201 +1. 调用上传接口 `POST /api/app/picture/category/upload` 拿到响应 `url`
  202 +2. 新增/编辑类别时把 `categoryPhotoUrl` 设为该 `url`
  203 +
... ...
项目相关文档/平台端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
... ... @@ -355,7 +355,8 @@ Swagger 地址:
355 355 ## 接口 5:Labels(按产品展示多个标签)
356 356  
357 357 说明:
358   -- 列表接口按 `ProductId` 查询,一个产品会对应多条标签记录。
  358 +- 列表接口以“标签”为维度分页展示(同一个标签会绑定多个产品)。
  359 +- 列表支持按 `ProductId` 过滤:仅返回“绑定了该产品”的标签。
359 360 - 标签详情/编辑/删除的 `id` 使用 `fl_label.LabelCode`。
360 361  
361 362 ### 5.1 分页列表(按产品)
... ... @@ -379,6 +380,11 @@ Swagger 地址:
379 380 }
380 381 ```
381 382  
  383 +列表出参要点(`LabelGetListOutputDto`):
  384 +
  385 +- `products`:同一个标签下绑定的产品名称,用 `,` 分割(例如:`Chicken,Sandwich`)
  386 +- 其他字段与之前一致:`labelName/locationName/category/type/template/state/lastEdited...`
  387 +
382 388 ### 5.2 详情
383 389  
384 390 方法:`GET /api/app/label/{id}`
... ...