Commit 1b68006dd4beff00099b4172b8240be6c0d00330

Authored by 李曜臣
1 parent 3b0c279a

20260708

Showing 51 changed files with 2430 additions and 541 deletions
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Common/ILabelEntityPartnerScopeInput.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Common;
  2 +
  3 +/// <summary>
  4 +/// 标签类型 / 分类 / 多选项创建编辑入参中的 Company 适用范围字段。
  5 +/// </summary>
  6 +public interface ILabelEntityPartnerScopeInput
  7 +{
  8 + /// <summary>适用 Company:ALL / SPECIFIED</summary>
  9 + string? AppliedPartnerType { get; }
  10 +
  11 + /// <summary>适用 Company(<c>fl_partner.Id</c>)</summary>
  12 + List<string>? PartnerIds { get; }
  13 +
  14 + /// <summary>与 <see cref="PartnerIds"/> 相同(兼容字段)</summary>
  15 + List<string>? CompanyIds { get; }
  16 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelCreateInputVo.cs
... ... @@ -45,7 +45,8 @@ public class LabelCreateInputVo
45 45  
46 46 public string LabelCategoryId { get; set; } = string.Empty;
47 47  
48   - public string LabelTypeId { get; set; } = string.Empty;
  48 + /// <summary>标签类型 Id(<c>fl_label_type.Id</c>);可选,未传时标签不绑定类型</summary>
  49 + public string? LabelTypeId { get; set; }
49 50  
50 51 public List<string> ProductIds { get; set; } = new();
51 52  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetListInputVo.cs
... ... @@ -31,6 +31,7 @@ public class LabelGetListInputVo : PagedAndSortedResultRequestDto
31 31  
32 32 public string? LabelCategoryId { get; set; }
33 33  
  34 + /// <summary>按标签类型 Id 筛选(可选)</summary>
34 35 public string? LabelTypeId { get; set; }
35 36  
36 37 /// <summary>
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelUpdateInputVo.cs
... ... @@ -31,7 +31,8 @@ public class LabelUpdateInputVo
31 31  
32 32 public string LabelCategoryId { get; set; } = string.Empty;
33 33  
34   - public string LabelTypeId { get; set; } = string.Empty;
  34 + /// <summary>标签类型 Id(<c>fl_label_type.Id</c>);可选,未传时清空类型绑定</summary>
  35 + public string? LabelTypeId { get; set; }
35 36  
36 37 public List<string> ProductIds { get; set; } = new();
37 38  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryCreateInputVo.cs
  1 +using FoodLabeling.Application.Contracts.Dtos.Common;
  2 +
1 3 namespace FoodLabeling.Application.Contracts.Dtos.LabelCategory;
2 4  
3   -public class LabelCategoryCreateInputVo
  5 +public class LabelCategoryCreateInputVo : ILabelEntityPartnerScopeInput
4 6 {
5 7 public string CategoryCode { get; set; } = string.Empty;
6 8  
... ... @@ -24,6 +26,17 @@ public class LabelCategoryCreateInputVo
24 26 public string ButtonAppearance { get; set; } = "TEXT";
25 27  
26 28 /// <summary>
  29 + /// 适用 Company:ALL / SPECIFIED
  30 + /// </summary>
  31 + public string? AppliedPartnerType { get; set; }
  32 +
  33 + /// <summary>适用 Company(<c>fl_partner.Id</c>)</summary>
  34 + public List<string>? PartnerIds { get; set; }
  35 +
  36 + /// <summary>与 <see cref="PartnerIds"/> 相同(兼容字段)</summary>
  37 + public List<string>? CompanyIds { get; set; }
  38 +
  39 + /// <summary>
27 40 /// 门店可用范围:ALL / SPECIFIED
28 41 /// </summary>
29 42 public string AvailabilityType { get; set; } = "ALL";
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListInputVo.cs
... ... @@ -8,6 +8,9 @@ public class LabelCategoryGetListInputVo : PagedAndSortedResultRequestDto
8 8  
9 9 public bool? State { get; set; }
10 10  
  11 + /// <summary>Company 筛选(<c>fl_partner.Id</c>)</summary>
  12 + public string? PartnerId { get; set; }
  13 +
11 14 /// <summary>
12 15 /// Region 筛选(<c>fl_group.Id</c>);含 <c>availabilityType=ALL</c> 的分类
13 16 /// </summary>
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListOutputDto.cs
... ... @@ -18,6 +18,16 @@ public class LabelCategoryGetListOutputDto
18 18  
19 19 public string AvailabilityType { get; set; } = "ALL";
20 20  
  21 + public string AppliedPartnerType { get; set; } = "ALL";
  22 +
  23 + /// <summary>适用 Company 展示(ALL 时为 All Companies)</summary>
  24 + public string Company { get; set; } = string.Empty;
  25 +
  26 + public List<string> PartnerIds { get; set; } = new();
  27 +
  28 + /// <summary>与 <see cref="PartnerIds"/> 相同</summary>
  29 + public List<string> CompanyIds { get; set; } = new();
  30 +
21 31 /// <summary>适用 Region 展示(ALL 时为 All Regions)</summary>
22 32 public string Region { get; set; } = string.Empty;
23 33  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetOutputDto.cs
... ... @@ -18,6 +18,16 @@ public class LabelCategoryGetOutputDto
18 18  
19 19 public string AvailabilityType { get; set; } = "ALL";
20 20  
  21 + public string AppliedPartnerType { get; set; } = "ALL";
  22 +
  23 + /// <summary>列表/详情 Company 展示</summary>
  24 + public string Company { get; set; } = string.Empty;
  25 +
  26 + public List<string> PartnerIds { get; set; } = new();
  27 +
  28 + /// <summary>与 <see cref="PartnerIds"/> 相同</summary>
  29 + public List<string> CompanyIds { get; set; } = new();
  30 +
21 31 /// <summary>适用 Region Id 列表(多选;<c>ALL</c> 时为空)</summary>
22 32 public List<string> RegionIds { get; set; } = new();
23 33  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionCreateInputVo.cs
  1 +using FoodLabeling.Application.Contracts.Dtos.Common;
  2 +
1 3 namespace FoodLabeling.Application.Contracts.Dtos.LabelMultipleOption;
2 4  
3   -public class LabelMultipleOptionCreateInputVo
  5 +public class LabelMultipleOptionCreateInputVo : ILabelEntityPartnerScopeInput
4 6 {
5 7 /// <summary>多选项编码(可选;未传或空字符串时存空,列表/详情出参为「无」)</summary>
6 8 public string? OptionCode { get; set; }
... ... @@ -12,6 +14,17 @@ public class LabelMultipleOptionCreateInputVo
12 14 public bool State { get; set; } = true;
13 15  
14 16 /// <summary>
  17 + /// 适用 Company:ALL / SPECIFIED
  18 + /// </summary>
  19 + public string? AppliedPartnerType { get; set; }
  20 +
  21 + /// <summary>适用 Company(<c>fl_partner.Id</c>)</summary>
  22 + public List<string>? PartnerIds { get; set; }
  23 +
  24 + /// <summary>与 <see cref="PartnerIds"/> 相同(兼容字段)</summary>
  25 + public List<string>? CompanyIds { get; set; }
  26 +
  27 + /// <summary>
15 28 /// 门店可用范围:ALL / SPECIFIED;传了 <see cref="RegionIds"/> 或 <see cref="LocationIds"/> 时自动为 SPECIFIED
16 29 /// </summary>
17 30 public string AvailabilityType { get; set; } = "ALL";
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListInputVo.cs
... ... @@ -8,6 +8,9 @@ public class LabelMultipleOptionGetListInputVo : PagedAndSortedResultRequestDto
8 8  
9 9 public bool? State { get; set; }
10 10  
  11 + /// <summary>Company 筛选(<c>fl_partner.Id</c>)</summary>
  12 + public string? PartnerId { get; set; }
  13 +
11 14 /// <summary>
12 15 /// Region 筛选(<c>fl_group.Id</c>)
13 16 /// </summary>
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListOutputDto.cs
... ... @@ -14,6 +14,14 @@ public class LabelMultipleOptionGetListOutputDto
14 14  
15 15 public string AvailabilityType { get; set; } = "ALL";
16 16  
  17 + public string AppliedPartnerType { get; set; } = "ALL";
  18 +
  19 + public string Company { get; set; } = string.Empty;
  20 +
  21 + public List<string> PartnerIds { get; set; } = new();
  22 +
  23 + public List<string> CompanyIds { get; set; } = new();
  24 +
17 25 public int OrderNum { get; set; }
18 26  
19 27 public DateTime LastEdited { get; set; }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetOutputDto.cs
... ... @@ -16,6 +16,14 @@ public class LabelMultipleOptionGetOutputDto
16 16  
17 17 public string AvailabilityType { get; set; } = "ALL";
18 18  
  19 + public string AppliedPartnerType { get; set; } = "ALL";
  20 +
  21 + public string Company { get; set; } = string.Empty;
  22 +
  23 + public List<string> PartnerIds { get; set; } = new();
  24 +
  25 + public List<string> CompanyIds { get; set; } = new();
  26 +
19 27 public List<string> RegionIds { get; set; } = new();
20 28  
21 29 public List<string> GroupIds { get; set; } = new();
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeCreateInputVo.cs
  1 +using FoodLabeling.Application.Contracts.Dtos.Common;
  2 +
1 3 namespace FoodLabeling.Application.Contracts.Dtos.LabelType;
2 4  
3   -public class LabelTypeCreateInputVo
  5 +public class LabelTypeCreateInputVo : ILabelEntityPartnerScopeInput
4 6 {
5 7 public string TypeCode { get; set; } = string.Empty;
6 8  
... ... @@ -9,6 +11,17 @@ public class LabelTypeCreateInputVo
9 11 public bool State { get; set; } = true;
10 12  
11 13 /// <summary>
  14 + /// 适用 Company:ALL / SPECIFIED
  15 + /// </summary>
  16 + public string? AppliedPartnerType { get; set; }
  17 +
  18 + /// <summary>适用 Company(<c>fl_partner.Id</c>)</summary>
  19 + public List<string>? PartnerIds { get; set; }
  20 +
  21 + /// <summary>与 <see cref="PartnerIds"/> 相同(兼容字段)</summary>
  22 + public List<string>? CompanyIds { get; set; }
  23 +
  24 + /// <summary>
12 25 /// 门店可用范围:ALL / SPECIFIED;传了 <see cref="RegionIds"/> 或 <see cref="LocationIds"/> 时自动为 SPECIFIED
13 26 /// </summary>
14 27 public string AvailabilityType { get; set; } = "ALL";
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListInputVo.cs
... ... @@ -8,6 +8,9 @@ public class LabelTypeGetListInputVo : PagedAndSortedResultRequestDto
8 8  
9 9 public bool? State { get; set; }
10 10  
  11 + /// <summary>Company 筛选(<c>fl_partner.Id</c>)</summary>
  12 + public string? PartnerId { get; set; }
  13 +
11 14 /// <summary>
12 15 /// Region 筛选(<c>fl_group.Id</c>);仅返回在该 Region 下存在标签实例的类型
13 16 /// </summary>
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListOutputDto.cs
... ... @@ -12,6 +12,16 @@ public class LabelTypeGetListOutputDto
12 12  
13 13 public string AvailabilityType { get; set; } = "ALL";
14 14  
  15 + public string AppliedPartnerType { get; set; } = "ALL";
  16 +
  17 + /// <summary>适用 Company 展示(ALL 时为 All Companies)</summary>
  18 + public string Company { get; set; } = string.Empty;
  19 +
  20 + public List<string> PartnerIds { get; set; } = new();
  21 +
  22 + /// <summary>与 <see cref="PartnerIds"/> 相同</summary>
  23 + public List<string> CompanyIds { get; set; } = new();
  24 +
15 25 public int OrderNum { get; set; }
16 26  
17 27 /// <summary>列表列 No. of Labels:该类型下未删除标签数(统计 <c>fl_label</c>,非库表物理列)</summary>
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetOutputDto.cs
... ... @@ -14,6 +14,16 @@ public class LabelTypeGetOutputDto
14 14  
15 15 public string AvailabilityType { get; set; } = "ALL";
16 16  
  17 + public string AppliedPartnerType { get; set; } = "ALL";
  18 +
  19 + /// <summary>列表/详情 Company 展示</summary>
  20 + public string Company { get; set; } = string.Empty;
  21 +
  22 + public List<string> PartnerIds { get; set; } = new();
  23 +
  24 + /// <summary>与 <see cref="PartnerIds"/> 相同</summary>
  25 + public List<string> CompanyIds { get; set; } = new();
  26 +
17 27 public List<string> RegionIds { get; set; } = new();
18 28  
19 29 public List<string> GroupIds { get; set; } = new();
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListInputVo.cs
... ... @@ -18,11 +18,16 @@ public class ProductCategoryGetListInputVo : PagedAndSortedResultRequestDto
18 18 public bool? State { get; set; }
19 19  
20 20 /// <summary>
21   - /// 按 Region 筛选(<c>fl_group.Id</c>):返回适用于该 Region 下任一门门店的分类,以及 <c>availabilityType=ALL</c> 的分类
  21 + /// 按 Region 筛选(<c>fl_group.Id</c>):返回适用于该 Region 下任一门门店的分类
22 22 /// </summary>
23 23 public string? GroupId { get; set; }
24 24  
25 25 /// <summary>
  26 + /// 按 Company 筛选(<c>fl_partner.Id</c>)
  27 + /// </summary>
  28 + public string? PartnerId { get; set; }
  29 +
  30 + /// <summary>
26 31 /// 按门店筛选(<c>location.Id</c>,Guid 字符串);优先于 <see cref="GroupId"/>
27 32 /// </summary>
28 33 public string? LocationId { get; set; }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelAppService.cs
... ... @@ -11,7 +11,7 @@ namespace FoodLabeling.Application.Contracts.IServices;
11 11 public interface ILabelAppService : IApplicationService
12 12 {
13 13 /// <summary>
14   - /// 按产品分页列表(一个产品展示多个标签)。支持 <c>GroupId</c> / <c>LocationId</c> 筛选(命中 <c>fl_label_region</c>、<c>fl_label_location</c> 或 <c>AppliedRegionType=ALL</c>);出参含 <c>locationIds</c>、<c>region</c>、<c>regionIds</c>、<c>appliedRegionType</c>。
  14 + /// 按产品分页列表(一个产品展示多个标签)。支持 <c>GroupId</c> / <c>LocationId</c> 筛选(命中 <c>fl_label_region</c>、<c>fl_label_location</c> 或 <c>AppliedRegionType=ALL</c>);<c>labelTypeId</c> 为可选筛选;出参含 <c>locationIds</c>、<c>region</c>、<c>regionIds</c>、<c>appliedRegionType</c>。
15 15 /// </summary>
16 16 Task<PagedResultWithPageDto<LabelGetListOutputDto>> GetListAsync(LabelGetListInputVo input);
17 17  
... ... @@ -21,7 +21,7 @@ public interface ILabelAppService : IApplicationService
21 21 Task<LabelGetOutputDto> GetAsync(string id);
22 22  
23 23 /// <summary>
24   - /// 新增标签。Body 支持 <c>appliedRegionType</c>(ALL/SPECIFIED)、<c>regionIds</c> / <c>groupIds</c>(落库 <c>fl_label_region</c>)、<c>locationIds</c>(主字段,落库 <c>fl_label_location</c>);<c>locationId</c> 与 <c>locationIds</c> 合并
  24 + /// 新增标签。Body 支持 <c>appliedRegionType</c>(ALL/SPECIFIED)、<c>regionIds</c> / <c>groupIds</c>(落库 <c>fl_label_region</c>)、<c>locationIds</c>(主字段,落库 <c>fl_label_location</c>);<c>locationId</c> 与 <c>locationIds</c> 合并;<c>labelTypeId</c> 可选
25 25 /// </summary>
26 26 Task<LabelGetOutputDto> CreateAsync(LabelCreateInputVo input);
27 27  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs
... ... @@ -11,12 +11,12 @@ namespace FoodLabeling.Application.Contracts.IServices;
11 11 public interface IUsAppAuthAppService : IApplicationService
12 12 {
13 13 /// <summary>
14   - /// App 登录:使用邮箱 + 密码校验并签发 Token,同时返回 userlocation 绑定的门店
  14 + /// App 登录:使用邮箱 + 密码校验并签发 Token,同时返回可选门店(Company Admin 为绑定 Company 下全部门店)
15 15 /// </summary>
16 16 Task<UsAppLoginOutputDto> LoginAsync(UsAppLoginInputVo input);
17 17  
18 18 /// <summary>
19   - /// 获取当前登录账号已绑定的门店(用于切换门店等场景)
  19 + /// 获取当前登录账号可选门店(Company Admin 返回绑定 Company 下全部门店,用于切换门店等场景)
20 20 /// </summary>
21 21 Task<List<UsAppBoundLocationDto>> GetMyLocationsAsync();
22 22  
... ... @@ -62,7 +62,7 @@ public interface IUsAppAuthAppService : IApplicationService
62 62 Task ChangePasswordAsync(UsAppChangePasswordInputVo input);
63 63  
64 64 /// <summary>
65   - /// 按门店 Id 查询 Location 详情(须为当前账号 userlocation 绑定门店)
  65 + /// 按门店 Id 查询 Location 详情(须为当前账号可选门店;Company Admin 含绑定 Company 下任意门店)
66 66 /// </summary>
67 67 /// <param name="locationId">门店 Guid 字符串</param>
68 68 /// <returns>店名、地址、电话、经营时间(operatingHours)、店长信息</returns>
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelEntityPartnerScopeHelper.cs 0 → 100644
  1 +using FoodLabeling.Application.Services.DbModels;
  2 +using SqlSugar;
  3 +using Volo.Abp;
  4 +using Volo.Abp.Guids;
  5 +
  6 +namespace FoodLabeling.Application.Helpers;
  7 +
  8 +/// <summary>
  9 +/// 标签类型 / 分类 / 多选项适用 Company:ALL/SPECIFIED + 多选落库(对齐 <see cref="LabelTemplateScopeHelper"/> 的 Company 维度)。
  10 +/// </summary>
  11 +public static class LabelEntityPartnerScopeHelper
  12 +{
  13 + public const string ScopeAll = "ALL";
  14 + public const string ScopeSpecified = "SPECIFIED";
  15 +
  16 + public const string AllCompaniesDisplay = "All Companies";
  17 + public const string EmptyDisplay = "无";
  18 +
  19 + /// <summary>标签实体种类(对应主表与 partner 关联表)。</summary>
  20 + public enum LabelEntityPartnerKind
  21 + {
  22 + /// <summary>标签类型 <c>fl_label_type</c></summary>
  23 + Type,
  24 +
  25 + /// <summary>标签分类 <c>fl_label_category</c></summary>
  26 + Category,
  27 +
  28 + /// <summary>标签多选项 <c>fl_label_multiple_option</c></summary>
  29 + MultipleOption
  30 + }
  31 +
  32 + public sealed class LabelEntityPartnerScopeSaveResult
  33 + {
  34 + public string AppliedPartnerType { get; init; } = ScopeAll;
  35 +
  36 + public List<string> PartnerIds { get; init; } = new();
  37 + }
  38 +
  39 + public sealed class LabelEntityPartnerScopeDisplay
  40 + {
  41 + public string Company { get; init; } = AllCompaniesDisplay;
  42 +
  43 + public string AppliedPartnerType { get; init; } = ScopeAll;
  44 +
  45 + public List<string> PartnerIds { get; init; } = new();
  46 + }
  47 +
  48 + private static LabelEntityPartnerScopeSchemaStatus? _cachedSchema;
  49 +
  50 + private sealed class LabelEntityPartnerScopeSchemaStatus
  51 + {
  52 + public bool HasTypePartnerColumn { get; init; }
  53 +
  54 + public bool HasCategoryPartnerColumn { get; init; }
  55 +
  56 + public bool HasMultipleOptionPartnerColumn { get; init; }
  57 +
  58 + public bool HasTypePartnerTable { get; init; }
  59 +
  60 + public bool HasCategoryPartnerTable { get; init; }
  61 +
  62 + public bool HasMultipleOptionPartnerTable { get; init; }
  63 + }
  64 +
  65 + /// <summary>
  66 + /// 合并 <paramref name="partnerIds"/> 与 <paramref name="companyIds"/> 并去重排序。
  67 + /// </summary>
  68 + public static List<string> NormalizePartnerIds(IReadOnlyList<string>? partnerIds, IReadOnlyList<string>? companyIds)
  69 + {
  70 + var merged = new HashSet<string>(StringComparer.Ordinal);
  71 + foreach (var id in LocationScopeBindingHelper.NormalizeIds(partnerIds))
  72 + {
  73 + merged.Add(id);
  74 + }
  75 +
  76 + foreach (var id in LocationScopeBindingHelper.NormalizeIds(companyIds))
  77 + {
  78 + merged.Add(id);
  79 + }
  80 +
  81 + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList();
  82 + }
  83 +
  84 + /// <summary>
  85 + /// 解析适用 Company 范围类型(ALL/SPECIFIED)。
  86 + /// </summary>
  87 + public static string ResolveAppliedPartnerType(string? type, List<string> partnerIds, bool hasArrayInPayload)
  88 + {
  89 + if (partnerIds.Count > 0)
  90 + {
  91 + return ScopeSpecified;
  92 + }
  93 +
  94 + var normalized = (type ?? ScopeAll).Trim().ToUpperInvariant();
  95 + if (hasArrayInPayload && string.Equals(normalized, ScopeAll, StringComparison.OrdinalIgnoreCase))
  96 + {
  97 + return ScopeAll;
  98 + }
  99 +
  100 + return string.Equals(normalized, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
  101 + ? ScopeSpecified
  102 + : ScopeAll;
  103 + }
  104 +
  105 + /// <summary>
  106 + /// 解析新增/编辑入参中的 Company 范围并校验 partner Id 有效性。
  107 + /// </summary>
  108 + public static async Task<LabelEntityPartnerScopeSaveResult> ResolvePartnerScopeForSaveAsync(
  109 + ISqlSugarClient db,
  110 + string? appliedPartnerType,
  111 + IReadOnlyList<string>? partnerIds,
  112 + IReadOnlyList<string>? companyIds)
  113 + {
  114 + var normalizedPartnerIds = NormalizePartnerIds(partnerIds, companyIds);
  115 + var partnerType = ResolveAppliedPartnerType(
  116 + appliedPartnerType,
  117 + normalizedPartnerIds,
  118 + partnerIds is not null || companyIds is not null);
  119 +
  120 + ValidatePartnerType(partnerType);
  121 +
  122 + if (string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
  123 + && normalizedPartnerIds.Count == 0)
  124 + {
  125 + throw new UserFriendlyException("指定适用 Company 时,partnerIds/companyIds 不能为空");
  126 + }
  127 +
  128 + if (normalizedPartnerIds.Count > 0)
  129 + {
  130 + await ValidatePartnerIdsExistAsync(db, normalizedPartnerIds);
  131 + }
  132 +
  133 + return new LabelEntityPartnerScopeSaveResult
  134 + {
  135 + AppliedPartnerType = partnerType,
  136 + PartnerIds = normalizedPartnerIds
  137 + };
  138 + }
  139 +
  140 + /// <summary>
  141 + /// 保存 Company 适用范围(关联表 + <c>AppliedPartnerType</c> 列);未执行迁移脚本时 no-op。
  142 + /// </summary>
  143 + public static async Task SavePartnerScopeAsync(
  144 + ISqlSugarClient db,
  145 + IGuidGenerator guidGenerator,
  146 + LabelEntityPartnerKind kind,
  147 + string entityId,
  148 + LabelEntityPartnerScopeSaveResult scope,
  149 + string? currentUserId,
  150 + DateTime now)
  151 + {
  152 + if (string.IsNullOrWhiteSpace(entityId))
  153 + {
  154 + return;
  155 + }
  156 +
  157 + var schema = await GetSchemaStatusAsync(db);
  158 + if (!HasPartnerScopeForKind(schema, kind))
  159 + {
  160 + return;
  161 + }
  162 +
  163 + await DeletePartnerScopeRowsAsync(db, kind, entityId);
  164 +
  165 + if (string.Equals(scope.AppliedPartnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)
  166 + && scope.PartnerIds.Count > 0)
  167 + {
  168 + await InsertPartnerRowsAsync(db, guidGenerator, kind, entityId, scope.PartnerIds, currentUserId, now);
  169 + }
  170 +
  171 + await SetAppliedPartnerTypeAsync(db, kind, entityId, scope.AppliedPartnerType);
  172 + }
  173 +
  174 + /// <summary>
  175 + /// 删除实体下 Company 关联行;未迁移 partner 表时跳过。
  176 + /// </summary>
  177 + public static async Task DeletePartnerScopeRowsAsync(
  178 + ISqlSugarClient db,
  179 + LabelEntityPartnerKind kind,
  180 + string entityId)
  181 + {
  182 + if (string.IsNullOrWhiteSpace(entityId))
  183 + {
  184 + return;
  185 + }
  186 +
  187 + var schema = await GetSchemaStatusAsync(db);
  188 + if (!HasPartnerTableForKind(schema, kind))
  189 + {
  190 + return;
  191 + }
  192 +
  193 + switch (kind)
  194 + {
  195 + case LabelEntityPartnerKind.Type:
  196 + await db.Deleteable<FlLabelTypePartnerDbEntity>()
  197 + .Where(x => x.LabelTypeId == entityId)
  198 + .ExecuteCommandAsync();
  199 + break;
  200 + case LabelEntityPartnerKind.Category:
  201 + await db.Deleteable<FlLabelCategoryPartnerDbEntity>()
  202 + .Where(x => x.CategoryId == entityId)
  203 + .ExecuteCommandAsync();
  204 + break;
  205 + case LabelEntityPartnerKind.MultipleOption:
  206 + await db.Deleteable<FlLabelMultipleOptionPartnerDbEntity>()
  207 + .Where(x => x.MultipleOptionId == entityId)
  208 + .ExecuteCommandAsync();
  209 + break;
  210 + }
  211 + }
  212 +
  213 + /// <summary>
  214 + /// 批量加载 Company 展示与 Id 数组(详情/列表)。
  215 + /// </summary>
  216 + public static async Task<Dictionary<string, LabelEntityPartnerScopeDisplay>> BuildPartnerScopeDisplayMapAsync(
  217 + ISqlSugarClient db,
  218 + LabelEntityPartnerKind kind,
  219 + IReadOnlyList<string> entityIds)
  220 + {
  221 + var result = new Dictionary<string, LabelEntityPartnerScopeDisplay>(StringComparer.Ordinal);
  222 + if (entityIds.Count == 0)
  223 + {
  224 + return result;
  225 + }
  226 +
  227 + var ids = entityIds.Where(x => !string.IsNullOrWhiteSpace(x))
  228 + .Select(x => x.Trim())
  229 + .Distinct(StringComparer.Ordinal)
  230 + .ToList();
  231 + if (ids.Count == 0)
  232 + {
  233 + return result;
  234 + }
  235 +
  236 + var schema = await GetSchemaStatusAsync(db);
  237 + var hasScope = HasPartnerScopeForKind(schema, kind);
  238 +
  239 + var partnerLinks = hasScope
  240 + ? await LoadPartnerLinksAsync(db, kind, ids)
  241 + : new List<(string EntityId, string PartnerId)>();
  242 +
  243 + var partnerIdSet = partnerLinks.Select(x => x.PartnerId).Distinct(StringComparer.Ordinal).ToList();
  244 + var partnerNameById = await LoadPartnerNameMapAsync(db, partnerIdSet);
  245 +
  246 + foreach (var entityId in ids)
  247 + {
  248 + var pIds = LocationScopeBindingHelper.NormalizeIds(
  249 + partnerLinks.Where(x => x.EntityId == entityId).Select(x => x.PartnerId).ToList());
  250 +
  251 + var partnerType = pIds.Count > 0 ? ScopeSpecified : ScopeAll;
  252 + if (hasScope)
  253 + {
  254 + var storedType = await GetAppliedPartnerTypeAsync(db, kind, entityId);
  255 + if (!string.IsNullOrWhiteSpace(storedType))
  256 + {
  257 + partnerType = NormalizeScopeType(storedType, ScopeAll);
  258 + }
  259 + else if (pIds.Count > 0)
  260 + {
  261 + partnerType = ScopeSpecified;
  262 + }
  263 + }
  264 +
  265 + var companyDisplay = string.Equals(partnerType, ScopeAll, StringComparison.OrdinalIgnoreCase)
  266 + ? AllCompaniesDisplay
  267 + : BuildCompanyDisplay(pIds, partnerNameById);
  268 +
  269 + result[entityId] = new LabelEntityPartnerScopeDisplay
  270 + {
  271 + Company = companyDisplay,
  272 + AppliedPartnerType = partnerType,
  273 + PartnerIds = pIds
  274 + };
  275 + }
  276 +
  277 + return result;
  278 + }
  279 +
  280 + /// <summary>
  281 + /// 列表 Company 列展示文案。
  282 + /// </summary>
  283 + public static string BuildCompanyDisplay(
  284 + IReadOnlyList<string> partnerIds,
  285 + IReadOnlyDictionary<string, string> partnerNameById)
  286 + {
  287 + if (partnerIds.Count == 0)
  288 + {
  289 + return EmptyDisplay;
  290 + }
  291 +
  292 + var names = partnerIds
  293 + .Select(id => partnerNameById.TryGetValue(id, out var name) ? name : id)
  294 + .Select(x => x?.Trim())
  295 + .Where(x => !string.IsNullOrEmpty(x))
  296 + .Distinct(StringComparer.OrdinalIgnoreCase)
  297 + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
  298 + .ToList();
  299 +
  300 + return names.Count > 0 ? string.Join(", ", names) : EmptyDisplay;
  301 + }
  302 +
  303 + /// <summary>
  304 + /// 列表筛选:按可见 Company 过滤标签类型(与 Region/Location 筛选 AND)。
  305 + /// </summary>
  306 + public static async Task<ISugarQueryable<FlLabelTypeDbEntity>> ApplyTypePartnerListFilterAsync(
  307 + ISqlSugarClient db,
  308 + ISugarQueryable<FlLabelTypeDbEntity> query,
  309 + IReadOnlyList<string>? scopedPartnerIds)
  310 + {
  311 + if (scopedPartnerIds is null)
  312 + {
  313 + return query;
  314 + }
  315 +
  316 + var schema = await GetSchemaStatusAsync(db);
  317 + if (!HasPartnerTableForKind(schema, LabelEntityPartnerKind.Type))
  318 + {
  319 + return query;
  320 + }
  321 +
  322 + if (scopedPartnerIds.Count == 0)
  323 + {
  324 + return query.Where(t =>
  325 + !SqlFunc.Subqueryable<FlLabelTypePartnerDbEntity>()
  326 + .Where(p => p.LabelTypeId == t.Id)
  327 + .Any());
  328 + }
  329 +
  330 + return query.Where(t =>
  331 + !SqlFunc.Subqueryable<FlLabelTypePartnerDbEntity>()
  332 + .Where(p => p.LabelTypeId == t.Id)
  333 + .Any()
  334 + || SqlFunc.Subqueryable<FlLabelTypePartnerDbEntity>()
  335 + .Where(p => p.LabelTypeId == t.Id && scopedPartnerIds.Contains(p.PartnerId))
  336 + .Any());
  337 + }
  338 +
  339 + /// <summary>
  340 + /// 列表筛选:按可见 Company 过滤标签分类。
  341 + /// </summary>
  342 + public static async Task<ISugarQueryable<FlLabelCategoryDbEntity>> ApplyCategoryPartnerListFilterAsync(
  343 + ISqlSugarClient db,
  344 + ISugarQueryable<FlLabelCategoryDbEntity> query,
  345 + IReadOnlyList<string>? scopedPartnerIds)
  346 + {
  347 + if (scopedPartnerIds is null)
  348 + {
  349 + return query;
  350 + }
  351 +
  352 + var schema = await GetSchemaStatusAsync(db);
  353 + if (!HasPartnerTableForKind(schema, LabelEntityPartnerKind.Category))
  354 + {
  355 + return query;
  356 + }
  357 +
  358 + if (scopedPartnerIds.Count == 0)
  359 + {
  360 + return query.Where(c =>
  361 + !SqlFunc.Subqueryable<FlLabelCategoryPartnerDbEntity>()
  362 + .Where(p => p.CategoryId == c.Id)
  363 + .Any());
  364 + }
  365 +
  366 + return query.Where(c =>
  367 + !SqlFunc.Subqueryable<FlLabelCategoryPartnerDbEntity>()
  368 + .Where(p => p.CategoryId == c.Id)
  369 + .Any()
  370 + || SqlFunc.Subqueryable<FlLabelCategoryPartnerDbEntity>()
  371 + .Where(p => p.CategoryId == c.Id && scopedPartnerIds.Contains(p.PartnerId))
  372 + .Any());
  373 + }
  374 +
  375 + /// <summary>
  376 + /// 列表筛选:按可见 Company 过滤标签多选项。
  377 + /// </summary>
  378 + public static async Task<ISugarQueryable<FlLabelMultipleOptionDbEntity>> ApplyMultipleOptionPartnerListFilterAsync(
  379 + ISqlSugarClient db,
  380 + ISugarQueryable<FlLabelMultipleOptionDbEntity> query,
  381 + IReadOnlyList<string>? scopedPartnerIds)
  382 + {
  383 + if (scopedPartnerIds is null)
  384 + {
  385 + return query;
  386 + }
  387 +
  388 + var schema = await GetSchemaStatusAsync(db);
  389 + if (!HasPartnerTableForKind(schema, LabelEntityPartnerKind.MultipleOption))
  390 + {
  391 + return query;
  392 + }
  393 +
  394 + if (scopedPartnerIds.Count == 0)
  395 + {
  396 + return query.Where(o =>
  397 + !SqlFunc.Subqueryable<FlLabelMultipleOptionPartnerDbEntity>()
  398 + .Where(p => p.MultipleOptionId == o.Id)
  399 + .Any());
  400 + }
  401 +
  402 + return query.Where(o =>
  403 + !SqlFunc.Subqueryable<FlLabelMultipleOptionPartnerDbEntity>()
  404 + .Where(p => p.MultipleOptionId == o.Id)
  405 + .Any()
  406 + || SqlFunc.Subqueryable<FlLabelMultipleOptionPartnerDbEntity>()
  407 + .Where(p => p.MultipleOptionId == o.Id && scopedPartnerIds.Contains(p.PartnerId))
  408 + .Any());
  409 + }
  410 +
  411 + /// <summary>
  412 + /// 由 Query <c>partnerId</c> 解析列表 Company 筛选 Id;未传则返回 <c>null</c>。
  413 + /// </summary>
  414 + public static async Task<List<string>?> ResolveScopedPartnerIdsForListAsync(
  415 + ISqlSugarClient db,
  416 + string? partnerId)
  417 + {
  418 + var pid = partnerId?.Trim();
  419 + if (string.IsNullOrWhiteSpace(pid))
  420 + {
  421 + return null;
  422 + }
  423 +
  424 + var exists = await db.Queryable<FlPartnerDbEntity>()
  425 + .AnyAsync(x => !x.IsDeleted && x.Id == pid);
  426 + return exists ? new List<string> { pid } : new List<string>();
  427 + }
  428 +
  429 + /// <summary>
  430 + /// 由 Query partnerId 或可见门店范围解析列表 Company 筛选 Id;均未限时返回 <c>null</c>。
  431 + /// </summary>
  432 + public static async Task<List<string>?> ResolveListPartnerIdsAsync(
  433 + ISqlSugarClient db,
  434 + string? partnerId,
  435 + List<string>? scopedLocationIds)
  436 + {
  437 + var fromQuery = await ResolveScopedPartnerIdsForListAsync(db, partnerId);
  438 + if (fromQuery is not null)
  439 + {
  440 + return fromQuery;
  441 + }
  442 +
  443 + if (scopedLocationIds is null)
  444 + {
  445 + return null;
  446 + }
  447 +
  448 + if (scopedLocationIds.Count == 0)
  449 + {
  450 + return new List<string>();
  451 + }
  452 +
  453 + return await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, scopedLocationIds);
  454 + }
  455 +
  456 + /// <summary>单元测试或切换库后调用。</summary>
  457 + public static void ResetSchemaCacheForTests() => _cachedSchema = null;
  458 +
  459 + private static void ValidatePartnerType(string type)
  460 + {
  461 + if (type != ScopeAll && type != ScopeSpecified)
  462 + {
  463 + throw new UserFriendlyException("适用 Company 范围不合法(ALL/SPECIFIED)");
  464 + }
  465 + }
  466 +
  467 + private static string NormalizeScopeType(string? type, string fallback)
  468 + {
  469 + var t = (type ?? fallback).Trim().ToUpperInvariant();
  470 + return t == ScopeSpecified ? ScopeSpecified : ScopeAll;
  471 + }
  472 +
  473 + private static async Task ValidatePartnerIdsExistAsync(ISqlSugarClient db, List<string> partnerIds)
  474 + {
  475 + if (partnerIds.Count == 0)
  476 + {
  477 + return;
  478 + }
  479 +
  480 + var count = await db.Queryable<FlPartnerDbEntity>()
  481 + .Where(x => !x.IsDeleted && partnerIds.Contains(x.Id))
  482 + .CountAsync();
  483 + if (count != partnerIds.Count)
  484 + {
  485 + throw new UserFriendlyException("存在无效的 Company(partnerIds/companyIds),请刷新后重试");
  486 + }
  487 + }
  488 +
  489 + private static async Task<LabelEntityPartnerScopeSchemaStatus> GetSchemaStatusAsync(ISqlSugarClient db)
  490 + {
  491 + if (_cachedSchema is not null)
  492 + {
  493 + return _cachedSchema;
  494 + }
  495 +
  496 + try
  497 + {
  498 + _cachedSchema = new LabelEntityPartnerScopeSchemaStatus
  499 + {
  500 + HasTypePartnerColumn = await ColumnExistsAsync(db, "fl_label_type", "AppliedPartnerType"),
  501 + HasCategoryPartnerColumn = await ColumnExistsAsync(db, "fl_label_category", "AppliedPartnerType"),
  502 + HasMultipleOptionPartnerColumn = await ColumnExistsAsync(
  503 + db, "fl_label_multiple_option", "AppliedPartnerType"),
  504 + HasTypePartnerTable = await TableExistsAsync(db, "fl_label_type_partner"),
  505 + HasCategoryPartnerTable = await TableExistsAsync(db, "fl_label_category_partner"),
  506 + HasMultipleOptionPartnerTable = await TableExistsAsync(db, "fl_label_multiple_option_partner")
  507 + };
  508 + }
  509 + catch
  510 + {
  511 + _cachedSchema = new LabelEntityPartnerScopeSchemaStatus();
  512 + }
  513 +
  514 + return _cachedSchema;
  515 + }
  516 +
  517 + private static bool HasPartnerScopeForKind(LabelEntityPartnerScopeSchemaStatus schema, LabelEntityPartnerKind kind) =>
  518 + HasPartnerTableForKind(schema, kind) && HasPartnerColumnForKind(schema, kind);
  519 +
  520 + private static bool HasPartnerTableForKind(LabelEntityPartnerScopeSchemaStatus schema, LabelEntityPartnerKind kind) =>
  521 + kind switch
  522 + {
  523 + LabelEntityPartnerKind.Type => schema.HasTypePartnerTable,
  524 + LabelEntityPartnerKind.Category => schema.HasCategoryPartnerTable,
  525 + LabelEntityPartnerKind.MultipleOption => schema.HasMultipleOptionPartnerTable,
  526 + _ => false
  527 + };
  528 +
  529 + private static bool HasPartnerColumnForKind(LabelEntityPartnerScopeSchemaStatus schema, LabelEntityPartnerKind kind) =>
  530 + kind switch
  531 + {
  532 + LabelEntityPartnerKind.Type => schema.HasTypePartnerColumn,
  533 + LabelEntityPartnerKind.Category => schema.HasCategoryPartnerColumn,
  534 + LabelEntityPartnerKind.MultipleOption => schema.HasMultipleOptionPartnerColumn,
  535 + _ => false
  536 + };
  537 +
  538 + private static async Task<bool> ColumnExistsAsync(ISqlSugarClient db, string tableName, string columnName)
  539 + {
  540 + var count = await db.Ado.GetIntAsync(
  541 + """
  542 + SELECT COUNT(*)
  543 + FROM information_schema.COLUMNS
  544 + WHERE TABLE_SCHEMA = DATABASE()
  545 + AND TABLE_NAME = @tableName
  546 + AND COLUMN_NAME = @columnName
  547 + """,
  548 + new { tableName, columnName });
  549 + return count > 0;
  550 + }
  551 +
  552 + private static async Task<bool> TableExistsAsync(ISqlSugarClient db, string tableName)
  553 + {
  554 + var count = await db.Ado.GetIntAsync(
  555 + """
  556 + SELECT COUNT(*)
  557 + FROM information_schema.TABLES
  558 + WHERE TABLE_SCHEMA = DATABASE()
  559 + AND TABLE_NAME = @tableName
  560 + """,
  561 + new { tableName });
  562 + return count > 0;
  563 + }
  564 +
  565 + private static async Task SetAppliedPartnerTypeAsync(
  566 + ISqlSugarClient db,
  567 + LabelEntityPartnerKind kind,
  568 + string entityId,
  569 + string appliedPartnerType)
  570 + {
  571 + var schema = await GetSchemaStatusAsync(db);
  572 + if (!HasPartnerColumnForKind(schema, kind))
  573 + {
  574 + return;
  575 + }
  576 +
  577 + var tableName = kind switch
  578 + {
  579 + LabelEntityPartnerKind.Type => "fl_label_type",
  580 + LabelEntityPartnerKind.Category => "fl_label_category",
  581 + LabelEntityPartnerKind.MultipleOption => "fl_label_multiple_option",
  582 + _ => string.Empty
  583 + };
  584 +
  585 + if (string.IsNullOrEmpty(tableName))
  586 + {
  587 + return;
  588 + }
  589 +
  590 + await db.Ado.ExecuteCommandAsync(
  591 + $"UPDATE `{tableName}` SET AppliedPartnerType = @type WHERE Id = @id",
  592 + new { type = appliedPartnerType.Trim(), id = entityId.Trim() });
  593 + }
  594 +
  595 + private static async Task<string?> GetAppliedPartnerTypeAsync(
  596 + ISqlSugarClient db,
  597 + LabelEntityPartnerKind kind,
  598 + string entityId)
  599 + {
  600 + var schema = await GetSchemaStatusAsync(db);
  601 + if (!HasPartnerColumnForKind(schema, kind))
  602 + {
  603 + return null;
  604 + }
  605 +
  606 + var tableName = kind switch
  607 + {
  608 + LabelEntityPartnerKind.Type => "fl_label_type",
  609 + LabelEntityPartnerKind.Category => "fl_label_category",
  610 + LabelEntityPartnerKind.MultipleOption => "fl_label_multiple_option",
  611 + _ => string.Empty
  612 + };
  613 +
  614 + if (string.IsNullOrEmpty(tableName))
  615 + {
  616 + return null;
  617 + }
  618 +
  619 + var value = await db.Ado.GetScalarAsync(
  620 + $"SELECT AppliedPartnerType FROM `{tableName}` WHERE Id = @id LIMIT 1",
  621 + new { id = entityId.Trim() });
  622 +
  623 + return value?.ToString()?.Trim();
  624 + }
  625 +
  626 + private static async Task<List<(string EntityId, string PartnerId)>> LoadPartnerLinksAsync(
  627 + ISqlSugarClient db,
  628 + LabelEntityPartnerKind kind,
  629 + IReadOnlyList<string> entityIds)
  630 + {
  631 + switch (kind)
  632 + {
  633 + case LabelEntityPartnerKind.Type:
  634 + {
  635 + var rows = await db.Queryable<FlLabelTypePartnerDbEntity>()
  636 + .Where(x => entityIds.Contains(x.LabelTypeId))
  637 + .ToListAsync();
  638 + return rows.Select(x => (x.LabelTypeId, x.PartnerId)).ToList();
  639 + }
  640 + case LabelEntityPartnerKind.Category:
  641 + {
  642 + var rows = await db.Queryable<FlLabelCategoryPartnerDbEntity>()
  643 + .Where(x => entityIds.Contains(x.CategoryId))
  644 + .ToListAsync();
  645 + return rows.Select(x => (x.CategoryId, x.PartnerId)).ToList();
  646 + }
  647 + case LabelEntityPartnerKind.MultipleOption:
  648 + {
  649 + var rows = await db.Queryable<FlLabelMultipleOptionPartnerDbEntity>()
  650 + .Where(x => entityIds.Contains(x.MultipleOptionId))
  651 + .ToListAsync();
  652 + return rows.Select(x => (x.MultipleOptionId, x.PartnerId)).ToList();
  653 + }
  654 + default:
  655 + return new List<(string EntityId, string PartnerId)>();
  656 + }
  657 + }
  658 +
  659 + private static async Task InsertPartnerRowsAsync(
  660 + ISqlSugarClient db,
  661 + IGuidGenerator guidGenerator,
  662 + LabelEntityPartnerKind kind,
  663 + string entityId,
  664 + IReadOnlyList<string> partnerIds,
  665 + string? currentUserId,
  666 + DateTime now)
  667 + {
  668 + switch (kind)
  669 + {
  670 + case LabelEntityPartnerKind.Type:
  671 + {
  672 + var rows = partnerIds.Select(pid => new FlLabelTypePartnerDbEntity
  673 + {
  674 + Id = guidGenerator.Create().ToString(),
  675 + LabelTypeId = entityId,
  676 + PartnerId = pid,
  677 + CreationTime = now,
  678 + CreatorId = currentUserId
  679 + }).ToList();
  680 + await db.Insertable(rows).ExecuteCommandAsync();
  681 + break;
  682 + }
  683 + case LabelEntityPartnerKind.Category:
  684 + {
  685 + var rows = partnerIds.Select(pid => new FlLabelCategoryPartnerDbEntity
  686 + {
  687 + Id = guidGenerator.Create().ToString(),
  688 + CategoryId = entityId,
  689 + PartnerId = pid,
  690 + CreationTime = now,
  691 + CreatorId = currentUserId
  692 + }).ToList();
  693 + await db.Insertable(rows).ExecuteCommandAsync();
  694 + break;
  695 + }
  696 + case LabelEntityPartnerKind.MultipleOption:
  697 + {
  698 + var rows = partnerIds.Select(pid => new FlLabelMultipleOptionPartnerDbEntity
  699 + {
  700 + Id = guidGenerator.Create().ToString(),
  701 + MultipleOptionId = entityId,
  702 + PartnerId = pid,
  703 + CreationTime = now,
  704 + CreatorId = currentUserId
  705 + }).ToList();
  706 + await db.Insertable(rows).ExecuteCommandAsync();
  707 + break;
  708 + }
  709 + }
  710 + }
  711 +
  712 + private static async Task<Dictionary<string, string>> LoadPartnerNameMapAsync(
  713 + ISqlSugarClient db,
  714 + IReadOnlyList<string> partnerIds)
  715 + {
  716 + var result = new Dictionary<string, string>(StringComparer.Ordinal);
  717 + if (partnerIds.Count == 0)
  718 + {
  719 + return result;
  720 + }
  721 +
  722 + var partners = await db.Queryable<FlPartnerDbEntity>()
  723 + .Where(x => !x.IsDeleted && partnerIds.Contains(x.Id))
  724 + .ToListAsync();
  725 + foreach (var p in partners)
  726 + {
  727 + var name = p.PartnerName?.Trim();
  728 + result[p.Id] = string.IsNullOrEmpty(name) ? p.Id : name;
  729 + }
  730 +
  731 + return result;
  732 + }
  733 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs
... ... @@ -54,6 +54,61 @@ public static class LocationScopeBindingHelper
54 54 return locationIds.OrderBy(x => x, StringComparer.Ordinal).ToList();
55 55 }
56 56  
  57 + /// <summary>
  58 + /// App 可选门店 Id:Company Admin 展开为绑定 Company 下全部门店;其它角色为 <c>userlocation</c> 绑定。
  59 + /// </summary>
  60 + public static Task<List<string>> ResolveAppAccessibleLocationIdsForUserAsync(
  61 + ISqlSugarClient db,
  62 + Guid userId) =>
  63 + ResolveAppAccessibleLocationIdsForUserAsync(db, userId, null);
  64 +
  65 + /// <summary>
  66 + /// App 可选门店 Id;传入 <paramref name="currentUser"/> 时优先 JWT 声明识别 Company Admin,避免 ORM 查 Role 触发数据权限过滤器。
  67 + /// </summary>
  68 + public static async Task<List<string>> ResolveAppAccessibleLocationIdsForUserAsync(
  69 + ISqlSugarClient db,
  70 + Guid userId,
  71 + ICurrentUser? currentUser)
  72 + {
  73 + var locationIds = await ResolveBoundLocationIdsForUserAsync(db, userId);
  74 + if (locationIds.Count == 0)
  75 + {
  76 + return locationIds;
  77 + }
  78 +
  79 + var isCompanyAdmin = currentUser?.Id == userId
  80 + ? await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser)
  81 + : await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, userId);
  82 + if (!isCompanyAdmin)
  83 + {
  84 + return locationIds;
  85 + }
  86 +
  87 + var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds);
  88 + if (partnerIds.Count == 0)
  89 + {
  90 + return locationIds;
  91 + }
  92 +
  93 + var expanded = await ResolveLocationIdsFromPartnerIdsAsync(db, partnerIds);
  94 + if (expanded.Count == 0)
  95 + {
  96 + return locationIds;
  97 + }
  98 +
  99 + var merged = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  100 + foreach (var id in expanded)
  101 + {
  102 + var key = TeamMemberListScopeHelper.NormalizeScopeKey(id);
  103 + if (!string.IsNullOrEmpty(key))
  104 + {
  105 + merged.Add(key);
  106 + }
  107 + }
  108 +
  109 + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList();
  110 + }
  111 +
57 112 public static async Task<List<string>> ResolveDisplayRegionIdsForCurrentUserAsync(
58 113 ISqlSugarClient db,
59 114 ICurrentUser currentUser)
... ... @@ -63,12 +118,7 @@ public static class LocationScopeBindingHelper
63 118 return new List<string>();
64 119 }
65 120  
66   - var roleId = await db.Queryable<UserRoleEntity>()
67   - .Where(ur => ur.UserId == currentUser.Id.Value)
68   - .Select(ur => (Guid?)ur.RoleId)
69   - .FirstAsync();
70   -
71   - return await ResolveDisplayRegionIdsForUserAsync(db, currentUser, roleId);
  121 + return await ResolveDisplayRegionIdsForUserAsync(db, currentUser, null);
72 122 }
73 123  
74 124 /// <summary>
... ... @@ -93,9 +143,7 @@ public static class LocationScopeBindingHelper
93 143 var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds);
94 144 var regionIds = await ResolveGroupIdsFromLocationIdsAsync(db, locationIds);
95 145  
96   - var isCompanyAdmin = await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser)
97   - || await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(db, roleId);
98   - if (isCompanyAdmin && partnerIds.Count > 0)
  146 + if (await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser) && partnerIds.Count > 0)
