Commit 8e0c49ebdba651766bb1bc597822e6af2063c69b

Authored by 李曜臣
1 parent 3ead62fc

产品标签类别优化;

菜单权限优化;
标签code设置不必填
Showing 29 changed files with 1086 additions and 21 deletions
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserBriefDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.AuthSession;
  2 +
  3 +/// <summary>
  4 +/// 当前登录用户简要信息(不含敏感字段)
  5 +/// </summary>
  6 +public class CurrentUserBriefDto
  7 +{
  8 + public Guid Id { get; set; }
  9 +
  10 + public string UserName { get; set; } = string.Empty;
  11 +
  12 + public string? Nick { get; set; }
  13 +
  14 + public string? Email { get; set; }
  15 +
  16 + public string? Icon { get; set; }
  17 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuNodeDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.AuthSession;
  2 +
  3 +/// <summary>
  4 +/// 当前用户可见菜单树节点(与权限分配一致)
  5 +/// </summary>
  6 +public class CurrentUserMenuNodeDto
  7 +{
  8 + public string Id { get; set; } = string.Empty;
  9 +
  10 + public string ParentId { get; set; } = "0";
  11 +
  12 + public string MenuName { get; set; } = string.Empty;
  13 +
  14 + public string? RouterName { get; set; }
  15 +
  16 + public string? Router { get; set; }
  17 +
  18 + public string? PermissionCode { get; set; }
  19 +
  20 + public int MenuType { get; set; }
  21 +
  22 + public int MenuSource { get; set; }
  23 +
  24 + public int OrderNum { get; set; }
  25 +
  26 + public bool State { get; set; }
  27 +
  28 + public string? MenuIcon { get; set; }
  29 +
  30 + public string? Component { get; set; }
  31 +
  32 + public bool IsLink { get; set; }
  33 +
  34 + public bool IsCache { get; set; }
  35 +
  36 + public bool IsShow { get; set; }
  37 +
  38 + public string? Query { get; set; }
  39 +
  40 + public string? Remark { get; set; }
  41 +
  42 + public List<CurrentUserMenuNodeDto> Children { get; set; } = new();
  43 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuPermissionsOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.AuthSession;
  2 +
  3 +/// <summary>
  4 +/// 当前登录用户的菜单与权限码(用于前端动态路由/按钮权限)
  5 +/// </summary>
  6 +public class CurrentUserMenuPermissionsOutputDto
  7 +{
  8 + public CurrentUserBriefDto User { get; set; } = new();
  9 +
  10 + public List<string> RoleCodes { get; set; } = new();
  11 +
  12 + public List<string> PermissionCodes { get; set; } = new();
  13 +
  14 + public List<CurrentUserMenuNodeDto> Menus { get; set; } = new();
  15 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelCreateInputVo.cs
... ... @@ -2,7 +2,7 @@ namespace FoodLabeling.Application.Contracts.Dtos.Label;
2 2  
3 3 public class LabelCreateInputVo
4 4 {
5   - public string LabelCode { get; set; } = string.Empty;
  5 + public string? LabelCode { get; set; }
6 6  
7 7 public string LabelName { get; set; } = string.Empty;
8 8  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryCreateInputVo.cs
... ... @@ -6,10 +6,33 @@ public class LabelCategoryCreateInputVo
6 6  
7 7 public string CategoryName { get; set; } = string.Empty;
8 8  
  9 + /// <summary>
  10 + /// 按钮展示文案(为空则默认使用 CategoryName)
  11 + /// </summary>
  12 + public string? DisplayText { get; set; }
  13 +
  14 + /// <summary>
  15 + /// COLOR 模式存色值、IMAGE 模式存图片 URL、TEXT 可为分类小图或空(与 buttonAppearance 配合)
  16 + /// </summary>
9 17 public string? CategoryPhotoUrl { get; set; }
10 18  
11 19 public bool State { get; set; } = true;
12 20  
  21 + /// <summary>
  22 + /// 按钮外观:TEXT / COLOR / IMAGE(展示值见 categoryPhotoUrl)
  23 + /// </summary>
  24 + public string ButtonAppearance { get; set; } = "TEXT";
  25 +
  26 + /// <summary>
  27 + /// 门店可用范围:ALL / SPECIFIED
  28 + /// </summary>
  29 + public string AvailabilityType { get; set; } = "ALL";
  30 +
  31 + /// <summary>
  32 + /// 指定门店 Id 列表(当 AvailabilityType=SPECIFIED 时必填)
  33 + /// </summary>
  34 + public List<string>? LocationIds { get; set; }
  35 +
13 36 public int OrderNum { get; set; }
14 37 }
15 38  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListOutputDto.cs
... ... @@ -8,10 +8,16 @@ public class LabelCategoryGetListOutputDto
8 8  
9 9 public string CategoryName { get; set; } = string.Empty;
10 10  
  11 + public string? DisplayText { get; set; }
  12 +
11 13 public string? CategoryPhotoUrl { get; set; }
12 14  
13 15 public bool State { get; set; }
14 16  
  17 + public string ButtonAppearance { get; set; } = "TEXT";
  18 +
  19 + public string AvailabilityType { get; set; } = "ALL";
  20 +
15 21 public int OrderNum { get; set; }
16 22  
17 23 public long NoOfLabels { get; set; }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetOutputDto.cs
... ... @@ -8,10 +8,18 @@ public class LabelCategoryGetOutputDto
8 8  
9 9 public string CategoryName { get; set; } = string.Empty;
10 10  
  11 + public string? DisplayText { get; set; }
  12 +
11 13 public string? CategoryPhotoUrl { get; set; }
12 14  
13 15 public bool State { get; set; }
14 16  
  17 + public string ButtonAppearance { get; set; } = "TEXT";
  18 +
  19 + public string AvailabilityType { get; set; } = "ALL";
  20 +
  21 + public List<string> LocationIds { get; set; } = new();
  22 +
15 23 public int OrderNum { get; set; }
16 24 }
17 25  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateElementDto.cs
... ... @@ -3,7 +3,7 @@ using System.Text.Json.Serialization;
3 3 namespace FoodLabeling.Application.Contracts.Dtos.LabelTemplate;
4 4  
5 5 /// <summary>
6   -/// 模板元素(对齐你给的 editor JSON:id/type/x/y/width/height/rotation/border/config
  6 +/// 模板元素(对齐 editor JSON:id/type/typeAdd/elementName/x/y/width/height/rotation/border/config 等
7 7 /// </summary>
8 8 public class LabelTemplateElementDto
9 9 {
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryCreateInputVo.cs
... ... @@ -9,10 +9,33 @@ public class ProductCategoryCreateInputVo
9 9  
10 10 public string CategoryName { get; set; } = string.Empty;
11 11  
  12 + /// <summary>
  13 + /// 按钮展示文案(为空则默认使用 CategoryName)
  14 + /// </summary>
  15 + public string? DisplayText { get; set; }
  16 +
  17 + /// <summary>
  18 + /// COLOR 模式存色值、IMAGE 模式存图片 URL、TEXT 可为分类小图或空(与 buttonAppearance 配合)
  19 + /// </summary>
12 20 public string? CategoryPhotoUrl { get; set; }
13 21  
  22 + /// <summary>
  23 + /// 按钮外观:TEXT / COLOR / IMAGE(展示值见 categoryPhotoUrl)
  24 + /// </summary>
  25 + public string ButtonAppearance { get; set; } = "TEXT";
  26 +
14 27 public bool State { get; set; } = true;
15 28  
  29 + /// <summary>
  30 + /// 门店可用范围:ALL / SPECIFIED
  31 + /// </summary>
  32 + public string AvailabilityType { get; set; } = "ALL";
  33 +
  34 + /// <summary>
  35 + /// 指定门店 Id 列表(当 AvailabilityType=SPECIFIED 时必填)
  36 + /// </summary>
  37 + public List<string>? LocationIds { get; set; }
  38 +
16 39 public int OrderNum { get; set; } = 0;
17 40 }
18 41  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListOutputDto.cs
... ... @@ -11,10 +11,16 @@ public class ProductCategoryGetListOutputDto
11 11  
12 12 public string CategoryName { get; set; } = string.Empty;
13 13  
  14 + public string? DisplayText { get; set; }
  15 +
14 16 public string? CategoryPhotoUrl { get; set; }
15 17  
  18 + public string ButtonAppearance { get; set; } = "TEXT";
  19 +
16 20 public bool State { get; set; }
17 21  
  22 + public string AvailabilityType { get; set; } = "ALL";
  23 +
18 24 public int OrderNum { get; set; }
19 25  
20 26 public DateTime LastEdited { get; set; }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs
... ... @@ -11,10 +11,19 @@ public class ProductCategoryGetOutputDto
11 11  
12 12 public string CategoryName { get; set; } = string.Empty;
13 13  
  14 + public string? DisplayText { get; set; }
  15 +
  16 + /// <summary>COLOR 色值 / IMAGE 图片 URL / TEXT 可选图</summary>
14 17 public string? CategoryPhotoUrl { get; set; }
15 18  
  19 + public string ButtonAppearance { get; set; } = "TEXT";
  20 +
16 21 public bool State { get; set; }
17 22  
  23 + public string AvailabilityType { get; set; } = "ALL";
  24 +
  25 + public List<string> LocationIds { get; set; } = new();
  26 +
18 27 public int OrderNum { get; set; }
19 28 }
20 29  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacMenu/RbacMenuGetListOutputDto.cs
... ... @@ -11,6 +11,10 @@ public class RbacMenuGetListOutputDto
11 11  
12 12 public string MenuName { get; set; } = string.Empty;
13 13  
  14 + public string? RouterName { get; set; }
  15 +
  16 + public string? Router { get; set; }
  17 +
14 18 public string? PermissionCode { get; set; }
15 19  
16 20 public int MenuType { get; set; }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelCategoryTreeNodeDto.cs
... ... @@ -11,6 +11,9 @@ public class UsAppLabelCategoryTreeNodeDto
11 11  
12 12 public string? CategoryPhotoUrl { get; set; }
13 13  
  14 + /// <summary>按钮外观:TEXT / COLOR / IMAGE(COLOR/IMAGE 的展示值在 categoryPhotoUrl)</summary>
  15 + public string ButtonAppearance { get; set; } = "TEXT";
  16 +
14 17 public int OrderNum { get; set; }
15 18  
16 19 public List<UsAppProductCategoryNodeDto> ProductCategories { get; set; } = new();
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintInputVo.cs
... ... @@ -49,11 +49,6 @@ public class UsAppLabelPrintInputVo
49 49 public JsonElement? PrintInputJson { get; set; }
50 50  
51 51 /// <summary>
52   - /// 客户端幂等请求 Id(可选);重复相同值时由服务端决定是否直接返回首次结果(见接口文档)。
53   - /// </summary>
54   - public string? ClientRequestId { get; set; }
55   -
56   - /// <summary>
57 52 /// 打印机Id(可选,若业务需要追踪)
58 53 /// </summary>
59 54 public string? PrinterId { get; set; }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppProductCategoryNodeDto.cs
... ... @@ -14,6 +14,18 @@ public class UsAppProductCategoryNodeDto
14 14 /// <summary>分类显示名;空为「无」</summary>
15 15 public string Name { get; set; } = string.Empty;
16 16  
  17 + /// <summary>按钮展示文案;为空时客户端可回退使用 Name</summary>
  18 + public string? DisplayText { get; set; }
  19 +
  20 + /// <summary>按钮外观:TEXT / COLOR / IMAGE(COLOR/IMAGE 的展示值在 categoryPhotoUrl)</summary>
  21 + public string ButtonAppearance { get; set; } = "TEXT";
  22 +
  23 + /// <summary>门店可用范围:ALL / SPECIFIED(本树已按当前门店过滤)</summary>
  24 + public string AvailabilityType { get; set; } = "ALL";
  25 +
  26 + /// <summary>排序号(来自 fl_product_category;未归类为较大值以排在后)</summary>
  27 + public int OrderNum { get; set; }
  28 +
17 29 public int ItemCount { get; set; }
18 30  
19 31 public List<UsAppLabelingProductNodeDto> Products { get; set; } = new();
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IAuthSessionAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.AuthSession;
  2 +using Volo.Abp.Application.Services;
  3 +
  4 +namespace FoodLabeling.Application.Contracts.IServices;
  5 +
  6 +/// <summary>
  7 +/// 当前登录会话:菜单权限与退出(美国版 Web 管理端)
  8 +/// </summary>
  9 +public interface IAuthSessionAppService : IApplicationService
  10 +{
  11 + /// <summary>
  12 + /// 获取当前登录用户的角色编码、权限码与可见菜单树
  13 + /// </summary>
  14 + /// <remarks>
  15 + /// 与框架 <c>UserManager.GetInfoAsync</c> 一致;用户名为 <c>admin</c> 时返回全部未删除菜单(与 <c>AccountService.GetVue3Router</c> 行为对齐)。
  16 + /// </remarks>
  17 + /// <returns>用户简要信息、权限码与菜单树</returns>
  18 + /// <response code="200">成功</response>
  19 + /// <response code="401">未登录或令牌无效</response>
  20 + /// <response code="500">服务器错误</response>
  21 + Task<CurrentUserMenuPermissionsOutputDto> GetMyMenusAsync();
  22 +
  23 + /// <summary>
  24 + /// 退出登录:清除服务端用户信息缓存(JWT 仍由前端丢弃)
  25 + /// </summary>
  26 + /// <remarks>
  27 + /// 与框架 <c>AccountService.PostLogout</c> 一致;未登录时返回 <c>false</c>。
  28 + /// </remarks>
  29 + /// <returns>是否执行了缓存清理(已登录为 true)</returns>
  30 + /// <response code="200">成功</response>
  31 + /// <response code="500">服务器错误</response>
  32 + Task<bool> LogoutAsync();
  33 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs
1 1 using FoodLabeling.Application.Contracts.Dtos.Common;
2 2 using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
3   -using FoodLabeling.Application.Contracts.Dtos.Common;
4 3 using Volo.Abp.Application.Services;
5 4  
6 5 namespace FoodLabeling.Application.Contracts.IServices;
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.AuthSession;
  2 +using FoodLabeling.Application.Contracts.IServices;
  3 +using Microsoft.AspNetCore.Authorization;
  4 +using Microsoft.AspNetCore.Mvc;
  5 +using Volo.Abp;
  6 +using Volo.Abp.Application.Services;
  7 +using Volo.Abp.Caching;
  8 +using FoodLabeling.Application.Services.DbModels;
  9 +using Yi.Framework.Rbac.Domain.Entities;
  10 +using Yi.Framework.Rbac.Domain.Shared.Caches;
  11 +using Yi.Framework.Rbac.Domain.Shared.Consts;
  12 +using Yi.Framework.SqlSugarCore.Abstractions;
  13 +
  14 +namespace FoodLabeling.Application.Services;
  15 +
  16 +/// <summary>
  17 +/// 当前登录会话:菜单权限与退出
  18 +/// </summary>
  19 +[Authorize]
  20 +public class AuthSessionAppService : ApplicationService, IAuthSessionAppService
  21 +{
  22 + private readonly IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> _userCache;
  23 + private readonly ISqlSugarDbContext _dbContext;
  24 + private readonly ISqlSugarRepository<UserAggregateRoot, Guid> _userRepository;
  25 +
  26 + public AuthSessionAppService(
  27 + ISqlSugarDbContext dbContext,
  28 + ISqlSugarRepository<UserAggregateRoot, Guid> userRepository,
  29 + IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> userCache)
  30 + {
  31 + _dbContext = dbContext;
  32 + _userRepository = userRepository;
  33 + _userCache = userCache;
  34 + }
  35 +
  36 + /// <inheritdoc />
  37 + public virtual async Task<CurrentUserMenuPermissionsOutputDto> GetMyMenusAsync()
  38 + {
  39 + if (!CurrentUser.Id.HasValue)
  40 + {
  41 + throw new UserFriendlyException("用户未登录");
  42 + }
  43 +
  44 + // 避免走 UserManager.GetInfoAsync -> UserRepository.GetUserAllInfoAsync 的导航加载
  45 + // 这里直接按 UserRole/RoleMenu/Menu 表关联查询当前用户可见菜单与权限码
  46 + var userId = CurrentUser.Id.Value;
  47 + var user = await _userRepository.GetByIdAsync(userId);
  48 + if (user is null || user.IsDeleted)
  49 + {
  50 + throw new UserFriendlyException("用户不存在");
  51 + }
  52 +
  53 + List<MenuDbEntity> menus;
  54 + if (UserConst.Admin.Equals(user.UserName))
  55 + {
  56 + // MenuAggregateRoot(ParentId 为 Guid) 无法兼容 menu.ParentId=0/字符串:这里统一用 MenuDbEntity
  57 + menus = await _dbContext.SqlSugarClient.Queryable<MenuDbEntity>()
  58 + .Where(x => x.IsDeleted == false)
  59 + .ToListAsync();
  60 + }
  61 + else
  62 + {
  63 + var roleIds = await _dbContext.SqlSugarClient.Queryable<UserRoleEntity>()
  64 + .Where(x => x.UserId == userId)
  65 + .Select(x => x.RoleId)
  66 + .ToListAsync();
  67 +
  68 + var roleIdStrs = roleIds.Select(x => x.ToString()).Distinct().ToList();
  69 + if (roleIdStrs.Count == 0)
  70 + {
  71 + menus = new List<MenuDbEntity>();
  72 + }
  73 + else
  74 + {
  75 + var menuIds = await _dbContext.SqlSugarClient.Queryable<RoleMenuDbEntity>()
  76 + .Where(x => roleIdStrs.Contains(x.RoleId))
  77 + .Select(x => x.MenuId)
  78 + .Distinct()
  79 + .ToListAsync();
  80 +
  81 + menus = menuIds.Count == 0
  82 + ? new List<MenuDbEntity>()
  83 + : await _dbContext.SqlSugarClient.Queryable<MenuDbEntity>()
  84 + .Where(x => x.IsDeleted == false && menuIds.Contains(x.Id))
  85 + .ToListAsync();
  86 + }
  87 + }
  88 +
  89 + var menuNodes = menus
  90 + .Select(MapToNode)
  91 + .OrderByDescending(x => x.OrderNum)
  92 + .ThenBy(x => x.MenuName)
  93 + .ToList();
  94 +
  95 + // 注意:查询 RoleAggregateRoot 会触发 YiRbacDbContext 的 IDataPermission 过滤,
  96 + // 其表达式包含 roleInfo.Select(...).Contains(...),在当前 SqlSugar 版本下会报“不支持 Select”。
  97 + // 这里直接使用 JWT 中的角色码(CurrentUser.Roles)返回,避免触发过滤器。
  98 + var roleCodes = CurrentUser.Roles?.ToList() ?? new List<string>();
  99 +
  100 + var permissionCodes = menuNodes
  101 + .Where(x => !string.IsNullOrWhiteSpace(x.PermissionCode))
  102 + .Select(x => x.PermissionCode!.Trim())
  103 + .Distinct()
  104 + .OrderBy(x => x)
  105 + .ToList();
  106 +
  107 + return new CurrentUserMenuPermissionsOutputDto
  108 + {
  109 + User = new CurrentUserBriefDto
  110 + {
  111 + Id = user.Id,
  112 + UserName = user.UserName,
  113 + Nick = user.Nick,
  114 + Email = user.Email,
  115 + Icon = user.Icon
  116 + },
  117 + RoleCodes = roleCodes.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().OrderBy(x => x).ToList(),
  118 + PermissionCodes = permissionCodes,
  119 + Menus = BuildMenuTree(menuNodes)
  120 + };
  121 + }
  122 +
  123 + /// <inheritdoc />
  124 + [HttpPost]
  125 + public virtual async Task<bool> LogoutAsync()
  126 + {
  127 + if (!CurrentUser.Id.HasValue)
  128 + {
  129 + return false;
  130 + }
  131 +
  132 + await _userCache.RemoveAsync(new UserInfoCacheKey(CurrentUser.Id.Value));
  133 + return true;
  134 + }
  135 +
  136 + private static List<CurrentUserMenuNodeDto> BuildMenuTree(List<CurrentUserMenuNodeDto> flat)
  137 + {
  138 + var nodes = flat
  139 + .GroupBy(x => x.Id)
  140 + .Select(g => g.First())
  141 + .ToList();
  142 + var byId = nodes.ToDictionary(n => n.Id, n => n);
  143 +
  144 + foreach (var n in nodes)
  145 + {
  146 + n.Children = new List<CurrentUserMenuNodeDto>();
  147 + }
  148 +
  149 + var roots = new List<CurrentUserMenuNodeDto>();
  150 + foreach (var n in nodes)
  151 + {
  152 + var pid = string.IsNullOrWhiteSpace(n.ParentId) ? "0" : n.ParentId.Trim();
  153 + if (pid == "0" || pid == "00000000-0000-0000-0000-000000000000")
  154 + {
  155 + roots.Add(n);
  156 + continue;
  157 + }
  158 +
  159 + if (byId.TryGetValue(pid, out var parent))
  160 + {
  161 + parent.Children.Add(n);
  162 + }
  163 + else
  164 + {
  165 + roots.Add(n);
  166 + }
  167 + }
  168 +
  169 + SortMenuTree(roots);
  170 + return roots;
  171 + }
  172 +
  173 + private static CurrentUserMenuNodeDto MapToNode(MenuDbEntity m)
  174 + {
  175 + return new CurrentUserMenuNodeDto
  176 + {
  177 + Id = m.Id,
  178 + ParentId = string.IsNullOrWhiteSpace(m.ParentId) ? "0" : m.ParentId.Trim(),
  179 + MenuName = m.MenuName ?? string.Empty,
  180 + RouterName = m.RouterName,
  181 + Router = m.Router,
  182 + PermissionCode = m.PermissionCode,
  183 + MenuType = m.MenuType,
  184 + MenuSource = m.MenuSource,
  185 + OrderNum = m.OrderNum,
  186 + State = m.State,
  187 + MenuIcon = m.MenuIcon,
  188 + Component = m.Component,
  189 + IsLink = m.IsLink,
  190 + IsCache = m.IsCache,
  191 + IsShow = m.IsShow,
  192 + Query = m.Query,
  193 + Remark = m.Remark
  194 + };
  195 + }
  196 +
  197 + private static void SortMenuTree(List<CurrentUserMenuNodeDto> level)
  198 + {
  199 + level.Sort((a, b) =>
  200 + {
  201 + var o = b.OrderNum.CompareTo(a.OrderNum);
  202 + return o != 0 ? o : string.Compare(a.MenuName, b.MenuName, StringComparison.Ordinal);
  203 + });
  204 +
  205 + foreach (var n in level)
  206 + {
  207 + if (n.Children.Count > 0)
  208 + {
  209 + SortMenuTree(n.Children);
  210 + }
  211 + }
  212 + }
  213 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryDbEntity.cs
... ... @@ -24,10 +24,28 @@ public class FlLabelCategoryDbEntity
24 24  
25 25 public string CategoryName { get; set; } = string.Empty;
26 26  
  27 + /// <summary>
  28 + /// 按钮展示文案(为空则默认使用 CategoryName)
  29 + /// </summary>
  30 + public string? DisplayText { get; set; }
  31 +
  32 + /// <summary>
  33 + /// 分类图/展示值:TEXT 可为图或空;COLOR 存色值(如 #409EFF);IMAGE 存图片 URL(与 ButtonAppearance 配合)
  34 + /// </summary>
27 35 public string? CategoryPhotoUrl { get; set; }
28 36  
29 37 public int OrderNum { get; set; }
30 38  
31 39 public bool State { get; set; }
  40 +
  41 + /// <summary>
  42 + /// 按钮外观:TEXT / COLOR / IMAGE(展示数据见 CategoryPhotoUrl)
  43 + /// </summary>
  44 + public string ButtonAppearance { get; set; } = "TEXT";
  45 +
  46 + /// <summary>
  47 + /// 门店可用范围:ALL / SPECIFIED
  48 + /// </summary>
  49 + public string AvailabilityType { get; set; } = "ALL";
32 50 }
33 51  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryLocationDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +/// <summary>
  6 +/// 标签分类可用门店关联(对应表:fl_label_category_location)
  7 +/// </summary>
  8 +[SugarTable("fl_label_category_location")]
  9 +public class FlLabelCategoryLocationDbEntity
  10 +{
  11 + [SugarColumn(IsPrimaryKey = true)]
  12 + public string Id { get; set; } = string.Empty;
  13 +
  14 + public string CategoryId { get; set; } = string.Empty;
  15 +
  16 + public string LocationId { get; set; } = string.Empty;
  17 +
  18 + public DateTime CreationTime { get; set; }
  19 +
  20 + public string? CreatorId { get; set; }
  21 +}
  22 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelDbEntity.cs
... ... @@ -20,7 +20,7 @@ public class FlLabelDbEntity
20 20  
21 21 public string ConcurrencyStamp { get; set; } = string.Empty;
22 22  
23   - public string LabelCode { get; set; } = string.Empty;
  23 + public string? LabelCode { get; set; }
24 24  
25 25 public string LabelName { get; set; } = string.Empty;
26 26  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryDbEntity.cs
... ... @@ -24,10 +24,28 @@ public class FlProductCategoryDbEntity
24 24  
25 25 public string CategoryName { get; set; } = string.Empty;
26 26  
  27 + /// <summary>
  28 + /// 按钮展示文案(为空则默认使用 CategoryName)
  29 + /// </summary>
  30 + public string? DisplayText { get; set; }
  31 +
  32 + /// <summary>
  33 + /// 分类图/展示值:TEXT 可为图或空;COLOR 存色值;IMAGE 存图片 URL(与 ButtonAppearance 配合)
  34 + /// </summary>
27 35 public string? CategoryPhotoUrl { get; set; }
28 36  
  37 + /// <summary>
  38 + /// 按钮外观:TEXT / COLOR / IMAGE(展示数据见 CategoryPhotoUrl)
  39 + /// </summary>
  40 + public string ButtonAppearance { get; set; } = "TEXT";
  41 +
29 42 public bool State { get; set; }
30 43  
  44 + /// <summary>
  45 + /// 门店可用范围:ALL / SPECIFIED
  46 + /// </summary>
  47 + public string AvailabilityType { get; set; } = "ALL";
  48 +
31 49 public int OrderNum { get; set; }
32 50 }
33 51  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryLocationDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +/// <summary>
  6 +/// 产品类别可用门店关联(对应表:fl_product_category_location)
  7 +/// </summary>
  8 +[SugarTable("fl_product_category_location")]
  9 +public class FlProductCategoryLocationDbEntity
  10 +{
  11 + [SugarColumn(IsPrimaryKey = true)]
  12 + public string Id { get; set; } = string.Empty;
  13 +
  14 + public string CategoryId { get; set; } = string.Empty;
  15 +
  16 + public string LocationId { get; set; } = string.Empty;
  17 +
  18 + public DateTime CreationTime { get; set; }
  19 +
  20 + public string? CreatorId { get; set; }
  21 +}
  22 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs
... ... @@ -261,7 +261,7 @@ public class LabelAppService : ApplicationService, ILabelAppService
261 261  
262 262 return new LabelGetOutputDto
263 263 {
264   - Id = label.LabelCode,
  264 + Id = label.LabelCode ?? string.Empty,
265 265 LabelName = label.LabelName,
266 266 LocationId = label.LocationId ?? string.Empty,
267 267 LocationName = location?.LocationName ?? location?.LocationCode ?? "无",
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelCategoryAppService.cs
... ... @@ -30,12 +30,34 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
30 30 var query = _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
31 31 .Where(x => !x.IsDeleted)
32 32 .WhereIF(!string.IsNullOrWhiteSpace(keyword),
33   - x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!))
  33 + x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!) ||
  34 + (x.DisplayText != null && x.DisplayText.Contains(keyword!)))
