Commit 28dc179d4556d684c1b73699886f150a5e2c18e2
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,7 +12,10 @@ public class LabelGetListOutputDto | ||
| 12 | 12 | ||
| 13 | public string ProductCategoryName { get; set; } = string.Empty; | 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 | public string TemplateName { get; set; } = string.Empty; | 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
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPictureAppService.cs
0 → 100644
美国版/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,55 +39,122 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 39 | var labelTypeId = input.LabelTypeId?.Trim(); | 39 | var labelTypeId = input.LabelTypeId?.Trim(); |
| 40 | var templateCode = input.TemplateCode?.Trim(); | 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 | l.LabelName.Contains(keyword!) || | 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 | if (!string.IsNullOrWhiteSpace(input.Sorting)) | 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 | LabelCategoryName = c.CategoryName, | 126 | LabelCategoryName = c.CategoryName, |
| 81 | - ProductCategoryName = p.CategoryName, | ||
| 82 | - ProductName = p.ProductName, | ||
| 83 | - TemplateName = tpl.TemplateName, | ||
| 84 | LabelTypeName = t.TypeName, | 127 | LabelTypeName = t.TypeName, |
| 85 | - State = l.State, | 128 | + TemplateName = tpl.TemplateName, |
| 129 | + l.State, | ||
| 86 | LastEdited = l.LastModificationTime ?? l.CreationTime | 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 | .Select(x => x.LocationId) | 158 | .Select(x => x.LocationId) |
| 92 | .Where(x => !string.IsNullOrWhiteSpace(x)) | 159 | .Where(x => !string.IsNullOrWhiteSpace(x)) |
| 93 | .Select(x => x!.Trim()) | 160 | .Select(x => x!.Trim()) |
| @@ -108,21 +175,23 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -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 | var locationName = string.Empty; | 180 | var locationName = string.Empty; |
| 114 | if (!string.IsNullOrWhiteSpace(x.LocationId) && locationMap.TryGetValue(x.LocationId!, out var loc)) | 181 | if (!string.IsNullOrWhiteSpace(x.LocationId) && locationMap.TryGetValue(x.LocationId!, out var loc)) |
| 115 | { | 182 | { |
| 116 | locationName = loc.LocationName ?? loc.LocationCode; | 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 | return new LabelGetListOutputDto | 187 | return new LabelGetListOutputDto |
| 119 | { | 188 | { |
| 120 | Id = x.LabelCode ?? string.Empty, | 189 | Id = x.LabelCode ?? string.Empty, |
| 121 | LabelName = x.LabelName ?? string.Empty, | 190 | LabelName = x.LabelName ?? string.Empty, |
| 122 | LocationName = string.IsNullOrWhiteSpace(locationName) ? "无" : locationName, | 191 | LocationName = string.IsNullOrWhiteSpace(locationName) ? "无" : locationName, |
| 123 | LabelCategoryName = x.LabelCategoryName ?? string.Empty, | 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 | TemplateName = x.TemplateName ?? string.Empty, | 195 | TemplateName = x.TemplateName ?? string.Empty, |
| 127 | LabelTypeName = x.LabelTypeName ?? string.Empty, | 196 | LabelTypeName = x.LabelTypeName ?? string.Empty, |
| 128 | State = x.State, | 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,6 +12,7 @@ using Microsoft.AspNetCore.Mvc; | ||
| 12 | using Microsoft.AspNetCore.StaticFiles; | 12 | using Microsoft.AspNetCore.StaticFiles; |
| 13 | using Microsoft.IdentityModel.Tokens; | 13 | using Microsoft.IdentityModel.Tokens; |
| 14 | using Microsoft.OpenApi.Models; | 14 | using Microsoft.OpenApi.Models; |
| 15 | +using Microsoft.Extensions.FileProviders; | ||
| 15 | using StackExchange.Redis; | 16 | using StackExchange.Redis; |
| 16 | using Volo.Abp.AspNetCore.Auditing; | 17 | using Volo.Abp.AspNetCore.Auditing; |
| 17 | using Volo.Abp.AspNetCore.Authentication.JwtBearer; | 18 | using Volo.Abp.AspNetCore.Authentication.JwtBearer; |
| @@ -397,6 +398,19 @@ namespace Yi.Abp.Web | @@ -397,6 +398,19 @@ namespace Yi.Abp.Web | ||
| 397 | }); | 398 | }); |
| 398 | app.UseDirectoryBrowser("/api/app/wwwroot"); | 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 | app.Properties.Add("_AbpExceptionHandlingMiddleware_Added", false); | 414 | app.Properties.Add("_AbpExceptionHandlingMiddleware_Added", false); |
| 401 | //工作单元 | 415 | //工作单元 |
| 402 | app.UseUnitOfWork(); | 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,7 +355,8 @@ Swagger 地址: | ||
| 355 | ## 接口 5:Labels(按产品展示多个标签) | 355 | ## 接口 5:Labels(按产品展示多个标签) |
| 356 | 356 | ||
| 357 | 说明: | 357 | 说明: |
| 358 | -- 列表接口按 `ProductId` 查询,一个产品会对应多条标签记录。 | 358 | +- 列表接口以“标签”为维度分页展示(同一个标签会绑定多个产品)。 |
| 359 | +- 列表支持按 `ProductId` 过滤:仅返回“绑定了该产品”的标签。 | ||
| 359 | - 标签详情/编辑/删除的 `id` 使用 `fl_label.LabelCode`。 | 360 | - 标签详情/编辑/删除的 `id` 使用 `fl_label.LabelCode`。 |
| 360 | 361 | ||
| 361 | ### 5.1 分页列表(按产品) | 362 | ### 5.1 分页列表(按产品) |
| @@ -379,6 +380,11 @@ Swagger 地址: | @@ -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 | ### 5.2 详情 | 388 | ### 5.2 详情 |
| 383 | 389 | ||
| 384 | 方法:`GET /api/app/label/{id}` | 390 | 方法:`GET /api/app/label/{id}` |