99 147 {
100 148 regionIds = await ResolveGroupIdsFromPartnerIdsAsync(db, partnerIds);
101 149 }
... ... @@ -120,7 +168,7 @@ public static class LocationScopeBindingHelper
120 168 var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds);
121 169 var regionIds = await ResolveGroupIdsFromLocationIdsAsync(db, locationIds);
122 170  
123   - if (await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(db, roleId) && partnerIds.Count > 0)
  171 + if (await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, userId) && partnerIds.Count > 0)
124 172 {
125 173 regionIds = await ResolveGroupIdsFromPartnerIdsAsync(db, partnerIds);
126 174 }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsDateTimeDisplayHelper.cs
... ... @@ -409,9 +409,29 @@ public static class ReportsDateTimeDisplayHelper
409 409  
410 410  
411 411 return false;
  412 + }
412 413  
  414 + /// <summary>将已解析的到期时刻格式化为 Print Log 展示文案。</summary>
  415 + public static string FormatExpiryDateTime(DateTime dt)
  416 + {
  417 + if (dt == DateTime.MinValue)
  418 + {
  419 + return "无";
  420 + }
  421 +
  422 + return dt.ToString(DisplayFormat, CultureInfo.InvariantCulture);
413 423 }
414 424  
  425 + /// <summary>解析到期字面量(含 anchor 补全年份/时间)。</summary>
  426 + public static bool TryParseExpiryDateTime(string s, DateTime anchor, out DateTime dt)
  427 + {
  428 + if (TryParseToDateTime(s, out dt))
  429 + {
  430 + return true;
  431 + }
  432 +
  433 + return TryParsePartialDateTime(s, anchor, out dt);
  434 + }