34 35 .WhereIF(input.State != null, x => x.State == input.State);
35 36  
  37 + // Sorting 仅允许白名单字段,避免 Unknown column/注入风险
36 38 if (!string.IsNullOrWhiteSpace(input.Sorting))
37 39 {
38   - query = query.OrderBy(input.Sorting);
  40 + var sorting = input.Sorting.Trim();
  41 + if (sorting.Equals("OrderNum desc", StringComparison.OrdinalIgnoreCase))
  42 + {
  43 + query = query.OrderByDescending(x => x.OrderNum);
  44 + }
  45 + else if (sorting.Equals("OrderNum asc", StringComparison.OrdinalIgnoreCase))
  46 + {
  47 + query = query.OrderBy(x => x.OrderNum);
  48 + }
  49 + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase))
  50 + {
  51 + query = query.OrderByDescending(x => x.CreationTime);
  52 + }
  53 + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase))
  54 + {
  55 + query = query.OrderBy(x => x.CreationTime);
  56 + }
  57 + else
  58 + {
  59 + query = query.OrderByDescending(x => x.OrderNum).OrderByDescending(x => x.CreationTime);
  60 + }
39 61 }
40 62 else
41 63 {
... ... @@ -58,8 +80,11 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
58 80 Id = x.Id,
59 81 CategoryCode = x.CategoryCode,
60 82 CategoryName = x.CategoryName,
  83 + DisplayText = x.DisplayText,
61 84 CategoryPhotoUrl = x.CategoryPhotoUrl,
62 85 State = x.State,
  86 + ButtonAppearance = x.ButtonAppearance,
  87 + AvailabilityType = x.AvailabilityType,
63 88 OrderNum = x.OrderNum,
64 89 NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0,
65 90 LastEdited = x.LastModificationTime ?? x.CreationTime
... ... @@ -77,7 +102,17 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
77 102 throw new UserFriendlyException("标签分类不存在");
78 103 }
79 104  
80   - return MapToGetOutput(entity);
  105 + var dto = MapToGetOutput(entity);
  106 + if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))
  107 + {
  108 + var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryLocationDbEntity>()
  109 + .Where(x => x.CategoryId == entity.Id)
  110 + .Select(x => x.LocationId)
  111 + .ToListAsync();
  112 + dto.LocationIds = locationIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList() ?? new();
  113 + }
  114 +
  115 + return dto;
