diff --git a/.codex/project/project-context.md b/.codex/project/project-context.md index 2f81d24..9e19973 100644 --- a/.codex/project/project-context.md +++ b/.codex/project/project-context.md @@ -66,3 +66,15 @@ - `打印机SDK/`、`nativeplugins/`、`scripts/`、`unpackage/` 等目录 后续修改时应避免误覆盖这些现有变更。 + +## 7. 美国版业务字段命名(固定映射) + +开发与文档中统一使用下列对应关系,**不要**在 `location` 表再新增名为 `Region` 的列: + +| 界面 / 接口英文 | 中文 | 主数据表 | 门店表 `location` 字段 | +|-----------------|------|----------|------------------------| +| Company / Partner | 公司 | `fl_partner` | `Partner`(公司名称字符串) | +| **Region / Group** | **区域 / 组织** | **`fl_group`**(`GroupName`) | **`GroupName`** | +| Location | 门店 | `location` | `LocationCode` / `LocationName` | + +**硬性规则**:**Region 对应 `location.GroupName`**(与 `fl_group.GroupName` 通过 `Partner` + 名称匹配)。列表/API 出参字段名可用 `region`,落库与查询一律用 `GroupName`。 diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/Mvc/YiConventionalRouteBuilder.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/Mvc/YiConventionalRouteBuilder.cs index 8a808b3..b6678af 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/Mvc/YiConventionalRouteBuilder.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/Mvc/YiConventionalRouteBuilder.cs @@ -78,6 +78,13 @@ namespace Yi.Framework.AspNetCore.Mvc return baseUrl; } + // Guid 必须带路由约束,否则与 export-xxx、download-xxx 等字面路径冲突 + var idClrType = Nullable.GetUnderlyingType(idParameter.ParameterType) ?? idParameter.ParameterType; + if (idClrType == typeof(Guid)) + { + return $"{baseUrl}/{{id:guid}}"; + } + // 处理原始类型ID if (TypeHelper.IsPrimitiveExtended(idParameter.ParameterType, includeEnums: true)) { diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuPermissionsOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuPermissionsOutputDto.cs index 5ea17eb..a7804a6 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuPermissionsOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuPermissionsOutputDto.cs @@ -12,4 +12,19 @@ public class CurrentUserMenuPermissionsOutputDto public List PermissionCodes { get; set; } = new(); public List Menus { get; set; } = new(); + + /// + /// 用户资料最后更新时间(User.LastModificationTime,无则前端可忽略或展示「无」) + /// + public DateTime? LastUpdated { get; set; } + + /// + /// 角色展示名(多角色英文逗号拼接;与 对应的库中 RoleName) + /// + public string Role { get; set; } = string.Empty; + + /// + /// 全名:优先姓名(User.Name),其次昵称(Nick),最后用户名 + /// + public string FullName { get; set; } = string.Empty; } 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 bd1107f..0d249b6 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 @@ -8,7 +8,35 @@ public class LabelCreateInputVo public string TemplateCode { get; set; } = string.Empty; - public string LocationId { get; set; } = string.Empty; + /// + /// 适用 Company(fl_partner.Id);与 合并,用于解析所属门店 + /// + public string? PartnerId { get; set; } + + /// + /// 适用 Company 多选(fl_partner.Id) + /// + public List? PartnerIds { get; set; } + + /// + /// 适用 Region 多选(fl_group.Id);与 合并 + /// + public List? RegionIds { get; set; } + + /// + /// 适用 Region 多选(与 相同) + /// + public List? GroupIds { get; set; } + + /// + /// 所属门店(location.Id);可与 Company/Region 一并传入以校验范围;单独传则直接落库 + /// + public string? LocationId { get; set; } + + /// + /// 门店候选(多选);标签仅绑定单门店:未传 且合并结果唯一时自动取该项 + /// + public List? LocationIds { get; set; } public string LabelCategoryId { get; set; } = string.Empty; 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 07dd5f8..5594c9a 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 @@ -10,7 +10,12 @@ public class LabelGetListInputVo : PagedAndSortedResultRequestDto public string? Keyword { get; set; } /// - /// 门店Id(可选) + /// 组织/Region Id(fl_group.Id);筛选标签所属门店落在该 Region 下的记录 + /// + public string? GroupId { get; set; } + + /// + /// 门店 Id(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/Dtos/Label/LabelGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetOutputDto.cs index 77cd258..4362731 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetOutputDto.cs @@ -10,6 +10,17 @@ public class LabelGetOutputDto public string LocationName { get; set; } = string.Empty; + /// 适用 Company Id(由所属门店反推) + public string? PartnerId { get; set; } + + public List PartnerIds { get; set; } = new(); + + /// 适用 Region Id(fl_group.Id + public List RegionIds { get; set; } = new(); + + /// 相同 + public List GroupIds { get; set; } = new(); + public string LabelCategoryId { get; set; } = string.Empty; public string LabelCategoryName { get; set; } = string.Empty; 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 f656110..e676348 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 @@ -6,7 +6,17 @@ public class LabelUpdateInputVo public string TemplateCode { get; set; } = string.Empty; - public string LocationId { get; set; } = string.Empty; + public string? PartnerId { get; set; } + + public List? PartnerIds { get; set; } + + public List? RegionIds { get; set; } + + public List? GroupIds { get; set; } + + public string? LocationId { get; set; } + + public List? LocationIds { get; set; } public string LabelCategoryId { get; set; } = string.Empty; 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 efdd7fd..349c5da 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 @@ -29,7 +29,17 @@ public class LabelCategoryCreateInputVo public string AvailabilityType { get; set; } = "ALL"; /// - /// 指定门店 Id 列表(当 AvailabilityType=SPECIFIED 时必填) + /// 适用 Region(多选),fl_group.Id;与 合并去重 + /// + public List? RegionIds { get; set; } + + /// + /// 适用 Region(多选),与 相同 + /// + public List? GroupIds { get; set; } + + /// + /// 适用门店(多选),location.Id;与 Region 合并后写入 fl_label_category_location /// public List? LocationIds { get; set; } 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 5f93662..b70e95d 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 @@ -7,5 +7,15 @@ public class LabelCategoryGetListInputVo : PagedAndSortedResultRequestDto public string? Keyword { get; set; } public bool? State { get; set; } + + /// + /// Region 筛选(fl_group.Id);含 availabilityType=ALL 的分类 + /// + public string? GroupId { get; set; } + + /// + /// 门店筛选(location.Id);优先于 + /// + public string? LocationId { get; set; } } 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 7a04c18..694232c 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,15 +18,33 @@ public class LabelCategoryGetListOutputDto public string AvailabilityType { get; set; } = "ALL"; - /// - /// 当 为 SPECIFIED 时返回绑定的门店 Id;否则为空列表。 - /// + /// 适用 Region 展示(ALL 时为 All Regions) + public string Region { get; set; } = string.Empty; + + /// 适用门店展示(ALL 时为 All Locations) + public string Location { get; set; } = string.Empty; + + /// 适用 Region Id 列表(多选) + public List RegionIds { get; set; } = new(); + + /// 适用门店 Id 列表(多选) public List LocationIds { get; set; } = new(); public int OrderNum { get; set; } + /// + /// 该分类下已创建标签数量(fl_label 未删除且 LabelCategoryId 匹配) + /// public long NoOfLabels { get; set; } + /// + /// 分类创建时间(兼容旧前端字段 creationTime) + /// + public DateTime CreationTime { get; set; } + + /// + /// 最后编辑时间:分类自身与下属标签的 LastModificationTime/CreationTime 取最大值 + /// public DateTime LastEdited { get; set; } } 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 c4a740b..5ccdc73 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,12 @@ public class LabelCategoryGetOutputDto public string AvailabilityType { get; set; } = "ALL"; + /// 适用 Region Id 列表(多选;ALL 时为空) + public List RegionIds { get; set; } = new(); + + /// 相同 + public List GroupIds { get; set; } = new(); + public List LocationIds { get; set; } = new(); public int OrderNum { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationCreateInputVo.cs index bf453f5..00b1d34 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationCreateInputVo.cs @@ -34,6 +34,11 @@ public class LocationCreateInputVo public decimal? Longitude { get; set; } + /// + /// 经营时间(自由文本) + /// + public string? OperatingHours { get; set; } + public bool State { get; set; } = true; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationGetListOutputDto.cs index 067aec1..7bcc7e1 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationGetListOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationGetListOutputDto.cs @@ -33,6 +33,11 @@ public class LocationGetListOutputDto public decimal? Longitude { get; set; } + /// + /// 经营时间 + /// + public string? OperatingHours { get; set; } + public bool State { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationUpdateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationUpdateInputVo.cs index 45f7731..7dc859f 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationUpdateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Location/LocationUpdateInputVo.cs @@ -33,6 +33,11 @@ public class LocationUpdateInputVo public decimal? Longitude { get; set; } /// + /// 经营时间(自由文本) + /// + public string? OperatingHours { get; set; } + + /// /// 启用状态 /// public bool State { get; set; } = true; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerCreateInputVo.cs index 18d024c..2f49729 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerCreateInputVo.cs @@ -3,7 +3,7 @@ namespace FoodLabeling.Application.Contracts.Dtos.Partner; /// /// 新建合作伙伴入参 /// -public class PartnerCreateInputVo +public class PartnerCreateInputVo : PartnerAddressFieldsDto { public string PartnerName { get; set; } = string.Empty; @@ -11,5 +11,8 @@ public class PartnerCreateInputVo public string? PhoneNumber { get; set; } + /// + /// 是否启用(Active / Inactive) + /// public bool State { get; set; } = true; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListOutputDto.cs index 42bef41..c3a5112 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListOutputDto.cs @@ -3,7 +3,7 @@ namespace FoodLabeling.Application.Contracts.Dtos.Partner; /// /// 合作伙伴列表项 /// -public class PartnerGetListOutputDto +public class PartnerGetListOutputDto : PartnerAddressFieldsDto { public string Id { get; set; } = string.Empty; @@ -13,6 +13,9 @@ public class PartnerGetListOutputDto public string? PhoneNumber { get; set; } + /// + /// 是否启用 + /// public bool State { get; set; } public DateTime CreationTime { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetOutputDto.cs index f4bf1ac..4bd4677 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetOutputDto.cs @@ -3,19 +3,7 @@ namespace FoodLabeling.Application.Contracts.Dtos.Partner; /// /// 合作伙伴详情 /// -public class PartnerGetOutputDto +public class PartnerGetOutputDto : PartnerGetListOutputDto { - public string Id { get; set; } = string.Empty; - - public string PartnerName { get; set; } = string.Empty; - - public string? ContactEmail { get; set; } - - public string? PhoneNumber { get; set; } - - public bool State { get; set; } - - public DateTime CreationTime { get; set; } - public DateTime? LastModificationTime { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerUpdateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerUpdateInputVo.cs index a24c330..9f450b0 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerUpdateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerUpdateInputVo.cs @@ -3,13 +3,6 @@ namespace FoodLabeling.Application.Contracts.Dtos.Partner; /// /// 编辑合作伙伴入参 /// -public class PartnerUpdateInputVo +public class PartnerUpdateInputVo : PartnerCreateInputVo { - public string PartnerName { get; set; } = string.Empty; - - public string? ContactEmail { get; set; } - - public string? PhoneNumber { get; set; } - - public bool State { get; set; } = true; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs index fb33b83..c45dab4 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs @@ -18,8 +18,18 @@ public class ProductCreateInputVo public bool State { get; set; } = true; /// - /// 可选。门店 Id 列表;每个 Id 在 fl_location_product 落一行(同一 fl_product 可对应多门店)。 - /// 不传或空列表则不在本接口写入门店关联(仍可用 product-location 接口维护)。 + /// 适用 Company(fl_partner.Id,UI 称 Company);展开该公司下全部门店后与 Region/门店合并写入 fl_location_product + /// + public string? PartnerId { get; set; } + + /// + /// 适用 Region(fl_group.Id,UI 称 Region;库字段为 location.GroupName) + /// + public List? GroupIds { get; set; } + + /// + /// 适用门店 Id 列表;与 合并后写入 fl_location_product。 + /// 不传则不在本接口写入门店关联。 /// public List? LocationIds { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetListInputVo.cs index b6e26fb..028e675 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetListInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetListInputVo.cs @@ -17,5 +17,20 @@ public class ProductGetListInputVo : PagedAndSortedResultRequestDto /// 启用状态 /// public bool? State { get; set; } + + /// + /// 公司(合作伙伴)Id,对应 fl_partner.Id;筛选绑定在该公司下门店的产品 + /// + public string? PartnerId { get; set; } + + /// + /// 组织/Region Id,对应 fl_group.Id;筛选绑定在该 Region 对应门店下的产品(优先于 ) + /// + public string? GroupId { get; set; } + + /// + /// 门店 Id(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/Dtos/Product/ProductGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetOutputDto.cs index 4308143..90d6071 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetOutputDto.cs @@ -18,8 +18,17 @@ public class ProductGetOutputDto public bool State { get; set; } + /// 适用 Company Id(fl_partner.Id,由关联门店反推;多公司时取第一个) + public string? PartnerId { get; set; } + + /// 适用 Company Id 列表(去重) + public List PartnerIds { get; set; } = new(); + + /// 适用 Region Id(fl_group.Id,由关联门店反推) + public List GroupIds { get; set; } = new(); + /// - /// 该产品关联的门店Id列表(来自 fl_location_product) + /// 适用门店 Id 列表(来自 fl_location_product) /// public List LocationIds { get; set; } = new(); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryCreateInputVo.cs index 9f45ada..f3e4a06 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryCreateInputVo.cs @@ -32,7 +32,17 @@ public class ProductCategoryCreateInputVo public string AvailabilityType { get; set; } = "ALL"; /// - /// 指定门店 Id 列表(当 AvailabilityType=SPECIFIED 时必填) + /// 适用 Region(**多选**),fl_group.Id 数组;与 等价,推荐本字段。 + /// + public List? RegionIds { get; set; } + + /// + /// 适用 Region(多选),与 相同;传任一会合并去重。 + /// + public List? GroupIds { get; set; } + + /// + /// 适用门店(**多选**),location.Id 数组;与 Region 合并后写入 fl_product_category_location。 /// public List? LocationIds { get; set; } 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 4828537..7350080 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 @@ -16,5 +16,15 @@ public class ProductCategoryGetListInputVo : PagedAndSortedResultRequestDto /// 启用状态过滤 /// public bool? State { get; set; } + + /// + /// 按 Region 筛选(fl_group.Id):返回适用于该 Region 下任一门门店的分类,以及 availabilityType=ALL 的分类 + /// + public string? GroupId { 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/Dtos/ProductCategory/ProductCategoryGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListOutputDto.cs index 51048ce..ebfc336 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListOutputDto.cs @@ -24,5 +24,21 @@ public class ProductCategoryGetListOutputDto public int OrderNum { get; set; } public DateTime LastEdited { get; set; } + + /// + /// 适用 Region 展示(availabilityType=ALL 时为 All RegionsSPECIFIED 时为绑定门店去重后的 location.GroupName,英文逗号拼接) + /// + public string Region { get; set; } = string.Empty; + + /// + /// 适用门店展示(availabilityType=ALL 时为 All LocationsSPECIFIED 时为绑定门店名称,英文逗号拼接) + /// + public string Location { get; set; } = string.Empty; + + /// 适用 Region Id 列表(多选;ALL 时为空数组) + public List RegionIds { get; set; } = new(); + + /// 适用门店 Id 列表(多选;ALL 时为空数组) + public List LocationIds { get; set; } = new(); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs index 2018bc2..09e0891 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs @@ -22,6 +22,13 @@ public class ProductCategoryGetOutputDto public string AvailabilityType { get; set; } = "ALL"; + /// 适用 Region Id 列表(多选,fl_group.Id;由绑定门店反推) + public List RegionIds { get; set; } = new(); + + /// 相同(兼容字段) + public List GroupIds { get; set; } = new(); + + /// 适用门店 Id 列表(多选,location.Id public List LocationIds { get; set; } = new(); public int OrderNum { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleCreateInputVo.cs index d545c08..af86fc8 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleCreateInputVo.cs @@ -19,18 +19,18 @@ public class RbacRoleCreateInputVo public bool State { get; set; } = true; /// - /// 排序;未传时服务端按 0 处理 + /// 排序号;不传或传 null 时新增默认为 0,编辑时保留原值 /// public int? OrderNum { get; set; } /// - /// 访问权限编码 JSON 数组字符串(如 ["manage_labels","manage_people"]) + /// 绑定菜单 Id;与 accessPermissions 同时传时以本字段为准 /// - public string? AccessPermissions { get; set; } + public List? MenuIds { get; set; } /// - /// 兼容旧字段名 accessPermissionCodes + /// 按 PermissionCode 绑定菜单(英文逗号分隔);传空字符串表示清空绑定;不传则不修改已有绑定(仅编辑时) /// - public List? AccessPermissionCodes { get; set; } + public string? AccessPermissions { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleGetListOutputDto.cs index e8b3f91..d981d13 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleGetListOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleGetListOutputDto.cs @@ -20,8 +20,8 @@ public class RbacRoleGetListOutputDto public int OrderNum { get; set; } /// - /// 访问权限编码列表 + /// 已绑定菜单的 PermissionCode 汇总(英文逗号+空格拼接,与 /api/app/role 一致) /// - public List AccessPermissionCodes { get; set; } = new(); + public string AccessPermissions { get; set; } = string.Empty; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs index aedc9e6..b443b71 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs @@ -18,9 +18,29 @@ public class TeamMemberCreateInputVo public Guid? RoleId { get; set; } /// - /// 关联门店(至少1个) + /// 适用 Company(fl_partner.Id,UI 称 Company);与 合并 /// - public List LocationIds { get; set; } = new(); + public string? PartnerId { get; set; } + + /// + /// 适用 Company 多选(fl_partner.Id) + /// + public List? PartnerIds { get; set; } + + /// + /// 适用 Region 多选(fl_group.Id);与 合并 + /// + public List? RegionIds { get; set; } + + /// + /// 适用 Region 多选(与 相同) + /// + public List? GroupIds { get; set; } + + /// + /// 适用门店多选(location.Id);与 Company/Region 合并后写入 userlocation + /// + public List? LocationIds { get; set; } public bool State { get; set; } = true; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs index 90a383c..e28a30a 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs @@ -21,6 +21,12 @@ public class TeamMemberGetListOutputDto public string? RoleName { get; set; } + /// 适用 Company Id 列表(多选) + public List PartnerIds { get; set; } = new(); + + /// 适用 Region Id 列表(多选) + public List RegionIds { get; set; } = new(); + public List AssignedLocations { get; set; } = new(); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetOutputDto.cs index 9daa71a..bf245f9 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetOutputDto.cs @@ -16,6 +16,15 @@ public class TeamMemberGetOutputDto public Guid? RoleId { get; set; } + /// 适用 Company Id(多选,由绑定门店反推) + public List PartnerIds { get; set; } = new(); + + /// 适用 Region Id(多选,fl_group.Id + public List RegionIds { get; set; } = new(); + + /// 相同 + public List GroupIds { get; set; } = new(); + public List LocationIds { get; set; } = new(); public List AssignedLocations { get; set; } = new(); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs index f0e1e7d..98299e9 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs @@ -17,7 +17,30 @@ public class TeamMemberUpdateInputVo public Guid? RoleId { get; set; } - public List LocationIds { get; set; } = new(); + /// + /// 适用 Company(fl_partner.Id);与 合并 + /// + public string? PartnerId { get; set; } + + /// + /// 适用 Company 多选(fl_partner.Id) + /// + public List? PartnerIds { get; set; } + + /// + /// 适用 Region 多选(fl_group.Id);与 合并 + /// + public List? RegionIds { get; set; } + + /// + /// 适用 Region 多选(与 相同) + /// + public List? GroupIds { get; set; } + + /// + /// 适用门店多选(location.Id);与 Company/Region 合并后写入 userlocation + /// + public List? LocationIds { get; set; } public bool State { get; set; } = true; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/FoodLabeling.Application.Contracts.csproj b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/FoodLabeling.Application.Contracts.csproj index 6a5eb2d..fbb5ead 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/FoodLabeling.Application.Contracts.csproj +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/FoodLabeling.Application.Contracts.csproj @@ -3,6 +3,7 @@ + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IAuthSessionAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IAuthSessionAppService.cs index a033cd9..e858484 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IAuthSessionAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IAuthSessionAppService.cs @@ -13,6 +13,8 @@ public interface IAuthSessionAppService : IApplicationService /// /// /// 与框架 UserManager.GetInfoAsync 一致;用户名为 admin 时返回全部未删除菜单(与 AccountService.GetVue3Router 行为对齐)。 + /// 返回体额外包含:lastUpdated(用户 LastModificationTime)、role(角色展示名,多角色英文逗号拼接)、fullName(姓名优先,其次昵称、用户名)。 + /// 角色名通过 Role 表直查(RoleDbEntity),避免走仓储 IDataPermission。 /// /// 用户简要信息、权限码与菜单树 /// 成功 diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs index 8fbb65f..d216d6c 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs @@ -14,13 +14,18 @@ public interface IGroupAppService : IApplicationService /// /// 组织分页列表(与导出使用相同筛选条件) /// + /// + /// 数据范围(按登录 Token): + /// 管理员可见全部 Region;其它角色仅可见其 userlocation 绑定门店对应的组织(location.Partner + location.GroupNamefl_group 匹配)。 + /// /// 分页与筛选;SkipCount 为页码(从 1 起) Task> GetListAsync(GroupGetListInputVo input); /// /// 组织详情 /// - Task GetAsync(string id); + /// 主键(Guid,与 fl_group.Id 一致;约定路由 {id:guid},避免与 export-pdf 等路径冲突) + Task GetAsync(Guid id); /// /// 新增组织 @@ -30,16 +35,19 @@ public interface IGroupAppService : IApplicationService /// /// 编辑组织 /// - Task UpdateAsync(string id, GroupUpdateInputVo input); + Task UpdateAsync(Guid id, GroupUpdateInputVo input); /// /// 删除组织(逻辑删除) /// - Task DeleteAsync(string id); + Task DeleteAsync(Guid id); /// - /// 按列表相同筛选条件全量导出组织(Region)为 PDF(不分页;与 相同筛选;单次最多 5000 条) + /// 按列表相同筛选条件全量导出组织(Region)为 PDF(不分页;与 相同筛选与 Token 数据范围;单次最多 5000 条) /// + /// + /// 与 使用同一套 BuildGroupJoinedQueryAsync(含 ResolveGroupScopeAsync),导出行集与列表一致。 + /// /// Keyword、PartnerId、State、Sorting;分页字段忽略 /// application/pdf Task ExportPdfAsync(GroupGetListInputVo input); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPartnerAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPartnerAppService.cs index 096be2c..1797cce 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPartnerAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPartnerAppService.cs @@ -14,6 +14,13 @@ public interface IPartnerAppService : IApplicationService /// /// 合作伙伴分页列表(与导出使用相同筛选条件) /// + /// + /// 数据范围(按登录 Token): + /// + /// 管理员(角色码 admin、用户名为 admin 或权限 *:*:*):可查看全部公司; + /// 其它角色:仅可查看当前用户在 userlocation 中绑定门店所属的合作伙伴(location.Partnerfl_partner 按名称或 Id 匹配)。 + /// + /// /// 分页与筛选;SkipCount 为页码(从 1 起) /// 分页数据 /// 成功 @@ -24,17 +31,17 @@ public interface IPartnerAppService : IApplicationService /// /// 合作伙伴详情 /// - /// 主键 Id + /// 主键(Guid,与 fl_partner.Id 一致;约定路由 {id:guid},避免与 export-pdf 等路径冲突) /// 详情 /// 成功 /// Id 无效 /// 服务器错误 - Task GetAsync(string id); + Task GetAsync(Guid id); /// /// 新增合作伙伴 /// - /// 名称、邮箱、电话、启用状态 + /// 名称、联系信息、地址、启用状态 /// 新建后的详情 /// /// 示例请求: @@ -43,9 +50,15 @@ public interface IPartnerAppService : IApplicationService /// "partnerName": "Global Foods Inc.", /// "contactEmail": "admin@globalfoods.com", /// "phoneNumber": "+1 (555) 100-2000", + /// "street": "123 Main St", + /// "city": "New York", + /// "stateCode": "NY", + /// "country": "USA", + /// "zipCode": "10001", /// "state": true /// } /// ``` + /// 地址中的州/省请传 stateCode(如 NY);state(boolean)表示是否启用。 /// /// 成功 /// 校验失败 @@ -56,12 +69,12 @@ public interface IPartnerAppService : IApplicationService /// 编辑合作伙伴 /// /// 主键 Id - /// 名称、邮箱、电话、启用状态 + /// 名称、联系信息、地址、启用状态(字段同新增) /// 更新后的详情 /// 成功 /// 校验失败或记录不存在 /// 服务器错误 - Task UpdateAsync(string id, PartnerUpdateInputVo input); + Task UpdateAsync(Guid id, PartnerUpdateInputVo input); /// /// 删除合作伙伴(逻辑删除) @@ -70,7 +83,7 @@ public interface IPartnerAppService : IApplicationService /// 成功 /// Id 无效或记录不存在 /// 服务器错误 - Task DeleteAsync(string id); + Task DeleteAsync(Guid id); /// /// 按当前列表筛选条件批量导出合作伙伴为 PDF(Account Management「Company」页签;不分页,上限 5000 条) @@ -78,7 +91,7 @@ public interface IPartnerAppService : IApplicationService /// 与列表相同的 Keyword、State;分页字段忽略 /// PDF 文件流 /// - /// 筛选条件需与 一致,便于统计与导出数据对齐。 + /// 筛选条件与数据范围需与 完全一致(含 Token 权限:管理员全部公司,其它角色仅绑定门店所属公司;见 PartnerScopeHelper)。 /// /// 成功返回 application/pdf /// 参数错误 diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs index 1ad120a..6b45621 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs @@ -14,19 +14,26 @@ public interface IProductAppService : IApplicationService /// /// 产品分页列表 /// + /// + /// 支持按公司 partnerId、组织 groupId、门店 locationId 筛选: + /// 仅返回在 fl_location_product 中关联到匹配门店的产品;三者均未传时不按门店收窄。 + /// locationId 最优先;传 groupId 时按 Region 对应门店的 Partner+GroupName 匹配。 + /// Task> GetListAsync(ProductGetListInputVo input); /// /// 产品详情 /// - Task GetAsync(string id); + /// 产品主键(Guid,与 fl_product.Id 一致;使用 Guid 路由约束,避免与 export-* 等字面路径冲突) + Task GetAsync(Guid id); /// /// 新增产品 /// /// /// 可选;为空时后端生成唯一编码(如 PRD_ + Guid)。 - /// 若 有值,将在同一事务内批量写入 fl_location_product(一门店一条)。 + /// 若传 (Company)、(Region) + /// 和/或 ,合并后写入 fl_location_product。 /// Task CreateAsync(ProductCreateInputVo input); @@ -34,15 +41,15 @@ public interface IProductAppService : IApplicationService /// 编辑产品 /// /// - /// 当请求体包含 属性时,按该列表整表替换本产品在各门店的关联; - /// 不传该属性则不改门店关联(兼容仅改名称/分类等调用)。 + /// 当请求体包含 + /// 和/或 时,合并后整表替换门店关联;均不传则不改。 /// - Task UpdateAsync(string id, ProductUpdateInputVo input); + Task UpdateAsync(Guid id, ProductUpdateInputVo input); /// /// 删除产品(逻辑删除) /// - Task DeleteAsync(string id); + Task DeleteAsync(Guid id); /// /// 下载 Product 批量导入模板(服务器 TemplateDirectory 下 xlsx) diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductCategoryAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductCategoryAppService.cs index 74f8bee..51b5b6e 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductCategoryAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductCategoryAppService.cs @@ -9,12 +9,21 @@ namespace FoodLabeling.Application.Contracts.IServices; /// public interface IProductCategoryAppService : IApplicationService { + /// + /// 类别分页列表;支持 groupId/locationId 筛选;出参含 regionlocation 展示字段。 + /// Task> GetListAsync(ProductCategoryGetListInputVo input); Task GetAsync(string id); + /// + /// 新增类别;body 传 regionIds(Region 多选)与 locationIds(门店多选)绑定适用范围。 + /// Task CreateAsync(ProductCategoryCreateInputVo input); + /// + /// 编辑类别;regionIds/locationIds 多选数组规则同新增;传空数组 [] 可清空对应范围。 + /// Task UpdateAsync(string id, ProductCategoryUpdateInputVo input); Task DeleteAsync(string id); 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 acfdbf1..8a764c2 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 @@ -1,5 +1,6 @@ using FoodLabeling.Application.Contracts.Dtos.UsAppAuth; using Volo.Abp.Application.Services; +using Yi.Framework.Rbac.Application.Contracts.Dtos.Account; namespace FoodLabeling.Application.Contracts.IServices; @@ -48,4 +49,26 @@ public interface IUsAppAuthAppService : IApplicationService /// 参数非法、未绑定或无权限 /// 服务器错误 Task GetLocationDetailAsync(string locationId); + + /// + /// App forgot password: send email verification code (same user store and rules as platform). + /// + /// + /// When RbacOptions.EnableCaptcha is true, pass Uuid and Code from platform captcha API. + /// + /// Email and optional image captcha. + /// Accepted. + /// Validation or rate limit. + /// Server error + Task PostSendForgotPasswordCodeByEmailAsync(EmailCaptchaImageDto input); + + /// + /// App forgot password: reset password with email OTP. + /// + /// Email, OTP, new password. + /// Account user name. + /// Password updated. + /// Invalid code or password policy. + /// Server error + Task PostResetPasswordByEmailAsync(RetrievePasswordByEmailDto input); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj index 713f6ec..dd58d12 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj @@ -9,6 +9,7 @@ + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs index 7507726..a6e8fc7 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs @@ -50,6 +50,12 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService throw new UserFriendlyException("用户不存在"); } + var userRoleIds = await _dbContext.SqlSugarClient.Queryable() + .Where(x => x.UserId == userId) + .Select(x => x.RoleId) + .ToListAsync(); + var distinctUserRoleIds = userRoleIds.Distinct().ToList(); + List menus; if (UserConst.Admin.Equals(user.UserName)) { @@ -60,12 +66,7 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService } else { - var roleIds = await _dbContext.SqlSugarClient.Queryable() - .Where(x => x.UserId == userId) - .Select(x => x.RoleId) - .ToListAsync(); - - var roleIdStrs = roleIds.Select(x => x.ToString()).Distinct().ToList(); + var roleIdStrs = distinctUserRoleIds.Select(x => x.ToString()).Distinct().ToList(); if (roleIdStrs.Count == 0) { menus = new List(); @@ -92,10 +93,11 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService .ThenBy(x => x.MenuName) .ToList(); - // 注意:查询 RoleAggregateRoot 会触发 YiRbacDbContext 的 IDataPermission 过滤, + // 注意:经仓储查询 RoleAggregateRoot 会触发 YiRbacDbContext 的 IDataPermission 过滤, // 其表达式包含 roleInfo.Select(...).Contains(...),在当前 SqlSugar 版本下会报“不支持 Select”。 - // 这里直接使用 JWT 中的角色码(CurrentUser.Roles)返回,避免触发过滤器。 + // 角色展示名使用 RoleDbEntity 直查 Role 表;角色编码列表仍用 JWT(CurrentUser.Roles),与原先 RoleCodes 行为一致。 var roleCodes = CurrentUser.Roles?.ToList() ?? new List(); + var roleDisplay = await BuildRoleDisplayAsync(distinctUserRoleIds, roleCodes); var permissionCodes = menuNodes .Where(x => !string.IsNullOrWhiteSpace(x.PermissionCode)) @@ -116,7 +118,10 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService }, RoleCodes = roleCodes.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().OrderBy(x => x).ToList(), PermissionCodes = permissionCodes, - Menus = BuildMenuTree(menuNodes) + Menus = BuildMenuTree(menuNodes), + LastUpdated = user.LastModificationTime, + Role = roleDisplay, + FullName = BuildFullName(user) }; } @@ -133,6 +138,67 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService return true; } + private static string BuildFullName(UserAggregateRoot user) + { + if (!string.IsNullOrWhiteSpace(user.Name)) + { + return user.Name.Trim(); + } + + if (!string.IsNullOrWhiteSpace(user.Nick)) + { + return user.Nick.Trim(); + } + + return user.UserName ?? string.Empty; + } + + private async Task BuildRoleDisplayAsync( + IReadOnlyList userRoleIds, + IReadOnlyList jwtRoleCodes) + { + var names = new List(); + if (userRoleIds.Count > 0) + { + names = await _dbContext.SqlSugarClient.Queryable() + .Where(r => !r.IsDeleted && userRoleIds.Contains(r.Id)) + .OrderBy(r => r.RoleName) + .Select(r => r.RoleName) + .ToListAsync(); + } + + if (names.Count == 0 && jwtRoleCodes.Count > 0) + { + var codes = jwtRoleCodes + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .Distinct() + .ToList(); + names = await _dbContext.SqlSugarClient.Queryable() + .Where(r => !r.IsDeleted && codes.Contains(r.RoleCode)) + .OrderBy(r => r.RoleName) + .Select(r => r.RoleName) + .ToListAsync(); + } + + var distinctNames = names + .Where(n => !string.IsNullOrWhiteSpace(n)) + .Select(n => n.Trim()) + .Distinct() + .ToList(); + if (distinctNames.Count > 0) + { + return string.Join(", ", distinctNames); + } + + var fallbackCodes = jwtRoleCodes + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .Distinct() + .ToList(); + return fallbackCodes.Count > 0 ? string.Join(", ", fallbackCodes) : string.Empty; + } + private static List BuildMenuTree(List flat) { var nodes = flat diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlPartnerDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlPartnerDbEntity.cs index 606d2c6..11e0b00 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlPartnerDbEntity.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlPartnerDbEntity.cs @@ -36,6 +36,21 @@ public class FlPartnerDbEntity /// public string? PhoneNumber { get; set; } + public string? Street { get; set; } + + public string? City { get; set; } + + /// + /// 州/省代码(如 NY);勿与启用状态字段 混淆 + /// + [SugarColumn(ColumnName = "StateCode")] + public string? StateCode { get; set; } + + public string? Country { get; set; } + + [SugarColumn(ColumnName = "ZipCode")] + public string? ZipCode { get; set; } + /// /// 是否启用(对应 UI Active) /// diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs index ed77a1f..b740889 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs @@ -36,13 +36,13 @@ public class GroupAppService : ApplicationService, IGroupAppService public async Task> GetListAsync(GroupGetListInputVo input) { RefAsync total = 0; - var query = BuildGroupJoinedQuery(input); + var query = await BuildGroupJoinedQueryAsync(input); var projected = query.Select((g, p) => new GroupGetListOutputDto { Id = g.Id, GroupName = g.GroupName, PartnerId = g.PartnerId, - PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "无" : p.PartnerName.Trim(), + PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "None" : p.PartnerName.Trim(), State = g.State, CreationTime = g.CreationTime }); @@ -52,19 +52,19 @@ public class GroupAppService : ApplicationService, IGroupAppService } /// - public async Task GetAsync(string id) + public async Task GetAsync(Guid id) { - var groupId = id?.Trim(); - if (string.IsNullOrWhiteSpace(groupId)) + if (id == Guid.Empty) { - throw new UserFriendlyException("组织Id不能为空"); + throw new UserFriendlyException("Group id is required."); } + var groupId = id.ToString(); var entity = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == groupId); if (entity is null) { - throw new UserFriendlyException("组织不存在"); + throw new UserFriendlyException("Group not found."); } var partnerName = await ResolvePartnerNameAsync(entity.PartnerId); @@ -78,13 +78,13 @@ public class GroupAppService : ApplicationService, IGroupAppService var name = input.GroupName?.Trim(); if (string.IsNullOrWhiteSpace(name)) { - throw new UserFriendlyException("组织名称不能为空"); + throw new UserFriendlyException("Region name is required."); } var partnerId = input.PartnerId?.Trim(); if (string.IsNullOrWhiteSpace(partnerId)) { - throw new UserFriendlyException("请选择所属合作伙伴"); + throw new UserFriendlyException("Parent company is required."); } await EnsurePartnerExistsAsync(partnerId); @@ -104,36 +104,36 @@ public class GroupAppService : ApplicationService, IGroupAppService }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); - return await GetAsync(entity.Id); + return await GetAsync(Guid.Parse(entity.Id)); } /// [UnitOfWork] - public async Task UpdateAsync(string id, GroupUpdateInputVo input) + public async Task UpdateAsync(Guid id, GroupUpdateInputVo input) { - var groupId = id?.Trim(); - if (string.IsNullOrWhiteSpace(groupId)) + if (id == Guid.Empty) { - throw new UserFriendlyException("组织Id不能为空"); + throw new UserFriendlyException("Group id is required."); } + var groupId = id.ToString(); var entity = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == groupId); if (entity is null) { - throw new UserFriendlyException("组织不存在"); + throw new UserFriendlyException("Group not found."); } var name = input.GroupName?.Trim(); if (string.IsNullOrWhiteSpace(name)) { - throw new UserFriendlyException("组织名称不能为空"); + throw new UserFriendlyException("Region name is required."); } var partnerId = input.PartnerId?.Trim(); if (string.IsNullOrWhiteSpace(partnerId)) { - throw new UserFriendlyException("请选择所属合作伙伴"); + throw new UserFriendlyException("Parent company is required."); } await EnsurePartnerExistsAsync(partnerId); @@ -145,24 +145,24 @@ public class GroupAppService : ApplicationService, IGroupAppService entity.LastModifierId = CurrentUser?.Id?.ToString(); await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); - return await GetAsync(groupId); + return await GetAsync(id); } /// [UnitOfWork] - public async Task DeleteAsync(string id) + public async Task DeleteAsync(Guid id) { - var groupId = id?.Trim(); - if (string.IsNullOrWhiteSpace(groupId)) + if (id == Guid.Empty) { - throw new UserFriendlyException("组织Id不能为空"); + throw new UserFriendlyException("Group id is required."); } + var groupId = id.ToString(); var entity = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == groupId); if (entity is null) { - throw new UserFriendlyException("组织不存在"); + throw new UserFriendlyException("Group not found."); } entity.IsDeleted = true; @@ -172,24 +172,26 @@ public class GroupAppService : ApplicationService, IGroupAppService } /// - public async Task ExportPdfAsync(GroupGetListInputVo input) + [HttpGet] + public async Task ExportPdfAsync([FromQuery] GroupGetListInputVo input) { QuestPDF.Settings.License = LicenseType.Community; - var exportBase = BuildGroupJoinedQuery(input); + var exportBase = await BuildGroupJoinedQueryAsync(input); var count = await exportBase.CountAsync(); if (count > ExportPdfMaxRows) { - throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); + throw new UserFriendlyException( + $"Export exceeds the maximum of {ExportPdfMaxRows} rows. Narrow your filters and try again."); } - var rows = await BuildGroupJoinedQuery(input) + var rows = await (await BuildGroupJoinedQueryAsync(input)) .Select((g, p) => new GroupGetListOutputDto { Id = g.Id, GroupName = g.GroupName, PartnerId = g.PartnerId, - PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "无" : p.PartnerName.Trim(), + PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "None" : p.PartnerName.Trim(), State = g.State, CreationTime = g.CreationTime }) @@ -228,7 +230,7 @@ public class GroupAppService : ApplicationService, IGroupAppService table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) .Text(e.GroupName ?? string.Empty); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) - .Text(string.IsNullOrWhiteSpace(e.PartnerName) ? "无" : e.PartnerName); + .Text(string.IsNullOrWhiteSpace(e.PartnerName) ? "None" : e.PartnerName); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm")); @@ -243,14 +245,21 @@ public class GroupAppService : ApplicationService, IGroupAppService return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName }; } - private ISugarQueryable BuildGroupJoinedQuery(GroupGetListInputVo input) + private async Task> BuildGroupJoinedQueryAsync( + GroupGetListInputVo input) { + var scope = await PartnerScopeHelper.ResolveGroupScopeAsync(CurrentUser, _dbContext); + var keyword = input.Keyword?.Trim(); var partnerId = input.PartnerId?.Trim(); var query = _dbContext.SqlSugarClient.Queryable() .LeftJoin((g, p) => g.PartnerId == p.Id && !p.IsDeleted) - .Where((g, p) => !g.IsDeleted) + .Where((g, p) => !g.IsDeleted); + + query = PartnerScopeHelper.ApplyGroupScope(query, scope); + + query = query .WhereIF(input.State != null, (g, p) => g.State == input.State) .WhereIF(!string.IsNullOrWhiteSpace(partnerId), (g, p) => g.PartnerId == partnerId) .WhereIF(!string.IsNullOrWhiteSpace(keyword), @@ -311,7 +320,7 @@ public class GroupAppService : ApplicationService, IGroupAppService .AnyAsync(x => !x.IsDeleted && x.Id == partnerId); if (!ok) { - throw new UserFriendlyException("所选合作伙伴不存在或已删除"); + throw new UserFriendlyException("The selected company does not exist or has been removed."); } } @@ -321,7 +330,7 @@ public class GroupAppService : ApplicationService, IGroupAppService .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); if (p is null || string.IsNullOrWhiteSpace(p.PartnerName)) { - return "无"; + return "None"; } return p.PartnerName.Trim(); 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 64fbea7..f62a369 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 @@ -34,6 +34,7 @@ public class LabelAppService : ApplicationService, ILabelAppService RefAsync total = 0; var productId = input.ProductId?.Trim(); + var groupId = input.GroupId?.Trim(); var locationId = input.LocationId?.Trim(); var keyword = input.Keyword?.Trim(); var labelCategoryId = input.LabelCategoryId?.Trim(); @@ -45,7 +46,6 @@ public class LabelAppService : ApplicationService, ILabelAppService var labelIdsQuery = _dbContext.SqlSugarClient.Queryable() .Where(l => !l.IsDeleted) - .WhereIF(!string.IsNullOrWhiteSpace(locationId), l => l.LocationId == locationId) .WhereIF(!string.IsNullOrWhiteSpace(labelCategoryId), l => l.LabelCategoryId == labelCategoryId) .WhereIF(!string.IsNullOrWhiteSpace(labelTypeId), l => l.LabelTypeId == labelTypeId) .WhereIF(input.State != null, l => l.State == input.State); @@ -58,6 +58,15 @@ public class LabelAppService : ApplicationService, ILabelAppService .Select((l, tpl) => l); } + var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( + _dbContext.SqlSugarClient, groupId, locationId); + if (scopedLocationIds is not null) + { + labelIdsQuery = scopedLocationIds.Count == 0 + ? labelIdsQuery.Where(_ => false) + : labelIdsQuery.Where(l => scopedLocationIds.Contains(l.LocationId)); + } + // 按产品筛选:存在 label-product 关联即可 if (!string.IsNullOrWhiteSpace(productId)) { @@ -259,12 +268,25 @@ public class LabelAppService : ApplicationService, ILabelAppService labelInfo = JsonSerializer.Deserialize(label.LabelInfoJson); } + var locationId = label.LocationId ?? string.Empty; + var locationIdList = string.IsNullOrWhiteSpace(locationId) + ? new List() + : new List { locationId.Trim() }; + var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIdList); + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIdList); + return new LabelGetOutputDto { Id = label.LabelCode ?? string.Empty, LabelName = label.LabelName, - LocationId = label.LocationId ?? string.Empty, + LocationId = locationId, LocationName = location?.LocationName ?? location?.LocationCode ?? "无", + PartnerId = partnerIds.Count > 0 ? partnerIds[0] : null, + PartnerIds = partnerIds, + RegionIds = regionIds, + GroupIds = regionIds, LabelCategoryId = label.LabelCategoryId ?? string.Empty, LabelCategoryName = category?.CategoryName ?? "无", LabelTypeId = label.LabelTypeId ?? string.Empty, @@ -301,10 +323,6 @@ public class LabelAppService : ApplicationService, ILabelAppService { throw new UserFriendlyException("模板编码不能为空"); } - if (string.IsNullOrWhiteSpace(input.LocationId)) - { - throw new UserFriendlyException("门店Id不能为空"); - } if (string.IsNullOrWhiteSpace(input.LabelCategoryId)) { throw new UserFriendlyException("标签分类Id不能为空"); @@ -314,6 +332,8 @@ public class LabelAppService : ApplicationService, ILabelAppService throw new UserFriendlyException("标签类型Id不能为空"); } + var resolvedLocationId = await ResolveLabelLocationIdForSaveAsync(input); + var template = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.TemplateCode == input.TemplateCode.Trim()); if (template is null) @@ -344,7 +364,7 @@ public class LabelAppService : ApplicationService, ILabelAppService LabelCode = labelCode, LabelName = labelName, TemplateId = template.Id, - LocationId = input.LocationId?.Trim(), + LocationId = resolvedLocationId, LabelCategoryId = input.LabelCategoryId?.Trim(), LabelTypeId = input.LabelTypeId?.Trim(), State = input.State, @@ -363,6 +383,9 @@ public class LabelAppService : ApplicationService, ILabelAppService }).ToList(); await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); + await LabelCategoryAppService.TouchLabelCategoryLastEditedAsync( + _dbContext, labelEntity.LabelCategoryId, CurrentUser?.Id?.ToString()); + return await GetAsync(labelCode); } @@ -391,10 +414,6 @@ public class LabelAppService : ApplicationService, ILabelAppService { throw new UserFriendlyException("模板编码不能为空"); } - if (string.IsNullOrWhiteSpace(input.LocationId)) - { - throw new UserFriendlyException("门店Id不能为空"); - } if (string.IsNullOrWhiteSpace(input.LabelCategoryId)) { throw new UserFriendlyException("标签分类Id不能为空"); @@ -404,6 +423,8 @@ public class LabelAppService : ApplicationService, ILabelAppService throw new UserFriendlyException("标签类型Id不能为空"); } + var resolvedLocationId = await ResolveLabelLocationIdForSaveAsync(input); + var template = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.TemplateCode == input.TemplateCode.Trim()); if (template is null) @@ -411,10 +432,11 @@ public class LabelAppService : ApplicationService, ILabelAppService throw new UserFriendlyException("模板不存在"); } + var oldCategoryId = label.LabelCategoryId; var now = DateTime.Now; label.LabelName = input.LabelName?.Trim() ?? label.LabelName; label.TemplateId = template.Id; - label.LocationId = input.LocationId?.Trim(); + label.LocationId = resolvedLocationId; label.LabelCategoryId = input.LabelCategoryId?.Trim(); label.LabelTypeId = input.LabelTypeId?.Trim(); label.State = input.State; @@ -438,6 +460,15 @@ public class LabelAppService : ApplicationService, ILabelAppService }).ToList(); await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); + var newCategoryId = label.LabelCategoryId; + await LabelCategoryAppService.TouchLabelCategoryLastEditedAsync( + _dbContext, newCategoryId, CurrentUser?.Id?.ToString()); + if (!string.Equals(oldCategoryId, newCategoryId, StringComparison.Ordinal)) + { + await LabelCategoryAppService.TouchLabelCategoryLastEditedAsync( + _dbContext, oldCategoryId, CurrentUser?.Id?.ToString()); + } + return await GetAsync(labelCode); } @@ -465,6 +496,9 @@ public class LabelAppService : ApplicationService, ILabelAppService await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.LabelId == label.Id) .ExecuteCommandAsync(); + + await LabelCategoryAppService.TouchLabelCategoryLastEditedAsync( + _dbContext, label.LabelCategoryId, CurrentUser?.Id?.ToString()); } /// @@ -730,5 +764,86 @@ public class LabelAppService : ApplicationService, ILabelAppService Elements = resolvedElements }; } + + private Task ResolveLabelLocationIdForSaveAsync(LabelUpdateInputVo input) => + ResolveLabelLocationIdForSaveAsync(new LabelCreateInputVo + { + PartnerId = input.PartnerId, + PartnerIds = input.PartnerIds, + RegionIds = input.RegionIds, + GroupIds = input.GroupIds, + LocationId = input.LocationId, + LocationIds = input.LocationIds + }); + + /// + /// 标签仅绑定单门店:解析 Company/Region/门店入参为唯一 location.Id。 + /// + private async Task ResolveLabelLocationIdForSaveAsync(LabelCreateInputVo input) + { + var partnerIds = NormalizePartnerIds(input); + var regionIds = NormalizeRegionIds(input); + var explicitLocationId = input.LocationId?.Trim(); + var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); + + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( + _dbContext.SqlSugarClient, partnerIds, regionIds, explicitLocationIds); + + if (!string.IsNullOrWhiteSpace(explicitLocationId)) + { + if (merged.Count > 0 && !merged.Contains(explicitLocationId, StringComparer.Ordinal)) + { + throw new UserFriendlyException("所选门店不在指定公司/区域范围内"); + } + + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync( + _dbContext.SqlSugarClient, new List { explicitLocationId }); + return explicitLocationId; + } + + if (merged.Count == 0) + { + throw new UserFriendlyException("须指定门店 locationId,或选择公司/区域以解析出门店"); + } + + if (merged.Count > 1) + { + throw new UserFriendlyException("所选公司/区域对应多个门店,请显式指定 locationId"); + } + + return merged[0]; + } + + private static List NormalizePartnerIds(LabelCreateInputVo input) + { + var merged = new HashSet(StringComparer.Ordinal); + if (!string.IsNullOrWhiteSpace(input.PartnerId)) + { + merged.Add(input.PartnerId.Trim()); + } + + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.PartnerIds)) + { + merged.Add(id); + } + + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); + } + + private static List NormalizeRegionIds(LabelCreateInputVo input) + { + var merged = new HashSet(StringComparer.Ordinal); + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds)) + { + merged.Add(id); + } + + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds)) + { + merged.Add(id); + } + + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); + } } 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 5688583..90abbc1 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 @@ -3,6 +3,7 @@ using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Contracts.Dtos.LabelCategory; using FoodLabeling.Application.Contracts.IServices; using FoodLabeling.Application.Services.DbModels; +using FoodLabeling.Domain.Entities; using SqlSugar; using Volo.Abp; using Volo.Abp.Application.Services; @@ -34,6 +35,8 @@ 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); + // Sorting 仅允许白名单字段,避免 Unknown column/注入风险 if (!string.IsNullOrWhiteSpace(input.Sorting)) { @@ -67,57 +70,33 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); var ids = entities.Select(x => x.Id).ToList(); - Dictionary> locationIdsMap = new(); - if (ids.Count > 0) - { - var locRows = await _dbContext.SqlSugarClient.Queryable() - .Where(x => ids.Contains(x.CategoryId)) - .Select(x => new { x.CategoryId, x.LocationId }) - .ToListAsync(); - foreach (var row in locRows) - { - var cid = row.CategoryId?.Trim(); - var lid = row.LocationId?.Trim(); - if (string.IsNullOrWhiteSpace(cid) || string.IsNullOrWhiteSpace(lid)) - { - continue; - } - - if (!locationIdsMap.TryGetValue(cid, out var list)) - { - list = new List(); - locationIdsMap[cid] = list; - } + var labelStatsMap = await BuildCategoryLabelStatsMapAsync(ids); + var scopeMap = await BuildCategoryScopeMapAsync(entities); - if (!list.Contains(lid)) - { - list.Add(lid); - } - } - } - - var countRows = await _dbContext.SqlSugarClient.Queryable() - .Where(x => !x.IsDeleted) - .Where(x => x.LabelCategoryId != null && ids.Contains(x.LabelCategoryId)) - .GroupBy(x => x.LabelCategoryId) - .Select(x => new { CategoryId = x.LabelCategoryId, Count = SqlFunc.AggregateCount(x.Id) }) - .ToListAsync(); - var countMap = countRows.ToDictionary(x => x.CategoryId!, x => (long)x.Count); - - var items = entities.Select(x => new LabelCategoryGetListOutputDto + var items = entities.Select(x => { - Id = x.Id, - CategoryCode = x.CategoryCode, - CategoryName = x.CategoryName, - DisplayText = x.DisplayText, - CategoryPhotoUrl = x.CategoryPhotoUrl, - State = x.State, - ButtonAppearance = x.ButtonAppearance, - AvailabilityType = x.AvailabilityType, - LocationIds = locationIdsMap.TryGetValue(x.Id, out var lids) ? lids : new List(), - OrderNum = x.OrderNum, - NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0, - LastEdited = x.LastModificationTime ?? x.CreationTime + scopeMap.TryGetValue(x.Id, out var scope); + labelStatsMap.TryGetValue(x.Id, out var labelStats); + var creationTime = ResolveCategoryCreationTime(x); + return new LabelCategoryGetListOutputDto + { + Id = x.Id, + CategoryCode = x.CategoryCode, + CategoryName = x.CategoryName, + DisplayText = x.DisplayText, + CategoryPhotoUrl = x.CategoryPhotoUrl, + State = x.State, + ButtonAppearance = x.ButtonAppearance, + AvailabilityType = x.AvailabilityType, + OrderNum = x.OrderNum, + NoOfLabels = labelStats?.Count ?? 0, + CreationTime = creationTime, + LastEdited = ResolveLastEdited(x, creationTime, labelStats), + Region = scope?.Region ?? string.Empty, + Location = scope?.Location ?? string.Empty, + RegionIds = scope?.RegionIds ?? new List(), + LocationIds = scope?.LocationIds ?? new List() + }; }).ToList(); return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); @@ -139,7 +118,11 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ .Where(x => x.CategoryId == entity.Id) .Select(x => x.LocationId) .ToListAsync(); - dto.LocationIds = locationIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList() ?? new(); + dto.LocationIds = LocationScopeBindingHelper.NormalizeIds(locationIds); + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, dto.LocationIds); + dto.RegionIds = regionIds; + dto.GroupIds = regionIds; } return dto; @@ -156,9 +139,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ var displayText = input.DisplayText?.Trim(); var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); - var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); - var locationIds = NormalizeLocationIds(input.LocationIds); - ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); + var (availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); @@ -172,6 +153,12 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ var entity = new FlLabelCategoryDbEntity { Id = _guidGenerator.Create().ToString(), + IsDeleted = false, + CreationTime = now, + CreatorId = currentUserId, + LastModificationTime = now, + LastModifierId = currentUserId, + ConcurrencyStamp = _guidGenerator.Create().ToString("N"), CategoryCode = code, CategoryName = name, DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText, @@ -183,7 +170,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); - await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, currentUserId, now); + await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); return await GetAsync(entity.Id); } @@ -205,9 +192,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ var displayText = input.DisplayText?.Trim(); var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); - var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); - var locationIds = NormalizeLocationIds(input.LocationIds); - ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); + var (availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); @@ -228,7 +213,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ entity.LastModifierId = CurrentUser?.Id?.ToString(); await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); - await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now); + await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, + entity.LastModificationTime ?? DateTime.Now); return await GetAsync(id); } @@ -270,26 +256,299 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ }; } - private static void ValidateAvailabilityTypeAndLocations(string availabilityType, List locationIds) + private async Task<(string AvailabilityType, List LocationIds)> ResolveCategoryScopeForSaveAsync( + LabelCategoryCreateInputVo input) { + var regionIds = NormalizeRegionIds(input); + var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); + + var hasScopeArrays = input.RegionIds is not null || input.GroupIds is not null || input.LocationIds is not null; + if (regionIds.Count > 0 || explicitLocationIds.Count > 0) + { + availabilityType = "SPECIFIED"; + } + else if (hasScopeArrays && string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase)) + { + availabilityType = "ALL"; + } + if (availabilityType != "ALL" && availabilityType != "SPECIFIED") { throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); } - if (availabilityType == "SPECIFIED" && locationIds.Count == 0) + if (availabilityType == "ALL") { - throw new UserFriendlyException("指定门店范围时必须至少选择一个门店"); + return ("ALL", new List()); } + + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( + _dbContext.SqlSugarClient, (IReadOnlyList?)null, regionIds, explicitLocationIds); + if (merged.Count == 0) + { + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); + } + + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); + return ("SPECIFIED", merged); + } + + private static List NormalizeRegionIds(LabelCategoryCreateInputVo input) + { + var merged = new HashSet(StringComparer.Ordinal); + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds)) + { + merged.Add(id); + } + + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds)) + { + merged.Add(id); + } + + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); + } + + private async Task> ApplyCategoryScopeFilterAsync( + ISugarQueryable query, + string? groupId, + string? locationId) + { + var scopeLocIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( + _dbContext.SqlSugarClient, groupId, locationId); + if (scopeLocIds is null) + { + return query; + } + + if (scopeLocIds.Count == 0) + { + return query.Where(c => c.AvailabilityType == "ALL"); + } + + return query.Where(c => + c.AvailabilityType == "ALL" || + SqlFunc.Subqueryable() + .Where(cl => cl.CategoryId == c.Id && scopeLocIds.Contains(cl.LocationId)) + .Any()); } - private static List NormalizeLocationIds(List? locationIds) + private const string AllRegionsDisplay = "All Regions"; + private const string AllLocationsDisplay = "All Locations"; + private const string EmptyDisplay = "无"; + + private async Task> BuildCategoryScopeMapAsync( + List entities) { - return locationIds? + var result = new Dictionary(StringComparer.Ordinal); + if (entities.Count == 0) + { + return result; + } + + foreach (var e in entities.Where(x => + !string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))) + { + result[e.Id] = new CategoryScopeData + { + Region = AllRegionsDisplay, + Location = AllLocationsDisplay, + RegionIds = new List(), + LocationIds = new List() + }; + } + + var specifiedIds = entities + .Where(x => string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Id) + .ToList(); + if (specifiedIds.Count == 0) + { + return result; + } + + var links = await _dbContext.SqlSugarClient.Queryable() + .Where(x => specifiedIds.Contains(x.CategoryId)) + .ToListAsync(); + + var locIdSet = links + .Select(x => x.LocationId) .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x.Trim()) - .Distinct() - .ToList() ?? new(); + .Distinct(StringComparer.Ordinal) + .ToList(); + + var locById = new Dictionary(StringComparer.Ordinal); + if (locIdSet.Count > 0) + { + var guidList = locIdSet.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); + if (guidList.Count > 0) + { + var locs = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) + .ToListAsync(); + foreach (var loc in locs) + { + locById[loc.Id.ToString()] = loc; + } + } + } + + foreach (var catId in specifiedIds) + { + var catLinks = links.Where(x => x.CategoryId == catId).ToList(); + var locationIds = LocationScopeBindingHelper.NormalizeIds( + catLinks.Select(x => x.LocationId).ToList()); + + if (catLinks.Count == 0) + { + result[catId] = new CategoryScopeData + { + Region = EmptyDisplay, + Location = EmptyDisplay, + RegionIds = new List(), + LocationIds = new List() + }; + continue; + } + + var regions = new HashSet(StringComparer.OrdinalIgnoreCase); + var locationNames = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var link in catLinks) + { + var lid = link.LocationId?.Trim() ?? string.Empty; + if (string.IsNullOrEmpty(lid) || !locById.TryGetValue(lid, out var loc)) + { + continue; + } + + var groupName = loc.GroupName?.Trim(); + if (!string.IsNullOrEmpty(groupName)) + { + regions.Add(groupName); + } + + var locName = loc.LocationName?.Trim(); + if (string.IsNullOrEmpty(locName)) + { + locName = loc.LocationCode?.Trim(); + } + + if (!string.IsNullOrEmpty(locName)) + { + locationNames.Add(locName); + } + } + + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIds); + + result[catId] = new CategoryScopeData + { + Region = regions.Count > 0 + ? string.Join(", ", regions.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + : EmptyDisplay, + Location = locationNames.Count > 0 + ? string.Join(", ", locationNames.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + : EmptyDisplay, + RegionIds = regionIds, + LocationIds = locationIds + }; + } + + return result; + } + + private sealed class CategoryScopeData + { + public string Region { get; init; } = string.Empty; + public string Location { get; init; } = string.Empty; + public List RegionIds { get; init; } = new(); + public List LocationIds { get; init; } = new(); + } + + private sealed class CategoryLabelStats + { + public long Count { get; init; } + public DateTime MaxEdited { get; init; } + } + + /// + /// 统计各分类下标签数量及下属标签最近编辑时间(用于列表 LastEdited 同步)。 + /// + private async Task> BuildCategoryLabelStatsMapAsync(List categoryIds) + { + var result = new Dictionary(StringComparer.Ordinal); + if (categoryIds.Count == 0) + { + return result; + } + + var rows = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted) + .Where(x => x.LabelCategoryId != null && categoryIds.Contains(x.LabelCategoryId)) + .Select(x => new { x.LabelCategoryId, x.CreationTime, x.LastModificationTime }) + .ToListAsync(); + + foreach (var g in rows.GroupBy(x => x.LabelCategoryId!)) + { + result[g.Key] = new CategoryLabelStats + { + Count = g.Count(), + MaxEdited = g.Max(l => l.LastModificationTime ?? l.CreationTime) + }; + } + + return result; + } + + private static DateTime ResolveCategoryCreationTime(FlLabelCategoryDbEntity entity) + { + if (entity.CreationTime > DateTime.MinValue.AddYears(1)) + { + return entity.CreationTime; + } + + return entity.LastModificationTime ?? DateTime.Now; + } + + private static DateTime ResolveLastEdited( + FlLabelCategoryDbEntity entity, + DateTime creationTime, + CategoryLabelStats? labelStats) + { + var categoryEdited = entity.LastModificationTime ?? creationTime; + if (labelStats is null) + { + return categoryEdited; + } + + return labelStats.MaxEdited > categoryEdited ? labelStats.MaxEdited : categoryEdited; + } + + /// + /// 下属标签变更时回写分类最后编辑时间(与列表 一致)。 + /// + internal static async Task TouchLabelCategoryLastEditedAsync( + ISqlSugarDbContext dbContext, + string? categoryId, + string? modifierId) + { + var cid = categoryId?.Trim(); + if (string.IsNullOrWhiteSpace(cid)) + { + return; + } + + await dbContext.SqlSugarClient.Updateable() + .SetColumns(x => new FlLabelCategoryDbEntity + { + LastModificationTime = DateTime.Now, + LastModifierId = modifierId + }) + .Where(x => x.Id == cid && !x.IsDeleted) + .ExecuteCommandAsync(); } private async Task SaveCategoryLocationsAsync( diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationAppService.cs index 9c64e83..80b3956 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationAppService.cs @@ -101,6 +101,7 @@ public class LocationAppService : ApplicationService, ILocationAppService Email = input.Email?.Trim(), Latitude = input.Latitude, Longitude = input.Longitude, + OperatingHours = input.OperatingHours?.Trim(), State = input.State }; @@ -136,6 +137,7 @@ public class LocationAppService : ApplicationService, ILocationAppService entity.Email = input.Email?.Trim(); entity.Latitude = input.Latitude; entity.Longitude = input.Longitude; + entity.OperatingHours = input.OperatingHours?.Trim(); entity.State = input.State; await _locationRepository.UpdateAsync(entity); @@ -347,7 +349,8 @@ public class LocationAppService : ApplicationService, ILocationAppService (x.Country != null && x.Country.Contains(keyword!)) || (x.ZipCode != null && x.ZipCode.Contains(keyword!)) || (x.Phone != null && x.Phone.Contains(keyword!)) || - (x.Email != null && x.Email.Contains(keyword!)) + (x.Email != null && x.Email.Contains(keyword!)) || + (x.OperatingHours != null && x.OperatingHours.Contains(keyword!)) ); } @@ -368,6 +371,7 @@ public class LocationAppService : ApplicationService, ILocationAppService Email = x.Email, Latitude = x.Latitude, Longitude = x.Longitude, + OperatingHours = x.OperatingHours, State = x.State }; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PartnerAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PartnerAppService.cs index dff0733..ab9f5fb 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PartnerAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PartnerAppService.cs @@ -36,7 +36,7 @@ public class PartnerAppService : ApplicationService, IPartnerAppService public async Task> GetListAsync(PartnerGetListInputVo input) { RefAsync total = 0; - var query = BuildPartnerListQuery(input); + var query = await BuildPartnerListQueryAsync(input); var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); var items = entities.Select(MapListItem).ToList(); @@ -44,19 +44,19 @@ public class PartnerAppService : ApplicationService, IPartnerAppService } /// - public async Task GetAsync(string id) + public async Task GetAsync(Guid id) { - var partnerId = id?.Trim(); - if (string.IsNullOrWhiteSpace(partnerId)) + if (id == Guid.Empty) { - throw new UserFriendlyException("合作伙伴Id不能为空"); + throw new UserFriendlyException("Partner id is required."); } + var partnerId = id.ToString(); var entity = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); if (entity is null) { - throw new UserFriendlyException("合作伙伴不存在"); + throw new UserFriendlyException("Partner not found."); } return MapDetail(entity); @@ -69,13 +69,13 @@ public class PartnerAppService : ApplicationService, IPartnerAppService var name = input.PartnerName?.Trim(); if (string.IsNullOrWhiteSpace(name)) { - throw new UserFriendlyException("合作伙伴名称不能为空"); + throw new UserFriendlyException("Partner name is required."); } var email = input.ContactEmail?.Trim(); if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email)) { - throw new UserFriendlyException("联系邮箱格式不正确"); + throw new UserFriendlyException("Invalid contact email format."); } var now = Clock.Now; @@ -85,73 +85,75 @@ public class PartnerAppService : ApplicationService, IPartnerAppService IsDeleted = false, PartnerName = name, ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email, - PhoneNumber = string.IsNullOrWhiteSpace(input.PhoneNumber) ? null : input.PhoneNumber.Trim(), + PhoneNumber = TrimToNull(input.PhoneNumber), State = input.State, CreationTime = now, CreatorId = CurrentUser?.Id?.ToString(), LastModificationTime = now, LastModifierId = CurrentUser?.Id?.ToString() }; + ApplyAddressFields(entity, input); await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); - return await GetAsync(entity.Id); + return await GetAsync(Guid.Parse(entity.Id)); } /// [UnitOfWork] - public async Task UpdateAsync(string id, PartnerUpdateInputVo input) + public async Task UpdateAsync(Guid id, PartnerUpdateInputVo input) { - var partnerId = id?.Trim(); - if (string.IsNullOrWhiteSpace(partnerId)) + if (id == Guid.Empty) { - throw new UserFriendlyException("合作伙伴Id不能为空"); + throw new UserFriendlyException("Partner id is required."); } + var partnerId = id.ToString(); var entity = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); if (entity is null) { - throw new UserFriendlyException("合作伙伴不存在"); + throw new UserFriendlyException("Partner not found."); } var name = input.PartnerName?.Trim(); if (string.IsNullOrWhiteSpace(name)) { - throw new UserFriendlyException("合作伙伴名称不能为空"); + throw new UserFriendlyException("Partner name is required."); } var email = input.ContactEmail?.Trim(); if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email)) { - throw new UserFriendlyException("联系邮箱格式不正确"); + throw new UserFriendlyException("Invalid contact email format."); } entity.PartnerName = name; entity.ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email; - entity.PhoneNumber = string.IsNullOrWhiteSpace(input.PhoneNumber) ? null : input.PhoneNumber.Trim(); + entity.PhoneNumber = TrimToNull(input.PhoneNumber); entity.State = input.State; + ApplyAddressFields(entity, input); entity.LastModificationTime = Clock.Now; entity.LastModifierId = CurrentUser?.Id?.ToString(); await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); - return await GetAsync(partnerId); + return await GetAsync(id); } /// [UnitOfWork] - public async Task DeleteAsync(string id) + public async Task DeleteAsync(Guid id) { - var partnerId = id?.Trim(); - if (string.IsNullOrWhiteSpace(partnerId)) + if (id == Guid.Empty) { - throw new UserFriendlyException("合作伙伴Id不能为空"); + throw new UserFriendlyException("Partner id is required."); } + var partnerId = id.ToString(); var entity = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); if (entity is null) { - throw new UserFriendlyException("合作伙伴不存在"); + throw new UserFriendlyException("Partner not found."); } entity.IsDeleted = true; @@ -161,15 +163,17 @@ public class PartnerAppService : ApplicationService, IPartnerAppService } /// - public async Task ExportPdfAsync(PartnerGetListInputVo input) + [HttpGet] + public async Task ExportPdfAsync([FromQuery] PartnerGetListInputVo input) { QuestPDF.Settings.License = LicenseType.Community; - var count = await BuildPartnerListQuery(input).CountAsync(); - var query = BuildPartnerListQuery(input); + var count = await (await BuildPartnerListQueryAsync(input)).CountAsync(); + var query = await BuildPartnerListQueryAsync(input); if (count > ExportPdfMaxRows) { - throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); + throw new UserFriendlyException( + $"Export exceeds the maximum of {ExportPdfMaxRows} rows. Narrow your filters and try again."); } var rows = await query.Take(ExportPdfMaxRows).ToListAsync(); @@ -208,9 +212,9 @@ public class PartnerAppService : ApplicationService, IPartnerAppService table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) .Text(e.PartnerName ?? string.Empty); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) - .Text(string.IsNullOrWhiteSpace(e.ContactEmail) ? "无" : e.ContactEmail!); + .Text(string.IsNullOrWhiteSpace(e.ContactEmail) ? "None" : e.ContactEmail!); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) - .Text(string.IsNullOrWhiteSpace(e.PhoneNumber) ? "无" : e.PhoneNumber!); + .Text(string.IsNullOrWhiteSpace(e.PhoneNumber) ? "None" : e.PhoneNumber!); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm")); @@ -225,16 +229,27 @@ public class PartnerAppService : ApplicationService, IPartnerAppService return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName }; } - private ISugarQueryable BuildPartnerListQuery(PartnerGetListInputVo input) + private async Task> BuildPartnerListQueryAsync(PartnerGetListInputVo input) { + var scope = await PartnerScopeHelper.ResolvePartnerScopeAsync(CurrentUser, _dbContext); + var keyword = input.Keyword?.Trim(); var query = _dbContext.SqlSugarClient.Queryable() - .Where(x => !x.IsDeleted) + .Where(x => !x.IsDeleted); + + query = PartnerScopeHelper.ApplyPartnerScope(query, scope); + + query = query .WhereIF(input.State != null, x => x.State == input.State) .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.PartnerName.Contains(keyword!) || (x.ContactEmail != null && x.ContactEmail.Contains(keyword!)) || - (x.PhoneNumber != null && x.PhoneNumber.Contains(keyword!))); + (x.PhoneNumber != null && x.PhoneNumber.Contains(keyword!)) || + (x.Street != null && x.Street.Contains(keyword!)) || + (x.City != null && x.City.Contains(keyword!)) || + (x.StateCode != null && x.StateCode.Contains(keyword!)) || + (x.Country != null && x.Country.Contains(keyword!)) || + (x.ZipCode != null && x.ZipCode.Contains(keyword!))); if (!string.IsNullOrWhiteSpace(input.Sorting)) { @@ -276,26 +291,60 @@ public class PartnerAppService : ApplicationService, IPartnerAppService return query; } - private static PartnerGetListOutputDto MapListItem(FlPartnerDbEntity x) => new() + private static PartnerGetListOutputDto MapListItem(FlPartnerDbEntity x) { - Id = x.Id, - PartnerName = x.PartnerName, - ContactEmail = x.ContactEmail, - PhoneNumber = x.PhoneNumber, - State = x.State, - CreationTime = x.CreationTime - }; - - private static PartnerGetOutputDto MapDetail(FlPartnerDbEntity x) => new() + var dto = new PartnerGetListOutputDto + { + Id = x.Id, + PartnerName = x.PartnerName, + ContactEmail = x.ContactEmail, + PhoneNumber = x.PhoneNumber, + State = x.State, + CreationTime = x.CreationTime + }; + MapAddressFields(dto, x); + return dto; + } + + private static PartnerGetOutputDto MapDetail(FlPartnerDbEntity x) { - Id = x.Id, - PartnerName = x.PartnerName, - ContactEmail = x.ContactEmail, - PhoneNumber = x.PhoneNumber, - State = x.State, - CreationTime = x.CreationTime, - LastModificationTime = x.LastModificationTime - }; + var dto = new PartnerGetOutputDto + { + Id = x.Id, + PartnerName = x.PartnerName, + ContactEmail = x.ContactEmail, + PhoneNumber = x.PhoneNumber, + State = x.State, + CreationTime = x.CreationTime, + LastModificationTime = x.LastModificationTime + }; + MapAddressFields(dto, x); + return dto; + } + + private static void ApplyAddressFields(FlPartnerDbEntity entity, PartnerAddressFieldsDto input) + { + entity.Street = TrimToNull(input.Street); + entity.City = TrimToNull(input.City); + entity.StateCode = TrimToNull(input.StateCode); + entity.Country = TrimToNull(input.Country); + entity.ZipCode = TrimToNull(input.ZipCode); + } + + private static void MapAddressFields(PartnerAddressFieldsDto dto, FlPartnerDbEntity entity) + { + dto.Street = entity.Street; + dto.City = entity.City; + dto.StateCode = entity.StateCode; + dto.Country = entity.Country; + dto.ZipCode = entity.ZipCode; + } + + private static string? TrimToNull(string? value) + { + var t = value?.Trim(); + return string.IsNullOrEmpty(t) ? null : t; + } private static bool IsPlausibleEmail(string email) { diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs index 7e2526b..c5484b8 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs @@ -36,11 +36,12 @@ public class ProductAppService : ApplicationService, IProductAppService _batchImportOptions = batchImportOptions; } + /// public async Task> GetListAsync(ProductGetListInputVo input) { RefAsync total = 0; - var query = BuildFilteredProductQuery(input); + var query = await BuildFilteredProductQueryAsync(input); if (!string.IsNullOrWhiteSpace(input.Sorting)) { query = query.OrderBy(input.Sorting); @@ -107,14 +108,14 @@ public class ProductAppService : ApplicationService, IProductAppService return BuildPagedResult(input.SkipCount, input.MaxResultCount, (int)total, items); } - public async Task GetAsync(string id) + public async Task GetAsync(Guid id) { - var productId = id?.Trim(); - if (string.IsNullOrWhiteSpace(productId)) + if (id == Guid.Empty) { throw new UserFriendlyException("产品Id不能为空"); } + var productId = id.ToString(); var entity = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == productId); @@ -134,11 +135,15 @@ public class ProductAppService : ApplicationService, IProductAppService } } - var locationIds = await _dbContext.SqlSugarClient.Queryable() - .Where(x => x.ProductId == productId) - .Select(x => x.LocationId) - .Distinct() - .ToListAsync(); + var locationIds = LocationScopeBindingHelper.NormalizeIds( + await _dbContext.SqlSugarClient.Queryable() + .Where(x => x.ProductId == productId) + .Select(x => x.LocationId) + .ToListAsync()); + var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIds); + var groupIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIds); return new ProductGetOutputDto { @@ -149,6 +154,9 @@ public class ProductAppService : ApplicationService, IProductAppService CategoryName = categoryName, ProductImageUrl = entity.ProductImageUrl, State = entity.State, + PartnerId = partnerIds.Count > 0 ? partnerIds[0] : null, + PartnerIds = partnerIds, + GroupIds = groupIds, LocationIds = locationIds }; } @@ -190,24 +198,24 @@ public class ProductAppService : ApplicationService, IProductAppService await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); - if (input.LocationIds is not null) + if (HasProductScopeBinding(input)) { - var locIds = await NormalizeAndValidateLocationIdsAsync(input.LocationIds); + var locIds = await ResolveProductLocationIdsForSaveAsync(input); await ReplaceProductLocationLinksAsync(entity.Id, locIds); } - return await GetAsync(entity.Id); + return await GetAsync(Guid.Parse(entity.Id)); } [UnitOfWork] - public async Task UpdateAsync(string id, ProductUpdateInputVo input) + public async Task UpdateAsync(Guid id, ProductUpdateInputVo input) { - var productId = id?.Trim(); - if (string.IsNullOrWhiteSpace(productId)) + if (id == Guid.Empty) { throw new UserFriendlyException("产品Id不能为空"); } + var productId = id.ToString(); var entity = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == productId); if (entity is null) @@ -246,24 +254,24 @@ public class ProductAppService : ApplicationService, IProductAppService await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); - if (input.LocationIds is not null) + if (HasProductScopeBinding(input)) { - var locIds = await NormalizeAndValidateLocationIdsAsync(input.LocationIds); + var locIds = await ResolveProductLocationIdsForSaveAsync(input); await ReplaceProductLocationLinksAsync(productId, locIds); } - return await GetAsync(productId); + return await GetAsync(id); } [UnitOfWork] - public async Task DeleteAsync(string id) + public async Task DeleteAsync(Guid id) { - var productId = id?.Trim(); - if (string.IsNullOrWhiteSpace(productId)) + if (id == Guid.Empty) { throw new UserFriendlyException("产品Id不能为空"); } + var productId = id.ToString(); var entity = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == productId); if (entity is null) @@ -276,6 +284,7 @@ public class ProductAppService : ApplicationService, IProductAppService } /// + [HttpGet] public Task DownloadProductImportTemplateAsync() { var opt = _batchImportOptions.Value; @@ -306,16 +315,20 @@ public class ProductAppService : ApplicationService, IProductAppService } /// + [HttpGet] public async Task ExportProductsExcelAsync([FromQuery] ProductGetListInputVo input) { var exportFilter = new ProductGetListInputVo { Sorting = input.Sorting, Keyword = input.Keyword, - State = input.State + State = input.State, + PartnerId = input.PartnerId, + GroupId = input.GroupId, + LocationId = input.LocationId }; - var query = BuildFilteredProductQuery(exportFilter); + var query = await BuildFilteredProductQueryAsync(exportFilter); if (!string.IsNullOrWhiteSpace(exportFilter.Sorting)) { query = query.OrderBy(exportFilter.Sorting); @@ -334,6 +347,7 @@ public class ProductAppService : ApplicationService, IProductAppService } /// + [HttpPost] public async Task ImportProductsBatchAsync( [FromForm] ProductBatchImportInputVo input) { @@ -423,10 +437,14 @@ public class ProductAppService : ApplicationService, IProductAppService throw new UserFriendlyException($"单次批量编辑最多允许 {maxItems} 条,请分批提交"); } - var effectiveCount = input.Items.Count(static x => x is not null && !string.IsNullOrWhiteSpace(x.Id)); + var effectiveCount = input.Items.Count(static x => + x is not null && + !string.IsNullOrWhiteSpace(x.Id) && + Guid.TryParse(x.Id!.Trim(), out var g) && + g != Guid.Empty); if (effectiveCount == 0) { - throw new UserFriendlyException("没有有效的产品 Id(请为待保存行填写 id)"); + throw new UserFriendlyException("没有有效的产品 Id(请为待保存行填写 Guid 格式 id)"); } var result = new ProductBulkUpdateResultDto(); @@ -438,9 +456,14 @@ public class ProductAppService : ApplicationService, IProductAppService continue; } + if (!Guid.TryParse(item.Id.Trim(), out var productGuid) || productGuid == Guid.Empty) + { + continue; + } + try { - await UpdateAsync(item.Id.Trim(), item); + await UpdateAsync(productGuid, item); result.SuccessCount++; } catch (UserFriendlyException ex) @@ -458,7 +481,7 @@ public class ProductAppService : ApplicationService, IProductAppService return result; } - private ISugarQueryable BuildFilteredProductQuery(ProductGetListInputVo input) + private async Task> BuildFilteredProductQueryAsync(ProductGetListInputVo input) { var keyword = input.Keyword?.Trim(); @@ -467,6 +490,22 @@ public class ProductAppService : ApplicationService, IProductAppService .Where(x => !x.IsDeleted) .WhereIF(input.State != null, x => x.State == input.State); + var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); + if (locationIds is not null) + { + if (locationIds.Count == 0) + { + query = query.Where(_ => false); + } + else + { + query = query.Where(p => + SqlFunc.Subqueryable() + .Where(lp => lp.ProductId == p.Id && locationIds.Contains(lp.LocationId)) + .Any()); + } + } + if (!string.IsNullOrWhiteSpace(keyword)) { query = query @@ -481,6 +520,66 @@ public class ProductAppService : ApplicationService, IProductAppService return query; } + /// + /// 与 Reports 列表一致:按门店 Id 优先,否则按 Region(groupId)或公司(partnerId)解析 location 主键集合。 + /// + private async Task?> ResolveFilteredLocationIdsAsync(string? partnerId, string? groupId, + string? locationId) + { + var locId = locationId?.Trim(); + if (!string.IsNullOrWhiteSpace(locId)) + { + if (!Guid.TryParse(locId, out var locationGuid)) + { + return new List(); + } + + var exists = await _dbContext.SqlSugarClient.Queryable() + .AnyAsync(x => !x.IsDeleted && x.Id == locationGuid); + return exists ? new List { locId } : new List(); + } + + 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); + } + + return await q.Select(x => SqlFunc.ToString(x.Id)).ToListAsync(); + } + private async Task> BuildProductExcelExportRowsAsync( List entities) { @@ -657,40 +756,23 @@ public class ProductAppService : ApplicationService, IProductAppService throw new UserFriendlyException("无法生成唯一产品编码,请稍后重试或手动填写产品编码"); } + private static bool HasProductScopeBinding(ProductCreateInputVo input) => + !string.IsNullOrWhiteSpace(input.PartnerId) || + input.GroupIds is not null || + input.LocationIds is not null; + /// - /// 去重、校验门店 Id 格式与存在性。 + /// 合并 Company(partnerId)、Region(groupIds)、门店(locationIds)并校验存在性。 /// - private async Task> NormalizeAndValidateLocationIdsAsync(IEnumerable rawIds) + private async Task> ResolveProductLocationIdsForSaveAsync(ProductCreateInputVo input) { - var distinct = rawIds - .Where(x => !string.IsNullOrWhiteSpace(x)) - .Select(x => x.Trim()) - .Distinct(StringComparer.Ordinal) - .ToList(); - - if (distinct.Count == 0) - { - return new List(); - } - - foreach (var id in distinct) - { - if (!Guid.TryParse(id, out _)) - { - throw new UserFriendlyException("门店Id格式不正确"); - } - } - - var guidList = distinct.Select(Guid.Parse).ToList(); - var existCount = await _dbContext.SqlSugarClient.Queryable() - .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) - .CountAsync(); - if (existCount != distinct.Count) - { - throw new UserFriendlyException("门店不存在"); - } - - return distinct; + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( + _dbContext.SqlSugarClient, + input.PartnerId, + input.GroupIds, + input.LocationIds); + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); + return merged; } /// 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 a66c2f2..bb7ebce 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 @@ -3,6 +3,7 @@ using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Contracts.Dtos.ProductCategory; using FoodLabeling.Application.Contracts.IServices; using FoodLabeling.Application.Services.DbModels; +using FoodLabeling.Domain.Entities; using SqlSugar; using Volo.Abp; using Volo.Abp.Application.Services; @@ -40,6 +41,8 @@ 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); + // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column // 同时避免将 input.Sorting 原样拼接到 SQL(存在注入风险) if (!string.IsNullOrWhiteSpace(input.Sorting)) @@ -74,18 +77,28 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); - var items = entities.Select(x => new ProductCategoryGetListOutputDto + var scopeMap = await BuildCategoryScopeMapAsync(entities); + + var items = entities.Select(x => { - Id = x.Id, - CategoryCode = x.CategoryCode, - CategoryName = x.CategoryName, - DisplayText = x.DisplayText, - CategoryPhotoUrl = x.CategoryPhotoUrl, - ButtonAppearance = x.ButtonAppearance, - State = x.State, - AvailabilityType = x.AvailabilityType, - OrderNum = x.OrderNum, - LastEdited = x.LastModificationTime ?? x.CreationTime + scopeMap.TryGetValue(x.Id, out var scope); + return new ProductCategoryGetListOutputDto + { + Id = x.Id, + CategoryCode = x.CategoryCode, + CategoryName = x.CategoryName, + DisplayText = x.DisplayText, + CategoryPhotoUrl = x.CategoryPhotoUrl, + ButtonAppearance = x.ButtonAppearance, + State = x.State, + AvailabilityType = x.AvailabilityType, + OrderNum = x.OrderNum, + LastEdited = x.LastModificationTime ?? x.CreationTime, + Region = scope?.Region ?? string.Empty, + Location = scope?.Location ?? string.Empty, + RegionIds = scope?.RegionIds ?? new List(), + LocationIds = scope?.LocationIds ?? new List() + }; }).ToList(); return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); @@ -110,7 +123,11 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp .Where(x => x.CategoryId == entity.Id) .Select(x => x.LocationId) .ToListAsync(); - dto.LocationIds = locationIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList() ?? new(); + dto.LocationIds = LocationScopeBindingHelper.NormalizeIds(locationIds); + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, dto.LocationIds); + dto.RegionIds = regionIds; + dto.GroupIds = regionIds; } return dto; @@ -130,9 +147,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp var displayText = input.DisplayText?.Trim(); var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); - var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); - var locationIds = NormalizeLocationIds(input.LocationIds); - ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); + var (availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); @@ -163,7 +178,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); - await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, currentUserId, now); + await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); return await GetAsync(entity.Id); } @@ -188,9 +203,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp var displayText = input.DisplayText?.Trim(); var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); - var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); - var locationIds = NormalizeLocationIds(input.LocationIds); - ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); + var (availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); @@ -211,7 +224,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp entity.LastModifierId = CurrentUser?.Id?.ToString(); await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); - await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now); + await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, + entity.LastModificationTime ?? DateTime.Now); return await GetAsync(id); } @@ -257,26 +271,86 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp }; } - private static void ValidateAvailabilityTypeAndLocations(string availabilityType, List locationIds) + private async Task<(string AvailabilityType, List LocationIds)> ResolveCategoryScopeForSaveAsync( + ProductCategoryCreateInputVo input) { + var regionIds = NormalizeRegionIds(input); + var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); + + var hasScopeArrays = input.RegionIds is not null || input.GroupIds is not null || input.LocationIds is not null; + if (regionIds.Count > 0 || explicitLocationIds.Count > 0) + { + availabilityType = "SPECIFIED"; + } + else if (hasScopeArrays && string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase)) + { + // 显式传了空数组 [] 表示清空指定范围,仍视为 SPECIFIED(0 门店)或改回 ALL + availabilityType = "ALL"; + } + if (availabilityType != "ALL" && availabilityType != "SPECIFIED") { throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); } - if (availabilityType == "SPECIFIED" && locationIds.Count == 0) + if (availabilityType == "ALL") + { + return ("ALL", new List()); + } + + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( + _dbContext.SqlSugarClient, (IReadOnlyList?)null, regionIds, explicitLocationIds); + if (merged.Count == 0) { - throw new UserFriendlyException("指定门店范围时必须至少选择一个门店"); + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); } + + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); + return ("SPECIFIED", merged); } - private static List NormalizeLocationIds(List? locationIds) + /// + /// 合并入参中的 Region 多选()。 + /// + private static List NormalizeRegionIds(ProductCategoryCreateInputVo input) { - return locationIds? - .Where(x => !string.IsNullOrWhiteSpace(x)) - .Select(x => x.Trim()) - .Distinct() - .ToList() ?? new(); + var merged = new HashSet(StringComparer.Ordinal); + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds)) + { + merged.Add(id); + } + + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds)) + { + merged.Add(id); + } + + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); + } + + private async Task> ApplyCategoryScopeFilterAsync( + ISugarQueryable query, + string? groupId, + string? locationId) + { + var scopeLocIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( + _dbContext.SqlSugarClient, groupId, locationId); + if (scopeLocIds is null) + { + return query; + } + + if (scopeLocIds.Count == 0) + { + return query.Where(c => c.AvailabilityType == "ALL"); + } + + return query.Where(c => + c.AvailabilityType == "ALL" || + SqlFunc.Subqueryable() + .Where(cl => cl.CategoryId == c.Id && scopeLocIds.Contains(cl.LocationId)) + .Any()); } private async Task SaveCategoryLocationsAsync( @@ -312,6 +386,143 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); } + private const string AllRegionsDisplay = "All Regions"; + private const string AllLocationsDisplay = "All Locations"; + private const string EmptyDisplay = "无"; + + /// + /// 列表行:Region/Location 展示文案 + 多选 Id 数组(编辑回显)。 + /// + private async Task> BuildCategoryScopeMapAsync( + List entities) + { + var result = new Dictionary(StringComparer.Ordinal); + if (entities.Count == 0) + { + return result; + } + + foreach (var e in entities.Where(x => + !string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))) + { + result[e.Id] = new CategoryScopeData + { + Region = AllRegionsDisplay, + Location = AllLocationsDisplay, + RegionIds = new List(), + LocationIds = new List() + }; + } + + var specifiedIds = entities + .Where(x => string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Id) + .ToList(); + if (specifiedIds.Count == 0) + { + return result; + } + + var links = await _dbContext.SqlSugarClient.Queryable() + .Where(x => specifiedIds.Contains(x.CategoryId)) + .ToListAsync(); + + var locIdSet = links + .Select(x => x.LocationId) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .Distinct(StringComparer.Ordinal) + .ToList(); + + var locById = new Dictionary(StringComparer.Ordinal); + if (locIdSet.Count > 0) + { + var guidList = locIdSet.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); + if (guidList.Count > 0) + { + var locs = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) + .ToListAsync(); + foreach (var loc in locs) + { + locById[loc.Id.ToString()] = loc; + } + } + } + + foreach (var catId in specifiedIds) + { + var catLinks = links.Where(x => x.CategoryId == catId).ToList(); + var locationIds = LocationScopeBindingHelper.NormalizeIds( + catLinks.Select(x => x.LocationId).ToList()); + + if (catLinks.Count == 0) + { + result[catId] = new CategoryScopeData + { + Region = EmptyDisplay, + Location = EmptyDisplay, + RegionIds = new List(), + LocationIds = new List() + }; + continue; + } + + var regions = new HashSet(StringComparer.OrdinalIgnoreCase); + var locationNames = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var link in catLinks) + { + var lid = link.LocationId?.Trim() ?? string.Empty; + if (string.IsNullOrEmpty(lid) || !locById.TryGetValue(lid, out var loc)) + { + continue; + } + + var groupName = loc.GroupName?.Trim(); + if (!string.IsNullOrEmpty(groupName)) + { + regions.Add(groupName); + } + + var locName = loc.LocationName?.Trim(); + if (string.IsNullOrEmpty(locName)) + { + locName = loc.LocationCode?.Trim(); + } + + if (!string.IsNullOrEmpty(locName)) + { + locationNames.Add(locName); + } + } + + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIds); + + result[catId] = new CategoryScopeData + { + Region = regions.Count > 0 + ? string.Join(", ", regions.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + : EmptyDisplay, + Location = locationNames.Count > 0 + ? string.Join(", ", locationNames.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + : EmptyDisplay, + RegionIds = regionIds, + LocationIds = locationIds + }; + } + + return result; + } + + private sealed class CategoryScopeData + { + public string Region { get; init; } = string.Empty; + public string Location { get; init; } = string.Empty; + public List RegionIds { get; init; } = new(); + public List LocationIds { get; init; } = new(); + } + private static PagedResultWithPageDto BuildPagedResult(int skipCount, int maxResultCount, int total, List items) { var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs index 922c4f5..b2f7950 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs @@ -23,6 +23,7 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService { private readonly ISqlSugarDbContext _dbContext; private readonly ISqlSugarRepository _roleRepository; + private readonly ISqlSugarRepository _menuRepository; private readonly ISqlSugarRepository _roleMenuRepository; private readonly ISqlSugarRepository _roleDeptRepository; private readonly ISqlSugarRepository _userRoleRepository; @@ -30,12 +31,14 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService public RbacRoleAppService( ISqlSugarDbContext dbContext, ISqlSugarRepository roleRepository, + ISqlSugarRepository menuRepository, ISqlSugarRepository roleMenuRepository, ISqlSugarRepository roleDeptRepository, ISqlSugarRepository userRoleRepository) { _dbContext = dbContext; _roleRepository = roleRepository; + _menuRepository = menuRepository; _roleMenuRepository = roleMenuRepository; _roleDeptRepository = roleDeptRepository; _userRoleRepository = userRoleRepository; @@ -75,6 +78,8 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService AccessPermissionCodes = DeserializeAccessPermissionCodes(x.AccessPermissionCodesJson) }).ToList(); + await FillAccessPermissionsAsync(items); + var pageSize = input.MaxResultCount <= 0 ? items.Count : input.MaxResultCount; var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize); @@ -103,7 +108,7 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService .Select(x => x.MenuId) .ToListAsync(); - return new RbacRoleGetOutputDto + var dto = new RbacRoleGetOutputDto { Id = entity.Id, RoleName = entity.RoleName ?? string.Empty, @@ -115,9 +120,12 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService AccessPermissionCodes = DeserializeAccessPermissionCodes(entity.AccessPermissionCodesJson), MenuIds = menuIds }; + await FillAccessPermissionsAsync(new List { dto }); + return dto; } /// + [UnitOfWork] public async Task CreateAsync([FromBody] RbacRoleCreateInputVo input) { var roleName = input.RoleName?.Trim(); @@ -145,17 +153,19 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService Remark = input.Remark?.Trim(), DataScope = (Yi.Framework.Rbac.Domain.Shared.Enums.DataScopeEnum)input.DataScope, State = input.State, - OrderNum = input.OrderNum ?? 0, - AccessPermissionCodesJson = SerializeAccessPermissionCodes(ResolveAccessPermissions(input)) + OrderNum = input.OrderNum ?? 0 }; EntityHelper.TrySetId(entity, () => GuidGenerator.Create()); await _roleRepository.InsertAsync(entity); + await ApplyRoleMenuBindingsAsync(entity.Id, input); + return await GetAsync(entity.Id); } /// + [UnitOfWork] public async Task UpdateAsync(Guid id, [FromBody] RbacRoleUpdateInputVo input) { var entity = await _roleRepository.GetSingleAsync(x => x.Id == id && x.IsDeleted == false); @@ -189,15 +199,157 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService entity.Remark = input.Remark?.Trim(); entity.DataScope = (Yi.Framework.Rbac.Domain.Shared.Enums.DataScopeEnum)input.DataScope; entity.State = input.State; - entity.OrderNum = input.OrderNum ?? 0; - entity.AccessPermissionCodesJson = - SerializeAccessPermissionCodes(ResolveAccessPermissions(input)); + if (input.OrderNum is not null) + { + entity.OrderNum = input.OrderNum.Value; + } await _roleRepository.UpdateAsync(entity); + await ApplyRoleMenuBindingsAsync(entity.Id, input); + return await GetAsync(entity.Id); } + /// + /// 新增/编辑时按 menuIds 或 accessPermissions 绑定角色菜单(二者都未传则不改绑定) + /// + private async Task ApplyRoleMenuBindingsAsync(Guid roleId, RbacRoleCreateInputVo input) + { + if (input.MenuIds is not null) + { + await SetRoleMenusAsync(roleId, input.MenuIds); + return; + } + + if (input.AccessPermissions is not null) + { + var menuIds = await ResolveMenuIdsFromAccessPermissionsAsync(input.AccessPermissions); + await SetRoleMenusAsync(roleId, menuIds); + } + } + + private async Task SetRoleMenusAsync(Guid roleId, List menuIds) + { + var distinct = menuIds?.Distinct().ToList() ?? new List(); + await _roleMenuRepository.DeleteAsync(x => x.RoleId == roleId); + + if (distinct.Count == 0) + { + return; + } + + var existMenuIds = await _menuRepository._DbQueryable + .Where(x => x.IsDeleted == false) + .Where(x => distinct.Contains(x.Id)) + .Select(x => x.Id) + .ToListAsync(); + + if (existMenuIds.Count == 0) + { + return; + } + + var entities = existMenuIds.Select(menuId => new RoleMenuEntity + { + RoleId = roleId, + MenuId = menuId + }).ToList(); + + await _roleMenuRepository.InsertRangeAsync(entities); + } + + private async Task> ResolveMenuIdsFromAccessPermissionsAsync(string accessPermissions) + { + var codes = accessPermissions + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(c => !string.IsNullOrWhiteSpace(c)) + .Distinct(StringComparer.Ordinal) + .ToList(); + + if (codes.Count == 0) + { + return new List(); + } + + var menus = await _menuRepository._DbQueryable + .Where(m => m.IsDeleted == false && m.PermissionCode != null) + .Where(m => codes.Contains(m.PermissionCode!)) + .Select(m => m.Id) + .ToListAsync(); + + return menus; + } + + private async Task FillAccessPermissionsAsync(List items) + { + if (items.Count == 0) + { + return; + } + + var map = await GetAccessPermissionsByRoleIdsAsync(items.Select(x => x.Id).ToList()); + foreach (var item in items) + { + item.AccessPermissions = map.GetValueOrDefault(item.Id, string.Empty); + } + } + + /// + /// 按角色汇总已绑定菜单上的 PermissionCode(去重、英文逗号+空格拼接) + /// + private async Task> GetAccessPermissionsByRoleIdsAsync(List roleIds) + { + var result = roleIds.Distinct().ToDictionary(id => id, _ => string.Empty); + if (result.Count == 0) + { + return result; + } + + var distinctRoleIds = result.Keys.ToList(); + var links = await _roleMenuRepository._DbQueryable + .Where(rm => distinctRoleIds.Contains(rm.RoleId)) + .Select(rm => new { rm.RoleId, rm.MenuId }) + .ToListAsync(); + if (links.Count == 0) + { + return result; + } + + var menuIds = links.Select(x => x.MenuId).Distinct().ToList(); + var menus = await _menuRepository._DbQueryable + .Where(m => menuIds.Contains(m.Id) && m.IsDeleted == false) + .Select(m => new { m.Id, m.PermissionCode }) + .ToListAsync(); + var permByMenuId = menus.ToDictionary(x => x.Id, x => x.PermissionCode); + + var byRole = distinctRoleIds.ToDictionary(id => id, _ => new HashSet(StringComparer.Ordinal)); + foreach (var link in links) + { + if (!permByMenuId.TryGetValue(link.MenuId, out var code) || string.IsNullOrWhiteSpace(code)) + { + continue; + } + + if (byRole.TryGetValue(link.RoleId, out var set)) + { + set.Add(code.Trim()); + } + } + + foreach (var kv in byRole) + { + if (kv.Value.Count == 0) + { + continue; + } + + result[kv.Key] = string.Join(", ", kv.Value.OrderBy(x => x, StringComparer.Ordinal)); + } + + return result; + } + /// [UnitOfWork] public async Task DeleteAsync([FromBody] List ids) diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs index 9b9b25d..5f35724 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs @@ -106,6 +106,11 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService var role = await _dbContext.SqlSugarClient.Queryable().FirstAsync(x => x.UserId == id); + var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIds); + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIds); + return new TeamMemberGetOutputDto { Id = user.Id, @@ -115,6 +120,9 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService Phone = user.Phone, State = user.State, RoleId = role?.RoleId, + PartnerIds = partnerIds, + RegionIds = regionIds, + GroupIds = regionIds, LocationIds = locationIds, AssignedLocations = assigned }; @@ -123,10 +131,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService /// public async Task CreateAsync(TeamMemberCreateInputVo input) { - if (input.LocationIds is null || input.LocationIds.Count == 0) - { - throw new UserFriendlyException("成员必须至少分配一个门店"); - } + var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input); var user = new UserAggregateRoot(input.UserName.Trim(), input.Password, input.Phone, input.FullName.Trim()) { @@ -145,7 +150,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService await _userManager.GiveUserSetRoleAsync(new List { user.Id }, new List { input.RoleId.Value }); } - await UpsertUserLocationsAsync(user.Id, input.LocationIds); + await UpsertUserLocationsAsync(user.Id, mergedLocationIds); return await GetAsync(user.Id); } @@ -153,10 +158,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService /// public async Task UpdateAsync(Guid id, TeamMemberUpdateInputVo input) { - if (input.LocationIds is null || input.LocationIds.Count == 0) - { - throw new UserFriendlyException("成员必须至少分配一个门店"); - } + var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input); var user = await _userRepository.GetByIdAsync(id); if (user is null || user.IsDeleted) @@ -187,7 +189,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService await _userManager.GiveUserSetRoleAsync(new List { id }, new List()); } - await UpsertUserLocationsAsync(id, input.LocationIds); + await UpsertUserLocationsAsync(id, mergedLocationIds); return await GetAsync(id); } @@ -584,10 +586,13 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService return null; }).Where(x => x != null).Cast().ToList()); + var scopeIdsMap = await BuildTeamMemberScopeIdsMapAsync(assignedMap); + return users.Select(u => { roleMap.TryGetValue(u.Id, out var role); assignedMap.TryGetValue(u.Id.ToString(), out var assigned); + scopeIdsMap.TryGetValue(u.Id.ToString(), out var scopeIds); return new TeamMemberGetListOutputDto { @@ -599,11 +604,109 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService State = u.State, RoleId = role?.Id, RoleName = role?.RoleName, + PartnerIds = scopeIds?.PartnerIds ?? new List(), + RegionIds = scopeIds?.RegionIds ?? new List(), AssignedLocations = assigned ?? new List() }; }).ToList(); } + private async Task> BuildTeamMemberScopeIdsMapAsync( + Dictionary> assignedMap) + { + var result = new Dictionary(StringComparer.Ordinal); + foreach (var (userId, assigned) in assignedMap) + { + var locationIds = assigned + .Select(x => x.Id) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .Distinct(StringComparer.Ordinal) + .ToList(); + if (locationIds.Count == 0) + { + result[userId] = new TeamMemberScopeIds(); + continue; + } + + var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIds); + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIds); + result[userId] = new TeamMemberScopeIds + { + PartnerIds = partnerIds, + RegionIds = regionIds + }; + } + + return result; + } + + private sealed class TeamMemberScopeIds + { + public List PartnerIds { get; init; } = new(); + public List RegionIds { get; init; } = new(); + } + + private Task> ResolveTeamMemberLocationIdsForSaveAsync(TeamMemberUpdateInputVo input) => + ResolveTeamMemberLocationIdsForSaveAsync(new TeamMemberCreateInputVo + { + PartnerId = input.PartnerId, + PartnerIds = input.PartnerIds, + RegionIds = input.RegionIds, + GroupIds = input.GroupIds, + LocationIds = input.LocationIds + }); + + private async Task> ResolveTeamMemberLocationIdsForSaveAsync(TeamMemberCreateInputVo input) + { + var partnerIds = NormalizePartnerIds(input); + var regionIds = NormalizeRegionIds(input); + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( + _dbContext.SqlSugarClient, partnerIds, regionIds, input.LocationIds); + + if (merged.Count == 0) + { + throw new UserFriendlyException("成员必须至少分配一个门店(公司/区域/门店至少选一项)"); + } + + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); + return merged; + } + + private static List NormalizePartnerIds(TeamMemberCreateInputVo input) + { + var merged = new HashSet(StringComparer.Ordinal); + if (!string.IsNullOrWhiteSpace(input.PartnerId)) + { + merged.Add(input.PartnerId.Trim()); + } + + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.PartnerIds)) + { + merged.Add(id); + } + + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); + } + + private static List NormalizeRegionIds(TeamMemberCreateInputVo input) + { + var merged = new HashSet(StringComparer.Ordinal); + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds)) + { + merged.Add(id); + } + + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds)) + { + merged.Add(id); + } + + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); + } + private async Task UpsertUserLocationsAsync(Guid userId, List locationIds) { var now = DateTime.Now; 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 1a8fd09..53aeea3 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 @@ -15,6 +15,7 @@ using Lazy.Captcha.Core; using Mapster; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using SqlSugar; @@ -25,6 +26,8 @@ using Volo.Abp.Security.Claims; using Volo.Abp.Uow; using Volo.Abp.Users; using Yi.Framework.Core.Helper; +using Yi.Framework.Rbac.Application.Contracts.Dtos.Account; +using Yi.Framework.Rbac.Application.Contracts.IServices; using Yi.Framework.Rbac.Domain.Entities; using Yi.Framework.Rbac.Domain.Managers; using Yi.Framework.Rbac.Domain.Shared.Consts; @@ -47,6 +50,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService private readonly ICaptcha _captcha; private readonly RbacOptions _rbacOptions; private readonly JwtOptions _jwtOptions; + private readonly IForgotPasswordByEmailService _forgotPasswordByEmailService; public UsAppAuthAppService( IAccountManager accountManager, @@ -55,7 +59,8 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService IHttpContextAccessor httpContextAccessor, ICaptcha captcha, IOptions jwtOptions, - IOptions rbacOptions) + IOptions rbacOptions, + IForgotPasswordByEmailService forgotPasswordByEmailService) { _accountManager = accountManager; _userRepository = userRepository; @@ -64,6 +69,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService _captcha = captcha; _jwtOptions = jwtOptions.Value; _rbacOptions = rbacOptions.Value; + _forgotPasswordByEmailService = forgotPasswordByEmailService; } protected ILocalEventBus LocalEventBus => LazyServiceProvider.LazyGetRequiredService(); @@ -124,6 +130,19 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService }; } + /// + [AllowAnonymous] + [HttpPost("us-app-auth/forgot-password/email/send-code")] + public virtual Task PostSendForgotPasswordCodeByEmailAsync(EmailCaptchaImageDto input) => + _forgotPasswordByEmailService.SendForgotPasswordCodeAsync(input); + + /// + [AllowAnonymous] + [UnitOfWork] + [HttpPost("us-app-auth/forgot-password/email/reset")] + public virtual Task PostResetPasswordByEmailAsync(RetrievePasswordByEmailDto input) => + _forgotPasswordByEmailService.ResetPasswordByEmailAsync(input); + /// /// 获取当前登录用户已绑定的门店(切换门店时可重新拉取) /// @@ -147,7 +166,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService /// 店长:在同店绑定用户中,取 Role.RoleCodeRole.RoleName(忽略大小写)包含 manager 的第一条; /// 若无匹配则店长姓名与电话均为「无」。 /// - /// OperatingHours:当前 location 表无营业时间字段,固定返回「无」。 + /// OperatingHours:读取 location.OperatingHours;为空时返回「无」。 /// /// 门店主键(Guid 字符串) /// 与原型一致的展示字段 @@ -193,7 +212,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService LocationName = string.IsNullOrWhiteSpace(loc.LocationName) ? "无" : loc.LocationName.Trim(), FullAddress = BuildFullAddress(loc), StorePhone = FormatStorePhoneDisplay(loc.Phone), - OperatingHours = "无", + OperatingHours = string.IsNullOrWhiteSpace(loc.OperatingHours) ? "无" : loc.OperatingHours.Trim(), ManagerName = mgrName, ManagerPhone = mgrPhone }; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Domain/Entities/LocationAggregateRoot.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Domain/Entities/LocationAggregateRoot.cs index 793d3e1..cbb177e 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Domain/Entities/LocationAggregateRoot.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Domain/Entities/LocationAggregateRoot.cs @@ -130,5 +130,11 @@ public class LocationAggregateRoot : AggregateRoot, ISoftDelete, IAuditedO /// Longitude /// public decimal? Longitude { get; set; } + + /// + /// 经营时间(自由文本,如 Mon–Fri 9:00 AM – 6:00 PM) + /// + [SugarColumn(ColumnName = "OperatingHours")] + public string? OperatingHours { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql index e4dee86..0efde28 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql @@ -11,6 +11,11 @@ CREATE TABLE IF NOT EXISTS `fl_partner` ( `PartnerName` varchar(256) NOT NULL COMMENT '合作伙伴名称', `ContactEmail` varchar(256) DEFAULT NULL COMMENT '联系邮箱', `PhoneNumber` varchar(64) DEFAULT NULL COMMENT '电话', + `Street` varchar(512) DEFAULT NULL COMMENT '街道地址', + `City` varchar(128) DEFAULT NULL COMMENT '城市', + `StateCode` varchar(32) DEFAULT NULL COMMENT '州/省代码(如 NY)', + `Country` varchar(128) DEFAULT NULL COMMENT '国家', + `ZipCode` varchar(32) DEFAULT NULL COMMENT '邮编', `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', PRIMARY KEY (`Id`), KEY `IX_fl_partner_IsDeleted` (`IsDeleted`), diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Role/RoleGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Role/RoleGetListOutputDto.cs index ec632a8..366a089 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Role/RoleGetListOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Role/RoleGetListOutputDto.cs @@ -14,5 +14,10 @@ namespace Yi.Framework.Rbac.Application.Contracts.Dtos.Role public bool State { get; set; } public int OrderNum { get; set; } + + /// + /// 访问权限:该角色在已绑定菜单上的 PermissionCode 汇总(去重、逗号+空格拼接;无则为空串) + /// + public string AccessPermissions { get; set; } = string.Empty; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Role/RoleGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Role/RoleGetOutputDto.cs index 923af32..822f050 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Role/RoleGetOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/Dtos/Role/RoleGetOutputDto.cs @@ -14,5 +14,10 @@ namespace Yi.Framework.Rbac.Application.Contracts.Dtos.Role public bool State { get; set; } public int OrderNum { get; set; } + + /// + /// 访问权限:该角色在已绑定菜单上的 PermissionCode 汇总(去重、逗号+空格拼接;无则为空串) + /// + public string AccessPermissions { get; set; } = string.Empty; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs index 2b58f5a..edad015 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs @@ -37,6 +37,7 @@ namespace Yi.Framework.Rbac.Application.Services private readonly IGuidGenerator _guidGenerator; private readonly RbacOptions _rbacOptions; private readonly IAliyunManger _aliyunManger; + private readonly IForgotPasswordByEmailService _forgotPasswordByEmailService; private IDistributedCache _userCache; private UserManager _userManager; private IHttpContextAccessor _httpContextAccessor; @@ -51,6 +52,7 @@ namespace Yi.Framework.Rbac.Application.Services IGuidGenerator guidGenerator, IOptions options, IAliyunManger aliyunManger, + IForgotPasswordByEmailService forgotPasswordByEmailService, UserManager userManager, IHttpContextAccessor httpContextAccessor) { _userRepository = userRepository; @@ -62,6 +64,7 @@ namespace Yi.Framework.Rbac.Application.Services _guidGenerator = guidGenerator; _rbacOptions = options.Value; _aliyunManger = aliyunManger; + _forgotPasswordByEmailService = forgotPasswordByEmailService; _userCache = userCache; _userManager = userManager; _httpContextAccessor = httpContextAccessor; @@ -328,6 +331,58 @@ namespace Yi.Framework.Rbac.Application.Services return entity.UserName; } + /// + /// Send a forgot-password verification code to the account email (platform). + /// + /// + /// When RbacOptions.EnableCaptcha is true, pass Uuid and Code from GetCaptchaImageAsync. + /// If the email is not registered, the API still returns 200 without sending mail (anti-enumeration). + /// + /// Example request: + /// ```json + /// { + /// "email": "user@example.com", + /// "uuid": "optional-captcha-uuid", + /// "code": "optional-captcha-text" + /// } + /// ``` + /// + /// Email and optional image captcha fields. + /// No payload when accepted. + /// Accepted (email may or may not exist). + /// Invalid input, captcha, rate limit, or SMTP not configured. + /// Server error + [AllowAnonymous] + [HttpPost("account/forgot-password/email/send-code")] + public virtual Task PostSendForgotPasswordCodeByEmailAsync(EmailCaptchaImageDto input) => + _forgotPasswordByEmailService.SendForgotPasswordCodeAsync(input); + + /// + /// Reset password using the email verification code (platform). + /// + /// + /// New password rules: at least 8 characters; upper and lower case; digit; special character. + /// + /// Example request: + /// ```json + /// { + /// "email": "user@example.com", + /// "code": "ABCDEF", + /// "password": "NewPassw0rd!" + /// } + /// ``` + /// + /// Email, OTP, and new password. + /// Matched account user name. + /// Password updated. + /// Invalid OTP, weak password, or account not found. + /// Server error + [AllowAnonymous] + [UnitOfWork] + [HttpPost("account/forgot-password/email/reset")] + public virtual Task PostResetPasswordByEmailAsync(RetrievePasswordByEmailDto input) => + _forgotPasswordByEmailService.ResetPasswordByEmailAsync(input); + /// /// 注册,需要验证码通过 diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/System/RoleService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/System/RoleService.cs index 5ea670d..0c28e33 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/System/RoleService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/System/RoleService.cs @@ -19,6 +19,7 @@ using Yi.Framework.Rbac.Domain.Managers; using Yi.Framework.Rbac.Domain.Shared.Consts; using Yi.Framework.Rbac.Domain.Shared.Enums; using Yi.Framework.SqlSugarCore.Abstractions; +using System.Linq; namespace Yi.Framework.Rbac.Application.Services.System { @@ -75,7 +76,17 @@ namespace Yi.Framework.Rbac.Application.Services.System .WhereIF(!string.IsNullOrEmpty(input.RoleName), x => x.RoleName.Contains(input.RoleName!)) .WhereIF(input.State is not null, x => x.State == input.State) .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); - return new PagedResultDto(total, await MapToGetListOutputDtosAsync(entities)); + var list = await MapToGetListOutputDtosAsync(entities); + await FillAccessPermissionsForRoleListAsync(list); + return new PagedResultDto(total, list); + } + + /// + public override async Task GetAsync(Guid id) + { + var dto = await base.GetAsync(id); + await FillAccessPermissionsForRoleGetAsync(dto); + return dto; } /// @@ -165,9 +176,7 @@ namespace Yi.Framework.Rbac.Application.Services.System var entity = await MapToEntityAsync(input); await _repository.InsertAsync(entity); - var outputDto = await MapToGetOutputDtoAsync(entity); - - return outputDto; + return await MapToGetOutputDtoWithPermissionsAsync(entity); } /// @@ -191,8 +200,7 @@ namespace Yi.Framework.Rbac.Application.Services.System await _roleManager.GiveRoleSetMenuAsync(new List { id }, input.MenuIds); - var dto = await MapToGetOutputDtoAsync(entity); - return dto; + return await MapToGetOutputDtoWithPermissionsAsync(entity); } @@ -213,7 +221,7 @@ namespace Yi.Framework.Rbac.Application.Services.System entity.State = state; await _repository.UpdateAsync(entity); - return await MapToGetOutputDtoAsync(entity); + return await MapToGetOutputDtoWithPermissionsAsync(entity); } @@ -337,5 +345,92 @@ namespace Yi.Framework.Rbac.Application.Services.System depts = deptList.DeptTreeBuild() }); } + + private async Task MapToGetOutputDtoWithPermissionsAsync(RoleAggregateRoot entity) + { + var dto = await MapToGetOutputDtoAsync(entity); + await FillAccessPermissionsForRoleGetAsync(dto); + return dto; + } + + private async Task FillAccessPermissionsForRoleListAsync(List items) + { + if (items.Count == 0) + { + return; + } + + var map = await GetAccessPermissionsByRoleIdsAsync(items.Select(x => x.Id).ToList()); + foreach (var item in items) + { + item.AccessPermissions = map.GetValueOrDefault(item.Id, string.Empty); + } + } + + private async Task FillAccessPermissionsForRoleGetAsync(RoleGetOutputDto dto) + { + var map = await GetAccessPermissionsByRoleIdsAsync(new List { dto.Id }); + dto.AccessPermissions = map.GetValueOrDefault(dto.Id, string.Empty); + } + + /// + /// 按角色汇总已绑定菜单上的 PermissionCode(去重、英文逗号+空格拼接) + /// + private async Task> GetAccessPermissionsByRoleIdsAsync(List roleIds) + { + var result = roleIds.Distinct().ToDictionary(id => id, _ => string.Empty); + if (result.Count == 0) + { + return result; + } + + var distinctRoleIds = result.Keys.ToList(); + var links = await _menuRepository._Db.Queryable() + .Where(rm => distinctRoleIds.Contains(rm.RoleId)) + .Select(rm => new { rm.RoleId, rm.MenuId }) + .ToListAsync(); + if (links.Count == 0) + { + return result; + } + + var menuIds = links.Select(x => x.MenuId).Distinct().ToList(); + var menus = await _menuRepository._DbQueryable + .Where(m => menuIds.Contains(m.Id) && m.IsDeleted == false) + .Select(m => new { m.Id, m.PermissionCode }) + .ToListAsync(); + var permByMenuId = menus.ToDictionary(x => x.Id, x => x.PermissionCode); + + var byRole = new Dictionary>(); + foreach (var rid in distinctRoleIds) + { + byRole[rid] = new HashSet(StringComparer.Ordinal); + } + + foreach (var link in links) + { + if (!permByMenuId.TryGetValue(link.MenuId, out var code) || string.IsNullOrWhiteSpace(code)) + { + continue; + } + + if (byRole.TryGetValue(link.RoleId, out var set)) + { + set.Add(code.Trim()); + } + } + + foreach (var kv in byRole) + { + if (kv.Value.Count == 0) + { + continue; + } + + result[kv.Key] = string.Join(", ", kv.Value.OrderBy(x => x, StringComparer.Ordinal)); + } + + return result; + } } } \ No newline at end of file diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Yi.Framework.Rbac.Application.csproj b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Yi.Framework.Rbac.Application.csproj index ac40ebe..b51c682 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Yi.Framework.Rbac.Application.csproj +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Yi.Framework.Rbac.Application.csproj @@ -8,6 +8,7 @@ + diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/YiFrameworkRbacDomainSharedModule.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/YiFrameworkRbacDomainSharedModule.cs index 38702b3..84b4928 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/YiFrameworkRbacDomainSharedModule.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/YiFrameworkRbacDomainSharedModule.cs @@ -18,6 +18,8 @@ namespace Yi.Framework.Rbac.Domain.Shared Configure(configuration.GetSection(nameof(JwtOptions))); Configure(configuration.GetSection(nameof(RefreshJwtOptions))); Configure(configuration.GetSection(nameof(RbacOptions))); + Configure( + configuration.GetSection(ForgotPasswordEmailOptions.SectionName)); } } } \ No newline at end of file diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj b/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj index 3c09fb7..eda99de 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj @@ -8,6 +8,8 @@ + + @@ -67,4 +69,11 @@ + + + + + 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 633703c..6deded6 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 @@ -18,10 +18,10 @@ "Microsoft.AspNetCore": "Warning" } }, - //应用启动 + //应用启动:SelfUrl 供 Program.cs UseUrls 绑定;用 0.0.0.0 避免写成固定局域网 IP 在本机无该网卡时启动失败(WinError 10049) "App": { "SelfUrl": "http://localhost:19001", - "CorsOrigins": "http://localhost:19001;http://localhost:18000;http://localhost:5666" + "CorsOrigins": "http://localhost:19001;http://localhost:18000;http://localhost:5666;http://localhost:3000" }, //配置 "Settings": { @@ -104,11 +104,39 @@ //开启定时数据库备份 "EnableDataBaseBackup": false }, + + // Forgot password: Microsoft 365 may return 535 until SMTP AUTH is enabled for the mailbox + // (Exchange admin) and credentials are correct; use an app password if MFA is on. + "ForgotPasswordEmail": { + "IsEnabled": true, + "SmtpHost": "smtp.office365.com", + "SmtpPort": 587, + "SecureSocketPreset": "StartTls", + "UserName": "Sandi.ma@3ffoodsafety.com", + "Password": "", + "FromAddress": "Sandi.ma@3ffoodsafety.com", + "FromDisplayName": "Food Labeling", + "CodeLength": 6, + "CacheExpirationMinutes": 10 + }, //语义内核 "SemanticKernel": { "ModelIds": ["gpt-4o"], "Endpoint": "https://xxx.com/v1", "ApiKey": "sk-xxxxxx" + }, + + "FoodLabeling": { + "BatchImport": { + "TemplateDirectory": "/www/wwwroot/FoodLabelingManagementUs/batchImportOfFiles", + "LocationTemplateFileName": "Location-Manager-批量导入模板.xlsx", + "TeamMemberTemplateFileName": "Team-Member-批量导入模板.xlsx", + "ProductTemplateFileName": "Product-Manager-批量导入模板.xlsx", + "TeamMemberImportDefaultPassword": "ChangeMe123!", + "MaxImportRows": 5000, + "MaxUploadBytes": 10485760, + "MaxBulkUpdateItems": 500 + } } } diff --git a/美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx b/美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx index 1703883..cfa55d6 100755 --- a/美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx +++ b/美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx @@ -1819,7 +1819,7 @@ function RoleDialog({ } setSubmitting(true); try { - const payload = { + const payload: RbacRoleUpsertInput = { roleName: roleName.trim(), roleCode: roleCode.trim(), remark: remark.trim() ? remark.trim() : null, diff --git a/美国版/Food Labeling Management Platform/src/types/authSession.ts b/美国版/Food Labeling Management Platform/src/types/authSession.ts index ceeb570..8b637fb 100644 --- a/美国版/Food Labeling Management Platform/src/types/authSession.ts +++ b/美国版/Food Labeling Management Platform/src/types/authSession.ts @@ -27,5 +27,11 @@ export type CurrentUserMenuPermissionsOutputDto = { roleCodes: string[]; permissionCodes: string[]; menus: CurrentUserMenuNodeDto[]; + /** User.LastModificationTime(ISO 字符串或 null) */ + lastUpdated?: string | null; + /** 角色展示名,多角色英文逗号拼接 */ + role?: string; + /** 全名:姓名 > 昵称 > 用户名 */ + fullName?: string; }; diff --git a/项目相关文档/批量导入导出接口说明.md b/项目相关文档/批量导入导出接口说明.md index 3016f32..83e9853 100644 --- a/项目相关文档/批量导入导出接口说明.md +++ b/项目相关文档/批量导入导出接口说明.md @@ -224,12 +224,14 @@ **列表筛选字段**(导出与列表对齐):`Keyword`、`State`、`Sorting` — 与产品分页列表一致(`Keyword` 匹配产品编码、名称、分类名称)。 +**路由说明**:单条 **GET/PUT/DELETE** 的 `{id}` 为 **`Guid` 类型**(与 `fl_product.Id` 的 Guid 字符串一致),约定路由会带 **`{id:guid}`** 约束,因此 **`export-products-excel`、`download-product-import-template`、`import-products-batch`、`update-products-bulk`** 等字面路径不会被误判为产品 Id。 + ### 3.1 下载批量导入模板 | 项目 | 说明 | |------|------| | 方法 | `DownloadProductImportTemplateAsync` | -| HTTP | `GET` | +| HTTP | `GET`(已标 `[HttpGet]`,与约定 `Download*` 可能判为 `POST` 的情况区分) | | 常见路径 | `/api/app/product/download-product-import-template` | | 作用 | 从 `TemplateDirectory` 读取 `ProductTemplateFileName` 指向的 xlsx 并下载(工作表名一般为 `Products`) | @@ -238,14 +240,17 @@ | 项目 | 说明 | |------|------| | 方法 | `ExportProductsExcelAsync` | -| HTTP | `GET` | +| HTTP | `GET`(应用服务方法上已标 `[HttpGet]`;ABP 对 `Export*` 默认易判成 `POST`,用 GET 否则会 **405**) | | 常见路径 | `/api/app/product/export-products-excel` | +| Postman | **不要** 填 Body;筛选用 **Params**(Query)或拼在 URL 上;导出**不需要**上传 `file`(上传文件是 **3.3 导入**) | | Query | 与产品列表筛选一致:`Keyword`、`State`、`Sorting` | | 数据范围 | **全量**:符合筛选条件的全部产品;**不使用** `SkipCount` / `MaxResultCount` | | 排序 | 有 `Sorting` 则按其排序,否则默认按 `ProductName` 降序 | | 响应文件名示例 | `products-export-yyyyMMdd-HHmmss.xlsx` | | 列(与导入模板一致) | **Location**(多门店英文逗号拼接门店名称)、**Product Category**(分类名称)、**Product**(产品名称)、**Product Code**(产品编码;可为空则导出为空单元格) | +**部署**:须使用 **`dotnet publish` 后的完整输出目录**(含 `ClosedXML.dll`、`DocumentFormat.OpenXml.dll`、`Yi.Abp.Web.deps.json` 等)部署;**不要用 `bin/Debug`(或 `bin/Release`)里挑文件当上线包**。也**禁止**「本地跑起来后只把若干 dll / 自己改过的文件」覆盖到线上(极易漏掉第三方依赖)。上线建议整包替换发布目录,或在服务器上 `git pull` + `dotnet publish`;发布时若输出里缺少 `ClosedXML.dll`,当前 `Yi.Abp.Web` 工程会在 `dotnet publish` 结束时报错提示。 + ### 3.3 批量导入 Excel | 项目 | 说明 | @@ -289,7 +294,7 @@ ## 5 Account Management(Company / Region) -前端 **Account Management** 菜单中 **Company**、**Region** 两个页签的「Bulk Export (PDF)」对应后端已有接口:数据模型分别为 **`fl_partner`**(合作伙伴 / 公司)、**`fl_group`**(组织 / 大区);应用服务为 **`PartnerAppService`**、**`GroupAppService`**。导出为 **QuestPDF** 生成的 **PDF**,筛选条件与各自**分页列表**一致,**不使用** `SkipCount` / `MaxResultCount`(全量导出)。单次导出超过 **5000** 条时返回业务错误,需缩小筛选范围。 +前端 **Account Management** 菜单中 **Company**、**Region** 两个页签的「Bulk Export (PDF)」对应后端已有接口:数据模型分别为 **`fl_partner`**(合作伙伴 / 公司)、**`fl_group`**(组织 / 大区);应用服务为 **`PartnerAppService`**、**`GroupAppService`**。导出为 **QuestPDF** 生成的 **PDF**,筛选条件与各自**分页列表**一致,**不使用** `SkipCount` / `MaxResultCount`(全量导出)。单次导出超过 **5000** 条时返回业务错误,需缩小筛选范围。单条 **GET/PUT/DELETE** 的 `{id}` 为 **`Guid`**(与 `fl_partner.Id` / `fl_group.Id` 的 Guid 字符串一致),约定路由 **`{id:guid}`**,**`export-pdf`** 等字面路径不会误判为资源 Id;`ExportPdfAsync` 已标 **`[HttpGet]`**。 ### 5.1 Company(合作伙伴 / `PartnerAppService`)