415 435 }
416 436  
417 437  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsLocationScopeHelper.cs
... ... @@ -8,7 +8,7 @@ namespace FoodLabeling.Application.Helpers;
8 8  
9 9 /// <summary>
10 10 /// Reports 门店数据范围:管理员可查全部(可按 Company/Region/Location 入参收窄);
11   -/// 非管理员仅统计 <c>userlocation</c> 绑定门店的打印任务
  11 +/// Company Admin 为绑定 Company 下全部门店;其它非管理员为 <c>userlocation</c> 绑定门店
12 12 /// </summary>
13 13 public static class ReportsLocationScopeHelper
14 14 {
... ... @@ -16,7 +16,7 @@ public static class ReportsLocationScopeHelper
16 16 /// 解析报表统计适用的门店 Id 列表。
17 17 /// <list type="bullet">
18 18 /// <item>返回 <c>null</c>:不限制门店(管理员且未传 Company/Region/Location 筛选);</item>
19   - /// <item>返回空列表:无绑定门店或筛选后无交集;</item>
  19 + /// <item>返回空列表:无可访问门店或筛选后无交集;</item>
20 20 /// <item>返回非空列表:仅统计这些 <c>fl_label_print_task.LocationId</c>。</item>
21 21 /// </list>
22 22 /// </summary>
... ... @@ -39,22 +39,16 @@ public static class ReportsLocationScopeHelper
39 39 return new List<string>();
40 40 }
41 41  
42   - var userId = currentUser.Id.Value.ToString();
43   - var boundIds = LocationScopeBindingHelper.NormalizeIds(
44   - await db.Queryable<UserLocationDbEntity>()
45   - .Where(x => !x.IsDeleted && x.UserId == userId)
46   - .Select(x => x.LocationId)
47   - .Distinct()
48   - .ToListAsync());
49   -
50   - if (boundIds.Count == 0)
  42 + var accessibleIds = await LocationScopeBindingHelper.ResolveAppAccessibleLocationIdsForUserAsync(
  43 + db, currentUser.Id.Value, currentUser);
  44 + if (accessibleIds.Count == 0)
51 45 {
52 46 return new List<string>();
53 47 }
54 48  
55 49 if (filterIds is null)
56 50 {
57   - return boundIds;
  51 + return accessibleIds;
58 52 }
59 53  
60 54 if (filterIds.Count == 0)
... ... @@ -62,8 +56,8 @@ public static class ReportsLocationScopeHelper
62 56 return new List<string>();
63 57 }
64 58  
65   - var boundSet = new HashSet<string>(boundIds, StringComparer.OrdinalIgnoreCase);
66   - return filterIds.Where(id => boundSet.Contains(id)).ToList();
  59 + var accessibleSet = new HashSet<string>(accessibleIds, StringComparer.OrdinalIgnoreCase);
  60 + return filterIds.Where(id => accessibleSet.Contains(id)).ToList();
67 61 }
68 62  
69 63 /// <summary>
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs
... ... @@ -53,10 +53,10 @@ public static class ReportsPrintLogExpiryHelper
53 53 return ReportsDateTimeDisplayHelper.FormatExpiryDateTime(resolved);
54 54 }
55 55  
56   - var raw = ExtractExpiryText(printInputJson);
  56 + var raw = ExtractExpiryText(printInputJson, reference);
57 57 if (string.Equals(raw, "无", StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(renderTemplateJson))
58 58 {
59   - raw = ExtractExpiryText(renderTemplateJson);
  59 + raw = ExtractExpiryText(renderTemplateJson, reference);
60 60 }
61 61  
62 62 return ReportsDateTimeDisplayHelper.FormatExpiryDisplay(raw, reference);
... ... @@ -185,7 +185,7 @@ public static class ReportsPrintLogExpiryHelper
185 185 return false;
186 186 }
187 187  
188   - return ReportsDateTimeDisplayHelper.TryParseToDateTime(display.Trim(), reference, out result);
  188 + return ReportsDateTimeDisplayHelper.TryParseExpiryDateTime(display.Trim(), reference, out result);
189 189 }
190 190  
191 191 private static string? ResolveElementDisplayText(JsonElement el, DateTime reference)
... ... @@ -453,35 +453,6 @@ public static class ReportsPrintLogExpiryHelper
453 453 dt.Hour != 0 || dt.Minute != 0;
454 454  
455 455 /// <summary>
456   - /// 解析并格式化为 Print Log Expiration 列展示。
457   - /// </summary>
458   - public static string ExtractFormattedExpiryText(
459   - string? printInputJson,
460   - string? renderTemplateJson = null,
461   - DateTime? baseTime = null,
462   - DateTime? printedAt = null)
463   - {
464   - var anchor = ResolveAnchor(baseTime, printedAt, printInputJson);
465   - var raw = ExtractExpiryText(printInputJson, anchor);
466   - if (IsMissingExpiry(raw) && !string.IsNullOrWhiteSpace(renderTemplateJson))
467   - {
468   - raw = ExtractExpiryText(renderTemplateJson, anchor);
469   - }
470   -
471   - if (TryParseOffsetPayload(raw.Trim(), out var amount, out var unit))
472   - {
473   - raw = ApplyOffset(anchor, amount, unit)
474   - .ToString(ReportsDateTimeDisplayHelper.DisplayFormat, CultureInfo.InvariantCulture);
475   - }
476   -
477   - return ReportsDateTimeDisplayHelper.FormatExpiryDisplay(raw, anchor);
478   - }
479   -
480   - /// <summary>兼容旧调用:仅 PrintInputJson。</summary>
481   - public static string ExtractFormattedExpiryText(string? printInputJson) =>
482   - ExtractFormattedExpiryText(printInputJson, null, null, null);
483   -
484   - /// <summary>
485 456 /// 解析保质期展示文本;无法解析时返回「无」。
486 457 /// </summary>
487 458 public static string ExtractExpiryText(string? printInputJson, DateTime anchor)
... ... @@ -830,7 +801,7 @@ public static class ReportsPrintLogExpiryHelper
830 801 }
831 802  
832 803 var t = raw.Trim();
833   - if (!t.StartsWith('{', StringComparison.Ordinal))
  804 + if (!t.StartsWith("{"))
834 805 {
835 806 if (Regex.IsMatch(t, @"^\d+$"))
836 807 {
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberRoleHelper.cs
1 1 using SqlSugar;
2 2 using Volo.Abp.Users;
3   -using Yi.Framework.Rbac.Domain.Entities;
4 3 using Yi.Framework.Rbac.Domain.Shared.Consts;
5 4  
6 5 namespace FoodLabeling.Application.Helpers;
... ... @@ -55,12 +54,17 @@ public static class TeamMemberRoleHelper
55 54 return false;
56 55 }
57 56  
58   - var roleId = await db.Queryable<UserRoleEntity>()
59   - .Where(ur => ur.UserId == currentUser.Id.Value)
60   - .Select(ur => (Guid?)ur.RoleId)
61   - .FirstAsync();
  57 + return await IsCompanyAdminUserAsync(db, currentUser.Id.Value);
  58 + }
62 59  
63   - return await IsCompanyAdminRoleAsync(db, roleId);
  60 + /// <summary>
  61 + /// 指定用户是否为 Company Admin 类角色(按库内 Role 判定,用于登录等无 JWT 声明场景)。
  62 + /// </summary>
  63 + public static async Task<bool> IsCompanyAdminUserAsync(ISqlSugarClient db, Guid userId)
  64 + {
  65 + var roleRows = await LoadUserRoleRowsAsync(db, userId);
  66 + return roleRows.Any(r =>
  67 + IsCompanyAdminRoleName(r.RoleName) || IsCompanyAdminRoleToken(r.RoleCode));
64 68 }
65 69  
66 70 /// <summary>
... ... @@ -73,8 +77,16 @@ public static class TeamMemberRoleHelper
73 77 return false;
74 78 }
75 79  
76   - var role = await db.Queryable<RoleAggregateRoot>()
77   - .FirstAsync(x => !x.IsDeleted && x.Id == roleId.Value);
  80 + var roleRows = await db.Ado.SqlQueryAsync<UserRoleNameRow>(
  81 + """
  82 + SELECT RoleCode, RoleName
  83 + FROM Role
  84 + WHERE IsDeleted = 0 AND Id = @RoleId
  85 + LIMIT 1
  86 + """,
  87 + new { RoleId = roleId.Value });
  88 +
  89 + var role = roleRows.FirstOrDefault();
78 90 if (role is null)
79 91 {
80 92 return false;
... ... @@ -84,6 +96,19 @@ public static class TeamMemberRoleHelper
84 96 }
85 97  
86 98 /// <summary>
  99 + /// 读取用户角色(raw SQL,避免 RBAC 数据权限过滤器中 Select 表达式导致 SqlSugar 解析失败)。
  100 + /// </summary>
  101 + private static Task<List<UserRoleNameRow>> LoadUserRoleRowsAsync(ISqlSugarClient db, Guid userId) =>
  102 + db.Ado.SqlQueryAsync<UserRoleNameRow>(
  103 + """
  104 + SELECT r.RoleCode, r.RoleName
  105 + FROM UserRole ur
  106 + INNER JOIN Role r ON ur.RoleId = r.Id
  107 + WHERE ur.UserId = @UserId AND r.IsDeleted = 0 AND r.State = 1
  108 + """,
  109 + new { UserId = userId });
  110 +
  111 + /// <summary>
87 112 /// 列表/详情出参:Partner Admin → Company Admin(与 Web UI 一致)。
88 113 /// </summary>
89 114 public static string? FormatDisplayRoleName(string? roleName)
... ... @@ -148,4 +173,11 @@ public static class TeamMemberRoleHelper
148 173  
149 174 private static string NormalizeRoleKey(string value) =>
150 175 new string(value.Trim().ToLowerInvariant().Where(c => char.IsLetterOrDigit(c)).ToArray());
  176 +
  177 + private sealed class UserRoleNameRow
  178 + {
  179 + public string? RoleCode { get; set; }
  180 +
  181 + public string? RoleName { get; set; }
  182 + }
151 183 }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppPrintLogScopeHelper.cs
... ... @@ -13,7 +13,7 @@ namespace FoodLabeling.Application.Helpers;
13 13 public static class UsAppPrintLogScopeHelper
14 14 {
15 15 /// <summary>
16   - /// 管理员(<see cref="ReportsRoleHelper.IsAdminRole"/>)或角色码/名含 <c>partner</c>(忽略大小写,如 Partner Admin)
  16 + /// 管理员、Company Admin,或角色码/名含 <c>partner</c>(如 Partner Admin):可查看门店下全部打印记录
17 17 /// </summary>
18 18 public static async Task<bool> CanViewAllPrintsAtLocationAsync(
19 19 ICurrentUser currentUser,
... ... @@ -29,6 +29,11 @@ public static class UsAppPrintLogScopeHelper
29 29 return false;
30 30 }
31 31  
  32 + if (await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser))
  33 + {
  34 + return true;
  35 + }
  36 +
32 37 var roleRows = await db.Ado.SqlQueryAsync<UserRoleRow>(
33 38 @"SELECT r.RoleCode, r.RoleName
34 39 FROM UserRole ur
... ... @@ -41,30 +46,18 @@ public static class UsAppPrintLogScopeHelper
41 46 }
42 47  
43 48 /// <summary>
44   - /// 校验当前用户是否绑定指定门店
  49 + /// 校验当前用户是否可访问指定门店(含 Company Admin 公司下全部门店)
45 50 /// </summary>
46 51 public static async Task EnsureUserBoundToLocationAsync(
47 52 ICurrentUser currentUser,
48 53 ISqlSugarClient db,
49 54 string locationId)
50 55 {
51   - if (currentUser.Id is null)
52   - {
53   - throw new UserFriendlyException("用户未登录");
54   - }
55   -
56   - var lid = locationId.Trim();
57   - var userIdStr = currentUser.Id.Value.ToString();
58   - var bound = await db.Queryable<UserLocationDbEntity>()
59   - .AnyAsync(x => !x.IsDeleted && x.UserId == userIdStr && x.LocationId == lid);
60   - if (!bound)
61   - {
62   - throw new UserFriendlyException("当前账号未绑定该门店,无法查看");
63   - }
  56 + await EnsureUserCanAccessLocationAsync(currentUser, db, locationId);
64 57 }
65 58  
66 59 /// <summary>
67   - /// 校验是否可访问指定门店:平台管理员(<see cref="ReportsRoleHelper.IsAdminRole"/>)不校验 userlocation;其它账号须绑定。
  60 + /// 校验是否可访问指定门店:平台管理员不校验;Company Admin 可访问绑定 Company 下任意门店;其它账号须 userlocation 绑定。
68 61 /// </summary>
69 62 public static async Task EnsureUserCanAccessLocationAsync(
70 63 ICurrentUser currentUser,
... ... @@ -76,7 +69,23 @@ public static class UsAppPrintLogScopeHelper
76 69 return;
77 70 }
78 71  
79   - await EnsureUserBoundToLocationAsync(currentUser, db, locationId);
  72 + if (currentUser.Id is null)
  73 + {
  74 + throw new UserFriendlyException("用户未登录");
  75 + }
  76 +
  77 + var lid = TeamMemberListScopeHelper.NormalizeScopeKey(locationId);
  78 + if (string.IsNullOrEmpty(lid))
  79 + {
  80 + throw new UserFriendlyException("门店参数无效");
  81 + }
  82 +
  83 + var accessible = await LocationScopeBindingHelper.ResolveAppAccessibleLocationIdsForUserAsync(
  84 + db, currentUser.Id.Value, currentUser);
  85 + if (!accessible.Contains(lid, StringComparer.OrdinalIgnoreCase))
  86 + {
  87 + throw new UserFriendlyException("当前账号未绑定该门店,无法查看");
  88 + }
80 89 }
81 90  
82 91 /// <summary>
... ... @@ -107,10 +116,9 @@ public static class UsAppPrintLogScopeHelper
107 116 var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
108 117 var guids = userIdStrings
109 118 .Where(x => !string.IsNullOrWhiteSpace(x))
110   - .Select(x => x!.Trim())
111   - .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null)
112   - .Where(x => x.HasValue)
113   - .Select(x => x!.Value)
  119 + .Select(x => TeamMemberListScopeHelper.NormalizeScopeKey(x!))
  120 + .Where(x => Guid.TryParse(x, out _))
  121 + .Select(Guid.Parse)
114 122 .Distinct()
115 123 .ToList();
116 124 if (guids.Count == 0)
... ... @@ -118,19 +126,27 @@ public static class UsAppPrintLogScopeHelper
118 126 return map;
119 127 }
120 128  
121   - var users = await db.Queryable<UserAggregateRoot>()
122   - .Where(u => !u.IsDeleted && guids.Contains(u.Id))
123   - .Select(u => new { u.Id, u.Name, u.Nick, u.UserName })
124   - .ToListAsync();
  129 + var parameters = new List<SugarParameter>(guids.Count);
  130 + var inClause = new List<string>(guids.Count);
  131 + for (var i = 0; i < guids.Count; i++)
  132 + {
  133 + var paramName = $"@uid{i}";
  134 + inClause.Add(paramName);
  135 + parameters.Add(new SugarParameter(paramName, guids[i]));
  136 + }
  137 +
  138 + var users = await db.Ado.SqlQueryAsync<OperatorUserRow>(
  139 + $"""
  140 + SELECT Id, UserName, Name, Nick
  141 + FROM User
  142 + WHERE IsDeleted = 0 AND Id IN ({string.Join(", ", inClause)})
  143 + """,
  144 + parameters);
125 145  
126 146 foreach (var u in users)
127 147 {
128   - var display = !string.IsNullOrWhiteSpace(u.Name)
129   - ? u.Name!.Trim()
130   - : !string.IsNullOrWhiteSpace(u.Nick)
131   - ? u.Nick!.Trim()
132   - : u.UserName?.Trim() ?? string.Empty;
133   - map[u.Id.ToString()] = string.IsNullOrWhiteSpace(display) ? "无" : display;
  148 + var display = BuildOperatorDisplayName(u.Name, u.Nick, u.UserName);
  149 + map[TeamMemberListScopeHelper.UserKey(u.Id)] = display;
134 150 }
135 151  
136 152 return map;
... ... @@ -143,7 +159,35 @@ public static class UsAppPrintLogScopeHelper
143 159 return "无";
144 160 }
145 161  
146   - return map.TryGetValue(createdBy.Trim(), out var name) ? name : "无";
  162 + var key = TeamMemberListScopeHelper.NormalizeScopeKey(createdBy);
  163 + return map.TryGetValue(key, out var name) ? name : "无";
  164 + }
  165 +
  166 + private static string BuildOperatorDisplayName(string? name, string? nick, string? userName)
  167 + {
  168 + if (!string.IsNullOrWhiteSpace(name))
  169 + {
  170 + return name.Trim();
  171 + }
  172 +
  173 + if (!string.IsNullOrWhiteSpace(nick))
  174 + {
  175 + return nick.Trim();
  176 + }
  177 +
  178 + var user = userName?.Trim();
  179 + return string.IsNullOrWhiteSpace(user) ? "无" : user;
  180 + }
  181 +
  182 + private sealed class OperatorUserRow
  183 + {
  184 + public Guid Id { get; set; }
  185 +
  186 + public string? UserName { get; set; }
  187 +
  188 + public string? Name { get; set; }
  189 +
  190 + public string? Nick { get; set; }
147 191 }
148 192  
149 193 private static bool ContainsPartnerKeyword(string? value) =>
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryPartnerDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +[SugarTable("fl_label_category_partner")]
  6 +public class FlLabelCategoryPartnerDbEntity
  7 +{
  8 + [SugarColumn(IsPrimaryKey = true)]
  9 + public string Id { get; set; } = string.Empty;
  10 +
  11 + public string CategoryId { get; set; } = string.Empty;
  12 +
  13 + public string PartnerId { get; set; } = string.Empty;
  14 +
  15 + public DateTime CreationTime { get; set; }
  16 +
  17 + public string? CreatorId { get; set; }
  18 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelMultipleOptionPartnerDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +[SugarTable("fl_label_multiple_option_partner")]
  6 +public class FlLabelMultipleOptionPartnerDbEntity
  7 +{
  8 + [SugarColumn(IsPrimaryKey = true)]
  9 + public string Id { get; set; } = string.Empty;
  10 +
  11 + public string MultipleOptionId { get; set; } = string.Empty;
  12 +
  13 + public string PartnerId { get; set; } = string.Empty;
  14 +
  15 + public DateTime CreationTime { get; set; }
  16 +
  17 + public string? CreatorId { get; set; }
  18 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTypePartnerDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +[SugarTable("fl_label_type_partner")]
  6 +public class FlLabelTypePartnerDbEntity
  7 +{
  8 + [SugarColumn(IsPrimaryKey = true)]
  9 + public string Id { get; set; } = string.Empty;
  10 +
  11 + public string LabelTypeId { get; set; } = string.Empty;
  12 +
  13 + public string PartnerId { get; set; } = string.Empty;
  14 +
  15 + public DateTime CreationTime { get; set; }
  16 +
  17 + public string? CreatorId { get; set; }
  18 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs
... ... @@ -151,12 +151,15 @@ public class LabelAppService : ApplicationService, ILabelAppService
151 151 };
152 152 }
153 153  
154   - // 查询标签基础信息(分类/类型/模板)
  154 + // 查询标签基础信息(分类/类型/模板);类型 LeftJoin,允许未绑定 labelTypeId 的标签出现在列表
155 155 var labelRows = await _dbContext.SqlSugarClient
156   - .Queryable<FlLabelDbEntity, FlLabelCategoryDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity>(
157   - (l, c, t, tpl) => l.LabelCategoryId == c.Id && l.LabelTypeId == t.Id && l.TemplateId == tpl.Id)
  156 + .Queryable<FlLabelDbEntity>()
  157 + .InnerJoin<FlLabelCategoryDbEntity>((l, c) => l.LabelCategoryId == c.Id)
  158 + .LeftJoin<FlLabelTypeDbEntity>((l, c, t) => l.LabelTypeId == t.Id)
  159 + .InnerJoin<FlLabelTemplateDbEntity>((l, c, t, tpl) => l.TemplateId == tpl.Id)
158 160 .Where((l, c, t, tpl) => pageLabelIds.Contains(l.Id))
159   - .Where((l, c, t, tpl) => !l.IsDeleted && !c.IsDeleted && !t.IsDeleted && !tpl.IsDeleted)
  161 + .Where((l, c, t, tpl) => !l.IsDeleted && !c.IsDeleted && !tpl.IsDeleted)
  162 + .Where((l, c, t, tpl) => t.Id == null || !t.IsDeleted)
160 163 .Select((l, c, t, tpl) => new
161 164 {
162 165 l.Id,
... ... @@ -248,7 +251,7 @@ public class LabelAppService : ApplicationService, ILabelAppService
248 251 Products = products,
249 252 TemplateName = x.TemplateName ?? string.Empty,
250 253 TemplateCode = x.TemplateCode ?? string.Empty,
251   - LabelTypeName = x.LabelTypeName ?? string.Empty,
  254 + LabelTypeName = string.IsNullOrWhiteSpace(x.LabelTypeName) ? "无" : x.LabelTypeName,
252 255 State = x.State,
253 256 LastEdited = x.LastEdited,
254 257 HasError = false
... ... @@ -289,8 +292,12 @@ public class LabelAppService : ApplicationService, ILabelAppService
289 292 .FirstAsync(x => x.Id == label.TemplateId);
290 293 var category = await db.Queryable<FlLabelCategoryDbEntity>()
291 294 .FirstAsync(x => x.Id == label.LabelCategoryId);
292   - var type = await db.Queryable<FlLabelTypeDbEntity>()
293   - .FirstAsync(x => x.Id == label.LabelTypeId);
  295 + FlLabelTypeDbEntity? type = null;
  296 + if (!string.IsNullOrWhiteSpace(label.LabelTypeId))
  297 + {
  298 + type = await db.Queryable<FlLabelTypeDbEntity>()
  299 + .FirstAsync(x => x.Id == label.LabelTypeId);
  300 + }
294 301  
295 302 var productIds = await db.Queryable<FlLabelProductDbEntity>()
296 303 .Where(x => x.LabelId == label.Id)
... ... @@ -368,10 +375,8 @@ public class LabelAppService : ApplicationService, ILabelAppService
368 375 {
369 376 throw new UserFriendlyException("标签分类Id不能为空");
370 377 }
371   - if (string.IsNullOrWhiteSpace(input.LabelTypeId))
372   - {
373   - throw new UserFriendlyException("标签类型Id不能为空");
374   - }
  378 +
  379 + await EnsureLabelTypeExistsIfProvidedAsync(input.LabelTypeId);
375 380  
376 381 var scope = await LabelRegionScopeHelper.ResolveScopeForSaveAsync(
377 382 _dbContext.SqlSugarClient,
... ... @@ -413,7 +418,7 @@ public class LabelAppService : ApplicationService, ILabelAppService
413 418 TemplateId = template.Id,
414 419 LocationId = scope.PrimaryLocationId,
415 420 LabelCategoryId = input.LabelCategoryId?.Trim(),
416   - LabelTypeId = input.LabelTypeId?.Trim(),
  421 + LabelTypeId = NormalizeOptionalLabelTypeId(input.LabelTypeId),
417 422 State = input.State,
418 423 LabelType = null,
419 424 LabelInfoJson = input.LabelInfoJson == null ? null : JsonSerializer.Serialize(input.LabelInfoJson)
... ... @@ -484,10 +489,8 @@ public class LabelAppService : ApplicationService, ILabelAppService
484 489 {
485 490 throw new UserFriendlyException("标签分类Id不能为空");
486 491 }
487   - if (string.IsNullOrWhiteSpace(input.LabelTypeId))
488   - {
489   - throw new UserFriendlyException("标签类型Id不能为空");
490   - }
  492 +
  493 + await EnsureLabelTypeExistsIfProvidedAsync(input.LabelTypeId);
491 494  
492 495 var scope = await LabelRegionScopeHelper.ResolveScopeForSaveAsync(
493 496 _dbContext.SqlSugarClient,
... ... @@ -510,7 +513,7 @@ public class LabelAppService : ApplicationService, ILabelAppService
510 513 label.TemplateId = template.Id;
511 514 label.LocationId = scope.PrimaryLocationId;
512 515 label.LabelCategoryId = input.LabelCategoryId?.Trim();
513   - label.LabelTypeId = input.LabelTypeId?.Trim();
  516 + label.LabelTypeId = NormalizeOptionalLabelTypeId(input.LabelTypeId);
514 517 label.State = input.State;
515 518 label.LastModifierId = CurrentUser?.Id?.ToString();
516 519 label.LastModificationTime = now;
... ... @@ -873,5 +876,27 @@ public class LabelAppService : ApplicationService, ILabelAppService
873 876 var v = (raw ?? string.Empty).Trim().ToLowerInvariant();
874 877 return v is "line" or "dotted" ? v : "none";
875 878 }
  879 +
  880 + private static string? NormalizeOptionalLabelTypeId(string? labelTypeId)
  881 + {
  882 + var id = labelTypeId?.Trim();
  883 + return string.IsNullOrWhiteSpace(id) ? null : id;
  884 + }
  885 +
  886 + private async Task EnsureLabelTypeExistsIfProvidedAsync(string? labelTypeId)
  887 + {
  888 + var id = NormalizeOptionalLabelTypeId(labelTypeId);
  889 + if (id is null)
  890 + {
  891 + return;
  892 + }
  893 +
  894 + var exists = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>()
  895 + .AnyAsync(x => !x.IsDeleted && x.Id == id);
  896 + if (!exists)
  897 + {
  898 + throw new UserFriendlyException("标签类型不存在");
  899 + }
  900 + }
876 901 }
877 902  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelCategoryAppService.cs
... ... @@ -35,7 +35,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
35 35 (x.DisplayText != null && x.DisplayText.Contains(keyword!)))
36 36 .WhereIF(input.State != null, x => x.State == input.State);
37 37  
38   - query = await ApplyCategoryScopeFilterAsync(query, input.GroupId, input.LocationId);
  38 + query = await ApplyCategoryScopeFilterAsync(query, input.PartnerId, input.GroupId, input.LocationId);
39 39  
40 40 // Sorting 仅允许白名单字段,避免 Unknown column/注入风险
41 41 if (!string.IsNullOrWhiteSpace(input.Sorting))
... ... @@ -72,10 +72,15 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
72 72  
73 73 var labelStatsMap = await BuildCategoryLabelStatsMapAsync(ids);
74 74 var scopeMap = await BuildCategoryScopeMapAsync(entities);
  75 + var partnerScopeMap = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync(
  76 + _dbContext.SqlSugarClient,
  77 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Category,
  78 + ids);
75 79  
76 80 var items = entities.Select(x =>
77 81 {
78 82 scopeMap.TryGetValue(x.Id, out var scope);
  83 + partnerScopeMap.TryGetValue(x.Id, out var partnerScope);
79 84 labelStatsMap.TryGetValue(x.Id, out var labelStats);
80 85 var creationTime = ResolveCategoryCreationTime(x);
81 86 return new LabelCategoryGetListOutputDto
... ... @@ -88,6 +93,10 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
88 93 State = x.State,
89 94 ButtonAppearance = x.ButtonAppearance,
90 95 AvailabilityType = x.AvailabilityType,
  96 + AppliedPartnerType = partnerScope?.AppliedPartnerType ?? LabelEntityPartnerScopeHelper.ScopeAll,
  97 + Company = partnerScope?.Company ?? LabelEntityPartnerScopeHelper.AllCompaniesDisplay,
  98 + PartnerIds = partnerScope?.PartnerIds ?? new List<string>(),
  99 + CompanyIds = partnerScope?.PartnerIds ?? new List<string>(),
91 100 OrderNum = x.OrderNum,
92 101 NoOfLabels = labelStats?.Count ?? 0,
93 102 CreationTime = creationTime,
... ... @@ -112,6 +121,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
112 121 }
113 122  
114 123 var dto = MapToGetOutput(entity);
  124 + await ApplyPartnerScopeToGetOutputAsync(dto, entity.Id);
115 125 if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))
116 126 {
117 127 var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryLocationDbEntity>()
... ... @@ -139,7 +149,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
139 149  
140 150 var displayText = input.DisplayText?.Trim();
141 151 var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance);
142   - var (availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input);
  152 + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input);