81 116 }
82 117  
83 118 public async Task<LabelCategoryGetOutputDto> CreateAsync(LabelCategoryCreateInputVo input)
... ... @@ -89,6 +124,13 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
89 124 throw new UserFriendlyException("分类编码和名称不能为空");
90 125 }
91 126  
  127 + var displayText = input.DisplayText?.Trim();
  128 + var appearance = (input.ButtonAppearance ?? "TEXT").Trim().ToUpperInvariant();
  129 + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
  130 + ValidateButtonAppearance(appearance);
  131 + var locationIds = NormalizeLocationIds(input.LocationIds);
  132 + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
  133 +
92 134 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
93 135 .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name));
94 136 if (duplicated)
... ... @@ -96,17 +138,23 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
96 138 throw new UserFriendlyException("分类编码或名称已存在");
97 139 }
98 140  
  141 + var now = DateTime.Now;
  142 + var currentUserId = CurrentUser?.Id?.ToString();
99 143 var entity = new FlLabelCategoryDbEntity
100 144 {
101 145 Id = _guidGenerator.Create().ToString(),
102 146 CategoryCode = code,
103 147 CategoryName = name,
  148 + DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText,
104 149 CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(),
105 150 State = input.State,
  151 + ButtonAppearance = appearance,
  152 + AvailabilityType = availabilityType,
106 153 OrderNum = input.OrderNum
107 154 };
108 155  
109 156 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  157 + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, currentUserId, now);
110 158 return await GetAsync(entity.Id);
111 159 }
112 160  
... ... @@ -126,6 +174,13 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
126 174 throw new UserFriendlyException("分类编码和名称不能为空");
127 175 }
128 176  
  177 + var displayText = input.DisplayText?.Trim();
  178 + var appearance = (input.ButtonAppearance ?? "TEXT").Trim().ToUpperInvariant();
  179 + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
  180 + ValidateButtonAppearance(appearance);
  181 + var locationIds = NormalizeLocationIds(input.LocationIds);
  182 + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
  183 +
