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 | 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
美国版/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 | 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}` | ... | ... |