143 153  
144 154 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
145 155 .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name));
... ... @@ -170,6 +180,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
170 180 };
171 181  
172 182 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  183 + await SaveCategoryPartnerScopeAsync(entity.Id, partnerScope, currentUserId, now);
173 184 await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now);
174 185 return await GetAsync(entity.Id);
175 186 }
... ... @@ -192,7 +203,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
192 203  
193 204 var displayText = input.DisplayText?.Trim();
194 205 var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance);
195   - var (availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input);
  206 + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input);
196 207  
197 208 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
198 209 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name));
... ... @@ -213,6 +224,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
213 224 entity.LastModifierId = CurrentUser?.Id?.ToString();
214 225  
215 226 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  227 + await SaveCategoryPartnerScopeAsync(entity.Id, partnerScope, entity.LastModifierId,
  228 + entity.LastModificationTime ?? DateTime.Now);
216 229 await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId,
217 230 entity.LastModificationTime ?? DateTime.Now);
218 231 return await GetAsync(id);
... ... @@ -234,6 +247,14 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
234 247 throw new UserFriendlyException("该标签分类已被标签引用,无法删除");
235 248 }
236 249  
  250 + await LabelEntityPartnerScopeHelper.DeletePartnerScopeRowsAsync(
  251 + _dbContext.SqlSugarClient,
  252 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Category,
  253 + id);
  254 + await _dbContext.SqlSugarClient.Deleteable<FlLabelCategoryLocationDbEntity>()
  255 + .Where(x => x.CategoryId == id)
  256 + .ExecuteCommandAsync();
  257 +
237 258 entity.IsDeleted = true;
238 259 entity.LastModificationTime = DateTime.Now;
239 260 entity.LastModifierId = CurrentUser?.Id?.ToString();
... ... @@ -256,9 +277,15 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
256 277 };
257 278 }
258 279  
259   - private async Task<(string AvailabilityType, List<string> LocationIds)> ResolveCategoryScopeForSaveAsync(
  280 + private async Task<(LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult PartnerScope, string AvailabilityType, List<string> LocationIds)> ResolveCategoryScopeForSaveAsync(
260 281 LabelCategoryCreateInputVo input)
261 282 {
  283 + var partnerScope = await LabelEntityPartnerScopeHelper.ResolvePartnerScopeForSaveAsync(
  284 + _dbContext.SqlSugarClient,
  285 + input.AppliedPartnerType,
  286 + input.PartnerIds,
  287 + input.CompanyIds);
  288 +
262 289 var regionIds = NormalizeRegionIds(input);
263 290 var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds);
264 291 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
... ... @@ -278,20 +305,37 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
278 305 throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)");
279 306 }
280 307  
281   - if (availabilityType == "ALL")
  308 + var partnerSpecified = string.Equals(
  309 + partnerScope.AppliedPartnerType,
  310 + LabelEntityPartnerScopeHelper.ScopeSpecified,
  311 + StringComparison.OrdinalIgnoreCase);
  312 + var locationSpecified = string.Equals(availabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase);
  313 +
  314 + if (partnerSpecified || locationSpecified)
282 315 {
283   - return ("ALL", new List<string>());
  316 + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
  317 + _dbContext.SqlSugarClient,
  318 + partnerSpecified ? partnerScope.PartnerIds : null,
  319 + locationSpecified ? regionIds : null,
  320 + locationSpecified ? explicitLocationIds : null);
  321 + if (merged.Count == 0)
  322 + {
  323 + throw new UserFriendlyException("指定适用范围后至少需要匹配到一个有效门店");
  324 + }
  325 +
  326 + if (locationSpecified)
  327 + {
  328 + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged);
  329 + return (partnerScope, "SPECIFIED", merged);
  330 + }
284 331 }
285 332  
286   - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
287   - _dbContext.SqlSugarClient, (IReadOnlyList<string>?)null, regionIds, explicitLocationIds);
288   - if (merged.Count == 0)
  333 + if (string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase))
289 334 {
290   - throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店");
  335 + return (partnerScope, "ALL", new List<string>());
291 336 }
292 337  
293   - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged);
294   - return ("SPECIFIED", merged);
  338 + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店");
295 339 }
296 340  
297 341 private static List<string> NormalizeRegionIds(LabelCategoryCreateInputVo input)
... ... @@ -312,17 +356,24 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
312 356  
313 357 private async Task<ISugarQueryable<FlLabelCategoryDbEntity>> ApplyCategoryScopeFilterAsync(
314 358 ISugarQueryable<FlLabelCategoryDbEntity> query,
  359 + string? partnerId,
315 360 string? groupId,
316 361 string? locationId)
317 362 {
318   - var scopeLocIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync(
319   - _dbContext.SqlSugarClient, groupId, locationId);
320   - if (scopeLocIds is null)
  363 + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync(
  364 + CurrentUser, _dbContext, partnerId, groupId, locationId);
  365 + var scopedPartnerIds = await LabelEntityPartnerScopeHelper.ResolveListPartnerIdsAsync(
  366 + _dbContext.SqlSugarClient, partnerId, scopedLocationIds);
  367 +
  368 + query = await LabelEntityPartnerScopeHelper.ApplyCategoryPartnerListFilterAsync(
  369 + _dbContext.SqlSugarClient, query, scopedPartnerIds);
  370 +
  371 + if (scopedLocationIds is null)
321 372 {
322 373 return query;
323 374 }
324 375  
325   - if (scopeLocIds.Count == 0)
  376 + if (scopedLocationIds.Count == 0)
326 377 {
327 378 return query.Where(c => c.AvailabilityType == "ALL");
328 379 }
... ... @@ -330,10 +381,43 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
330 381 return query.Where(c =>
331 382 c.AvailabilityType == "ALL" ||
332 383 SqlFunc.Subqueryable<FlLabelCategoryLocationDbEntity>()
333   - .Where(cl => cl.CategoryId == c.Id && scopeLocIds.Contains(cl.LocationId))
  384 + .Where(cl => cl.CategoryId == c.Id && scopedLocationIds.Contains(cl.LocationId))
334 385 .Any());
335 386 }
336 387  
  388 + private async Task SaveCategoryPartnerScopeAsync(
  389 + string categoryId,
  390 + LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult partnerScope,
  391 + string? currentUserId,
  392 + DateTime now)
  393 + {
  394 + await LabelEntityPartnerScopeHelper.SavePartnerScopeAsync(
  395 + _dbContext.SqlSugarClient,
  396 + _guidGenerator,
  397 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Category,
  398 + categoryId,
  399 + partnerScope,
  400 + currentUserId,
  401 + now);
  402 + }
  403 +
  404 + private async Task ApplyPartnerScopeToGetOutputAsync(LabelCategoryGetOutputDto dto, string entityId)
  405 + {
  406 + var map = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync(
  407 + _dbContext.SqlSugarClient,
  408 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Category,
  409 + new[] { entityId });
  410 + if (!map.TryGetValue(entityId, out var scope))
  411 + {
  412 + return;
  413 + }
  414 +
  415 + dto.AppliedPartnerType = scope.AppliedPartnerType;
  416 + dto.Company = scope.Company;
  417 + dto.PartnerIds = scope.PartnerIds;
  418 + dto.CompanyIds = scope.PartnerIds;
  419 + }
  420 +
337 421 private const string AllRegionsDisplay = "All Regions";
338 422 private const string AllLocationsDisplay = "All Locations";
339 423 private const string EmptyDisplay = "无";
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelMultipleOptionAppService.cs
... ... @@ -27,14 +27,22 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
27 27 {
28 28 RefAsync<int> total = 0;
29 29 var keyword = input.Keyword?.Trim();
30   - var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync(
31   - _dbContext.SqlSugarClient, input.GroupId, input.LocationId);
  30 + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync(
  31 + CurrentUser,
  32 + _dbContext,
  33 + input.PartnerId,
  34 + input.GroupId,
  35 + input.LocationId);
  36 + var scopedPartnerIds = await LabelEntityPartnerScopeHelper.ResolveListPartnerIdsAsync(
  37 + _dbContext.SqlSugarClient, input.PartnerId, scopedLocationIds);
32 38  
33 39 var query = _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionDbEntity>()
34 40 .Where(x => !x.IsDeleted)
35 41 .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.OptionCode.Contains(keyword!) || x.OptionName.Contains(keyword!))
36 42 .WhereIF(input.State != null, x => x.State == input.State);
37 43  
  44 + query = await LabelEntityPartnerScopeHelper.ApplyMultipleOptionPartnerListFilterAsync(
  45 + _dbContext.SqlSugarClient, query, scopedPartnerIds);
38 46 query = ApplyMultipleOptionScopeFilter(query, scopedLocationIds);
39 47  
40 48 if (!string.IsNullOrWhiteSpace(input.Sorting))
... ... @@ -48,10 +56,15 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
48 56  
49 57 var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
50 58 var scopeMap = await BuildMultipleOptionScopeMapAsync(entities);
  59 + var partnerScopeMap = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync(
  60 + _dbContext.SqlSugarClient,
  61 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.MultipleOption,
  62 + entities.Select(x => x.Id).ToList());
51 63  
52 64 var items = entities.Select(x =>
53 65 {
54 66 scopeMap.TryGetValue(x.Id, out var scope);
  67 + partnerScopeMap.TryGetValue(x.Id, out var partnerScope);
55 68 return new LabelMultipleOptionGetListOutputDto
56 69 {
57 70 Id = x.Id,
... ... @@ -60,6 +73,10 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
60 73 OptionValuesJson = x.OptionValuesJson,
61 74 State = x.State,
62 75 AvailabilityType = x.AvailabilityType,
  76 + AppliedPartnerType = partnerScope?.AppliedPartnerType ?? LabelEntityPartnerScopeHelper.ScopeAll,
  77 + Company = partnerScope?.Company ?? LabelEntityPartnerScopeHelper.AllCompaniesDisplay,
  78 + PartnerIds = partnerScope?.PartnerIds ?? new List<string>(),
  79 + CompanyIds = partnerScope?.PartnerIds ?? new List<string>(),
63 80 OrderNum = x.OrderNum,
64 81 LastEdited = x.LastModificationTime ?? x.CreationTime,
65 82 Region = scope?.Region ?? EmptyDisplay,
... ... @@ -82,6 +99,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
82 99 }
83 100  
84 101 var dto = MapToGetOutput(entity);
  102 + await ApplyPartnerScopeToGetOutputAsync(dto, entity.Id);
85 103 if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))
86 104 {
87 105 var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionLocationDbEntity>()
... ... @@ -107,7 +125,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
107 125 throw new UserFriendlyException("多选项名称不能为空");
108 126 }
109 127  
110   - var (availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input);
  128 + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input);
111 129  
112 130 if (await IsMultipleOptionDuplicatedAsync(code, name, excludeId: null))
113 131 {
... ... @@ -134,6 +152,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
134 152 };
135 153  
136 154 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  155 + await SaveMultipleOptionPartnerScopeAsync(entity.Id, partnerScope, currentUserId, now);
137 156 await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now);
138 157 return await GetAsync(entity.Id);
139 158 }
... ... @@ -154,7 +173,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
154 173 throw new UserFriendlyException("多选项名称不能为空");
155 174 }
156 175  
157   - var (availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input);
  176 + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input);
158 177  
159 178 if (await IsMultipleOptionDuplicatedAsync(code, name, excludeId: id))
160 179 {
... ... @@ -171,6 +190,8 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
171 190 entity.LastModifierId = CurrentUser?.Id?.ToString();
172 191  
173 192 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  193 + await SaveMultipleOptionPartnerScopeAsync(entity.Id, partnerScope, entity.LastModifierId,
  194 + entity.LastModificationTime ?? DateTime.Now);
174 195 await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId,
175 196 entity.LastModificationTime ?? DateTime.Now);
176 197 return await GetAsync(id);
... ... @@ -185,6 +206,10 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
185 206 return;
186 207 }
187 208  
  209 + await LabelEntityPartnerScopeHelper.DeletePartnerScopeRowsAsync(
  210 + _dbContext.SqlSugarClient,
  211 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.MultipleOption,
  212 + id);
188 213 await _dbContext.SqlSugarClient.Deleteable<FlLabelMultipleOptionLocationDbEntity>()
189 214 .Where(x => x.MultipleOptionId == id)
190 215 .ExecuteCommandAsync();
... ... @@ -199,9 +224,15 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
199 224 private const string AllRegionsDisplay = "All Regions";
200 225 private const string AllLocationsDisplay = "All Locations";
201 226  
202   - private async Task<(string AvailabilityType, List<string> LocationIds)> ResolveMultipleOptionScopeForSaveAsync(
  227 + private async Task<(LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult PartnerScope, string AvailabilityType, List<string> LocationIds)> ResolveMultipleOptionScopeForSaveAsync(
203 228 LabelMultipleOptionCreateInputVo input)
204 229 {
  230 + var partnerScope = await LabelEntityPartnerScopeHelper.ResolvePartnerScopeForSaveAsync(
  231 + _dbContext.SqlSugarClient,
  232 + input.AppliedPartnerType,
  233 + input.PartnerIds,
  234 + input.CompanyIds);
  235 +
205 236 var regionIds = NormalizeRegionIds(input);
206 237 var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds);
207 238 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
... ... @@ -221,20 +252,70 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
221 252 throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)");
222 253 }
223 254  
224   - if (availabilityType == "ALL")
  255 + var partnerSpecified = string.Equals(
  256 + partnerScope.AppliedPartnerType,
  257 + LabelEntityPartnerScopeHelper.ScopeSpecified,
  258 + StringComparison.OrdinalIgnoreCase);
  259 + var locationSpecified = string.Equals(availabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase);
  260 +
  261 + if (partnerSpecified || locationSpecified)
225 262 {
226   - return ("ALL", new List<string>());
  263 + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
  264 + _dbContext.SqlSugarClient,
  265 + partnerSpecified ? partnerScope.PartnerIds : null,
  266 + locationSpecified ? regionIds : null,
  267 + locationSpecified ? explicitLocationIds : null);
  268 + if (merged.Count == 0)
  269 + {
  270 + throw new UserFriendlyException("指定适用范围后至少需要匹配到一个有效门店");
  271 + }
  272 +
  273 + if (locationSpecified)
  274 + {
  275 + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged);
  276 + return (partnerScope, "SPECIFIED", merged);
  277 + }
227 278 }
228 279  
229   - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
230   - _dbContext.SqlSugarClient, (IReadOnlyList<string>?)null, regionIds, explicitLocationIds);
231   - if (merged.Count == 0)
  280 + if (string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase))
232 281 {
233   - throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店");
  282 + return (partnerScope, "ALL", new List<string>());
  283 + }
  284 +
  285 + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店");
  286 + }
  287 +
  288 + private async Task SaveMultipleOptionPartnerScopeAsync(
  289 + string multipleOptionId,
  290 + LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult partnerScope,
  291 + string? currentUserId,
  292 + DateTime now)
  293 + {
  294 + await LabelEntityPartnerScopeHelper.SavePartnerScopeAsync(
  295 + _dbContext.SqlSugarClient,
  296 + _guidGenerator,
  297 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.MultipleOption,
  298 + multipleOptionId,
  299 + partnerScope,
  300 + currentUserId,
  301 + now);
  302 + }
  303 +
  304 + private async Task ApplyPartnerScopeToGetOutputAsync(LabelMultipleOptionGetOutputDto dto, string entityId)
  305 + {
  306 + var map = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync(
  307 + _dbContext.SqlSugarClient,
  308 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.MultipleOption,
  309 + new[] { entityId });
  310 + if (!map.TryGetValue(entityId, out var scope))
  311 + {
  312 + return;
234 313 }
235 314  
236   - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged);
237   - return ("SPECIFIED", merged);
  315 + dto.AppliedPartnerType = scope.AppliedPartnerType;
  316 + dto.Company = scope.Company;
  317 + dto.PartnerIds = scope.PartnerIds;
  318 + dto.CompanyIds = scope.PartnerIds;
238 319 }
239 320  
240 321 private static List<string> NormalizeRegionIds(LabelMultipleOptionCreateInputVo input)
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTypeAppService.cs
... ... @@ -27,14 +27,22 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
27 27 {
28 28 RefAsync<int> total = 0;
29 29 var keyword = input.Keyword?.Trim();
30   - var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync(
31   - _dbContext.SqlSugarClient, input.GroupId, input.LocationId);
  30 + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync(
  31 + CurrentUser,
  32 + _dbContext,
  33 + input.PartnerId,
  34 + input.GroupId,
  35 + input.LocationId);
  36 + var scopedPartnerIds = await LabelEntityPartnerScopeHelper.ResolveListPartnerIdsAsync(
  37 + _dbContext.SqlSugarClient, input.PartnerId, scopedLocationIds);
32 38  
33 39 var query = _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>()
34 40 .Where(x => !x.IsDeleted)
35 41 .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.TypeCode.Contains(keyword!) || x.TypeName.Contains(keyword!))
36 42 .WhereIF(input.State != null, x => x.State == input.State);
37 43  
  44 + query = await LabelEntityPartnerScopeHelper.ApplyTypePartnerListFilterAsync(
  45 + _dbContext.SqlSugarClient, query, scopedPartnerIds);
38 46 query = ApplyLabelTypeScopeFilter(query, scopedLocationIds);
39 47  
40 48 if (!string.IsNullOrWhiteSpace(input.Sorting))
... ... @@ -51,10 +59,15 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
51 59  
52 60 var countMap = await BuildTypeLabelStatsMapAsync(ids, scopedLocationIds);
53 61 var scopeMap = await BuildTypeConfiguredScopeMapAsync(entities);
  62 + var partnerScopeMap = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync(
  63 + _dbContext.SqlSugarClient,
  64 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Type,
  65 + ids);
54 66  
55 67 var items = entities.Select(x =>
56 68 {
57 69 scopeMap.TryGetValue(x.Id, out var scope);
  70 + partnerScopeMap.TryGetValue(x.Id, out var partnerScope);
58 71 countMap.TryGetValue(x.Id, out var stats);
59 72 var locationDisplay = scope?.Location ?? EmptyDisplay;
60 73 return new LabelTypeGetListOutputDto
... ... @@ -64,6 +77,10 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
64 77 TypeName = x.TypeName,
65 78 State = x.State,
66 79 AvailabilityType = x.AvailabilityType,
  80 + AppliedPartnerType = partnerScope?.AppliedPartnerType ?? LabelEntityPartnerScopeHelper.ScopeAll,
  81 + Company = partnerScope?.Company ?? LabelEntityPartnerScopeHelper.AllCompaniesDisplay,
  82 + PartnerIds = partnerScope?.PartnerIds ?? new List<string>(),
  83 + CompanyIds = partnerScope?.PartnerIds ?? new List<string>(),
67 84 OrderNum = x.OrderNum,
68 85 NoOfLabels = stats?.Count ?? 0,
69 86 LastEdited = stats?.MaxEdited ?? x.LastModificationTime ?? x.CreationTime,
... ... @@ -88,6 +105,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
88 105 }
89 106  
90 107 var dto = MapToGetOutput(entity);
  108 + await ApplyPartnerScopeToGetOutputAsync(dto, entity.Id);
91 109 if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))
92 110 {
93 111 var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeLocationDbEntity>()
... ... @@ -113,7 +131,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
113 131 throw new UserFriendlyException("类型编码和名称不能为空");
114 132 }
115 133  
116   - var (availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input);
  134 + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input);
117 135  
118 136 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>()
119 137 .AnyAsync(x => !x.IsDeleted && (x.TypeCode == code || x.TypeName == name));
... ... @@ -141,6 +159,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
141 159 };
142 160  
143 161 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  162 + await SaveTypePartnerScopeAsync(entity.Id, partnerScope, currentUserId, now);
144 163 await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now);
145 164 return await GetAsync(entity.Id);
146 165 }
... ... @@ -161,7 +180,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
161 180 throw new UserFriendlyException("类型编码和名称不能为空");
162 181 }
163 182  
164   - var (availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input);
  183 + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input);
165 184  
166 185 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>()
167 186 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.TypeCode == code || x.TypeName == name));
... ... @@ -179,6 +198,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
179 198 entity.LastModifierId = CurrentUser?.Id?.ToString();
180 199  
181 200 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  201 + await SaveTypePartnerScopeAsync(entity.Id, partnerScope, entity.LastModifierId,
  202 + entity.LastModificationTime ?? DateTime.Now);
182 203 await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId,
183 204 entity.LastModificationTime ?? DateTime.Now);
184 205 return await GetAsync(id);
... ... @@ -200,6 +221,10 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
200 221 throw new UserFriendlyException("该标签类型已被标签引用,无法删除");
201 222 }
202 223  
  224 + await LabelEntityPartnerScopeHelper.DeletePartnerScopeRowsAsync(
  225 + _dbContext.SqlSugarClient,
  226 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Type,
  227 + id);
203 228 await _dbContext.SqlSugarClient.Deleteable<FlLabelTypeLocationDbEntity>()
204 229 .Where(x => x.LabelTypeId == id)
205 230 .ExecuteCommandAsync();
... ... @@ -214,9 +239,15 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
214 239 private const string AllRegionsDisplay = "All Regions";
215 240 private const string AllLocationsDisplay = "All Locations";
216 241  
217   - private async Task<(string AvailabilityType, List<string> LocationIds)> ResolveTypeScopeForSaveAsync(
  242 + private async Task<(LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult PartnerScope, string AvailabilityType, List<string> LocationIds)> ResolveTypeScopeForSaveAsync(
218 243 LabelTypeCreateInputVo input)
219 244 {
  245 + var partnerScope = await LabelEntityPartnerScopeHelper.ResolvePartnerScopeForSaveAsync(
  246 + _dbContext.SqlSugarClient,
  247 + input.AppliedPartnerType,
  248 + input.PartnerIds,
  249 + input.CompanyIds);
  250 +
220 251 var regionIds = NormalizeRegionIds(input);
221 252 var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds);
222 253 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
... ... @@ -236,20 +267,70 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
236 267 throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)");
237 268 }
238 269  
239   - if (availabilityType == "ALL")
  270 + var partnerSpecified = string.Equals(
  271 + partnerScope.AppliedPartnerType,
  272 + LabelEntityPartnerScopeHelper.ScopeSpecified,
  273 + StringComparison.OrdinalIgnoreCase);
  274 + var locationSpecified = string.Equals(availabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase);
  275 +
  276 + if (partnerSpecified || locationSpecified)