129 184 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
130 185 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name));
131 186 if (duplicated)
... ... @@ -135,13 +190,17 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
135 190  
136 191 entity.CategoryCode = code;
137 192 entity.CategoryName = name;
  193 + entity.DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText;
138 194 entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim();
139 195 entity.State = input.State;
  196 + entity.ButtonAppearance = appearance;
  197 + entity.AvailabilityType = availabilityType;
140 198 entity.OrderNum = input.OrderNum;
141 199 entity.LastModificationTime = DateTime.Now;
142 200 entity.LastModifierId = CurrentUser?.Id?.ToString();
143 201  
144 202 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  203 + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now);
145 204 return await GetAsync(id);
146 205 }
147 206  
... ... @@ -174,12 +233,78 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
174 233 Id = x.Id,
175 234 CategoryCode = x.CategoryCode,
176 235 CategoryName = x.CategoryName,
  236 + DisplayText = x.DisplayText,
177 237 CategoryPhotoUrl = x.CategoryPhotoUrl,
178 238 State = x.State,
  239 + ButtonAppearance = x.ButtonAppearance,
  240 + AvailabilityType = x.AvailabilityType,
179 241 OrderNum = x.OrderNum
180 242 };
181 243 }
182 244  
  245 + private static void ValidateAvailabilityTypeAndLocations(string availabilityType, List<string> locationIds)
  246 + {
  247 + if (availabilityType != "ALL" && availabilityType != "SPECIFIED")
  248 + {
  249 + throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)");
  250 + }
  251 +
  252 + if (availabilityType == "SPECIFIED" && locationIds.Count == 0)
  253 + {
  254 + throw new UserFriendlyException("指定门店范围时必须至少选择一个门店");
  255 + }
  256 + }
  257 +
  258 + private static List<string> NormalizeLocationIds(List<string>? locationIds)
  259 + {
  260 + return locationIds?
  261 + .Where(x => !string.IsNullOrWhiteSpace(x))
  262 + .Select(x => x.Trim())
  263 + .Distinct()
  264 + .ToList() ?? new();
  265 + }
  266 +
  267 + private static void ValidateButtonAppearance(string appearance)
  268 + {
  269 + if (appearance != "TEXT" && appearance != "COLOR" && appearance != "IMAGE")
  270 + {
  271 + throw new UserFriendlyException("按钮外观不合法(TEXT/COLOR/IMAGE)");
  272 + }
  273 + }
  274 +
  275 + private async Task SaveCategoryLocationsAsync(
  276 + string categoryId,
  277 + string availabilityType,
  278 + List<string> locationIds,
  279 + string? currentUserId,
  280 + DateTime now)
  281 + {
  282 + await _dbContext.SqlSugarClient.Deleteable<FlLabelCategoryLocationDbEntity>()
  283 + .Where(x => x.CategoryId == categoryId)
  284 + .ExecuteCommandAsync();
  285 +
  286 + if (availabilityType != "SPECIFIED")
  287 + {
  288 + return;
  289 + }
  290 +
  291 + if (locationIds.Count == 0)
  292 + {
  293 + return;
  294 + }
  295 +
  296 + var rows = locationIds.Select(locId => new FlLabelCategoryLocationDbEntity
  297 + {
  298 + Id = _guidGenerator.Create().ToString(),
  299 + CategoryId = categoryId,
  300 + LocationId = locId,
  301 + CreationTime = now,
  302 + CreatorId = currentUserId
  303 + }).ToList();
  304 +
  305 + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
  306 + }
  307 +
