diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Common/ILabelEntityPartnerScopeInput.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Common/ILabelEntityPartnerScopeInput.cs new file mode 100644 index 0000000..b104e3e --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Common/ILabelEntityPartnerScopeInput.cs @@ -0,0 +1,16 @@ +namespace FoodLabeling.Application.Contracts.Dtos.Common; + +/// +/// 标签类型 / 分类 / 多选项创建编辑入参中的 Company 适用范围字段。 +/// +public interface ILabelEntityPartnerScopeInput +{ + /// 适用 Company:ALL / SPECIFIED + string? AppliedPartnerType { get; } + + /// 适用 Company(fl_partner.Id + List? PartnerIds { get; } + + /// 相同(兼容字段) + List? CompanyIds { get; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelCreateInputVo.cs index feae498..759481e 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelCreateInputVo.cs @@ -45,7 +45,7 @@ public class LabelCreateInputVo public string LabelCategoryId { get; set; } = string.Empty; - /// 标签类型 Id(可选) + /// 标签类型 Id(fl_label_type.Id);可选,未传时标签不绑定类型 public string? LabelTypeId { get; set; } public List ProductIds { get; set; } = new(); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetListInputVo.cs index e58f7bd..737f7ba 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetListInputVo.cs +++ b/美国版/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 public string? LabelCategoryId { get; set; } + /// 按标签类型 Id 筛选(可选) public string? LabelTypeId { get; set; } /// diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelUpdateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelUpdateInputVo.cs index c44717f..c1904fa 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelUpdateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelUpdateInputVo.cs @@ -31,7 +31,7 @@ public class LabelUpdateInputVo public string LabelCategoryId { get; set; } = string.Empty; - /// 标签类型 Id(可选) + /// 标签类型 Id(fl_label_type.Id);可选,未传时清空类型绑定 public string? LabelTypeId { get; set; } public List ProductIds { get; set; } = new(); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryCreateInputVo.cs index 349c5da..3e630ec 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryCreateInputVo.cs @@ -1,6 +1,8 @@ +using FoodLabeling.Application.Contracts.Dtos.Common; + namespace FoodLabeling.Application.Contracts.Dtos.LabelCategory; -public class LabelCategoryCreateInputVo +public class LabelCategoryCreateInputVo : ILabelEntityPartnerScopeInput { public string CategoryCode { get; set; } = string.Empty; @@ -24,6 +26,17 @@ public class LabelCategoryCreateInputVo public string ButtonAppearance { get; set; } = "TEXT"; /// + /// 适用 Company:ALL / SPECIFIED + /// + public string? AppliedPartnerType { get; set; } + + /// 适用 Company(fl_partner.Id + public List? PartnerIds { get; set; } + + /// 相同(兼容字段) + public List? CompanyIds { get; set; } + + /// /// 门店可用范围:ALL / SPECIFIED /// public string AvailabilityType { get; set; } = "ALL"; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListInputVo.cs index b70e95d..cc078aa 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListInputVo.cs +++ b/美国版/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 public bool? State { get; set; } + /// Company 筛选(fl_partner.Id + public string? PartnerId { get; set; } + /// /// Region 筛选(fl_group.Id);含 availabilityType=ALL 的分类 /// diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListOutputDto.cs index 694232c..3f61caf 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListOutputDto.cs +++ b/美国版/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 public string AvailabilityType { get; set; } = "ALL"; + public string AppliedPartnerType { get; set; } = "ALL"; + + /// 适用 Company 展示(ALL 时为 All Companies) + public string Company { get; set; } = string.Empty; + + public List PartnerIds { get; set; } = new(); + + /// 相同 + public List CompanyIds { get; set; } = new(); + /// 适用 Region 展示(ALL 时为 All Regions) public string Region { get; set; } = string.Empty; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetOutputDto.cs index 5ccdc73..6e6d7ef 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetOutputDto.cs +++ b/美国版/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 public string AvailabilityType { get; set; } = "ALL"; + public string AppliedPartnerType { get; set; } = "ALL"; + + /// 列表/详情 Company 展示 + public string Company { get; set; } = string.Empty; + + public List PartnerIds { get; set; } = new(); + + /// 相同 + public List CompanyIds { get; set; } = new(); + /// 适用 Region Id 列表(多选;ALL 时为空) public List RegionIds { get; set; } = new(); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionCreateInputVo.cs index 007d8b5..7802d0d 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionCreateInputVo.cs @@ -1,6 +1,8 @@ +using FoodLabeling.Application.Contracts.Dtos.Common; + namespace FoodLabeling.Application.Contracts.Dtos.LabelMultipleOption; -public class LabelMultipleOptionCreateInputVo +public class LabelMultipleOptionCreateInputVo : ILabelEntityPartnerScopeInput { /// 多选项编码(可选;未传或空字符串时存空,列表/详情出参为「无」) public string? OptionCode { get; set; } @@ -12,6 +14,17 @@ public class LabelMultipleOptionCreateInputVo public bool State { get; set; } = true; /// + /// 适用 Company:ALL / SPECIFIED + /// + public string? AppliedPartnerType { get; set; } + + /// 适用 Company(fl_partner.Id + public List? PartnerIds { get; set; } + + /// 相同(兼容字段) + public List? CompanyIds { get; set; } + + /// /// 门店可用范围:ALL / SPECIFIED;传了 时自动为 SPECIFIED /// public string AvailabilityType { get; set; } = "ALL"; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListInputVo.cs index 718e484..d283320 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListInputVo.cs +++ b/美国版/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 public bool? State { get; set; } + /// Company 筛选(fl_partner.Id + public string? PartnerId { get; set; } + /// /// Region 筛选(fl_group.Id) /// diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListOutputDto.cs index e5011fa..6d79240 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListOutputDto.cs +++ b/美国版/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 public string AvailabilityType { get; set; } = "ALL"; + public string AppliedPartnerType { get; set; } = "ALL"; + + public string Company { get; set; } = string.Empty; + + public List PartnerIds { get; set; } = new(); + + public List CompanyIds { get; set; } = new(); + public int OrderNum { get; set; } public DateTime LastEdited { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetOutputDto.cs index 23d92fd..865f190 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetOutputDto.cs +++ b/美国版/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 public string AvailabilityType { get; set; } = "ALL"; + public string AppliedPartnerType { get; set; } = "ALL"; + + public string Company { get; set; } = string.Empty; + + public List PartnerIds { get; set; } = new(); + + public List CompanyIds { get; set; } = new(); + public List RegionIds { get; set; } = new(); public List GroupIds { get; set; } = new(); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeCreateInputVo.cs index 084c6ce..131dcdc 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeCreateInputVo.cs @@ -1,6 +1,8 @@ +using FoodLabeling.Application.Contracts.Dtos.Common; + namespace FoodLabeling.Application.Contracts.Dtos.LabelType; -public class LabelTypeCreateInputVo +public class LabelTypeCreateInputVo : ILabelEntityPartnerScopeInput { public string TypeCode { get; set; } = string.Empty; @@ -9,6 +11,17 @@ public class LabelTypeCreateInputVo public bool State { get; set; } = true; /// + /// 适用 Company:ALL / SPECIFIED + /// + public string? AppliedPartnerType { get; set; } + + /// 适用 Company(fl_partner.Id + public List? PartnerIds { get; set; } + + /// 相同(兼容字段) + public List? CompanyIds { get; set; } + + /// /// 门店可用范围:ALL / SPECIFIED;传了 时自动为 SPECIFIED /// public string AvailabilityType { get; set; } = "ALL"; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListInputVo.cs index 1441853..7b412cb 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListInputVo.cs +++ b/美国版/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 public bool? State { get; set; } + /// Company 筛选(fl_partner.Id + public string? PartnerId { get; set; } + /// /// Region 筛选(fl_group.Id);仅返回在该 Region 下存在标签实例的类型 /// diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListOutputDto.cs index 8ede0d8..56ac9ca 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListOutputDto.cs +++ b/美国版/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 public string AvailabilityType { get; set; } = "ALL"; + public string AppliedPartnerType { get; set; } = "ALL"; + + /// 适用 Company 展示(ALL 时为 All Companies) + public string Company { get; set; } = string.Empty; + + public List PartnerIds { get; set; } = new(); + + /// 相同 + public List CompanyIds { get; set; } = new(); + public int OrderNum { get; set; } /// 列表列 No. of Labels:该类型下未删除标签数(统计 fl_label,非库表物理列) diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetOutputDto.cs index 54440ba..028a50b 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetOutputDto.cs +++ b/美国版/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 public string AvailabilityType { get; set; } = "ALL"; + public string AppliedPartnerType { get; set; } = "ALL"; + + /// 列表/详情 Company 展示 + public string Company { get; set; } = string.Empty; + + public List PartnerIds { get; set; } = new(); + + /// 相同 + public List CompanyIds { get; set; } = new(); + public List RegionIds { get; set; } = new(); public List GroupIds { get; set; } = new(); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListInputVo.cs index 7350080..64d5e8b 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListInputVo.cs +++ b/美国版/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 public bool? State { get; set; } /// - /// 按 Region 筛选(fl_group.Id):返回适用于该 Region 下任一门门店的分类,以及 availabilityType=ALL 的分类 + /// 按 Region 筛选(fl_group.Id):返回适用于该 Region 下任一门门店的分类 /// public string? GroupId { get; set; } /// + /// 按 Company 筛选(fl_partner.Id) + /// + public string? PartnerId { get; set; } + + /// /// 按门店筛选(location.Id,Guid 字符串);优先于 /// public string? LocationId { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelAppService.cs index 6d8b8ae..a7870cf 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelAppService.cs +++ b/美国版/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; public interface ILabelAppService : IApplicationService { /// - /// 按产品分页列表(一个产品展示多个标签)。支持 GroupId / LocationId 筛选(命中 fl_label_regionfl_label_locationAppliedRegionType=ALL);出参含 locationIdsregionregionIdsappliedRegionType。 + /// 按产品分页列表(一个产品展示多个标签)。支持 GroupId / LocationId 筛选(命中 fl_label_regionfl_label_locationAppliedRegionType=ALL);labelTypeId 为可选筛选;出参含 locationIdsregionregionIdsappliedRegionType。 /// Task> GetListAsync(LabelGetListInputVo input); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs index e49108e..18db503 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs +++ b/美国版/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; public interface IUsAppAuthAppService : IApplicationService { /// - /// App 登录:使用邮箱 + 密码校验并签发 Token,同时返回 userlocation 绑定的门店 + /// App 登录:使用邮箱 + 密码校验并签发 Token,同时返回可选门店(Company Admin 为绑定 Company 下全部门店) /// Task LoginAsync(UsAppLoginInputVo input); /// - /// 获取当前登录账号已绑定的门店(用于切换门店等场景) + /// 获取当前登录账号可选门店(Company Admin 返回绑定 Company 下全部门店,用于切换门店等场景) /// Task> GetMyLocationsAsync(); @@ -62,7 +62,7 @@ public interface IUsAppAuthAppService : IApplicationService Task ChangePasswordAsync(UsAppChangePasswordInputVo input); /// - /// 按门店 Id 查询 Location 详情(须为当前账号 userlocation 绑定门店) + /// 按门店 Id 查询 Location 详情(须为当前账号可选门店;Company Admin 含绑定 Company 下任意门店) /// /// 门店 Guid 字符串 /// 店名、地址、电话、经营时间(operatingHours)、店长信息 diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelEntityPartnerScopeHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelEntityPartnerScopeHelper.cs new file mode 100644 index 0000000..748d61e --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelEntityPartnerScopeHelper.cs @@ -0,0 +1,733 @@ +using FoodLabeling.Application.Services.DbModels; +using SqlSugar; +using Volo.Abp; +using Volo.Abp.Guids; + +namespace FoodLabeling.Application.Helpers; + +/// +/// 标签类型 / 分类 / 多选项适用 Company:ALL/SPECIFIED + 多选落库(对齐 的 Company 维度)。 +/// +public static class LabelEntityPartnerScopeHelper +{ + public const string ScopeAll = "ALL"; + public const string ScopeSpecified = "SPECIFIED"; + + public const string AllCompaniesDisplay = "All Companies"; + public const string EmptyDisplay = "无"; + + /// 标签实体种类(对应主表与 partner 关联表)。 + public enum LabelEntityPartnerKind + { + /// 标签类型 fl_label_type + Type, + + /// 标签分类 fl_label_category + Category, + + /// 标签多选项 fl_label_multiple_option + MultipleOption + } + + public sealed class LabelEntityPartnerScopeSaveResult + { + public string AppliedPartnerType { get; init; } = ScopeAll; + + public List PartnerIds { get; init; } = new(); + } + + public sealed class LabelEntityPartnerScopeDisplay + { + public string Company { get; init; } = AllCompaniesDisplay; + + public string AppliedPartnerType { get; init; } = ScopeAll; + + public List PartnerIds { get; init; } = new(); + } + + private static LabelEntityPartnerScopeSchemaStatus? _cachedSchema; + + private sealed class LabelEntityPartnerScopeSchemaStatus + { + public bool HasTypePartnerColumn { get; init; } + + public bool HasCategoryPartnerColumn { get; init; } + + public bool HasMultipleOptionPartnerColumn { get; init; } + + public bool HasTypePartnerTable { get; init; } + + public bool HasCategoryPartnerTable { get; init; } + + public bool HasMultipleOptionPartnerTable { get; init; } + } + + /// + /// 合并 并去重排序。 + /// + public static List NormalizePartnerIds(IReadOnlyList? partnerIds, IReadOnlyList? companyIds) + { + var merged = new HashSet(StringComparer.Ordinal); + foreach (var id in LocationScopeBindingHelper.NormalizeIds(partnerIds)) + { + merged.Add(id); + } + + foreach (var id in LocationScopeBindingHelper.NormalizeIds(companyIds)) + { + merged.Add(id); + } + + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); + } + + /// + /// 解析适用 Company 范围类型(ALL/SPECIFIED)。 + /// + public static string ResolveAppliedPartnerType(string? type, List partnerIds, bool hasArrayInPayload) + { + if (partnerIds.Count > 0) + { + return ScopeSpecified; + } + + var normalized = (type ?? ScopeAll).Trim().ToUpperInvariant(); + if (hasArrayInPayload && string.Equals(normalized, ScopeAll, StringComparison.OrdinalIgnoreCase)) + { + return ScopeAll; + } + + return string.Equals(normalized, ScopeSpecified, StringComparison.OrdinalIgnoreCase) + ? ScopeSpecified + : ScopeAll; + } + + /// + /// 解析新增/编辑入参中的 Company 范围并校验 partner Id 有效性。 + /// + public static async Task ResolvePartnerScopeForSaveAsync( + ISqlSugarClient db, + string? appliedPartnerType, + IReadOnlyList? partnerIds, + IReadOnlyList? companyIds) + { + var normalizedPartnerIds = NormalizePartnerIds(partnerIds, companyIds); + var partnerType = ResolveAppliedPartnerType( + appliedPartnerType, + normalizedPartnerIds, + partnerIds is not null || companyIds is not null); + + ValidatePartnerType(partnerType); + + if (string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) + && normalizedPartnerIds.Count == 0) + { + throw new UserFriendlyException("指定适用 Company 时,partnerIds/companyIds 不能为空"); + } + + if (normalizedPartnerIds.Count > 0) + { + await ValidatePartnerIdsExistAsync(db, normalizedPartnerIds); + } + + return new LabelEntityPartnerScopeSaveResult + { + AppliedPartnerType = partnerType, + PartnerIds = normalizedPartnerIds + }; + } + + /// + /// 保存 Company 适用范围(关联表 + AppliedPartnerType 列);未执行迁移脚本时 no-op。 + /// + public static async Task SavePartnerScopeAsync( + ISqlSugarClient db, + IGuidGenerator guidGenerator, + LabelEntityPartnerKind kind, + string entityId, + LabelEntityPartnerScopeSaveResult scope, + string? currentUserId, + DateTime now) + { + if (string.IsNullOrWhiteSpace(entityId)) + { + return; + } + + var schema = await GetSchemaStatusAsync(db); + if (!HasPartnerScopeForKind(schema, kind)) + { + return; + } + + await DeletePartnerScopeRowsAsync(db, kind, entityId); + + if (string.Equals(scope.AppliedPartnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) + && scope.PartnerIds.Count > 0) + { + await InsertPartnerRowsAsync(db, guidGenerator, kind, entityId, scope.PartnerIds, currentUserId, now); + } + + await SetAppliedPartnerTypeAsync(db, kind, entityId, scope.AppliedPartnerType); + } + + /// + /// 删除实体下 Company 关联行;未迁移 partner 表时跳过。 + /// + public static async Task DeletePartnerScopeRowsAsync( + ISqlSugarClient db, + LabelEntityPartnerKind kind, + string entityId) + { + if (string.IsNullOrWhiteSpace(entityId)) + { + return; + } + + var schema = await GetSchemaStatusAsync(db); + if (!HasPartnerTableForKind(schema, kind)) + { + return; + } + + switch (kind) + { + case LabelEntityPartnerKind.Type: + await db.Deleteable() + .Where(x => x.LabelTypeId == entityId) + .ExecuteCommandAsync(); + break; + case LabelEntityPartnerKind.Category: + await db.Deleteable() + .Where(x => x.CategoryId == entityId) + .ExecuteCommandAsync(); + break; + case LabelEntityPartnerKind.MultipleOption: + await db.Deleteable() + .Where(x => x.MultipleOptionId == entityId) + .ExecuteCommandAsync(); + break; + } + } + + /// + /// 批量加载 Company 展示与 Id 数组(详情/列表)。 + /// + public static async Task> BuildPartnerScopeDisplayMapAsync( + ISqlSugarClient db, + LabelEntityPartnerKind kind, + IReadOnlyList entityIds) + { + var result = new Dictionary(StringComparer.Ordinal); + if (entityIds.Count == 0) + { + return result; + } + + var ids = entityIds.Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .Distinct(StringComparer.Ordinal) + .ToList(); + if (ids.Count == 0) + { + return result; + } + + var schema = await GetSchemaStatusAsync(db); + var hasScope = HasPartnerScopeForKind(schema, kind); + + var partnerLinks = hasScope + ? await LoadPartnerLinksAsync(db, kind, ids) + : new List<(string EntityId, string PartnerId)>(); + + var partnerIdSet = partnerLinks.Select(x => x.PartnerId).Distinct(StringComparer.Ordinal).ToList(); + var partnerNameById = await LoadPartnerNameMapAsync(db, partnerIdSet); + + foreach (var entityId in ids) + { + var pIds = LocationScopeBindingHelper.NormalizeIds( + partnerLinks.Where(x => x.EntityId == entityId).Select(x => x.PartnerId).ToList()); + + var partnerType = pIds.Count > 0 ? ScopeSpecified : ScopeAll; + if (hasScope) + { + var storedType = await GetAppliedPartnerTypeAsync(db, kind, entityId); + if (!string.IsNullOrWhiteSpace(storedType)) + { + partnerType = NormalizeScopeType(storedType, ScopeAll); + } + else if (pIds.Count > 0) + { + partnerType = ScopeSpecified; + } + } + + var companyDisplay = string.Equals(partnerType, ScopeAll, StringComparison.OrdinalIgnoreCase) + ? AllCompaniesDisplay + : BuildCompanyDisplay(pIds, partnerNameById); + + result[entityId] = new LabelEntityPartnerScopeDisplay + { + Company = companyDisplay, + AppliedPartnerType = partnerType, + PartnerIds = pIds + }; + } + + return result; + } + + /// + /// 列表 Company 列展示文案。 + /// + public static string BuildCompanyDisplay( + IReadOnlyList partnerIds, + IReadOnlyDictionary partnerNameById) + { + if (partnerIds.Count == 0) + { + return EmptyDisplay; + } + + var names = partnerIds + .Select(id => partnerNameById.TryGetValue(id, out var name) ? name : id) + .Select(x => x?.Trim()) + .Where(x => !string.IsNullOrEmpty(x)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) + .ToList(); + + return names.Count > 0 ? string.Join(", ", names) : EmptyDisplay; + } + + /// + /// 列表筛选:按可见 Company 过滤标签类型(与 Region/Location 筛选 AND)。 + /// + public static async Task> ApplyTypePartnerListFilterAsync( + ISqlSugarClient db, + ISugarQueryable query, + IReadOnlyList? scopedPartnerIds) + { + if (scopedPartnerIds is null) + { + return query; + } + + var schema = await GetSchemaStatusAsync(db); + if (!HasPartnerTableForKind(schema, LabelEntityPartnerKind.Type)) + { + return query; + } + + if (scopedPartnerIds.Count == 0) + { + return query.Where(t => + !SqlFunc.Subqueryable() + .Where(p => p.LabelTypeId == t.Id) + .Any()); + } + + return query.Where(t => + !SqlFunc.Subqueryable() + .Where(p => p.LabelTypeId == t.Id) + .Any() + || SqlFunc.Subqueryable() + .Where(p => p.LabelTypeId == t.Id && scopedPartnerIds.Contains(p.PartnerId)) + .Any()); + } + + /// + /// 列表筛选:按可见 Company 过滤标签分类。 + /// + public static async Task> ApplyCategoryPartnerListFilterAsync( + ISqlSugarClient db, + ISugarQueryable query, + IReadOnlyList? scopedPartnerIds) + { + if (scopedPartnerIds is null) + { + return query; + } + + var schema = await GetSchemaStatusAsync(db); + if (!HasPartnerTableForKind(schema, LabelEntityPartnerKind.Category)) + { + return query; + } + + if (scopedPartnerIds.Count == 0) + { + return query.Where(c => + !SqlFunc.Subqueryable() + .Where(p => p.CategoryId == c.Id) + .Any()); + } + + return query.Where(c => + !SqlFunc.Subqueryable() + .Where(p => p.CategoryId == c.Id) + .Any() + || SqlFunc.Subqueryable() + .Where(p => p.CategoryId == c.Id && scopedPartnerIds.Contains(p.PartnerId)) + .Any()); + } + + /// + /// 列表筛选:按可见 Company 过滤标签多选项。 + /// + public static async Task> ApplyMultipleOptionPartnerListFilterAsync( + ISqlSugarClient db, + ISugarQueryable query, + IReadOnlyList? scopedPartnerIds) + { + if (scopedPartnerIds is null) + { + return query; + } + + var schema = await GetSchemaStatusAsync(db); + if (!HasPartnerTableForKind(schema, LabelEntityPartnerKind.MultipleOption)) + { + return query; + } + + if (scopedPartnerIds.Count == 0) + { + return query.Where(o => + !SqlFunc.Subqueryable() + .Where(p => p.MultipleOptionId == o.Id) + .Any()); + } + + return query.Where(o => + !SqlFunc.Subqueryable() + .Where(p => p.MultipleOptionId == o.Id) + .Any() + || SqlFunc.Subqueryable() + .Where(p => p.MultipleOptionId == o.Id && scopedPartnerIds.Contains(p.PartnerId)) + .Any()); + } + + /// + /// 由 Query partnerId 解析列表 Company 筛选 Id;未传则返回 null。 + /// + public static async Task?> ResolveScopedPartnerIdsForListAsync( + ISqlSugarClient db, + string? partnerId) + { + var pid = partnerId?.Trim(); + if (string.IsNullOrWhiteSpace(pid)) + { + return null; + } + + var exists = await db.Queryable() + .AnyAsync(x => !x.IsDeleted && x.Id == pid); + return exists ? new List { pid } : new List(); + } + + /// + /// 由 Query partnerId 或可见门店范围解析列表 Company 筛选 Id;均未限时返回 null。 + /// + public static async Task?> ResolveListPartnerIdsAsync( + ISqlSugarClient db, + string? partnerId, + List? scopedLocationIds) + { + var fromQuery = await ResolveScopedPartnerIdsForListAsync(db, partnerId); + if (fromQuery is not null) + { + return fromQuery; + } + + if (scopedLocationIds is null) + { + return null; + } + + if (scopedLocationIds.Count == 0) + { + return new List(); + } + + return await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, scopedLocationIds); + } + + /// 单元测试或切换库后调用。 + public static void ResetSchemaCacheForTests() => _cachedSchema = null; + + private static void ValidatePartnerType(string type) + { + if (type != ScopeAll && type != ScopeSpecified) + { + throw new UserFriendlyException("适用 Company 范围不合法(ALL/SPECIFIED)"); + } + } + + private static string NormalizeScopeType(string? type, string fallback) + { + var t = (type ?? fallback).Trim().ToUpperInvariant(); + return t == ScopeSpecified ? ScopeSpecified : ScopeAll; + } + + private static async Task ValidatePartnerIdsExistAsync(ISqlSugarClient db, List partnerIds) + { + if (partnerIds.Count == 0) + { + return; + } + + var count = await db.Queryable() + .Where(x => !x.IsDeleted && partnerIds.Contains(x.Id)) + .CountAsync(); + if (count != partnerIds.Count) + { + throw new UserFriendlyException("存在无效的 Company(partnerIds/companyIds),请刷新后重试"); + } + } + + private static async Task GetSchemaStatusAsync(ISqlSugarClient db) + { + if (_cachedSchema is not null) + { + return _cachedSchema; + } + + try + { + _cachedSchema = new LabelEntityPartnerScopeSchemaStatus + { + HasTypePartnerColumn = await ColumnExistsAsync(db, "fl_label_type", "AppliedPartnerType"), + HasCategoryPartnerColumn = await ColumnExistsAsync(db, "fl_label_category", "AppliedPartnerType"), + HasMultipleOptionPartnerColumn = await ColumnExistsAsync( + db, "fl_label_multiple_option", "AppliedPartnerType"), + HasTypePartnerTable = await TableExistsAsync(db, "fl_label_type_partner"), + HasCategoryPartnerTable = await TableExistsAsync(db, "fl_label_category_partner"), + HasMultipleOptionPartnerTable = await TableExistsAsync(db, "fl_label_multiple_option_partner") + }; + } + catch + { + _cachedSchema = new LabelEntityPartnerScopeSchemaStatus(); + } + + return _cachedSchema; + } + + private static bool HasPartnerScopeForKind(LabelEntityPartnerScopeSchemaStatus schema, LabelEntityPartnerKind kind) => + HasPartnerTableForKind(schema, kind) && HasPartnerColumnForKind(schema, kind); + + private static bool HasPartnerTableForKind(LabelEntityPartnerScopeSchemaStatus schema, LabelEntityPartnerKind kind) => + kind switch + { + LabelEntityPartnerKind.Type => schema.HasTypePartnerTable, + LabelEntityPartnerKind.Category => schema.HasCategoryPartnerTable, + LabelEntityPartnerKind.MultipleOption => schema.HasMultipleOptionPartnerTable, + _ => false + }; + + private static bool HasPartnerColumnForKind(LabelEntityPartnerScopeSchemaStatus schema, LabelEntityPartnerKind kind) => + kind switch + { + LabelEntityPartnerKind.Type => schema.HasTypePartnerColumn, + LabelEntityPartnerKind.Category => schema.HasCategoryPartnerColumn, + LabelEntityPartnerKind.MultipleOption => schema.HasMultipleOptionPartnerColumn, + _ => false + }; + + private static async Task ColumnExistsAsync(ISqlSugarClient db, string tableName, string columnName) + { + var count = await db.Ado.GetIntAsync( + """ + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = @tableName + AND COLUMN_NAME = @columnName + """, + new { tableName, columnName }); + return count > 0; + } + + private static async Task TableExistsAsync(ISqlSugarClient db, string tableName) + { + var count = await db.Ado.GetIntAsync( + """ + SELECT COUNT(*) + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = @tableName + """, + new { tableName }); + return count > 0; + } + + private static async Task SetAppliedPartnerTypeAsync( + ISqlSugarClient db, + LabelEntityPartnerKind kind, + string entityId, + string appliedPartnerType) + { + var schema = await GetSchemaStatusAsync(db); + if (!HasPartnerColumnForKind(schema, kind)) + { + return; + } + + var tableName = kind switch + { + LabelEntityPartnerKind.Type => "fl_label_type", + LabelEntityPartnerKind.Category => "fl_label_category", + LabelEntityPartnerKind.MultipleOption => "fl_label_multiple_option", + _ => string.Empty + }; + + if (string.IsNullOrEmpty(tableName)) + { + return; + } + + await db.Ado.ExecuteCommandAsync( + $"UPDATE `{tableName}` SET AppliedPartnerType = @type WHERE Id = @id", + new { type = appliedPartnerType.Trim(), id = entityId.Trim() }); + } + + private static async Task GetAppliedPartnerTypeAsync( + ISqlSugarClient db, + LabelEntityPartnerKind kind, + string entityId) + { + var schema = await GetSchemaStatusAsync(db); + if (!HasPartnerColumnForKind(schema, kind)) + { + return null; + } + + var tableName = kind switch + { + LabelEntityPartnerKind.Type => "fl_label_type", + LabelEntityPartnerKind.Category => "fl_label_category", + LabelEntityPartnerKind.MultipleOption => "fl_label_multiple_option", + _ => string.Empty + }; + + if (string.IsNullOrEmpty(tableName)) + { + return null; + } + + var value = await db.Ado.GetScalarAsync( + $"SELECT AppliedPartnerType FROM `{tableName}` WHERE Id = @id LIMIT 1", + new { id = entityId.Trim() }); + + return value?.ToString()?.Trim(); + } + + private static async Task> LoadPartnerLinksAsync( + ISqlSugarClient db, + LabelEntityPartnerKind kind, + IReadOnlyList entityIds) + { + switch (kind) + { + case LabelEntityPartnerKind.Type: + { + var rows = await db.Queryable() + .Where(x => entityIds.Contains(x.LabelTypeId)) + .ToListAsync(); + return rows.Select(x => (x.LabelTypeId, x.PartnerId)).ToList(); + } + case LabelEntityPartnerKind.Category: + { + var rows = await db.Queryable() + .Where(x => entityIds.Contains(x.CategoryId)) + .ToListAsync(); + return rows.Select(x => (x.CategoryId, x.PartnerId)).ToList(); + } + case LabelEntityPartnerKind.MultipleOption: + { + var rows = await db.Queryable() + .Where(x => entityIds.Contains(x.MultipleOptionId)) + .ToListAsync(); + return rows.Select(x => (x.MultipleOptionId, x.PartnerId)).ToList(); + } + default: + return new List<(string EntityId, string PartnerId)>(); + } + } + + private static async Task InsertPartnerRowsAsync( + ISqlSugarClient db, + IGuidGenerator guidGenerator, + LabelEntityPartnerKind kind, + string entityId, + IReadOnlyList partnerIds, + string? currentUserId, + DateTime now) + { + switch (kind) + { + case LabelEntityPartnerKind.Type: + { + var rows = partnerIds.Select(pid => new FlLabelTypePartnerDbEntity + { + Id = guidGenerator.Create().ToString(), + LabelTypeId = entityId, + PartnerId = pid, + CreationTime = now, + CreatorId = currentUserId + }).ToList(); + await db.Insertable(rows).ExecuteCommandAsync(); + break; + } + case LabelEntityPartnerKind.Category: + { + var rows = partnerIds.Select(pid => new FlLabelCategoryPartnerDbEntity + { + Id = guidGenerator.Create().ToString(), + CategoryId = entityId, + PartnerId = pid, + CreationTime = now, + CreatorId = currentUserId + }).ToList(); + await db.Insertable(rows).ExecuteCommandAsync(); + break; + } + case LabelEntityPartnerKind.MultipleOption: + { + var rows = partnerIds.Select(pid => new FlLabelMultipleOptionPartnerDbEntity + { + Id = guidGenerator.Create().ToString(), + MultipleOptionId = entityId, + PartnerId = pid, + CreationTime = now, + CreatorId = currentUserId + }).ToList(); + await db.Insertable(rows).ExecuteCommandAsync(); + break; + } + } + } + + private static async Task> LoadPartnerNameMapAsync( + ISqlSugarClient db, + IReadOnlyList partnerIds) + { + var result = new Dictionary(StringComparer.Ordinal); + if (partnerIds.Count == 0) + { + return result; + } + + var partners = await db.Queryable() + .Where(x => !x.IsDeleted && partnerIds.Contains(x.Id)) + .ToListAsync(); + foreach (var p in partners) + { + var name = p.PartnerName?.Trim(); + result[p.Id] = string.IsNullOrEmpty(name) ? p.Id : name; + } + + return result; + } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs index e9ffbfc..1fe9817 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs @@ -54,6 +54,61 @@ public static class LocationScopeBindingHelper return locationIds.OrderBy(x => x, StringComparer.Ordinal).ToList(); } + /// + /// App 可选门店 Id:Company Admin 展开为绑定 Company 下全部门店;其它角色为 userlocation 绑定。 + /// + public static Task> ResolveAppAccessibleLocationIdsForUserAsync( + ISqlSugarClient db, + Guid userId) => + ResolveAppAccessibleLocationIdsForUserAsync(db, userId, null); + + /// + /// App 可选门店 Id;传入 时优先 JWT 声明识别 Company Admin,避免 ORM 查 Role 触发数据权限过滤器。 + /// + public static async Task> ResolveAppAccessibleLocationIdsForUserAsync( + ISqlSugarClient db, + Guid userId, + ICurrentUser? currentUser) + { + var locationIds = await ResolveBoundLocationIdsForUserAsync(db, userId); + if (locationIds.Count == 0) + { + return locationIds; + } + + var isCompanyAdmin = currentUser?.Id == userId + ? await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser) + : await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, userId); + if (!isCompanyAdmin) + { + return locationIds; + } + + var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds); + if (partnerIds.Count == 0) + { + return locationIds; + } + + var expanded = await ResolveLocationIdsFromPartnerIdsAsync(db, partnerIds); + if (expanded.Count == 0) + { + return locationIds; + } + + var merged = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var id in expanded) + { + var key = TeamMemberListScopeHelper.NormalizeScopeKey(id); + if (!string.IsNullOrEmpty(key)) + { + merged.Add(key); + } + } + + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); + } + public static async Task> ResolveDisplayRegionIdsForCurrentUserAsync( ISqlSugarClient db, ICurrentUser currentUser) @@ -63,12 +118,7 @@ public static class LocationScopeBindingHelper return new List(); } - var roleId = await db.Queryable() - .Where(ur => ur.UserId == currentUser.Id.Value) - .Select(ur => (Guid?)ur.RoleId) - .FirstAsync(); - - return await ResolveDisplayRegionIdsForUserAsync(db, currentUser, roleId); + return await ResolveDisplayRegionIdsForUserAsync(db, currentUser, null); } /// @@ -93,9 +143,7 @@ public static class LocationScopeBindingHelper var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds); var regionIds = await ResolveGroupIdsFromLocationIdsAsync(db, locationIds); - var isCompanyAdmin = await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser) - || await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(db, roleId); - if (isCompanyAdmin && partnerIds.Count > 0) + if (await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser) && partnerIds.Count > 0) { regionIds = await ResolveGroupIdsFromPartnerIdsAsync(db, partnerIds); } @@ -120,7 +168,7 @@ public static class LocationScopeBindingHelper var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds); var regionIds = await ResolveGroupIdsFromLocationIdsAsync(db, locationIds); - if (await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(db, roleId) && partnerIds.Count > 0) + if (await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, userId) && partnerIds.Count > 0) { regionIds = await ResolveGroupIdsFromPartnerIdsAsync(db, partnerIds); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsDateTimeDisplayHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsDateTimeDisplayHelper.cs index efb8bfd..ba86174 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsDateTimeDisplayHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsDateTimeDisplayHelper.cs @@ -409,9 +409,29 @@ public static class ReportsDateTimeDisplayHelper return false; + } + /// 将已解析的到期时刻格式化为 Print Log 展示文案。 + public static string FormatExpiryDateTime(DateTime dt) + { + if (dt == DateTime.MinValue) + { + return "无"; + } + + return dt.ToString(DisplayFormat, CultureInfo.InvariantCulture); } + /// 解析到期字面量(含 anchor 补全年份/时间)。 + public static bool TryParseExpiryDateTime(string s, DateTime anchor, out DateTime dt) + { + if (TryParseToDateTime(s, out dt)) + { + return true; + } + + return TryParsePartialDateTime(s, anchor, out dt); + } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsLocationScopeHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsLocationScopeHelper.cs index 1a35f5a..156af62 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsLocationScopeHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsLocationScopeHelper.cs @@ -8,7 +8,7 @@ namespace FoodLabeling.Application.Helpers; /// /// Reports 门店数据范围:管理员可查全部(可按 Company/Region/Location 入参收窄); -/// 非管理员仅统计 userlocation 绑定门店的打印任务。 +/// Company Admin 为绑定 Company 下全部门店;其它非管理员为 userlocation 绑定门店。 /// public static class ReportsLocationScopeHelper { @@ -16,7 +16,7 @@ public static class ReportsLocationScopeHelper /// 解析报表统计适用的门店 Id 列表。 /// /// 返回 null:不限制门店(管理员且未传 Company/Region/Location 筛选); - /// 返回空列表:无绑定门店或筛选后无交集; + /// 返回空列表:无可访问门店或筛选后无交集; /// 返回非空列表:仅统计这些 fl_label_print_task.LocationId /// /// @@ -39,22 +39,16 @@ public static class ReportsLocationScopeHelper return new List(); } - var userId = currentUser.Id.Value.ToString(); - var boundIds = LocationScopeBindingHelper.NormalizeIds( - await db.Queryable() - .Where(x => !x.IsDeleted && x.UserId == userId) - .Select(x => x.LocationId) - .Distinct() - .ToListAsync()); - - if (boundIds.Count == 0) + var accessibleIds = await LocationScopeBindingHelper.ResolveAppAccessibleLocationIdsForUserAsync( + db, currentUser.Id.Value, currentUser); + if (accessibleIds.Count == 0) { return new List(); } if (filterIds is null) { - return boundIds; + return accessibleIds; } if (filterIds.Count == 0) @@ -62,8 +56,8 @@ public static class ReportsLocationScopeHelper return new List(); } - var boundSet = new HashSet(boundIds, StringComparer.OrdinalIgnoreCase); - return filterIds.Where(id => boundSet.Contains(id)).ToList(); + var accessibleSet = new HashSet(accessibleIds, StringComparer.OrdinalIgnoreCase); + return filterIds.Where(id => accessibleSet.Contains(id)).ToList(); } /// diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs index 793ec38..72d2a33 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs @@ -53,10 +53,10 @@ public static class ReportsPrintLogExpiryHelper return ReportsDateTimeDisplayHelper.FormatExpiryDateTime(resolved); } - var raw = ExtractExpiryText(printInputJson); + var raw = ExtractExpiryText(printInputJson, reference); if (string.Equals(raw, "无", StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(renderTemplateJson)) { - raw = ExtractExpiryText(renderTemplateJson); + raw = ExtractExpiryText(renderTemplateJson, reference); } return ReportsDateTimeDisplayHelper.FormatExpiryDisplay(raw, reference); @@ -185,7 +185,7 @@ public static class ReportsPrintLogExpiryHelper return false; } - return ReportsDateTimeDisplayHelper.TryParseToDateTime(display.Trim(), reference, out result); + return ReportsDateTimeDisplayHelper.TryParseExpiryDateTime(display.Trim(), reference, out result); } private static string? ResolveElementDisplayText(JsonElement el, DateTime reference) @@ -453,35 +453,6 @@ public static class ReportsPrintLogExpiryHelper dt.Hour != 0 || dt.Minute != 0; /// - /// 解析并格式化为 Print Log Expiration 列展示。 - /// - public static string ExtractFormattedExpiryText( - string? printInputJson, - string? renderTemplateJson = null, - DateTime? baseTime = null, - DateTime? printedAt = null) - { - var anchor = ResolveAnchor(baseTime, printedAt, printInputJson); - var raw = ExtractExpiryText(printInputJson, anchor); - if (IsMissingExpiry(raw) && !string.IsNullOrWhiteSpace(renderTemplateJson)) - { - raw = ExtractExpiryText(renderTemplateJson, anchor); - } - - if (TryParseOffsetPayload(raw.Trim(), out var amount, out var unit)) - { - raw = ApplyOffset(anchor, amount, unit) - .ToString(ReportsDateTimeDisplayHelper.DisplayFormat, CultureInfo.InvariantCulture); - } - - return ReportsDateTimeDisplayHelper.FormatExpiryDisplay(raw, anchor); - } - - /// 兼容旧调用:仅 PrintInputJson。 - public static string ExtractFormattedExpiryText(string? printInputJson) => - ExtractFormattedExpiryText(printInputJson, null, null, null); - - /// /// 解析保质期展示文本;无法解析时返回「无」。 /// public static string ExtractExpiryText(string? printInputJson, DateTime anchor) @@ -830,7 +801,7 @@ public static class ReportsPrintLogExpiryHelper } var t = raw.Trim(); - if (!t.StartsWith('{', StringComparison.Ordinal)) + if (!t.StartsWith("{")) { if (Regex.IsMatch(t, @"^\d+$")) { diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberRoleHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberRoleHelper.cs index 2b06a92..50aee70 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberRoleHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberRoleHelper.cs @@ -1,6 +1,5 @@ using SqlSugar; using Volo.Abp.Users; -using Yi.Framework.Rbac.Domain.Entities; using Yi.Framework.Rbac.Domain.Shared.Consts; namespace FoodLabeling.Application.Helpers; @@ -55,12 +54,17 @@ public static class TeamMemberRoleHelper return false; } - var roleId = await db.Queryable() - .Where(ur => ur.UserId == currentUser.Id.Value) - .Select(ur => (Guid?)ur.RoleId) - .FirstAsync(); + return await IsCompanyAdminUserAsync(db, currentUser.Id.Value); + } - return await IsCompanyAdminRoleAsync(db, roleId); + /// + /// 指定用户是否为 Company Admin 类角色(按库内 Role 判定,用于登录等无 JWT 声明场景)。 + /// + public static async Task IsCompanyAdminUserAsync(ISqlSugarClient db, Guid userId) + { + var roleRows = await LoadUserRoleRowsAsync(db, userId); + return roleRows.Any(r => + IsCompanyAdminRoleName(r.RoleName) || IsCompanyAdminRoleToken(r.RoleCode)); } /// @@ -73,8 +77,16 @@ public static class TeamMemberRoleHelper return false; } - var role = await db.Queryable() - .FirstAsync(x => !x.IsDeleted && x.Id == roleId.Value); + var roleRows = await db.Ado.SqlQueryAsync( + """ + SELECT RoleCode, RoleName + FROM Role + WHERE IsDeleted = 0 AND Id = @RoleId + LIMIT 1 + """, + new { RoleId = roleId.Value }); + + var role = roleRows.FirstOrDefault(); if (role is null) { return false; @@ -84,6 +96,19 @@ public static class TeamMemberRoleHelper } /// + /// 读取用户角色(raw SQL,避免 RBAC 数据权限过滤器中 Select 表达式导致 SqlSugar 解析失败)。 + /// + private static Task> LoadUserRoleRowsAsync(ISqlSugarClient db, Guid userId) => + db.Ado.SqlQueryAsync( + """ + SELECT r.RoleCode, r.RoleName + FROM UserRole ur + INNER JOIN Role r ON ur.RoleId = r.Id + WHERE ur.UserId = @UserId AND r.IsDeleted = 0 AND r.State = 1 + """, + new { UserId = userId }); + + /// /// 列表/详情出参:Partner Admin → Company Admin(与 Web UI 一致)。 /// public static string? FormatDisplayRoleName(string? roleName) @@ -148,4 +173,11 @@ public static class TeamMemberRoleHelper private static string NormalizeRoleKey(string value) => new string(value.Trim().ToLowerInvariant().Where(c => char.IsLetterOrDigit(c)).ToArray()); + + private sealed class UserRoleNameRow + { + public string? RoleCode { get; set; } + + public string? RoleName { get; set; } + } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppPrintLogScopeHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppPrintLogScopeHelper.cs index be73bd4..8b8cd0a 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppPrintLogScopeHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppPrintLogScopeHelper.cs @@ -13,7 +13,7 @@ namespace FoodLabeling.Application.Helpers; public static class UsAppPrintLogScopeHelper { /// - /// 管理员()或角色码/名含 partner(忽略大小写,如 Partner Admin)。 + /// 管理员、Company Admin,或角色码/名含 partner(如 Partner Admin):可查看门店下全部打印记录。 /// public static async Task CanViewAllPrintsAtLocationAsync( ICurrentUser currentUser, @@ -29,6 +29,11 @@ public static class UsAppPrintLogScopeHelper return false; } + if (await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser)) + { + return true; + } + var roleRows = await db.Ado.SqlQueryAsync( @"SELECT r.RoleCode, r.RoleName FROM UserRole ur @@ -41,30 +46,18 @@ public static class UsAppPrintLogScopeHelper } /// - /// 校验当前用户是否绑定指定门店。 + /// 校验当前用户是否可访问指定门店(含 Company Admin 公司下全部门店)。 /// public static async Task EnsureUserBoundToLocationAsync( ICurrentUser currentUser, ISqlSugarClient db, string locationId) { - if (currentUser.Id is null) - { - throw new UserFriendlyException("用户未登录"); - } - - var lid = locationId.Trim(); - var userIdStr = currentUser.Id.Value.ToString(); - var bound = await db.Queryable() - .AnyAsync(x => !x.IsDeleted && x.UserId == userIdStr && x.LocationId == lid); - if (!bound) - { - throw new UserFriendlyException("当前账号未绑定该门店,无法查看"); - } + await EnsureUserCanAccessLocationAsync(currentUser, db, locationId); } /// - /// 校验是否可访问指定门店:平台管理员()不校验 userlocation;其它账号须绑定。 + /// 校验是否可访问指定门店:平台管理员不校验;Company Admin 可访问绑定 Company 下任意门店;其它账号须 userlocation 绑定。 /// public static async Task EnsureUserCanAccessLocationAsync( ICurrentUser currentUser, @@ -76,7 +69,23 @@ public static class UsAppPrintLogScopeHelper return; } - await EnsureUserBoundToLocationAsync(currentUser, db, locationId); + if (currentUser.Id is null) + { + throw new UserFriendlyException("用户未登录"); + } + + var lid = TeamMemberListScopeHelper.NormalizeScopeKey(locationId); + if (string.IsNullOrEmpty(lid)) + { + throw new UserFriendlyException("门店参数无效"); + } + + var accessible = await LocationScopeBindingHelper.ResolveAppAccessibleLocationIdsForUserAsync( + db, currentUser.Id.Value, currentUser); + if (!accessible.Contains(lid, StringComparer.OrdinalIgnoreCase)) + { + throw new UserFriendlyException("当前账号未绑定该门店,无法查看"); + } } /// @@ -107,10 +116,9 @@ public static class UsAppPrintLogScopeHelper var map = new Dictionary(StringComparer.OrdinalIgnoreCase); var guids = userIdStrings .Where(x => !string.IsNullOrWhiteSpace(x)) - .Select(x => x!.Trim()) - .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null) - .Where(x => x.HasValue) - .Select(x => x!.Value) + .Select(x => TeamMemberListScopeHelper.NormalizeScopeKey(x!)) + .Where(x => Guid.TryParse(x, out _)) + .Select(Guid.Parse) .Distinct() .ToList(); if (guids.Count == 0) @@ -118,19 +126,27 @@ public static class UsAppPrintLogScopeHelper return map; } - var users = await db.Queryable() - .Where(u => !u.IsDeleted && guids.Contains(u.Id)) - .Select(u => new { u.Id, u.Name, u.Nick, u.UserName }) - .ToListAsync(); + var parameters = new List(guids.Count); + var inClause = new List(guids.Count); + for (var i = 0; i < guids.Count; i++) + { + var paramName = $"@uid{i}"; + inClause.Add(paramName); + parameters.Add(new SugarParameter(paramName, guids[i])); + } + + var users = await db.Ado.SqlQueryAsync( + $""" + SELECT Id, UserName, Name, Nick + FROM User + WHERE IsDeleted = 0 AND Id IN ({string.Join(", ", inClause)}) + """, + parameters); foreach (var u in users) { - var display = !string.IsNullOrWhiteSpace(u.Name) - ? u.Name!.Trim() - : !string.IsNullOrWhiteSpace(u.Nick) - ? u.Nick!.Trim() - : u.UserName?.Trim() ?? string.Empty; - map[u.Id.ToString()] = string.IsNullOrWhiteSpace(display) ? "无" : display; + var display = BuildOperatorDisplayName(u.Name, u.Nick, u.UserName); + map[TeamMemberListScopeHelper.UserKey(u.Id)] = display; } return map; @@ -143,7 +159,35 @@ public static class UsAppPrintLogScopeHelper return "无"; } - return map.TryGetValue(createdBy.Trim(), out var name) ? name : "无"; + var key = TeamMemberListScopeHelper.NormalizeScopeKey(createdBy); + return map.TryGetValue(key, out var name) ? name : "无"; + } + + private static string BuildOperatorDisplayName(string? name, string? nick, string? userName) + { + if (!string.IsNullOrWhiteSpace(name)) + { + return name.Trim(); + } + + if (!string.IsNullOrWhiteSpace(nick)) + { + return nick.Trim(); + } + + var user = userName?.Trim(); + return string.IsNullOrWhiteSpace(user) ? "无" : user; + } + + private sealed class OperatorUserRow + { + public Guid Id { get; set; } + + public string? UserName { get; set; } + + public string? Name { get; set; } + + public string? Nick { get; set; } } private static bool ContainsPartnerKeyword(string? value) => diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryPartnerDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryPartnerDbEntity.cs new file mode 100644 index 0000000..3d6bcbe --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryPartnerDbEntity.cs @@ -0,0 +1,18 @@ +using SqlSugar; + +namespace FoodLabeling.Application.Services.DbModels; + +[SugarTable("fl_label_category_partner")] +public class FlLabelCategoryPartnerDbEntity +{ + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } = string.Empty; + + public string CategoryId { get; set; } = string.Empty; + + public string PartnerId { get; set; } = string.Empty; + + public DateTime CreationTime { get; set; } + + public string? CreatorId { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelMultipleOptionPartnerDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelMultipleOptionPartnerDbEntity.cs new file mode 100644 index 0000000..de72f62 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelMultipleOptionPartnerDbEntity.cs @@ -0,0 +1,18 @@ +using SqlSugar; + +namespace FoodLabeling.Application.Services.DbModels; + +[SugarTable("fl_label_multiple_option_partner")] +public class FlLabelMultipleOptionPartnerDbEntity +{ + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } = string.Empty; + + public string MultipleOptionId { get; set; } = string.Empty; + + public string PartnerId { get; set; } = string.Empty; + + public DateTime CreationTime { get; set; } + + public string? CreatorId { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTypePartnerDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTypePartnerDbEntity.cs new file mode 100644 index 0000000..4248b98 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTypePartnerDbEntity.cs @@ -0,0 +1,18 @@ +using SqlSugar; + +namespace FoodLabeling.Application.Services.DbModels; + +[SugarTable("fl_label_type_partner")] +public class FlLabelTypePartnerDbEntity +{ + [SugarColumn(IsPrimaryKey = true)] + public string Id { get; set; } = string.Empty; + + public string LabelTypeId { get; set; } = string.Empty; + + public string PartnerId { get; set; } = string.Empty; + + public DateTime CreationTime { get; set; } + + public string? CreatorId { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs index 4fe5403..2b7202a 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs +++ b/美国版/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 }; } - // 查询标签基础信息(分类/类型/模板) + // 查询标签基础信息(分类/类型/模板);类型 LeftJoin,允许未绑定 labelTypeId 的标签出现在列表 var labelRows = await _dbContext.SqlSugarClient - .Queryable( - (l, c, t, tpl) => l.LabelCategoryId == c.Id && l.LabelTypeId == t.Id && l.TemplateId == tpl.Id) + .Queryable() + .InnerJoin((l, c) => l.LabelCategoryId == c.Id) + .LeftJoin((l, c, t) => l.LabelTypeId == t.Id) + .InnerJoin((l, c, t, tpl) => l.TemplateId == tpl.Id) .Where((l, c, t, tpl) => pageLabelIds.Contains(l.Id)) - .Where((l, c, t, tpl) => !l.IsDeleted && !c.IsDeleted && !t.IsDeleted && !tpl.IsDeleted) + .Where((l, c, t, tpl) => !l.IsDeleted && !c.IsDeleted && !tpl.IsDeleted) + .Where((l, c, t, tpl) => t.Id == null || !t.IsDeleted) .Select((l, c, t, tpl) => new { l.Id, @@ -248,7 +251,7 @@ public class LabelAppService : ApplicationService, ILabelAppService Products = products, TemplateName = x.TemplateName ?? string.Empty, TemplateCode = x.TemplateCode ?? string.Empty, - LabelTypeName = x.LabelTypeName ?? string.Empty, + LabelTypeName = string.IsNullOrWhiteSpace(x.LabelTypeName) ? "无" : x.LabelTypeName, State = x.State, LastEdited = x.LastEdited, HasError = false @@ -289,8 +292,12 @@ public class LabelAppService : ApplicationService, ILabelAppService .FirstAsync(x => x.Id == label.TemplateId); var category = await db.Queryable() .FirstAsync(x => x.Id == label.LabelCategoryId); - var type = await db.Queryable() - .FirstAsync(x => x.Id == label.LabelTypeId); + FlLabelTypeDbEntity? type = null; + if (!string.IsNullOrWhiteSpace(label.LabelTypeId)) + { + type = await db.Queryable() + .FirstAsync(x => x.Id == label.LabelTypeId); + } var productIds = await db.Queryable() .Where(x => x.LabelId == label.Id) @@ -368,10 +375,8 @@ public class LabelAppService : ApplicationService, ILabelAppService { throw new UserFriendlyException("标签分类Id不能为空"); } - if (string.IsNullOrWhiteSpace(input.LabelTypeId)) - { - throw new UserFriendlyException("标签类型Id不能为空"); - } + + await EnsureLabelTypeExistsIfProvidedAsync(input.LabelTypeId); var scope = await LabelRegionScopeHelper.ResolveScopeForSaveAsync( _dbContext.SqlSugarClient, @@ -413,7 +418,7 @@ public class LabelAppService : ApplicationService, ILabelAppService TemplateId = template.Id, LocationId = scope.PrimaryLocationId, LabelCategoryId = input.LabelCategoryId?.Trim(), - LabelTypeId = input.LabelTypeId?.Trim(), + LabelTypeId = NormalizeOptionalLabelTypeId(input.LabelTypeId), State = input.State, LabelType = null, LabelInfoJson = input.LabelInfoJson == null ? null : JsonSerializer.Serialize(input.LabelInfoJson) @@ -484,10 +489,8 @@ public class LabelAppService : ApplicationService, ILabelAppService { throw new UserFriendlyException("标签分类Id不能为空"); } - if (string.IsNullOrWhiteSpace(input.LabelTypeId)) - { - throw new UserFriendlyException("标签类型Id不能为空"); - } + + await EnsureLabelTypeExistsIfProvidedAsync(input.LabelTypeId); var scope = await LabelRegionScopeHelper.ResolveScopeForSaveAsync( _dbContext.SqlSugarClient, @@ -510,7 +513,7 @@ public class LabelAppService : ApplicationService, ILabelAppService label.TemplateId = template.Id; label.LocationId = scope.PrimaryLocationId; label.LabelCategoryId = input.LabelCategoryId?.Trim(); - label.LabelTypeId = input.LabelTypeId?.Trim(); + label.LabelTypeId = NormalizeOptionalLabelTypeId(input.LabelTypeId); label.State = input.State; label.LastModifierId = CurrentUser?.Id?.ToString(); label.LastModificationTime = now; @@ -873,5 +876,27 @@ public class LabelAppService : ApplicationService, ILabelAppService var v = (raw ?? string.Empty).Trim().ToLowerInvariant(); return v is "line" or "dotted" ? v : "none"; } + + private static string? NormalizeOptionalLabelTypeId(string? labelTypeId) + { + var id = labelTypeId?.Trim(); + return string.IsNullOrWhiteSpace(id) ? null : id; + } + + private async Task EnsureLabelTypeExistsIfProvidedAsync(string? labelTypeId) + { + var id = NormalizeOptionalLabelTypeId(labelTypeId); + if (id is null) + { + return; + } + + var exists = await _dbContext.SqlSugarClient.Queryable() + .AnyAsync(x => !x.IsDeleted && x.Id == id); + if (!exists) + { + throw new UserFriendlyException("标签类型不存在"); + } + } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelCategoryAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelCategoryAppService.cs index 90abbc1..0c23f30 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelCategoryAppService.cs +++ b/美国版/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 (x.DisplayText != null && x.DisplayText.Contains(keyword!))) .WhereIF(input.State != null, x => x.State == input.State); - query = await ApplyCategoryScopeFilterAsync(query, input.GroupId, input.LocationId); + query = await ApplyCategoryScopeFilterAsync(query, input.PartnerId, input.GroupId, input.LocationId); // Sorting 仅允许白名单字段,避免 Unknown column/注入风险 if (!string.IsNullOrWhiteSpace(input.Sorting)) @@ -72,10 +72,15 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ var labelStatsMap = await BuildCategoryLabelStatsMapAsync(ids); var scopeMap = await BuildCategoryScopeMapAsync(entities); + var partnerScopeMap = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync( + _dbContext.SqlSugarClient, + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Category, + ids); var items = entities.Select(x => { scopeMap.TryGetValue(x.Id, out var scope); + partnerScopeMap.TryGetValue(x.Id, out var partnerScope); labelStatsMap.TryGetValue(x.Id, out var labelStats); var creationTime = ResolveCategoryCreationTime(x); return new LabelCategoryGetListOutputDto @@ -88,6 +93,10 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ State = x.State, ButtonAppearance = x.ButtonAppearance, AvailabilityType = x.AvailabilityType, + AppliedPartnerType = partnerScope?.AppliedPartnerType ?? LabelEntityPartnerScopeHelper.ScopeAll, + Company = partnerScope?.Company ?? LabelEntityPartnerScopeHelper.AllCompaniesDisplay, + PartnerIds = partnerScope?.PartnerIds ?? new List(), + CompanyIds = partnerScope?.PartnerIds ?? new List(), OrderNum = x.OrderNum, NoOfLabels = labelStats?.Count ?? 0, CreationTime = creationTime, @@ -112,6 +121,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ } var dto = MapToGetOutput(entity); + await ApplyPartnerScopeToGetOutputAsync(dto, entity.Id); if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) { var locationIds = await _dbContext.SqlSugarClient.Queryable() @@ -139,7 +149,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ var displayText = input.DisplayText?.Trim(); var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); - var (availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); @@ -170,6 +180,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); + await SaveCategoryPartnerScopeAsync(entity.Id, partnerScope, currentUserId, now); await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); return await GetAsync(entity.Id); } @@ -192,7 +203,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ var displayText = input.DisplayText?.Trim(); var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); - var (availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); @@ -213,6 +224,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ entity.LastModifierId = CurrentUser?.Id?.ToString(); await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + await SaveCategoryPartnerScopeAsync(entity.Id, partnerScope, entity.LastModifierId, + entity.LastModificationTime ?? DateTime.Now); await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now); return await GetAsync(id); @@ -234,6 +247,14 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ throw new UserFriendlyException("该标签分类已被标签引用,无法删除"); } + await LabelEntityPartnerScopeHelper.DeletePartnerScopeRowsAsync( + _dbContext.SqlSugarClient, + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Category, + id); + await _dbContext.SqlSugarClient.Deleteable() + .Where(x => x.CategoryId == id) + .ExecuteCommandAsync(); + entity.IsDeleted = true; entity.LastModificationTime = DateTime.Now; entity.LastModifierId = CurrentUser?.Id?.ToString(); @@ -256,9 +277,15 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ }; } - private async Task<(string AvailabilityType, List LocationIds)> ResolveCategoryScopeForSaveAsync( + private async Task<(LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult PartnerScope, string AvailabilityType, List LocationIds)> ResolveCategoryScopeForSaveAsync( LabelCategoryCreateInputVo input) { + var partnerScope = await LabelEntityPartnerScopeHelper.ResolvePartnerScopeForSaveAsync( + _dbContext.SqlSugarClient, + input.AppliedPartnerType, + input.PartnerIds, + input.CompanyIds); + var regionIds = NormalizeRegionIds(input); var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); @@ -278,20 +305,37 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); } - if (availabilityType == "ALL") + var partnerSpecified = string.Equals( + partnerScope.AppliedPartnerType, + LabelEntityPartnerScopeHelper.ScopeSpecified, + StringComparison.OrdinalIgnoreCase); + var locationSpecified = string.Equals(availabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase); + + if (partnerSpecified || locationSpecified) { - return ("ALL", new List()); + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( + _dbContext.SqlSugarClient, + partnerSpecified ? partnerScope.PartnerIds : null, + locationSpecified ? regionIds : null, + locationSpecified ? explicitLocationIds : null); + if (merged.Count == 0) + { + throw new UserFriendlyException("指定适用范围后至少需要匹配到一个有效门店"); + } + + if (locationSpecified) + { + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); + return (partnerScope, "SPECIFIED", merged); + } } - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( - _dbContext.SqlSugarClient, (IReadOnlyList?)null, regionIds, explicitLocationIds); - if (merged.Count == 0) + if (string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase)) { - throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); + return (partnerScope, "ALL", new List()); } - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); - return ("SPECIFIED", merged); + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); } private static List NormalizeRegionIds(LabelCategoryCreateInputVo input) @@ -312,17 +356,24 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ private async Task> ApplyCategoryScopeFilterAsync( ISugarQueryable query, + string? partnerId, string? groupId, string? locationId) { - var scopeLocIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( - _dbContext.SqlSugarClient, groupId, locationId); - if (scopeLocIds is null) + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync( + CurrentUser, _dbContext, partnerId, groupId, locationId); + var scopedPartnerIds = await LabelEntityPartnerScopeHelper.ResolveListPartnerIdsAsync( + _dbContext.SqlSugarClient, partnerId, scopedLocationIds); + + query = await LabelEntityPartnerScopeHelper.ApplyCategoryPartnerListFilterAsync( + _dbContext.SqlSugarClient, query, scopedPartnerIds); + + if (scopedLocationIds is null) { return query; } - if (scopeLocIds.Count == 0) + if (scopedLocationIds.Count == 0) { return query.Where(c => c.AvailabilityType == "ALL"); } @@ -330,10 +381,43 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ return query.Where(c => c.AvailabilityType == "ALL" || SqlFunc.Subqueryable() - .Where(cl => cl.CategoryId == c.Id && scopeLocIds.Contains(cl.LocationId)) + .Where(cl => cl.CategoryId == c.Id && scopedLocationIds.Contains(cl.LocationId)) .Any()); } + private async Task SaveCategoryPartnerScopeAsync( + string categoryId, + LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult partnerScope, + string? currentUserId, + DateTime now) + { + await LabelEntityPartnerScopeHelper.SavePartnerScopeAsync( + _dbContext.SqlSugarClient, + _guidGenerator, + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Category, + categoryId, + partnerScope, + currentUserId, + now); + } + + private async Task ApplyPartnerScopeToGetOutputAsync(LabelCategoryGetOutputDto dto, string entityId) + { + var map = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync( + _dbContext.SqlSugarClient, + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Category, + new[] { entityId }); + if (!map.TryGetValue(entityId, out var scope)) + { + return; + } + + dto.AppliedPartnerType = scope.AppliedPartnerType; + dto.Company = scope.Company; + dto.PartnerIds = scope.PartnerIds; + dto.CompanyIds = scope.PartnerIds; + } + private const string AllRegionsDisplay = "All Regions"; private const string AllLocationsDisplay = "All Locations"; private const string EmptyDisplay = "无"; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelMultipleOptionAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelMultipleOptionAppService.cs index a126fa3..e02596a 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelMultipleOptionAppService.cs +++ b/美国版/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 { RefAsync total = 0; var keyword = input.Keyword?.Trim(); - var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( - _dbContext.SqlSugarClient, input.GroupId, input.LocationId); + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync( + CurrentUser, + _dbContext, + input.PartnerId, + input.GroupId, + input.LocationId); + var scopedPartnerIds = await LabelEntityPartnerScopeHelper.ResolveListPartnerIdsAsync( + _dbContext.SqlSugarClient, input.PartnerId, scopedLocationIds); var query = _dbContext.SqlSugarClient.Queryable() .Where(x => !x.IsDeleted) .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.OptionCode.Contains(keyword!) || x.OptionName.Contains(keyword!)) .WhereIF(input.State != null, x => x.State == input.State); + query = await LabelEntityPartnerScopeHelper.ApplyMultipleOptionPartnerListFilterAsync( + _dbContext.SqlSugarClient, query, scopedPartnerIds); query = ApplyMultipleOptionScopeFilter(query, scopedLocationIds); if (!string.IsNullOrWhiteSpace(input.Sorting)) @@ -48,10 +56,15 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); var scopeMap = await BuildMultipleOptionScopeMapAsync(entities); + var partnerScopeMap = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync( + _dbContext.SqlSugarClient, + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.MultipleOption, + entities.Select(x => x.Id).ToList()); var items = entities.Select(x => { scopeMap.TryGetValue(x.Id, out var scope); + partnerScopeMap.TryGetValue(x.Id, out var partnerScope); return new LabelMultipleOptionGetListOutputDto { Id = x.Id, @@ -60,6 +73,10 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO OptionValuesJson = x.OptionValuesJson, State = x.State, AvailabilityType = x.AvailabilityType, + AppliedPartnerType = partnerScope?.AppliedPartnerType ?? LabelEntityPartnerScopeHelper.ScopeAll, + Company = partnerScope?.Company ?? LabelEntityPartnerScopeHelper.AllCompaniesDisplay, + PartnerIds = partnerScope?.PartnerIds ?? new List(), + CompanyIds = partnerScope?.PartnerIds ?? new List(), OrderNum = x.OrderNum, LastEdited = x.LastModificationTime ?? x.CreationTime, Region = scope?.Region ?? EmptyDisplay, @@ -82,6 +99,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO } var dto = MapToGetOutput(entity); + await ApplyPartnerScopeToGetOutputAsync(dto, entity.Id); if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) { var locationIds = await _dbContext.SqlSugarClient.Queryable() @@ -107,7 +125,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO throw new UserFriendlyException("多选项名称不能为空"); } - var (availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input); + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input); if (await IsMultipleOptionDuplicatedAsync(code, name, excludeId: null)) { @@ -134,6 +152,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); + await SaveMultipleOptionPartnerScopeAsync(entity.Id, partnerScope, currentUserId, now); await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); return await GetAsync(entity.Id); } @@ -154,7 +173,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO throw new UserFriendlyException("多选项名称不能为空"); } - var (availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input); + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input); if (await IsMultipleOptionDuplicatedAsync(code, name, excludeId: id)) { @@ -171,6 +190,8 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO entity.LastModifierId = CurrentUser?.Id?.ToString(); await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + await SaveMultipleOptionPartnerScopeAsync(entity.Id, partnerScope, entity.LastModifierId, + entity.LastModificationTime ?? DateTime.Now); await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now); return await GetAsync(id); @@ -185,6 +206,10 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO return; } + await LabelEntityPartnerScopeHelper.DeletePartnerScopeRowsAsync( + _dbContext.SqlSugarClient, + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.MultipleOption, + id); await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.MultipleOptionId == id) .ExecuteCommandAsync(); @@ -199,9 +224,15 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO private const string AllRegionsDisplay = "All Regions"; private const string AllLocationsDisplay = "All Locations"; - private async Task<(string AvailabilityType, List LocationIds)> ResolveMultipleOptionScopeForSaveAsync( + private async Task<(LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult PartnerScope, string AvailabilityType, List LocationIds)> ResolveMultipleOptionScopeForSaveAsync( LabelMultipleOptionCreateInputVo input) { + var partnerScope = await LabelEntityPartnerScopeHelper.ResolvePartnerScopeForSaveAsync( + _dbContext.SqlSugarClient, + input.AppliedPartnerType, + input.PartnerIds, + input.CompanyIds); + var regionIds = NormalizeRegionIds(input); var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); @@ -221,20 +252,70 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); } - if (availabilityType == "ALL") + var partnerSpecified = string.Equals( + partnerScope.AppliedPartnerType, + LabelEntityPartnerScopeHelper.ScopeSpecified, + StringComparison.OrdinalIgnoreCase); + var locationSpecified = string.Equals(availabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase); + + if (partnerSpecified || locationSpecified) { - return ("ALL", new List()); + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( + _dbContext.SqlSugarClient, + partnerSpecified ? partnerScope.PartnerIds : null, + locationSpecified ? regionIds : null, + locationSpecified ? explicitLocationIds : null); + if (merged.Count == 0) + { + throw new UserFriendlyException("指定适用范围后至少需要匹配到一个有效门店"); + } + + if (locationSpecified) + { + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); + return (partnerScope, "SPECIFIED", merged); + } } - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( - _dbContext.SqlSugarClient, (IReadOnlyList?)null, regionIds, explicitLocationIds); - if (merged.Count == 0) + if (string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase)) { - throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); + return (partnerScope, "ALL", new List()); + } + + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); + } + + private async Task SaveMultipleOptionPartnerScopeAsync( + string multipleOptionId, + LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult partnerScope, + string? currentUserId, + DateTime now) + { + await LabelEntityPartnerScopeHelper.SavePartnerScopeAsync( + _dbContext.SqlSugarClient, + _guidGenerator, + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.MultipleOption, + multipleOptionId, + partnerScope, + currentUserId, + now); + } + + private async Task ApplyPartnerScopeToGetOutputAsync(LabelMultipleOptionGetOutputDto dto, string entityId) + { + var map = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync( + _dbContext.SqlSugarClient, + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.MultipleOption, + new[] { entityId }); + if (!map.TryGetValue(entityId, out var scope)) + { + return; } - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); - return ("SPECIFIED", merged); + dto.AppliedPartnerType = scope.AppliedPartnerType; + dto.Company = scope.Company; + dto.PartnerIds = scope.PartnerIds; + dto.CompanyIds = scope.PartnerIds; } private static List NormalizeRegionIds(LabelMultipleOptionCreateInputVo input) diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTypeAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTypeAppService.cs index d012691..34ac840 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTypeAppService.cs +++ b/美国版/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 { RefAsync total = 0; var keyword = input.Keyword?.Trim(); - var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( - _dbContext.SqlSugarClient, input.GroupId, input.LocationId); + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync( + CurrentUser, + _dbContext, + input.PartnerId, + input.GroupId, + input.LocationId); + var scopedPartnerIds = await LabelEntityPartnerScopeHelper.ResolveListPartnerIdsAsync( + _dbContext.SqlSugarClient, input.PartnerId, scopedLocationIds); var query = _dbContext.SqlSugarClient.Queryable() .Where(x => !x.IsDeleted) .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.TypeCode.Contains(keyword!) || x.TypeName.Contains(keyword!)) .WhereIF(input.State != null, x => x.State == input.State); + query = await LabelEntityPartnerScopeHelper.ApplyTypePartnerListFilterAsync( + _dbContext.SqlSugarClient, query, scopedPartnerIds); query = ApplyLabelTypeScopeFilter(query, scopedLocationIds); if (!string.IsNullOrWhiteSpace(input.Sorting)) @@ -51,10 +59,15 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService var countMap = await BuildTypeLabelStatsMapAsync(ids, scopedLocationIds); var scopeMap = await BuildTypeConfiguredScopeMapAsync(entities); + var partnerScopeMap = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync( + _dbContext.SqlSugarClient, + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Type, + ids); var items = entities.Select(x => { scopeMap.TryGetValue(x.Id, out var scope); + partnerScopeMap.TryGetValue(x.Id, out var partnerScope); countMap.TryGetValue(x.Id, out var stats); var locationDisplay = scope?.Location ?? EmptyDisplay; return new LabelTypeGetListOutputDto @@ -64,6 +77,10 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService TypeName = x.TypeName, State = x.State, AvailabilityType = x.AvailabilityType, + AppliedPartnerType = partnerScope?.AppliedPartnerType ?? LabelEntityPartnerScopeHelper.ScopeAll, + Company = partnerScope?.Company ?? LabelEntityPartnerScopeHelper.AllCompaniesDisplay, + PartnerIds = partnerScope?.PartnerIds ?? new List(), + CompanyIds = partnerScope?.PartnerIds ?? new List(), OrderNum = x.OrderNum, NoOfLabels = stats?.Count ?? 0, LastEdited = stats?.MaxEdited ?? x.LastModificationTime ?? x.CreationTime, @@ -88,6 +105,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService } var dto = MapToGetOutput(entity); + await ApplyPartnerScopeToGetOutputAsync(dto, entity.Id); if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) { var locationIds = await _dbContext.SqlSugarClient.Queryable() @@ -113,7 +131,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService throw new UserFriendlyException("类型编码和名称不能为空"); } - var (availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input); + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input); var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && (x.TypeCode == code || x.TypeName == name)); @@ -141,6 +159,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); + await SaveTypePartnerScopeAsync(entity.Id, partnerScope, currentUserId, now); await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); return await GetAsync(entity.Id); } @@ -161,7 +180,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService throw new UserFriendlyException("类型编码和名称不能为空"); } - var (availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input); + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input); var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.TypeCode == code || x.TypeName == name)); @@ -179,6 +198,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService entity.LastModifierId = CurrentUser?.Id?.ToString(); await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + await SaveTypePartnerScopeAsync(entity.Id, partnerScope, entity.LastModifierId, + entity.LastModificationTime ?? DateTime.Now); await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now); return await GetAsync(id); @@ -200,6 +221,10 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService throw new UserFriendlyException("该标签类型已被标签引用,无法删除"); } + await LabelEntityPartnerScopeHelper.DeletePartnerScopeRowsAsync( + _dbContext.SqlSugarClient, + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Type, + id); await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.LabelTypeId == id) .ExecuteCommandAsync(); @@ -214,9 +239,15 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService private const string AllRegionsDisplay = "All Regions"; private const string AllLocationsDisplay = "All Locations"; - private async Task<(string AvailabilityType, List LocationIds)> ResolveTypeScopeForSaveAsync( + private async Task<(LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult PartnerScope, string AvailabilityType, List LocationIds)> ResolveTypeScopeForSaveAsync( LabelTypeCreateInputVo input) { + var partnerScope = await LabelEntityPartnerScopeHelper.ResolvePartnerScopeForSaveAsync( + _dbContext.SqlSugarClient, + input.AppliedPartnerType, + input.PartnerIds, + input.CompanyIds); + var regionIds = NormalizeRegionIds(input); var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); @@ -236,20 +267,70 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); } - if (availabilityType == "ALL") + var partnerSpecified = string.Equals( + partnerScope.AppliedPartnerType, + LabelEntityPartnerScopeHelper.ScopeSpecified, + StringComparison.OrdinalIgnoreCase); + var locationSpecified = string.Equals(availabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase); + + if (partnerSpecified || locationSpecified) { - return ("ALL", new List()); + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( + _dbContext.SqlSugarClient, + partnerSpecified ? partnerScope.PartnerIds : null, + locationSpecified ? regionIds : null, + locationSpecified ? explicitLocationIds : null); + if (merged.Count == 0) + { + throw new UserFriendlyException("指定适用范围后至少需要匹配到一个有效门店"); + } + + if (locationSpecified) + { + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); + return (partnerScope, "SPECIFIED", merged); + } } - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( - _dbContext.SqlSugarClient, (IReadOnlyList?)null, regionIds, explicitLocationIds); - if (merged.Count == 0) + if (string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase)) { - throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); + return (partnerScope, "ALL", new List()); + } + + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); + } + + private async Task SaveTypePartnerScopeAsync( + string labelTypeId, + LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult partnerScope, + string? currentUserId, + DateTime now) + { + await LabelEntityPartnerScopeHelper.SavePartnerScopeAsync( + _dbContext.SqlSugarClient, + _guidGenerator, + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Type, + labelTypeId, + partnerScope, + currentUserId, + now); + } + + private async Task ApplyPartnerScopeToGetOutputAsync(LabelTypeGetOutputDto dto, string entityId) + { + var map = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync( + _dbContext.SqlSugarClient, + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Type, + new[] { entityId }); + if (!map.TryGetValue(entityId, out var scope)) + { + return; } - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); - return ("SPECIFIED", merged); + dto.AppliedPartnerType = scope.AppliedPartnerType; + dto.Company = scope.Company; + dto.PartnerIds = scope.PartnerIds; + dto.CompanyIds = scope.PartnerIds; } private static List NormalizeRegionIds(LabelTypeCreateInputVo input) diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs index 8f6ff0d..c39f1d4 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs +++ b/美国版/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 (x.DisplayText != null && x.DisplayText.Contains(keyword!))) .WhereIF(input.State != null, x => x.State == input.State); - query = await ApplyCategoryScopeFilterAsync(query, input.GroupId, input.LocationId); + query = await ApplyCategoryScopeFilterAsync(query, input.PartnerId, input.GroupId, input.LocationId); // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column // 同时避免将 input.Sorting 原样拼接到 SQL(存在注入风险) @@ -116,6 +116,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp throw new UserFriendlyException("类别不存在"); } + await EnsureCategoryVisibleToCurrentUserAsync(entity); + var dto = MapToGetOutput(entity); if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) { @@ -189,6 +191,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp throw new UserFriendlyException("类别不存在"); } + await EnsureCategoryVisibleToCurrentUserAsync(entity); + var code = NormalizeCategoryCode(input.CategoryCode); var name = input.CategoryName?.Trim(); if (string.IsNullOrWhiteSpace(name)) @@ -231,6 +235,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp return; } + await EnsureCategoryVisibleToCurrentUserAsync(entity); + // 若被产品引用则不允许删除 var usedByProduct = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && x.CategoryId == id); @@ -321,28 +327,59 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp private async Task> ApplyCategoryScopeFilterAsync( ISugarQueryable query, + string? partnerId, string? groupId, string? locationId) { - var scopeLocIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( - _dbContext.SqlSugarClient, groupId, locationId); - if (scopeLocIds is null) + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync( + CurrentUser, _dbContext, partnerId, groupId, locationId); + if (scopedLocationIds is null) { return query; } - if (scopeLocIds.Count == 0) + if (scopedLocationIds.Count == 0) { - return query.Where(c => c.AvailabilityType == "ALL"); + return query.Where(_ => false); } + // 非平台管理员:仅返回绑定门店与可见范围有交集的分类(不再因 AvailabilityType=ALL 暴露其它公司数据) return query.Where(c => - c.AvailabilityType == "ALL" || SqlFunc.Subqueryable() - .Where(cl => cl.CategoryId == c.Id && scopeLocIds.Contains(cl.LocationId)) + .Where(cl => cl.CategoryId == c.Id && scopedLocationIds.Contains(cl.LocationId)) .Any()); } + /// + /// 详情/编辑/删除:非管理员须与绑定门店范围有交集。 + /// + private async Task EnsureCategoryVisibleToCurrentUserAsync(FlProductCategoryDbEntity entity) + { + if (ReportsRoleHelper.IsAdminRole(CurrentUser)) + { + return; + } + + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync( + CurrentUser, _dbContext, null, null, null); + if (scopedLocationIds is null) + { + return; + } + + if (scopedLocationIds.Count == 0) + { + throw new UserFriendlyException("无权访问该类别"); + } + + var visible = await _dbContext.SqlSugarClient.Queryable() + .AnyAsync(cl => cl.CategoryId == entity.Id && scopedLocationIds.Contains(cl.LocationId)); + if (!visible) + { + throw new UserFriendlyException("无权访问该类别"); + } + } + private async Task SaveCategoryLocationsAsync( string categoryId, string availabilityType, diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs index 6379a81..443f1ec 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs +++ b/美国版/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 throw new UserFriendlyException("用户未登录"); } - var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); + var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync( + CurrentUser, + _dbContext.SqlSugarClient, + input.PartnerId, + input.GroupId, + input.LocationId); if (locationIds is not null && locationIds.Count == 0) { return EmptyPrintLogPage(input); } var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); + var viewAllPrints = await ResolveViewAllPrintsAsync(); var currentUserIdStr = CurrentUser.Id.Value.ToString(); var keyword = input.Keyword?.Trim(); RefAsync total = 0; - var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + var query = BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword) .LeftJoin((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) .Where((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && @@ -166,18 +171,23 @@ public class ReportsAppService : ApplicationService, IReportsAppService throw new UserFriendlyException("用户未登录"); } - var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); + var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync( + CurrentUser, + _dbContext.SqlSugarClient, + input.PartnerId, + input.GroupId, + input.LocationId); if (locationIds is not null && locationIds.Count == 0) { return BuildEmptyPdf("print-log-empty.pdf"); } var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); + var viewAllPrints = await ResolveViewAllPrintsAsync(); var currentUserIdStr = CurrentUser.Id.Value.ToString(); var keyword = input.Keyword?.Trim(); - var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + var query = BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword) .LeftJoin((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) .Where((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && @@ -305,7 +315,12 @@ public class ReportsAppService : ApplicationService, IReportsAppService throw new UserFriendlyException("用户未登录"); } - var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); + var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync( + CurrentUser, + _dbContext.SqlSugarClient, + input.PartnerId, + input.GroupId, + input.LocationId); if (locationIds is not null && locationIds.Count == 0) { var emptyMs = ReportsPrintLogExcelHelper.BuildWorkbook(Array.Empty()); @@ -315,11 +330,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService } var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); + var viewAllPrints = await ResolveViewAllPrintsAsync(); var currentUserIdStr = CurrentUser.Id.Value.ToString(); var keyword = input.Keyword?.Trim(); - var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + var query = BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword) .LeftJoin((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) .Where((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && @@ -414,11 +429,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService } var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); + var viewAllPrints = await ResolveViewAllPrintsAsync(); var currentUserIdStr = CurrentUser.Id.Value.ToString(); var templateKeyword = input.Keyword?.Trim(); - var groupedRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword: null, + var groupedRows = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword: null, restrictToCreator: false) .LeftJoin((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) .Where((t, l, p, lc, pc, loc, tpl) => @@ -499,17 +514,17 @@ public class ReportsAppService : ApplicationService, IReportsAppService var prevEndExcl = curStart; var prevStart = curStart - span; - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); + var viewAllPrints = await ResolveViewAllPrintsAsync(); var currentUserIdStr = CurrentUser.Id.Value.ToString(); var keyword = input.Keyword?.Trim(); - var totalCur = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) + var totalCur = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) .CountAsync(); - var totalPrev = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) + var totalPrev = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl) @@ -520,7 +535,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService var avgDaily = Math.Round((decimal)totalCur / dayCount, 2); var avgDailyPrev = Math.Round((decimal)totalPrev / prevDayCount, 2); - var categoryRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) + var categoryRows = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => l.LabelCategoryId != null && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && @@ -531,7 +546,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService var topCat = categoryRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); - var productRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) + var productRows = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => !string.IsNullOrEmpty(p.Id) && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && @@ -552,7 +567,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService var trendEndExcl = trendEndDay.AddDays(1); - var trendRaw = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) + var trendRaw = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl) @@ -704,7 +719,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService FlProductCategoryDbEntity, LocationAggregateRoot> BuildReportTaskCore( List? locationIds, - bool isAdmin, + bool viewAllPrints, string currentUserIdStr, string? keyword, bool restrictToCreator = true) @@ -717,7 +732,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService .LeftJoin((t, l, p, lc, pc, loc) => t.LocationId != null && SqlFunc.ToString(loc.Id) == t.LocationId) .Where((t, l, p, lc, pc, loc) => !loc.IsDeleted) - .WhereIF(restrictToCreator && !isAdmin, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr) + .WhereIF(restrictToCreator && !viewAllPrints, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr) .WhereIF(locationIds is not null, (t, l, p, lc, pc, loc) => locationIds!.Contains(t.LocationId!)) .WhereIF(!string.IsNullOrWhiteSpace(keyword), (t, l, p, lc, pc, loc) => @@ -739,56 +754,8 @@ public class ReportsAppService : ApplicationService, IReportsAppService private static decimal CalcChangeRate(int current, int previous) => CalcChangeRate((decimal)current, (decimal)previous); - private async Task?> ResolveFilteredLocationIdsAsync(string? partnerId, string? groupId, - string? locationId) - { - var locId = locationId?.Trim(); - if (!string.IsNullOrWhiteSpace(locId)) - { - return new List { locId }; - } - - var gid = groupId?.Trim(); - var pid = partnerId?.Trim(); - - if (string.IsNullOrWhiteSpace(pid) && string.IsNullOrWhiteSpace(gid)) - { - return null; - } - - var q = _dbContext.SqlSugarClient.Queryable().Where(x => !x.IsDeleted); - - if (!string.IsNullOrWhiteSpace(gid)) - { - var g = await _dbContext.SqlSugarClient.Queryable() - .FirstAsync(x => !x.IsDeleted && x.Id == gid); - if (g is null) - { - return new List(); - } - - var gName = g.GroupName?.Trim() ?? string.Empty; - var partner = await _dbContext.SqlSugarClient.Queryable() - .FirstAsync(x => !x.IsDeleted && x.Id == g.PartnerId); - var pName = partner?.PartnerName?.Trim() ?? string.Empty; - q = q.Where(x => x.GroupName == gName && x.Partner == pName); - } - else if (!string.IsNullOrWhiteSpace(pid)) - { - var partner = await _dbContext.SqlSugarClient.Queryable() - .FirstAsync(x => !x.IsDeleted && x.Id == pid); - if (partner is null) - { - return new List(); - } - - var pName = partner.PartnerName?.Trim() ?? string.Empty; - q = q.Where(x => x.Partner == pName); - } - - var ids = await q.Select(x => SqlFunc.ToString(x.Id)).ToListAsync(); - return ids; - } + private Task ResolveViewAllPrintsAsync() => + UsAppPrintLogScopeHelper.CanViewAllPrintsAtLocationAsync(CurrentUser, _dbContext.SqlSugarClient); private static (DateTime rangeStart, DateTime rangeEndExcl) ResolveDateRange(DateTime? startDate, DateTime? endDate) diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs index 704240c..6b692e0 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs +++ b/美国版/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 /// /// /// 行为与系统 AccountService.PostLoginAsync 一致(含验证码、登录日志事件)。 - /// 门店数据来自 userlocationlocation 表。 + /// 门店来自 userlocation;Company Admin 展开为绑定 Company 下全部门店。 /// /// 邮箱、密码;若系统开启验证码则需传 Uuid、Code /// Token、RefreshToken 与绑定门店 @@ -150,7 +150,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService _forgotPasswordByEmailService.ResetPasswordByEmailAsync(input); /// - /// 获取当前登录用户已绑定的门店(切换门店时可重新拉取) + /// 获取当前登录用户可选门店(切换门店时可重新拉取);Company Admin 返回绑定 Company 下全部门店。 /// [Authorize] public virtual async Task> GetMyLocationsAsync() @@ -549,21 +549,25 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService private async Task> LoadBoundLocationsAsync(Guid userId) { - var userIdStr = userId.ToString(); - var links = await _dbContext.SqlSugarClient.Queryable() - .Where(x => !x.IsDeleted && x.UserId == userIdStr) - .Select(x => x.LocationId) - .ToListAsync(); + var accessibleIds = await LocationScopeBindingHelper.ResolveAppAccessibleLocationIdsForUserAsync( + _dbContext.SqlSugarClient, userId); + if (accessibleIds.Count == 0) + { + return new List(); + } - if (links.Count == 0) + var guidList = accessibleIds + .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null) + .Where(x => x.HasValue) + .Select(x => x!.Value) + .ToList(); + if (guidList.Count == 0) { return new List(); } - var wanted = links.Distinct().ToList(); var locations = (await _dbContext.SqlSugarClient.Queryable() - .Where(x => !x.IsDeleted) - .Where(x => wanted.Contains(x.Id.ToString())) + .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) .ToListAsync()) .OrderBy(x => x.OrderNum) .ThenBy(x => x.LocationName) diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs index e397ea1..8606243 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs +++ b/美国版/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 }) .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); + var pageTaskIds = pageRows.Select(x => x.Id).ToList(); + var createdByByTaskId = pageTaskIds.Count == 0 + ? new Dictionary(StringComparer.Ordinal) + : (await _dbContext.SqlSugarClient.Queryable() + .Where(t => pageTaskIds.Contains(t.Id)) + .Select(t => new { t.Id, t.CreatedBy }) + .ToListAsync()) + .ToDictionary(x => x.Id, x => x.CreatedBy, StringComparer.Ordinal); + var operatorMap = await UsAppPrintLogScopeHelper.LoadOperatorNameMapAsync( _dbContext.SqlSugarClient, - pageRows.Select(x => x.CreatedBy)); + createdByByTaskId.Values.Concat(pageRows.Select(x => x.CreatedBy))); var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync( _dbContext.SqlSugarClient, @@ -968,7 +977,9 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ LabelSizeText = FormatLabelSizeWithUnit(x.TemplateWidth, x.TemplateHeight, x.TemplateUnit), PrintInputJson = x.PrintInputJson, PrintedAt = x.PrintedAt ?? x.CreationTime, - OperatorName = UsAppPrintLogScopeHelper.ResolveOperatorName(operatorMap, x.CreatedBy), + OperatorName = UsAppPrintLogScopeHelper.ResolveOperatorName( + operatorMap, + createdByByTaskId.TryGetValue(x.Id, out var createdBy) ? createdBy : x.CreatedBy), LocationName = locationName }).ToList(); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_entity_partner_scope.sql b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_entity_partner_scope.sql new file mode 100644 index 0000000..d5886b1 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_entity_partner_scope.sql @@ -0,0 +1,79 @@ +-- 标签类型 / 分类 / 多选项适用 Company 多选(ALL/SPECIFIED) +-- 执行前请备份;可重复执行 + +SET @db := DATABASE(); + +-- fl_label_type.AppliedPartnerType +SET @col_type_partner := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'fl_label_type' AND COLUMN_NAME = 'AppliedPartnerType' +); +SET @ddl_type_partner := IF( + @col_type_partner = 0, + 'ALTER TABLE `fl_label_type` ADD COLUMN `AppliedPartnerType` varchar(20) NOT NULL DEFAULT ''ALL'' COMMENT ''适用Company:ALL/SPECIFIED'' AFTER `AvailabilityType`', + 'SELECT 1' +); +PREPARE stmt_type_partner FROM @ddl_type_partner; +EXECUTE stmt_type_partner; +DEALLOCATE PREPARE stmt_type_partner; + +-- fl_label_category.AppliedPartnerType +SET @col_cat_partner := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'fl_label_category' AND COLUMN_NAME = 'AppliedPartnerType' +); +SET @ddl_cat_partner := IF( + @col_cat_partner = 0, + 'ALTER TABLE `fl_label_category` ADD COLUMN `AppliedPartnerType` varchar(20) NOT NULL DEFAULT ''ALL'' COMMENT ''适用Company:ALL/SPECIFIED'' AFTER `AvailabilityType`', + 'SELECT 1' +); +PREPARE stmt_cat_partner FROM @ddl_cat_partner; +EXECUTE stmt_cat_partner; +DEALLOCATE PREPARE stmt_cat_partner; + +-- fl_label_multiple_option.AppliedPartnerType +SET @col_opt_partner := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'fl_label_multiple_option' AND COLUMN_NAME = 'AppliedPartnerType' +); +SET @ddl_opt_partner := IF( + @col_opt_partner = 0, + 'ALTER TABLE `fl_label_multiple_option` ADD COLUMN `AppliedPartnerType` varchar(20) NOT NULL DEFAULT ''ALL'' COMMENT ''适用Company:ALL/SPECIFIED'' AFTER `AvailabilityType`', + 'SELECT 1' +); +PREPARE stmt_opt_partner FROM @ddl_opt_partner; +EXECUTE stmt_opt_partner; +DEALLOCATE PREPARE stmt_opt_partner; + +CREATE TABLE IF NOT EXISTS `fl_label_type_partner` ( + `Id` varchar(36) NOT NULL COMMENT '主键', + `LabelTypeId` varchar(36) NOT NULL COMMENT 'fl_label_type.Id', + `PartnerId` varchar(36) NOT NULL COMMENT 'fl_partner.Id', + `CreationTime` datetime NOT NULL COMMENT '创建时间', + `CreatorId` varchar(36) DEFAULT NULL COMMENT '创建者', + PRIMARY KEY (`Id`), + KEY `idx_fl_ltyp_template` (`LabelTypeId`), + KEY `idx_fl_ltyp_partner` (`PartnerId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签类型适用Company'; + +CREATE TABLE IF NOT EXISTS `fl_label_category_partner` ( + `Id` varchar(36) NOT NULL COMMENT '主键', + `CategoryId` varchar(36) NOT NULL COMMENT 'fl_label_category.Id', + `PartnerId` varchar(36) NOT NULL COMMENT 'fl_partner.Id', + `CreationTime` datetime NOT NULL COMMENT '创建时间', + `CreatorId` varchar(36) DEFAULT NULL COMMENT '创建者', + PRIMARY KEY (`Id`), + KEY `idx_fl_lcatp_category` (`CategoryId`), + KEY `idx_fl_lcatp_partner` (`PartnerId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签分类适用Company'; + +CREATE TABLE IF NOT EXISTS `fl_label_multiple_option_partner` ( + `Id` varchar(36) NOT NULL COMMENT '主键', + `MultipleOptionId` varchar(36) NOT NULL COMMENT 'fl_label_multiple_option.Id', + `PartnerId` varchar(36) NOT NULL COMMENT 'fl_partner.Id', + `CreationTime` datetime NOT NULL COMMENT '创建时间', + `CreatorId` varchar(36) DEFAULT NULL COMMENT '创建者', + PRIMARY KEY (`Id`), + KEY `idx_fl_lmop_option` (`MultipleOptionId`), + KEY `idx_fl_lmop_partner` (`PartnerId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签多选项适用Company'; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json b/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json index 1078ea3..1a80493 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json @@ -20,7 +20,7 @@ }, //应用启动:SelfUrl 供 Program.cs UseUrls 绑定;用 0.0.0.0 避免写成固定局域网 IP 在本机无该网卡时启动失败(WinError 10049) "App": { - "SelfUrl": "http://192.168.31.89:19001", + "SelfUrl": "http://192.168.31.88:19001", "CorsOrigins": "http://localhost:19001;http://localhost:18000;http://localhost:5666;http://localhost:5173;http://localhost:3000" }, //配置 diff --git a/美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx b/美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx index 31cc22c..c34b89f 100755 --- a/美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx +++ b/美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx @@ -60,6 +60,7 @@ import { } from "../ui/pagination"; import { getLabelCategories, + getLabelCategory, createLabelCategory, updateLabelCategory, deleteLabelCategory, @@ -68,9 +69,9 @@ import { getLocations } from "../../services/locationService"; import { getGroups } from "../../services/groupService"; import { getPartners } from "../../services/partnerService"; import { - buildLabelingScopeSaveBody, + buildEntityScopeSaveFromForm, hydrateCategoryScopeFromLocationIds, - hydrateLabelingScopeFromLocationIds, + hydrateEntityScopeFromDto, } from "../../lib/categoryScopeForm"; import { CategoryScopeFields } from "../shared/category-scope-fields"; import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth"; @@ -923,17 +924,17 @@ function CreateLabelCategoryDialog({ } } - const scopePayload = buildLabelingScopeSaveBody({ + const scopePayload = buildEntityScopeSaveFromForm({ requireCompanySelection, selectedPartnerId, - fixedPartnerId, selectedPartnerIds, selectedRegionNames, selectedRegionIds, selectedLocationIds, + fixedPartnerId, + locations, partners, groups, - locations, }); if (!scopePayload.ok) { toast.error("Validation failed", { description: scopePayload.message }); @@ -967,13 +968,7 @@ function CreateLabelCategoryDialog({ buttonImageUrl: apSel.image ? String(form.categoryPhotoUrl ?? "").trim() || null : null, buttonAppearance: tokens, buttonStyleJson, - availabilityType: scopePayload.body.availabilityType, - appliedPartnerType: scopePayload.body.appliedPartnerType, - partnerIds: scopePayload.body.partnerIds, - companyIds: scopePayload.body.companyIds, - regionIds: scopePayload.body.regionIds, - groupIds: scopePayload.body.groupIds, - locationIds: scopePayload.body.locationIds, + ...scopePayload.body, }); toast.success("Label category created.", { description: "The label category has been created successfully.", @@ -1028,6 +1023,10 @@ function CreateLabelCategoryDialog({ requireCompanySelection={requireCompanySelection} fixedPartnerId={fixedPartnerId} templateScopeMode={requireCompanySelection} + selectedPartnerIds={selectedPartnerIds} + onPartnerIdsChange={setSelectedPartnerIds} + selectedRegionIds={selectedRegionIds} + onRegionIdsChange={setSelectedRegionIds} />
@@ -1234,6 +1233,7 @@ function EditLabelCategoryDialog({ onUpdated: () => void; }) { const [submitting, setSubmitting] = useState(false); + const [loadingDetail, setLoadingDetail] = useState(false); const [apSel, setApSel] = useState({ text: true, color: false, image: false }); const [displayTextForPhoto, setDisplayTextForPhoto] = useState(""); const [buttonBgColor, setButtonBgColor] = useState("#3B82F6"); @@ -1257,117 +1257,145 @@ function EditLabelCategoryDialog({ }, [apSel.text, apSel.color]); useEffect(() => { - if (!open || !category) return; + if (!open || !category?.id) return; - const hydrateAvailability = () => { - const rawAt = String(category.availabilityType ?? "ALL").trim().toUpperCase(); - if (rawAt !== "SPECIFIED") { + const ac = new AbortController(); + setLoadingDetail(true); + + (async () => { + try { + const detail = await getLabelCategory(category.id, ac.signal); + if (ac.signal.aborted) return; + const row = detail.id ? detail : category; + + const hydrateAvailability = () => { + if (requireCompanySelection) { + const scope = hydrateEntityScopeFromDto(row, locations, partners, groups); + setSelectedPartnerIds(scope.partnerIds); + setSelectedRegionIds(scope.regionIds); + setSelectedLocationIds(scope.locationIds); + setSelectedPartnerId(""); + setSelectedRegionNames([]); + return; + } + const rawAt = String(row.availabilityType ?? "ALL").trim().toUpperCase(); + if (rawAt !== "SPECIFIED") { + setSelectedPartnerId(""); + setSelectedRegionNames([]); + setSelectedLocationIds([]); + setSelectedPartnerIds([]); + setSelectedRegionIds([]); + return; + } + const lids = (row.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean); + const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups); + setSelectedPartnerId(scope.partnerId); + setSelectedRegionNames(scope.regionNames); + setSelectedLocationIds(scope.locationIds); + setSelectedPartnerIds([]); + setSelectedRegionIds([]); + }; + + const styleParsed = parseCategoryButtonStyleV1( + row.buttonStyleJson ?? (row as { ButtonStyleJson?: string | null }).ButtonStyleJson, + ); + if (styleParsed) { + const sel = appearanceSelectionFromTokens(styleParsed.appearances); + setApSel(sel); + setDisplayTextForPhoto(String(styleParsed.displayText ?? "")); + setButtonBgColor(styleParsed.buttonBgColor || "#3B82F6"); + setForm({ + categoryName: row.categoryName ?? "", + categoryPhotoUrl: sel.image ? row.categoryPhotoUrl ?? null : null, + state: row.state ?? true, + orderNum: row.orderNum ?? null, + }); + hydrateAvailability(); + return; + } + + const rawBa = + row.buttonAppearance ?? + (row as { ButtonAppearance?: string | null }).ButtonAppearance ?? + undefined; + const tokens = parseAppearanceTokens(rawBa); + const valArrZip = parseCategoryPhotoUrlValueArray(row.categoryPhotoUrl); + if (valArrZip && tokens.length > 0 && tokens.length === valArrZip.length) { + const merged = visualInputFromAppearanceAndValueArray(tokens, valArrZip, { + categoryName: row.categoryName, + name: undefined, + buttonTextColor: row.buttonTextColor ?? null, + }); + const selZip = appearanceSelectionFromTokens(tokens); + setApSel(selZip); + setDisplayTextForPhoto(String(merged.displayText ?? "")); + setButtonBgColor(merged.buttonBgColor?.trim() ? merged.buttonBgColor : "#3B82F6"); + setForm({ + categoryName: row.categoryName ?? "", + categoryPhotoUrl: selZip.image ? (merged.buttonImageUrl ?? "").trim() || null : null, + state: row.state ?? true, + orderNum: row.orderNum ?? null, + }); + hydrateAvailability(); + return; + } + + let sel = appearanceSelectionFromTokens(tokens); + if (tokens.length === 0) { + const v = resolveCategoryButtonVisual({ + buttonAppearance: rawBa, + displayText: row.displayText, + buttonBgColor: row.buttonBgColor, + buttonImageUrl: row.buttonImageUrl, + categoryPhotoUrl: row.categoryPhotoUrl, + categoryName: row.categoryName, + }); + if (v.mode === "image") sel = { text: false, color: false, image: true }; + else if (v.mode === "colorText") sel = { text: true, color: true, image: false }; + else if (v.mode === "color") sel = { text: false, color: true, image: false }; + else if (v.mode === "text") sel = { text: true, color: false, image: false }; + else sel = { text: true, color: false, image: false }; + } + setApSel(sel); + const photo = String(row.categoryPhotoUrl ?? "").trim(); + const disp = String(row.displayText ?? "").trim(); + const bgField = normalizeHexColor(row.buttonBgColor); + const hexFromPhoto = normalizeHexColor(photo); + setDisplayTextForPhoto( + String( + disp || + (!sel.image && sel.text && photo && !hexFromPhoto && !looksLikeImageUrl(photo) ? photo : "") || + (!sel.image && sel.text && !sel.color ? (row.categoryName ?? "").trim() : ""), + ), + ); + setButtonBgColor(bgField || (sel.color && hexFromPhoto ? hexFromPhoto : "") || "#3B82F6"); + setForm({ + categoryName: row.categoryName ?? "", + categoryPhotoUrl: sel.image ? row.categoryPhotoUrl ?? null : null, + state: row.state ?? true, + orderNum: row.orderNum ?? null, + }); + hydrateAvailability(); + } catch { + if (ac.signal.aborted) return; + setForm({ + categoryName: category.categoryName ?? "", + categoryPhotoUrl: category.categoryPhotoUrl ?? null, + state: category.state ?? true, + orderNum: category.orderNum ?? null, + }); setSelectedPartnerId(""); setSelectedPartnerIds([]); setSelectedRegionNames([]); setSelectedRegionIds([]); setSelectedLocationIds([]); - return; - } - const lids = (category.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean); - if (requireCompanySelection) { - const scope = hydrateLabelingScopeFromLocationIds(lids, locations, partners, groups); - setSelectedPartnerIds(scope.partnerIds); - setSelectedRegionIds(scope.regionIds); - setSelectedLocationIds(scope.locationIds); - setSelectedPartnerId(""); - setSelectedRegionNames([]); - return; + } finally { + if (!ac.signal.aborted) setLoadingDetail(false); } - const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups); - setSelectedPartnerId(scope.partnerId); - setSelectedRegionNames(scope.regionNames); - setSelectedLocationIds(scope.locationIds); - setSelectedPartnerIds([]); - setSelectedRegionIds([]); - }; - - const styleParsed = parseCategoryButtonStyleV1( - category.buttonStyleJson ?? (category as { ButtonStyleJson?: string | null }).ButtonStyleJson, - ); - if (styleParsed) { - const sel = appearanceSelectionFromTokens(styleParsed.appearances); - setApSel(sel); - setDisplayTextForPhoto(String(styleParsed.displayText ?? "")); - setButtonBgColor(styleParsed.buttonBgColor || "#3B82F6"); - setForm({ - categoryName: category.categoryName ?? "", - categoryPhotoUrl: sel.image ? category.categoryPhotoUrl ?? null : null, - state: category.state ?? true, - orderNum: category.orderNum ?? null, - }); - hydrateAvailability(); - return; - } - - const rawBa = - category.buttonAppearance ?? - (category as { ButtonAppearance?: string | null }).ButtonAppearance ?? - undefined; - const tokens = parseAppearanceTokens(rawBa); - const valArrZip = parseCategoryPhotoUrlValueArray(category.categoryPhotoUrl); - if (valArrZip && tokens.length > 0 && tokens.length === valArrZip.length) { - const merged = visualInputFromAppearanceAndValueArray(tokens, valArrZip, { - categoryName: category.categoryName, - name: undefined, - buttonTextColor: category.buttonTextColor ?? null, - }); - const selZip = appearanceSelectionFromTokens(tokens); - setApSel(selZip); - setDisplayTextForPhoto(String(merged.displayText ?? "")); - setButtonBgColor(merged.buttonBgColor?.trim() ? merged.buttonBgColor : "#3B82F6"); - setForm({ - categoryName: category.categoryName ?? "", - categoryPhotoUrl: selZip.image ? (merged.buttonImageUrl ?? "").trim() || null : null, - state: category.state ?? true, - orderNum: category.orderNum ?? null, - }); - hydrateAvailability(); - return; - } + })(); - let sel = appearanceSelectionFromTokens(tokens); - if (tokens.length === 0) { - const v = resolveCategoryButtonVisual({ - buttonAppearance: rawBa, - displayText: category.displayText, - buttonBgColor: category.buttonBgColor, - buttonImageUrl: category.buttonImageUrl, - categoryPhotoUrl: category.categoryPhotoUrl, - categoryName: category.categoryName, - }); - if (v.mode === "image") sel = { text: false, color: false, image: true }; - else if (v.mode === "colorText") sel = { text: true, color: true, image: false }; - else if (v.mode === "color") sel = { text: false, color: true, image: false }; - else if (v.mode === "text") sel = { text: true, color: false, image: false }; - else sel = { text: true, color: false, image: false }; - } - setApSel(sel); - const photo = String(category.categoryPhotoUrl ?? "").trim(); - const disp = String(category.displayText ?? "").trim(); - const bgField = normalizeHexColor(category.buttonBgColor); - const hexFromPhoto = normalizeHexColor(photo); - setDisplayTextForPhoto( - String( - disp || - (!sel.image && sel.text && photo && !hexFromPhoto && !looksLikeImageUrl(photo) ? photo : "") || - (!sel.image && sel.text && !sel.color ? (category.categoryName ?? "").trim() : ""), - ), - ); - setButtonBgColor(bgField || (sel.color && hexFromPhoto ? hexFromPhoto : "") || "#3B82F6"); - setForm({ - categoryName: category.categoryName ?? "", - categoryPhotoUrl: sel.image ? category.categoryPhotoUrl ?? null : null, - state: category.state ?? true, - orderNum: category.orderNum ?? null, - }); - hydrateAvailability(); - }, [open, category, locations, partners, groups]); + return () => ac.abort(); + }, [open, category, locations, partners, groups, requireCompanySelection]); const submit = async () => { if (!category?.id) return; @@ -1412,17 +1440,17 @@ function EditLabelCategoryDialog({ } } - const scopePayload = buildLabelingScopeSaveBody({ + const scopePayload = buildEntityScopeSaveFromForm({ requireCompanySelection, selectedPartnerId, - fixedPartnerId, selectedPartnerIds, selectedRegionNames, selectedRegionIds, selectedLocationIds, + fixedPartnerId, + locations, partners, groups, - locations, }); if (!scopePayload.ok) { toast.error("Validation failed", { description: scopePayload.message }); @@ -1456,13 +1484,7 @@ function EditLabelCategoryDialog({ buttonImageUrl: apSel.image ? String(form.categoryPhotoUrl ?? "").trim() || null : null, buttonAppearance: tokens, buttonStyleJson, - availabilityType: scopePayload.body.availabilityType, - appliedPartnerType: scopePayload.body.appliedPartnerType, - partnerIds: scopePayload.body.partnerIds, - companyIds: scopePayload.body.companyIds, - regionIds: scopePayload.body.regionIds, - groupIds: scopePayload.body.groupIds, - locationIds: scopePayload.body.locationIds, + ...scopePayload.body, }); toast.success("Label category updated.", { description: "The label category has been updated successfully.", @@ -1500,24 +1522,28 @@ function EditLabelCategoryDialog({ />
- + {loadingDetail ? ( +

Loading company, region and location…

+ ) : ( + + )}
@@ -1689,7 +1715,7 @@ function EditLabelCategoryDialog({
@@ -1023,23 +1023,24 @@ function EditLabelTypeDialog({ orderNum: detail.orderNum ?? type.orderNum ?? null, }); + if (requireCompanySelection) { + const scope = hydrateEntityScopeFromDto(detail, locations, partners, groups); + setSelectedPartnerIds(scope.partnerIds); + setSelectedRegionIds(scope.regionIds); + setSelectedLocationIds(scope.locationIds); + setSelectedPartnerId(""); + setSelectedRegionNames([]); + return; + } + const lids = (detail.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean); if (lids.length > 0) { - if (requireCompanySelection) { - const scope = hydrateLabelingScopeFromLocationIds(lids, locations, partners, groups); - setSelectedPartnerIds(scope.partnerIds); - setSelectedRegionIds(scope.regionIds); - setSelectedLocationIds(scope.locationIds); - setSelectedPartnerId(""); - setSelectedRegionNames([]); - } else { - const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups); - setSelectedPartnerId(scope.partnerId); - setSelectedRegionNames(scope.regionNames); - setSelectedLocationIds(scope.locationIds); - setSelectedPartnerIds([]); - setSelectedRegionIds([]); - } + const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups); + setSelectedPartnerId(scope.partnerId); + setSelectedRegionNames(scope.regionNames); + setSelectedLocationIds(scope.locationIds); + setSelectedPartnerIds([]); + setSelectedRegionIds([]); return; } @@ -1048,26 +1049,15 @@ function EditLabelTypeDialog({ .filter(Boolean); if (gids.length > 0) { const matchedGroups = groups.filter((g) => gids.includes(g.id)); - if (requireCompanySelection) { - const partnerIds = [ - ...new Set(matchedGroups.map((g) => String(g.partnerId ?? "").trim()).filter(Boolean)), - ]; - setSelectedPartnerIds(partnerIds); - setSelectedRegionIds(gids); - setSelectedLocationIds([]); - setSelectedPartnerId(""); - setSelectedRegionNames([]); - } else { - const pid = matchedGroups[0]?.partnerId ?? ""; - const names = [ - ...new Set(matchedGroups.map((g) => (g.groupName ?? "").trim()).filter(Boolean)), - ]; - setSelectedPartnerId(pid); - setSelectedRegionNames(names); - setSelectedLocationIds([]); - setSelectedPartnerIds([]); - setSelectedRegionIds([]); - } + const pid = matchedGroups[0]?.partnerId ?? ""; + const names = [ + ...new Set(matchedGroups.map((g) => (g.groupName ?? "").trim()).filter(Boolean)), + ]; + setSelectedPartnerId(pid); + setSelectedRegionNames(names); + setSelectedLocationIds([]); + setSelectedPartnerIds([]); + setSelectedRegionIds([]); return; } @@ -1096,6 +1086,8 @@ function EditLabelTypeDialog({ setSelectedRegionIds([]); setSelectedLocationIds([]); } + setSelectedPartnerIds([]); + setSelectedRegionIds([]); } catch { if (ac.signal.aborted) return; setForm({ @@ -1104,7 +1096,9 @@ function EditLabelTypeDialog({ orderNum: type.orderNum ?? null, }); setSelectedPartnerId(""); + setSelectedPartnerIds([]); setSelectedRegionNames([]); + setSelectedRegionIds([]); setSelectedLocationIds([]); } finally { if (!ac.signal.aborted) setLoadingDetail(false); @@ -1127,17 +1121,17 @@ function EditLabelTypeDialog({ return; } - const scopePayload = buildLabelingScopeSaveBody({ + const scopePayload = buildEntityScopeSaveFromForm({ requireCompanySelection, selectedPartnerId, - fixedPartnerId, selectedPartnerIds, selectedRegionNames, selectedRegionIds, selectedLocationIds, + fixedPartnerId, + locations, partners, groups, - locations, }); if (!scopePayload.ok) { toast.error("Validation failed", { description: scopePayload.message }); @@ -1149,13 +1143,7 @@ function EditLabelTypeDialog({ await updateLabelType(type.id, { ...form, typeCode: type.typeCode ?? "", - availabilityType: scopePayload.body.availabilityType, - appliedPartnerType: scopePayload.body.appliedPartnerType, - partnerIds: scopePayload.body.partnerIds, - companyIds: scopePayload.body.companyIds, - groupIds: scopePayload.body.groupIds, - regionIds: scopePayload.body.regionIds, - locationIds: scopePayload.body.locationIds, + ...scopePayload.body, }); toast.success("Label type updated.", { description: "The label type has been updated successfully.", @@ -1227,6 +1215,10 @@ function EditLabelTypeDialog({ requireCompanySelection={requireCompanySelection} fixedPartnerId={fixedPartnerId} templateScopeMode={requireCompanySelection} + selectedPartnerIds={selectedPartnerIds} + onPartnerIdsChange={setSelectedPartnerIds} + selectedRegionIds={selectedRegionIds} + onRegionIdsChange={setSelectedRegionIds} /> )} diff --git a/美国版/Food Labeling Management Platform/src/components/labels/MultipleOptionsView.tsx b/美国版/Food Labeling Management Platform/src/components/labels/MultipleOptionsView.tsx index 5cfb498..7b2765d 100755 --- a/美国版/Food Labeling Management Platform/src/components/labels/MultipleOptionsView.tsx +++ b/美国版/Food Labeling Management Platform/src/components/labels/MultipleOptionsView.tsx @@ -51,9 +51,9 @@ import { getLocations } from "../../services/locationService"; import { getGroups } from "../../services/groupService"; import { getPartners } from "../../services/partnerService"; import { - buildLabelingScopeSaveBody, + buildEntityScopeSaveFromForm, hydrateCategoryScopeFromLocationIds, - hydrateLabelingScopeFromLocationIds, + hydrateEntityScopeFromDto, } from "../../lib/categoryScopeForm"; import { CategoryScopeFields } from "../shared/category-scope-fields"; import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth"; @@ -818,7 +818,9 @@ function CreateMultipleOptionDialog({ const resetForm = () => { setSelectedPartnerId(""); + setSelectedPartnerIds([]); setSelectedRegionNames([]); + setSelectedRegionIds([]); setSelectedLocationIds([]); setForm({ optionName: "", @@ -876,17 +878,17 @@ function CreateMultipleOptionDialog({ return; } - const scopePayload = buildLabelingScopeSaveBody({ + const scopePayload = buildEntityScopeSaveFromForm({ requireCompanySelection, selectedPartnerId, - fixedPartnerId, selectedPartnerIds, selectedRegionNames, selectedRegionIds, selectedLocationIds, + fixedPartnerId, + locations, partners, groups, - locations, }); if (!scopePayload.ok) { toast.error("Validation failed", { description: scopePayload.message }); @@ -898,13 +900,7 @@ function CreateMultipleOptionDialog({ await createLabelMultipleOption({ ...form, optionCode: optionCodeFromName(form.optionName), - availabilityType: scopePayload.body.availabilityType, - appliedPartnerType: scopePayload.body.appliedPartnerType, - partnerIds: scopePayload.body.partnerIds, - companyIds: scopePayload.body.companyIds, - groupIds: scopePayload.body.groupIds, - regionIds: scopePayload.body.regionIds, - locationIds: scopePayload.body.locationIds, + ...scopePayload.body, }); toast.success("Multiple option created.", { description: "The multiple option has been created successfully.", @@ -1009,6 +1005,10 @@ function CreateMultipleOptionDialog({ requireCompanySelection={requireCompanySelection} fixedPartnerId={fixedPartnerId} templateScopeMode={requireCompanySelection} + selectedPartnerIds={selectedPartnerIds} + onPartnerIdsChange={setSelectedPartnerIds} + selectedRegionIds={selectedRegionIds} + onRegionIdsChange={setSelectedRegionIds} /> @@ -1080,23 +1080,24 @@ function EditMultipleOptionDialog({ }); setNewValue(""); + if (requireCompanySelection) { + const scope = hydrateEntityScopeFromDto(detail, locations, partners, groups); + setSelectedPartnerIds(scope.partnerIds); + setSelectedRegionIds(scope.regionIds); + setSelectedLocationIds(scope.locationIds); + setSelectedPartnerId(""); + setSelectedRegionNames([]); + return; + } + const lids = (detail.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean); if (lids.length > 0) { - if (requireCompanySelection) { - const scope = hydrateLabelingScopeFromLocationIds(lids, locations, partners, groups); - setSelectedPartnerIds(scope.partnerIds); - setSelectedRegionIds(scope.regionIds); - setSelectedLocationIds(scope.locationIds); - setSelectedPartnerId(""); - setSelectedRegionNames([]); - } else { - const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups); - setSelectedPartnerId(scope.partnerId); - setSelectedRegionNames(scope.regionNames); - setSelectedLocationIds(scope.locationIds); - setSelectedPartnerIds([]); - setSelectedRegionIds([]); - } + const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups); + setSelectedPartnerId(scope.partnerId); + setSelectedRegionNames(scope.regionNames); + setSelectedLocationIds(scope.locationIds); + setSelectedPartnerIds([]); + setSelectedRegionIds([]); return; } @@ -1105,26 +1106,15 @@ function EditMultipleOptionDialog({ .filter(Boolean); if (gids.length > 0) { const matchedGroups = groups.filter((g) => gids.includes(g.id)); - if (requireCompanySelection) { - const partnerIds = [ - ...new Set(matchedGroups.map((g) => String(g.partnerId ?? "").trim()).filter(Boolean)), - ]; - setSelectedPartnerIds(partnerIds); - setSelectedRegionIds(gids); - setSelectedLocationIds([]); - setSelectedPartnerId(""); - setSelectedRegionNames([]); - } else { - const pid = matchedGroups[0]?.partnerId ?? ""; - const names = [ - ...new Set(matchedGroups.map((g) => (g.groupName ?? "").trim()).filter(Boolean)), - ]; - setSelectedPartnerId(pid); - setSelectedRegionNames(names); - setSelectedLocationIds([]); - setSelectedPartnerIds([]); - setSelectedRegionIds([]); - } + const pid = matchedGroups[0]?.partnerId ?? ""; + const names = [ + ...new Set(matchedGroups.map((g) => (g.groupName ?? "").trim()).filter(Boolean)), + ]; + setSelectedPartnerId(pid); + setSelectedRegionNames(names); + setSelectedLocationIds([]); + setSelectedPartnerIds([]); + setSelectedRegionIds([]); return; } @@ -1153,6 +1143,8 @@ function EditMultipleOptionDialog({ setSelectedRegionIds([]); setSelectedLocationIds([]); } + setSelectedPartnerIds([]); + setSelectedRegionIds([]); } catch { if (ac.signal.aborted) return; setForm({ @@ -1162,7 +1154,9 @@ function EditMultipleOptionDialog({ orderNum: option.orderNum ?? null, }); setSelectedPartnerId(""); + setSelectedPartnerIds([]); setSelectedRegionNames([]); + setSelectedRegionIds([]); setSelectedLocationIds([]); setNewValue(""); } finally { @@ -1215,17 +1209,17 @@ function EditMultipleOptionDialog({ return; } - const scopePayload = buildLabelingScopeSaveBody({ + const scopePayload = buildEntityScopeSaveFromForm({ requireCompanySelection, selectedPartnerId, - fixedPartnerId, selectedPartnerIds, selectedRegionNames, selectedRegionIds, selectedLocationIds, + fixedPartnerId, + locations, partners, groups, - locations, }); if (!scopePayload.ok) { toast.error("Validation failed", { description: scopePayload.message }); @@ -1237,13 +1231,7 @@ function EditMultipleOptionDialog({ await updateLabelMultipleOption(option.id, { ...form, optionCode: option.optionCode ?? "", - availabilityType: scopePayload.body.availabilityType, - appliedPartnerType: scopePayload.body.appliedPartnerType, - partnerIds: scopePayload.body.partnerIds, - companyIds: scopePayload.body.companyIds, - groupIds: scopePayload.body.groupIds, - regionIds: scopePayload.body.regionIds, - locationIds: scopePayload.body.locationIds, + ...scopePayload.body, }); toast.success("Multiple option updated.", { description: "The multiple option has been updated successfully.", @@ -1351,6 +1339,10 @@ function EditMultipleOptionDialog({ requireCompanySelection={requireCompanySelection} fixedPartnerId={fixedPartnerId} templateScopeMode={requireCompanySelection} + selectedPartnerIds={selectedPartnerIds} + onPartnerIdsChange={setSelectedPartnerIds} + selectedRegionIds={selectedRegionIds} + onRegionIdsChange={setSelectedRegionIds} /> )} diff --git a/美国版/Food Labeling Management Platform/src/components/shared/category-scope-fields.tsx b/美国版/Food Labeling Management Platform/src/components/shared/category-scope-fields.tsx index 40f19ee..8bf3835 100644 --- a/美国版/Food Labeling Management Platform/src/components/shared/category-scope-fields.tsx +++ b/美国版/Food Labeling Management Platform/src/components/shared/category-scope-fields.tsx @@ -411,7 +411,7 @@ export function CategoryScopeFields({ )}

{templateScopeMode - ? "Required. Select one or more companies, or Select All." + ? "Required. Leave empty or select all for every company; narrows region and location lists." : "Required. Select company before region and location."}

@@ -427,10 +427,18 @@ export function CategoryScopeFields({ placeholder={regionPlaceholder} searchPlaceholder="Search regions…" selectAllRowLabel="Select All" - disabled={!companyReady} + disabled={!templateScopeMode && !companyReady} />

- {companyReady ? "Required. Narrows the location list." : requireCompanySelection ? "Select a company first." : "Your account company is loading or not assigned."} + {templateScopeMode + ? companyReady + ? "Required. Leave empty or select all for every region in selected companies." + : "Select company first." + : companyReady + ? "Required. Narrows the location list." + : requireCompanySelection + ? "Select a company first." + : "Your account company is loading or not assigned."}

@@ -440,22 +448,34 @@ export function CategoryScopeFields({ onValuesChange={onLocationChange} options={locationOptionsForPicker} placeholder={ - companyReady - ? "Select location(s)…" - : requireCompanySelection - ? "Select company first" - : "Loading company scope…" + templateScopeMode + ? companyReady + ? "All Locations" + : requireCompanySelection + ? "Select company first" + : "Loading company scope…" + : companyReady + ? "Select location(s)…" + : requireCompanySelection + ? "Select company first" + : "Loading company scope…" } searchPlaceholder="Search locations…" selectAllRowLabel="ALL" - disabled={!companyReady} + disabled={!templateScopeMode && !companyReady} />

- {companyReady - ? "Required. ALL selects every location in the filtered list (within selected regions)." - : requireCompanySelection - ? "Select a company first." - : "Your account company is loading or not assigned."} + {templateScopeMode + ? companyReady + ? "Required. ALL selects every location in the filtered list (within selected regions)." + : requireCompanySelection + ? "Select a company first." + : "Your account company is loading or not assigned." + : companyReady + ? "Required. ALL selects every location in the filtered list (within selected regions)." + : requireCompanySelection + ? "Select a company first." + : "Your account company is loading or not assigned."}

diff --git a/美国版/Food Labeling Management Platform/src/lib/categoryScopeForm.ts b/美国版/Food Labeling Management Platform/src/lib/categoryScopeForm.ts index 20fa7fe..d2e41f8 100644 --- a/美国版/Food Labeling Management Platform/src/lib/categoryScopeForm.ts +++ b/美国版/Food Labeling Management Platform/src/lib/categoryScopeForm.ts @@ -1,11 +1,8 @@ import type { GroupListItem } from "../types/group"; import type { LocationDto } from "../types/location"; import type { PartnerListItem } from "../types/partner"; -import { - buildLabelTemplateScopePayload, - type AppliedScopeType, - parseAppliedScopeType, -} from "../types/labelTemplate"; +import type { AppliedScopeType } from "../types/labelTemplate"; +import { parseAppliedScopeType } from "../types/labelTemplate"; /** 表单提交用的公司 Id(非管理员取账号绑定公司) */ export function effectiveScopePartnerId( @@ -616,3 +613,223 @@ export function buildProductScopeSavePayload( } return { locationIds: locIds }; } + +function resolveScopeDimensionForSave( + selected: string[], + available: string[], +): { type: AppliedScopeType; ids: string[] } { + const avail = dedupeIds(available); + const sel = dedupeIds(selected).filter((id) => !avail.length || avail.includes(id)); + if (!avail.length || !sel.length) { + return { type: "ALL", ids: [] }; + } + if (sel.length >= avail.length && avail.every((id) => sel.includes(id))) { + return { type: "ALL", ids: [] }; + } + return { type: "SPECIFIED", ids: sel }; +} + +/** 模板/实体表单:按当前选中 Id 计算可选 Company / Region / Location 全集 */ +export function computeTemplateScopeAvailableIds( + partners: PartnerListItem[], + groups: GroupListItem[], + locations: LocationDto[], + selectedPartnerIds: string[], + selectedRegionIds: string[], +): { + availablePartnerIds: string[]; + availableRegionIds: string[]; + availableLocationIds: string[]; +} { + const availablePartnerIds = partners.map((p) => String(p.id ?? "").trim()).filter(Boolean); + const effectivePartnerIds = + selectedPartnerIds.length > 0 ? selectedPartnerIds : availablePartnerIds; + const availableRegionIds = regionOptionsForPartners(groups, effectivePartnerIds).map( + (o) => o.value, + ); + const availableLocationIds = locationsScopedForTemplateScope( + locations, + partners, + groups, + effectivePartnerIds, + selectedRegionIds, + ).map((l) => l.id); + return { availablePartnerIds, availableRegionIds, availableLocationIds }; +} + +export type EntityScopePayloadBody = { + appliedPartnerType: AppliedScopeType; + partnerIds: string[]; + companyIds: string[]; + regionIds: string[]; + groupIds: string[]; + locationIds: string[]; + availabilityType: "ALL" | "SPECIFIED"; +}; + +/** + * 标签类型 / 分类 / 多选项保存:Company + Region + Location 三维适用范围。 + * 空选或全选当前可选项 → 该维度 ALL;部分选中 → SPECIFIED + Id 数组。 + */ +export function buildEntityPartnerScopePayload(input: { + selectedPartnerIds: string[]; + selectedRegionIds: string[]; + selectedLocationIds: string[]; + availablePartnerIds: string[]; + availableRegionIds: string[]; + availableLocationIds: string[]; +}): { ok: true; body: EntityScopePayloadBody } | { ok: false; message: string } { + const partnerDim = resolveScopeDimensionForSave( + input.selectedPartnerIds, + input.availablePartnerIds, + ); + const regionDim = resolveScopeDimensionForSave( + input.selectedRegionIds, + input.availableRegionIds, + ); + const locationDim = resolveScopeDimensionForSave( + input.selectedLocationIds, + input.availableLocationIds, + ); + + const locationSpecified = + regionDim.type === "SPECIFIED" || locationDim.type === "SPECIFIED"; + const availabilityType: "ALL" | "SPECIFIED" = locationSpecified ? "SPECIFIED" : "ALL"; + + const anySpecified = + partnerDim.type === "SPECIFIED" || locationSpecified; + const effectiveLocationIds = + locationDim.type === "SPECIFIED" ? locationDim.ids : input.availableLocationIds; + + if (anySpecified && effectiveLocationIds.length === 0) { + return { + ok: false, + message: "Please adjust company, region, or location so at least one store matches.", + }; + } + + const partnerIds = partnerDim.type === "SPECIFIED" ? partnerDim.ids : []; + const regionIds = regionDim.type === "SPECIFIED" ? regionDim.ids : []; + const locationIds = locationDim.type === "SPECIFIED" ? locationDim.ids : []; + + return { + ok: true, + body: { + appliedPartnerType: partnerDim.type, + partnerIds, + companyIds: partnerIds, + regionIds, + groupIds: regionIds, + locationIds, + availabilityType, + }, + }; +} + +/** 创建/编辑弹窗:按管理员多选或单公司模式构建保存 Body */ +export function buildEntityScopeSaveFromForm(input: { + requireCompanySelection: boolean; + selectedPartnerId: string; + selectedPartnerIds: string[]; + selectedRegionNames: string[]; + selectedRegionIds: string[]; + selectedLocationIds: string[]; + fixedPartnerId: string; + locations: LocationDto[]; + partners: PartnerListItem[]; + groups: GroupListItem[]; +}): { ok: true; body: EntityScopePayloadBody } | { ok: false; message: string } { + if (input.requireCompanySelection) { + const { availablePartnerIds, availableRegionIds, availableLocationIds } = + computeTemplateScopeAvailableIds( + input.partners, + input.groups, + input.locations, + input.selectedPartnerIds, + input.selectedRegionIds, + ); + return buildEntityPartnerScopePayload({ + selectedPartnerIds: input.selectedPartnerIds, + selectedRegionIds: input.selectedRegionIds, + selectedLocationIds: input.selectedLocationIds, + availablePartnerIds, + availableRegionIds, + availableLocationIds, + }); + } + + const scopePartnerId = effectiveScopePartnerId( + false, + input.selectedPartnerId, + input.fixedPartnerId, + ); + const partnerErr = scopePartnerValidationMessage(false, scopePartnerId); + if (partnerErr) { + return { ok: false, message: partnerErr }; + } + + const locPayload = buildSpecifiedLocationPayload( + input.locations, + input.partners, + input.groups, + scopePartnerId, + input.selectedRegionNames, + input.selectedLocationIds, + ); + if (!locPayload.ok) { + return { ok: false, message: locPayload.message }; + } + + const groupIds = regionNamesToGroupIds( + input.selectedRegionNames, + input.groups, + scopePartnerId, + ); + const partnerIds = scopePartnerId ? [scopePartnerId] : []; + return { + ok: true, + body: { + appliedPartnerType: partnerIds.length ? "SPECIFIED" : "ALL", + partnerIds, + companyIds: partnerIds, + regionIds: groupIds, + groupIds, + locationIds: locPayload.locationIds, + availabilityType: "SPECIFIED", + }, + }; +} + +/** 编辑弹窗:从详情 DTO 回填三维 Id 数组(管理员多选模式) */ +export function hydrateEntityScopeFromDto( + dto: { + appliedPartnerType?: string | null; + appliedRegionType?: string | null; + appliedLocation?: string | null; + appliedLocationType?: string | null; + availabilityType?: string | null; + partnerIds?: string[] | null; + companyIds?: string[] | null; + regionIds?: string[] | null; + groupIds?: string[] | null; + locationIds?: string[] | null; + appliedLocationIds?: string[] | null; + }, + locations: LocationDto[], + partners: PartnerListItem[], + groups: GroupListItem[], +): { partnerIds: string[]; regionIds: string[]; locationIds: string[] } { + return hydrateLabelTemplateScopeFromDto( + { + ...dto, + appliedLocation: + dto.appliedLocation ?? + (String(dto.availabilityType ?? "").trim().toUpperCase() === "SPECIFIED" + ? "SPECIFIED" + : dto.appliedLocation), + }, + locations, + partners, + groups, + ); +} diff --git a/美国版/Food Labeling Management Platform/src/services/labelCategoryService.ts b/美国版/Food Labeling Management Platform/src/services/labelCategoryService.ts index 6b18ab5..6c6dbfd 100644 --- a/美国版/Food Labeling Management Platform/src/services/labelCategoryService.ts +++ b/美国版/Food Labeling Management Platform/src/services/labelCategoryService.ts @@ -22,6 +22,13 @@ const PATH = "/label-category"; function normalizeLabelCategoryDto(raw: unknown): LabelCategoryDto { const r = (raw && typeof raw === "object" ? raw : {}) as Record; + const parseIds = (v: unknown): string[] | undefined => { + if (!Array.isArray(v)) return undefined; + return [...new Set(v.map((x) => String(x).trim()).filter(Boolean))]; + }; + const groupIds = parseIds(r.groupIds ?? r.GroupIds ?? r.regionIds ?? r.RegionIds); + const locationIds = parseIds(r.locationIds ?? r.LocationIds); + const partnerIds = parseIds(r.partnerIds ?? r.PartnerIds ?? r.companyIds ?? r.CompanyIds); const noRaw = r.noOfLabels ?? r.NoOfLabels; const noOfLabels = typeof noRaw === "number" && Number.isFinite(noRaw) @@ -36,7 +43,60 @@ function normalizeLabelCategoryDto(raw: unknown): LabelCategoryDto { : typeof orderRaw === "string" && orderRaw.trim() !== "" && Number.isFinite(Number(orderRaw)) ? Number(orderRaw) : null; - return { ...(r as object), id: String(r.id ?? r.Id ?? ""), noOfLabels, orderNum } as LabelCategoryDto; + const companyRaw = r.company ?? r.Company; + return { + ...(r as object), + id: String(r.id ?? r.Id ?? ""), + appliedPartnerType: String(r.appliedPartnerType ?? r.AppliedPartnerType ?? "").trim() || undefined, + partnerIds, + companyIds: partnerIds, + availabilityType: String(r.availabilityType ?? r.AvailabilityType ?? "").trim() || undefined, + groupIds, + regionIds: groupIds, + locationIds, + company: typeof companyRaw === "string" ? companyRaw : null, + noOfLabels, + orderNum, + } as LabelCategoryDto; +} + +function scopeBodyFromInput(input: LabelCategoryCreateInput): Record { + const groupIds = [ + ...new Set( + [...(input.groupIds ?? []), ...(input.regionIds ?? [])] + .map((x) => String(x).trim()) + .filter(Boolean), + ), + ]; + const locationIds = [ + ...new Set((input.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean)), + ]; + const partnerIds = [ + ...new Set( + [...(input.partnerIds ?? []), ...(input.companyIds ?? [])] + .map((x) => String(x).trim()) + .filter(Boolean), + ), + ]; + const body: Record = {}; + const appliedPartnerType = String(input.appliedPartnerType ?? "").trim().toUpperCase(); + if (appliedPartnerType === "ALL" || appliedPartnerType === "SPECIFIED") { + body.appliedPartnerType = appliedPartnerType; + } + if (partnerIds.length) { + body.partnerIds = partnerIds; + body.companyIds = partnerIds; + } + const availabilityType = String(input.availabilityType ?? "ALL").trim().toUpperCase() || "ALL"; + body.availabilityType = availabilityType; + if (groupIds.length) { + body.groupIds = groupIds; + body.regionIds = groupIds; + } + if (locationIds.length) { + body.locationIds = locationIds; + } + return body; } export async function getLabelCategories(input: LabelCategoryGetListInput, signal?: AbortSignal): Promise> { @@ -60,11 +120,12 @@ export async function getLabelCategories(input: LabelCategoryGetListInput, signa } export async function getLabelCategory(id: string, signal?: AbortSignal): Promise { - return api.requestJson({ + const raw = await api.requestJson({ path: `${PATH}/${encodeURIComponent(id)}`, method: "GET", signal, }); + return normalizeLabelCategoryDto(raw); } export async function createLabelCategory(input: LabelCategoryCreateInput): Promise { @@ -82,17 +143,13 @@ export async function createLabelCategory(input: LabelCategoryCreateInput): Prom buttonStyleJson: (input.buttonStyleJson ?? "").trim() || null, state: input.state ?? true, orderNum: input.orderNum, - availabilityType: String(input.availabilityType ?? "ALL").trim().toUpperCase() || "ALL", - locationIds: - Array.isArray(input.locationIds) && input.locationIds.length - ? [...new Set(input.locationIds.map((x) => String(x).trim()).filter(Boolean))] - : null, + ...scopeBodyFromInput(input), }, }); } export async function updateLabelCategory(id: string, input: LabelCategoryUpdateInput): Promise { - return api.requestJson({ + const raw = await api.requestJson({ path: `${PATH}/${encodeURIComponent(id)}`, method: "PUT", body: { @@ -106,13 +163,10 @@ export async function updateLabelCategory(id: string, input: LabelCategoryUpdate buttonStyleJson: (input.buttonStyleJson ?? "").trim() || null, state: input.state ?? true, orderNum: input.orderNum, - availabilityType: String(input.availabilityType ?? "ALL").trim().toUpperCase() || "ALL", - locationIds: - Array.isArray(input.locationIds) && input.locationIds.length - ? [...new Set(input.locationIds.map((x) => String(x).trim()).filter(Boolean))] - : null, + ...scopeBodyFromInput(input), }, }); + return normalizeLabelCategoryDto(raw); } export async function deleteLabelCategory(id: string): Promise { diff --git a/美国版/Food Labeling Management Platform/src/services/labelMultipleOptionService.ts b/美国版/Food Labeling Management Platform/src/services/labelMultipleOptionService.ts index 88d3a70..15da9ad 100644 --- a/美国版/Food Labeling Management Platform/src/services/labelMultipleOptionService.ts +++ b/美国版/Food Labeling Management Platform/src/services/labelMultipleOptionService.ts @@ -63,13 +63,35 @@ function normalizeLabelMultipleOptionDto(item: LabelMultipleOptionDto): LabelMul const locationIds = parseIds( (raw as Record).locationIds ?? (raw as Record).LocationIds, ); + const partnerIds = parseIds( + (raw as Record).partnerIds ?? + (raw as Record).PartnerIds ?? + (raw as Record).companyIds ?? + (raw as Record).CompanyIds, + ); + const companyRaw = (raw as Record).company ?? (raw as Record).Company; return { ...item, optionValuesJson: parseOptionValuesJsonField(raw.optionValuesJson), lastEdited, + appliedPartnerType: + String( + (raw as Record).appliedPartnerType ?? + (raw as Record).AppliedPartnerType ?? + "", + ).trim() || undefined, + partnerIds, + companyIds: partnerIds, + availabilityType: + String( + (raw as Record).availabilityType ?? + (raw as Record).AvailabilityType ?? + "", + ).trim() || undefined, groupIds, regionIds: groupIds, locationIds, + company: typeof companyRaw === "string" ? companyRaw : null, }; } @@ -96,11 +118,34 @@ function scopeBodyFromInput(input: LabelMultipleOptionCreateInput): Record String(x).trim()).filter(Boolean)), ]; - return { - groupIds: groupIds.length ? groupIds : undefined, - regionIds: groupIds.length ? groupIds : undefined, - locationIds: locationIds.length ? locationIds : undefined, - }; + const partnerIds = [ + ...new Set( + [...(input.partnerIds ?? []), ...(input.companyIds ?? [])] + .map((x) => String(x).trim()) + .filter(Boolean), + ), + ]; + const body: Record = {}; + const appliedPartnerType = String(input.appliedPartnerType ?? "").trim().toUpperCase(); + if (appliedPartnerType === "ALL" || appliedPartnerType === "SPECIFIED") { + body.appliedPartnerType = appliedPartnerType; + } + if (partnerIds.length) { + body.partnerIds = partnerIds; + body.companyIds = partnerIds; + } + const availabilityType = String(input.availabilityType ?? "").trim().toUpperCase(); + if (availabilityType === "ALL" || availabilityType === "SPECIFIED") { + body.availabilityType = availabilityType; + } + if (groupIds.length) { + body.groupIds = groupIds; + body.regionIds = groupIds; + } + if (locationIds.length) { + body.locationIds = locationIds; + } + return body; } export async function getLabelMultipleOptions(input: LabelMultipleOptionGetListInput, signal?: AbortSignal): Promise> { diff --git a/美国版/Food Labeling Management Platform/src/services/labelTypeService.ts b/美国版/Food Labeling Management Platform/src/services/labelTypeService.ts index 7566872..75b1682 100644 --- a/美国版/Food Labeling Management Platform/src/services/labelTypeService.ts +++ b/美国版/Food Labeling Management Platform/src/services/labelTypeService.ts @@ -27,6 +27,10 @@ function normalizeLabelTypeDto(raw: unknown): LabelTypeDto { }; const groupIds = parseIds(r.groupIds ?? r.GroupIds ?? r.regionIds ?? r.RegionIds); const locationIds = parseIds(r.locationIds ?? r.LocationIds); + const partnerIds = parseIds(r.partnerIds ?? r.PartnerIds ?? r.companyIds ?? r.CompanyIds); + const appliedPartnerType = String(r.appliedPartnerType ?? r.AppliedPartnerType ?? "").trim() || undefined; + const availabilityType = String(r.availabilityType ?? r.AvailabilityType ?? "").trim() || undefined; + const companyRaw = r.company ?? r.Company; const noRaw = r.noOfLabels ?? r.NoOfLabels; const noOfLabels = typeof noRaw === "number" && Number.isFinite(noRaw) @@ -46,9 +50,14 @@ function normalizeLabelTypeDto(raw: unknown): LabelTypeDto { return { ...(r as object), id: String(r.id ?? r.Id ?? ""), + appliedPartnerType, + partnerIds, + companyIds: partnerIds, + availabilityType, groupIds, regionIds: groupIds, locationIds, + company: typeof companyRaw === "string" ? companyRaw : null, noOfLabels, lastEdited, region: typeof regionRaw === "string" ? regionRaw : null, @@ -67,11 +76,34 @@ function scopeBodyFromInput(input: LabelTypeCreateInput): Record String(x).trim()).filter(Boolean)), ]; - return { - groupIds: groupIds.length ? groupIds : undefined, - regionIds: groupIds.length ? groupIds : undefined, - locationIds: locationIds.length ? locationIds : undefined, - }; + const partnerIds = [ + ...new Set( + [...(input.partnerIds ?? []), ...(input.companyIds ?? [])] + .map((x) => String(x).trim()) + .filter(Boolean), + ), + ]; + const body: Record = {}; + const appliedPartnerType = String(input.appliedPartnerType ?? "").trim().toUpperCase(); + if (appliedPartnerType === "ALL" || appliedPartnerType === "SPECIFIED") { + body.appliedPartnerType = appliedPartnerType; + } + if (partnerIds.length) { + body.partnerIds = partnerIds; + body.companyIds = partnerIds; + } + const availabilityType = String(input.availabilityType ?? "").trim().toUpperCase(); + if (availabilityType === "ALL" || availabilityType === "SPECIFIED") { + body.availabilityType = availabilityType; + } + if (groupIds.length) { + body.groupIds = groupIds; + body.regionIds = groupIds; + } + if (locationIds.length) { + body.locationIds = locationIds; + } + return body; } export async function getLabelTypes(input: LabelTypeGetListInput, signal?: AbortSignal): Promise> { diff --git a/美国版/Food Labeling Management Platform/src/types/label.ts b/美国版/Food Labeling Management Platform/src/types/label.ts index 213207a..ee64a7f 100644 --- a/美国版/Food Labeling Management Platform/src/types/label.ts +++ b/美国版/Food Labeling Management Platform/src/types/label.ts @@ -81,7 +81,7 @@ export type LabelUpdateInput = { /** 适用门店(至少 1 个) */ locationIds: string[]; labelCategoryId: string; - labelTypeId: string; + labelTypeId?: string | null; productIds: string[]; // 至少 1 个 labelInfoJson?: Record | null; state?: boolean; diff --git a/美国版/Food Labeling Management Platform/src/types/labelCategory.ts b/美国版/Food Labeling Management Platform/src/types/labelCategory.ts index ff8f637..60d57a3 100644 --- a/美国版/Food Labeling Management Platform/src/types/labelCategory.ts +++ b/美国版/Food Labeling Management Platform/src/types/labelCategory.ts @@ -18,10 +18,19 @@ export type LabelCategoryDto = { /** 该分类下标签数量(列表列 No. of Label) */ noOfLabels?: number | null; creationTime?: string | null; + /** 适用 Company:ALL / SPECIFIED */ + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; + partnerIds?: string[] | null; + companyIds?: string[] | null; /** ALL / SPECIFIED */ availabilityType?: "ALL" | "SPECIFIED" | string | null; + /** SPECIFIED 时绑定的 Region Id */ + regionIds?: string[] | null; + groupIds?: string[] | null; /** SPECIFIED 时绑定的门店 Id */ locationIds?: string[] | null; + /** 列表:Company 展示 */ + company?: string | null; /** 最近编辑时间(列表接口映射 LastModificationTime ?? CreationTime) */ lastEdited?: string | null; }; @@ -56,10 +65,11 @@ export type LabelCategoryCreateInput = { buttonStyleJson?: string | null; state?: boolean; orderNum?: number | null; - availabilityType?: "ALL" | "SPECIFIED" | string | null; + /** 适用 Company:ALL / SPECIFIED */ appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; partnerIds?: string[] | null; companyIds?: string[] | null; + availabilityType?: "ALL" | "SPECIFIED" | string | null; regionIds?: string[] | null; groupIds?: string[] | null; locationIds?: string[] | null; diff --git a/美国版/Food Labeling Management Platform/src/types/labelMultipleOption.ts b/美国版/Food Labeling Management Platform/src/types/labelMultipleOption.ts index f34234b..927028d 100644 --- a/美国版/Food Labeling Management Platform/src/types/labelMultipleOption.ts +++ b/美国版/Food Labeling Management Platform/src/types/labelMultipleOption.ts @@ -7,6 +7,12 @@ export type LabelMultipleOptionDto = { state?: boolean | null; orderNum?: number | null; creationTime?: string | null; + /** 适用 Company:ALL / SPECIFIED */ + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; + partnerIds?: string[] | null; + companyIds?: string[] | null; + /** 门店可用范围:ALL / SPECIFIED */ + availabilityType?: "ALL" | "SPECIFIED" | string | null; /** 列表:最近编辑时间 */ lastEdited?: string | null; /** 适用范围:Region Id(`fl_group.Id`) */ @@ -14,6 +20,8 @@ export type LabelMultipleOptionDto = { regionIds?: string[] | null; /** 适用范围:门店 Id */ locationIds?: string[] | null; + /** 列表:Company 展示 */ + company?: string | null; }; export type PagedResultDto = { @@ -39,10 +47,12 @@ export type LabelMultipleOptionCreateInput = { optionValuesJson: string[]; state?: boolean; orderNum?: number | null; - availabilityType?: "ALL" | "SPECIFIED" | string | null; + /** 适用 Company:ALL / SPECIFIED */ appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; partnerIds?: string[] | null; companyIds?: string[] | null; + /** 门店可用范围:ALL / SPECIFIED */ + availabilityType?: "ALL" | "SPECIFIED" | string | null; groupIds?: string[] | null; regionIds?: string[] | null; locationIds?: string[] | null; diff --git a/美国版/Food Labeling Management Platform/src/types/labelType.ts b/美国版/Food Labeling Management Platform/src/types/labelType.ts index d6db61b..4dc914c 100644 --- a/美国版/Food Labeling Management Platform/src/types/labelType.ts +++ b/美国版/Food Labeling Management Platform/src/types/labelType.ts @@ -5,11 +5,19 @@ export type LabelTypeDto = { state?: boolean | null; orderNum?: number | null; creationTime?: string | null; + /** 适用 Company:ALL / SPECIFIED */ + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; + partnerIds?: string[] | null; + companyIds?: string[] | null; + /** 门店可用范围:ALL / SPECIFIED */ + availabilityType?: "ALL" | "SPECIFIED" | string | null; /** 详情:Region Id 列表(`fl_group.Id`) */ groupIds?: string[] | null; regionIds?: string[] | null; /** 详情:门店 Id 列表 */ locationIds?: string[] | null; + /** 列表:Company 展示 */ + company?: string | null; /** 列表:该类型下标签数量 */ noOfLabels?: number | null; /** 列表:最近编辑时间 */ @@ -44,10 +52,12 @@ export type LabelTypeCreateInput = { typeName: string; state?: boolean; orderNum?: number | null; - availabilityType?: "ALL" | "SPECIFIED" | string | null; + /** 适用 Company:ALL / SPECIFIED */ appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; partnerIds?: string[] | null; companyIds?: string[] | null; + /** 门店可用范围:ALL / SPECIFIED */ + availabilityType?: "ALL" | "SPECIFIED" | string | null; /** Region 多选(`fl_group.Id`) */ groupIds?: string[] | null; regionIds?: string[] | null;