240 277 {
241   - return ("ALL", new List<string>());
  278 + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
  279 + _dbContext.SqlSugarClient,
  280 + partnerSpecified ? partnerScope.PartnerIds : null,
  281 + locationSpecified ? regionIds : null,
  282 + locationSpecified ? explicitLocationIds : null);
  283 + if (merged.Count == 0)
  284 + {
  285 + throw new UserFriendlyException("指定适用范围后至少需要匹配到一个有效门店");
  286 + }
  287 +
  288 + if (locationSpecified)
  289 + {
  290 + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged);
  291 + return (partnerScope, "SPECIFIED", merged);
  292 + }
242 293 }
243 294  
244   - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
245   - _dbContext.SqlSugarClient, (IReadOnlyList<string>?)null, regionIds, explicitLocationIds);
246   - if (merged.Count == 0)
  295 + if (string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase))
247 296 {
248   - throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店");
  297 + return (partnerScope, "ALL", new List<string>());
  298 + }
  299 +
  300 + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店");
  301 + }
  302 +
  303 + private async Task SaveTypePartnerScopeAsync(
  304 + string labelTypeId,
  305 + LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult partnerScope,
  306 + string? currentUserId,
  307 + DateTime now)
  308 + {
  309 + await LabelEntityPartnerScopeHelper.SavePartnerScopeAsync(
  310 + _dbContext.SqlSugarClient,
  311 + _guidGenerator,
  312 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Type,
  313 + labelTypeId,
  314 + partnerScope,
  315 + currentUserId,
  316 + now);
  317 + }
  318 +
  319 + private async Task ApplyPartnerScopeToGetOutputAsync(LabelTypeGetOutputDto dto, string entityId)
  320 + {
  321 + var map = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync(
  322 + _dbContext.SqlSugarClient,
  323 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Type,
  324 + new[] { entityId });
  325 + if (!map.TryGetValue(entityId, out var scope))
  326 + {
  327 + return;
249 328 }
250 329  
251   - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged);
252   - return ("SPECIFIED", merged);
  330 + dto.AppliedPartnerType = scope.AppliedPartnerType;
  331 + dto.Company = scope.Company;
  332 + dto.PartnerIds = scope.PartnerIds;
  333 + dto.CompanyIds = scope.PartnerIds;
253 334 }
254 335  
255 336 private static List<string> NormalizeRegionIds(LabelTypeCreateInputVo input)
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs
... ... @@ -41,7 +41,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
41 41 (x.DisplayText != null && x.DisplayText.Contains(keyword!)))
42 42 .WhereIF(input.State != null, x => x.State == input.State);
43 43  
44   - query = await ApplyCategoryScopeFilterAsync(query, input.GroupId, input.LocationId);
  44 + query = await ApplyCategoryScopeFilterAsync(query, input.PartnerId, input.GroupId, input.LocationId);
45 45  
46 46 // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column
47 47 // 同时避免将 input.Sorting 原样拼接到 SQL(存在注入风险)
... ... @@ -116,6 +116,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
116 116 throw new UserFriendlyException("类别不存在");
117 117 }
118 118  
  119 + await EnsureCategoryVisibleToCurrentUserAsync(entity);
  120 +
119 121 var dto = MapToGetOutput(entity);
120 122 if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))
121 123 {
... ... @@ -189,6 +191,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
189 191 throw new UserFriendlyException("类别不存在");
190 192 }
191 193  
  194 + await EnsureCategoryVisibleToCurrentUserAsync(entity);
  195 +
192 196 var code = NormalizeCategoryCode(input.CategoryCode);
193 197 var name = input.CategoryName?.Trim();
194 198 if (string.IsNullOrWhiteSpace(name))
... ... @@ -231,6 +235,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
231 235 return;
232 236 }
233 237  
  238 + await EnsureCategoryVisibleToCurrentUserAsync(entity);
  239 +
234 240 // 若被产品引用则不允许删除
235 241 var usedByProduct = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>()
236 242 .AnyAsync(x => !x.IsDeleted && x.CategoryId == id);
... ... @@ -321,28 +327,59 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
321 327  
322 328 private async Task<ISugarQueryable<FlProductCategoryDbEntity>> ApplyCategoryScopeFilterAsync(
323 329 ISugarQueryable<FlProductCategoryDbEntity> query,
  330 + string? partnerId,
324 331 string? groupId,
325 332 string? locationId)
326 333 {
327   - var scopeLocIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync(
328   - _dbContext.SqlSugarClient, groupId, locationId);
329   - if (scopeLocIds is null)
  334 + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync(
  335 + CurrentUser, _dbContext, partnerId, groupId, locationId);
  336 + if (scopedLocationIds is null)
330 337 {
331 338 return query;
332 339 }
333 340  
334   - if (scopeLocIds.Count == 0)
  341 + if (scopedLocationIds.Count == 0)
335 342 {
336   - return query.Where(c => c.AvailabilityType == "ALL");
  343 + return query.Where(_ => false);
337 344 }
338 345  
  346 + // 非平台管理员:仅返回绑定门店与可见范围有交集的分类(不再因 AvailabilityType=ALL 暴露其它公司数据)
339 347 return query.Where(c =>
340   - c.AvailabilityType == "ALL" ||
341 348 SqlFunc.Subqueryable<FlProductCategoryLocationDbEntity>()
342   - .Where(cl => cl.CategoryId == c.Id && scopeLocIds.Contains(cl.LocationId))
  349 + .Where(cl => cl.CategoryId == c.Id && scopedLocationIds.Contains(cl.LocationId))
343 350 .Any());
344 351 }
345 352  
  353 + /// <summary>
  354 + /// 详情/编辑/删除:非管理员须与绑定门店范围有交集。
  355 + /// </summary>
  356 + private async Task EnsureCategoryVisibleToCurrentUserAsync(FlProductCategoryDbEntity entity)
  357 + {
  358 + if (ReportsRoleHelper.IsAdminRole(CurrentUser))
  359 + {
  360 + return;
  361 + }
  362 +
  363 + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync(
  364 + CurrentUser, _dbContext, null, null, null);
  365 + if (scopedLocationIds is null)
  366 + {
  367 + return;
  368 + }
  369 +
  370 + if (scopedLocationIds.Count == 0)
  371 + {
  372 + throw new UserFriendlyException("无权访问该类别");
  373 + }
  374 +
  375 + var visible = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryLocationDbEntity>()
  376 + .AnyAsync(cl => cl.CategoryId == entity.Id && scopedLocationIds.Contains(cl.LocationId));
  377 + if (!visible)
  378 + {
  379 + throw new UserFriendlyException("无权访问该类别");
  380 + }
  381 + }
  382 +
346 383 private async Task SaveCategoryLocationsAsync(
347 384 string categoryId,
348 385 string availabilityType,
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs
... ... @@ -51,20 +51,25 @@ public class ReportsAppService : ApplicationService, IReportsAppService
51 51 throw new UserFriendlyException("用户未登录");
52 52 }
53 53  
54   - var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId);
  54 + var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync(
  55 + CurrentUser,
  56 + _dbContext.SqlSugarClient,
  57 + input.PartnerId,
  58 + input.GroupId,
  59 + input.LocationId);
55 60 if (locationIds is not null && locationIds.Count == 0)
56 61 {
57 62 return EmptyPrintLogPage(input);
58 63 }
59 64  
60 65 var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate);
61   - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser);
  66 + var viewAllPrints = await ResolveViewAllPrintsAsync();
62 67 var currentUserIdStr = CurrentUser.Id.Value.ToString();
63 68 var keyword = input.Keyword?.Trim();
64 69  
65 70 RefAsync<int> total = 0;
66 71  
67   - var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword)
  72 + var query = BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword)
68 73 .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id)
69 74 .Where((t, l, p, lc, pc, loc, tpl) =>
70 75 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart &&
... ... @@ -166,18 +171,23 @@ public class ReportsAppService : ApplicationService, IReportsAppService
166 171 throw new UserFriendlyException("用户未登录");
167 172 }
168 173  
169   - var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId);
  174 + var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync(
  175 + CurrentUser,
  176 + _dbContext.SqlSugarClient,
  177 + input.PartnerId,
  178 + input.GroupId,
  179 + input.LocationId);
170 180 if (locationIds is not null && locationIds.Count == 0)
171 181 {
172 182 return BuildEmptyPdf("print-log-empty.pdf");
173 183 }
174 184  
175 185 var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate);
176   - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser);
  186 + var viewAllPrints = await ResolveViewAllPrintsAsync();
177 187 var currentUserIdStr = CurrentUser.Id.Value.ToString();
178 188 var keyword = input.Keyword?.Trim();
179 189  
180   - var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword)
  190 + var query = BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword)
181 191 .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id)
182 192 .Where((t, l, p, lc, pc, loc, tpl) =>
183 193 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart &&
... ... @@ -305,7 +315,12 @@ public class ReportsAppService : ApplicationService, IReportsAppService
305 315 throw new UserFriendlyException("用户未登录");
306 316 }
307 317  
308   - var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId);
  318 + var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync(
  319 + CurrentUser,
  320 + _dbContext.SqlSugarClient,
  321 + input.PartnerId,
  322 + input.GroupId,
  323 + input.LocationId);
309 324 if (locationIds is not null && locationIds.Count == 0)
310 325 {
311 326 var emptyMs = ReportsPrintLogExcelHelper.BuildWorkbook(Array.Empty<ReportsPrintLogListItemDto>());
... ... @@ -315,11 +330,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService
315 330 }
316 331  
317 332 var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate);
318   - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser);
  333 + var viewAllPrints = await ResolveViewAllPrintsAsync();
319 334 var currentUserIdStr = CurrentUser.Id.Value.ToString();
320 335 var keyword = input.Keyword?.Trim();
321 336  
322   - var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword)
  337 + var query = BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword)
323 338 .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id)
324 339 .Where((t, l, p, lc, pc, loc, tpl) =>
325 340 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart &&
... ... @@ -414,11 +429,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService
414 429 }
415 430  
416 431 var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate);
417   - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser);
  432 + var viewAllPrints = await ResolveViewAllPrintsAsync();
418 433 var currentUserIdStr = CurrentUser.Id.Value.ToString();
419 434 var templateKeyword = input.Keyword?.Trim();
420 435  
421   - var groupedRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword: null,
  436 + var groupedRows = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword: null,
422 437 restrictToCreator: false)
423 438 .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id)
424 439 .Where((t, l, p, lc, pc, loc, tpl) =>
... ... @@ -499,17 +514,17 @@ public class ReportsAppService : ApplicationService, IReportsAppService
499 514  
500 515 var prevEndExcl = curStart;
501 516 var prevStart = curStart - span;
502   - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser);
  517 + var viewAllPrints = await ResolveViewAllPrintsAsync();
503 518 var currentUserIdStr = CurrentUser.Id.Value.ToString();
504 519 var keyword = input.Keyword?.Trim();
505 520  
506   - var totalCur = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false)
  521 + var totalCur = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false)
507 522 .Where((t, l, p, lc, pc, loc) =>
508 523 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart &&
509 524 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl)
510 525 .CountAsync();
511 526  
512   - var totalPrev = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false)
  527 + var totalPrev = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false)
513 528 .Where((t, l, p, lc, pc, loc) =>
514 529 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart &&
515 530 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl)
... ... @@ -520,7 +535,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
520 535 var avgDaily = Math.Round((decimal)totalCur / dayCount, 2);
521 536 var avgDailyPrev = Math.Round((decimal)totalPrev / prevDayCount, 2);
522 537  
523   - var categoryRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false)
  538 + var categoryRows = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false)
524 539 .Where((t, l, p, lc, pc, loc) =>
525 540 l.LabelCategoryId != null &&
526 541 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart &&
... ... @@ -531,7 +546,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
531 546  
532 547 var topCat = categoryRows.OrderByDescending(x => x.Cnt).FirstOrDefault();
533 548  
534   - var productRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false)
  549 + var productRows = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false)
535 550 .Where((t, l, p, lc, pc, loc) =>
536 551 !string.IsNullOrEmpty(p.Id) &&
537 552 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart &&
... ... @@ -552,7 +567,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
552 567  
553 568 var trendEndExcl = trendEndDay.AddDays(1);
554 569  
555   - var trendRaw = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false)
  570 + var trendRaw = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false)
556 571 .Where((t, l, p, lc, pc, loc) =>
557 572 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay &&
558 573 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl)
... ... @@ -704,7 +719,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
704 719 FlProductCategoryDbEntity, LocationAggregateRoot>
705 720 BuildReportTaskCore(
706 721 List<string>? locationIds,
707   - bool isAdmin,
  722 + bool viewAllPrints,
708 723 string currentUserIdStr,
709 724 string? keyword,
710 725 bool restrictToCreator = true)
... ... @@ -717,7 +732,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
717 732 .LeftJoin<LocationAggregateRoot>((t, l, p, lc, pc, loc) =>
718 733 t.LocationId != null && SqlFunc.ToString(loc.Id) == t.LocationId)
719 734 .Where((t, l, p, lc, pc, loc) => !loc.IsDeleted)
720   - .WhereIF(restrictToCreator && !isAdmin, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr)
  735 + .WhereIF(restrictToCreator && !viewAllPrints, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr)
721 736 .WhereIF(locationIds is not null, (t, l, p, lc, pc, loc) => locationIds!.Contains(t.LocationId!))
722 737 .WhereIF(!string.IsNullOrWhiteSpace(keyword),
723 738 (t, l, p, lc, pc, loc) =>
... ... @@ -739,56 +754,8 @@ public class ReportsAppService : ApplicationService, IReportsAppService
739 754 private static decimal CalcChangeRate(int current, int previous) =>
740 755 CalcChangeRate((decimal)current, (decimal)previous);
741 756  
742   - private async Task<List<string>?> ResolveFilteredLocationIdsAsync(string? partnerId, string? groupId,
743   - string? locationId)
744   - {
745   - var locId = locationId?.Trim();
746   - if (!string.IsNullOrWhiteSpace(locId))
747   - {
748   - return new List<string> { locId };
749   - }
750   -
751   - var gid = groupId?.Trim();
752   - var pid = partnerId?.Trim();
753   -
754   - if (string.IsNullOrWhiteSpace(pid) && string.IsNullOrWhiteSpace(gid))
755   - {
756   - return null;
757   - }
758   -
759   - var q = _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>().Where(x => !x.IsDeleted);
760   -
761   - if (!string.IsNullOrWhiteSpace(gid))
762   - {
763   - var g = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>()
764   - .FirstAsync(x => !x.IsDeleted && x.Id == gid);
765   - if (g is null)
766   - {
767   - return new List<string>();
768   - }
769   -
770   - var gName = g.GroupName?.Trim() ?? string.Empty;
771   - var partner = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
772   - .FirstAsync(x => !x.IsDeleted && x.Id == g.PartnerId);
773   - var pName = partner?.PartnerName?.Trim() ?? string.Empty;
774   - q = q.Where(x => x.GroupName == gName && x.Partner == pName);
775   - }
776   - else if (!string.IsNullOrWhiteSpace(pid))
777   - {
778   - var partner = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
779   - .FirstAsync(x => !x.IsDeleted && x.Id == pid);
780   - if (partner is null)
781   - {
782   - return new List<string>();
783   - }
784   -
785   - var pName = partner.PartnerName?.Trim() ?? string.Empty;
786   - q = q.Where(x => x.Partner == pName);
787   - }
788   -
789   - var ids = await q.Select(x => SqlFunc.ToString(x.Id)).ToListAsync();
790   - return ids;
791   - }
  757 + private Task<bool> ResolveViewAllPrintsAsync() =>
  758 + UsAppPrintLogScopeHelper.CanViewAllPrintsAtLocationAsync(CurrentUser, _dbContext.SqlSugarClient);
792 759  
793 760 private static (DateTime rangeStart, DateTime rangeEndExcl) ResolveDateRange(DateTime? startDate,
794 761 DateTime? endDate)
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs
... ... @@ -85,7 +85,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
85 85 /// </summary>
86 86 /// <remarks>
87 87 /// 行为与系统 <c>AccountService.PostLoginAsync</c> 一致(含验证码、登录日志事件)。
88   - /// 门店数据来自 <c>userlocation</c> 与 <c>location</c> 表
  88 + /// 门店来自 <c>userlocation</c>;Company Admin 展开为绑定 Company 下全部门店
89 89 /// </remarks>
90 90 /// <param name="input">邮箱、密码;若系统开启验证码则需传 Uuid、Code</param>
91 91 /// <returns>Token、RefreshToken 与绑定门店</returns>
... ... @@ -150,7 +150,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
150 150 _forgotPasswordByEmailService.ResetPasswordByEmailAsync(input);
151 151  
152 152 /// <summary>
153   - /// 获取当前登录用户已绑定的门店(切换门店时可重新拉取)
  153 + /// 获取当前登录用户可选门店(切换门店时可重新拉取);Company Admin 返回绑定 Company 下全部门店。
154 154 /// </summary>
155 155 [Authorize]
156 156 public virtual async Task<List<UsAppBoundLocationDto>> GetMyLocationsAsync()
... ... @@ -549,21 +549,25 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
549 549  
550 550 private async Task<List<UsAppBoundLocationDto>> LoadBoundLocationsAsync(Guid userId)
551 551 {
552   - var userIdStr = userId.ToString();
553   - var links = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>()
554   - .Where(x => !x.IsDeleted && x.UserId == userIdStr)
555   - .Select(x => x.LocationId)
556   - .ToListAsync();
  552 + var accessibleIds = await LocationScopeBindingHelper.ResolveAppAccessibleLocationIdsForUserAsync(
  553 + _dbContext.SqlSugarClient, userId);
  554 + if (accessibleIds.Count == 0)
  555 + {
  556 + return new List<UsAppBoundLocationDto>();
  557 + }
557 558  
558   - if (links.Count == 0)
  559 + var guidList = accessibleIds
  560 + .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null)
  561 + .Where(x => x.HasValue)
  562 + .Select(x => x!.Value)
  563 + .ToList();
  564 + if (guidList.Count == 0)
559 565 {
560 566 return new List<UsAppBoundLocationDto>();
561 567 }
562 568  
563   - var wanted = links.Distinct().ToList();
564 569 var locations = (await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>()
565   - .Where(x => !x.IsDeleted)
566   - .Where(x => wanted.Contains(x.Id.ToString()))
  570 + .Where(x => !x.IsDeleted && guidList.Contains(x.Id))
567 571 .ToListAsync())
568 572 .OrderBy(x => x.OrderNum)
569 573 .ThenBy(x => x.LocationName)
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs
... ... @@ -945,9 +945,18 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
945 945 })
946 946 .ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
947 947  
  948 + var pageTaskIds = pageRows.Select(x => x.Id).ToList();
  949 + var createdByByTaskId = pageTaskIds.Count == 0
  950 + ? new Dictionary<string, string?>(StringComparer.Ordinal)
  951 + : (await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>()
  952 + .Where(t => pageTaskIds.Contains(t.Id))
  953 + .Select(t => new { t.Id, t.CreatedBy })
  954 + .ToListAsync())
  955 + .ToDictionary(x => x.Id, x => x.CreatedBy, StringComparer.Ordinal);
  956 +
948 957 var operatorMap = await UsAppPrintLogScopeHelper.LoadOperatorNameMapAsync(
949 958 _dbContext.SqlSugarClient,
950   - pageRows.Select(x => x.CreatedBy));
  959 + createdByByTaskId.Values.Concat(pageRows.Select(x => x.CreatedBy)));