183 308 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items)
184 309 {
185 310 var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount;
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs
... ... @@ -35,7 +35,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
35 35 var query = _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
36 36 .Where(x => !x.IsDeleted)
37 37 .WhereIF(!string.IsNullOrWhiteSpace(keyword),
38   - x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!))
  38 + x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!) ||
  39 + (x.DisplayText != null && x.DisplayText.Contains(keyword!)))
39 40 .WhereIF(input.State != null, x => x.State == input.State);
40 41  
41 42 // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column
... ... @@ -77,8 +78,11 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
77 78 Id = x.Id,
78 79 CategoryCode = x.CategoryCode,
79 80 CategoryName = x.CategoryName,
  81 + DisplayText = x.DisplayText,
80 82 CategoryPhotoUrl = x.CategoryPhotoUrl,
  83 + ButtonAppearance = x.ButtonAppearance,
81 84 State = x.State,
  85 + AvailabilityType = x.AvailabilityType,
82 86 OrderNum = x.OrderNum,
83 87 LastEdited = x.LastModificationTime ?? x.CreationTime
84 88 }).ToList();
... ... @@ -98,7 +102,17 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
98 102 throw new UserFriendlyException("类别不存在");
99 103 }
100 104  
101   - return MapToGetOutput(entity);
  105 + var dto = MapToGetOutput(entity);
  106 + if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))
  107 + {
  108 + var locationIds = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryLocationDbEntity>()
  109 + .Where(x => x.CategoryId == entity.Id)
  110 + .Select(x => x.LocationId)
  111 + .ToListAsync();
  112 + dto.LocationIds = locationIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList() ?? new();
  113 + }
  114 +
  115 + return dto;
102 116 }
103 117  
104 118 /// <summary>
... ... @@ -113,6 +127,13 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
113 127 throw new UserFriendlyException("类别编码和名称不能为空");
114 128 }
115 129  
  130 + var displayText = input.DisplayText?.Trim();
  131 + var appearance = (input.ButtonAppearance ?? "TEXT").Trim().ToUpperInvariant();
  132 + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
  133 + ValidateButtonAppearance(appearance);
  134 + var locationIds = NormalizeLocationIds(input.LocationIds);
  135 + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
  136 +
116 137 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
117 138 .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name));
118 139 if (duplicated)
... ... @@ -133,12 +154,16 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
133 154 ConcurrencyStamp = _guidGenerator.Create().ToString("N"),
134 155 CategoryCode = code,
135 156 CategoryName = name,
  157 + DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText,
136 158 CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(),
  159 + ButtonAppearance = appearance,
137 160 State = input.State,
  161 + AvailabilityType = availabilityType,
138 162 OrderNum = input.OrderNum
139 163 };
140 164  
141 165 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  166 + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, currentUserId, now);
142 167 return await GetAsync(entity.Id);
143 168 }
144 169  
... ... @@ -161,6 +186,13 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
161 186 throw new UserFriendlyException("类别编码和名称不能为空");
162 187 }
163 188  
  189 + var displayText = input.DisplayText?.Trim();
  190 + var appearance = (input.ButtonAppearance ?? "TEXT").Trim().ToUpperInvariant();
  191 + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
  192 + ValidateButtonAppearance(appearance);
  193 + var locationIds = NormalizeLocationIds(input.LocationIds);
  194 + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
  195 +
164 196 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
165 197 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name));
166 198 if (duplicated)
... ... @@ -170,13 +202,17 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
170 202  
171 203 entity.CategoryCode = code;
172 204 entity.CategoryName = name;
  205 + entity.DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText;
173 206 entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim();
  207 + entity.ButtonAppearance = appearance;
174 208 entity.State = input.State;
  209 + entity.AvailabilityType = availabilityType;
175 210 entity.OrderNum = input.OrderNum;
176 211 entity.LastModificationTime = DateTime.Now;
177 212 entity.LastModifierId = CurrentUser?.Id?.ToString();
178 213  
179 214 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  215 + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now);
180 216 return await GetAsync(id);
181 217 }
182 218  
... ... @@ -213,12 +249,78 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
213 249 Id = x.Id,
214 250 CategoryCode = x.CategoryCode,
215 251 CategoryName = x.CategoryName,
  252 + DisplayText = x.DisplayText,
216 253 CategoryPhotoUrl = x.CategoryPhotoUrl,
  254 + ButtonAppearance = x.ButtonAppearance,
217 255 State = x.State,
  256 + AvailabilityType = x.AvailabilityType,
218 257 OrderNum = x.OrderNum
219 258 };
220 259 }
221 260  
  261 + private static void ValidateAvailabilityTypeAndLocations(string availabilityType, List<string> locationIds)
  262 + {
  263 + if (availabilityType != "ALL" && availabilityType != "SPECIFIED")
  264 + {
  265 + throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)");
  266 + }
  267 +
  268 + if (availabilityType == "SPECIFIED" && locationIds.Count == 0)
  269 + {
  270 + throw new UserFriendlyException("指定门店范围时必须至少选择一个门店");
  271 + }
  272 + }
  273 +
  274 + private static List<string> NormalizeLocationIds(List<string>? locationIds)
  275 + {
  276 + return locationIds?
  277 + .Where(x => !string.IsNullOrWhiteSpace(x))
  278 + .Select(x => x.Trim())
  279 + .Distinct()
  280 + .ToList() ?? new();
  281 + }
  282 +
  283 + private static void ValidateButtonAppearance(string appearance)
  284 + {
  285 + if (appearance != "TEXT" && appearance != "COLOR" && appearance != "IMAGE")
  286 + {
  287 + throw new UserFriendlyException("按钮外观不合法(TEXT/COLOR/IMAGE)");
  288 + }
  289 + }
  290 +
  291 + private async Task SaveCategoryLocationsAsync(
  292 + string categoryId,
  293 + string availabilityType,
  294 + List<string> locationIds,
  295 + string? currentUserId,
  296 + DateTime now)
  297 + {
  298 + await _dbContext.SqlSugarClient.Deleteable<FlProductCategoryLocationDbEntity>()
  299 + .Where(x => x.CategoryId == categoryId)
  300 + .ExecuteCommandAsync();
  301 +
  302 + if (availabilityType != "SPECIFIED")
  303 + {
  304 + return;
  305 + }
  306 +
  307 + if (locationIds.Count == 0)
  308 + {
  309 + return;
  310 + }
  311 +
  312 + var rows = locationIds.Select(locId => new FlProductCategoryLocationDbEntity
  313 + {
  314 + Id = _guidGenerator.Create().ToString(),
  315 + CategoryId = categoryId,
  316 + LocationId = locId,
  317 + CreationTime = now,
  318 + CreatorId = currentUserId
  319 + }).ToList();
  320 +
  321 + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
  322 + }
  323 +
222 324 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items)
223 325 {
224 326 var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount;
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacMenuAppService.cs
... ... @@ -38,6 +38,8 @@ public class RbacMenuAppService : ApplicationService, IRbacMenuAppService
38 38 Id = x.Id,
39 39 ParentId = x.ParentId,
40 40 MenuName = x.MenuName ?? string.Empty,
  41 + RouterName = x.RouterName,
  42 + Router = x.Router,
41 43 PermissionCode = x.PermissionCode,
42 44 MenuType = x.MenuType,
43 45 MenuSource = x.MenuSource,
... ... @@ -62,6 +64,8 @@ public class RbacMenuAppService : ApplicationService, IRbacMenuAppService
62 64 Id = entity.Id,
63 65 ParentId = entity.ParentId,
64 66 MenuName = entity.MenuName ?? string.Empty,
  67 + RouterName = entity.RouterName,
  68 + Router = entity.Router,
65 69 PermissionCode = entity.PermissionCode,
66 70 MenuType = entity.MenuType,
67 71 MenuSource = entity.MenuSource,
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs
... ... @@ -50,8 +50,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
50 50 /// 获取当前门店下四级嵌套数据
51 51 /// </summary>
52 52 /// <remarks>
53   - /// L1 标签分类 fl_label_category;L2 产品分类 fl_product.CategoryId join fl_product_category;
  53 + /// L1 标签分类 fl_label_category(含 buttonAppearance;COLOR/IMAGE 展示值在 categoryPhotoUrl);仅对当前门店可用:ALL 或 SPECIFIED 且在 fl_label_category_location;
  54 + /// L2 产品分类 fl_product.CategoryId join fl_product_category(同上,展示值在 categoryPhotoUrl);
54 55 /// L3 产品;L4 与该门店、该标签分类、该产品关联的标签实例(fl_label + fl_label_type)。
  56 + /// L2 仅包含对当前门店可用的类别:AvailabilityType=ALL,或 SPECIFIED 且在 fl_product_category_location 存在该门店记录;
  57 + /// 未归类或分类行未关联到 fl_product_category 时仍归入「无」节点。
55 58 /// </remarks>
56 59 [Authorize]
57 60 public virtual async Task<List<UsAppLabelCategoryTreeNodeDto>> GetLabelingTreeAsync(UsAppLabelingTreeInputVo input)
... ... @@ -83,10 +86,15 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
83 86 LabelCategoryId = c.Id,
84 87 LabelCategoryName = c.CategoryName,
85 88 LabelCategoryPhotoUrl = c.CategoryPhotoUrl,
  89 + LabelCategoryButtonAppearance = c.ButtonAppearance,
86 90 LabelCategoryOrderNum = c.OrderNum,
87 91 ProductCategoryId = p.CategoryId,
88 92 ProductCategoryName = pc.CategoryName,
89 93 ProductCategoryPhotoUrl = pc.CategoryPhotoUrl,
  94 + ProductCategoryDisplayText = pc.DisplayText,
  95 + ProductCategoryButtonAppearance = pc.ButtonAppearance,
  96 + ProductCategoryAvailabilityType = pc.AvailabilityType,
  97 + ProductCategoryOrderNum = pc.OrderNum,
90 98 ProductId = p.Id,
91 99 ProductName = p.ProductName,
92 100 ProductCode = p.ProductCode,
... ... @@ -112,17 +120,22 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
112 120 x.LabelCategoryId,
113 121 x.LabelCategoryName,
114 122 x.LabelCategoryPhotoUrl,
  123 + x.LabelCategoryButtonAppearance,
115 124 x.LabelCategoryOrderNum
116 125 }).OrderBy(g => g.Key.LabelCategoryOrderNum).ThenBy(g => g.Key.LabelCategoryName);
117 126  
118 127 var result = new List<UsAppLabelCategoryTreeNodeDto>();
119 128 foreach (var g1 in byL1)
120 129 {
  130 + var l1Appearance = string.IsNullOrWhiteSpace(g1.Key.LabelCategoryButtonAppearance)
  131 + ? "TEXT"
  132 + : g1.Key.LabelCategoryButtonAppearance.Trim().ToUpperInvariant();
121 133 var l1 = new UsAppLabelCategoryTreeNodeDto
122 134 {
123 135 Id = g1.Key.LabelCategoryId,
124 136 CategoryName = g1.Key.LabelCategoryName ?? string.Empty,
125 137 CategoryPhotoUrl = g1.Key.LabelCategoryPhotoUrl,
  138 + ButtonAppearance = l1Appearance,
126 139 OrderNum = g1.Key.LabelCategoryOrderNum,
127 140 ProductCategories = new List<UsAppProductCategoryNodeDto>()
128 141 };
... ... @@ -136,7 +149,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
136 149 {
137 150 CategoryId = (string?)null,
138 151 CategoryName = "无",
139   - CategoryPhotoUrl = (string?)null
  152 + CategoryPhotoUrl = (string?)null,
  153 + DisplayText = (string?)null,
  154 + ButtonAppearance = (string?)null,
  155 + AvailabilityType = (string?)null,
  156 + CategoryOrderNum = int.MaxValue
140 157 };
141 158 }
142 159  
... ... @@ -146,19 +163,34 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
146 163 {
147 164 CategoryId = (string?)categoryId,
148 165 CategoryName = categoryName,
149   - CategoryPhotoUrl = categoryPhotoUrl
  166 + CategoryPhotoUrl = categoryPhotoUrl,
  167 + DisplayText = NormalizeNullableUrl(x.ProductCategoryDisplayText),
  168 + ButtonAppearance = NormalizeNullableId(x.ProductCategoryButtonAppearance),
  169 + AvailabilityType = NormalizeNullableId(x.ProductCategoryAvailabilityType),
  170 + CategoryOrderNum = x.ProductCategoryOrderNum
150 171 };
151 172 })
152   - .OrderBy(g => g.Key.CategoryName);
  173 + .OrderBy(g => g.Key.CategoryOrderNum)
  174 + .ThenBy(g => g.Key.CategoryName);
153 175  
154 176 foreach (var g2 in byL2)
155 177 {
156 178 var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName);
  179 + var appearance = string.IsNullOrWhiteSpace(g2.Key.ButtonAppearance)
  180 + ? "TEXT"
  181 + : g2.Key.ButtonAppearance.Trim().ToUpperInvariant();
  182 + var availability = string.IsNullOrWhiteSpace(g2.Key.AvailabilityType)
  183 + ? "ALL"
  184 + : g2.Key.AvailabilityType.Trim().ToUpperInvariant();
157 185 var l2 = new UsAppProductCategoryNodeDto
158 186 {
159 187 CategoryId = g2.Key.CategoryId,
160 188 CategoryPhotoUrl = g2.Key.CategoryPhotoUrl,
161 189 Name = g2.Key.CategoryName,
  190 + DisplayText = g2.Key.DisplayText,
  191 + ButtonAppearance = appearance,
  192 + AvailabilityType = availability,
  193 + OrderNum = g2.Key.CategoryOrderNum == int.MaxValue ? 0 : g2.Key.CategoryOrderNum,
162 194 ItemCount = productsGrouped.Count(),
163 195 Products = new List<UsAppLabelingProductNodeDto>()
164 196 };
... ... @@ -424,7 +456,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
424 456 }
425 457  
426 458 var previewProductId = await ResolvePreviewProductIdAsync(labelRow.Id, input.ProductId);
427   - var normalizedPrintInput = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value);
  459 + var normalizedPrintInput = ParsePrintInputJsonToDictionary(input.PrintInputJson);
428 460  
429 461 // 解析模板 elements(与预览一致的渲染数据)
430 462 var resolvedTemplate = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo
... ... @@ -852,14 +884,29 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
852 884 .Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId)
853 885 .Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State)
854 886 .Where((lp, l, p, c, t, tpl, pc) => !p.IsDeleted && p.State)
855   - .Where((lp, l, p, c, t, tpl, pc) => !c.IsDeleted && c.State)
  887 + .Where((lp, l, p, c, t, tpl, pc) =>
  888 + !c.IsDeleted && c.State &&
  889 + (c.AvailabilityType == "ALL" ||
  890 + (c.AvailabilityType == "SPECIFIED" &&
  891 + SqlFunc.Subqueryable<FlLabelCategoryLocationDbEntity>()
  892 + .Where(loc => loc.CategoryId == c.Id && loc.LocationId == locationId)
  893 + .Any())))