951 960  
952 961 var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync(
953 962 _dbContext.SqlSugarClient,
... ... @@ -968,7 +977,9 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
968 977 LabelSizeText = FormatLabelSizeWithUnit(x.TemplateWidth, x.TemplateHeight, x.TemplateUnit),
969 978 PrintInputJson = x.PrintInputJson,
970 979 PrintedAt = x.PrintedAt ?? x.CreationTime,
971   - OperatorName = UsAppPrintLogScopeHelper.ResolveOperatorName(operatorMap, x.CreatedBy),
  980 + OperatorName = UsAppPrintLogScopeHelper.ResolveOperatorName(
  981 + operatorMap,
  982 + createdByByTaskId.TryGetValue(x.Id, out var createdBy) ? createdBy : x.CreatedBy),
972 983 LocationName = locationName
973 984 }).ToList();
974 985  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_entity_partner_scope.sql 0 → 100644
  1 +-- 标签类型 / 分类 / 多选项适用 Company 多选(ALL/SPECIFIED)
  2 +-- 执行前请备份;可重复执行
  3 +
  4 +SET @db := DATABASE();
  5 +
  6 +-- fl_label_type.AppliedPartnerType
  7 +SET @col_type_partner := (
  8 + SELECT COUNT(*) FROM information_schema.COLUMNS
  9 + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'fl_label_type' AND COLUMN_NAME = 'AppliedPartnerType'
  10 +);
  11 +SET @ddl_type_partner := IF(
  12 + @col_type_partner = 0,
  13 + 'ALTER TABLE `fl_label_type` ADD COLUMN `AppliedPartnerType` varchar(20) NOT NULL DEFAULT ''ALL'' COMMENT ''适用Company:ALL/SPECIFIED'' AFTER `AvailabilityType`',
  14 + 'SELECT 1'
  15 +);
  16 +PREPARE stmt_type_partner FROM @ddl_type_partner;
  17 +EXECUTE stmt_type_partner;
  18 +DEALLOCATE PREPARE stmt_type_partner;
  19 +
  20 +-- fl_label_category.AppliedPartnerType
  21 +SET @col_cat_partner := (
  22 + SELECT COUNT(*) FROM information_schema.COLUMNS
  23 + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'fl_label_category' AND COLUMN_NAME = 'AppliedPartnerType'
  24 +);
  25 +SET @ddl_cat_partner := IF(
  26 + @col_cat_partner = 0,
  27 + 'ALTER TABLE `fl_label_category` ADD COLUMN `AppliedPartnerType` varchar(20) NOT NULL DEFAULT ''ALL'' COMMENT ''适用Company:ALL/SPECIFIED'' AFTER `AvailabilityType`',
  28 + 'SELECT 1'
  29 +);
  30 +PREPARE stmt_cat_partner FROM @ddl_cat_partner;
  31 +EXECUTE stmt_cat_partner;
  32 +DEALLOCATE PREPARE stmt_cat_partner;
  33 +
  34 +-- fl_label_multiple_option.AppliedPartnerType
  35 +SET @col_opt_partner := (
  36 + SELECT COUNT(*) FROM information_schema.COLUMNS
  37 + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'fl_label_multiple_option' AND COLUMN_NAME = 'AppliedPartnerType'
  38 +);
  39 +SET @ddl_opt_partner := IF(
  40 + @col_opt_partner = 0,
  41 + 'ALTER TABLE `fl_label_multiple_option` ADD COLUMN `AppliedPartnerType` varchar(20) NOT NULL DEFAULT ''ALL'' COMMENT ''适用Company:ALL/SPECIFIED'' AFTER `AvailabilityType`',
  42 + 'SELECT 1'
  43 +);
  44 +PREPARE stmt_opt_partner FROM @ddl_opt_partner;
  45 +EXECUTE stmt_opt_partner;
  46 +DEALLOCATE PREPARE stmt_opt_partner;
  47 +
  48 +CREATE TABLE IF NOT EXISTS `fl_label_type_partner` (
  49 + `Id` varchar(36) NOT NULL COMMENT '主键',
  50 + `LabelTypeId` varchar(36) NOT NULL COMMENT 'fl_label_type.Id',
  51 + `PartnerId` varchar(36) NOT NULL COMMENT 'fl_partner.Id',
  52 + `CreationTime` datetime NOT NULL COMMENT '创建时间',
  53 + `CreatorId` varchar(36) DEFAULT NULL COMMENT '创建者',
  54 + PRIMARY KEY (`Id`),
  55 + KEY `idx_fl_ltyp_template` (`LabelTypeId`),
  56 + KEY `idx_fl_ltyp_partner` (`PartnerId`)
  57 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签类型适用Company';
  58 +
  59 +CREATE TABLE IF NOT EXISTS `fl_label_category_partner` (
  60 + `Id` varchar(36) NOT NULL COMMENT '主键',
  61 + `CategoryId` varchar(36) NOT NULL COMMENT 'fl_label_category.Id',
  62 + `PartnerId` varchar(36) NOT NULL COMMENT 'fl_partner.Id',
  63 + `CreationTime` datetime NOT NULL COMMENT '创建时间',
  64 + `CreatorId` varchar(36) DEFAULT NULL COMMENT '创建者',
  65 + PRIMARY KEY (`Id`),
  66 + KEY `idx_fl_lcatp_category` (`CategoryId`),
  67 + KEY `idx_fl_lcatp_partner` (`PartnerId`)
  68 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签分类适用Company';
  69 +
  70 +CREATE TABLE IF NOT EXISTS `fl_label_multiple_option_partner` (
  71 + `Id` varchar(36) NOT NULL COMMENT '主键',
  72 + `MultipleOptionId` varchar(36) NOT NULL COMMENT 'fl_label_multiple_option.Id',
  73 + `PartnerId` varchar(36) NOT NULL COMMENT 'fl_partner.Id',
  74 + `CreationTime` datetime NOT NULL COMMENT '创建时间',
  75 + `CreatorId` varchar(36) DEFAULT NULL COMMENT '创建者',
  76 + PRIMARY KEY (`Id`),
  77 + KEY `idx_fl_lmop_option` (`MultipleOptionId`),
  78 + KEY `idx_fl_lmop_partner` (`PartnerId`)
  79 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签多选项适用Company';
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json
... ... @@ -20,7 +20,7 @@
20 20 },
21 21 //应用启动:SelfUrl 供 Program.cs UseUrls 绑定;用 0.0.0.0 避免写成固定局域网 IP 在本机无该网卡时启动失败(WinError 10049)
22 22 "App": {
23   - "SelfUrl": "http://192.168.31.89:19001",
  23 + "SelfUrl": "http://192.168.31.88:19001",
24 24 "CorsOrigins": "http://localhost:19001;http://localhost:18000;http://localhost:5666;http://localhost:5173;http://localhost:3000"
25 25 },
26 26 //配置
... ...
美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx
... ... @@ -60,6 +60,7 @@ import {
60 60 } from "../ui/pagination";
61 61 import {
62 62 getLabelCategories,
  63 + getLabelCategory,
63 64 createLabelCategory,
64 65 updateLabelCategory,
65 66 deleteLabelCategory,
... ... @@ -68,10 +69,9 @@ import { getLocations } from &quot;../../services/locationService&quot;;
68 69 import { getGroups } from "../../services/groupService";
69 70 import { getPartners } from "../../services/partnerService";
70 71 import {
71   - buildSpecifiedLocationPayload,
72   - effectiveScopePartnerId,
  72 + buildEntityScopeSaveFromForm,
73 73 hydrateCategoryScopeFromLocationIds,
74   - scopePartnerValidationMessage,
  74 + hydrateEntityScopeFromDto,
75 75 } from "../../lib/categoryScopeForm";
76 76 import { CategoryScopeFields } from "../shared/category-scope-fields";
77 77 import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth";
... ... @@ -841,7 +841,9 @@ function CreateLabelCategoryDialog({
841 841 const [displayTextForPhoto, setDisplayTextForPhoto] = useState("");
842 842 const [buttonBgColor, setButtonBgColor] = useState("#3B82F6");
843 843 const [selectedPartnerId, setSelectedPartnerId] = useState("");
  844 + const [selectedPartnerIds, setSelectedPartnerIds] = useState<string[]>([]);
844 845 const [selectedRegionNames, setSelectedRegionNames] = useState<string[]>([]);
  846 + const [selectedRegionIds, setSelectedRegionIds] = useState<string[]>([]);
845 847 const [selectedLocationIds, setSelectedLocationIds] = useState<string[]>([]);
846 848 const [form, setForm] = useState<LabelCategoryCreateInput>({
847 849 categoryName: "",
... ... @@ -862,7 +864,9 @@ function CreateLabelCategoryDialog({
862 864 setDisplayTextForPhoto("");
863 865 setButtonBgColor("#3B82F6");
864 866 setSelectedPartnerId("");
  867 + setSelectedPartnerIds([]);
865 868 setSelectedRegionNames([]);
  869 + setSelectedRegionIds([]);
866 870 setSelectedLocationIds([]);
867 871 setForm({
868 872 categoryName: "",
... ... @@ -920,27 +924,20 @@ function CreateLabelCategoryDialog({
920 924 }
921 925 }
922 926  
923   - const scopePartnerId = effectiveScopePartnerId(
  927 + const scopePayload = buildEntityScopeSaveFromForm({
924 928 requireCompanySelection,
925 929 selectedPartnerId,
  930 + selectedPartnerIds,
  931 + selectedRegionNames,
  932 + selectedRegionIds,
  933 + selectedLocationIds,
926 934 fixedPartnerId,
927   - );
928   - const partnerErr = scopePartnerValidationMessage(requireCompanySelection, scopePartnerId);
929   - if (partnerErr) {
930   - toast.error("Validation failed", { description: partnerErr });
931   - return;
932   - }
933   -
934   - const locPayload = buildSpecifiedLocationPayload(
935 935 locations,
936 936 partners,
937 937 groups,
938   - scopePartnerId,
939   - selectedRegionNames,
940   - selectedLocationIds,
941   - );
942   - if (!locPayload.ok) {
943   - toast.error("Validation failed", { description: locPayload.message });
  938 + });
  939 + if (!scopePayload.ok) {
  940 + toast.error("Validation failed", { description: scopePayload.message });
944 941 return;
945 942 }
946 943  
... ... @@ -971,8 +968,7 @@ function CreateLabelCategoryDialog({
971 968 buttonImageUrl: apSel.image ? String(form.categoryPhotoUrl ?? "").trim() || null : null,
972 969 buttonAppearance: tokens,
973 970 buttonStyleJson,
974   - availabilityType: "SPECIFIED",
975   - locationIds: locPayload.locationIds,
  971 + ...scopePayload.body,
976 972 });
977 973 toast.success("Label category created.", {
978 974 description: "The label category has been created successfully.",
... ... @@ -1022,6 +1018,11 @@ function CreateLabelCategoryDialog({
1022 1018 onLocationChange={setSelectedLocationIds}
1023 1019 requireCompanySelection={requireCompanySelection}
1024 1020 fixedPartnerId={fixedPartnerId}
  1021 + templateScopeMode={requireCompanySelection}
  1022 + selectedPartnerIds={selectedPartnerIds}
  1023 + onPartnerIdsChange={setSelectedPartnerIds}
  1024 + selectedRegionIds={selectedRegionIds}
  1025 + onRegionIdsChange={setSelectedRegionIds}
1025 1026 />
1026 1027  
1027 1028 <div className="space-y-2">
... ... @@ -1228,11 +1229,14 @@ function EditLabelCategoryDialog({
1228 1229 onUpdated: () => void;
1229 1230 }) {
1230 1231 const [submitting, setSubmitting] = useState(false);
  1232 + const [loadingDetail, setLoadingDetail] = useState(false);
1231 1233 const [apSel, setApSel] = useState({ text: true, color: false, image: false });
1232 1234 const [displayTextForPhoto, setDisplayTextForPhoto] = useState("");
1233 1235 const [buttonBgColor, setButtonBgColor] = useState("#3B82F6");
1234 1236 const [selectedPartnerId, setSelectedPartnerId] = useState("");
  1237 + const [selectedPartnerIds, setSelectedPartnerIds] = useState<string[]>([]);
1235 1238 const [selectedRegionNames, setSelectedRegionNames] = useState<string[]>([]);
  1239 + const [selectedRegionIds, setSelectedRegionIds] = useState<string[]>([]);
1236 1240 const [selectedLocationIds, setSelectedLocationIds] = useState<string[]>([]);
1237 1241 const [form, setForm] = useState<LabelCategoryUpdateInput>({
1238 1242 categoryName: "",
... ... @@ -1249,104 +1253,145 @@ function EditLabelCategoryDialog({
1249 1253 }, [apSel.text, apSel.color]);
1250 1254  
1251 1255 useEffect(() => {
1252   - if (!open || !category) return;
  1256 + if (!open || !category?.id) return;
  1257 +
  1258 + const ac = new AbortController();
  1259 + setLoadingDetail(true);
  1260 +
  1261 + (async () => {
  1262 + try {
  1263 + const detail = await getLabelCategory(category.id, ac.signal);
  1264 + if (ac.signal.aborted) return;
  1265 + const row = detail.id ? detail : category;
  1266 +
  1267 + const hydrateAvailability = () => {
  1268 + if (requireCompanySelection) {
  1269 + const scope = hydrateEntityScopeFromDto(row, locations, partners, groups);
  1270 + setSelectedPartnerIds(scope.partnerIds);
  1271 + setSelectedRegionIds(scope.regionIds);
  1272 + setSelectedLocationIds(scope.locationIds);
  1273 + setSelectedPartnerId("");
  1274 + setSelectedRegionNames([]);
  1275 + return;
  1276 + }
  1277 + const rawAt = String(row.availabilityType ?? "ALL").trim().toUpperCase();
  1278 + if (rawAt !== "SPECIFIED") {
  1279 + setSelectedPartnerId("");
  1280 + setSelectedRegionNames([]);
  1281 + setSelectedLocationIds([]);
  1282 + setSelectedPartnerIds([]);
  1283 + setSelectedRegionIds([]);
  1284 + return;
  1285 + }
  1286 + const lids = (row.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean);
  1287 + const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups);
  1288 + setSelectedPartnerId(scope.partnerId);
  1289 + setSelectedRegionNames(scope.regionNames);
  1290 + setSelectedLocationIds(scope.locationIds);
  1291 + setSelectedPartnerIds([]);
  1292 + setSelectedRegionIds([]);
  1293 + };
  1294 +
  1295 + const styleParsed = parseCategoryButtonStyleV1(
  1296 + row.buttonStyleJson ?? (row as { ButtonStyleJson?: string | null }).ButtonStyleJson,
  1297 + );
  1298 + if (styleParsed) {
  1299 + const sel = appearanceSelectionFromTokens(styleParsed.appearances);
  1300 + setApSel(sel);
  1301 + setDisplayTextForPhoto(String(styleParsed.displayText ?? ""));
  1302 + setButtonBgColor(styleParsed.buttonBgColor || "#3B82F6");
  1303 + setForm({
  1304 + categoryName: row.categoryName ?? "",
  1305 + categoryPhotoUrl: sel.image ? row.categoryPhotoUrl ?? null : null,
  1306 + state: row.state ?? true,
  1307 + orderNum: row.orderNum ?? null,
  1308 + });
  1309 + hydrateAvailability();
  1310 + return;
  1311 + }
1253 1312  
1254   - const hydrateAvailability = () => {
1255   - const rawAt = String(category.availabilityType ?? "ALL").trim().toUpperCase();
1256   - if (rawAt !== "SPECIFIED") {
  1313 + const rawBa =
  1314 + row.buttonAppearance ??
  1315 + (row as { ButtonAppearance?: string | null }).ButtonAppearance ??
  1316 + undefined;
  1317 + const tokens = parseAppearanceTokens(rawBa);
  1318 + const valArrZip = parseCategoryPhotoUrlValueArray(row.categoryPhotoUrl);
  1319 + if (valArrZip && tokens.length > 0 && tokens.length === valArrZip.length) {
  1320 + const merged = visualInputFromAppearanceAndValueArray(tokens, valArrZip, {
  1321 + categoryName: row.categoryName,
  1322 + name: undefined,
  1323 + buttonTextColor: row.buttonTextColor ?? null,
  1324 + });
  1325 + const selZip = appearanceSelectionFromTokens(tokens);
  1326 + setApSel(selZip);
  1327 + setDisplayTextForPhoto(String(merged.displayText ?? ""));
  1328 + setButtonBgColor(merged.buttonBgColor?.trim() ? merged.buttonBgColor : "#3B82F6");
  1329 + setForm({
  1330 + categoryName: row.categoryName ?? "",
  1331 + categoryPhotoUrl: selZip.image ? (merged.buttonImageUrl ?? "").trim() || null : null,
  1332 + state: row.state ?? true,
  1333 + orderNum: row.orderNum ?? null,
  1334 + });
  1335 + hydrateAvailability();
  1336 + return;
  1337 + }
  1338 +
  1339 + let sel = appearanceSelectionFromTokens(tokens);
  1340 + if (tokens.length === 0) {
  1341 + const v = resolveCategoryButtonVisual({
  1342 + buttonAppearance: rawBa,
  1343 + displayText: row.displayText,
  1344 + buttonBgColor: row.buttonBgColor,
  1345 + buttonImageUrl: row.buttonImageUrl,
  1346 + categoryPhotoUrl: row.categoryPhotoUrl,
  1347 + categoryName: row.categoryName,
  1348 + });
  1349 + if (v.mode === "image") sel = { text: false, color: false, image: true };
  1350 + else if (v.mode === "colorText") sel = { text: true, color: true, image: false };
  1351 + else if (v.mode === "color") sel = { text: false, color: true, image: false };
  1352 + else if (v.mode === "text") sel = { text: true, color: false, image: false };
  1353 + else sel = { text: true, color: false, image: false };
  1354 + }
  1355 + setApSel(sel);
  1356 + const photo = String(row.categoryPhotoUrl ?? "").trim();
  1357 + const disp = String(row.displayText ?? "").trim();
  1358 + const bgField = normalizeHexColor(row.buttonBgColor);
  1359 + const hexFromPhoto = normalizeHexColor(photo);
  1360 + setDisplayTextForPhoto(
  1361 + String(
  1362 + disp ||
  1363 + (!sel.image && sel.text && photo && !hexFromPhoto && !looksLikeImageUrl(photo) ? photo : "") ||
  1364 + (!sel.image && sel.text && !sel.color ? (row.categoryName ?? "").trim() : ""),
  1365 + ),
  1366 + );
  1367 + setButtonBgColor(bgField || (sel.color && hexFromPhoto ? hexFromPhoto : "") || "#3B82F6");
  1368 + setForm({
  1369 + categoryName: row.categoryName ?? "",
  1370 + categoryPhotoUrl: sel.image ? row.categoryPhotoUrl ?? null : null,
  1371 + state: row.state ?? true,
  1372 + orderNum: row.orderNum ?? null,
  1373 + });
  1374 + hydrateAvailability();
  1375 + } catch {
  1376 + if (ac.signal.aborted) return;
  1377 + setForm({
  1378 + categoryName: category.categoryName ?? "",
  1379 + categoryPhotoUrl: category.categoryPhotoUrl ?? null,
  1380 + state: category.state ?? true,
  1381 + orderNum: category.orderNum ?? null,
  1382 + });
1257 1383 setSelectedPartnerId("");
  1384 + setSelectedPartnerIds([]);
1258 1385 setSelectedRegionNames([]);
  1386 + setSelectedRegionIds([]);
1259 1387 setSelectedLocationIds([]);
1260   - return;
  1388 + } finally {
  1389 + if (!ac.signal.aborted) setLoadingDetail(false);
1261 1390 }
1262   - const lids = (category.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean);
1263   - const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups);
1264   - setSelectedPartnerId(scope.partnerId);
1265   - setSelectedRegionNames(scope.regionNames);
1266   - setSelectedLocationIds(scope.locationIds);
1267   - };
1268   -
1269   - const styleParsed = parseCategoryButtonStyleV1(
1270   - category.buttonStyleJson ?? (category as { ButtonStyleJson?: string | null }).ButtonStyleJson,
1271   - );
1272   - if (styleParsed) {
1273   - const sel = appearanceSelectionFromTokens(styleParsed.appearances);
1274   - setApSel(sel);
1275   - setDisplayTextForPhoto(String(styleParsed.displayText ?? ""));
1276   - setButtonBgColor(styleParsed.buttonBgColor || "#3B82F6");
1277   - setForm({
1278   - categoryName: category.categoryName ?? "",
1279   - categoryPhotoUrl: sel.image ? category.categoryPhotoUrl ?? null : null,
1280   - state: category.state ?? true,
1281   - orderNum: category.orderNum ?? null,
1282   - });
1283   - hydrateAvailability();
1284   - return;
1285   - }
1286   -
1287   - const rawBa =
1288   - category.buttonAppearance ??
1289   - (category as { ButtonAppearance?: string | null }).ButtonAppearance ??
1290   - undefined;
1291   - const tokens = parseAppearanceTokens(rawBa);
1292   - const valArrZip = parseCategoryPhotoUrlValueArray(category.categoryPhotoUrl);
1293   - if (valArrZip && tokens.length > 0 && tokens.length === valArrZip.length) {
1294   - const merged = visualInputFromAppearanceAndValueArray(tokens, valArrZip, {
1295   - categoryName: category.categoryName,
1296   - name: undefined,
1297   - buttonTextColor: category.buttonTextColor ?? null,
1298   - });
1299   - const selZip = appearanceSelectionFromTokens(tokens);
1300   - setApSel(selZip);
1301   - setDisplayTextForPhoto(String(merged.displayText ?? ""));
1302   - setButtonBgColor(merged.buttonBgColor?.trim() ? merged.buttonBgColor : "#3B82F6");
1303   - setForm({
1304   - categoryName: category.categoryName ?? "",
1305   - categoryPhotoUrl: selZip.image ? (merged.buttonImageUrl ?? "").trim() || null : null,
1306   - state: category.state ?? true,
1307   - orderNum: category.orderNum ?? null,
1308   - });
1309   - hydrateAvailability();
1310   - return;
1311   - }
  1391 + })();
1312 1392  
1313   - let sel = appearanceSelectionFromTokens(tokens);
1314   - if (tokens.length === 0) {
1315   - const v = resolveCategoryButtonVisual({
1316   - buttonAppearance: rawBa,
1317   - displayText: category.displayText,
1318   - buttonBgColor: category.buttonBgColor,
1319   - buttonImageUrl: category.buttonImageUrl,
1320   - categoryPhotoUrl: category.categoryPhotoUrl,
1321   - categoryName: category.categoryName,
1322   - });
1323   - if (v.mode === "image") sel = { text: false, color: false, image: true };
1324   - else if (v.mode === "colorText") sel = { text: true, color: true, image: false };
1325   - else if (v.mode === "color") sel = { text: false, color: true, image: false };
1326   - else if (v.mode === "text") sel = { text: true, color: false, image: false };
1327   - else sel = { text: true, color: false, image: false };
1328   - }
1329   - setApSel(sel);
1330   - const photo = String(category.categoryPhotoUrl ?? "").trim();
1331   - const disp = String(category.displayText ?? "").trim();
1332   - const bgField = normalizeHexColor(category.buttonBgColor);
1333   - const hexFromPhoto = normalizeHexColor(photo);
1334   - setDisplayTextForPhoto(
1335   - String(
1336   - disp ||
1337   - (!sel.image && sel.text && photo && !hexFromPhoto && !looksLikeImageUrl(photo) ? photo : "") ||
1338   - (!sel.image && sel.text && !sel.color ? (category.categoryName ?? "").trim() : ""),
1339   - ),
1340   - );
1341   - setButtonBgColor(bgField || (sel.color && hexFromPhoto ? hexFromPhoto : "") || "#3B82F6");
1342   - setForm({
1343   - categoryName: category.categoryName ?? "",
1344   - categoryPhotoUrl: sel.image ? category.categoryPhotoUrl ?? null : null,
1345   - state: category.state ?? true,
1346   - orderNum: category.orderNum ?? null,
1347   - });
1348   - hydrateAvailability();
1349   - }, [open, category, locations, partners, groups]);
  1393 + return () => ac.abort();
  1394 + }, [open, category, locations, partners, groups, requireCompanySelection]);
1350 1395  
1351 1396 const submit = async () => {
1352 1397 if (!category?.id) return;
... ... @@ -1391,27 +1436,20 @@ function EditLabelCategoryDialog({
1391 1436 }
1392 1437 }
1393 1438  
1394   - const scopePartnerId = effectiveScopePartnerId(
  1439 + const scopePayload = buildEntityScopeSaveFromForm({
1395 1440 requireCompanySelection,
1396 1441 selectedPartnerId,
  1442 + selectedPartnerIds,
  1443 + selectedRegionNames,
  1444 + selectedRegionIds,
  1445 + selectedLocationIds,
1397 1446 fixedPartnerId,
1398   - );
1399   - const partnerErr = scopePartnerValidationMessage(requireCompanySelection, scopePartnerId);
1400   - if (partnerErr) {
1401   - toast.error("Validation failed", { description: partnerErr });
1402   - return;
1403   - }
1404   -
1405   - const locPayload = buildSpecifiedLocationPayload(
1406 1447 locations,
1407 1448 partners,
1408 1449 groups,
1409   - scopePartnerId,
1410   - selectedRegionNames,
1411   - selectedLocationIds,
1412   - );
1413   - if (!locPayload.ok) {
1414   - toast.error("Validation failed", { description: locPayload.message });
  1450 + });
  1451 + if (!scopePayload.ok) {
  1452 + toast.error("Validation failed", { description: scopePayload.message });
1415 1453 return;
1416 1454 }
1417 1455  
... ... @@ -1442,8 +1480,7 @@ function EditLabelCategoryDialog({
1442 1480 buttonImageUrl: apSel.image ? String(form.categoryPhotoUrl ?? "").trim() || null : null,
1443 1481 buttonAppearance: tokens,
1444 1482 buttonStyleJson,
1445   - availabilityType: "SPECIFIED",
1446   - locationIds: locPayload.locationIds,
  1483 + ...scopePayload.body,
1447 1484 });
1448 1485 toast.success("Label category updated.", {
1449 1486 description: "The label category has been updated successfully.",
... ... @@ -1481,19 +1518,28 @@ function EditLabelCategoryDialog({
1481 1518 />
1482 1519 </div>
1483 1520  
1484   - <CategoryScopeFields
1485   - partners={partners}
1486   - groups={groups}
1487   - locations={locations}
1488   - selectedPartnerId={selectedPartnerId}
1489   - onPartnerChange={setSelectedPartnerId}
1490   - selectedRegionNames={selectedRegionNames}
1491   - onRegionChange={setSelectedRegionNames}
1492   - selectedLocationIds={selectedLocationIds}
1493   - onLocationChange={setSelectedLocationIds}
1494   - requireCompanySelection={requireCompanySelection}
1495   - fixedPartnerId={fixedPartnerId}
1496   - />
  1521 + {loadingDetail ? (
  1522 + <p className="text-sm text-gray-500">Loading company, region and location…</p>
  1523 + ) : (
  1524 + <CategoryScopeFields
  1525 + partners={partners}
  1526 + groups={groups}
  1527 + locations={locations}
  1528 + selectedPartnerId={selectedPartnerId}
  1529 + onPartnerChange={setSelectedPartnerId}
  1530 + selectedRegionNames={selectedRegionNames}
  1531 + onRegionChange={setSelectedRegionNames}
  1532 + selectedLocationIds={selectedLocationIds}
  1533 + onLocationChange={setSelectedLocationIds}
  1534 + requireCompanySelection={requireCompanySelection}
  1535 + fixedPartnerId={fixedPartnerId}
  1536 + templateScopeMode={requireCompanySelection}
  1537 + selectedPartnerIds={selectedPartnerIds}
  1538 + onPartnerIdsChange={setSelectedPartnerIds}
  1539 + selectedRegionIds={selectedRegionIds}
  1540 + onRegionIdsChange={setSelectedRegionIds}
  1541 + />
  1542 + )}
1497 1543  
1498 1544 <div className="space-y-2">
1499 1545 <Label>Button Appearance</Label>
... ... @@ -1665,7 +1711,7 @@ function EditLabelCategoryDialog({
1665 1711 </Button>
1666 1712 <Button
1667 1713 type="button"
1668   - disabled={submitting}
  1714 + disabled={submitting || loadingDetail}
1669 1715 onClick={submit}
1670 1716 className="bg-blue-600 hover:bg-blue-700 text-white"
1671 1717 >
... ...
美国版/Food Labeling Management Platform/src/components/labels/LabelTypesView.tsx
... ... @@ -51,11 +51,9 @@ import { getLocations } from &quot;../../services/locationService&quot;;
51 51 import { getGroups } from "../../services/groupService";
52 52 import { getPartners } from "../../services/partnerService";
53 53 import {
54   - buildSpecifiedLocationPayload,
55   - effectiveScopePartnerId,
  54 + buildEntityScopeSaveFromForm,
56 55 hydrateCategoryScopeFromLocationIds,
57   - regionNamesToGroupIds,
58   - scopePartnerValidationMessage,
  56 + hydrateEntityScopeFromDto,
59 57 } from "../../lib/categoryScopeForm";
60 58 import { CategoryScopeFields } from "../shared/category-scope-fields";
61 59 import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth";
... ... @@ -822,7 +820,9 @@ function CreateLabelTypeDialog({
822 820 }) {
823 821 const [submitting, setSubmitting] = useState(false);
824 822 const [selectedPartnerId, setSelectedPartnerId] = useState("");
  823 + const [selectedPartnerIds, setSelectedPartnerIds] = useState<string[]>([]);
825 824 const [selectedRegionNames, setSelectedRegionNames] = useState<string[]>([]);
  825 + const [selectedRegionIds, setSelectedRegionIds] = useState<string[]>([]);
826 826 const [selectedLocationIds, setSelectedLocationIds] = useState<string[]>([]);
827 827 const [form, setForm] = useState<LabelTypeCreateInput>({
828 828 typeName: "",
... ... @@ -832,7 +832,9 @@ function CreateLabelTypeDialog({
832 832  
833 833 const resetForm = () => {
834 834 setSelectedPartnerId("");
  835 + setSelectedPartnerIds([]);
835 836 setSelectedRegionNames([]);
  837 + setSelectedRegionIds([]);
836 838 setSelectedLocationIds([]);
837 839 setForm({
838 840 typeName: "",
... ... @@ -859,40 +861,29 @@ function CreateLabelTypeDialog({
859 861 return;
860 862 }
861 863  
862   - const scopePartnerId = effectiveScopePartnerId(
  864 + const scopePayload = buildEntityScopeSaveFromForm({
863 865 requireCompanySelection,
864 866 selectedPartnerId,
  867 + selectedPartnerIds,
  868 + selectedRegionNames,
  869 + selectedRegionIds,
  870 + selectedLocationIds,
865 871 fixedPartnerId,
866   - );
867   - const partnerErr = scopePartnerValidationMessage(requireCompanySelection, scopePartnerId);
868   - if (partnerErr) {
869   - toast.error("Validation failed", { description: partnerErr });
870   - return;
871   - }
872   -
873   - const locPayload = buildSpecifiedLocationPayload(
874 872 locations,
875 873 partners,
876 874 groups,
877   - scopePartnerId,
878   - selectedRegionNames,
879   - selectedLocationIds,
880   - );
881   - if (!locPayload.ok) {
882   - toast.error("Validation failed", { description: locPayload.message });
  875 + });
  876 + if (!scopePayload.ok) {
  877 + toast.error("Validation failed", { description: scopePayload.message });
883 878 return;
884 879 }
885 880  
886   - const groupIds = regionNamesToGroupIds(selectedRegionNames, groups, scopePartnerId);
887   -
888 881 setSubmitting(true);
889 882 try {
890 883 await createLabelType({
891 884 ...form,
892 885 typeCode: typeCodeFromName(form.typeName),
893   - groupIds,
894   - regionIds: groupIds,
895   - locationIds: locPayload.locationIds,
  886 + ...scopePayload.body,
896 887 });
897 888 toast.success("Label type created.", {
898 889 description: "The label type has been created successfully.",
... ... @@ -956,6 +947,11 @@ function CreateLabelTypeDialog({
956 947 onLocationChange={setSelectedLocationIds}
957 948 requireCompanySelection={requireCompanySelection}
958 949 fixedPartnerId={fixedPartnerId}
  950 + templateScopeMode={requireCompanySelection}
  951 + selectedPartnerIds={selectedPartnerIds}
  952 + onPartnerIdsChange={setSelectedPartnerIds}
  953 + selectedRegionIds={selectedRegionIds}
  954 + onRegionIdsChange={setSelectedRegionIds}
959 955 />
960 956 </div>
961 957  
... ... @@ -996,7 +992,9 @@ function EditLabelTypeDialog({
996 992 const [submitting, setSubmitting] = useState(false);
997 993 const [loadingDetail, setLoadingDetail] = useState(false);
998 994 const [selectedPartnerId, setSelectedPartnerId] = useState("");
  995 + const [selectedPartnerIds, setSelectedPartnerIds] = useState<string[]>([]);
999 996 const [selectedRegionNames, setSelectedRegionNames] = useState<string[]>([]);
  997 + const [selectedRegionIds, setSelectedRegionIds] = useState<string[]>([]);
1000 998 const [selectedLocationIds, setSelectedLocationIds] = useState<string[]>([]);
1001 999 const [form, setForm] = useState<LabelTypeUpdateInput>({
1002 1000 typeName: "",
... ... @@ -1021,12 +1019,24 @@ function EditLabelTypeDialog({
1021 1019 orderNum: detail.orderNum ?? type.orderNum ?? null,
1022 1020 });
1023 1021  
  1022 + if (requireCompanySelection) {
  1023 + const scope = hydrateEntityScopeFromDto(detail, locations, partners, groups);
  1024 + setSelectedPartnerIds(scope.partnerIds);
  1025 + setSelectedRegionIds(scope.regionIds);
  1026 + setSelectedLocationIds(scope.locationIds);
  1027 + setSelectedPartnerId("");
  1028 + setSelectedRegionNames([]);
  1029 + return;
  1030 + }
  1031 +
1024 1032 const lids = (detail.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean);
1025 1033 if (lids.length > 0) {
1026 1034 const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups);
1027 1035 setSelectedPartnerId(scope.partnerId);
1028 1036 setSelectedRegionNames(scope.regionNames);
1029 1037 setSelectedLocationIds(scope.locationIds);
  1038 + setSelectedPartnerIds([]);
  1039 + setSelectedRegionIds([]);
1030 1040 return;
1031 1041 }
1032 1042  
... ... @@ -1042,6 +1052,8 @@ function EditLabelTypeDialog({
1042 1052 setSelectedPartnerId(pid);
1043 1053 setSelectedRegionNames(names);
1044 1054 setSelectedLocationIds([]);
  1055 + setSelectedPartnerIds([]);
  1056 + setSelectedRegionIds([]);
1045 1057 return;
1046 1058 }
1047 1059  
... ... @@ -1057,6 +1069,8 @@ function EditLabelTypeDialog({
1057 1069 setSelectedRegionNames([]);
1058 1070 setSelectedLocationIds([]);
1059 1071 }
  1072 + setSelectedPartnerIds([]);
  1073 + setSelectedRegionIds([]);
1060 1074 } catch {
1061 1075 if (ac.signal.aborted) return;
1062 1076 setForm({
... ... @@ -1065,7 +1079,9 @@ function EditLabelTypeDialog({
1065 1079 orderNum: type.orderNum ?? null,
1066 1080 });
1067 1081 setSelectedPartnerId("");
  1082 + setSelectedPartnerIds([]);
1068 1083 setSelectedRegionNames([]);
  1084 + setSelectedRegionIds([]);
1069 1085 setSelectedLocationIds([]);
1070 1086 } finally {
1071 1087 if (!ac.signal.aborted) setLoadingDetail(false);
... ... @@ -1073,7 +1089,7 @@ function EditLabelTypeDialog({
1073 1089 })();
1074 1090  
1075 1091 return () => ac.abort();
1076   - }, [open, type, locations, partners, groups]);
  1092 + }, [open, type, locations, partners, groups, requireCompanySelection]);
1077 1093  
1078 1094 const submit = async () => {
1079 1095 if (!type?.id) return;
... ... @@ -1088,40 +1104,29 @@ function EditLabelTypeDialog({
1088 1104 return;
1089 1105 }
1090 1106  
1091   - const scopePartnerId = effectiveScopePartnerId(
  1107 + const scopePayload = buildEntityScopeSaveFromForm({
1092 1108 requireCompanySelection,
1093 1109 selectedPartnerId,
  1110 + selectedPartnerIds,
  1111 + selectedRegionNames,
  1112 + selectedRegionIds,
  1113 + selectedLocationIds,
1094 1114 fixedPartnerId,
1095   - );
1096   - const partnerErr = scopePartnerValidationMessage(requireCompanySelection, scopePartnerId);
1097   - if (partnerErr) {
1098   - toast.error("Validation failed", { description: partnerErr });
1099   - return;
1100   - }
1101   -
1102   - const locPayload = buildSpecifiedLocationPayload(
1103 1115 locations,
1104 1116 partners,
1105 1117 groups,
1106   - scopePartnerId,
1107   - selectedRegionNames,
1108   - selectedLocationIds,
1109   - );
1110   - if (!locPayload.ok) {
1111   - toast.error("Validation failed", { description: locPayload.message });
  1118 + });
  1119 + if (!scopePayload.ok) {
  1120 + toast.error("Validation failed", { description: scopePayload.message });
1112 1121 return;
1113 1122 }
1114 1123  
1115   - const groupIds = regionNamesToGroupIds(selectedRegionNames, groups, scopePartnerId);
1116   -
1117 1124 setSubmitting(true);
1118 1125 try {
1119 1126 await updateLabelType(type.id, {
1120 1127 ...form,
1121 1128 typeCode: type.typeCode ?? "",
1122   - groupIds,
1123   - regionIds: groupIds,
1124   - locationIds: locPayload.locationIds,
  1129 + ...scopePayload.body,
1125 1130 });
1126 1131 toast.success("Label type updated.", {
1127 1132 description: "The label type has been updated successfully.",
... ... @@ -1188,6 +1193,11 @@ function EditLabelTypeDialog({
1188 1193 onLocationChange={setSelectedLocationIds}
1189 1194 requireCompanySelection={requireCompanySelection}
1190 1195 fixedPartnerId={fixedPartnerId}
  1196 + templateScopeMode={requireCompanySelection}
  1197 + selectedPartnerIds={selectedPartnerIds}
  1198 + onPartnerIdsChange={setSelectedPartnerIds}
  1199 + selectedRegionIds={selectedRegionIds}
  1200 + onRegionIdsChange={setSelectedRegionIds}
1191 1201 />
1192 1202 )}
1193 1203 </div>
... ...
美国版/Food Labeling Management Platform/src/components/labels/MultipleOptionsView.tsx
... ... @@ -51,11 +51,9 @@ import { getLocations } from &quot;../../services/locationService&quot;;
51 51 import { getGroups } from "../../services/groupService";
52 52 import { getPartners } from "../../services/partnerService";
53 53 import {
54   - buildSpecifiedLocationPayload,
55   - effectiveScopePartnerId,
  54 + buildEntityScopeSaveFromForm,
56 55 hydrateCategoryScopeFromLocationIds,
57   - regionNamesToGroupIds,
58   - scopePartnerValidationMessage,
  56 + hydrateEntityScopeFromDto,
59 57 } from "../../lib/categoryScopeForm";
60 58 import { CategoryScopeFields } from "../shared/category-scope-fields";
61 59 import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth";
... ... @@ -806,7 +804,9 @@ function CreateMultipleOptionDialog({
806 804 }) {
807 805 const [submitting, setSubmitting] = useState(false);
808 806 const [selectedPartnerId, setSelectedPartnerId] = useState("");
  807 + const [selectedPartnerIds, setSelectedPartnerIds] = useState<string[]>([]);
809 808 const [selectedRegionNames, setSelectedRegionNames] = useState<string[]>([]);
  809 + const [selectedRegionIds, setSelectedRegionIds] = useState<string[]>([]);
810 810 const [selectedLocationIds, setSelectedLocationIds] = useState<string[]>([]);
811 811 const [form, setForm] = useState<LabelMultipleOptionCreateInput>({
812 812 optionName: "",
... ... @@ -818,7 +818,9 @@ function CreateMultipleOptionDialog({
818 818  
819 819 const resetForm = () => {
820 820 setSelectedPartnerId("");
  821 + setSelectedPartnerIds([]);
821 822 setSelectedRegionNames([]);
  823 + setSelectedRegionIds([]);
822 824 setSelectedLocationIds([]);
823 825 setForm({
824 826 optionName: "",
... ... @@ -876,40 +878,29 @@ function CreateMultipleOptionDialog({
876 878 return;
877 879 }
878 880  
879   - const scopePartnerId = effectiveScopePartnerId(
  881 + const scopePayload = buildEntityScopeSaveFromForm({
880 882 requireCompanySelection,
881 883 selectedPartnerId,
  884 + selectedPartnerIds,
  885 + selectedRegionNames,
  886 + selectedRegionIds,
  887 + selectedLocationIds,
882 888 fixedPartnerId,
883   - );
884   - const partnerErr = scopePartnerValidationMessage(requireCompanySelection, scopePartnerId);
885   - if (partnerErr) {
886   - toast.error("Validation failed", { description: partnerErr });
887   - return;
888   - }
889   -
890   - const locPayload = buildSpecifiedLocationPayload(
891 889 locations,
892 890 partners,
893 891 groups,
894   - scopePartnerId,
895   - selectedRegionNames,
896   - selectedLocationIds,
897   - );
898   - if (!locPayload.ok) {
899   - toast.error("Validation failed", { description: locPayload.message });
  892 + });
  893 + if (!scopePayload.ok) {
  894 + toast.error("Validation failed", { description: scopePayload.message });
900 895 return;
901 896 }
902 897  
903   - const groupIds = regionNamesToGroupIds(selectedRegionNames, groups, scopePartnerId);
904   -
905 898 setSubmitting(true);
906 899 try {
907 900 await createLabelMultipleOption({
908 901 ...form,
909 902 optionCode: optionCodeFromName(form.optionName),
910   - groupIds,
911   - regionIds: groupIds,
912   - locationIds: locPayload.locationIds,
  903 + ...scopePayload.body,
913 904 });
914 905 toast.success("Multiple option created.", {
915 906 description: "The multiple option has been created successfully.",
... ... @@ -1009,6 +1000,11 @@ function CreateMultipleOptionDialog({
1009 1000 onLocationChange={setSelectedLocationIds}
1010 1001 requireCompanySelection={requireCompanySelection}
1011 1002 fixedPartnerId={fixedPartnerId}
  1003 + templateScopeMode={requireCompanySelection}
  1004 + selectedPartnerIds={selectedPartnerIds}
  1005 + onPartnerIdsChange={setSelectedPartnerIds}
  1006 + selectedRegionIds={selectedRegionIds}
  1007 + onRegionIdsChange={setSelectedRegionIds}
1012 1008 />
1013 1009 </div>
1014 1010  
... ... @@ -1049,7 +1045,9 @@ function EditMultipleOptionDialog({
1049 1045 const [submitting, setSubmitting] = useState(false);
1050 1046 const [loadingDetail, setLoadingDetail] = useState(false);
1051 1047 const [selectedPartnerId, setSelectedPartnerId] = useState("");
  1048 + const [selectedPartnerIds, setSelectedPartnerIds] = useState<string[]>([]);
1052 1049 const [selectedRegionNames, setSelectedRegionNames] = useState<string[]>([]);
  1050 + const [selectedRegionIds, setSelectedRegionIds] = useState<string[]>([]);
1053 1051 const [selectedLocationIds, setSelectedLocationIds] = useState<string[]>([]);
1054 1052 const [form, setForm] = useState<LabelMultipleOptionUpdateInput>({
1055 1053 optionName: "",
... ... @@ -1078,12 +1076,24 @@ function EditMultipleOptionDialog({
1078 1076 });
1079 1077 setNewValue("");
1080 1078  
  1079 + if (requireCompanySelection) {
  1080 + const scope = hydrateEntityScopeFromDto(detail, locations, partners, groups);
  1081 + setSelectedPartnerIds(scope.partnerIds);
  1082 + setSelectedRegionIds(scope.regionIds);
  1083 + setSelectedLocationIds(scope.locationIds);
  1084 + setSelectedPartnerId("");
  1085 + setSelectedRegionNames([]);
  1086 + return;
  1087 + }
  1088 +
1081 1089 const lids = (detail.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean);
1082 1090 if (lids.length > 0) {
1083 1091 const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups);
1084 1092 setSelectedPartnerId(scope.partnerId);
1085 1093 setSelectedRegionNames(scope.regionNames);
1086 1094 setSelectedLocationIds(scope.locationIds);
  1095 + setSelectedPartnerIds([]);
  1096 + setSelectedRegionIds([]);
1087 1097 return;
1088 1098 }
1089 1099  
... ... @@ -1099,6 +1109,8 @@ function EditMultipleOptionDialog({
1099 1109 setSelectedPartnerId(pid);
1100 1110 setSelectedRegionNames(names);
1101 1111 setSelectedLocationIds([]);
  1112 + setSelectedPartnerIds([]);
  1113 + setSelectedRegionIds([]);
1102 1114 return;
1103 1115 }
1104 1116  
... ... @@ -1114,6 +1126,8 @@ function EditMultipleOptionDialog({
1114 1126 setSelectedRegionNames([]);
1115 1127 setSelectedLocationIds([]);
1116 1128 }
  1129 + setSelectedPartnerIds([]);
  1130 + setSelectedRegionIds([]);
1117 1131 } catch {
1118 1132 if (ac.signal.aborted) return;
1119 1133 setForm({
... ... @@ -1123,7 +1137,9 @@ function EditMultipleOptionDialog({
1123 1137 orderNum: option.orderNum ?? null,
1124 1138 });
1125 1139 setSelectedPartnerId("");
  1140 + setSelectedPartnerIds([]);
1126 1141 setSelectedRegionNames([]);
  1142 + setSelectedRegionIds([]);
1127 1143 setSelectedLocationIds([]);
1128 1144 setNewValue("");
1129 1145 } finally {
... ... @@ -1132,7 +1148,7 @@ function EditMultipleOptionDialog({
1132 1148 })();
1133 1149  
1134 1150 return () => ac.abort();
1135   - }, [open, option, locations, partners, groups]);
  1151 + }, [open, option, locations, partners, groups, requireCompanySelection]);
1136 1152  
1137 1153 const addValue = () => {
1138 1154 const trimmed = newValue.trim();
... ... @@ -1176,40 +1192,29 @@ function EditMultipleOptionDialog({
1176 1192 return;
1177 1193 }
1178 1194  
1179   - const scopePartnerId = effectiveScopePartnerId(
  1195 + const scopePayload = buildEntityScopeSaveFromForm({
1180 1196 requireCompanySelection,
1181 1197 selectedPartnerId,
  1198 + selectedPartnerIds,
  1199 + selectedRegionNames,
  1200 + selectedRegionIds,
  1201 + selectedLocationIds,
1182 1202 fixedPartnerId,
1183   - );
1184   - const partnerErr = scopePartnerValidationMessage(requireCompanySelection, scopePartnerId);
1185   - if (partnerErr) {
1186   - toast.error("Validation failed", { description: partnerErr });
1187   - return;
1188   - }
1189   -
1190   - const locPayload = buildSpecifiedLocationPayload(
1191 1203 locations,
1192 1204 partners,
1193 1205 groups,
1194   - scopePartnerId,
1195   - selectedRegionNames,
1196   - selectedLocationIds,
1197   - );
1198   - if (!locPayload.ok) {
1199   - toast.error("Validation failed", { description: locPayload.message });
  1206 + });
  1207 + if (!scopePayload.ok) {
  1208 + toast.error("Validation failed", { description: scopePayload.message });
1200 1209 return;
1201 1210 }
1202 1211  
1203   - const groupIds = regionNamesToGroupIds(selectedRegionNames, groups, scopePartnerId);
1204   -
1205 1212 setSubmitting(true);
1206 1213 try {
1207 1214 await updateLabelMultipleOption(option.id, {
1208 1215 ...form,
1209 1216 optionCode: option.optionCode ?? "",
1210   - groupIds,
1211   - regionIds: groupIds,
1212   - locationIds: locPayload.locationIds,
  1217 + ...scopePayload.body,
1213 1218 });
1214 1219 toast.success("Multiple option updated.", {
1215 1220 description: "The multiple option has been updated successfully.",
... ... @@ -1312,6 +1317,11 @@ function EditMultipleOptionDialog({
1312 1317 onLocationChange={setSelectedLocationIds}
1313 1318 requireCompanySelection={requireCompanySelection}
1314 1319 fixedPartnerId={fixedPartnerId}
  1320 + templateScopeMode={requireCompanySelection}
  1321 + selectedPartnerIds={selectedPartnerIds}
  1322 + onPartnerIdsChange={setSelectedPartnerIds}
  1323 + selectedRegionIds={selectedRegionIds}
  1324 + onRegionIdsChange={setSelectedRegionIds}
1315 1325 />
1316 1326 )}
1317 1327 </div>
... ...
美国版/Food Labeling Management Platform/src/components/shared/category-scope-fields.tsx
... ... @@ -390,15 +390,30 @@ export function CategoryScopeFields({
390 390 {requireCompanySelection ? (
391 391 <div className="space-y-2">
392 392 <Label>Company *</Label>
393   - <SearchableSelect
394   - value={selectedPartnerId}
395   - onValueChange={onCompanyChange}
396   - options={companyOptions}
397   - placeholder="Select company"
398   - searchPlaceholder="Search company…"
399   - emptyText="No companies found."
400   - />
401   - <p className="text-xs text-gray-500">Required. Select company before region and location.</p>
  393 + {templateScopeMode ? (
  394 + <SearchableMultiSelect
  395 + values={selectedPartnerIds}
  396 + onValuesChange={onCompaniesChange}
  397 + options={companyOptions}
  398 + placeholder="All Companies"
  399 + searchPlaceholder="Search companies…"
  400 + selectAllRowLabel="Select All"
  401 + />
  402 + ) : (
  403 + <SearchableSelect
  404 + value={selectedPartnerId}
  405 + onValueChange={onCompanyChange}
  406 + options={companyOptions}
  407 + placeholder="Select company"
  408 + searchPlaceholder="Search company…"
  409 + emptyText="No companies found."
  410 + />
  411 + )}
  412 + <p className="text-xs text-gray-500">
  413 + {templateScopeMode
  414 + ? "Required. Leave empty or select all for every company; narrows region and location lists."
  415 + : "Required. Select company before region and location."}
  416 + </p>
402 417 </div>
403 418 ) : null}
404 419  
... ... @@ -406,16 +421,24 @@ export function CategoryScopeFields({
406 421 <div className="space-y-2">
407 422 <Label>Region *</Label>
408 423 <SearchableMultiSelect
409   - values={selectedRegionNames}
410   - onValuesChange={onRegionMultiChange}
  424 + values={templateScopeMode ? selectedRegionIds : selectedRegionNames}
  425 + onValuesChange={templateScopeMode ? onRegionIdsMultiChange : onRegionMultiChange}
411 426 options={regionOptions}
412 427 placeholder={regionPlaceholder}
413 428 searchPlaceholder="Search regions…"
414 429 selectAllRowLabel="Select All"
415   - disabled={!companyReady}
  430 + disabled={!templateScopeMode && !companyReady}
416 431 />
417 432 <p className="text-xs text-gray-500">
418   - {companyReady ? "Required. Narrows the location list." : requireCompanySelection ? "Select a company first." : "Your account company is loading or not assigned."}
  433 + {templateScopeMode
  434 + ? companyReady
  435 + ? "Required. Leave empty or select all for every region in selected companies."
  436 + : "Select company first."
  437 + : companyReady
  438 + ? "Required. Narrows the location list."
  439 + : requireCompanySelection
  440 + ? "Select a company first."
  441 + : "Your account company is loading or not assigned."}
419 442 </p>
420 443 </div>
421 444 <div className="space-y-2">
... ... @@ -425,22 +448,34 @@ export function CategoryScopeFields({
425 448 onValuesChange={onLocationChange}
426 449 options={locationOptionsForPicker}
427 450 placeholder={
428   - companyReady
429   - ? "Select location(s)…"
430   - : requireCompanySelection
431   - ? "Select company first"
432   - : "Loading company scope…"
  451 + templateScopeMode
  452 + ? companyReady
  453 + ? "All Locations"
  454 + : requireCompanySelection
  455 + ? "Select company first"
  456 + : "Loading company scope…"
  457 + : companyReady
  458 + ? "Select location(s)…"
  459 + : requireCompanySelection
  460 + ? "Select company first"
  461 + : "Loading company scope…"
433 462 }
434 463 searchPlaceholder="Search locations…"
435 464 selectAllRowLabel="ALL"
436   - disabled={!companyReady}
  465 + disabled={!templateScopeMode && !companyReady}
437 466 />
438 467 <p className="text-xs text-gray-500">
439   - {companyReady
440   - ? "Required. ALL selects every location in the filtered list (within selected regions)."
441   - : requireCompanySelection
442   - ? "Select a company first."
443   - : "Your account company is loading or not assigned."}
  468 + {templateScopeMode
  469 + ? companyReady
  470 + ? "Required. ALL selects every location in the filtered list (within selected regions)."
  471 + : requireCompanySelection
  472 + ? "Select a company first."
  473 + : "Your account company is loading or not assigned."
  474 + : companyReady
  475 + ? "Required. ALL selects every location in the filtered list (within selected regions)."
  476 + : requireCompanySelection
  477 + ? "Select a company first."
  478 + : "Your account company is loading or not assigned."}
444 479 </p>
445 480 </div>
446 481 </div>
... ...
美国版/Food Labeling Management Platform/src/lib/categoryScopeForm.ts
1 1 import type { GroupListItem } from "../types/group";
2 2 import type { LocationDto } from "../types/location";
3 3 import type { PartnerListItem } from "../types/partner";
  4 +import type { AppliedScopeType } from "../types/labelTemplate";
4 5 import { parseAppliedScopeType } from "../types/labelTemplate";
5 6  
6 7 /** 表单提交用的公司 Id(非管理员取账号绑定公司) */
... ... @@ -424,3 +425,223 @@ export function buildProductScopeSavePayload(
424 425 }
425 426 return { locationIds: locIds };
426 427 }
  428 +
  429 +function resolveScopeDimensionForSave(
  430 + selected: string[],
  431 + available: string[],
  432 +): { type: AppliedScopeType; ids: string[] } {
  433 + const avail = dedupeIds(available);
  434 + const sel = dedupeIds(selected).filter((id) => !avail.length || avail.includes(id));
  435 + if (!avail.length || !sel.length) {
  436 + return { type: "ALL", ids: [] };
  437 + }
  438 + if (sel.length >= avail.length && avail.every((id) => sel.includes(id))) {
  439 + return { type: "ALL", ids: [] };
  440 + }
  441 + return { type: "SPECIFIED", ids: sel };
  442 +}
  443 +
  444 +/** 模板/实体表单:按当前选中 Id 计算可选 Company / Region / Location 全集 */
  445 +export function computeTemplateScopeAvailableIds(
  446 + partners: PartnerListItem[],
  447 + groups: GroupListItem[],
  448 + locations: LocationDto[],
  449 + selectedPartnerIds: string[],
  450 + selectedRegionIds: string[],
  451 +): {
  452 + availablePartnerIds: string[];
  453 + availableRegionIds: string[];
  454 + availableLocationIds: string[];
  455 +} {
  456 + const availablePartnerIds = partners.map((p) => String(p.id ?? "").trim()).filter(Boolean);
  457 + const effectivePartnerIds =
  458 + selectedPartnerIds.length > 0 ? selectedPartnerIds : availablePartnerIds;
  459 + const availableRegionIds = regionOptionsForPartners(groups, effectivePartnerIds).map(
  460 + (o) => o.value,
  461 + );
  462 + const availableLocationIds = locationsScopedForTemplateScope(
  463 + locations,
  464 + partners,
  465 + groups,
  466 + effectivePartnerIds,
  467 + selectedRegionIds,
  468 + ).map((l) => l.id);
  469 + return { availablePartnerIds, availableRegionIds, availableLocationIds };
  470 +}
  471 +
  472 +export type EntityScopePayloadBody = {
  473 + appliedPartnerType: AppliedScopeType;
  474 + partnerIds: string[];
  475 + companyIds: string[];
  476 + regionIds: string[];
  477 + groupIds: string[];
  478 + locationIds: string[];
  479 + availabilityType: "ALL" | "SPECIFIED";
  480 +};
  481 +
  482 +/**
  483 + * 标签类型 / 分类 / 多选项保存:Company + Region + Location 三维适用范围。
  484 + * 空选或全选当前可选项 → 该维度 ALL;部分选中 → SPECIFIED + Id 数组。
  485 + */
  486 +export function buildEntityPartnerScopePayload(input: {
  487 + selectedPartnerIds: string[];
  488 + selectedRegionIds: string[];
  489 + selectedLocationIds: string[];
  490 + availablePartnerIds: string[];
  491 + availableRegionIds: string[];
  492 + availableLocationIds: string[];
  493 +}): { ok: true; body: EntityScopePayloadBody } | { ok: false; message: string } {
  494 + const partnerDim = resolveScopeDimensionForSave(
  495 + input.selectedPartnerIds,
  496 + input.availablePartnerIds,
  497 + );
  498 + const regionDim = resolveScopeDimensionForSave(
  499 + input.selectedRegionIds,
  500 + input.availableRegionIds,
  501 + );
  502 + const locationDim = resolveScopeDimensionForSave(
  503 + input.selectedLocationIds,
  504 + input.availableLocationIds,
  505 + );
  506 +
  507 + const locationSpecified =
  508 + regionDim.type === "SPECIFIED" || locationDim.type === "SPECIFIED";
  509 + const availabilityType: "ALL" | "SPECIFIED" = locationSpecified ? "SPECIFIED" : "ALL";
  510 +
  511 + const anySpecified =
  512 + partnerDim.type === "SPECIFIED" || locationSpecified;
  513 + const effectiveLocationIds =
  514 + locationDim.type === "SPECIFIED" ? locationDim.ids : input.availableLocationIds;
  515 +
  516 + if (anySpecified && effectiveLocationIds.length === 0) {
  517 + return {
  518 + ok: false,
  519 + message: "Please adjust company, region, or location so at least one store matches.",
  520 + };
  521 + }
  522 +
  523 + const partnerIds = partnerDim.type === "SPECIFIED" ? partnerDim.ids : [];
  524 + const regionIds = regionDim.type === "SPECIFIED" ? regionDim.ids : [];
  525 + const locationIds = locationDim.type === "SPECIFIED" ? locationDim.ids : [];
  526 +
  527 + return {
  528 + ok: true,
  529 + body: {
  530 + appliedPartnerType: partnerDim.type,
  531 + partnerIds,
  532 + companyIds: partnerIds,
  533 + regionIds,
  534 + groupIds: regionIds,
  535 + locationIds,
  536 + availabilityType,
  537 + },
  538 + };
  539 +}
  540 +
  541 +/** 创建/编辑弹窗:按管理员多选或单公司模式构建保存 Body */
  542 +export function buildEntityScopeSaveFromForm(input: {
  543 + requireCompanySelection: boolean;
  544 + selectedPartnerId: string;
  545 + selectedPartnerIds: string[];
  546 + selectedRegionNames: string[];
  547 + selectedRegionIds: string[];
  548 + selectedLocationIds: string[];
  549 + fixedPartnerId: string;
  550 + locations: LocationDto[];
  551 + partners: PartnerListItem[];
  552 + groups: GroupListItem[];
  553 +}): { ok: true; body: EntityScopePayloadBody } | { ok: false; message: string } {
  554 + if (input.requireCompanySelection) {
  555 + const { availablePartnerIds, availableRegionIds, availableLocationIds } =
  556 + computeTemplateScopeAvailableIds(
  557 + input.partners,
  558 + input.groups,
  559 + input.locations,
  560 + input.selectedPartnerIds,
  561 + input.selectedRegionIds,
  562 + );
  563 + return buildEntityPartnerScopePayload({
  564 + selectedPartnerIds: input.selectedPartnerIds,
  565 + selectedRegionIds: input.selectedRegionIds,
  566 + selectedLocationIds: input.selectedLocationIds,
  567 + availablePartnerIds,
  568 + availableRegionIds,
  569 + availableLocationIds,
  570 + });
  571 + }
  572 +
  573 + const scopePartnerId = effectiveScopePartnerId(
  574 + false,
  575 + input.selectedPartnerId,
  576 + input.fixedPartnerId,
  577 + );
  578 + const partnerErr = scopePartnerValidationMessage(false, scopePartnerId);
  579 + if (partnerErr) {
  580 + return { ok: false, message: partnerErr };
  581 + }
  582 +
  583 + const locPayload = buildSpecifiedLocationPayload(
  584 + input.locations,
  585 + input.partners,
  586 + input.groups,
  587 + scopePartnerId,
  588 + input.selectedRegionNames,
  589 + input.selectedLocationIds,
  590 + );
  591 + if (!locPayload.ok) {
  592 + return { ok: false, message: locPayload.message };
  593 + }
  594 +
  595 + const groupIds = regionNamesToGroupIds(
  596 + input.selectedRegionNames,
  597 + input.groups,
  598 + scopePartnerId,
  599 + );
  600 + const partnerIds = scopePartnerId ? [scopePartnerId] : [];
  601 + return {
  602 + ok: true,
  603 + body: {
  604 + appliedPartnerType: partnerIds.length ? "SPECIFIED" : "ALL",
  605 + partnerIds,
  606 + companyIds: partnerIds,
  607 + regionIds: groupIds,
  608 + groupIds,
  609 + locationIds: locPayload.locationIds,
  610 + availabilityType: "SPECIFIED",
  611 + },
  612 + };
  613 +}
  614 +
  615 +/** 编辑弹窗:从详情 DTO 回填三维 Id 数组(管理员多选模式) */
  616 +export function hydrateEntityScopeFromDto(
  617 + dto: {
  618 + appliedPartnerType?: string | null;
  619 + appliedRegionType?: string | null;
  620 + appliedLocation?: string | null;
  621 + appliedLocationType?: string | null;
  622 + availabilityType?: string | null;
  623 + partnerIds?: string[] | null;
  624 + companyIds?: string[] | null;
  625 + regionIds?: string[] | null;
  626 + groupIds?: string[] | null;
  627 + locationIds?: string[] | null;
  628 + appliedLocationIds?: string[] | null;
  629 + },
  630 + locations: LocationDto[],
  631 + partners: PartnerListItem[],
  632 + groups: GroupListItem[],
  633 +): { partnerIds: string[]; regionIds: string[]; locationIds: string[] } {
  634 + return hydrateLabelTemplateScopeFromDto(
  635 + {
  636 + ...dto,
  637 + appliedLocation:
  638 + dto.appliedLocation ??
  639 + (String(dto.availabilityType ?? "").trim().toUpperCase() === "SPECIFIED"
  640 + ? "SPECIFIED"
  641 + : dto.appliedLocation),
  642 + },
  643 + locations,
  644 + partners,
  645 + groups,
  646 + );
  647 +}
... ...
美国版/Food Labeling Management Platform/src/services/labelCategoryService.ts
... ... @@ -22,6 +22,13 @@ const PATH = &quot;/label-category&quot;;
22 22  
23 23 function normalizeLabelCategoryDto(raw: unknown): LabelCategoryDto {
24 24 const r = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>;
  25 + const parseIds = (v: unknown): string[] | undefined => {
  26 + if (!Array.isArray(v)) return undefined;
  27 + return [...new Set(v.map((x) => String(x).trim()).filter(Boolean))];
  28 + };
  29 + const groupIds = parseIds(r.groupIds ?? r.GroupIds ?? r.regionIds ?? r.RegionIds);
  30 + const locationIds = parseIds(r.locationIds ?? r.LocationIds);
  31 + const partnerIds = parseIds(r.partnerIds ?? r.PartnerIds ?? r.companyIds ?? r.CompanyIds);
25 32 const noRaw = r.noOfLabels ?? r.NoOfLabels;
26 33 const noOfLabels =
27 34 typeof noRaw === "number" && Number.isFinite(noRaw)
... ... @@ -36,7 +43,60 @@ function normalizeLabelCategoryDto(raw: unknown): LabelCategoryDto {
36 43 : typeof orderRaw === "string" && orderRaw.trim() !== "" && Number.isFinite(Number(orderRaw))
37 44 ? Number(orderRaw)
38 45 : null;
39   - return { ...(r as object), id: String(r.id ?? r.Id ?? ""), noOfLabels, orderNum } as LabelCategoryDto;
  46 + const companyRaw = r.company ?? r.Company;
  47 + return {
  48 + ...(r as object),
  49 + id: String(r.id ?? r.Id ?? ""),
  50 + appliedPartnerType: String(r.appliedPartnerType ?? r.AppliedPartnerType ?? "").trim() || undefined,
  51 + partnerIds,
  52 + companyIds: partnerIds,
  53 + availabilityType: String(r.availabilityType ?? r.AvailabilityType ?? "").trim() || undefined,
  54 + groupIds,
  55 + regionIds: groupIds,
  56 + locationIds,
  57 + company: typeof companyRaw === "string" ? companyRaw : null,
  58 + noOfLabels,
  59 + orderNum,
  60 + } as LabelCategoryDto;
  61 +}
  62 +
  63 +function scopeBodyFromInput(input: LabelCategoryCreateInput): Record<string, unknown> {
  64 + const groupIds = [
  65 + ...new Set(
  66 + [...(input.groupIds ?? []), ...(input.regionIds ?? [])]
  67 + .map((x) => String(x).trim())
  68 + .filter(Boolean),
  69 + ),
  70 + ];
  71 + const locationIds = [
  72 + ...new Set((input.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean)),
  73 + ];
  74 + const partnerIds = [
  75 + ...new Set(
  76 + [...(input.partnerIds ?? []), ...(input.companyIds ?? [])]
  77 + .map((x) => String(x).trim())
  78 + .filter(Boolean),
  79 + ),
  80 + ];
  81 + const body: Record<string, unknown> = {};
  82 + const appliedPartnerType = String(input.appliedPartnerType ?? "").trim().toUpperCase();
  83 + if (appliedPartnerType === "ALL" || appliedPartnerType === "SPECIFIED") {
  84 + body.appliedPartnerType = appliedPartnerType;
  85 + }
  86 + if (partnerIds.length) {
  87 + body.partnerIds = partnerIds;
  88 + body.companyIds = partnerIds;
  89 + }
  90 + const availabilityType = String(input.availabilityType ?? "ALL").trim().toUpperCase() || "ALL";
  91 + body.availabilityType = availabilityType;
  92 + if (groupIds.length) {
  93 + body.groupIds = groupIds;
  94 + body.regionIds = groupIds;
  95 + }
  96 + if (locationIds.length) {
  97 + body.locationIds = locationIds;
  98 + }
  99 + return body;
40 100 }
41 101  
42 102 export async function getLabelCategories(input: LabelCategoryGetListInput, signal?: AbortSignal): Promise<PagedResultDto<LabelCategoryDto>> {
... ... @@ -60,11 +120,12 @@ export async function getLabelCategories(input: LabelCategoryGetListInput, signa
60 120 }
61 121  
62 122 export async function getLabelCategory(id: string, signal?: AbortSignal): Promise<LabelCategoryDto> {
63   - return api.requestJson<LabelCategoryDto>({
  123 + const raw = await api.requestJson<unknown>({
64 124 path: `${PATH}/${encodeURIComponent(id)}`,
65 125 method: "GET",
66 126 signal,
67 127 });
  128 + return normalizeLabelCategoryDto(raw);
68 129 }
69 130  
70 131 export async function createLabelCategory(input: LabelCategoryCreateInput): Promise<LabelCategoryDto> {
... ... @@ -82,17 +143,13 @@ export async function createLabelCategory(input: LabelCategoryCreateInput): Prom
82 143 buttonStyleJson: (input.buttonStyleJson ?? "").trim() || null,
83 144 state: input.state ?? true,
84 145 orderNum: input.orderNum,
85   - availabilityType: String(input.availabilityType ?? "ALL").trim().toUpperCase() || "ALL",
86   - locationIds:
87   - Array.isArray(input.locationIds) && input.locationIds.length
88   - ? [...new Set(input.locationIds.map((x) => String(x).trim()).filter(Boolean))]
89   - : null,
  146 + ...scopeBodyFromInput(input),
90 147 },
91 148 });
92 149 }
93 150  
94 151 export async function updateLabelCategory(id: string, input: LabelCategoryUpdateInput): Promise<LabelCategoryDto> {
95   - return api.requestJson<LabelCategoryDto>({
  152 + const raw = await api.requestJson<unknown>({
96 153 path: `${PATH}/${encodeURIComponent(id)}`,
97 154 method: "PUT",
98 155 body: {
... ... @@ -106,13 +163,10 @@ export async function updateLabelCategory(id: string, input: LabelCategoryUpdate
106 163 buttonStyleJson: (input.buttonStyleJson ?? "").trim() || null,
107 164 state: input.state ?? true,
108 165 orderNum: input.orderNum,
109   - availabilityType: String(input.availabilityType ?? "ALL").trim().toUpperCase() || "ALL",
110   - locationIds:
111   - Array.isArray(input.locationIds) && input.locationIds.length
112   - ? [...new Set(input.locationIds.map((x) => String(x).trim()).filter(Boolean))]
113   - : null,
  166 + ...scopeBodyFromInput(input),
114 167 },
115 168 });
  169 + return normalizeLabelCategoryDto(raw);
116 170 }
117 171  
118 172 export async function deleteLabelCategory(id: string): Promise<void> {
... ...
美国版/Food Labeling Management Platform/src/services/labelMultipleOptionService.ts
... ... @@ -63,13 +63,35 @@ function normalizeLabelMultipleOptionDto(item: LabelMultipleOptionDto): LabelMul
63 63 const locationIds = parseIds(
64 64 (raw as Record<string, unknown>).locationIds ?? (raw as Record<string, unknown>).LocationIds,
65 65 );
  66 + const partnerIds = parseIds(
  67 + (raw as Record<string, unknown>).partnerIds ??
  68 + (raw as Record<string, unknown>).PartnerIds ??
  69 + (raw as Record<string, unknown>).companyIds ??
  70 + (raw as Record<string, unknown>).CompanyIds,
  71 + );
  72 + const companyRaw = (raw as Record<string, unknown>).company ?? (raw as Record<string, unknown>).Company;
66 73 return {
67 74 ...item,
68 75 optionValuesJson: parseOptionValuesJsonField(raw.optionValuesJson),
69 76 lastEdited,
  77 + appliedPartnerType:
  78 + String(
  79 + (raw as Record<string, unknown>).appliedPartnerType ??
  80 + (raw as Record<string, unknown>).AppliedPartnerType ??
  81 + "",
  82 + ).trim() || undefined,
  83 + partnerIds,
  84 + companyIds: partnerIds,
  85 + availabilityType:
  86 + String(
  87 + (raw as Record<string, unknown>).availabilityType ??
  88 + (raw as Record<string, unknown>).AvailabilityType ??
  89 + "",
  90 + ).trim() || undefined,
70 91 groupIds,
71 92 regionIds: groupIds,
72 93 locationIds,
  94 + company: typeof companyRaw === "string" ? companyRaw : null,
73 95 };
74 96 }
75 97  
... ... @@ -96,11 +118,34 @@ function scopeBodyFromInput(input: LabelMultipleOptionCreateInput): Record&lt;strin
96 118 const locationIds = [
97 119 ...new Set((input.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean)),
98 120 ];
99   - return {
100   - groupIds: groupIds.length ? groupIds : undefined,
101   - regionIds: groupIds.length ? groupIds : undefined,
102   - locationIds: locationIds.length ? locationIds : undefined,
103   - };
  121 + const partnerIds = [
  122 + ...new Set(
  123 + [...(input.partnerIds ?? []), ...(input.companyIds ?? [])]
  124 + .map((x) => String(x).trim())
  125 + .filter(Boolean),
  126 + ),
  127 + ];
  128 + const body: Record<string, unknown> = {};
  129 + const appliedPartnerType = String(input.appliedPartnerType ?? "").trim().toUpperCase();
  130 + if (appliedPartnerType === "ALL" || appliedPartnerType === "SPECIFIED") {
  131 + body.appliedPartnerType = appliedPartnerType;
  132 + }
  133 + if (partnerIds.length) {
  134 + body.partnerIds = partnerIds;
  135 + body.companyIds = partnerIds;
  136 + }
  137 + const availabilityType = String(input.availabilityType ?? "").trim().toUpperCase();
  138 + if (availabilityType === "ALL" || availabilityType === "SPECIFIED") {
  139 + body.availabilityType = availabilityType;
  140 + }
  141 + if (groupIds.length) {
  142 + body.groupIds = groupIds;
  143 + body.regionIds = groupIds;
  144 + }
  145 + if (locationIds.length) {
  146 + body.locationIds = locationIds;
  147 + }
  148 + return body;
104 149 }
105 150  
106 151 export async function getLabelMultipleOptions(input: LabelMultipleOptionGetListInput, signal?: AbortSignal): Promise<PagedResultDto<LabelMultipleOptionDto>> {
... ...
美国版/Food Labeling Management Platform/src/services/labelTypeService.ts
... ... @@ -27,6 +27,10 @@ function normalizeLabelTypeDto(raw: unknown): LabelTypeDto {
27 27 };
28 28 const groupIds = parseIds(r.groupIds ?? r.GroupIds ?? r.regionIds ?? r.RegionIds);
29 29 const locationIds = parseIds(r.locationIds ?? r.LocationIds);
  30 + const partnerIds = parseIds(r.partnerIds ?? r.PartnerIds ?? r.companyIds ?? r.CompanyIds);
  31 + const appliedPartnerType = String(r.appliedPartnerType ?? r.AppliedPartnerType ?? "").trim() || undefined;
  32 + const availabilityType = String(r.availabilityType ?? r.AvailabilityType ?? "").trim() || undefined;
  33 + const companyRaw = r.company ?? r.Company;
30 34 const noRaw = r.noOfLabels ?? r.NoOfLabels;
31 35 const noOfLabels =
32 36 typeof noRaw === "number" && Number.isFinite(noRaw)
... ... @@ -46,9 +50,14 @@ function normalizeLabelTypeDto(raw: unknown): LabelTypeDto {
46 50 return {
47 51 ...(r as object),
48 52 id: String(r.id ?? r.Id ?? ""),
  53 + appliedPartnerType,
  54 + partnerIds,
  55 + companyIds: partnerIds,
  56 + availabilityType,
49 57 groupIds,
50 58 regionIds: groupIds,
51 59 locationIds,
  60 + company: typeof companyRaw === "string" ? companyRaw : null,
52 61 noOfLabels,
53 62 lastEdited,
54 63 region: typeof regionRaw === "string" ? regionRaw : null,
... ... @@ -67,11 +76,34 @@ function scopeBodyFromInput(input: LabelTypeCreateInput): Record&lt;string, unknown
67 76 const locationIds = [
68 77 ...new Set((input.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean)),
69 78 ];
70   - return {
71   - groupIds: groupIds.length ? groupIds : undefined,
72   - regionIds: groupIds.length ? groupIds : undefined,
73   - locationIds: locationIds.length ? locationIds : undefined,
74   - };
  79 + const partnerIds = [
  80 + ...new Set(
  81 + [...(input.partnerIds ?? []), ...(input.companyIds ?? [])]
  82 + .map((x) => String(x).trim())
  83 + .filter(Boolean),
  84 + ),
  85 + ];
  86 + const body: Record<string, unknown> = {};
  87 + const appliedPartnerType = String(input.appliedPartnerType ?? "").trim().toUpperCase();
  88 + if (appliedPartnerType === "ALL" || appliedPartnerType === "SPECIFIED") {
  89 + body.appliedPartnerType = appliedPartnerType;
  90 + }
  91 + if (partnerIds.length) {
  92 + body.partnerIds = partnerIds;
  93 + body.companyIds = partnerIds;
  94 + }
  95 + const availabilityType = String(input.availabilityType ?? "").trim().toUpperCase();
  96 + if (availabilityType === "ALL" || availabilityType === "SPECIFIED") {
  97 + body.availabilityType = availabilityType;
  98 + }
  99 + if (groupIds.length) {
  100 + body.groupIds = groupIds;
  101 + body.regionIds = groupIds;
  102 + }
  103 + if (locationIds.length) {
  104 + body.locationIds = locationIds;
  105 + }
  106 + return body;
75 107 }
76 108  
77 109 export async function getLabelTypes(input: LabelTypeGetListInput, signal?: AbortSignal): Promise<PagedResultDto<LabelTypeDto>> {
... ...
美国版/Food Labeling Management Platform/src/types/label.ts
... ... @@ -66,7 +66,7 @@ export type LabelCreateInput = {
66 66 /** 适用门店(至少 1 个;Select All 时为当前 Region 下全部门店 Id) */
67 67 locationIds: string[];
68 68 labelCategoryId: string;
69   - labelTypeId: string;
  69 + labelTypeId?: string | null;
70 70 productIds: string[]; // 至少 1 个
71 71 labelInfoJson?: Record<string, unknown> | null;
72 72 state?: boolean;
... ... @@ -81,7 +81,7 @@ export type LabelUpdateInput = {
81 81 /** 适用门店(至少 1 个) */
82 82 locationIds: string[];
83 83 labelCategoryId: string;
84   - labelTypeId: string;
  84 + labelTypeId?: string | null;
85 85 productIds: string[]; // 至少 1 个
86 86 labelInfoJson?: Record<string, unknown> | null;
87 87 state?: boolean;
... ...
美国版/Food Labeling Management Platform/src/types/labelCategory.ts
... ... @@ -18,10 +18,19 @@ export type LabelCategoryDto = {
18 18 /** 该分类下标签数量(列表列 No. of Label) */
19 19 noOfLabels?: number | null;
20 20 creationTime?: string | null;
  21 + /** 适用 Company:ALL / SPECIFIED */
  22 + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null;
  23 + partnerIds?: string[] | null;
  24 + companyIds?: string[] | null;
21 25 /** ALL / SPECIFIED */
22 26 availabilityType?: "ALL" | "SPECIFIED" | string | null;
  27 + /** SPECIFIED 时绑定的 Region Id */
  28 + regionIds?: string[] | null;
  29 + groupIds?: string[] | null;
23 30 /** SPECIFIED 时绑定的门店 Id */
24 31 locationIds?: string[] | null;
  32 + /** 列表:Company 展示 */
  33 + company?: string | null;
25 34 /** 最近编辑时间(列表接口映射 LastModificationTime ?? CreationTime) */
26 35 lastEdited?: string | null;
27 36 };
... ... @@ -56,7 +65,13 @@ export type LabelCategoryCreateInput = {
56 65 buttonStyleJson?: string | null;
57 66 state?: boolean;
58 67 orderNum?: number | null;
  68 + /** 适用 Company:ALL / SPECIFIED */
  69 + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null;
  70 + partnerIds?: string[] | null;
  71 + companyIds?: string[] | null;
59 72 availabilityType?: "ALL" | "SPECIFIED" | string | null;
  73 + regionIds?: string[] | null;
  74 + groupIds?: string[] | null;
60 75 locationIds?: string[] | null;
61 76 };
62 77  
... ...
美国版/Food Labeling Management Platform/src/types/labelMultipleOption.ts
... ... @@ -7,6 +7,12 @@ export type LabelMultipleOptionDto = {
7 7 state?: boolean | null;
8 8 orderNum?: number | null;
9 9 creationTime?: string | null;
  10 + /** 适用 Company:ALL / SPECIFIED */
  11 + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null;
  12 + partnerIds?: string[] | null;
  13 + companyIds?: string[] | null;
  14 + /** 门店可用范围:ALL / SPECIFIED */
  15 + availabilityType?: "ALL" | "SPECIFIED" | string | null;
10 16 /** 列表:最近编辑时间 */
11 17 lastEdited?: string | null;
12 18 /** 适用范围:Region Id(`fl_group.Id`) */
... ... @@ -14,6 +20,8 @@ export type LabelMultipleOptionDto = {
14 20 regionIds?: string[] | null;
15 21 /** 适用范围:门店 Id */
16 22 locationIds?: string[] | null;
  23 + /** 列表:Company 展示 */
  24 + company?: string | null;
17 25 };
18 26  
19 27 export type PagedResultDto<T> = {
... ... @@ -39,6 +47,12 @@ export type LabelMultipleOptionCreateInput = {
39 47 optionValuesJson: string[];
40 48 state?: boolean;
41 49 orderNum?: number | null;
  50 + /** 适用 Company:ALL / SPECIFIED */
  51 + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null;
  52 + partnerIds?: string[] | null;
  53 + companyIds?: string[] | null;
  54 + /** 门店可用范围:ALL / SPECIFIED */
  55 + availabilityType?: "ALL" | "SPECIFIED" | string | null;
42 56 groupIds?: string[] | null;
43 57 regionIds?: string[] | null;
44 58 locationIds?: string[] | null;
... ...
美国版/Food Labeling Management Platform/src/types/labelType.ts
... ... @@ -5,11 +5,19 @@ export type LabelTypeDto = {
5 5 state?: boolean | null;
6 6 orderNum?: number | null;
7 7 creationTime?: string | null;
  8 + /** 适用 Company:ALL / SPECIFIED */
  9 + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null;
  10 + partnerIds?: string[] | null;
  11 + companyIds?: string[] | null;
  12 + /** 门店可用范围:ALL / SPECIFIED */
  13 + availabilityType?: "ALL" | "SPECIFIED" | string | null;
8 14 /** 详情:Region Id 列表(`fl_group.Id`) */
9 15 groupIds?: string[] | null;
10 16 regionIds?: string[] | null;
11 17 /** 详情:门店 Id 列表 */
12 18 locationIds?: string[] | null;
  19 + /** 列表:Company 展示 */
  20 + company?: string | null;
13 21 /** 列表:该类型下标签数量 */
14 22 noOfLabels?: number | null;
15 23 /** 列表:最近编辑时间 */
... ... @@ -44,6 +52,12 @@ export type LabelTypeCreateInput = {
44 52 typeName: string;
45 53 state?: boolean;
46 54 orderNum?: number | null;
  55 + /** 适用 Company:ALL / SPECIFIED */
  56 + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null;
  57 + partnerIds?: string[] | null;
  58 + companyIds?: string[] | null;
  59 + /** 门店可用范围:ALL / SPECIFIED */
  60 + availabilityType?: "ALL" | "SPECIFIED" | string | null;
47 61 /** Region 多选(`fl_group.Id`) */
48 62 groupIds?: string[] | null;
49 63 regionIds?: string[] | null;
... ...