856 894 .Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State)
857 895 .Where((lp, l, p, c, t, tpl, pc) => !tpl.IsDeleted)
  896 + .Where((lp, l, p, c, t, tpl, pc) =>
  897 + pc.Id == null ||
  898 + (!pc.IsDeleted && pc.State &&
  899 + (pc.AvailabilityType == "ALL" ||
  900 + (pc.AvailabilityType == "SPECIFIED" &&
  901 + SqlFunc.Subqueryable<FlProductCategoryLocationDbEntity>()
  902 + .Where(loc => loc.CategoryId == pc.Id && loc.LocationId == locationId)
  903 + .Any()))))
858 904 .WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId)
859 905 .WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) =>
860 906 (l.LabelName != null && l.LabelName.Contains(keyword!)) ||
861 907 (p.ProductName != null && p.ProductName.Contains(keyword!)) ||
862 908 (pc.CategoryName != null && pc.CategoryName.Contains(keyword!)) ||
  909 + (pc.DisplayText != null && pc.DisplayText.Contains(keyword!)) ||
863 910 (c.CategoryName != null && c.CategoryName.Contains(keyword!)) ||
864 911 (t.TypeName != null && t.TypeName.Contains(keyword!)) ||
865 912 (l.LabelCode != null && l.LabelCode.Contains(keyword!)));
... ... @@ -875,6 +922,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
875 922  
876 923 public string? LabelCategoryPhotoUrl { get; set; }
877 924  
  925 + public string? LabelCategoryButtonAppearance { get; set; }
  926 +
878 927 public int LabelCategoryOrderNum { get; set; }
879 928  
880 929 public string? ProductCategoryId { get; set; }
... ... @@ -883,6 +932,14 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
883 932  
884 933 public string? ProductCategoryPhotoUrl { get; set; }
885 934  
  935 + public string? ProductCategoryDisplayText { get; set; }
  936 +
  937 + public string? ProductCategoryButtonAppearance { get; set; }
  938 +
  939 + public string? ProductCategoryAvailabilityType { get; set; }
  940 +
  941 + public int ProductCategoryOrderNum { get; set; }
  942 +
886 943 public string ProductId { get; set; } = string.Empty;
887 944  
888 945 public string? ProductName { get; set; }
... ... @@ -908,6 +965,32 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
908 965 public string TemplateUnit { get; set; } = "inch";
909 966 }
910 967  
  968 + /// <summary>
  969 + /// 将 App 入参中的 JsonElement(对象或 null)反序列化为 PreviewAsync 所需的扁平字典。
  970 + /// </summary>
  971 + private static Dictionary<string, object?>? ParsePrintInputJsonToDictionary(JsonElement? printInputJson)
  972 + {
  973 + if (printInputJson is null)
  974 + {
  975 + return null;
  976 + }
  977 +
  978 + var je = printInputJson.Value;
  979 + if (je.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
  980 + {
  981 + return null;
  982 + }
  983 +
  984 + try
  985 + {
  986 + return JsonSerializer.Deserialize<Dictionary<string, object?>>(je.GetRawText());
  987 + }
  988 + catch
  989 + {
  990 + return null;
  991 + }
  992 + }
  993 +
911 994 private static string NormalizeCategoryName(string? categoryName)
912 995 {
913 996 var s = categoryName?.Trim();
... ...
项目相关文档/本次新增与优化接口汇总.md 0 → 100644
  1 +# 本次接口变更汇总(标签 / 产品类别 / 模板组件 / App 树 / Web 会话)
  2 +
  3 +> 说明:本文只汇总本次迭代中相关内容:
  4 +> - 标签模块 Label Categories:对齐“新增类别”原型图(`buttonAppearance` + `categoryPhotoUrl` / 展示文案 / 门店范围)
  5 +> - 产品模块 Categories:对齐“新增产品类别”原型图(同上)
  6 +> - 模板组件(`fl_label_template_element`):新增字段 `TypeAdd`(并补齐 `ElementName`)
  7 +> - App `labeling-tree`:L1 标签分类返回 `buttonAppearance`
  8 +> - Web 管理端 `auth-session`:**当前用户菜单与权限**、**退出登录**(食品标签-美国版模块)
  9 +> - Web `rbac-menu` 列表/详情:补充返回 `routerName`、`router`
  10 +>
  11 +> 其余标签打印相关接口不在本文范围内。
  12 +
  13 +---
  14 +
  15 +## 1. 模板组件字段(`fl_label_template_element`)
  16 +
  17 +### 1.1 字段变更
  18 +
  19 +- 新增字段:`TypeAdd`(元素附加类型,如 `label_Duration`)
  20 +- 新增字段:`ElementName`(元素名称,用于更稳定的显示/快照)
  21 +
  22 +### 1.2 接口影响范围
  23 +
  24 +- 模板新增/编辑:保存 `elements[].typeAdd` / `elements[].elementName`
  25 +- 模板详情/预览:返回 `elements[].typeAdd` / `elements[].elementName`
  26 +
  27 +### 1.3 JSON 对齐(elements[])
  28 +
  29 +| 前端字段 | 后端字段 | 说明 |
  30 +|---|---|---|
  31 +| `type` | `ElementType` | 元素类型 |
  32 +| `typeAdd` | `TypeAdd` | 元素附加类型 |
  33 +| `elementName` | `ElementName` | 元素名称 |
  34 +
  35 +---
  36 +
  37 +## 2. 产品模块 Categories(Products → Categories)
  38 +
  39 +> 数据库侧你已完成:`fl_product_category` 新字段、`fl_product_category_location` 新表。
  40 +
  41 +### 2.1 表结构要点
  42 +
  43 +- `fl_product_category`(主表)关键字段:
  44 + - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`)
  45 + - `ButtonAppearance`:`TEXT/COLOR/IMAGE`(前端按类型解析)
  46 + - `CategoryPhotoUrl`:与 `ButtonAppearance` 配合——`COLOR` 存颜色值、`IMAGE` 存图片 URL;`TEXT` 可空或不用
  47 + - **已删除列**(需在库上执行 `DROP COLUMN`):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson`
  48 + - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围)
  49 +- `fl_product_category_location`(关联表):
  50 + - `(CategoryId, LocationId)` 唯一约束;用于 `AvailabilityType=SPECIFIED` 指定门店
  51 +
  52 +### 2.2 CRUD 接口(字段扩展)
  53 +
  54 +接口路径不变,仅扩展字段。
  55 +
  56 +#### 2.2.1 列表
  57 +
  58 +- **方法**:`GET`
  59 +- **路径**:`/api/app/product-category`
  60 +- **列表行新增返回**:
  61 + - `displayText`
  62 + - `buttonAppearance`
  63 + - `categoryPhotoUrl`
  64 + - `availabilityType`
  65 +
  66 +#### 2.2.2 详情
  67 +
  68 +- **方法**:`GET`
  69 +- **路径**:`/api/app/product-category/{id}`
  70 +- **新增返回字段**:
  71 + - `displayText`
  72 + - `buttonAppearance`、`categoryPhotoUrl`(COLOR/IMAGE 的展示数据统一在此字段)
  73 + - `availabilityType`
  74 + - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组)
  75 +
  76 +#### 2.2.3 新增
  77 +
  78 +- **方法**:`POST`
  79 +- **路径**:`/api/app/product-category`
  80 +- **新增入参字段**:
  81 + - `displayText`
  82 + - `buttonAppearance`、`categoryPhotoUrl`
  83 + - `availabilityType`
  84 + - `locationIds`(当 `availabilityType=SPECIFIED` 必填且至少 1 个)
  85 +
  86 +#### 2.2.4 编辑
  87 +
  88 +- **方法**:`PUT`
  89 +- **路径**:`/api/app/product-category/{id}`
  90 +- **入参同新增**
  91 +
  92 +#### 2.2.5 删除
  93 +
  94 +- **方法**:`DELETE`
  95 +- **路径**:`/api/app/product-category/{id}`
  96 +- **说明**:逻辑删除;若被产品引用会阻止删除(保持原行为)
  97 +
  98 +### 2.3 后端校验规则(本次新增)
  99 +
  100 +- `availabilityType` 仅允许 `ALL/SPECIFIED`
  101 + - `SPECIFIED` 时 `locationIds` 至少 1 个
  102 +- `buttonAppearance` 仅允许 `TEXT/COLOR/IMAGE`(接口层校验枚举;`categoryPhotoUrl` 是否必填由业务/前端约定,`COLOR/IMAGE` 时应写入该字段)
  103 +
  104 +---
  105 +
  106 +## 3. 标签模块 Label Categories(Labels → Label Categories)
  107 +
  108 +> 数据库侧新增:`fl_label_category` 新字段、`fl_label_category_location` 新表。
  109 +
  110 +### 3.1 表结构要点
  111 +
  112 +- `fl_label_category`(主表)关键字段:
  113 + - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`)
  114 + - `ButtonAppearance`:`TEXT/COLOR/IMAGE`(前端按类型解析)
  115 + - `CategoryPhotoUrl`:与 `ButtonAppearance` 配合——`COLOR` 存颜色值、`IMAGE` 存图片 URL;`TEXT` 可空或不用
  116 + - **已删除列**(需在库上执行 `DROP COLUMN`):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson`
  117 + - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围)
  118 +- `fl_label_category_location`(关联表):
  119 + - `(CategoryId, LocationId)` 唯一约束;用于 `AvailabilityType=SPECIFIED` 指定门店(`LocationId` 对应 `location` 表主键)
  120 +
  121 +### 3.2 CRUD 接口(字段扩展)
  122 +
  123 +接口路径不变,仅扩展字段。
  124 +
  125 +#### 3.2.1 列表
  126 +
  127 +- **方法**:`GET`
  128 +- **路径**:`/api/app/label-category`
  129 +- **列表行新增返回**:
  130 + - `displayText`
  131 + - `buttonAppearance`
  132 + - `categoryPhotoUrl`
  133 + - `availabilityType`
  134 +
  135 +#### 3.2.2 详情
  136 +
  137 +- **方法**:`GET`
  138 +- **路径**:`/api/app/label-category/{id}`
  139 +- **新增返回字段**:
  140 + - `displayText`
  141 + - `buttonAppearance`、`categoryPhotoUrl`(COLOR/IMAGE 的展示数据统一在此字段)
  142 + - `availabilityType`
  143 + - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组)
  144 +
  145 +#### 3.2.3 新增
  146 +
  147 +- **方法**:`POST`
  148 +- **路径**:`/api/app/label-category`
  149 +- **新增入参字段**:
  150 + - `displayText`
  151 + - `buttonAppearance`、`categoryPhotoUrl`
  152 + - `availabilityType`
  153 + - `locationIds`(当 `availabilityType=SPECIFIED` 必填且至少 1 个)
  154 +
  155 +#### 3.2.4 编辑
  156 +
  157 +- **方法**:`PUT`
  158 +- **路径**:`/api/app/label-category/{id}`
  159 +- **入参同新增**
  160 +
  161 +#### 3.2.5 删除
  162 +
  163 +- **方法**:`DELETE`
  164 +- **路径**:`/api/app/label-category/{id}`
  165 +- **说明**:逻辑删除;若被标签引用会阻止删除(保持原行为)
  166 +
  167 +### 3.3 后端校验规则(本次新增)
  168 +
  169 +- `availabilityType` 仅允许 `ALL/SPECIFIED`
  170 + - `SPECIFIED` 时 `locationIds` 至少 1 个
  171 +- `buttonAppearance` 仅允许 `TEXT/COLOR/IMAGE`(接口层校验枚举;`categoryPhotoUrl` 是否必填由业务/前端约定,`COLOR/IMAGE` 时应写入该字段)
  172 +
  173 +---
  174 +
  175 +## 4. App 端 `GET /api/app/us-app-labeling/labeling-tree`
  176 +
  177 +- **L1(标签分类)节点**:除原有 `categoryName`、`categoryPhotoUrl`、`orderNum` 等外,**返回 `buttonAppearance`**(缺省或空时后端按 `TEXT` 规范化为大写)。
  178 +- **L2(产品分类)节点**:仅 `buttonAppearance` + `categoryPhotoUrl` 承载外观数据(已不再返回 `buttonTextColor`、`buttonBgColor`、`buttonImageUrl`、`buttonStyleJson`)。
  179 +
  180 +### 4.1 数据库迁移(两张主表)
  181 +
  182 +在确认历史数据已按需迁到 `CategoryPhotoUrl` 后,可执行(列不存在时需跳过或调整):
  183 +
  184 +```sql
  185 +ALTER TABLE `fl_label_category`
  186 + DROP COLUMN `ButtonTextColor`,
  187 + DROP COLUMN `ButtonBgColor`,
  188 + DROP COLUMN `ButtonImageUrl`,
  189 + DROP COLUMN `ButtonStyleJson`;
  190 +
  191 +ALTER TABLE `fl_product_category`
  192 + DROP COLUMN `ButtonTextColor`,
  193 + DROP COLUMN `ButtonBgColor`,
  194 + DROP COLUMN `ButtonImageUrl`,
  195 + DROP COLUMN `ButtonStyleJson`;
  196 +```
  197 +
  198 +---
  199 +
  200 +## 5. Web 管理端会话(`AuthSession` / 食品标签-美国版)
  201 +
  202 +> 实现:`IAuthSessionAppService` / `AuthSessionAppService`。需携带与后台一致的 **JWT**(`Authorization: Bearer {token}`)。具体 action 路径以部署环境 **Swagger / OpenAPI** 为准;下列为 ABP 常规约定(`RootPath = api/app`)。
  203 +
  204 +### 5.1 获取当前登录用户菜单与权限
  205 +
  206 +- **方法**:`GET`
  207 +- **路径**(约定):`/api/app/auth-session/my-menus`
  208 +- **鉴权**:需要登录
  209 +- **用途**:前端动态路由、侧边栏、按钮级权限(`permissionCodes`)
  210 +- **返回体**(`CurrentUserMenuPermissionsOutputDto`,JSON 字段名为 camelCase):
  211 +
  212 +| 字段 | 类型 | 说明 |
  213 +|---|---|---|
  214 +| `user` | object | 当前用户简要信息(无密码) |
  215 +| `user.id` | guid | 用户 Id |
  216 +| `user.userName` | string | 登录名 |
  217 +| `user.nick` | string? | 昵称 |
  218 +| `user.email` | string? | 邮箱 |
  219 +| `user.icon` | string? | 头像 |
  220 +| `roleCodes` | string[] | 角色编码列表(已排序) |
  221 +| `permissionCodes` | string[] | 权限码列表(已排序;超级管理员常见为 `*:*:*`) |
  222 +| `menus` | array | **菜单树**(根节点列表,子节点在 `children`) |
  223 +
  224 +**菜单树节点**(`CurrentUserMenuNodeDto`)主要字段:
  225 +
  226 +| 字段 | 说明 |
  227 +|---|---|
  228 +| `id` / `parentId` | 菜单 Id、父 Id(根父级多为 `"0"`) |
  229 +| `menuName` | 菜单名称 |
  230 +| `routerName` / `router` | 路由名、路径 |
  231 +| `permissionCode` | 权限标识(按钮/接口控制用) |
  232 +| `menuType` / `menuSource` | 枚举整型值(与 `Menu` 表一致) |
  233 +| `orderNum` / `state` | 排序、是否启用 |
  234 +| `menuIcon` / `component` / `isLink` / `isCache` / `isShow` / `query` / `remark` | 与菜单表一致 |
  235 +| `children` | 子节点数组 |
  236 +
  237 +**业务说明**:
  238 +
  239 +- 数据来源与框架 `UserManager.GetInfoAsync` 一致:按用户角色合并菜单与权限码。
  240 +- 用户名为 **`admin`** 时:与 `AccountService.GetVue3Router` 对齐,返回 **`Menu` 表中未逻辑删除** 的全部菜单再组树(`permissionCodes` 仍为超级管理员约定值)。
  241 +
  242 +### 5.2 退出登录
  243 +
  244 +- **方法**:`POST`
  245 +- **路径**(约定):`/api/app/auth-session/logout`
  246 +- **鉴权**:需要登录(未登录或无法解析用户时返回 `false`)
  247 +- **请求体**:无
  248 +- **返回**:`boolean`
  249 + - `true`:已清除服务端 **用户信息分布式缓存**(与 `AccountService.PostLogout` 一致)
  250 + - `false`:当前请求未识别到用户 Id(例如未登录)
  251 +- **说明**:JWT 为无状态令牌,**前端仍需丢弃本地 Token**;退出接口主要清理服务端缓存侧用户信息。
  252 +
  253 +---
  254 +
  255 +## 6. 权限菜单 `rbac-menu` 列表/详情补充字段
  256 +
  257 +- **路径**:`GET /api/app/rbac-menu`(列表)、`GET /api/app/rbac-menu/{id}`(详情)
  258 +- **新增返回**(与 `menu` 表字段一致,JSON 一般为 camelCase):
  259 + - `routerName`:路由名称
  260 + - `router`:路由路径
  261 +- **说明**:树接口 `GET /api/app/rbac-menu/tree`(若已使用)本身已包含完整菜单字段,无需重复改动。
  262 +
... ...