Commit 1aa1dca91e51af53c9afce2216a23c34024347c2
合并
Showing
64 changed files
with
1990 additions
and
351 deletions
.codex/project/project-context.md
| @@ -66,3 +66,15 @@ | @@ -66,3 +66,15 @@ | ||
| 66 | - `打印机SDK/`、`nativeplugins/`、`scripts/`、`unpackage/` 等目录 | 66 | - `打印机SDK/`、`nativeplugins/`、`scripts/`、`unpackage/` 等目录 |
| 67 | 67 | ||
| 68 | 后续修改时应避免误覆盖这些现有变更。 | 68 | 后续修改时应避免误覆盖这些现有变更。 |
| 69 | + | ||
| 70 | +## 7. 美国版业务字段命名(固定映射) | ||
| 71 | + | ||
| 72 | +开发与文档中统一使用下列对应关系,**不要**在 `location` 表再新增名为 `Region` 的列: | ||
| 73 | + | ||
| 74 | +| 界面 / 接口英文 | 中文 | 主数据表 | 门店表 `location` 字段 | | ||
| 75 | +|-----------------|------|----------|------------------------| | ||
| 76 | +| Company / Partner | 公司 | `fl_partner` | `Partner`(公司名称字符串) | | ||
| 77 | +| **Region / Group** | **区域 / 组织** | **`fl_group`**(`GroupName`) | **`GroupName`** | | ||
| 78 | +| Location | 门店 | `location` | `LocationCode` / `LocationName` | | ||
| 79 | + | ||
| 80 | +**硬性规则**:**Region 对应 `location.GroupName`**(与 `fl_group.GroupName` 通过 `Partner` + 名称匹配)。列表/API 出参字段名可用 `region`,落库与查询一律用 `GroupName`。 |
美国版/Food Labeling Management Code/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/Mvc/YiConventionalRouteBuilder.cs
| @@ -78,6 +78,13 @@ namespace Yi.Framework.AspNetCore.Mvc | @@ -78,6 +78,13 @@ namespace Yi.Framework.AspNetCore.Mvc | ||
| 78 | return baseUrl; | 78 | return baseUrl; |
| 79 | } | 79 | } |
| 80 | 80 | ||
| 81 | + // Guid 必须带路由约束,否则与 export-xxx、download-xxx 等字面路径冲突 | ||
| 82 | + var idClrType = Nullable.GetUnderlyingType(idParameter.ParameterType) ?? idParameter.ParameterType; | ||
| 83 | + if (idClrType == typeof(Guid)) | ||
| 84 | + { | ||
| 85 | + return $"{baseUrl}/{{id:guid}}"; | ||
| 86 | + } | ||
| 87 | + | ||
| 81 | // 处理原始类型ID | 88 | // 处理原始类型ID |
| 82 | if (TypeHelper.IsPrimitiveExtended(idParameter.ParameterType, includeEnums: true)) | 89 | if (TypeHelper.IsPrimitiveExtended(idParameter.ParameterType, includeEnums: true)) |
| 83 | { | 90 | { |
美国版/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 | @@ -12,4 +12,19 @@ public class CurrentUserMenuPermissionsOutputDto | ||
| 12 | public List<string> PermissionCodes { get; set; } = new(); | 12 | public List<string> PermissionCodes { get; set; } = new(); |
| 13 | 13 | ||
| 14 | public List<CurrentUserMenuNodeDto> Menus { get; set; } = new(); | 14 | public List<CurrentUserMenuNodeDto> Menus { get; set; } = new(); |
| 15 | + | ||
| 16 | + /// <summary> | ||
| 17 | + /// 用户资料最后更新时间(User.LastModificationTime,无则前端可忽略或展示「无」) | ||
| 18 | + /// </summary> | ||
| 19 | + public DateTime? LastUpdated { get; set; } | ||
| 20 | + | ||
| 21 | + /// <summary> | ||
| 22 | + /// 角色展示名(多角色英文逗号拼接;与 <see cref="RoleCodes"/> 对应的库中 RoleName) | ||
| 23 | + /// </summary> | ||
| 24 | + public string Role { get; set; } = string.Empty; | ||
| 25 | + | ||
| 26 | + /// <summary> | ||
| 27 | + /// 全名:优先姓名(User.Name),其次昵称(Nick),最后用户名 | ||
| 28 | + /// </summary> | ||
| 29 | + public string FullName { get; set; } = string.Empty; | ||
| 15 | } | 30 | } |
美国版/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 | @@ -8,7 +8,35 @@ public class LabelCreateInputVo | ||
| 8 | 8 | ||
| 9 | public string TemplateCode { get; set; } = string.Empty; | 9 | public string TemplateCode { get; set; } = string.Empty; |
| 10 | 10 | ||
| 11 | - public string LocationId { get; set; } = string.Empty; | 11 | + /// <summary> |
| 12 | + /// 适用 Company(<c>fl_partner.Id</c>);与 <see cref="PartnerIds"/> 合并,用于解析所属门店 | ||
| 13 | + /// </summary> | ||
| 14 | + public string? PartnerId { get; set; } | ||
| 15 | + | ||
| 16 | + /// <summary> | ||
| 17 | + /// 适用 Company 多选(<c>fl_partner.Id</c>) | ||
| 18 | + /// </summary> | ||
| 19 | + public List<string>? PartnerIds { get; set; } | ||
| 20 | + | ||
| 21 | + /// <summary> | ||
| 22 | + /// 适用 Region 多选(<c>fl_group.Id</c>);与 <see cref="GroupIds"/> 合并 | ||
| 23 | + /// </summary> | ||
| 24 | + public List<string>? RegionIds { get; set; } | ||
| 25 | + | ||
| 26 | + /// <summary> | ||
| 27 | + /// 适用 Region 多选(与 <see cref="RegionIds"/> 相同) | ||
| 28 | + /// </summary> | ||
| 29 | + public List<string>? GroupIds { get; set; } | ||
| 30 | + | ||
| 31 | + /// <summary> | ||
| 32 | + /// 所属门店(<c>location.Id</c>);可与 Company/Region 一并传入以校验范围;单独传则直接落库 | ||
| 33 | + /// </summary> | ||
| 34 | + public string? LocationId { get; set; } | ||
| 35 | + | ||
| 36 | + /// <summary> | ||
| 37 | + /// 门店候选(多选);标签仅绑定单门店:未传 <see cref="LocationId"/> 且合并结果唯一时自动取该项 | ||
| 38 | + /// </summary> | ||
| 39 | + public List<string>? LocationIds { get; set; } | ||
| 12 | 40 | ||
| 13 | public string LabelCategoryId { get; set; } = string.Empty; | 41 | public string LabelCategoryId { get; set; } = string.Empty; |
| 14 | 42 |
美国版/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 | @@ -10,7 +10,12 @@ public class LabelGetListInputVo : PagedAndSortedResultRequestDto | ||
| 10 | public string? Keyword { get; set; } | 10 | public string? Keyword { get; set; } |
| 11 | 11 | ||
| 12 | /// <summary> | 12 | /// <summary> |
| 13 | - /// 门店Id(可选) | 13 | + /// 组织/Region Id(<c>fl_group.Id</c>);筛选标签所属门店落在该 Region 下的记录 |
| 14 | + /// </summary> | ||
| 15 | + public string? GroupId { get; set; } | ||
| 16 | + | ||
| 17 | + /// <summary> | ||
| 18 | + /// 门店 Id(<c>location.Id</c>,Guid 字符串);最精确筛选,传则忽略 <see cref="GroupId"/> | ||
| 14 | /// </summary> | 19 | /// </summary> |
| 15 | public string? LocationId { get; set; } | 20 | public string? LocationId { get; set; } |
| 16 | 21 |
美国版/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 | @@ -10,6 +10,17 @@ public class LabelGetOutputDto | ||
| 10 | 10 | ||
| 11 | public string LocationName { get; set; } = string.Empty; | 11 | public string LocationName { get; set; } = string.Empty; |
| 12 | 12 | ||
| 13 | + /// <summary>适用 Company Id(由所属门店反推)</summary> | ||
| 14 | + public string? PartnerId { get; set; } | ||
| 15 | + | ||
| 16 | + public List<string> PartnerIds { get; set; } = new(); | ||
| 17 | + | ||
| 18 | + /// <summary>适用 Region Id(<c>fl_group.Id</c>)</summary> | ||
| 19 | + public List<string> RegionIds { get; set; } = new(); | ||
| 20 | + | ||
| 21 | + /// <summary>与 <see cref="RegionIds"/> 相同</summary> | ||
| 22 | + public List<string> GroupIds { get; set; } = new(); | ||
| 23 | + | ||
| 13 | public string LabelCategoryId { get; set; } = string.Empty; | 24 | public string LabelCategoryId { get; set; } = string.Empty; |
| 14 | 25 | ||
| 15 | public string LabelCategoryName { get; set; } = string.Empty; | 26 | public string LabelCategoryName { get; set; } = string.Empty; |
美国版/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 | @@ -6,7 +6,17 @@ public class LabelUpdateInputVo | ||
| 6 | 6 | ||
| 7 | public string TemplateCode { get; set; } = string.Empty; | 7 | public string TemplateCode { get; set; } = string.Empty; |
| 8 | 8 | ||
| 9 | - public string LocationId { get; set; } = string.Empty; | 9 | + public string? PartnerId { get; set; } |
| 10 | + | ||
| 11 | + public List<string>? PartnerIds { get; set; } | ||
| 12 | + | ||
| 13 | + public List<string>? RegionIds { get; set; } | ||
| 14 | + | ||
| 15 | + public List<string>? GroupIds { get; set; } | ||
| 16 | + | ||
| 17 | + public string? LocationId { get; set; } | ||
| 18 | + | ||
| 19 | + public List<string>? LocationIds { get; set; } | ||
| 10 | 20 | ||
| 11 | public string LabelCategoryId { get; set; } = string.Empty; | 21 | public string LabelCategoryId { get; set; } = string.Empty; |
| 12 | 22 |
美国版/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 | @@ -29,7 +29,17 @@ public class LabelCategoryCreateInputVo | ||
| 29 | public string AvailabilityType { get; set; } = "ALL"; | 29 | public string AvailabilityType { get; set; } = "ALL"; |
| 30 | 30 | ||
| 31 | /// <summary> | 31 | /// <summary> |
| 32 | - /// 指定门店 Id 列表(当 AvailabilityType=SPECIFIED 时必填) | 32 | + /// 适用 Region(多选),<c>fl_group.Id</c>;与 <see cref="GroupIds"/> 合并去重 |
| 33 | + /// </summary> | ||
| 34 | + public List<string>? RegionIds { get; set; } | ||
| 35 | + | ||
| 36 | + /// <summary> | ||
| 37 | + /// 适用 Region(多选),与 <see cref="RegionIds"/> 相同 | ||
| 38 | + /// </summary> | ||
| 39 | + public List<string>? GroupIds { get; set; } | ||
| 40 | + | ||
| 41 | + /// <summary> | ||
| 42 | + /// 适用门店(多选),<c>location.Id</c>;与 Region 合并后写入 <c>fl_label_category_location</c> | ||
| 33 | /// </summary> | 43 | /// </summary> |
| 34 | public List<string>? LocationIds { get; set; } | 44 | public List<string>? LocationIds { get; set; } |
| 35 | 45 |
美国版/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 | @@ -7,5 +7,15 @@ public class LabelCategoryGetListInputVo : PagedAndSortedResultRequestDto | ||
| 7 | public string? Keyword { get; set; } | 7 | public string? Keyword { get; set; } |
| 8 | 8 | ||
| 9 | public bool? State { get; set; } | 9 | public bool? State { get; set; } |
| 10 | + | ||
| 11 | + /// <summary> | ||
| 12 | + /// Region 筛选(<c>fl_group.Id</c>);含 <c>availabilityType=ALL</c> 的分类 | ||
| 13 | + /// </summary> | ||
| 14 | + public string? GroupId { get; set; } | ||
| 15 | + | ||
| 16 | + /// <summary> | ||
| 17 | + /// 门店筛选(<c>location.Id</c>);优先于 <see cref="GroupId"/> | ||
| 18 | + /// </summary> | ||
| 19 | + public string? LocationId { get; set; } | ||
| 10 | } | 20 | } |
| 11 | 21 |
美国版/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 | @@ -18,15 +18,33 @@ public class LabelCategoryGetListOutputDto | ||
| 18 | 18 | ||
| 19 | public string AvailabilityType { get; set; } = "ALL"; | 19 | public string AvailabilityType { get; set; } = "ALL"; |
| 20 | 20 | ||
| 21 | - /// <summary> | ||
| 22 | - /// 当 <see cref="AvailabilityType"/> 为 SPECIFIED 时返回绑定的门店 Id;否则为空列表。 | ||
| 23 | - /// </summary> | 21 | + /// <summary>适用 Region 展示(ALL 时为 All Regions)</summary> |
| 22 | + public string Region { get; set; } = string.Empty; | ||
| 23 | + | ||
| 24 | + /// <summary>适用门店展示(ALL 时为 All Locations)</summary> | ||
| 25 | + public string Location { get; set; } = string.Empty; | ||
| 26 | + | ||
| 27 | + /// <summary>适用 Region Id 列表(多选)</summary> | ||
| 28 | + public List<string> RegionIds { get; set; } = new(); | ||
| 29 | + | ||
| 30 | + /// <summary>适用门店 Id 列表(多选)</summary> | ||
| 24 | public List<string> LocationIds { get; set; } = new(); | 31 | public List<string> LocationIds { get; set; } = new(); |
| 25 | 32 | ||
| 26 | public int OrderNum { get; set; } | 33 | public int OrderNum { get; set; } |
| 27 | 34 | ||
| 35 | + /// <summary> | ||
| 36 | + /// 该分类下已创建标签数量(<c>fl_label</c> 未删除且 <c>LabelCategoryId</c> 匹配) | ||
| 37 | + /// </summary> | ||
| 28 | public long NoOfLabels { get; set; } | 38 | public long NoOfLabels { get; set; } |
| 29 | 39 | ||
| 40 | + /// <summary> | ||
| 41 | + /// 分类创建时间(兼容旧前端字段 <c>creationTime</c>) | ||
| 42 | + /// </summary> | ||
| 43 | + public DateTime CreationTime { get; set; } | ||
| 44 | + | ||
| 45 | + /// <summary> | ||
| 46 | + /// 最后编辑时间:分类自身与下属标签的 <c>LastModificationTime</c>/<c>CreationTime</c> 取最大值 | ||
| 47 | + /// </summary> | ||
| 30 | public DateTime LastEdited { get; set; } | 48 | public DateTime LastEdited { get; set; } |
| 31 | } | 49 | } |
| 32 | 50 |
美国版/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 | @@ -18,6 +18,12 @@ public class LabelCategoryGetOutputDto | ||
| 18 | 18 | ||
| 19 | public string AvailabilityType { get; set; } = "ALL"; | 19 | public string AvailabilityType { get; set; } = "ALL"; |
| 20 | 20 | ||
| 21 | + /// <summary>适用 Region Id 列表(多选;<c>ALL</c> 时为空)</summary> | ||
| 22 | + public List<string> RegionIds { get; set; } = new(); | ||
| 23 | + | ||
| 24 | + /// <summary>与 <see cref="RegionIds"/> 相同</summary> | ||
| 25 | + public List<string> GroupIds { get; set; } = new(); | ||
| 26 | + | ||
| 21 | public List<string> LocationIds { get; set; } = new(); | 27 | public List<string> LocationIds { get; set; } = new(); |
| 22 | 28 | ||
| 23 | public int OrderNum { get; set; } | 29 | public int OrderNum { get; set; } |
美国版/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 | @@ -34,6 +34,11 @@ public class LocationCreateInputVo | ||
| 34 | 34 | ||
| 35 | public decimal? Longitude { get; set; } | 35 | public decimal? Longitude { get; set; } |
| 36 | 36 | ||
| 37 | + /// <summary> | ||
| 38 | + /// 经营时间(自由文本) | ||
| 39 | + /// </summary> | ||
| 40 | + public string? OperatingHours { get; set; } | ||
| 41 | + | ||
| 37 | public bool State { get; set; } = true; | 42 | public bool State { get; set; } = true; |
| 38 | } | 43 | } |
| 39 | 44 |
美国版/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 | @@ -33,6 +33,11 @@ public class LocationGetListOutputDto | ||
| 33 | 33 | ||
| 34 | public decimal? Longitude { get; set; } | 34 | public decimal? Longitude { get; set; } |
| 35 | 35 | ||
| 36 | + /// <summary> | ||
| 37 | + /// 经营时间 | ||
| 38 | + /// </summary> | ||
| 39 | + public string? OperatingHours { get; set; } | ||
| 40 | + | ||
| 36 | public bool State { get; set; } | 41 | public bool State { get; set; } |
| 37 | } | 42 | } |
| 38 | 43 |
美国版/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 | @@ -33,6 +33,11 @@ public class LocationUpdateInputVo | ||
| 33 | public decimal? Longitude { get; set; } | 33 | public decimal? Longitude { get; set; } |
| 34 | 34 | ||
| 35 | /// <summary> | 35 | /// <summary> |
| 36 | + /// 经营时间(自由文本) | ||
| 37 | + /// </summary> | ||
| 38 | + public string? OperatingHours { get; set; } | ||
| 39 | + | ||
| 40 | + /// <summary> | ||
| 36 | /// 启用状态 | 41 | /// 启用状态 |
| 37 | /// </summary> | 42 | /// </summary> |
| 38 | public bool State { get; set; } = true; | 43 | public bool State { get; set; } = true; |
美国版/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; | @@ -3,7 +3,7 @@ namespace FoodLabeling.Application.Contracts.Dtos.Partner; | ||
| 3 | /// <summary> | 3 | /// <summary> |
| 4 | /// 新建合作伙伴入参 | 4 | /// 新建合作伙伴入参 |
| 5 | /// </summary> | 5 | /// </summary> |
| 6 | -public class PartnerCreateInputVo | 6 | +public class PartnerCreateInputVo : PartnerAddressFieldsDto |
| 7 | { | 7 | { |
| 8 | public string PartnerName { get; set; } = string.Empty; | 8 | public string PartnerName { get; set; } = string.Empty; |
| 9 | 9 | ||
| @@ -11,5 +11,8 @@ public class PartnerCreateInputVo | @@ -11,5 +11,8 @@ public class PartnerCreateInputVo | ||
| 11 | 11 | ||
| 12 | public string? PhoneNumber { get; set; } | 12 | public string? PhoneNumber { get; set; } |
| 13 | 13 | ||
| 14 | + /// <summary> | ||
| 15 | + /// 是否启用(Active / Inactive) | ||
| 16 | + /// </summary> | ||
| 14 | public bool State { get; set; } = true; | 17 | public bool State { get; set; } = true; |
| 15 | } | 18 | } |
美国版/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; | @@ -3,7 +3,7 @@ namespace FoodLabeling.Application.Contracts.Dtos.Partner; | ||
| 3 | /// <summary> | 3 | /// <summary> |
| 4 | /// 合作伙伴列表项 | 4 | /// 合作伙伴列表项 |
| 5 | /// </summary> | 5 | /// </summary> |
| 6 | -public class PartnerGetListOutputDto | 6 | +public class PartnerGetListOutputDto : PartnerAddressFieldsDto |
| 7 | { | 7 | { |
| 8 | public string Id { get; set; } = string.Empty; | 8 | public string Id { get; set; } = string.Empty; |
| 9 | 9 | ||
| @@ -13,6 +13,9 @@ public class PartnerGetListOutputDto | @@ -13,6 +13,9 @@ public class PartnerGetListOutputDto | ||
| 13 | 13 | ||
| 14 | public string? PhoneNumber { get; set; } | 14 | public string? PhoneNumber { get; set; } |
| 15 | 15 | ||
| 16 | + /// <summary> | ||
| 17 | + /// 是否启用 | ||
| 18 | + /// </summary> | ||
| 16 | public bool State { get; set; } | 19 | public bool State { get; set; } |
| 17 | 20 | ||
| 18 | public DateTime CreationTime { get; set; } | 21 | public DateTime CreationTime { get; set; } |
美国版/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; | @@ -3,19 +3,7 @@ namespace FoodLabeling.Application.Contracts.Dtos.Partner; | ||
| 3 | /// <summary> | 3 | /// <summary> |
| 4 | /// 合作伙伴详情 | 4 | /// 合作伙伴详情 |
| 5 | /// </summary> | 5 | /// </summary> |
| 6 | -public class PartnerGetOutputDto | 6 | +public class PartnerGetOutputDto : PartnerGetListOutputDto |
| 7 | { | 7 | { |
| 8 | - public string Id { get; set; } = string.Empty; | ||
| 9 | - | ||
| 10 | - public string PartnerName { get; set; } = string.Empty; | ||
| 11 | - | ||
| 12 | - public string? ContactEmail { get; set; } | ||
| 13 | - | ||
| 14 | - public string? PhoneNumber { get; set; } | ||
| 15 | - | ||
| 16 | - public bool State { get; set; } | ||
| 17 | - | ||
| 18 | - public DateTime CreationTime { get; set; } | ||
| 19 | - | ||
| 20 | public DateTime? LastModificationTime { get; set; } | 8 | public DateTime? LastModificationTime { get; set; } |
| 21 | } | 9 | } |
美国版/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; | @@ -3,13 +3,6 @@ namespace FoodLabeling.Application.Contracts.Dtos.Partner; | ||
| 3 | /// <summary> | 3 | /// <summary> |
| 4 | /// 编辑合作伙伴入参 | 4 | /// 编辑合作伙伴入参 |
| 5 | /// </summary> | 5 | /// </summary> |
| 6 | -public class PartnerUpdateInputVo | 6 | +public class PartnerUpdateInputVo : PartnerCreateInputVo |
| 7 | { | 7 | { |
| 8 | - public string PartnerName { get; set; } = string.Empty; | ||
| 9 | - | ||
| 10 | - public string? ContactEmail { get; set; } | ||
| 11 | - | ||
| 12 | - public string? PhoneNumber { get; set; } | ||
| 13 | - | ||
| 14 | - public bool State { get; set; } = true; | ||
| 15 | } | 8 | } |
美国版/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 | @@ -18,8 +18,18 @@ public class ProductCreateInputVo | ||
| 18 | public bool State { get; set; } = true; | 18 | public bool State { get; set; } = true; |
| 19 | 19 | ||
| 20 | /// <summary> | 20 | /// <summary> |
| 21 | - /// 可选。门店 Id 列表;每个 Id 在 fl_location_product 落一行(同一 fl_product 可对应多门店)。 | ||
| 22 | - /// 不传或空列表则不在本接口写入门店关联(仍可用 product-location 接口维护)。 | 21 | + /// 适用 Company(<c>fl_partner.Id</c>,UI 称 Company);展开该公司下全部门店后与 Region/门店合并写入 <c>fl_location_product</c> |
| 22 | + /// </summary> | ||
| 23 | + public string? PartnerId { get; set; } | ||
| 24 | + | ||
| 25 | + /// <summary> | ||
| 26 | + /// 适用 Region(<c>fl_group.Id</c>,UI 称 Region;库字段为 <c>location.GroupName</c>) | ||
| 27 | + /// </summary> | ||
| 28 | + public List<string>? GroupIds { get; set; } | ||
| 29 | + | ||
| 30 | + /// <summary> | ||
| 31 | + /// 适用门店 Id 列表;与 <see cref="GroupIds"/> 合并后写入 <c>fl_location_product</c>。 | ||
| 32 | + /// 不传则不在本接口写入门店关联。 | ||
| 23 | /// </summary> | 33 | /// </summary> |
| 24 | public List<string>? LocationIds { get; set; } | 34 | public List<string>? LocationIds { get; set; } |
| 25 | } | 35 | } |
美国版/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 | @@ -17,5 +17,20 @@ public class ProductGetListInputVo : PagedAndSortedResultRequestDto | ||
| 17 | /// 启用状态 | 17 | /// 启用状态 |
| 18 | /// </summary> | 18 | /// </summary> |
| 19 | public bool? State { get; set; } | 19 | public bool? State { get; set; } |
| 20 | + | ||
| 21 | + /// <summary> | ||
| 22 | + /// 公司(合作伙伴)Id,对应 <c>fl_partner.Id</c>;筛选绑定在该公司下门店的产品 | ||
| 23 | + /// </summary> | ||
| 24 | + public string? PartnerId { get; set; } | ||
| 25 | + | ||
| 26 | + /// <summary> | ||
| 27 | + /// 组织/Region Id,对应 <c>fl_group.Id</c>;筛选绑定在该 Region 对应门店下的产品(优先于 <see cref="PartnerId"/>) | ||
| 28 | + /// </summary> | ||
| 29 | + public string? GroupId { get; set; } | ||
| 30 | + | ||
| 31 | + /// <summary> | ||
| 32 | + /// 门店 Id(<c>location.Id</c>,Guid 字符串);最精确筛选,传则忽略 <see cref="PartnerId"/> / <see cref="GroupId"/> | ||
| 33 | + /// </summary> | ||
| 34 | + public string? LocationId { get; set; } | ||
| 20 | } | 35 | } |
| 21 | 36 |
美国版/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 | @@ -18,8 +18,17 @@ public class ProductGetOutputDto | ||
| 18 | 18 | ||
| 19 | public bool State { get; set; } | 19 | public bool State { get; set; } |
| 20 | 20 | ||
| 21 | + /// <summary>适用 Company Id(<c>fl_partner.Id</c>,由关联门店反推;多公司时取第一个)</summary> | ||
| 22 | + public string? PartnerId { get; set; } | ||
| 23 | + | ||
| 24 | + /// <summary>适用 Company Id 列表(去重)</summary> | ||
| 25 | + public List<string> PartnerIds { get; set; } = new(); | ||
| 26 | + | ||
| 27 | + /// <summary>适用 Region Id(<c>fl_group.Id</c>,由关联门店反推)</summary> | ||
| 28 | + public List<string> GroupIds { get; set; } = new(); | ||
| 29 | + | ||
| 21 | /// <summary> | 30 | /// <summary> |
| 22 | - /// 该产品关联的门店Id列表(来自 fl_location_product) | 31 | + /// 适用门店 Id 列表(来自 fl_location_product) |
| 23 | /// </summary> | 32 | /// </summary> |
| 24 | public List<string> LocationIds { get; set; } = new(); | 33 | public List<string> LocationIds { get; set; } = new(); |
| 25 | } | 34 | } |
美国版/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 | @@ -32,7 +32,17 @@ public class ProductCategoryCreateInputVo | ||
| 32 | public string AvailabilityType { get; set; } = "ALL"; | 32 | public string AvailabilityType { get; set; } = "ALL"; |
| 33 | 33 | ||
| 34 | /// <summary> | 34 | /// <summary> |
| 35 | - /// 指定门店 Id 列表(当 AvailabilityType=SPECIFIED 时必填) | 35 | + /// 适用 Region(**多选**),<c>fl_group.Id</c> 数组;与 <see cref="GroupIds"/> 等价,推荐本字段。 |
| 36 | + /// </summary> | ||
| 37 | + public List<string>? RegionIds { get; set; } | ||
| 38 | + | ||
| 39 | + /// <summary> | ||
| 40 | + /// 适用 Region(多选),与 <see cref="RegionIds"/> 相同;传任一会合并去重。 | ||
| 41 | + /// </summary> | ||
| 42 | + public List<string>? GroupIds { get; set; } | ||
| 43 | + | ||
| 44 | + /// <summary> | ||
| 45 | + /// 适用门店(**多选**),<c>location.Id</c> 数组;与 Region 合并后写入 <c>fl_product_category_location</c>。 | ||
| 36 | /// </summary> | 46 | /// </summary> |
| 37 | public List<string>? LocationIds { get; set; } | 47 | public List<string>? LocationIds { get; set; } |
| 38 | 48 |
美国版/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 | @@ -16,5 +16,15 @@ public class ProductCategoryGetListInputVo : PagedAndSortedResultRequestDto | ||
| 16 | /// 启用状态过滤 | 16 | /// 启用状态过滤 |
| 17 | /// </summary> | 17 | /// </summary> |
| 18 | public bool? State { get; set; } | 18 | public bool? State { get; set; } |
| 19 | + | ||
| 20 | + /// <summary> | ||
| 21 | + /// 按 Region 筛选(<c>fl_group.Id</c>):返回适用于该 Region 下任一门门店的分类,以及 <c>availabilityType=ALL</c> 的分类 | ||
| 22 | + /// </summary> | ||
| 23 | + public string? GroupId { get; set; } | ||
| 24 | + | ||
| 25 | + /// <summary> | ||
| 26 | + /// 按门店筛选(<c>location.Id</c>,Guid 字符串);优先于 <see cref="GroupId"/> | ||
| 27 | + /// </summary> | ||
| 28 | + public string? LocationId { get; set; } | ||
| 19 | } | 29 | } |
| 20 | 30 |
美国版/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 | @@ -24,5 +24,21 @@ public class ProductCategoryGetListOutputDto | ||
| 24 | public int OrderNum { get; set; } | 24 | public int OrderNum { get; set; } |
| 25 | 25 | ||
| 26 | public DateTime LastEdited { get; set; } | 26 | public DateTime LastEdited { get; set; } |
| 27 | + | ||
| 28 | + /// <summary> | ||
| 29 | + /// 适用 Region 展示(<c>availabilityType=ALL</c> 时为 <c>All Regions</c>;<c>SPECIFIED</c> 时为绑定门店去重后的 <c>location.GroupName</c>,英文逗号拼接) | ||
| 30 | + /// </summary> | ||
| 31 | + public string Region { get; set; } = string.Empty; | ||
| 32 | + | ||
| 33 | + /// <summary> | ||
| 34 | + /// 适用门店展示(<c>availabilityType=ALL</c> 时为 <c>All Locations</c>;<c>SPECIFIED</c> 时为绑定门店名称,英文逗号拼接) | ||
| 35 | + /// </summary> | ||
| 36 | + public string Location { get; set; } = string.Empty; | ||
| 37 | + | ||
| 38 | + /// <summary>适用 Region Id 列表(多选;<c>ALL</c> 时为空数组)</summary> | ||
| 39 | + public List<string> RegionIds { get; set; } = new(); | ||
| 40 | + | ||
| 41 | + /// <summary>适用门店 Id 列表(多选;<c>ALL</c> 时为空数组)</summary> | ||
| 42 | + public List<string> LocationIds { get; set; } = new(); | ||
| 27 | } | 43 | } |
| 28 | 44 |
美国版/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 | @@ -22,6 +22,13 @@ public class ProductCategoryGetOutputDto | ||
| 22 | 22 | ||
| 23 | public string AvailabilityType { get; set; } = "ALL"; | 23 | public string AvailabilityType { get; set; } = "ALL"; |
| 24 | 24 | ||
| 25 | + /// <summary>适用 Region Id 列表(多选,<c>fl_group.Id</c>;由绑定门店反推)</summary> | ||
| 26 | + public List<string> RegionIds { get; set; } = new(); | ||
| 27 | + | ||
| 28 | + /// <summary>与 <see cref="RegionIds"/> 相同(兼容字段)</summary> | ||
| 29 | + public List<string> GroupIds { get; set; } = new(); | ||
| 30 | + | ||
| 31 | + /// <summary>适用门店 Id 列表(多选,<c>location.Id</c>)</summary> | ||
| 25 | public List<string> LocationIds { get; set; } = new(); | 32 | public List<string> LocationIds { get; set; } = new(); |
| 26 | 33 | ||
| 27 | public int OrderNum { get; set; } | 34 | public int OrderNum { get; set; } |
美国版/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 | @@ -19,18 +19,18 @@ public class RbacRoleCreateInputVo | ||
| 19 | public bool State { get; set; } = true; | 19 | public bool State { get; set; } = true; |
| 20 | 20 | ||
| 21 | /// <summary> | 21 | /// <summary> |
| 22 | - /// 排序;未传时服务端按 0 处理 | 22 | + /// 排序号;不传或传 null 时新增默认为 0,编辑时保留原值 |
| 23 | /// </summary> | 23 | /// </summary> |
| 24 | public int? OrderNum { get; set; } | 24 | public int? OrderNum { get; set; } |
| 25 | 25 | ||
| 26 | /// <summary> | 26 | /// <summary> |
| 27 | - /// 访问权限编码 JSON 数组字符串(如 ["manage_labels","manage_people"]) | 27 | + /// 绑定菜单 Id;与 accessPermissions 同时传时以本字段为准 |
| 28 | /// </summary> | 28 | /// </summary> |
| 29 | - public string? AccessPermissions { get; set; } | 29 | + public List<Guid>? MenuIds { get; set; } |
| 30 | 30 | ||
| 31 | /// <summary> | 31 | /// <summary> |
| 32 | - /// 兼容旧字段名 <c>accessPermissionCodes</c> | 32 | + /// 按 PermissionCode 绑定菜单(英文逗号分隔);传空字符串表示清空绑定;不传则不修改已有绑定(仅编辑时) |
| 33 | /// </summary> | 33 | /// </summary> |
| 34 | - public List<string>? AccessPermissionCodes { get; set; } | 34 | + public string? AccessPermissions { get; set; } |
| 35 | } | 35 | } |
| 36 | 36 |
美国版/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 | @@ -20,8 +20,8 @@ public class RbacRoleGetListOutputDto | ||
| 20 | public int OrderNum { get; set; } | 20 | public int OrderNum { get; set; } |
| 21 | 21 | ||
| 22 | /// <summary> | 22 | /// <summary> |
| 23 | - /// 访问权限编码列表 | 23 | + /// 已绑定菜单的 PermissionCode 汇总(英文逗号+空格拼接,与 /api/app/role 一致) |
| 24 | /// </summary> | 24 | /// </summary> |
| 25 | - public List<string> AccessPermissionCodes { get; set; } = new(); | 25 | + public string AccessPermissions { get; set; } = string.Empty; |
| 26 | } | 26 | } |
| 27 | 27 |
美国版/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 | @@ -18,9 +18,29 @@ public class TeamMemberCreateInputVo | ||
| 18 | public Guid? RoleId { get; set; } | 18 | public Guid? RoleId { get; set; } |
| 19 | 19 | ||
| 20 | /// <summary> | 20 | /// <summary> |
| 21 | - /// 关联门店(至少1个) | 21 | + /// 适用 Company(<c>fl_partner.Id</c>,UI 称 Company);与 <see cref="PartnerIds"/> 合并 |
| 22 | /// </summary> | 22 | /// </summary> |
| 23 | - public List<string> LocationIds { get; set; } = new(); | 23 | + public string? PartnerId { get; set; } |
| 24 | + | ||
| 25 | + /// <summary> | ||
| 26 | + /// 适用 Company 多选(<c>fl_partner.Id</c>) | ||
| 27 | + /// </summary> | ||
| 28 | + public List<string>? PartnerIds { get; set; } | ||
| 29 | + | ||
| 30 | + /// <summary> | ||
| 31 | + /// 适用 Region 多选(<c>fl_group.Id</c>);与 <see cref="GroupIds"/> 合并 | ||
| 32 | + /// </summary> | ||
| 33 | + public List<string>? RegionIds { get; set; } | ||
| 34 | + | ||
| 35 | + /// <summary> | ||
| 36 | + /// 适用 Region 多选(与 <see cref="RegionIds"/> 相同) | ||
| 37 | + /// </summary> | ||
| 38 | + public List<string>? GroupIds { get; set; } | ||
| 39 | + | ||
| 40 | + /// <summary> | ||
| 41 | + /// 适用门店多选(<c>location.Id</c>);与 Company/Region 合并后写入 <c>userlocation</c> | ||
| 42 | + /// </summary> | ||
| 43 | + public List<string>? LocationIds { get; set; } | ||
| 24 | 44 | ||
| 25 | public bool State { get; set; } = true; | 45 | public bool State { get; set; } = true; |
| 26 | } | 46 | } |
美国版/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 | @@ -21,6 +21,12 @@ public class TeamMemberGetListOutputDto | ||
| 21 | 21 | ||
| 22 | public string? RoleName { get; set; } | 22 | public string? RoleName { get; set; } |
| 23 | 23 | ||
| 24 | + /// <summary>适用 Company Id 列表(多选)</summary> | ||
| 25 | + public List<string> PartnerIds { get; set; } = new(); | ||
| 26 | + | ||
| 27 | + /// <summary>适用 Region Id 列表(多选)</summary> | ||
| 28 | + public List<string> RegionIds { get; set; } = new(); | ||
| 29 | + | ||
| 24 | public List<TeamMemberAssignedLocationDto> AssignedLocations { get; set; } = new(); | 30 | public List<TeamMemberAssignedLocationDto> AssignedLocations { get; set; } = new(); |
| 25 | } | 31 | } |
| 26 | 32 |
美国版/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 | @@ -16,6 +16,15 @@ public class TeamMemberGetOutputDto | ||
| 16 | 16 | ||
| 17 | public Guid? RoleId { get; set; } | 17 | public Guid? RoleId { get; set; } |
| 18 | 18 | ||
| 19 | + /// <summary>适用 Company Id(多选,由绑定门店反推)</summary> | ||
| 20 | + public List<string> PartnerIds { get; set; } = new(); | ||
| 21 | + | ||
| 22 | + /// <summary>适用 Region Id(多选,<c>fl_group.Id</c>)</summary> | ||
| 23 | + public List<string> RegionIds { get; set; } = new(); | ||
| 24 | + | ||
| 25 | + /// <summary>与 <see cref="RegionIds"/> 相同</summary> | ||
| 26 | + public List<string> GroupIds { get; set; } = new(); | ||
| 27 | + | ||
| 19 | public List<string> LocationIds { get; set; } = new(); | 28 | public List<string> LocationIds { get; set; } = new(); |
| 20 | 29 | ||
| 21 | public List<TeamMemberAssignedLocationDto> AssignedLocations { get; set; } = new(); | 30 | public List<TeamMemberAssignedLocationDto> AssignedLocations { get; set; } = new(); |
美国版/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 | @@ -17,7 +17,30 @@ public class TeamMemberUpdateInputVo | ||
| 17 | 17 | ||
| 18 | public Guid? RoleId { get; set; } | 18 | public Guid? RoleId { get; set; } |
| 19 | 19 | ||
| 20 | - public List<string> LocationIds { get; set; } = new(); | 20 | + /// <summary> |
| 21 | + /// 适用 Company(<c>fl_partner.Id</c>);与 <see cref="PartnerIds"/> 合并 | ||
| 22 | + /// </summary> | ||
| 23 | + public string? PartnerId { get; set; } | ||
| 24 | + | ||
| 25 | + /// <summary> | ||
| 26 | + /// 适用 Company 多选(<c>fl_partner.Id</c>) | ||
| 27 | + /// </summary> | ||
| 28 | + public List<string>? PartnerIds { get; set; } | ||
| 29 | + | ||
| 30 | + /// <summary> | ||
| 31 | + /// 适用 Region 多选(<c>fl_group.Id</c>);与 <see cref="GroupIds"/> 合并 | ||
| 32 | + /// </summary> | ||
| 33 | + public List<string>? RegionIds { get; set; } | ||
| 34 | + | ||
| 35 | + /// <summary> | ||
| 36 | + /// 适用 Region 多选(与 <see cref="RegionIds"/> 相同) | ||
| 37 | + /// </summary> | ||
| 38 | + public List<string>? GroupIds { get; set; } | ||
| 39 | + | ||
| 40 | + /// <summary> | ||
| 41 | + /// 适用门店多选(<c>location.Id</c>);与 Company/Region 合并后写入 <c>userlocation</c> | ||
| 42 | + /// </summary> | ||
| 43 | + public List<string>? LocationIds { get; set; } | ||
| 21 | 44 | ||
| 22 | public bool State { get; set; } = true; | 45 | public bool State { get; set; } = true; |
| 23 | } | 46 | } |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/FoodLabeling.Application.Contracts.csproj
| @@ -3,6 +3,7 @@ | @@ -3,6 +3,7 @@ | ||
| 3 | 3 | ||
| 4 | <ItemGroup> | 4 | <ItemGroup> |
| 5 | <ProjectReference Include="..\..\..\framework\Yi.Framework.Ddd.Application.Contracts\Yi.Framework.Ddd.Application.Contracts.csproj" /> | 5 | <ProjectReference Include="..\..\..\framework\Yi.Framework.Ddd.Application.Contracts\Yi.Framework.Ddd.Application.Contracts.csproj" /> |
| 6 | + <ProjectReference Include="..\..\rbac\Yi.Framework.Rbac.Application.Contracts\Yi.Framework.Rbac.Application.Contracts.csproj" /> | ||
| 6 | <ProjectReference Include="..\FoodLabeling.Domain.Shared\FoodLabeling.Domain.Shared.csproj" /> | 7 | <ProjectReference Include="..\FoodLabeling.Domain.Shared\FoodLabeling.Domain.Shared.csproj" /> |
| 7 | <ProjectReference Include="..\..\rbac\Yi.Framework.Rbac.Domain.Shared\Yi.Framework.Rbac.Domain.Shared.csproj" /> | 8 | <ProjectReference Include="..\..\rbac\Yi.Framework.Rbac.Domain.Shared\Yi.Framework.Rbac.Domain.Shared.csproj" /> |
| 8 | </ItemGroup> | 9 | </ItemGroup> |
美国版/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 | @@ -13,6 +13,8 @@ public interface IAuthSessionAppService : IApplicationService | ||
| 13 | /// </summary> | 13 | /// </summary> |
| 14 | /// <remarks> | 14 | /// <remarks> |
| 15 | /// 与框架 <c>UserManager.GetInfoAsync</c> 一致;用户名为 <c>admin</c> 时返回全部未删除菜单(与 <c>AccountService.GetVue3Router</c> 行为对齐)。 | 15 | /// 与框架 <c>UserManager.GetInfoAsync</c> 一致;用户名为 <c>admin</c> 时返回全部未删除菜单(与 <c>AccountService.GetVue3Router</c> 行为对齐)。 |
| 16 | + /// 返回体额外包含:<c>lastUpdated</c>(用户 LastModificationTime)、<c>role</c>(角色展示名,多角色英文逗号拼接)、<c>fullName</c>(姓名优先,其次昵称、用户名)。 | ||
| 17 | + /// 角色名通过 <c>Role</c> 表直查(<c>RoleDbEntity</c>),避免走仓储 IDataPermission。 | ||
| 16 | /// </remarks> | 18 | /// </remarks> |
| 17 | /// <returns>用户简要信息、权限码与菜单树</returns> | 19 | /// <returns>用户简要信息、权限码与菜单树</returns> |
| 18 | /// <response code="200">成功</response> | 20 | /// <response code="200">成功</response> |
美国版/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 | @@ -14,13 +14,18 @@ public interface IGroupAppService : IApplicationService | ||
| 14 | /// <summary> | 14 | /// <summary> |
| 15 | /// 组织分页列表(与导出使用相同筛选条件) | 15 | /// 组织分页列表(与导出使用相同筛选条件) |
| 16 | /// </summary> | 16 | /// </summary> |
| 17 | + /// <remarks> | ||
| 18 | + /// 数据范围(按登录 Token): | ||
| 19 | + /// 管理员可见全部 Region;其它角色仅可见其 <c>userlocation</c> 绑定门店对应的组织(<c>location.Partner</c> + <c>location.GroupName</c> 与 <c>fl_group</c> 匹配)。 | ||
| 20 | + /// </remarks> | ||
| 17 | /// <param name="input">分页与筛选;SkipCount 为页码(从 1 起)</param> | 21 | /// <param name="input">分页与筛选;SkipCount 为页码(从 1 起)</param> |
| 18 | Task<PagedResultWithPageDto<GroupGetListOutputDto>> GetListAsync(GroupGetListInputVo input); | 22 | Task<PagedResultWithPageDto<GroupGetListOutputDto>> GetListAsync(GroupGetListInputVo input); |
| 19 | 23 | ||
| 20 | /// <summary> | 24 | /// <summary> |
| 21 | /// 组织详情 | 25 | /// 组织详情 |
| 22 | /// </summary> | 26 | /// </summary> |
| 23 | - Task<GroupGetOutputDto> GetAsync(string id); | 27 | + /// <param name="id">主键(Guid,与 <c>fl_group.Id</c> 一致;约定路由 <c>{id:guid}</c>,避免与 <c>export-pdf</c> 等路径冲突)</param> |
| 28 | + Task<GroupGetOutputDto> GetAsync(Guid id); | ||
| 24 | 29 | ||
| 25 | /// <summary> | 30 | /// <summary> |
| 26 | /// 新增组织 | 31 | /// 新增组织 |
| @@ -30,16 +35,19 @@ public interface IGroupAppService : IApplicationService | @@ -30,16 +35,19 @@ public interface IGroupAppService : IApplicationService | ||
| 30 | /// <summary> | 35 | /// <summary> |
| 31 | /// 编辑组织 | 36 | /// 编辑组织 |
| 32 | /// </summary> | 37 | /// </summary> |
| 33 | - Task<GroupGetOutputDto> UpdateAsync(string id, GroupUpdateInputVo input); | 38 | + Task<GroupGetOutputDto> UpdateAsync(Guid id, GroupUpdateInputVo input); |
| 34 | 39 | ||
| 35 | /// <summary> | 40 | /// <summary> |
| 36 | /// 删除组织(逻辑删除) | 41 | /// 删除组织(逻辑删除) |
| 37 | /// </summary> | 42 | /// </summary> |
| 38 | - Task DeleteAsync(string id); | 43 | + Task DeleteAsync(Guid id); |
| 39 | 44 | ||
| 40 | /// <summary> | 45 | /// <summary> |
| 41 | - /// 按列表相同筛选条件全量导出组织(Region)为 PDF(不分页;与 <see cref="GetListAsync"/> 相同筛选;单次最多 5000 条) | 46 | + /// 按列表相同筛选条件全量导出组织(Region)为 PDF(不分页;与 <see cref="GetListAsync"/> 相同筛选与 Token 数据范围;单次最多 5000 条) |
| 42 | /// </summary> | 47 | /// </summary> |
| 48 | + /// <remarks> | ||
| 49 | + /// 与 <see cref="GetListAsync"/> 使用同一套 <c>BuildGroupJoinedQueryAsync</c>(含 <c>ResolveGroupScopeAsync</c>),导出行集与列表一致。 | ||
| 50 | + /// </remarks> | ||
| 43 | /// <param name="input">Keyword、PartnerId、State、Sorting;分页字段忽略</param> | 51 | /// <param name="input">Keyword、PartnerId、State、Sorting;分页字段忽略</param> |
| 44 | /// <returns>application/pdf</returns> | 52 | /// <returns>application/pdf</returns> |
| 45 | Task<IActionResult> ExportPdfAsync(GroupGetListInputVo input); | 53 | Task<IActionResult> ExportPdfAsync(GroupGetListInputVo input); |
美国版/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 | @@ -14,6 +14,13 @@ public interface IPartnerAppService : IApplicationService | ||
| 14 | /// <summary> | 14 | /// <summary> |
| 15 | /// 合作伙伴分页列表(与导出使用相同筛选条件) | 15 | /// 合作伙伴分页列表(与导出使用相同筛选条件) |
| 16 | /// </summary> | 16 | /// </summary> |
| 17 | + /// <remarks> | ||
| 18 | + /// 数据范围(按登录 Token): | ||
| 19 | + /// <list type="bullet"> | ||
| 20 | + /// <item>管理员(角色码 <c>admin</c>、用户名为 <c>admin</c> 或权限 <c>*:*:*</c>):可查看全部公司;</item> | ||
| 21 | + /// <item>其它角色:仅可查看当前用户在 <c>userlocation</c> 中绑定门店所属的合作伙伴(<c>location.Partner</c> 与 <c>fl_partner</c> 按名称或 Id 匹配)。</item> | ||
| 22 | + /// </list> | ||
| 23 | + /// </remarks> | ||
| 17 | /// <param name="input">分页与筛选;SkipCount 为页码(从 1 起)</param> | 24 | /// <param name="input">分页与筛选;SkipCount 为页码(从 1 起)</param> |
| 18 | /// <returns>分页数据</returns> | 25 | /// <returns>分页数据</returns> |
| 19 | /// <response code="200">成功</response> | 26 | /// <response code="200">成功</response> |
| @@ -24,17 +31,17 @@ public interface IPartnerAppService : IApplicationService | @@ -24,17 +31,17 @@ public interface IPartnerAppService : IApplicationService | ||
| 24 | /// <summary> | 31 | /// <summary> |
| 25 | /// 合作伙伴详情 | 32 | /// 合作伙伴详情 |
| 26 | /// </summary> | 33 | /// </summary> |
| 27 | - /// <param name="id">主键 Id</param> | 34 | + /// <param name="id">主键(Guid,与 <c>fl_partner.Id</c> 一致;约定路由 <c>{id:guid}</c>,避免与 <c>export-pdf</c> 等路径冲突)</param> |
| 28 | /// <returns>详情</returns> | 35 | /// <returns>详情</returns> |
| 29 | /// <response code="200">成功</response> | 36 | /// <response code="200">成功</response> |
| 30 | /// <response code="400">Id 无效</response> | 37 | /// <response code="400">Id 无效</response> |
| 31 | /// <response code="500">服务器错误</response> | 38 | /// <response code="500">服务器错误</response> |
| 32 | - Task<PartnerGetOutputDto> GetAsync(string id); | 39 | + Task<PartnerGetOutputDto> GetAsync(Guid id); |
| 33 | 40 | ||
| 34 | /// <summary> | 41 | /// <summary> |
| 35 | /// 新增合作伙伴 | 42 | /// 新增合作伙伴 |
| 36 | /// </summary> | 43 | /// </summary> |
| 37 | - /// <param name="input">名称、邮箱、电话、启用状态</param> | 44 | + /// <param name="input">名称、联系信息、地址、启用状态</param> |
| 38 | /// <returns>新建后的详情</returns> | 45 | /// <returns>新建后的详情</returns> |
| 39 | /// <remarks> | 46 | /// <remarks> |
| 40 | /// 示例请求: | 47 | /// 示例请求: |
| @@ -43,9 +50,15 @@ public interface IPartnerAppService : IApplicationService | @@ -43,9 +50,15 @@ public interface IPartnerAppService : IApplicationService | ||
| 43 | /// "partnerName": "Global Foods Inc.", | 50 | /// "partnerName": "Global Foods Inc.", |
| 44 | /// "contactEmail": "admin@globalfoods.com", | 51 | /// "contactEmail": "admin@globalfoods.com", |
| 45 | /// "phoneNumber": "+1 (555) 100-2000", | 52 | /// "phoneNumber": "+1 (555) 100-2000", |
| 53 | + /// "street": "123 Main St", | ||
| 54 | + /// "city": "New York", | ||
| 55 | + /// "stateCode": "NY", | ||
| 56 | + /// "country": "USA", | ||
| 57 | + /// "zipCode": "10001", | ||
| 46 | /// "state": true | 58 | /// "state": true |
| 47 | /// } | 59 | /// } |
| 48 | /// ``` | 60 | /// ``` |
| 61 | + /// 地址中的州/省请传 <c>stateCode</c>(如 NY);<c>state</c>(boolean)表示是否启用。 | ||
| 49 | /// </remarks> | 62 | /// </remarks> |
| 50 | /// <response code="200">成功</response> | 63 | /// <response code="200">成功</response> |
| 51 | /// <response code="400">校验失败</response> | 64 | /// <response code="400">校验失败</response> |
| @@ -56,12 +69,12 @@ public interface IPartnerAppService : IApplicationService | @@ -56,12 +69,12 @@ public interface IPartnerAppService : IApplicationService | ||
| 56 | /// 编辑合作伙伴 | 69 | /// 编辑合作伙伴 |
| 57 | /// </summary> | 70 | /// </summary> |
| 58 | /// <param name="id">主键 Id</param> | 71 | /// <param name="id">主键 Id</param> |
| 59 | - /// <param name="input">名称、邮箱、电话、启用状态</param> | 72 | + /// <param name="input">名称、联系信息、地址、启用状态(字段同新增)</param> |
| 60 | /// <returns>更新后的详情</returns> | 73 | /// <returns>更新后的详情</returns> |
| 61 | /// <response code="200">成功</response> | 74 | /// <response code="200">成功</response> |
| 62 | /// <response code="400">校验失败或记录不存在</response> | 75 | /// <response code="400">校验失败或记录不存在</response> |
| 63 | /// <response code="500">服务器错误</response> | 76 | /// <response code="500">服务器错误</response> |
| 64 | - Task<PartnerGetOutputDto> UpdateAsync(string id, PartnerUpdateInputVo input); | 77 | + Task<PartnerGetOutputDto> UpdateAsync(Guid id, PartnerUpdateInputVo input); |
| 65 | 78 | ||
| 66 | /// <summary> | 79 | /// <summary> |
| 67 | /// 删除合作伙伴(逻辑删除) | 80 | /// 删除合作伙伴(逻辑删除) |
| @@ -70,7 +83,7 @@ public interface IPartnerAppService : IApplicationService | @@ -70,7 +83,7 @@ public interface IPartnerAppService : IApplicationService | ||
| 70 | /// <response code="200">成功</response> | 83 | /// <response code="200">成功</response> |
| 71 | /// <response code="400">Id 无效或记录不存在</response> | 84 | /// <response code="400">Id 无效或记录不存在</response> |
| 72 | /// <response code="500">服务器错误</response> | 85 | /// <response code="500">服务器错误</response> |
| 73 | - Task DeleteAsync(string id); | 86 | + Task DeleteAsync(Guid id); |
| 74 | 87 | ||
| 75 | /// <summary> | 88 | /// <summary> |
| 76 | /// 按当前列表筛选条件批量导出合作伙伴为 PDF(Account Management「Company」页签;不分页,上限 5000 条) | 89 | /// 按当前列表筛选条件批量导出合作伙伴为 PDF(Account Management「Company」页签;不分页,上限 5000 条) |
| @@ -78,7 +91,7 @@ public interface IPartnerAppService : IApplicationService | @@ -78,7 +91,7 @@ public interface IPartnerAppService : IApplicationService | ||
| 78 | /// <param name="input">与列表相同的 Keyword、State;分页字段忽略</param> | 91 | /// <param name="input">与列表相同的 Keyword、State;分页字段忽略</param> |
| 79 | /// <returns>PDF 文件流</returns> | 92 | /// <returns>PDF 文件流</returns> |
| 80 | /// <remarks> | 93 | /// <remarks> |
| 81 | - /// 筛选条件需与 <see cref="GetListAsync"/> 一致,便于统计与导出数据对齐。 | 94 | + /// 筛选条件与数据范围需与 <see cref="GetListAsync"/> 完全一致(含 Token 权限:管理员全部公司,其它角色仅绑定门店所属公司;见 PartnerScopeHelper)。 |
| 82 | /// </remarks> | 95 | /// </remarks> |
| 83 | /// <response code="200">成功返回 application/pdf</response> | 96 | /// <response code="200">成功返回 application/pdf</response> |
| 84 | /// <response code="400">参数错误</response> | 97 | /// <response code="400">参数错误</response> |
美国版/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 | @@ -14,19 +14,26 @@ public interface IProductAppService : IApplicationService | ||
| 14 | /// <summary> | 14 | /// <summary> |
| 15 | /// 产品分页列表 | 15 | /// 产品分页列表 |
| 16 | /// </summary> | 16 | /// </summary> |
| 17 | + /// <remarks> | ||
| 18 | + /// 支持按公司 <c>partnerId</c>、组织 <c>groupId</c>、门店 <c>locationId</c> 筛选: | ||
| 19 | + /// 仅返回在 <c>fl_location_product</c> 中关联到匹配门店的产品;三者均未传时不按门店收窄。 | ||
| 20 | + /// <c>locationId</c> 最优先;传 <c>groupId</c> 时按 Region 对应门店的 Partner+GroupName 匹配。 | ||
| 21 | + /// </remarks> | ||
| 17 | Task<PagedResultWithPageDto<ProductGetListOutputDto>> GetListAsync(ProductGetListInputVo input); | 22 | Task<PagedResultWithPageDto<ProductGetListOutputDto>> GetListAsync(ProductGetListInputVo input); |
| 18 | 23 | ||
| 19 | /// <summary> | 24 | /// <summary> |
| 20 | /// 产品详情 | 25 | /// 产品详情 |
| 21 | /// </summary> | 26 | /// </summary> |
| 22 | - Task<ProductGetOutputDto> GetAsync(string id); | 27 | + /// <param name="id">产品主键(Guid,与 <c>fl_product.Id</c> 一致;使用 Guid 路由约束,避免与 <c>export-*</c> 等字面路径冲突)</param> |
| 28 | + Task<ProductGetOutputDto> GetAsync(Guid id); | ||
| 23 | 29 | ||
| 24 | /// <summary> | 30 | /// <summary> |
| 25 | /// 新增产品 | 31 | /// 新增产品 |
| 26 | /// </summary> | 32 | /// </summary> |
| 27 | /// <remarks> | 33 | /// <remarks> |
| 28 | /// <see cref="ProductCreateInputVo.ProductCode"/> 可选;为空时后端生成唯一编码(如 PRD_ + Guid)。 | 34 | /// <see cref="ProductCreateInputVo.ProductCode"/> 可选;为空时后端生成唯一编码(如 PRD_ + Guid)。 |
| 29 | - /// 若 <see cref="ProductCreateInputVo.LocationIds"/> 有值,将在同一事务内批量写入 fl_location_product(一门店一条)。 | 35 | + /// 若传 <see cref="ProductCreateInputVo.PartnerId"/>(Company)、<see cref="ProductCreateInputVo.GroupIds"/>(Region) |
| 36 | + /// 和/或 <see cref="ProductCreateInputVo.LocationIds"/>,合并后写入 fl_location_product。 | ||
| 30 | /// </remarks> | 37 | /// </remarks> |
| 31 | Task<ProductGetOutputDto> CreateAsync(ProductCreateInputVo input); | 38 | Task<ProductGetOutputDto> CreateAsync(ProductCreateInputVo input); |
| 32 | 39 | ||
| @@ -34,15 +41,15 @@ public interface IProductAppService : IApplicationService | @@ -34,15 +41,15 @@ public interface IProductAppService : IApplicationService | ||
| 34 | /// 编辑产品 | 41 | /// 编辑产品 |
| 35 | /// </summary> | 42 | /// </summary> |
| 36 | /// <remarks> | 43 | /// <remarks> |
| 37 | - /// 当请求体包含 <see cref="ProductCreateInputVo.LocationIds"/> 属性时,按该列表整表替换本产品在各门店的关联; | ||
| 38 | - /// 不传该属性则不改门店关联(兼容仅改名称/分类等调用)。 | 44 | + /// 当请求体包含 <see cref="ProductCreateInputVo.PartnerId"/>、<see cref="ProductCreateInputVo.GroupIds"/> |
| 45 | + /// 和/或 <see cref="ProductCreateInputVo.LocationIds"/> 时,合并后整表替换门店关联;均不传则不改。 | ||
| 39 | /// </remarks> | 46 | /// </remarks> |
| 40 | - Task<ProductGetOutputDto> UpdateAsync(string id, ProductUpdateInputVo input); | 47 | + Task<ProductGetOutputDto> UpdateAsync(Guid id, ProductUpdateInputVo input); |
| 41 | 48 | ||
| 42 | /// <summary> | 49 | /// <summary> |
| 43 | /// 删除产品(逻辑删除) | 50 | /// 删除产品(逻辑删除) |
| 44 | /// </summary> | 51 | /// </summary> |
| 45 | - Task DeleteAsync(string id); | 52 | + Task DeleteAsync(Guid id); |
| 46 | 53 | ||
| 47 | /// <summary> | 54 | /// <summary> |
| 48 | /// 下载 Product 批量导入模板(服务器 <c>TemplateDirectory</c> 下 xlsx) | 55 | /// 下载 Product 批量导入模板(服务器 <c>TemplateDirectory</c> 下 xlsx) |
美国版/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; | @@ -9,12 +9,21 @@ namespace FoodLabeling.Application.Contracts.IServices; | ||
| 9 | /// </summary> | 9 | /// </summary> |
| 10 | public interface IProductCategoryAppService : IApplicationService | 10 | public interface IProductCategoryAppService : IApplicationService |
| 11 | { | 11 | { |
| 12 | + /// <summary> | ||
| 13 | + /// 类别分页列表;支持 <c>groupId</c>/<c>locationId</c> 筛选;出参含 <c>region</c>、<c>location</c> 展示字段。 | ||
| 14 | + /// </summary> | ||
| 12 | Task<PagedResultWithPageDto<ProductCategoryGetListOutputDto>> GetListAsync(ProductCategoryGetListInputVo input); | 15 | Task<PagedResultWithPageDto<ProductCategoryGetListOutputDto>> GetListAsync(ProductCategoryGetListInputVo input); |
| 13 | 16 | ||
| 14 | Task<ProductCategoryGetOutputDto> GetAsync(string id); | 17 | Task<ProductCategoryGetOutputDto> GetAsync(string id); |
| 15 | 18 | ||
| 19 | + /// <summary> | ||
| 20 | + /// 新增类别;body 传 <c>regionIds</c>(Region 多选)与 <c>locationIds</c>(门店多选)绑定适用范围。 | ||
| 21 | + /// </summary> | ||
| 16 | Task<ProductCategoryGetOutputDto> CreateAsync(ProductCategoryCreateInputVo input); | 22 | Task<ProductCategoryGetOutputDto> CreateAsync(ProductCategoryCreateInputVo input); |
| 17 | 23 | ||
| 24 | + /// <summary> | ||
| 25 | + /// 编辑类别;<c>regionIds</c>/<c>locationIds</c> 多选数组规则同新增;传空数组 <c>[]</c> 可清空对应范围。 | ||
| 26 | + /// </summary> | ||
| 18 | Task<ProductCategoryGetOutputDto> UpdateAsync(string id, ProductCategoryUpdateInputVo input); | 27 | Task<ProductCategoryGetOutputDto> UpdateAsync(string id, ProductCategoryUpdateInputVo input); |
| 19 | 28 | ||
| 20 | Task DeleteAsync(string id); | 29 | Task DeleteAsync(string id); |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs
| 1 | using FoodLabeling.Application.Contracts.Dtos.UsAppAuth; | 1 | using FoodLabeling.Application.Contracts.Dtos.UsAppAuth; |
| 2 | using Volo.Abp.Application.Services; | 2 | using Volo.Abp.Application.Services; |
| 3 | +using Yi.Framework.Rbac.Application.Contracts.Dtos.Account; | ||
| 3 | 4 | ||
| 4 | namespace FoodLabeling.Application.Contracts.IServices; | 5 | namespace FoodLabeling.Application.Contracts.IServices; |
| 5 | 6 | ||
| @@ -48,4 +49,26 @@ public interface IUsAppAuthAppService : IApplicationService | @@ -48,4 +49,26 @@ public interface IUsAppAuthAppService : IApplicationService | ||
| 48 | /// <response code="400">参数非法、未绑定或无权限</response> | 49 | /// <response code="400">参数非法、未绑定或无权限</response> |
| 49 | /// <response code="500">服务器错误</response> | 50 | /// <response code="500">服务器错误</response> |
| 50 | Task<UsAppLocationDetailOutputDto> GetLocationDetailAsync(string locationId); | 51 | Task<UsAppLocationDetailOutputDto> GetLocationDetailAsync(string locationId); |
| 52 | + | ||
| 53 | + /// <summary> | ||
| 54 | + /// App forgot password: send email verification code (same user store and rules as platform). | ||
| 55 | + /// </summary> | ||
| 56 | + /// <remarks> | ||
| 57 | + /// When <c>RbacOptions.EnableCaptcha</c> is true, pass <c>Uuid</c> and <c>Code</c> from platform captcha API. | ||
| 58 | + /// </remarks> | ||
| 59 | + /// <param name="input">Email and optional image captcha.</param> | ||
| 60 | + /// <response code="200">Accepted.</response> | ||
| 61 | + /// <response code="400">Validation or rate limit.</response> | ||
| 62 | + /// <response code="500">Server error</response> | ||
| 63 | + Task PostSendForgotPasswordCodeByEmailAsync(EmailCaptchaImageDto input); | ||
| 64 | + | ||
| 65 | + /// <summary> | ||
| 66 | + /// App forgot password: reset password with email OTP. | ||
| 67 | + /// </summary> | ||
| 68 | + /// <param name="input">Email, OTP, new password.</param> | ||
| 69 | + /// <returns>Account user name.</returns> | ||
| 70 | + /// <response code="200">Password updated.</response> | ||
| 71 | + /// <response code="400">Invalid code or password policy.</response> | ||
| 72 | + /// <response code="500">Server error</response> | ||
| 73 | + Task<string> PostResetPasswordByEmailAsync(RetrievePasswordByEmailDto input); | ||
| 51 | } | 74 | } |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj
| @@ -9,6 +9,7 @@ | @@ -9,6 +9,7 @@ | ||
| 9 | 9 | ||
| 10 | <ItemGroup> | 10 | <ItemGroup> |
| 11 | <ProjectReference Include="..\..\..\framework\Yi.Framework.Ddd.Application\Yi.Framework.Ddd.Application.csproj" /> | 11 | <ProjectReference Include="..\..\..\framework\Yi.Framework.Ddd.Application\Yi.Framework.Ddd.Application.csproj" /> |
| 12 | + <ProjectReference Include="..\..\rbac\Yi.Framework.Rbac.Application.Contracts\Yi.Framework.Rbac.Application.Contracts.csproj" /> | ||
| 12 | <ProjectReference Include="..\FoodLabeling.Application.Contracts\FoodLabeling.Application.Contracts.csproj" /> | 13 | <ProjectReference Include="..\FoodLabeling.Application.Contracts\FoodLabeling.Application.Contracts.csproj" /> |
| 13 | <ProjectReference Include="..\FoodLabeling.Domain\FoodLabeling.Domain.csproj" /> | 14 | <ProjectReference Include="..\FoodLabeling.Domain\FoodLabeling.Domain.csproj" /> |
| 14 | <ProjectReference Include="..\..\rbac\Yi.Framework.Rbac.Domain\Yi.Framework.Rbac.Domain.csproj" /> | 15 | <ProjectReference Include="..\..\rbac\Yi.Framework.Rbac.Domain\Yi.Framework.Rbac.Domain.csproj" /> |
美国版/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 | @@ -50,6 +50,12 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | ||
| 50 | throw new UserFriendlyException("用户不存在"); | 50 | throw new UserFriendlyException("用户不存在"); |
| 51 | } | 51 | } |
| 52 | 52 | ||
| 53 | + var userRoleIds = await _dbContext.SqlSugarClient.Queryable<UserRoleEntity>() | ||
| 54 | + .Where(x => x.UserId == userId) | ||
| 55 | + .Select(x => x.RoleId) | ||
| 56 | + .ToListAsync(); | ||
| 57 | + var distinctUserRoleIds = userRoleIds.Distinct().ToList(); | ||
| 58 | + | ||
| 53 | List<MenuDbEntity> menus; | 59 | List<MenuDbEntity> menus; |
| 54 | if (UserConst.Admin.Equals(user.UserName)) | 60 | if (UserConst.Admin.Equals(user.UserName)) |
| 55 | { | 61 | { |
| @@ -60,12 +66,7 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | @@ -60,12 +66,7 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | ||
| 60 | } | 66 | } |
| 61 | else | 67 | else |
| 62 | { | 68 | { |
| 63 | - var roleIds = await _dbContext.SqlSugarClient.Queryable<UserRoleEntity>() | ||
| 64 | - .Where(x => x.UserId == userId) | ||
| 65 | - .Select(x => x.RoleId) | ||
| 66 | - .ToListAsync(); | ||
| 67 | - | ||
| 68 | - var roleIdStrs = roleIds.Select(x => x.ToString()).Distinct().ToList(); | 69 | + var roleIdStrs = distinctUserRoleIds.Select(x => x.ToString()).Distinct().ToList(); |
| 69 | if (roleIdStrs.Count == 0) | 70 | if (roleIdStrs.Count == 0) |
| 70 | { | 71 | { |
| 71 | menus = new List<MenuDbEntity>(); | 72 | menus = new List<MenuDbEntity>(); |
| @@ -92,10 +93,11 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | @@ -92,10 +93,11 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | ||
| 92 | .ThenBy(x => x.MenuName) | 93 | .ThenBy(x => x.MenuName) |
| 93 | .ToList(); | 94 | .ToList(); |
| 94 | 95 | ||
| 95 | - // 注意:查询 RoleAggregateRoot 会触发 YiRbacDbContext 的 IDataPermission 过滤, | 96 | + // 注意:经仓储查询 RoleAggregateRoot 会触发 YiRbacDbContext 的 IDataPermission 过滤, |
| 96 | // 其表达式包含 roleInfo.Select(...).Contains(...),在当前 SqlSugar 版本下会报“不支持 Select”。 | 97 | // 其表达式包含 roleInfo.Select(...).Contains(...),在当前 SqlSugar 版本下会报“不支持 Select”。 |
| 97 | - // 这里直接使用 JWT 中的角色码(CurrentUser.Roles)返回,避免触发过滤器。 | 98 | + // 角色展示名使用 RoleDbEntity 直查 Role 表;角色编码列表仍用 JWT(CurrentUser.Roles),与原先 RoleCodes 行为一致。 |
| 98 | var roleCodes = CurrentUser.Roles?.ToList() ?? new List<string>(); | 99 | var roleCodes = CurrentUser.Roles?.ToList() ?? new List<string>(); |
| 100 | + var roleDisplay = await BuildRoleDisplayAsync(distinctUserRoleIds, roleCodes); | ||
| 99 | 101 | ||
| 100 | var permissionCodes = menuNodes | 102 | var permissionCodes = menuNodes |
| 101 | .Where(x => !string.IsNullOrWhiteSpace(x.PermissionCode)) | 103 | .Where(x => !string.IsNullOrWhiteSpace(x.PermissionCode)) |
| @@ -116,7 +118,10 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | @@ -116,7 +118,10 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | ||
| 116 | }, | 118 | }, |
| 117 | RoleCodes = roleCodes.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().OrderBy(x => x).ToList(), | 119 | RoleCodes = roleCodes.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().OrderBy(x => x).ToList(), |
| 118 | PermissionCodes = permissionCodes, | 120 | PermissionCodes = permissionCodes, |
| 119 | - Menus = BuildMenuTree(menuNodes) | 121 | + Menus = BuildMenuTree(menuNodes), |
| 122 | + LastUpdated = user.LastModificationTime, | ||
| 123 | + Role = roleDisplay, | ||
| 124 | + FullName = BuildFullName(user) | ||
| 120 | }; | 125 | }; |
| 121 | } | 126 | } |
| 122 | 127 | ||
| @@ -133,6 +138,67 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | @@ -133,6 +138,67 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | ||
| 133 | return true; | 138 | return true; |
| 134 | } | 139 | } |
| 135 | 140 | ||
| 141 | + private static string BuildFullName(UserAggregateRoot user) | ||
| 142 | + { | ||
| 143 | + if (!string.IsNullOrWhiteSpace(user.Name)) | ||
| 144 | + { | ||
| 145 | + return user.Name.Trim(); | ||
| 146 | + } | ||
| 147 | + | ||
| 148 | + if (!string.IsNullOrWhiteSpace(user.Nick)) | ||
| 149 | + { | ||
| 150 | + return user.Nick.Trim(); | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + return user.UserName ?? string.Empty; | ||
| 154 | + } | ||
| 155 | + | ||
| 156 | + private async Task<string> BuildRoleDisplayAsync( | ||
| 157 | + IReadOnlyList<Guid> userRoleIds, | ||
| 158 | + IReadOnlyList<string> jwtRoleCodes) | ||
| 159 | + { | ||
| 160 | + var names = new List<string>(); | ||
| 161 | + if (userRoleIds.Count > 0) | ||
| 162 | + { | ||
| 163 | + names = await _dbContext.SqlSugarClient.Queryable<RoleDbEntity>() | ||
| 164 | + .Where(r => !r.IsDeleted && userRoleIds.Contains(r.Id)) | ||
| 165 | + .OrderBy(r => r.RoleName) | ||
| 166 | + .Select(r => r.RoleName) | ||
| 167 | + .ToListAsync(); | ||
| 168 | + } | ||
| 169 | + | ||
| 170 | + if (names.Count == 0 && jwtRoleCodes.Count > 0) | ||
| 171 | + { | ||
| 172 | + var codes = jwtRoleCodes | ||
| 173 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | ||
| 174 | + .Select(x => x.Trim()) | ||
| 175 | + .Distinct() | ||
| 176 | + .ToList(); | ||
| 177 | + names = await _dbContext.SqlSugarClient.Queryable<RoleDbEntity>() | ||
| 178 | + .Where(r => !r.IsDeleted && codes.Contains(r.RoleCode)) | ||
| 179 | + .OrderBy(r => r.RoleName) | ||
| 180 | + .Select(r => r.RoleName) | ||
| 181 | + .ToListAsync(); | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + var distinctNames = names | ||
| 185 | + .Where(n => !string.IsNullOrWhiteSpace(n)) | ||
| 186 | + .Select(n => n.Trim()) | ||
| 187 | + .Distinct() | ||
| 188 | + .ToList(); | ||
| 189 | + if (distinctNames.Count > 0) | ||
| 190 | + { | ||
| 191 | + return string.Join(", ", distinctNames); | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + var fallbackCodes = jwtRoleCodes | ||
| 195 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | ||
| 196 | + .Select(x => x.Trim()) | ||
| 197 | + .Distinct() | ||
| 198 | + .ToList(); | ||
| 199 | + return fallbackCodes.Count > 0 ? string.Join(", ", fallbackCodes) : string.Empty; | ||
| 200 | + } | ||
| 201 | + | ||
| 136 | private static List<CurrentUserMenuNodeDto> BuildMenuTree(List<CurrentUserMenuNodeDto> flat) | 202 | private static List<CurrentUserMenuNodeDto> BuildMenuTree(List<CurrentUserMenuNodeDto> flat) |
| 137 | { | 203 | { |
| 138 | var nodes = flat | 204 | var nodes = flat |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlPartnerDbEntity.cs
| @@ -36,6 +36,21 @@ public class FlPartnerDbEntity | @@ -36,6 +36,21 @@ public class FlPartnerDbEntity | ||
| 36 | /// </summary> | 36 | /// </summary> |
| 37 | public string? PhoneNumber { get; set; } | 37 | public string? PhoneNumber { get; set; } |
| 38 | 38 | ||
| 39 | + public string? Street { get; set; } | ||
| 40 | + | ||
| 41 | + public string? City { get; set; } | ||
| 42 | + | ||
| 43 | + /// <summary> | ||
| 44 | + /// 州/省代码(如 NY);勿与启用状态字段 <see cref="State"/> 混淆 | ||
| 45 | + /// </summary> | ||
| 46 | + [SugarColumn(ColumnName = "StateCode")] | ||
| 47 | + public string? StateCode { get; set; } | ||
| 48 | + | ||
| 49 | + public string? Country { get; set; } | ||
| 50 | + | ||
| 51 | + [SugarColumn(ColumnName = "ZipCode")] | ||
| 52 | + public string? ZipCode { get; set; } | ||
| 53 | + | ||
| 39 | /// <summary> | 54 | /// <summary> |
| 40 | /// 是否启用(对应 UI Active) | 55 | /// 是否启用(对应 UI Active) |
| 41 | /// </summary> | 56 | /// </summary> |
美国版/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 | @@ -36,13 +36,13 @@ public class GroupAppService : ApplicationService, IGroupAppService | ||
| 36 | public async Task<PagedResultWithPageDto<GroupGetListOutputDto>> GetListAsync(GroupGetListInputVo input) | 36 | public async Task<PagedResultWithPageDto<GroupGetListOutputDto>> GetListAsync(GroupGetListInputVo input) |
| 37 | { | 37 | { |
| 38 | RefAsync<int> total = 0; | 38 | RefAsync<int> total = 0; |
| 39 | - var query = BuildGroupJoinedQuery(input); | 39 | + var query = await BuildGroupJoinedQueryAsync(input); |
| 40 | var projected = query.Select((g, p) => new GroupGetListOutputDto | 40 | var projected = query.Select((g, p) => new GroupGetListOutputDto |
| 41 | { | 41 | { |
| 42 | Id = g.Id, | 42 | Id = g.Id, |
| 43 | GroupName = g.GroupName, | 43 | GroupName = g.GroupName, |
| 44 | PartnerId = g.PartnerId, | 44 | PartnerId = g.PartnerId, |
| 45 | - PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "无" : p.PartnerName.Trim(), | 45 | + PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "None" : p.PartnerName.Trim(), |
| 46 | State = g.State, | 46 | State = g.State, |
| 47 | CreationTime = g.CreationTime | 47 | CreationTime = g.CreationTime |
| 48 | }); | 48 | }); |
| @@ -52,19 +52,19 @@ public class GroupAppService : ApplicationService, IGroupAppService | @@ -52,19 +52,19 @@ public class GroupAppService : ApplicationService, IGroupAppService | ||
| 52 | } | 52 | } |
| 53 | 53 | ||
| 54 | /// <inheritdoc /> | 54 | /// <inheritdoc /> |
| 55 | - public async Task<GroupGetOutputDto> GetAsync(string id) | 55 | + public async Task<GroupGetOutputDto> GetAsync(Guid id) |
| 56 | { | 56 | { |
| 57 | - var groupId = id?.Trim(); | ||
| 58 | - if (string.IsNullOrWhiteSpace(groupId)) | 57 | + if (id == Guid.Empty) |
| 59 | { | 58 | { |
| 60 | - throw new UserFriendlyException("组织Id不能为空"); | 59 | + throw new UserFriendlyException("Group id is required."); |
| 61 | } | 60 | } |
| 62 | 61 | ||
| 62 | + var groupId = id.ToString(); | ||
| 63 | var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | 63 | var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() |
| 64 | .FirstAsync(x => !x.IsDeleted && x.Id == groupId); | 64 | .FirstAsync(x => !x.IsDeleted && x.Id == groupId); |
| 65 | if (entity is null) | 65 | if (entity is null) |
| 66 | { | 66 | { |
| 67 | - throw new UserFriendlyException("组织不存在"); | 67 | + throw new UserFriendlyException("Group not found."); |
| 68 | } | 68 | } |
| 69 | 69 | ||
| 70 | var partnerName = await ResolvePartnerNameAsync(entity.PartnerId); | 70 | var partnerName = await ResolvePartnerNameAsync(entity.PartnerId); |
| @@ -78,13 +78,13 @@ public class GroupAppService : ApplicationService, IGroupAppService | @@ -78,13 +78,13 @@ public class GroupAppService : ApplicationService, IGroupAppService | ||
| 78 | var name = input.GroupName?.Trim(); | 78 | var name = input.GroupName?.Trim(); |
| 79 | if (string.IsNullOrWhiteSpace(name)) | 79 | if (string.IsNullOrWhiteSpace(name)) |
| 80 | { | 80 | { |
| 81 | - throw new UserFriendlyException("组织名称不能为空"); | 81 | + throw new UserFriendlyException("Region name is required."); |
| 82 | } | 82 | } |
| 83 | 83 | ||
| 84 | var partnerId = input.PartnerId?.Trim(); | 84 | var partnerId = input.PartnerId?.Trim(); |
| 85 | if (string.IsNullOrWhiteSpace(partnerId)) | 85 | if (string.IsNullOrWhiteSpace(partnerId)) |
| 86 | { | 86 | { |
| 87 | - throw new UserFriendlyException("请选择所属合作伙伴"); | 87 | + throw new UserFriendlyException("Parent company is required."); |
| 88 | } | 88 | } |
| 89 | 89 | ||
| 90 | await EnsurePartnerExistsAsync(partnerId); | 90 | await EnsurePartnerExistsAsync(partnerId); |
| @@ -104,36 +104,36 @@ public class GroupAppService : ApplicationService, IGroupAppService | @@ -104,36 +104,36 @@ public class GroupAppService : ApplicationService, IGroupAppService | ||
| 104 | }; | 104 | }; |
| 105 | 105 | ||
| 106 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); | 106 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); |
| 107 | - return await GetAsync(entity.Id); | 107 | + return await GetAsync(Guid.Parse(entity.Id)); |
| 108 | } | 108 | } |
| 109 | 109 | ||
| 110 | /// <inheritdoc /> | 110 | /// <inheritdoc /> |
| 111 | [UnitOfWork] | 111 | [UnitOfWork] |
| 112 | - public async Task<GroupGetOutputDto> UpdateAsync(string id, GroupUpdateInputVo input) | 112 | + public async Task<GroupGetOutputDto> UpdateAsync(Guid id, GroupUpdateInputVo input) |
| 113 | { | 113 | { |
| 114 | - var groupId = id?.Trim(); | ||
| 115 | - if (string.IsNullOrWhiteSpace(groupId)) | 114 | + if (id == Guid.Empty) |
| 116 | { | 115 | { |
| 117 | - throw new UserFriendlyException("组织Id不能为空"); | 116 | + throw new UserFriendlyException("Group id is required."); |
| 118 | } | 117 | } |
| 119 | 118 | ||
| 119 | + var groupId = id.ToString(); | ||
| 120 | var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | 120 | var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() |
| 121 | .FirstAsync(x => !x.IsDeleted && x.Id == groupId); | 121 | .FirstAsync(x => !x.IsDeleted && x.Id == groupId); |
| 122 | if (entity is null) | 122 | if (entity is null) |
| 123 | { | 123 | { |
| 124 | - throw new UserFriendlyException("组织不存在"); | 124 | + throw new UserFriendlyException("Group not found."); |
| 125 | } | 125 | } |
| 126 | 126 | ||
| 127 | var name = input.GroupName?.Trim(); | 127 | var name = input.GroupName?.Trim(); |
| 128 | if (string.IsNullOrWhiteSpace(name)) | 128 | if (string.IsNullOrWhiteSpace(name)) |
| 129 | { | 129 | { |
| 130 | - throw new UserFriendlyException("组织名称不能为空"); | 130 | + throw new UserFriendlyException("Region name is required."); |
| 131 | } | 131 | } |
| 132 | 132 | ||
| 133 | var partnerId = input.PartnerId?.Trim(); | 133 | var partnerId = input.PartnerId?.Trim(); |
| 134 | if (string.IsNullOrWhiteSpace(partnerId)) | 134 | if (string.IsNullOrWhiteSpace(partnerId)) |
| 135 | { | 135 | { |
| 136 | - throw new UserFriendlyException("请选择所属合作伙伴"); | 136 | + throw new UserFriendlyException("Parent company is required."); |
| 137 | } | 137 | } |
| 138 | 138 | ||
| 139 | await EnsurePartnerExistsAsync(partnerId); | 139 | await EnsurePartnerExistsAsync(partnerId); |
| @@ -145,24 +145,24 @@ public class GroupAppService : ApplicationService, IGroupAppService | @@ -145,24 +145,24 @@ public class GroupAppService : ApplicationService, IGroupAppService | ||
| 145 | entity.LastModifierId = CurrentUser?.Id?.ToString(); | 145 | entity.LastModifierId = CurrentUser?.Id?.ToString(); |
| 146 | 146 | ||
| 147 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | 147 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 148 | - return await GetAsync(groupId); | 148 | + return await GetAsync(id); |
| 149 | } | 149 | } |
| 150 | 150 | ||
| 151 | /// <inheritdoc /> | 151 | /// <inheritdoc /> |
| 152 | [UnitOfWork] | 152 | [UnitOfWork] |
| 153 | - public async Task DeleteAsync(string id) | 153 | + public async Task DeleteAsync(Guid id) |
| 154 | { | 154 | { |
| 155 | - var groupId = id?.Trim(); | ||
| 156 | - if (string.IsNullOrWhiteSpace(groupId)) | 155 | + if (id == Guid.Empty) |
| 157 | { | 156 | { |
| 158 | - throw new UserFriendlyException("组织Id不能为空"); | 157 | + throw new UserFriendlyException("Group id is required."); |
| 159 | } | 158 | } |
| 160 | 159 | ||
| 160 | + var groupId = id.ToString(); | ||
| 161 | var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | 161 | var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() |
| 162 | .FirstAsync(x => !x.IsDeleted && x.Id == groupId); | 162 | .FirstAsync(x => !x.IsDeleted && x.Id == groupId); |
| 163 | if (entity is null) | 163 | if (entity is null) |
| 164 | { | 164 | { |
| 165 | - throw new UserFriendlyException("组织不存在"); | 165 | + throw new UserFriendlyException("Group not found."); |
| 166 | } | 166 | } |
| 167 | 167 | ||
| 168 | entity.IsDeleted = true; | 168 | entity.IsDeleted = true; |
| @@ -172,24 +172,26 @@ public class GroupAppService : ApplicationService, IGroupAppService | @@ -172,24 +172,26 @@ public class GroupAppService : ApplicationService, IGroupAppService | ||
| 172 | } | 172 | } |
| 173 | 173 | ||
| 174 | /// <inheritdoc /> | 174 | /// <inheritdoc /> |
| 175 | - public async Task<IActionResult> ExportPdfAsync(GroupGetListInputVo input) | 175 | + [HttpGet] |
| 176 | + public async Task<IActionResult> ExportPdfAsync([FromQuery] GroupGetListInputVo input) | ||
| 176 | { | 177 | { |
| 177 | QuestPDF.Settings.License = LicenseType.Community; | 178 | QuestPDF.Settings.License = LicenseType.Community; |
| 178 | 179 | ||
| 179 | - var exportBase = BuildGroupJoinedQuery(input); | 180 | + var exportBase = await BuildGroupJoinedQueryAsync(input); |
| 180 | var count = await exportBase.CountAsync(); | 181 | var count = await exportBase.CountAsync(); |
| 181 | if (count > ExportPdfMaxRows) | 182 | if (count > ExportPdfMaxRows) |
| 182 | { | 183 | { |
| 183 | - throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); | 184 | + throw new UserFriendlyException( |
| 185 | + $"Export exceeds the maximum of {ExportPdfMaxRows} rows. Narrow your filters and try again."); | ||
| 184 | } | 186 | } |
| 185 | 187 | ||
| 186 | - var rows = await BuildGroupJoinedQuery(input) | 188 | + var rows = await (await BuildGroupJoinedQueryAsync(input)) |
| 187 | .Select((g, p) => new GroupGetListOutputDto | 189 | .Select((g, p) => new GroupGetListOutputDto |
| 188 | { | 190 | { |
| 189 | Id = g.Id, | 191 | Id = g.Id, |
| 190 | GroupName = g.GroupName, | 192 | GroupName = g.GroupName, |
| 191 | PartnerId = g.PartnerId, | 193 | PartnerId = g.PartnerId, |
| 192 | - PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "无" : p.PartnerName.Trim(), | 194 | + PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "None" : p.PartnerName.Trim(), |
| 193 | State = g.State, | 195 | State = g.State, |
| 194 | CreationTime = g.CreationTime | 196 | CreationTime = g.CreationTime |
| 195 | }) | 197 | }) |
| @@ -228,7 +230,7 @@ public class GroupAppService : ApplicationService, IGroupAppService | @@ -228,7 +230,7 @@ public class GroupAppService : ApplicationService, IGroupAppService | ||
| 228 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | 230 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) |
| 229 | .Text(e.GroupName ?? string.Empty); | 231 | .Text(e.GroupName ?? string.Empty); |
| 230 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | 232 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) |
| 231 | - .Text(string.IsNullOrWhiteSpace(e.PartnerName) ? "无" : e.PartnerName); | 233 | + .Text(string.IsNullOrWhiteSpace(e.PartnerName) ? "None" : e.PartnerName); |
| 232 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status); | 234 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status); |
| 233 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | 235 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) |
| 234 | .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm")); | 236 | .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm")); |
| @@ -243,14 +245,21 @@ public class GroupAppService : ApplicationService, IGroupAppService | @@ -243,14 +245,21 @@ public class GroupAppService : ApplicationService, IGroupAppService | ||
| 243 | return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName }; | 245 | return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName }; |
| 244 | } | 246 | } |
| 245 | 247 | ||
| 246 | - private ISugarQueryable<FlGroupDbEntity, FlPartnerDbEntity> BuildGroupJoinedQuery(GroupGetListInputVo input) | 248 | + private async Task<ISugarQueryable<FlGroupDbEntity, FlPartnerDbEntity>> BuildGroupJoinedQueryAsync( |
| 249 | + GroupGetListInputVo input) | ||
| 247 | { | 250 | { |
| 251 | + var scope = await PartnerScopeHelper.ResolveGroupScopeAsync(CurrentUser, _dbContext); | ||
| 252 | + | ||
| 248 | var keyword = input.Keyword?.Trim(); | 253 | var keyword = input.Keyword?.Trim(); |
| 249 | var partnerId = input.PartnerId?.Trim(); | 254 | var partnerId = input.PartnerId?.Trim(); |
| 250 | 255 | ||
| 251 | var query = _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | 256 | var query = _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() |
| 252 | .LeftJoin<FlPartnerDbEntity>((g, p) => g.PartnerId == p.Id && !p.IsDeleted) | 257 | .LeftJoin<FlPartnerDbEntity>((g, p) => g.PartnerId == p.Id && !p.IsDeleted) |
| 253 | - .Where((g, p) => !g.IsDeleted) | 258 | + .Where((g, p) => !g.IsDeleted); |
| 259 | + | ||
| 260 | + query = PartnerScopeHelper.ApplyGroupScope(query, scope); | ||
| 261 | + | ||
| 262 | + query = query | ||
| 254 | .WhereIF(input.State != null, (g, p) => g.State == input.State) | 263 | .WhereIF(input.State != null, (g, p) => g.State == input.State) |
| 255 | .WhereIF(!string.IsNullOrWhiteSpace(partnerId), (g, p) => g.PartnerId == partnerId) | 264 | .WhereIF(!string.IsNullOrWhiteSpace(partnerId), (g, p) => g.PartnerId == partnerId) |
| 256 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), | 265 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), |
| @@ -311,7 +320,7 @@ public class GroupAppService : ApplicationService, IGroupAppService | @@ -311,7 +320,7 @@ public class GroupAppService : ApplicationService, IGroupAppService | ||
| 311 | .AnyAsync(x => !x.IsDeleted && x.Id == partnerId); | 320 | .AnyAsync(x => !x.IsDeleted && x.Id == partnerId); |
| 312 | if (!ok) | 321 | if (!ok) |
| 313 | { | 322 | { |
| 314 | - throw new UserFriendlyException("所选合作伙伴不存在或已删除"); | 323 | + throw new UserFriendlyException("The selected company does not exist or has been removed."); |
| 315 | } | 324 | } |
| 316 | } | 325 | } |
| 317 | 326 | ||
| @@ -321,7 +330,7 @@ public class GroupAppService : ApplicationService, IGroupAppService | @@ -321,7 +330,7 @@ public class GroupAppService : ApplicationService, IGroupAppService | ||
| 321 | .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); | 330 | .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); |
| 322 | if (p is null || string.IsNullOrWhiteSpace(p.PartnerName)) | 331 | if (p is null || string.IsNullOrWhiteSpace(p.PartnerName)) |
| 323 | { | 332 | { |
| 324 | - return "无"; | 333 | + return "None"; |
| 325 | } | 334 | } |
| 326 | 335 | ||
| 327 | return p.PartnerName.Trim(); | 336 | return p.PartnerName.Trim(); |
美国版/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 | @@ -34,6 +34,7 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 34 | RefAsync<int> total = 0; | 34 | RefAsync<int> total = 0; |
| 35 | 35 | ||
| 36 | var productId = input.ProductId?.Trim(); | 36 | var productId = input.ProductId?.Trim(); |
| 37 | + var groupId = input.GroupId?.Trim(); | ||
| 37 | var locationId = input.LocationId?.Trim(); | 38 | var locationId = input.LocationId?.Trim(); |
| 38 | var keyword = input.Keyword?.Trim(); | 39 | var keyword = input.Keyword?.Trim(); |
| 39 | var labelCategoryId = input.LabelCategoryId?.Trim(); | 40 | var labelCategoryId = input.LabelCategoryId?.Trim(); |
| @@ -45,7 +46,6 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -45,7 +46,6 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 45 | 46 | ||
| 46 | var labelIdsQuery = _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>() | 47 | var labelIdsQuery = _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>() |
| 47 | .Where(l => !l.IsDeleted) | 48 | .Where(l => !l.IsDeleted) |
| 48 | - .WhereIF(!string.IsNullOrWhiteSpace(locationId), l => l.LocationId == locationId) | ||
| 49 | .WhereIF(!string.IsNullOrWhiteSpace(labelCategoryId), l => l.LabelCategoryId == labelCategoryId) | 49 | .WhereIF(!string.IsNullOrWhiteSpace(labelCategoryId), l => l.LabelCategoryId == labelCategoryId) |
| 50 | .WhereIF(!string.IsNullOrWhiteSpace(labelTypeId), l => l.LabelTypeId == labelTypeId) | 50 | .WhereIF(!string.IsNullOrWhiteSpace(labelTypeId), l => l.LabelTypeId == labelTypeId) |
| 51 | .WhereIF(input.State != null, l => l.State == input.State); | 51 | .WhereIF(input.State != null, l => l.State == input.State); |
| @@ -58,6 +58,15 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -58,6 +58,15 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 58 | .Select((l, tpl) => l); | 58 | .Select((l, tpl) => l); |
| 59 | } | 59 | } |
| 60 | 60 | ||
| 61 | + var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( | ||
| 62 | + _dbContext.SqlSugarClient, groupId, locationId); | ||
| 63 | + if (scopedLocationIds is not null) | ||
| 64 | + { | ||
| 65 | + labelIdsQuery = scopedLocationIds.Count == 0 | ||
| 66 | + ? labelIdsQuery.Where(_ => false) | ||
| 67 | + : labelIdsQuery.Where(l => scopedLocationIds.Contains(l.LocationId)); | ||
| 68 | + } | ||
| 69 | + | ||
| 61 | // 按产品筛选:存在 label-product 关联即可 | 70 | // 按产品筛选:存在 label-product 关联即可 |
| 62 | if (!string.IsNullOrWhiteSpace(productId)) | 71 | if (!string.IsNullOrWhiteSpace(productId)) |
| 63 | { | 72 | { |
| @@ -259,12 +268,25 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -259,12 +268,25 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 259 | labelInfo = JsonSerializer.Deserialize<object>(label.LabelInfoJson); | 268 | labelInfo = JsonSerializer.Deserialize<object>(label.LabelInfoJson); |
| 260 | } | 269 | } |
| 261 | 270 | ||
| 271 | + var locationId = label.LocationId ?? string.Empty; | ||
| 272 | + var locationIdList = string.IsNullOrWhiteSpace(locationId) | ||
| 273 | + ? new List<string>() | ||
| 274 | + : new List<string> { locationId.Trim() }; | ||
| 275 | + var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( | ||
| 276 | + _dbContext.SqlSugarClient, locationIdList); | ||
| 277 | + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( | ||
| 278 | + _dbContext.SqlSugarClient, locationIdList); | ||
| 279 | + | ||
| 262 | return new LabelGetOutputDto | 280 | return new LabelGetOutputDto |
| 263 | { | 281 | { |
| 264 | Id = label.LabelCode ?? string.Empty, | 282 | Id = label.LabelCode ?? string.Empty, |
| 265 | LabelName = label.LabelName, | 283 | LabelName = label.LabelName, |
| 266 | - LocationId = label.LocationId ?? string.Empty, | 284 | + LocationId = locationId, |
| 267 | LocationName = location?.LocationName ?? location?.LocationCode ?? "无", | 285 | LocationName = location?.LocationName ?? location?.LocationCode ?? "无", |
| 286 | + PartnerId = partnerIds.Count > 0 ? partnerIds[0] : null, | ||
| 287 | + PartnerIds = partnerIds, | ||
| 288 | + RegionIds = regionIds, | ||
| 289 | + GroupIds = regionIds, | ||
| 268 | LabelCategoryId = label.LabelCategoryId ?? string.Empty, | 290 | LabelCategoryId = label.LabelCategoryId ?? string.Empty, |
| 269 | LabelCategoryName = category?.CategoryName ?? "无", | 291 | LabelCategoryName = category?.CategoryName ?? "无", |
| 270 | LabelTypeId = label.LabelTypeId ?? string.Empty, | 292 | LabelTypeId = label.LabelTypeId ?? string.Empty, |
| @@ -301,10 +323,6 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -301,10 +323,6 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 301 | { | 323 | { |
| 302 | throw new UserFriendlyException("模板编码不能为空"); | 324 | throw new UserFriendlyException("模板编码不能为空"); |
| 303 | } | 325 | } |
| 304 | - if (string.IsNullOrWhiteSpace(input.LocationId)) | ||
| 305 | - { | ||
| 306 | - throw new UserFriendlyException("门店Id不能为空"); | ||
| 307 | - } | ||
| 308 | if (string.IsNullOrWhiteSpace(input.LabelCategoryId)) | 326 | if (string.IsNullOrWhiteSpace(input.LabelCategoryId)) |
| 309 | { | 327 | { |
| 310 | throw new UserFriendlyException("标签分类Id不能为空"); | 328 | throw new UserFriendlyException("标签分类Id不能为空"); |
| @@ -314,6 +332,8 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -314,6 +332,8 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 314 | throw new UserFriendlyException("标签类型Id不能为空"); | 332 | throw new UserFriendlyException("标签类型Id不能为空"); |
| 315 | } | 333 | } |
| 316 | 334 | ||
| 335 | + var resolvedLocationId = await ResolveLabelLocationIdForSaveAsync(input); | ||
| 336 | + | ||
| 317 | var template = await _dbContext.SqlSugarClient.Queryable<FlLabelTemplateDbEntity>() | 337 | var template = await _dbContext.SqlSugarClient.Queryable<FlLabelTemplateDbEntity>() |
| 318 | .FirstAsync(x => !x.IsDeleted && x.TemplateCode == input.TemplateCode.Trim()); | 338 | .FirstAsync(x => !x.IsDeleted && x.TemplateCode == input.TemplateCode.Trim()); |
| 319 | if (template is null) | 339 | if (template is null) |
| @@ -344,7 +364,7 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -344,7 +364,7 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 344 | LabelCode = labelCode, | 364 | LabelCode = labelCode, |
| 345 | LabelName = labelName, | 365 | LabelName = labelName, |
| 346 | TemplateId = template.Id, | 366 | TemplateId = template.Id, |
| 347 | - LocationId = input.LocationId?.Trim(), | 367 | + LocationId = resolvedLocationId, |
| 348 | LabelCategoryId = input.LabelCategoryId?.Trim(), | 368 | LabelCategoryId = input.LabelCategoryId?.Trim(), |
| 349 | LabelTypeId = input.LabelTypeId?.Trim(), | 369 | LabelTypeId = input.LabelTypeId?.Trim(), |
| 350 | State = input.State, | 370 | State = input.State, |
| @@ -363,6 +383,9 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -363,6 +383,9 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 363 | }).ToList(); | 383 | }).ToList(); |
| 364 | await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); | 384 | await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); |
| 365 | 385 | ||
| 386 | + await LabelCategoryAppService.TouchLabelCategoryLastEditedAsync( | ||
| 387 | + _dbContext, labelEntity.LabelCategoryId, CurrentUser?.Id?.ToString()); | ||
| 388 | + | ||
| 366 | return await GetAsync(labelCode); | 389 | return await GetAsync(labelCode); |
| 367 | } | 390 | } |
| 368 | 391 | ||
| @@ -391,10 +414,6 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -391,10 +414,6 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 391 | { | 414 | { |
| 392 | throw new UserFriendlyException("模板编码不能为空"); | 415 | throw new UserFriendlyException("模板编码不能为空"); |
| 393 | } | 416 | } |
| 394 | - if (string.IsNullOrWhiteSpace(input.LocationId)) | ||
| 395 | - { | ||
| 396 | - throw new UserFriendlyException("门店Id不能为空"); | ||
| 397 | - } | ||
| 398 | if (string.IsNullOrWhiteSpace(input.LabelCategoryId)) | 417 | if (string.IsNullOrWhiteSpace(input.LabelCategoryId)) |
| 399 | { | 418 | { |
| 400 | throw new UserFriendlyException("标签分类Id不能为空"); | 419 | throw new UserFriendlyException("标签分类Id不能为空"); |
| @@ -404,6 +423,8 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -404,6 +423,8 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 404 | throw new UserFriendlyException("标签类型Id不能为空"); | 423 | throw new UserFriendlyException("标签类型Id不能为空"); |
| 405 | } | 424 | } |
| 406 | 425 | ||
| 426 | + var resolvedLocationId = await ResolveLabelLocationIdForSaveAsync(input); | ||
| 427 | + | ||
| 407 | var template = await _dbContext.SqlSugarClient.Queryable<FlLabelTemplateDbEntity>() | 428 | var template = await _dbContext.SqlSugarClient.Queryable<FlLabelTemplateDbEntity>() |
| 408 | .FirstAsync(x => !x.IsDeleted && x.TemplateCode == input.TemplateCode.Trim()); | 429 | .FirstAsync(x => !x.IsDeleted && x.TemplateCode == input.TemplateCode.Trim()); |
| 409 | if (template is null) | 430 | if (template is null) |
| @@ -411,10 +432,11 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -411,10 +432,11 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 411 | throw new UserFriendlyException("模板不存在"); | 432 | throw new UserFriendlyException("模板不存在"); |
| 412 | } | 433 | } |
| 413 | 434 | ||
| 435 | + var oldCategoryId = label.LabelCategoryId; | ||
| 414 | var now = DateTime.Now; | 436 | var now = DateTime.Now; |
| 415 | label.LabelName = input.LabelName?.Trim() ?? label.LabelName; | 437 | label.LabelName = input.LabelName?.Trim() ?? label.LabelName; |
| 416 | label.TemplateId = template.Id; | 438 | label.TemplateId = template.Id; |
| 417 | - label.LocationId = input.LocationId?.Trim(); | 439 | + label.LocationId = resolvedLocationId; |
| 418 | label.LabelCategoryId = input.LabelCategoryId?.Trim(); | 440 | label.LabelCategoryId = input.LabelCategoryId?.Trim(); |
| 419 | label.LabelTypeId = input.LabelTypeId?.Trim(); | 441 | label.LabelTypeId = input.LabelTypeId?.Trim(); |
| 420 | label.State = input.State; | 442 | label.State = input.State; |
| @@ -438,6 +460,15 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -438,6 +460,15 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 438 | }).ToList(); | 460 | }).ToList(); |
| 439 | await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); | 461 | await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); |
| 440 | 462 | ||
| 463 | + var newCategoryId = label.LabelCategoryId; | ||
| 464 | + await LabelCategoryAppService.TouchLabelCategoryLastEditedAsync( | ||
| 465 | + _dbContext, newCategoryId, CurrentUser?.Id?.ToString()); | ||
| 466 | + if (!string.Equals(oldCategoryId, newCategoryId, StringComparison.Ordinal)) | ||
| 467 | + { | ||
| 468 | + await LabelCategoryAppService.TouchLabelCategoryLastEditedAsync( | ||
| 469 | + _dbContext, oldCategoryId, CurrentUser?.Id?.ToString()); | ||
| 470 | + } | ||
| 471 | + | ||
| 441 | return await GetAsync(labelCode); | 472 | return await GetAsync(labelCode); |
| 442 | } | 473 | } |
| 443 | 474 | ||
| @@ -465,6 +496,9 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -465,6 +496,9 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 465 | await _dbContext.SqlSugarClient.Deleteable<FlLabelProductDbEntity>() | 496 | await _dbContext.SqlSugarClient.Deleteable<FlLabelProductDbEntity>() |
| 466 | .Where(x => x.LabelId == label.Id) | 497 | .Where(x => x.LabelId == label.Id) |
| 467 | .ExecuteCommandAsync(); | 498 | .ExecuteCommandAsync(); |
| 499 | + | ||
| 500 | + await LabelCategoryAppService.TouchLabelCategoryLastEditedAsync( | ||
| 501 | + _dbContext, label.LabelCategoryId, CurrentUser?.Id?.ToString()); | ||
| 468 | } | 502 | } |
| 469 | 503 | ||
| 470 | /// <summary> | 504 | /// <summary> |
| @@ -730,5 +764,86 @@ public class LabelAppService : ApplicationService, ILabelAppService | @@ -730,5 +764,86 @@ public class LabelAppService : ApplicationService, ILabelAppService | ||
| 730 | Elements = resolvedElements | 764 | Elements = resolvedElements |
| 731 | }; | 765 | }; |
| 732 | } | 766 | } |
| 767 | + | ||
| 768 | + private Task<string> ResolveLabelLocationIdForSaveAsync(LabelUpdateInputVo input) => | ||
| 769 | + ResolveLabelLocationIdForSaveAsync(new LabelCreateInputVo | ||
| 770 | + { | ||
| 771 | + PartnerId = input.PartnerId, | ||
| 772 | + PartnerIds = input.PartnerIds, | ||
| 773 | + RegionIds = input.RegionIds, | ||
| 774 | + GroupIds = input.GroupIds, | ||
| 775 | + LocationId = input.LocationId, | ||
| 776 | + LocationIds = input.LocationIds | ||
| 777 | + }); | ||
| 778 | + | ||
| 779 | + /// <summary> | ||
| 780 | + /// 标签仅绑定单门店:解析 Company/Region/门店入参为唯一 <c>location.Id</c>。 | ||
| 781 | + /// </summary> | ||
| 782 | + private async Task<string> ResolveLabelLocationIdForSaveAsync(LabelCreateInputVo input) | ||
| 783 | + { | ||
| 784 | + var partnerIds = NormalizePartnerIds(input); | ||
| 785 | + var regionIds = NormalizeRegionIds(input); | ||
| 786 | + var explicitLocationId = input.LocationId?.Trim(); | ||
| 787 | + var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); | ||
| 788 | + | ||
| 789 | + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | ||
| 790 | + _dbContext.SqlSugarClient, partnerIds, regionIds, explicitLocationIds); | ||
| 791 | + | ||
| 792 | + if (!string.IsNullOrWhiteSpace(explicitLocationId)) | ||
| 793 | + { | ||
| 794 | + if (merged.Count > 0 && !merged.Contains(explicitLocationId, StringComparer.Ordinal)) | ||
| 795 | + { | ||
| 796 | + throw new UserFriendlyException("所选门店不在指定公司/区域范围内"); | ||
| 797 | + } | ||
| 798 | + | ||
| 799 | + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync( | ||
| 800 | + _dbContext.SqlSugarClient, new List<string> { explicitLocationId }); | ||
| 801 | + return explicitLocationId; | ||
| 802 | + } | ||
| 803 | + | ||
| 804 | + if (merged.Count == 0) | ||
| 805 | + { | ||
| 806 | + throw new UserFriendlyException("须指定门店 locationId,或选择公司/区域以解析出门店"); | ||
| 807 | + } | ||
| 808 | + | ||
| 809 | + if (merged.Count > 1) | ||
| 810 | + { | ||
| 811 | + throw new UserFriendlyException("所选公司/区域对应多个门店,请显式指定 locationId"); | ||
| 812 | + } | ||
| 813 | + | ||
| 814 | + return merged[0]; | ||
| 815 | + } | ||
| 816 | + | ||
| 817 | + private static List<string> NormalizePartnerIds(LabelCreateInputVo input) | ||
| 818 | + { | ||
| 819 | + var merged = new HashSet<string>(StringComparer.Ordinal); | ||
| 820 | + if (!string.IsNullOrWhiteSpace(input.PartnerId)) | ||
| 821 | + { | ||
| 822 | + merged.Add(input.PartnerId.Trim()); | ||
| 823 | + } | ||
| 824 | + | ||
| 825 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.PartnerIds)) | ||
| 826 | + { | ||
| 827 | + merged.Add(id); | ||
| 828 | + } | ||
| 829 | + | ||
| 830 | + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); | ||
| 831 | + } | ||
| 832 | + | ||
| 833 | + private static List<string> NormalizeRegionIds(LabelCreateInputVo input) | ||
| 834 | + { | ||
| 835 | + var merged = new HashSet<string>(StringComparer.Ordinal); | ||
| 836 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds)) | ||
| 837 | + { | ||
| 838 | + merged.Add(id); | ||
| 839 | + } | ||
| 840 | + | ||
| 841 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds)) | ||
| 842 | + { | ||
| 843 | + merged.Add(id); | ||
| 844 | + } | ||
| 845 | + | ||
| 846 | + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); | ||
| 847 | + } | ||
| 733 | } | 848 | } |
| 734 | 849 |
美国版/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; | @@ -3,6 +3,7 @@ using FoodLabeling.Application.Contracts.Dtos.Common; | ||
| 3 | using FoodLabeling.Application.Contracts.Dtos.LabelCategory; | 3 | using FoodLabeling.Application.Contracts.Dtos.LabelCategory; |
| 4 | using FoodLabeling.Application.Contracts.IServices; | 4 | using FoodLabeling.Application.Contracts.IServices; |
| 5 | using FoodLabeling.Application.Services.DbModels; | 5 | using FoodLabeling.Application.Services.DbModels; |
| 6 | +using FoodLabeling.Domain.Entities; | ||
| 6 | using SqlSugar; | 7 | using SqlSugar; |
| 7 | using Volo.Abp; | 8 | using Volo.Abp; |
| 8 | using Volo.Abp.Application.Services; | 9 | using Volo.Abp.Application.Services; |
| @@ -34,6 +35,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | @@ -34,6 +35,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | ||
| 34 | (x.DisplayText != null && x.DisplayText.Contains(keyword!))) | 35 | (x.DisplayText != null && x.DisplayText.Contains(keyword!))) |
| 35 | .WhereIF(input.State != null, x => x.State == input.State); | 36 | .WhereIF(input.State != null, x => x.State == input.State); |
| 36 | 37 | ||
| 38 | + query = await ApplyCategoryScopeFilterAsync(query, input.GroupId, input.LocationId); | ||
| 39 | + | ||
| 37 | // Sorting 仅允许白名单字段,避免 Unknown column/注入风险 | 40 | // Sorting 仅允许白名单字段,避免 Unknown column/注入风险 |
| 38 | if (!string.IsNullOrWhiteSpace(input.Sorting)) | 41 | if (!string.IsNullOrWhiteSpace(input.Sorting)) |
| 39 | { | 42 | { |
| @@ -67,57 +70,33 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | @@ -67,57 +70,33 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | ||
| 67 | var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); | 70 | var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); |
| 68 | var ids = entities.Select(x => x.Id).ToList(); | 71 | var ids = entities.Select(x => x.Id).ToList(); |
| 69 | 72 | ||
| 70 | - Dictionary<string, List<string>> locationIdsMap = new(); | ||
| 71 | - if (ids.Count > 0) | ||
| 72 | - { | ||
| 73 | - var locRows = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryLocationDbEntity>() | ||
| 74 | - .Where(x => ids.Contains(x.CategoryId)) | ||
| 75 | - .Select(x => new { x.CategoryId, x.LocationId }) | ||
| 76 | - .ToListAsync(); | ||
| 77 | - foreach (var row in locRows) | ||
| 78 | - { | ||
| 79 | - var cid = row.CategoryId?.Trim(); | ||
| 80 | - var lid = row.LocationId?.Trim(); | ||
| 81 | - if (string.IsNullOrWhiteSpace(cid) || string.IsNullOrWhiteSpace(lid)) | ||
| 82 | - { | ||
| 83 | - continue; | ||
| 84 | - } | ||
| 85 | - | ||
| 86 | - if (!locationIdsMap.TryGetValue(cid, out var list)) | ||
| 87 | - { | ||
| 88 | - list = new List<string>(); | ||
| 89 | - locationIdsMap[cid] = list; | ||
| 90 | - } | 73 | + var labelStatsMap = await BuildCategoryLabelStatsMapAsync(ids); |
| 74 | + var scopeMap = await BuildCategoryScopeMapAsync(entities); | ||
| 91 | 75 | ||
| 92 | - if (!list.Contains(lid)) | ||
| 93 | - { | ||
| 94 | - list.Add(lid); | ||
| 95 | - } | ||
| 96 | - } | ||
| 97 | - } | ||
| 98 | - | ||
| 99 | - var countRows = await _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>() | ||
| 100 | - .Where(x => !x.IsDeleted) | ||
| 101 | - .Where(x => x.LabelCategoryId != null && ids.Contains(x.LabelCategoryId)) | ||
| 102 | - .GroupBy(x => x.LabelCategoryId) | ||
| 103 | - .Select(x => new { CategoryId = x.LabelCategoryId, Count = SqlFunc.AggregateCount(x.Id) }) | ||
| 104 | - .ToListAsync(); | ||
| 105 | - var countMap = countRows.ToDictionary(x => x.CategoryId!, x => (long)x.Count); | ||
| 106 | - | ||
| 107 | - var items = entities.Select(x => new LabelCategoryGetListOutputDto | 76 | + var items = entities.Select(x => |
| 108 | { | 77 | { |
| 109 | - Id = x.Id, | ||
| 110 | - CategoryCode = x.CategoryCode, | ||
| 111 | - CategoryName = x.CategoryName, | ||
| 112 | - DisplayText = x.DisplayText, | ||
| 113 | - CategoryPhotoUrl = x.CategoryPhotoUrl, | ||
| 114 | - State = x.State, | ||
| 115 | - ButtonAppearance = x.ButtonAppearance, | ||
| 116 | - AvailabilityType = x.AvailabilityType, | ||
| 117 | - LocationIds = locationIdsMap.TryGetValue(x.Id, out var lids) ? lids : new List<string>(), | ||
| 118 | - OrderNum = x.OrderNum, | ||
| 119 | - NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0, | ||
| 120 | - LastEdited = x.LastModificationTime ?? x.CreationTime | 78 | + scopeMap.TryGetValue(x.Id, out var scope); |
| 79 | + labelStatsMap.TryGetValue(x.Id, out var labelStats); | ||
| 80 | + var creationTime = ResolveCategoryCreationTime(x); | ||
| 81 | + return new LabelCategoryGetListOutputDto | ||
| 82 | + { | ||
| 83 | + Id = x.Id, | ||
| 84 | + CategoryCode = x.CategoryCode, | ||
| 85 | + CategoryName = x.CategoryName, | ||
| 86 | + DisplayText = x.DisplayText, | ||
| 87 | + CategoryPhotoUrl = x.CategoryPhotoUrl, | ||
| 88 | + State = x.State, | ||
| 89 | + ButtonAppearance = x.ButtonAppearance, | ||
| 90 | + AvailabilityType = x.AvailabilityType, | ||
| 91 | + OrderNum = x.OrderNum, | ||
| 92 | + NoOfLabels = labelStats?.Count ?? 0, | ||
| 93 | + CreationTime = creationTime, | ||
| 94 | + LastEdited = ResolveLastEdited(x, creationTime, labelStats), | ||
| 95 | + Region = scope?.Region ?? string.Empty, | ||
| 96 | + Location = scope?.Location ?? string.Empty, | ||
| 97 | + RegionIds = scope?.RegionIds ?? new List<string>(), | ||
| 98 | + LocationIds = scope?.LocationIds ?? new List<string>() | ||
| 99 | + }; | ||
| 121 | }).ToList(); | 100 | }).ToList(); |
| 122 | 101 | ||
| 123 | return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); | 102 | return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); |
| @@ -139,7 +118,11 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | @@ -139,7 +118,11 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | ||
| 139 | .Where(x => x.CategoryId == entity.Id) | 118 | .Where(x => x.CategoryId == entity.Id) |
| 140 | .Select(x => x.LocationId) | 119 | .Select(x => x.LocationId) |
| 141 | .ToListAsync(); | 120 | .ToListAsync(); |
| 142 | - dto.LocationIds = locationIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList() ?? new(); | 121 | + dto.LocationIds = LocationScopeBindingHelper.NormalizeIds(locationIds); |
| 122 | + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( | ||
| 123 | + _dbContext.SqlSugarClient, dto.LocationIds); | ||
| 124 | + dto.RegionIds = regionIds; | ||
| 125 | + dto.GroupIds = regionIds; | ||
| 143 | } | 126 | } |
| 144 | 127 | ||
| 145 | return dto; | 128 | return dto; |
| @@ -156,9 +139,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | @@ -156,9 +139,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | ||
| 156 | 139 | ||
| 157 | var displayText = input.DisplayText?.Trim(); | 140 | var displayText = input.DisplayText?.Trim(); |
| 158 | var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); | 141 | var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); |
| 159 | - var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); | ||
| 160 | - var locationIds = NormalizeLocationIds(input.LocationIds); | ||
| 161 | - ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); | 142 | + var (availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); |
| 162 | 143 | ||
| 163 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() | 144 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() |
| 164 | .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); | 145 | .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); |
| @@ -172,6 +153,12 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | @@ -172,6 +153,12 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | ||
| 172 | var entity = new FlLabelCategoryDbEntity | 153 | var entity = new FlLabelCategoryDbEntity |
| 173 | { | 154 | { |
| 174 | Id = _guidGenerator.Create().ToString(), | 155 | Id = _guidGenerator.Create().ToString(), |
| 156 | + IsDeleted = false, | ||
| 157 | + CreationTime = now, | ||
| 158 | + CreatorId = currentUserId, | ||
| 159 | + LastModificationTime = now, | ||
| 160 | + LastModifierId = currentUserId, | ||
| 161 | + ConcurrencyStamp = _guidGenerator.Create().ToString("N"), | ||
| 175 | CategoryCode = code, | 162 | CategoryCode = code, |
| 176 | CategoryName = name, | 163 | CategoryName = name, |
| 177 | DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText, | 164 | DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText, |
| @@ -183,7 +170,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | @@ -183,7 +170,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | ||
| 183 | }; | 170 | }; |
| 184 | 171 | ||
| 185 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); | 172 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); |
| 186 | - await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, currentUserId, now); | 173 | + await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); |
| 187 | return await GetAsync(entity.Id); | 174 | return await GetAsync(entity.Id); |
| 188 | } | 175 | } |
| 189 | 176 | ||
| @@ -205,9 +192,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | @@ -205,9 +192,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | ||
| 205 | 192 | ||
| 206 | var displayText = input.DisplayText?.Trim(); | 193 | var displayText = input.DisplayText?.Trim(); |
| 207 | var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); | 194 | var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); |
| 208 | - var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); | ||
| 209 | - var locationIds = NormalizeLocationIds(input.LocationIds); | ||
| 210 | - ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); | 195 | + var (availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); |
| 211 | 196 | ||
| 212 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() | 197 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() |
| 213 | .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); | 198 | .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); |
| @@ -228,7 +213,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | @@ -228,7 +213,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | ||
| 228 | entity.LastModifierId = CurrentUser?.Id?.ToString(); | 213 | entity.LastModifierId = CurrentUser?.Id?.ToString(); |
| 229 | 214 | ||
| 230 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | 215 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 231 | - await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now); | 216 | + await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, |
| 217 | + entity.LastModificationTime ?? DateTime.Now); | ||
| 232 | return await GetAsync(id); | 218 | return await GetAsync(id); |
| 233 | } | 219 | } |
| 234 | 220 | ||
| @@ -270,26 +256,299 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | @@ -270,26 +256,299 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ | ||
| 270 | }; | 256 | }; |
| 271 | } | 257 | } |
| 272 | 258 | ||
| 273 | - private static void ValidateAvailabilityTypeAndLocations(string availabilityType, List<string> locationIds) | 259 | + private async Task<(string AvailabilityType, List<string> LocationIds)> ResolveCategoryScopeForSaveAsync( |
| 260 | + LabelCategoryCreateInputVo input) | ||
| 274 | { | 261 | { |
| 262 | + var regionIds = NormalizeRegionIds(input); | ||
| 263 | + var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); | ||
| 264 | + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); | ||
| 265 | + | ||
| 266 | + var hasScopeArrays = input.RegionIds is not null || input.GroupIds is not null || input.LocationIds is not null; | ||
| 267 | + if (regionIds.Count > 0 || explicitLocationIds.Count > 0) | ||
| 268 | + { | ||
| 269 | + availabilityType = "SPECIFIED"; | ||
| 270 | + } | ||
| 271 | + else if (hasScopeArrays && string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase)) | ||
| 272 | + { | ||
| 273 | + availabilityType = "ALL"; | ||
| 274 | + } | ||
| 275 | + | ||
| 275 | if (availabilityType != "ALL" && availabilityType != "SPECIFIED") | 276 | if (availabilityType != "ALL" && availabilityType != "SPECIFIED") |
| 276 | { | 277 | { |
| 277 | throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); | 278 | throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); |
| 278 | } | 279 | } |
| 279 | 280 | ||
| 280 | - if (availabilityType == "SPECIFIED" && locationIds.Count == 0) | 281 | + if (availabilityType == "ALL") |
| 281 | { | 282 | { |
| 282 | - throw new UserFriendlyException("指定门店范围时必须至少选择一个门店"); | 283 | + return ("ALL", new List<string>()); |
| 283 | } | 284 | } |
| 285 | + | ||
| 286 | + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | ||
| 287 | + _dbContext.SqlSugarClient, (IReadOnlyList<string>?)null, regionIds, explicitLocationIds); | ||
| 288 | + if (merged.Count == 0) | ||
| 289 | + { | ||
| 290 | + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); | ||
| 291 | + } | ||
| 292 | + | ||
| 293 | + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); | ||
| 294 | + return ("SPECIFIED", merged); | ||
| 295 | + } | ||
| 296 | + | ||
| 297 | + private static List<string> NormalizeRegionIds(LabelCategoryCreateInputVo input) | ||
| 298 | + { | ||
| 299 | + var merged = new HashSet<string>(StringComparer.Ordinal); | ||
| 300 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds)) | ||
| 301 | + { | ||
| 302 | + merged.Add(id); | ||
| 303 | + } | ||
| 304 | + | ||
| 305 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds)) | ||
| 306 | + { | ||
| 307 | + merged.Add(id); | ||
| 308 | + } | ||
| 309 | + | ||
| 310 | + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); | ||
| 311 | + } | ||
| 312 | + | ||
| 313 | + private async Task<ISugarQueryable<FlLabelCategoryDbEntity>> ApplyCategoryScopeFilterAsync( | ||
| 314 | + ISugarQueryable<FlLabelCategoryDbEntity> query, | ||
| 315 | + string? groupId, | ||
| 316 | + string? locationId) | ||
| 317 | + { | ||
| 318 | + var scopeLocIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( | ||
| 319 | + _dbContext.SqlSugarClient, groupId, locationId); | ||
| 320 | + if (scopeLocIds is null) | ||
| 321 | + { | ||
| 322 | + return query; | ||
| 323 | + } | ||
| 324 | + | ||
| 325 | + if (scopeLocIds.Count == 0) | ||
| 326 | + { | ||
| 327 | + return query.Where(c => c.AvailabilityType == "ALL"); | ||
| 328 | + } | ||
| 329 | + | ||
| 330 | + return query.Where(c => | ||
| 331 | + c.AvailabilityType == "ALL" || | ||
| 332 | + SqlFunc.Subqueryable<FlLabelCategoryLocationDbEntity>() | ||
| 333 | + .Where(cl => cl.CategoryId == c.Id && scopeLocIds.Contains(cl.LocationId)) | ||
| 334 | + .Any()); | ||
| 284 | } | 335 | } |
| 285 | 336 | ||
| 286 | - private static List<string> NormalizeLocationIds(List<string>? locationIds) | 337 | + private const string AllRegionsDisplay = "All Regions"; |
| 338 | + private const string AllLocationsDisplay = "All Locations"; | ||
| 339 | + private const string EmptyDisplay = "无"; | ||
| 340 | + | ||
| 341 | + private async Task<Dictionary<string, CategoryScopeData>> BuildCategoryScopeMapAsync( | ||
| 342 | + List<FlLabelCategoryDbEntity> entities) | ||
| 287 | { | 343 | { |
| 288 | - return locationIds? | 344 | + var result = new Dictionary<string, CategoryScopeData>(StringComparer.Ordinal); |
| 345 | + if (entities.Count == 0) | ||
| 346 | + { | ||
| 347 | + return result; | ||
| 348 | + } | ||
| 349 | + | ||
| 350 | + foreach (var e in entities.Where(x => | ||
| 351 | + !string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))) | ||
| 352 | + { | ||
| 353 | + result[e.Id] = new CategoryScopeData | ||
| 354 | + { | ||
| 355 | + Region = AllRegionsDisplay, | ||
| 356 | + Location = AllLocationsDisplay, | ||
| 357 | + RegionIds = new List<string>(), | ||
| 358 | + LocationIds = new List<string>() | ||
| 359 | + }; | ||
| 360 | + } | ||
| 361 | + | ||
| 362 | + var specifiedIds = entities | ||
| 363 | + .Where(x => string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) | ||
| 364 | + .Select(x => x.Id) | ||
| 365 | + .ToList(); | ||
| 366 | + if (specifiedIds.Count == 0) | ||
| 367 | + { | ||
| 368 | + return result; | ||
| 369 | + } | ||
| 370 | + | ||
| 371 | + var links = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryLocationDbEntity>() | ||
| 372 | + .Where(x => specifiedIds.Contains(x.CategoryId)) | ||
| 373 | + .ToListAsync(); | ||
| 374 | + | ||
| 375 | + var locIdSet = links | ||
| 376 | + .Select(x => x.LocationId) | ||
| 289 | .Where(x => !string.IsNullOrWhiteSpace(x)) | 377 | .Where(x => !string.IsNullOrWhiteSpace(x)) |
| 290 | .Select(x => x.Trim()) | 378 | .Select(x => x.Trim()) |
| 291 | - .Distinct() | ||
| 292 | - .ToList() ?? new(); | 379 | + .Distinct(StringComparer.Ordinal) |
| 380 | + .ToList(); | ||
| 381 | + | ||
| 382 | + var locById = new Dictionary<string, LocationAggregateRoot>(StringComparer.Ordinal); | ||
| 383 | + if (locIdSet.Count > 0) | ||
| 384 | + { | ||
| 385 | + var guidList = locIdSet.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); | ||
| 386 | + if (guidList.Count > 0) | ||
| 387 | + { | ||
| 388 | + var locs = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>() | ||
| 389 | + .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) | ||
| 390 | + .ToListAsync(); | ||
| 391 | + foreach (var loc in locs) | ||
| 392 | + { | ||
| 393 | + locById[loc.Id.ToString()] = loc; | ||
| 394 | + } | ||
| 395 | + } | ||
| 396 | + } | ||
| 397 | + | ||
| 398 | + foreach (var catId in specifiedIds) | ||
| 399 | + { | ||
| 400 | + var catLinks = links.Where(x => x.CategoryId == catId).ToList(); | ||
| 401 | + var locationIds = LocationScopeBindingHelper.NormalizeIds( | ||
| 402 | + catLinks.Select(x => x.LocationId).ToList()); | ||
| 403 | + | ||
| 404 | + if (catLinks.Count == 0) | ||
| 405 | + { | ||
| 406 | + result[catId] = new CategoryScopeData | ||
| 407 | + { | ||
| 408 | + Region = EmptyDisplay, | ||
| 409 | + Location = EmptyDisplay, | ||
| 410 | + RegionIds = new List<string>(), | ||
| 411 | + LocationIds = new List<string>() | ||
| 412 | + }; | ||
| 413 | + continue; | ||
| 414 | + } | ||
| 415 | + | ||
| 416 | + var regions = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
| 417 | + var locationNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
| 418 | + foreach (var link in catLinks) | ||
| 419 | + { | ||
| 420 | + var lid = link.LocationId?.Trim() ?? string.Empty; | ||
| 421 | + if (string.IsNullOrEmpty(lid) || !locById.TryGetValue(lid, out var loc)) | ||
| 422 | + { | ||
| 423 | + continue; | ||
| 424 | + } | ||
| 425 | + | ||
| 426 | + var groupName = loc.GroupName?.Trim(); | ||
| 427 | + if (!string.IsNullOrEmpty(groupName)) | ||
| 428 | + { | ||
| 429 | + regions.Add(groupName); | ||
| 430 | + } | ||
| 431 | + | ||
| 432 | + var locName = loc.LocationName?.Trim(); | ||
| 433 | + if (string.IsNullOrEmpty(locName)) | ||
| 434 | + { | ||
| 435 | + locName = loc.LocationCode?.Trim(); | ||
| 436 | + } | ||
| 437 | + | ||
| 438 | + if (!string.IsNullOrEmpty(locName)) | ||
| 439 | + { | ||
| 440 | + locationNames.Add(locName); | ||
| 441 | + } | ||
| 442 | + } | ||
| 443 | + | ||
| 444 | + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( | ||
| 445 | + _dbContext.SqlSugarClient, locationIds); | ||
| 446 | + | ||
| 447 | + result[catId] = new CategoryScopeData | ||
| 448 | + { | ||
| 449 | + Region = regions.Count > 0 | ||
| 450 | + ? string.Join(", ", regions.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) | ||
| 451 | + : EmptyDisplay, | ||
| 452 | + Location = locationNames.Count > 0 | ||
| 453 | + ? string.Join(", ", locationNames.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) | ||
| 454 | + : EmptyDisplay, | ||
| 455 | + RegionIds = regionIds, | ||
| 456 | + LocationIds = locationIds | ||
| 457 | + }; | ||
| 458 | + } | ||
| 459 | + | ||
| 460 | + return result; | ||
| 461 | + } | ||
| 462 | + | ||
| 463 | + private sealed class CategoryScopeData | ||
| 464 | + { | ||
| 465 | + public string Region { get; init; } = string.Empty; | ||
| 466 | + public string Location { get; init; } = string.Empty; | ||
| 467 | + public List<string> RegionIds { get; init; } = new(); | ||
| 468 | + public List<string> LocationIds { get; init; } = new(); | ||
| 469 | + } | ||
| 470 | + | ||
| 471 | + private sealed class CategoryLabelStats | ||
| 472 | + { | ||
| 473 | + public long Count { get; init; } | ||
| 474 | + public DateTime MaxEdited { get; init; } | ||
| 475 | + } | ||
| 476 | + | ||
| 477 | + /// <summary> | ||
| 478 | + /// 统计各分类下标签数量及下属标签最近编辑时间(用于列表 LastEdited 同步)。 | ||
| 479 | + /// </summary> | ||
| 480 | + private async Task<Dictionary<string, CategoryLabelStats>> BuildCategoryLabelStatsMapAsync(List<string> categoryIds) | ||
| 481 | + { | ||
| 482 | + var result = new Dictionary<string, CategoryLabelStats>(StringComparer.Ordinal); | ||
| 483 | + if (categoryIds.Count == 0) | ||
| 484 | + { | ||
| 485 | + return result; | ||
| 486 | + } | ||
| 487 | + | ||
| 488 | + var rows = await _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>() | ||
| 489 | + .Where(x => !x.IsDeleted) | ||
| 490 | + .Where(x => x.LabelCategoryId != null && categoryIds.Contains(x.LabelCategoryId)) | ||
| 491 | + .Select(x => new { x.LabelCategoryId, x.CreationTime, x.LastModificationTime }) | ||
| 492 | + .ToListAsync(); | ||
| 493 | + | ||
| 494 | + foreach (var g in rows.GroupBy(x => x.LabelCategoryId!)) | ||
| 495 | + { | ||
| 496 | + result[g.Key] = new CategoryLabelStats | ||
| 497 | + { | ||
| 498 | + Count = g.Count(), | ||
| 499 | + MaxEdited = g.Max(l => l.LastModificationTime ?? l.CreationTime) | ||
| 500 | + }; | ||
| 501 | + } | ||
| 502 | + | ||
| 503 | + return result; | ||
| 504 | + } | ||
| 505 | + | ||
| 506 | + private static DateTime ResolveCategoryCreationTime(FlLabelCategoryDbEntity entity) | ||
| 507 | + { | ||
| 508 | + if (entity.CreationTime > DateTime.MinValue.AddYears(1)) | ||
| 509 | + { | ||
| 510 | + return entity.CreationTime; | ||
| 511 | + } | ||
| 512 | + | ||
| 513 | + return entity.LastModificationTime ?? DateTime.Now; | ||
| 514 | + } | ||
| 515 | + | ||
| 516 | + private static DateTime ResolveLastEdited( | ||
| 517 | + FlLabelCategoryDbEntity entity, | ||
| 518 | + DateTime creationTime, | ||
| 519 | + CategoryLabelStats? labelStats) | ||
| 520 | + { | ||
| 521 | + var categoryEdited = entity.LastModificationTime ?? creationTime; | ||
| 522 | + if (labelStats is null) | ||
| 523 | + { | ||
| 524 | + return categoryEdited; | ||
| 525 | + } | ||
| 526 | + | ||
| 527 | + return labelStats.MaxEdited > categoryEdited ? labelStats.MaxEdited : categoryEdited; | ||
| 528 | + } | ||
| 529 | + | ||
| 530 | + /// <summary> | ||
| 531 | + /// 下属标签变更时回写分类最后编辑时间(与列表 <see cref="LabelCategoryGetListOutputDto.LastEdited"/> 一致)。 | ||
| 532 | + /// </summary> | ||
| 533 | + internal static async Task TouchLabelCategoryLastEditedAsync( | ||
| 534 | + ISqlSugarDbContext dbContext, | ||
| 535 | + string? categoryId, | ||
| 536 | + string? modifierId) | ||
| 537 | + { | ||
| 538 | + var cid = categoryId?.Trim(); | ||
| 539 | + if (string.IsNullOrWhiteSpace(cid)) | ||
| 540 | + { | ||
| 541 | + return; | ||
| 542 | + } | ||
| 543 | + | ||
| 544 | + await dbContext.SqlSugarClient.Updateable<FlLabelCategoryDbEntity>() | ||
| 545 | + .SetColumns(x => new FlLabelCategoryDbEntity | ||
| 546 | + { | ||
| 547 | + LastModificationTime = DateTime.Now, | ||
| 548 | + LastModifierId = modifierId | ||
| 549 | + }) | ||
| 550 | + .Where(x => x.Id == cid && !x.IsDeleted) | ||
| 551 | + .ExecuteCommandAsync(); | ||
| 293 | } | 552 | } |
| 294 | 553 | ||
| 295 | private async Task SaveCategoryLocationsAsync( | 554 | private async Task SaveCategoryLocationsAsync( |
美国版/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 | @@ -101,6 +101,7 @@ public class LocationAppService : ApplicationService, ILocationAppService | ||
| 101 | Email = input.Email?.Trim(), | 101 | Email = input.Email?.Trim(), |
| 102 | Latitude = input.Latitude, | 102 | Latitude = input.Latitude, |
| 103 | Longitude = input.Longitude, | 103 | Longitude = input.Longitude, |
| 104 | + OperatingHours = input.OperatingHours?.Trim(), | ||
| 104 | State = input.State | 105 | State = input.State |
| 105 | }; | 106 | }; |
| 106 | 107 | ||
| @@ -136,6 +137,7 @@ public class LocationAppService : ApplicationService, ILocationAppService | @@ -136,6 +137,7 @@ public class LocationAppService : ApplicationService, ILocationAppService | ||
| 136 | entity.Email = input.Email?.Trim(); | 137 | entity.Email = input.Email?.Trim(); |
| 137 | entity.Latitude = input.Latitude; | 138 | entity.Latitude = input.Latitude; |
| 138 | entity.Longitude = input.Longitude; | 139 | entity.Longitude = input.Longitude; |
| 140 | + entity.OperatingHours = input.OperatingHours?.Trim(); | ||
| 139 | entity.State = input.State; | 141 | entity.State = input.State; |
| 140 | 142 | ||
| 141 | await _locationRepository.UpdateAsync(entity); | 143 | await _locationRepository.UpdateAsync(entity); |
| @@ -347,7 +349,8 @@ public class LocationAppService : ApplicationService, ILocationAppService | @@ -347,7 +349,8 @@ public class LocationAppService : ApplicationService, ILocationAppService | ||
| 347 | (x.Country != null && x.Country.Contains(keyword!)) || | 349 | (x.Country != null && x.Country.Contains(keyword!)) || |
| 348 | (x.ZipCode != null && x.ZipCode.Contains(keyword!)) || | 350 | (x.ZipCode != null && x.ZipCode.Contains(keyword!)) || |
| 349 | (x.Phone != null && x.Phone.Contains(keyword!)) || | 351 | (x.Phone != null && x.Phone.Contains(keyword!)) || |
| 350 | - (x.Email != null && x.Email.Contains(keyword!)) | 352 | + (x.Email != null && x.Email.Contains(keyword!)) || |
| 353 | + (x.OperatingHours != null && x.OperatingHours.Contains(keyword!)) | ||
| 351 | ); | 354 | ); |
| 352 | } | 355 | } |
| 353 | 356 | ||
| @@ -368,6 +371,7 @@ public class LocationAppService : ApplicationService, ILocationAppService | @@ -368,6 +371,7 @@ public class LocationAppService : ApplicationService, ILocationAppService | ||
| 368 | Email = x.Email, | 371 | Email = x.Email, |
| 369 | Latitude = x.Latitude, | 372 | Latitude = x.Latitude, |
| 370 | Longitude = x.Longitude, | 373 | Longitude = x.Longitude, |
| 374 | + OperatingHours = x.OperatingHours, | ||
| 371 | State = x.State | 375 | State = x.State |
| 372 | }; | 376 | }; |
| 373 | } | 377 | } |
美国版/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 | @@ -36,7 +36,7 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | ||
| 36 | public async Task<PagedResultWithPageDto<PartnerGetListOutputDto>> GetListAsync(PartnerGetListInputVo input) | 36 | public async Task<PagedResultWithPageDto<PartnerGetListOutputDto>> GetListAsync(PartnerGetListInputVo input) |
| 37 | { | 37 | { |
| 38 | RefAsync<int> total = 0; | 38 | RefAsync<int> total = 0; |
| 39 | - var query = BuildPartnerListQuery(input); | 39 | + var query = await BuildPartnerListQueryAsync(input); |
| 40 | 40 | ||
| 41 | var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); | 41 | var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); |
| 42 | var items = entities.Select(MapListItem).ToList(); | 42 | var items = entities.Select(MapListItem).ToList(); |
| @@ -44,19 +44,19 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | @@ -44,19 +44,19 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | ||
| 44 | } | 44 | } |
| 45 | 45 | ||
| 46 | /// <inheritdoc /> | 46 | /// <inheritdoc /> |
| 47 | - public async Task<PartnerGetOutputDto> GetAsync(string id) | 47 | + public async Task<PartnerGetOutputDto> GetAsync(Guid id) |
| 48 | { | 48 | { |
| 49 | - var partnerId = id?.Trim(); | ||
| 50 | - if (string.IsNullOrWhiteSpace(partnerId)) | 49 | + if (id == Guid.Empty) |
| 51 | { | 50 | { |
| 52 | - throw new UserFriendlyException("合作伙伴Id不能为空"); | 51 | + throw new UserFriendlyException("Partner id is required."); |
| 53 | } | 52 | } |
| 54 | 53 | ||
| 54 | + var partnerId = id.ToString(); | ||
| 55 | var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | 55 | var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() |
| 56 | .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); | 56 | .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); |
| 57 | if (entity is null) | 57 | if (entity is null) |
| 58 | { | 58 | { |
| 59 | - throw new UserFriendlyException("合作伙伴不存在"); | 59 | + throw new UserFriendlyException("Partner not found."); |
| 60 | } | 60 | } |
| 61 | 61 | ||
| 62 | return MapDetail(entity); | 62 | return MapDetail(entity); |
| @@ -69,13 +69,13 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | @@ -69,13 +69,13 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | ||
| 69 | var name = input.PartnerName?.Trim(); | 69 | var name = input.PartnerName?.Trim(); |
| 70 | if (string.IsNullOrWhiteSpace(name)) | 70 | if (string.IsNullOrWhiteSpace(name)) |
| 71 | { | 71 | { |
| 72 | - throw new UserFriendlyException("合作伙伴名称不能为空"); | 72 | + throw new UserFriendlyException("Partner name is required."); |
| 73 | } | 73 | } |
| 74 | 74 | ||
| 75 | var email = input.ContactEmail?.Trim(); | 75 | var email = input.ContactEmail?.Trim(); |
| 76 | if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email)) | 76 | if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email)) |
| 77 | { | 77 | { |
| 78 | - throw new UserFriendlyException("联系邮箱格式不正确"); | 78 | + throw new UserFriendlyException("Invalid contact email format."); |
| 79 | } | 79 | } |
| 80 | 80 | ||
| 81 | var now = Clock.Now; | 81 | var now = Clock.Now; |
| @@ -85,73 +85,75 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | @@ -85,73 +85,75 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | ||
| 85 | IsDeleted = false, | 85 | IsDeleted = false, |
| 86 | PartnerName = name, | 86 | PartnerName = name, |
| 87 | ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email, | 87 | ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email, |
| 88 | - PhoneNumber = string.IsNullOrWhiteSpace(input.PhoneNumber) ? null : input.PhoneNumber.Trim(), | 88 | + PhoneNumber = TrimToNull(input.PhoneNumber), |
| 89 | State = input.State, | 89 | State = input.State, |
| 90 | CreationTime = now, | 90 | CreationTime = now, |
| 91 | CreatorId = CurrentUser?.Id?.ToString(), | 91 | CreatorId = CurrentUser?.Id?.ToString(), |
| 92 | LastModificationTime = now, | 92 | LastModificationTime = now, |
| 93 | LastModifierId = CurrentUser?.Id?.ToString() | 93 | LastModifierId = CurrentUser?.Id?.ToString() |
| 94 | }; | 94 | }; |
| 95 | + ApplyAddressFields(entity, input); | ||
| 95 | 96 | ||
| 96 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); | 97 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); |
| 97 | - return await GetAsync(entity.Id); | 98 | + return await GetAsync(Guid.Parse(entity.Id)); |
| 98 | } | 99 | } |
| 99 | 100 | ||
| 100 | /// <inheritdoc /> | 101 | /// <inheritdoc /> |
| 101 | [UnitOfWork] | 102 | [UnitOfWork] |
| 102 | - public async Task<PartnerGetOutputDto> UpdateAsync(string id, PartnerUpdateInputVo input) | 103 | + public async Task<PartnerGetOutputDto> UpdateAsync(Guid id, PartnerUpdateInputVo input) |
| 103 | { | 104 | { |
| 104 | - var partnerId = id?.Trim(); | ||
| 105 | - if (string.IsNullOrWhiteSpace(partnerId)) | 105 | + if (id == Guid.Empty) |
| 106 | { | 106 | { |
| 107 | - throw new UserFriendlyException("合作伙伴Id不能为空"); | 107 | + throw new UserFriendlyException("Partner id is required."); |
| 108 | } | 108 | } |
| 109 | 109 | ||
| 110 | + var partnerId = id.ToString(); | ||
| 110 | var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | 111 | var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() |
| 111 | .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); | 112 | .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); |
| 112 | if (entity is null) | 113 | if (entity is null) |
| 113 | { | 114 | { |
| 114 | - throw new UserFriendlyException("合作伙伴不存在"); | 115 | + throw new UserFriendlyException("Partner not found."); |
| 115 | } | 116 | } |
| 116 | 117 | ||
| 117 | var name = input.PartnerName?.Trim(); | 118 | var name = input.PartnerName?.Trim(); |
| 118 | if (string.IsNullOrWhiteSpace(name)) | 119 | if (string.IsNullOrWhiteSpace(name)) |
| 119 | { | 120 | { |
| 120 | - throw new UserFriendlyException("合作伙伴名称不能为空"); | 121 | + throw new UserFriendlyException("Partner name is required."); |
| 121 | } | 122 | } |
| 122 | 123 | ||
| 123 | var email = input.ContactEmail?.Trim(); | 124 | var email = input.ContactEmail?.Trim(); |
| 124 | if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email)) | 125 | if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email)) |
| 125 | { | 126 | { |
| 126 | - throw new UserFriendlyException("联系邮箱格式不正确"); | 127 | + throw new UserFriendlyException("Invalid contact email format."); |
| 127 | } | 128 | } |
| 128 | 129 | ||
| 129 | entity.PartnerName = name; | 130 | entity.PartnerName = name; |
| 130 | entity.ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email; | 131 | entity.ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email; |
| 131 | - entity.PhoneNumber = string.IsNullOrWhiteSpace(input.PhoneNumber) ? null : input.PhoneNumber.Trim(); | 132 | + entity.PhoneNumber = TrimToNull(input.PhoneNumber); |
| 132 | entity.State = input.State; | 133 | entity.State = input.State; |
| 134 | + ApplyAddressFields(entity, input); | ||
| 133 | entity.LastModificationTime = Clock.Now; | 135 | entity.LastModificationTime = Clock.Now; |
| 134 | entity.LastModifierId = CurrentUser?.Id?.ToString(); | 136 | entity.LastModifierId = CurrentUser?.Id?.ToString(); |
| 135 | 137 | ||
| 136 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | 138 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 137 | - return await GetAsync(partnerId); | 139 | + return await GetAsync(id); |
| 138 | } | 140 | } |
| 139 | 141 | ||
| 140 | /// <inheritdoc /> | 142 | /// <inheritdoc /> |
| 141 | [UnitOfWork] | 143 | [UnitOfWork] |
| 142 | - public async Task DeleteAsync(string id) | 144 | + public async Task DeleteAsync(Guid id) |
| 143 | { | 145 | { |
| 144 | - var partnerId = id?.Trim(); | ||
| 145 | - if (string.IsNullOrWhiteSpace(partnerId)) | 146 | + if (id == Guid.Empty) |
| 146 | { | 147 | { |
| 147 | - throw new UserFriendlyException("合作伙伴Id不能为空"); | 148 | + throw new UserFriendlyException("Partner id is required."); |
| 148 | } | 149 | } |
| 149 | 150 | ||
| 151 | + var partnerId = id.ToString(); | ||
| 150 | var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | 152 | var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() |
| 151 | .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); | 153 | .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); |
| 152 | if (entity is null) | 154 | if (entity is null) |
| 153 | { | 155 | { |
| 154 | - throw new UserFriendlyException("合作伙伴不存在"); | 156 | + throw new UserFriendlyException("Partner not found."); |
| 155 | } | 157 | } |
| 156 | 158 | ||
| 157 | entity.IsDeleted = true; | 159 | entity.IsDeleted = true; |
| @@ -161,15 +163,17 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | @@ -161,15 +163,17 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | ||
| 161 | } | 163 | } |
| 162 | 164 | ||
| 163 | /// <inheritdoc /> | 165 | /// <inheritdoc /> |
| 164 | - public async Task<IActionResult> ExportPdfAsync(PartnerGetListInputVo input) | 166 | + [HttpGet] |
| 167 | + public async Task<IActionResult> ExportPdfAsync([FromQuery] PartnerGetListInputVo input) | ||
| 165 | { | 168 | { |
| 166 | QuestPDF.Settings.License = LicenseType.Community; | 169 | QuestPDF.Settings.License = LicenseType.Community; |
| 167 | 170 | ||
| 168 | - var count = await BuildPartnerListQuery(input).CountAsync(); | ||
| 169 | - var query = BuildPartnerListQuery(input); | 171 | + var count = await (await BuildPartnerListQueryAsync(input)).CountAsync(); |
| 172 | + var query = await BuildPartnerListQueryAsync(input); | ||
| 170 | if (count > ExportPdfMaxRows) | 173 | if (count > ExportPdfMaxRows) |
| 171 | { | 174 | { |
| 172 | - throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围"); | 175 | + throw new UserFriendlyException( |
| 176 | + $"Export exceeds the maximum of {ExportPdfMaxRows} rows. Narrow your filters and try again."); | ||
| 173 | } | 177 | } |
| 174 | 178 | ||
| 175 | var rows = await query.Take(ExportPdfMaxRows).ToListAsync(); | 179 | var rows = await query.Take(ExportPdfMaxRows).ToListAsync(); |
| @@ -208,9 +212,9 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | @@ -208,9 +212,9 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | ||
| 208 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | 212 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) |
| 209 | .Text(e.PartnerName ?? string.Empty); | 213 | .Text(e.PartnerName ?? string.Empty); |
| 210 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | 214 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) |
| 211 | - .Text(string.IsNullOrWhiteSpace(e.ContactEmail) ? "无" : e.ContactEmail!); | 215 | + .Text(string.IsNullOrWhiteSpace(e.ContactEmail) ? "None" : e.ContactEmail!); |
| 212 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | 216 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) |
| 213 | - .Text(string.IsNullOrWhiteSpace(e.PhoneNumber) ? "无" : e.PhoneNumber!); | 217 | + .Text(string.IsNullOrWhiteSpace(e.PhoneNumber) ? "None" : e.PhoneNumber!); |
| 214 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status); | 218 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status); |
| 215 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) | 219 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5) |
| 216 | .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm")); | 220 | .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm")); |
| @@ -225,16 +229,27 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | @@ -225,16 +229,27 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | ||
| 225 | return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName }; | 229 | return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName }; |
| 226 | } | 230 | } |
| 227 | 231 | ||
| 228 | - private ISugarQueryable<FlPartnerDbEntity> BuildPartnerListQuery(PartnerGetListInputVo input) | 232 | + private async Task<ISugarQueryable<FlPartnerDbEntity>> BuildPartnerListQueryAsync(PartnerGetListInputVo input) |
| 229 | { | 233 | { |
| 234 | + var scope = await PartnerScopeHelper.ResolvePartnerScopeAsync(CurrentUser, _dbContext); | ||
| 235 | + | ||
| 230 | var keyword = input.Keyword?.Trim(); | 236 | var keyword = input.Keyword?.Trim(); |
| 231 | var query = _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | 237 | var query = _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() |
| 232 | - .Where(x => !x.IsDeleted) | 238 | + .Where(x => !x.IsDeleted); |
| 239 | + | ||
| 240 | + query = PartnerScopeHelper.ApplyPartnerScope(query, scope); | ||
| 241 | + | ||
| 242 | + query = query | ||
| 233 | .WhereIF(input.State != null, x => x.State == input.State) | 243 | .WhereIF(input.State != null, x => x.State == input.State) |
| 234 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), | 244 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), |
| 235 | x => x.PartnerName.Contains(keyword!) || | 245 | x => x.PartnerName.Contains(keyword!) || |
| 236 | (x.ContactEmail != null && x.ContactEmail.Contains(keyword!)) || | 246 | (x.ContactEmail != null && x.ContactEmail.Contains(keyword!)) || |
| 237 | - (x.PhoneNumber != null && x.PhoneNumber.Contains(keyword!))); | 247 | + (x.PhoneNumber != null && x.PhoneNumber.Contains(keyword!)) || |
| 248 | + (x.Street != null && x.Street.Contains(keyword!)) || | ||
| 249 | + (x.City != null && x.City.Contains(keyword!)) || | ||
| 250 | + (x.StateCode != null && x.StateCode.Contains(keyword!)) || | ||
| 251 | + (x.Country != null && x.Country.Contains(keyword!)) || | ||
| 252 | + (x.ZipCode != null && x.ZipCode.Contains(keyword!))); | ||
| 238 | 253 | ||
| 239 | if (!string.IsNullOrWhiteSpace(input.Sorting)) | 254 | if (!string.IsNullOrWhiteSpace(input.Sorting)) |
| 240 | { | 255 | { |
| @@ -276,26 +291,60 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | @@ -276,26 +291,60 @@ public class PartnerAppService : ApplicationService, IPartnerAppService | ||
| 276 | return query; | 291 | return query; |
| 277 | } | 292 | } |
| 278 | 293 | ||
| 279 | - private static PartnerGetListOutputDto MapListItem(FlPartnerDbEntity x) => new() | 294 | + private static PartnerGetListOutputDto MapListItem(FlPartnerDbEntity x) |
| 280 | { | 295 | { |
| 281 | - Id = x.Id, | ||
| 282 | - PartnerName = x.PartnerName, | ||
| 283 | - ContactEmail = x.ContactEmail, | ||
| 284 | - PhoneNumber = x.PhoneNumber, | ||
| 285 | - State = x.State, | ||
| 286 | - CreationTime = x.CreationTime | ||
| 287 | - }; | ||
| 288 | - | ||
| 289 | - private static PartnerGetOutputDto MapDetail(FlPartnerDbEntity x) => new() | 296 | + var dto = new PartnerGetListOutputDto |
| 297 | + { | ||
| 298 | + Id = x.Id, | ||
| 299 | + PartnerName = x.PartnerName, | ||
| 300 | + ContactEmail = x.ContactEmail, | ||
| 301 | + PhoneNumber = x.PhoneNumber, | ||
| 302 | + State = x.State, | ||
| 303 | + CreationTime = x.CreationTime | ||
| 304 | + }; | ||
| 305 | + MapAddressFields(dto, x); | ||
| 306 | + return dto; | ||
| 307 | + } | ||
| 308 | + | ||
| 309 | + private static PartnerGetOutputDto MapDetail(FlPartnerDbEntity x) | ||
| 290 | { | 310 | { |
| 291 | - Id = x.Id, | ||
| 292 | - PartnerName = x.PartnerName, | ||
| 293 | - ContactEmail = x.ContactEmail, | ||
| 294 | - PhoneNumber = x.PhoneNumber, | ||
| 295 | - State = x.State, | ||
| 296 | - CreationTime = x.CreationTime, | ||
| 297 | - LastModificationTime = x.LastModificationTime | ||
| 298 | - }; | 311 | + var dto = new PartnerGetOutputDto |
| 312 | + { | ||
| 313 | + Id = x.Id, | ||
| 314 | + PartnerName = x.PartnerName, | ||
| 315 | + ContactEmail = x.ContactEmail, | ||
| 316 | + PhoneNumber = x.PhoneNumber, | ||
| 317 | + State = x.State, | ||
| 318 | + CreationTime = x.CreationTime, | ||
| 319 | + LastModificationTime = x.LastModificationTime | ||
| 320 | + }; | ||
| 321 | + MapAddressFields(dto, x); | ||
| 322 | + return dto; | ||
| 323 | + } | ||
| 324 | + | ||
| 325 | + private static void ApplyAddressFields(FlPartnerDbEntity entity, PartnerAddressFieldsDto input) | ||
| 326 | + { | ||
| 327 | + entity.Street = TrimToNull(input.Street); | ||
| 328 | + entity.City = TrimToNull(input.City); | ||
| 329 | + entity.StateCode = TrimToNull(input.StateCode); | ||
| 330 | + entity.Country = TrimToNull(input.Country); | ||
| 331 | + entity.ZipCode = TrimToNull(input.ZipCode); | ||
| 332 | + } | ||
| 333 | + | ||
| 334 | + private static void MapAddressFields(PartnerAddressFieldsDto dto, FlPartnerDbEntity entity) | ||
| 335 | + { | ||
| 336 | + dto.Street = entity.Street; | ||
| 337 | + dto.City = entity.City; | ||
| 338 | + dto.StateCode = entity.StateCode; | ||
| 339 | + dto.Country = entity.Country; | ||
| 340 | + dto.ZipCode = entity.ZipCode; | ||
| 341 | + } | ||
| 342 | + | ||
| 343 | + private static string? TrimToNull(string? value) | ||
| 344 | + { | ||
| 345 | + var t = value?.Trim(); | ||
| 346 | + return string.IsNullOrEmpty(t) ? null : t; | ||
| 347 | + } | ||
| 299 | 348 | ||
| 300 | private static bool IsPlausibleEmail(string email) | 349 | private static bool IsPlausibleEmail(string email) |
| 301 | { | 350 | { |
美国版/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 | @@ -36,11 +36,12 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 36 | _batchImportOptions = batchImportOptions; | 36 | _batchImportOptions = batchImportOptions; |
| 37 | } | 37 | } |
| 38 | 38 | ||
| 39 | + /// <inheritdoc /> | ||
| 39 | public async Task<PagedResultWithPageDto<ProductGetListOutputDto>> GetListAsync(ProductGetListInputVo input) | 40 | public async Task<PagedResultWithPageDto<ProductGetListOutputDto>> GetListAsync(ProductGetListInputVo input) |
| 40 | { | 41 | { |
| 41 | RefAsync<int> total = 0; | 42 | RefAsync<int> total = 0; |
| 42 | 43 | ||
| 43 | - var query = BuildFilteredProductQuery(input); | 44 | + var query = await BuildFilteredProductQueryAsync(input); |
| 44 | if (!string.IsNullOrWhiteSpace(input.Sorting)) | 45 | if (!string.IsNullOrWhiteSpace(input.Sorting)) |
| 45 | { | 46 | { |
| 46 | query = query.OrderBy(input.Sorting); | 47 | query = query.OrderBy(input.Sorting); |
| @@ -107,14 +108,14 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -107,14 +108,14 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 107 | return BuildPagedResult(input.SkipCount, input.MaxResultCount, (int)total, items); | 108 | return BuildPagedResult(input.SkipCount, input.MaxResultCount, (int)total, items); |
| 108 | } | 109 | } |
| 109 | 110 | ||
| 110 | - public async Task<ProductGetOutputDto> GetAsync(string id) | 111 | + public async Task<ProductGetOutputDto> GetAsync(Guid id) |
| 111 | { | 112 | { |
| 112 | - var productId = id?.Trim(); | ||
| 113 | - if (string.IsNullOrWhiteSpace(productId)) | 113 | + if (id == Guid.Empty) |
| 114 | { | 114 | { |
| 115 | throw new UserFriendlyException("产品Id不能为空"); | 115 | throw new UserFriendlyException("产品Id不能为空"); |
| 116 | } | 116 | } |
| 117 | 117 | ||
| 118 | + var productId = id.ToString(); | ||
| 118 | var entity = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() | 119 | var entity = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() |
| 119 | .FirstAsync(x => !x.IsDeleted && x.Id == productId); | 120 | .FirstAsync(x => !x.IsDeleted && x.Id == productId); |
| 120 | 121 | ||
| @@ -134,11 +135,15 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -134,11 +135,15 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 134 | } | 135 | } |
| 135 | } | 136 | } |
| 136 | 137 | ||
| 137 | - var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLocationProductDbEntity>() | ||
| 138 | - .Where(x => x.ProductId == productId) | ||
| 139 | - .Select(x => x.LocationId) | ||
| 140 | - .Distinct() | ||
| 141 | - .ToListAsync(); | 138 | + var locationIds = LocationScopeBindingHelper.NormalizeIds( |
| 139 | + await _dbContext.SqlSugarClient.Queryable<FlLocationProductDbEntity>() | ||
| 140 | + .Where(x => x.ProductId == productId) | ||
| 141 | + .Select(x => x.LocationId) | ||
| 142 | + .ToListAsync()); | ||
| 143 | + var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( | ||
| 144 | + _dbContext.SqlSugarClient, locationIds); | ||
| 145 | + var groupIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( | ||
| 146 | + _dbContext.SqlSugarClient, locationIds); | ||
| 142 | 147 | ||
| 143 | return new ProductGetOutputDto | 148 | return new ProductGetOutputDto |
| 144 | { | 149 | { |
| @@ -149,6 +154,9 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -149,6 +154,9 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 149 | CategoryName = categoryName, | 154 | CategoryName = categoryName, |
| 150 | ProductImageUrl = entity.ProductImageUrl, | 155 | ProductImageUrl = entity.ProductImageUrl, |
| 151 | State = entity.State, | 156 | State = entity.State, |
| 157 | + PartnerId = partnerIds.Count > 0 ? partnerIds[0] : null, | ||
| 158 | + PartnerIds = partnerIds, | ||
| 159 | + GroupIds = groupIds, | ||
| 152 | LocationIds = locationIds | 160 | LocationIds = locationIds |
| 153 | }; | 161 | }; |
| 154 | } | 162 | } |
| @@ -190,24 +198,24 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -190,24 +198,24 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 190 | 198 | ||
| 191 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); | 199 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); |
| 192 | 200 | ||
| 193 | - if (input.LocationIds is not null) | 201 | + if (HasProductScopeBinding(input)) |
| 194 | { | 202 | { |
| 195 | - var locIds = await NormalizeAndValidateLocationIdsAsync(input.LocationIds); | 203 | + var locIds = await ResolveProductLocationIdsForSaveAsync(input); |
| 196 | await ReplaceProductLocationLinksAsync(entity.Id, locIds); | 204 | await ReplaceProductLocationLinksAsync(entity.Id, locIds); |
| 197 | } | 205 | } |
| 198 | 206 | ||
| 199 | - return await GetAsync(entity.Id); | 207 | + return await GetAsync(Guid.Parse(entity.Id)); |
| 200 | } | 208 | } |
| 201 | 209 | ||
| 202 | [UnitOfWork] | 210 | [UnitOfWork] |
| 203 | - public async Task<ProductGetOutputDto> UpdateAsync(string id, ProductUpdateInputVo input) | 211 | + public async Task<ProductGetOutputDto> UpdateAsync(Guid id, ProductUpdateInputVo input) |
| 204 | { | 212 | { |
| 205 | - var productId = id?.Trim(); | ||
| 206 | - if (string.IsNullOrWhiteSpace(productId)) | 213 | + if (id == Guid.Empty) |
| 207 | { | 214 | { |
| 208 | throw new UserFriendlyException("产品Id不能为空"); | 215 | throw new UserFriendlyException("产品Id不能为空"); |
| 209 | } | 216 | } |
| 210 | 217 | ||
| 218 | + var productId = id.ToString(); | ||
| 211 | var entity = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() | 219 | var entity = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() |
| 212 | .FirstAsync(x => !x.IsDeleted && x.Id == productId); | 220 | .FirstAsync(x => !x.IsDeleted && x.Id == productId); |
| 213 | if (entity is null) | 221 | if (entity is null) |
| @@ -246,24 +254,24 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -246,24 +254,24 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 246 | 254 | ||
| 247 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | 255 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 248 | 256 | ||
| 249 | - if (input.LocationIds is not null) | 257 | + if (HasProductScopeBinding(input)) |
| 250 | { | 258 | { |
| 251 | - var locIds = await NormalizeAndValidateLocationIdsAsync(input.LocationIds); | 259 | + var locIds = await ResolveProductLocationIdsForSaveAsync(input); |
| 252 | await ReplaceProductLocationLinksAsync(productId, locIds); | 260 | await ReplaceProductLocationLinksAsync(productId, locIds); |
| 253 | } | 261 | } |
| 254 | 262 | ||
| 255 | - return await GetAsync(productId); | 263 | + return await GetAsync(id); |
| 256 | } | 264 | } |
| 257 | 265 | ||
| 258 | [UnitOfWork] | 266 | [UnitOfWork] |
| 259 | - public async Task DeleteAsync(string id) | 267 | + public async Task DeleteAsync(Guid id) |
| 260 | { | 268 | { |
| 261 | - var productId = id?.Trim(); | ||
| 262 | - if (string.IsNullOrWhiteSpace(productId)) | 269 | + if (id == Guid.Empty) |
| 263 | { | 270 | { |
| 264 | throw new UserFriendlyException("产品Id不能为空"); | 271 | throw new UserFriendlyException("产品Id不能为空"); |
| 265 | } | 272 | } |
| 266 | 273 | ||
| 274 | + var productId = id.ToString(); | ||
| 267 | var entity = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() | 275 | var entity = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() |
| 268 | .FirstAsync(x => !x.IsDeleted && x.Id == productId); | 276 | .FirstAsync(x => !x.IsDeleted && x.Id == productId); |
| 269 | if (entity is null) | 277 | if (entity is null) |
| @@ -276,6 +284,7 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -276,6 +284,7 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 276 | } | 284 | } |
| 277 | 285 | ||
| 278 | /// <inheritdoc /> | 286 | /// <inheritdoc /> |
| 287 | + [HttpGet] | ||
| 279 | public Task<IActionResult> DownloadProductImportTemplateAsync() | 288 | public Task<IActionResult> DownloadProductImportTemplateAsync() |
| 280 | { | 289 | { |
| 281 | var opt = _batchImportOptions.Value; | 290 | var opt = _batchImportOptions.Value; |
| @@ -306,16 +315,20 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -306,16 +315,20 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 306 | } | 315 | } |
| 307 | 316 | ||
| 308 | /// <inheritdoc /> | 317 | /// <inheritdoc /> |
| 318 | + [HttpGet] | ||
| 309 | public async Task<IActionResult> ExportProductsExcelAsync([FromQuery] ProductGetListInputVo input) | 319 | public async Task<IActionResult> ExportProductsExcelAsync([FromQuery] ProductGetListInputVo input) |
| 310 | { | 320 | { |
| 311 | var exportFilter = new ProductGetListInputVo | 321 | var exportFilter = new ProductGetListInputVo |
| 312 | { | 322 | { |
| 313 | Sorting = input.Sorting, | 323 | Sorting = input.Sorting, |
| 314 | Keyword = input.Keyword, | 324 | Keyword = input.Keyword, |
| 315 | - State = input.State | 325 | + State = input.State, |
| 326 | + PartnerId = input.PartnerId, | ||
| 327 | + GroupId = input.GroupId, | ||
| 328 | + LocationId = input.LocationId | ||
| 316 | }; | 329 | }; |
| 317 | 330 | ||
| 318 | - var query = BuildFilteredProductQuery(exportFilter); | 331 | + var query = await BuildFilteredProductQueryAsync(exportFilter); |
| 319 | if (!string.IsNullOrWhiteSpace(exportFilter.Sorting)) | 332 | if (!string.IsNullOrWhiteSpace(exportFilter.Sorting)) |
| 320 | { | 333 | { |
| 321 | query = query.OrderBy(exportFilter.Sorting); | 334 | query = query.OrderBy(exportFilter.Sorting); |
| @@ -334,6 +347,7 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -334,6 +347,7 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 334 | } | 347 | } |
| 335 | 348 | ||
| 336 | /// <inheritdoc /> | 349 | /// <inheritdoc /> |
| 350 | + [HttpPost] | ||
| 337 | public async Task<ProductBatchImportResultDto> ImportProductsBatchAsync( | 351 | public async Task<ProductBatchImportResultDto> ImportProductsBatchAsync( |
| 338 | [FromForm] ProductBatchImportInputVo input) | 352 | [FromForm] ProductBatchImportInputVo input) |
| 339 | { | 353 | { |
| @@ -423,10 +437,14 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -423,10 +437,14 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 423 | throw new UserFriendlyException($"单次批量编辑最多允许 {maxItems} 条,请分批提交"); | 437 | throw new UserFriendlyException($"单次批量编辑最多允许 {maxItems} 条,请分批提交"); |
| 424 | } | 438 | } |
| 425 | 439 | ||
| 426 | - var effectiveCount = input.Items.Count(static x => x is not null && !string.IsNullOrWhiteSpace(x.Id)); | 440 | + var effectiveCount = input.Items.Count(static x => |
| 441 | + x is not null && | ||
| 442 | + !string.IsNullOrWhiteSpace(x.Id) && | ||
| 443 | + Guid.TryParse(x.Id!.Trim(), out var g) && | ||
| 444 | + g != Guid.Empty); | ||
| 427 | if (effectiveCount == 0) | 445 | if (effectiveCount == 0) |
| 428 | { | 446 | { |
| 429 | - throw new UserFriendlyException("没有有效的产品 Id(请为待保存行填写 id)"); | 447 | + throw new UserFriendlyException("没有有效的产品 Id(请为待保存行填写 Guid 格式 id)"); |
| 430 | } | 448 | } |
| 431 | 449 | ||
| 432 | var result = new ProductBulkUpdateResultDto(); | 450 | var result = new ProductBulkUpdateResultDto(); |
| @@ -438,9 +456,14 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -438,9 +456,14 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 438 | continue; | 456 | continue; |
| 439 | } | 457 | } |
| 440 | 458 | ||
| 459 | + if (!Guid.TryParse(item.Id.Trim(), out var productGuid) || productGuid == Guid.Empty) | ||
| 460 | + { | ||
| 461 | + continue; | ||
| 462 | + } | ||
| 463 | + | ||
| 441 | try | 464 | try |
| 442 | { | 465 | { |
| 443 | - await UpdateAsync(item.Id.Trim(), item); | 466 | + await UpdateAsync(productGuid, item); |
| 444 | result.SuccessCount++; | 467 | result.SuccessCount++; |
| 445 | } | 468 | } |
| 446 | catch (UserFriendlyException ex) | 469 | catch (UserFriendlyException ex) |
| @@ -458,7 +481,7 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -458,7 +481,7 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 458 | return result; | 481 | return result; |
| 459 | } | 482 | } |
| 460 | 483 | ||
| 461 | - private ISugarQueryable<FlProductDbEntity> BuildFilteredProductQuery(ProductGetListInputVo input) | 484 | + private async Task<ISugarQueryable<FlProductDbEntity>> BuildFilteredProductQueryAsync(ProductGetListInputVo input) |
| 462 | { | 485 | { |
| 463 | var keyword = input.Keyword?.Trim(); | 486 | var keyword = input.Keyword?.Trim(); |
| 464 | 487 | ||
| @@ -467,6 +490,22 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -467,6 +490,22 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 467 | .Where(x => !x.IsDeleted) | 490 | .Where(x => !x.IsDeleted) |
| 468 | .WhereIF(input.State != null, x => x.State == input.State); | 491 | .WhereIF(input.State != null, x => x.State == input.State); |
| 469 | 492 | ||
| 493 | + var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); | ||
| 494 | + if (locationIds is not null) | ||
| 495 | + { | ||
| 496 | + if (locationIds.Count == 0) | ||
| 497 | + { | ||
| 498 | + query = query.Where(_ => false); | ||
| 499 | + } | ||
| 500 | + else | ||
| 501 | + { | ||
| 502 | + query = query.Where(p => | ||
| 503 | + SqlFunc.Subqueryable<FlLocationProductDbEntity>() | ||
| 504 | + .Where(lp => lp.ProductId == p.Id && locationIds.Contains(lp.LocationId)) | ||
| 505 | + .Any()); | ||
| 506 | + } | ||
| 507 | + } | ||
| 508 | + | ||
| 470 | if (!string.IsNullOrWhiteSpace(keyword)) | 509 | if (!string.IsNullOrWhiteSpace(keyword)) |
| 471 | { | 510 | { |
| 472 | query = query | 511 | query = query |
| @@ -481,6 +520,66 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -481,6 +520,66 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 481 | return query; | 520 | return query; |
| 482 | } | 521 | } |
| 483 | 522 | ||
| 523 | + /// <summary> | ||
| 524 | + /// 与 Reports 列表一致:按门店 Id 优先,否则按 Region(groupId)或公司(partnerId)解析 location 主键集合。 | ||
| 525 | + /// </summary> | ||
| 526 | + private async Task<List<string>?> ResolveFilteredLocationIdsAsync(string? partnerId, string? groupId, | ||
| 527 | + string? locationId) | ||
| 528 | + { | ||
| 529 | + var locId = locationId?.Trim(); | ||
| 530 | + if (!string.IsNullOrWhiteSpace(locId)) | ||
| 531 | + { | ||
| 532 | + if (!Guid.TryParse(locId, out var locationGuid)) | ||
| 533 | + { | ||
| 534 | + return new List<string>(); | ||
| 535 | + } | ||
| 536 | + | ||
| 537 | + var exists = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>() | ||
| 538 | + .AnyAsync(x => !x.IsDeleted && x.Id == locationGuid); | ||
| 539 | + return exists ? new List<string> { locId } : new List<string>(); | ||
| 540 | + } | ||
| 541 | + | ||
| 542 | + var gid = groupId?.Trim(); | ||
| 543 | + var pid = partnerId?.Trim(); | ||
| 544 | + | ||
| 545 | + if (string.IsNullOrWhiteSpace(pid) && string.IsNullOrWhiteSpace(gid)) | ||
| 546 | + { | ||
| 547 | + return null; | ||
| 548 | + } | ||
| 549 | + | ||
| 550 | + var q = _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>().Where(x => !x.IsDeleted); | ||
| 551 | + | ||
| 552 | + if (!string.IsNullOrWhiteSpace(gid)) | ||
| 553 | + { | ||
| 554 | + var g = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | ||
| 555 | + .FirstAsync(x => !x.IsDeleted && x.Id == gid); | ||
| 556 | + if (g is null) | ||
| 557 | + { | ||
| 558 | + return new List<string>(); | ||
| 559 | + } | ||
| 560 | + | ||
| 561 | + var gName = g.GroupName?.Trim() ?? string.Empty; | ||
| 562 | + var partner = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | ||
| 563 | + .FirstAsync(x => !x.IsDeleted && x.Id == g.PartnerId); | ||
| 564 | + var pName = partner?.PartnerName?.Trim() ?? string.Empty; | ||
| 565 | + q = q.Where(x => x.GroupName == gName && x.Partner == pName); | ||
| 566 | + } | ||
| 567 | + else if (!string.IsNullOrWhiteSpace(pid)) | ||
| 568 | + { | ||
| 569 | + var partner = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | ||
| 570 | + .FirstAsync(x => !x.IsDeleted && x.Id == pid); | ||
| 571 | + if (partner is null) | ||
| 572 | + { | ||
| 573 | + return new List<string>(); | ||
| 574 | + } | ||
| 575 | + | ||
| 576 | + var pName = partner.PartnerName?.Trim() ?? string.Empty; | ||
| 577 | + q = q.Where(x => x.Partner == pName); | ||
| 578 | + } | ||
| 579 | + | ||
| 580 | + return await q.Select(x => SqlFunc.ToString(x.Id)).ToListAsync(); | ||
| 581 | + } | ||
| 582 | + | ||
| 484 | private async Task<List<ProductBatchExcelHelper.ExportRow>> BuildProductExcelExportRowsAsync( | 583 | private async Task<List<ProductBatchExcelHelper.ExportRow>> BuildProductExcelExportRowsAsync( |
| 485 | List<FlProductDbEntity> entities) | 584 | List<FlProductDbEntity> entities) |
| 486 | { | 585 | { |
| @@ -657,40 +756,23 @@ public class ProductAppService : ApplicationService, IProductAppService | @@ -657,40 +756,23 @@ public class ProductAppService : ApplicationService, IProductAppService | ||
| 657 | throw new UserFriendlyException("无法生成唯一产品编码,请稍后重试或手动填写产品编码"); | 756 | throw new UserFriendlyException("无法生成唯一产品编码,请稍后重试或手动填写产品编码"); |
| 658 | } | 757 | } |
| 659 | 758 | ||
| 759 | + private static bool HasProductScopeBinding(ProductCreateInputVo input) => | ||
| 760 | + !string.IsNullOrWhiteSpace(input.PartnerId) || | ||
| 761 | + input.GroupIds is not null || | ||
| 762 | + input.LocationIds is not null; | ||
| 763 | + | ||
| 660 | /// <summary> | 764 | /// <summary> |
| 661 | - /// 去重、校验门店 Id 格式与存在性。 | 765 | + /// 合并 Company(partnerId)、Region(groupIds)、门店(locationIds)并校验存在性。 |
| 662 | /// </summary> | 766 | /// </summary> |
| 663 | - private async Task<List<string>> NormalizeAndValidateLocationIdsAsync(IEnumerable<string> rawIds) | 767 | + private async Task<List<string>> ResolveProductLocationIdsForSaveAsync(ProductCreateInputVo input) |
| 664 | { | 768 | { |
| 665 | - var distinct = rawIds | ||
| 666 | - .Where(x => !string.IsNullOrWhiteSpace(x)) | ||
| 667 | - .Select(x => x.Trim()) | ||
| 668 | - .Distinct(StringComparer.Ordinal) | ||
| 669 | - .ToList(); | ||
| 670 | - | ||
| 671 | - if (distinct.Count == 0) | ||
| 672 | - { | ||
| 673 | - return new List<string>(); | ||
| 674 | - } | ||
| 675 | - | ||
| 676 | - foreach (var id in distinct) | ||
| 677 | - { | ||
| 678 | - if (!Guid.TryParse(id, out _)) | ||
| 679 | - { | ||
| 680 | - throw new UserFriendlyException("门店Id格式不正确"); | ||
| 681 | - } | ||
| 682 | - } | ||
| 683 | - | ||
| 684 | - var guidList = distinct.Select(Guid.Parse).ToList(); | ||
| 685 | - var existCount = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>() | ||
| 686 | - .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) | ||
| 687 | - .CountAsync(); | ||
| 688 | - if (existCount != distinct.Count) | ||
| 689 | - { | ||
| 690 | - throw new UserFriendlyException("门店不存在"); | ||
| 691 | - } | ||
| 692 | - | ||
| 693 | - return distinct; | 769 | + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( |
| 770 | + _dbContext.SqlSugarClient, | ||
| 771 | + input.PartnerId, | ||
| 772 | + input.GroupIds, | ||
| 773 | + input.LocationIds); | ||
| 774 | + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); | ||
| 775 | + return merged; | ||
| 694 | } | 776 | } |
| 695 | 777 | ||
| 696 | /// <summary> | 778 | /// <summary> |
美国版/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; | @@ -3,6 +3,7 @@ using FoodLabeling.Application.Contracts.Dtos.Common; | ||
| 3 | using FoodLabeling.Application.Contracts.Dtos.ProductCategory; | 3 | using FoodLabeling.Application.Contracts.Dtos.ProductCategory; |
| 4 | using FoodLabeling.Application.Contracts.IServices; | 4 | using FoodLabeling.Application.Contracts.IServices; |
| 5 | using FoodLabeling.Application.Services.DbModels; | 5 | using FoodLabeling.Application.Services.DbModels; |
| 6 | +using FoodLabeling.Domain.Entities; | ||
| 6 | using SqlSugar; | 7 | using SqlSugar; |
| 7 | using Volo.Abp; | 8 | using Volo.Abp; |
| 8 | using Volo.Abp.Application.Services; | 9 | using Volo.Abp.Application.Services; |
| @@ -40,6 +41,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -40,6 +41,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 40 | (x.DisplayText != null && x.DisplayText.Contains(keyword!))) | 41 | (x.DisplayText != null && x.DisplayText.Contains(keyword!))) |
| 41 | .WhereIF(input.State != null, x => x.State == input.State); | 42 | .WhereIF(input.State != null, x => x.State == input.State); |
| 42 | 43 | ||
| 44 | + query = await ApplyCategoryScopeFilterAsync(query, input.GroupId, input.LocationId); | ||
| 45 | + | ||
| 43 | // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column | 46 | // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column |
| 44 | // 同时避免将 input.Sorting 原样拼接到 SQL(存在注入风险) | 47 | // 同时避免将 input.Sorting 原样拼接到 SQL(存在注入风险) |
| 45 | if (!string.IsNullOrWhiteSpace(input.Sorting)) | 48 | if (!string.IsNullOrWhiteSpace(input.Sorting)) |
| @@ -74,18 +77,28 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -74,18 +77,28 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 74 | 77 | ||
| 75 | var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); | 78 | var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); |
| 76 | 79 | ||
| 77 | - var items = entities.Select(x => new ProductCategoryGetListOutputDto | 80 | + var scopeMap = await BuildCategoryScopeMapAsync(entities); |
| 81 | + | ||
| 82 | + var items = entities.Select(x => | ||
| 78 | { | 83 | { |
| 79 | - Id = x.Id, | ||
| 80 | - CategoryCode = x.CategoryCode, | ||
| 81 | - CategoryName = x.CategoryName, | ||
| 82 | - DisplayText = x.DisplayText, | ||
| 83 | - CategoryPhotoUrl = x.CategoryPhotoUrl, | ||
| 84 | - ButtonAppearance = x.ButtonAppearance, | ||
| 85 | - State = x.State, | ||
| 86 | - AvailabilityType = x.AvailabilityType, | ||
| 87 | - OrderNum = x.OrderNum, | ||
| 88 | - LastEdited = x.LastModificationTime ?? x.CreationTime | 84 | + scopeMap.TryGetValue(x.Id, out var scope); |
| 85 | + return new ProductCategoryGetListOutputDto | ||
| 86 | + { | ||
| 87 | + Id = x.Id, | ||
| 88 | + CategoryCode = x.CategoryCode, | ||
| 89 | + CategoryName = x.CategoryName, | ||
| 90 | + DisplayText = x.DisplayText, | ||
| 91 | + CategoryPhotoUrl = x.CategoryPhotoUrl, | ||
| 92 | + ButtonAppearance = x.ButtonAppearance, | ||
| 93 | + State = x.State, | ||
| 94 | + AvailabilityType = x.AvailabilityType, | ||
| 95 | + OrderNum = x.OrderNum, | ||
| 96 | + LastEdited = x.LastModificationTime ?? x.CreationTime, | ||
| 97 | + Region = scope?.Region ?? string.Empty, | ||
| 98 | + Location = scope?.Location ?? string.Empty, | ||
| 99 | + RegionIds = scope?.RegionIds ?? new List<string>(), | ||
| 100 | + LocationIds = scope?.LocationIds ?? new List<string>() | ||
| 101 | + }; | ||
| 89 | }).ToList(); | 102 | }).ToList(); |
| 90 | 103 | ||
| 91 | return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); | 104 | return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); |
| @@ -110,7 +123,11 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -110,7 +123,11 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 110 | .Where(x => x.CategoryId == entity.Id) | 123 | .Where(x => x.CategoryId == entity.Id) |
| 111 | .Select(x => x.LocationId) | 124 | .Select(x => x.LocationId) |
| 112 | .ToListAsync(); | 125 | .ToListAsync(); |
| 113 | - dto.LocationIds = locationIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList() ?? new(); | 126 | + dto.LocationIds = LocationScopeBindingHelper.NormalizeIds(locationIds); |
| 127 | + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( | ||
| 128 | + _dbContext.SqlSugarClient, dto.LocationIds); | ||
| 129 | + dto.RegionIds = regionIds; | ||
| 130 | + dto.GroupIds = regionIds; | ||
| 114 | } | 131 | } |
| 115 | 132 | ||
| 116 | return dto; | 133 | return dto; |
| @@ -130,9 +147,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -130,9 +147,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 130 | 147 | ||
| 131 | var displayText = input.DisplayText?.Trim(); | 148 | var displayText = input.DisplayText?.Trim(); |
| 132 | var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); | 149 | var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); |
| 133 | - var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); | ||
| 134 | - var locationIds = NormalizeLocationIds(input.LocationIds); | ||
| 135 | - ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); | 150 | + var (availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); |
| 136 | 151 | ||
| 137 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() | 152 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() |
| 138 | .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); | 153 | .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); |
| @@ -163,7 +178,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -163,7 +178,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 163 | }; | 178 | }; |
| 164 | 179 | ||
| 165 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); | 180 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); |
| 166 | - await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, currentUserId, now); | 181 | + await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); |
| 167 | return await GetAsync(entity.Id); | 182 | return await GetAsync(entity.Id); |
| 168 | } | 183 | } |
| 169 | 184 | ||
| @@ -188,9 +203,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -188,9 +203,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 188 | 203 | ||
| 189 | var displayText = input.DisplayText?.Trim(); | 204 | var displayText = input.DisplayText?.Trim(); |
| 190 | var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); | 205 | var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); |
| 191 | - var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); | ||
| 192 | - var locationIds = NormalizeLocationIds(input.LocationIds); | ||
| 193 | - ValidateAvailabilityTypeAndLocations(availabilityType, locationIds); | 206 | + var (availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); |
| 194 | 207 | ||
| 195 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() | 208 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>() |
| 196 | .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); | 209 | .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); |
| @@ -211,7 +224,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -211,7 +224,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 211 | entity.LastModifierId = CurrentUser?.Id?.ToString(); | 224 | entity.LastModifierId = CurrentUser?.Id?.ToString(); |
| 212 | 225 | ||
| 213 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); | 226 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 214 | - await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now); | 227 | + await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, |
| 228 | + entity.LastModificationTime ?? DateTime.Now); | ||
| 215 | return await GetAsync(id); | 229 | return await GetAsync(id); |
| 216 | } | 230 | } |
| 217 | 231 | ||
| @@ -257,26 +271,86 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -257,26 +271,86 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 257 | }; | 271 | }; |
| 258 | } | 272 | } |
| 259 | 273 | ||
| 260 | - private static void ValidateAvailabilityTypeAndLocations(string availabilityType, List<string> locationIds) | 274 | + private async Task<(string AvailabilityType, List<string> LocationIds)> ResolveCategoryScopeForSaveAsync( |
| 275 | + ProductCategoryCreateInputVo input) | ||
| 261 | { | 276 | { |
| 277 | + var regionIds = NormalizeRegionIds(input); | ||
| 278 | + var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); | ||
| 279 | + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); | ||
| 280 | + | ||
| 281 | + var hasScopeArrays = input.RegionIds is not null || input.GroupIds is not null || input.LocationIds is not null; | ||
| 282 | + if (regionIds.Count > 0 || explicitLocationIds.Count > 0) | ||
| 283 | + { | ||
| 284 | + availabilityType = "SPECIFIED"; | ||
| 285 | + } | ||
| 286 | + else if (hasScopeArrays && string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase)) | ||
| 287 | + { | ||
| 288 | + // 显式传了空数组 [] 表示清空指定范围,仍视为 SPECIFIED(0 门店)或改回 ALL | ||
| 289 | + availabilityType = "ALL"; | ||
| 290 | + } | ||
| 291 | + | ||
| 262 | if (availabilityType != "ALL" && availabilityType != "SPECIFIED") | 292 | if (availabilityType != "ALL" && availabilityType != "SPECIFIED") |
| 263 | { | 293 | { |
| 264 | throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); | 294 | throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); |
| 265 | } | 295 | } |
| 266 | 296 | ||
| 267 | - if (availabilityType == "SPECIFIED" && locationIds.Count == 0) | 297 | + if (availabilityType == "ALL") |
| 298 | + { | ||
| 299 | + return ("ALL", new List<string>()); | ||
| 300 | + } | ||
| 301 | + | ||
| 302 | + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | ||
| 303 | + _dbContext.SqlSugarClient, (IReadOnlyList<string>?)null, regionIds, explicitLocationIds); | ||
| 304 | + if (merged.Count == 0) | ||
| 268 | { | 305 | { |
| 269 | - throw new UserFriendlyException("指定门店范围时必须至少选择一个门店"); | 306 | + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); |
| 270 | } | 307 | } |
| 308 | + | ||
| 309 | + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); | ||
| 310 | + return ("SPECIFIED", merged); | ||
| 271 | } | 311 | } |
| 272 | 312 | ||
| 273 | - private static List<string> NormalizeLocationIds(List<string>? locationIds) | 313 | + /// <summary> |
| 314 | + /// 合并入参中的 Region 多选(<see cref="ProductCategoryCreateInputVo.RegionIds"/> 与 <see cref="ProductCategoryCreateInputVo.GroupIds"/>)。 | ||
| 315 | + /// </summary> | ||
| 316 | + private static List<string> NormalizeRegionIds(ProductCategoryCreateInputVo input) | ||
| 274 | { | 317 | { |
| 275 | - return locationIds? | ||
| 276 | - .Where(x => !string.IsNullOrWhiteSpace(x)) | ||
| 277 | - .Select(x => x.Trim()) | ||
| 278 | - .Distinct() | ||
| 279 | - .ToList() ?? new(); | 318 | + var merged = new HashSet<string>(StringComparer.Ordinal); |
| 319 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds)) | ||
| 320 | + { | ||
| 321 | + merged.Add(id); | ||
| 322 | + } | ||
| 323 | + | ||
| 324 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds)) | ||
| 325 | + { | ||
| 326 | + merged.Add(id); | ||
| 327 | + } | ||
| 328 | + | ||
| 329 | + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); | ||
| 330 | + } | ||
| 331 | + | ||
| 332 | + private async Task<ISugarQueryable<FlProductCategoryDbEntity>> ApplyCategoryScopeFilterAsync( | ||
| 333 | + ISugarQueryable<FlProductCategoryDbEntity> query, | ||
| 334 | + string? groupId, | ||
| 335 | + string? locationId) | ||
| 336 | + { | ||
| 337 | + var scopeLocIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( | ||
| 338 | + _dbContext.SqlSugarClient, groupId, locationId); | ||
| 339 | + if (scopeLocIds is null) | ||
| 340 | + { | ||
| 341 | + return query; | ||
| 342 | + } | ||
| 343 | + | ||
| 344 | + if (scopeLocIds.Count == 0) | ||
| 345 | + { | ||
| 346 | + return query.Where(c => c.AvailabilityType == "ALL"); | ||
| 347 | + } | ||
| 348 | + | ||
| 349 | + return query.Where(c => | ||
| 350 | + c.AvailabilityType == "ALL" || | ||
| 351 | + SqlFunc.Subqueryable<FlProductCategoryLocationDbEntity>() | ||
| 352 | + .Where(cl => cl.CategoryId == c.Id && scopeLocIds.Contains(cl.LocationId)) | ||
| 353 | + .Any()); | ||
| 280 | } | 354 | } |
| 281 | 355 | ||
| 282 | private async Task SaveCategoryLocationsAsync( | 356 | private async Task SaveCategoryLocationsAsync( |
| @@ -312,6 +386,143 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | @@ -312,6 +386,143 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp | ||
| 312 | await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); | 386 | await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); |
| 313 | } | 387 | } |
| 314 | 388 | ||
| 389 | + private const string AllRegionsDisplay = "All Regions"; | ||
| 390 | + private const string AllLocationsDisplay = "All Locations"; | ||
| 391 | + private const string EmptyDisplay = "无"; | ||
| 392 | + | ||
| 393 | + /// <summary> | ||
| 394 | + /// 列表行:Region/Location 展示文案 + 多选 Id 数组(编辑回显)。 | ||
| 395 | + /// </summary> | ||
| 396 | + private async Task<Dictionary<string, CategoryScopeData>> BuildCategoryScopeMapAsync( | ||
| 397 | + List<FlProductCategoryDbEntity> entities) | ||
| 398 | + { | ||
| 399 | + var result = new Dictionary<string, CategoryScopeData>(StringComparer.Ordinal); | ||
| 400 | + if (entities.Count == 0) | ||
| 401 | + { | ||
| 402 | + return result; | ||
| 403 | + } | ||
| 404 | + | ||
| 405 | + foreach (var e in entities.Where(x => | ||
| 406 | + !string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))) | ||
| 407 | + { | ||
| 408 | + result[e.Id] = new CategoryScopeData | ||
| 409 | + { | ||
| 410 | + Region = AllRegionsDisplay, | ||
| 411 | + Location = AllLocationsDisplay, | ||
| 412 | + RegionIds = new List<string>(), | ||
| 413 | + LocationIds = new List<string>() | ||
| 414 | + }; | ||
| 415 | + } | ||
| 416 | + | ||
| 417 | + var specifiedIds = entities | ||
| 418 | + .Where(x => string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) | ||
| 419 | + .Select(x => x.Id) | ||
| 420 | + .ToList(); | ||
| 421 | + if (specifiedIds.Count == 0) | ||
| 422 | + { | ||
| 423 | + return result; | ||
| 424 | + } | ||
| 425 | + | ||
| 426 | + var links = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryLocationDbEntity>() | ||
| 427 | + .Where(x => specifiedIds.Contains(x.CategoryId)) | ||
| 428 | + .ToListAsync(); | ||
| 429 | + | ||
| 430 | + var locIdSet = links | ||
| 431 | + .Select(x => x.LocationId) | ||
| 432 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | ||
| 433 | + .Select(x => x.Trim()) | ||
| 434 | + .Distinct(StringComparer.Ordinal) | ||
| 435 | + .ToList(); | ||
| 436 | + | ||
| 437 | + var locById = new Dictionary<string, LocationAggregateRoot>(StringComparer.Ordinal); | ||
| 438 | + if (locIdSet.Count > 0) | ||
| 439 | + { | ||
| 440 | + var guidList = locIdSet.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); | ||
| 441 | + if (guidList.Count > 0) | ||
| 442 | + { | ||
| 443 | + var locs = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>() | ||
| 444 | + .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) | ||
| 445 | + .ToListAsync(); | ||
| 446 | + foreach (var loc in locs) | ||
| 447 | + { | ||
| 448 | + locById[loc.Id.ToString()] = loc; | ||
| 449 | + } | ||
| 450 | + } | ||
| 451 | + } | ||
| 452 | + | ||
| 453 | + foreach (var catId in specifiedIds) | ||
| 454 | + { | ||
| 455 | + var catLinks = links.Where(x => x.CategoryId == catId).ToList(); | ||
| 456 | + var locationIds = LocationScopeBindingHelper.NormalizeIds( | ||
| 457 | + catLinks.Select(x => x.LocationId).ToList()); | ||
| 458 | + | ||
| 459 | + if (catLinks.Count == 0) | ||
| 460 | + { | ||
| 461 | + result[catId] = new CategoryScopeData | ||
| 462 | + { | ||
| 463 | + Region = EmptyDisplay, | ||
| 464 | + Location = EmptyDisplay, | ||
| 465 | + RegionIds = new List<string>(), | ||
| 466 | + LocationIds = new List<string>() | ||
| 467 | + }; | ||
| 468 | + continue; | ||
| 469 | + } | ||
| 470 | + | ||
| 471 | + var regions = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
| 472 | + var locationNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
| 473 | + foreach (var link in catLinks) | ||
| 474 | + { | ||
| 475 | + var lid = link.LocationId?.Trim() ?? string.Empty; | ||
| 476 | + if (string.IsNullOrEmpty(lid) || !locById.TryGetValue(lid, out var loc)) | ||
| 477 | + { | ||
| 478 | + continue; | ||
| 479 | + } | ||
| 480 | + | ||
| 481 | + var groupName = loc.GroupName?.Trim(); | ||
| 482 | + if (!string.IsNullOrEmpty(groupName)) | ||
| 483 | + { | ||
| 484 | + regions.Add(groupName); | ||
| 485 | + } | ||
| 486 | + | ||
| 487 | + var locName = loc.LocationName?.Trim(); | ||
| 488 | + if (string.IsNullOrEmpty(locName)) | ||
| 489 | + { | ||
| 490 | + locName = loc.LocationCode?.Trim(); | ||
| 491 | + } | ||
| 492 | + | ||
| 493 | + if (!string.IsNullOrEmpty(locName)) | ||
| 494 | + { | ||
| 495 | + locationNames.Add(locName); | ||
| 496 | + } | ||
| 497 | + } | ||
| 498 | + | ||
| 499 | + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( | ||
| 500 | + _dbContext.SqlSugarClient, locationIds); | ||
| 501 | + | ||
| 502 | + result[catId] = new CategoryScopeData | ||
| 503 | + { | ||
| 504 | + Region = regions.Count > 0 | ||
| 505 | + ? string.Join(", ", regions.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) | ||
| 506 | + : EmptyDisplay, | ||
| 507 | + Location = locationNames.Count > 0 | ||
| 508 | + ? string.Join(", ", locationNames.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) | ||
| 509 | + : EmptyDisplay, | ||
| 510 | + RegionIds = regionIds, | ||
| 511 | + LocationIds = locationIds | ||
| 512 | + }; | ||
| 513 | + } | ||
| 514 | + | ||
| 515 | + return result; | ||
| 516 | + } | ||
| 517 | + | ||
| 518 | + private sealed class CategoryScopeData | ||
| 519 | + { | ||
| 520 | + public string Region { get; init; } = string.Empty; | ||
| 521 | + public string Location { get; init; } = string.Empty; | ||
| 522 | + public List<string> RegionIds { get; init; } = new(); | ||
| 523 | + public List<string> LocationIds { get; init; } = new(); | ||
| 524 | + } | ||
| 525 | + | ||
| 315 | private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items) | 526 | private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items) |
| 316 | { | 527 | { |
| 317 | var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; | 528 | var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; |
美国版/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 | @@ -23,6 +23,7 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService | ||
| 23 | { | 23 | { |
| 24 | private readonly ISqlSugarDbContext _dbContext; | 24 | private readonly ISqlSugarDbContext _dbContext; |
| 25 | private readonly ISqlSugarRepository<RoleAggregateRoot, Guid> _roleRepository; | 25 | private readonly ISqlSugarRepository<RoleAggregateRoot, Guid> _roleRepository; |
| 26 | + private readonly ISqlSugarRepository<MenuAggregateRoot, Guid> _menuRepository; | ||
| 26 | private readonly ISqlSugarRepository<RoleMenuEntity> _roleMenuRepository; | 27 | private readonly ISqlSugarRepository<RoleMenuEntity> _roleMenuRepository; |
| 27 | private readonly ISqlSugarRepository<RoleDeptEntity> _roleDeptRepository; | 28 | private readonly ISqlSugarRepository<RoleDeptEntity> _roleDeptRepository; |
| 28 | private readonly ISqlSugarRepository<UserRoleEntity> _userRoleRepository; | 29 | private readonly ISqlSugarRepository<UserRoleEntity> _userRoleRepository; |
| @@ -30,12 +31,14 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService | @@ -30,12 +31,14 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService | ||
| 30 | public RbacRoleAppService( | 31 | public RbacRoleAppService( |
| 31 | ISqlSugarDbContext dbContext, | 32 | ISqlSugarDbContext dbContext, |
| 32 | ISqlSugarRepository<RoleAggregateRoot, Guid> roleRepository, | 33 | ISqlSugarRepository<RoleAggregateRoot, Guid> roleRepository, |
| 34 | + ISqlSugarRepository<MenuAggregateRoot, Guid> menuRepository, | ||
| 33 | ISqlSugarRepository<RoleMenuEntity> roleMenuRepository, | 35 | ISqlSugarRepository<RoleMenuEntity> roleMenuRepository, |
| 34 | ISqlSugarRepository<RoleDeptEntity> roleDeptRepository, | 36 | ISqlSugarRepository<RoleDeptEntity> roleDeptRepository, |
| 35 | ISqlSugarRepository<UserRoleEntity> userRoleRepository) | 37 | ISqlSugarRepository<UserRoleEntity> userRoleRepository) |
| 36 | { | 38 | { |
| 37 | _dbContext = dbContext; | 39 | _dbContext = dbContext; |
| 38 | _roleRepository = roleRepository; | 40 | _roleRepository = roleRepository; |
| 41 | + _menuRepository = menuRepository; | ||
| 39 | _roleMenuRepository = roleMenuRepository; | 42 | _roleMenuRepository = roleMenuRepository; |
| 40 | _roleDeptRepository = roleDeptRepository; | 43 | _roleDeptRepository = roleDeptRepository; |
| 41 | _userRoleRepository = userRoleRepository; | 44 | _userRoleRepository = userRoleRepository; |
| @@ -75,6 +78,8 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService | @@ -75,6 +78,8 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService | ||
| 75 | AccessPermissionCodes = DeserializeAccessPermissionCodes(x.AccessPermissionCodesJson) | 78 | AccessPermissionCodes = DeserializeAccessPermissionCodes(x.AccessPermissionCodesJson) |
| 76 | }).ToList(); | 79 | }).ToList(); |
| 77 | 80 | ||
| 81 | + await FillAccessPermissionsAsync(items); | ||
| 82 | + | ||
| 78 | var pageSize = input.MaxResultCount <= 0 ? items.Count : input.MaxResultCount; | 83 | var pageSize = input.MaxResultCount <= 0 ? items.Count : input.MaxResultCount; |
| 79 | var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); | 84 | var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); |
| 80 | var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize); | 85 | var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize); |
| @@ -103,7 +108,7 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService | @@ -103,7 +108,7 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService | ||
| 103 | .Select(x => x.MenuId) | 108 | .Select(x => x.MenuId) |
| 104 | .ToListAsync(); | 109 | .ToListAsync(); |
| 105 | 110 | ||
| 106 | - return new RbacRoleGetOutputDto | 111 | + var dto = new RbacRoleGetOutputDto |
| 107 | { | 112 | { |
| 108 | Id = entity.Id, | 113 | Id = entity.Id, |
| 109 | RoleName = entity.RoleName ?? string.Empty, | 114 | RoleName = entity.RoleName ?? string.Empty, |
| @@ -115,9 +120,12 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService | @@ -115,9 +120,12 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService | ||
| 115 | AccessPermissionCodes = DeserializeAccessPermissionCodes(entity.AccessPermissionCodesJson), | 120 | AccessPermissionCodes = DeserializeAccessPermissionCodes(entity.AccessPermissionCodesJson), |
| 116 | MenuIds = menuIds | 121 | MenuIds = menuIds |
| 117 | }; | 122 | }; |
| 123 | + await FillAccessPermissionsAsync(new List<RbacRoleGetListOutputDto> { dto }); | ||
| 124 | + return dto; | ||
| 118 | } | 125 | } |
| 119 | 126 | ||
| 120 | /// <inheritdoc /> | 127 | /// <inheritdoc /> |
| 128 | + [UnitOfWork] | ||
| 121 | public async Task<RbacRoleGetOutputDto> CreateAsync([FromBody] RbacRoleCreateInputVo input) | 129 | public async Task<RbacRoleGetOutputDto> CreateAsync([FromBody] RbacRoleCreateInputVo input) |
| 122 | { | 130 | { |
| 123 | var roleName = input.RoleName?.Trim(); | 131 | var roleName = input.RoleName?.Trim(); |
| @@ -145,17 +153,19 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService | @@ -145,17 +153,19 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService | ||
| 145 | Remark = input.Remark?.Trim(), | 153 | Remark = input.Remark?.Trim(), |
| 146 | DataScope = (Yi.Framework.Rbac.Domain.Shared.Enums.DataScopeEnum)input.DataScope, | 154 | DataScope = (Yi.Framework.Rbac.Domain.Shared.Enums.DataScopeEnum)input.DataScope, |
| 147 | State = input.State, | 155 | State = input.State, |
| 148 | - OrderNum = input.OrderNum ?? 0, | ||
| 149 | - AccessPermissionCodesJson = SerializeAccessPermissionCodes(ResolveAccessPermissions(input)) | 156 | + OrderNum = input.OrderNum ?? 0 |
| 150 | }; | 157 | }; |
| 151 | EntityHelper.TrySetId(entity, () => GuidGenerator.Create()); | 158 | EntityHelper.TrySetId(entity, () => GuidGenerator.Create()); |
| 152 | 159 | ||
| 153 | await _roleRepository.InsertAsync(entity); | 160 | await _roleRepository.InsertAsync(entity); |
| 154 | 161 | ||
| 162 | + await ApplyRoleMenuBindingsAsync(entity.Id, input); | ||
| 163 | + | ||
| 155 | return await GetAsync(entity.Id); | 164 | return await GetAsync(entity.Id); |
| 156 | } | 165 | } |
| 157 | 166 | ||
| 158 | /// <inheritdoc /> | 167 | /// <inheritdoc /> |
| 168 | + [UnitOfWork] | ||
| 159 | public async Task<RbacRoleGetOutputDto> UpdateAsync(Guid id, [FromBody] RbacRoleUpdateInputVo input) | 169 | public async Task<RbacRoleGetOutputDto> UpdateAsync(Guid id, [FromBody] RbacRoleUpdateInputVo input) |
| 160 | { | 170 | { |
| 161 | var entity = await _roleRepository.GetSingleAsync(x => x.Id == id && x.IsDeleted == false); | 171 | var entity = await _roleRepository.GetSingleAsync(x => x.Id == id && x.IsDeleted == false); |
| @@ -189,15 +199,157 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService | @@ -189,15 +199,157 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService | ||
| 189 | entity.Remark = input.Remark?.Trim(); | 199 | entity.Remark = input.Remark?.Trim(); |
| 190 | entity.DataScope = (Yi.Framework.Rbac.Domain.Shared.Enums.DataScopeEnum)input.DataScope; | 200 | entity.DataScope = (Yi.Framework.Rbac.Domain.Shared.Enums.DataScopeEnum)input.DataScope; |
| 191 | entity.State = input.State; | 201 | entity.State = input.State; |
| 192 | - entity.OrderNum = input.OrderNum ?? 0; | ||
| 193 | - entity.AccessPermissionCodesJson = | ||
| 194 | - SerializeAccessPermissionCodes(ResolveAccessPermissions(input)); | 202 | + if (input.OrderNum is not null) |
| 203 | + { | ||
| 204 | + entity.OrderNum = input.OrderNum.Value; | ||
| 205 | + } | ||
| 195 | 206 | ||
| 196 | await _roleRepository.UpdateAsync(entity); | 207 | await _roleRepository.UpdateAsync(entity); |
| 197 | 208 | ||
| 209 | + await ApplyRoleMenuBindingsAsync(entity.Id, input); | ||
| 210 | + | ||
| 198 | return await GetAsync(entity.Id); | 211 | return await GetAsync(entity.Id); |
| 199 | } | 212 | } |
| 200 | 213 | ||
| 214 | + /// <summary> | ||
| 215 | + /// 新增/编辑时按 menuIds 或 accessPermissions 绑定角色菜单(二者都未传则不改绑定) | ||
| 216 | + /// </summary> | ||
| 217 | + private async Task ApplyRoleMenuBindingsAsync(Guid roleId, RbacRoleCreateInputVo input) | ||
| 218 | + { | ||
| 219 | + if (input.MenuIds is not null) | ||
| 220 | + { | ||
| 221 | + await SetRoleMenusAsync(roleId, input.MenuIds); | ||
| 222 | + return; | ||
| 223 | + } | ||
| 224 | + | ||
| 225 | + if (input.AccessPermissions is not null) | ||
| 226 | + { | ||
| 227 | + var menuIds = await ResolveMenuIdsFromAccessPermissionsAsync(input.AccessPermissions); | ||
| 228 | + await SetRoleMenusAsync(roleId, menuIds); | ||
| 229 | + } | ||
| 230 | + } | ||
| 231 | + | ||
| 232 | + private async Task SetRoleMenusAsync(Guid roleId, List<Guid> menuIds) | ||
| 233 | + { | ||
| 234 | + var distinct = menuIds?.Distinct().ToList() ?? new List<Guid>(); | ||
| 235 | + await _roleMenuRepository.DeleteAsync(x => x.RoleId == roleId); | ||
| 236 | + | ||
| 237 | + if (distinct.Count == 0) | ||
| 238 | + { | ||
| 239 | + return; | ||
| 240 | + } | ||
| 241 | + | ||
| 242 | + var existMenuIds = await _menuRepository._DbQueryable | ||
| 243 | + .Where(x => x.IsDeleted == false) | ||
| 244 | + .Where(x => distinct.Contains(x.Id)) | ||
| 245 | + .Select(x => x.Id) | ||
| 246 | + .ToListAsync(); | ||
| 247 | + | ||
| 248 | + if (existMenuIds.Count == 0) | ||
| 249 | + { | ||
| 250 | + return; | ||
| 251 | + } | ||
| 252 | + | ||
| 253 | + var entities = existMenuIds.Select(menuId => new RoleMenuEntity | ||
| 254 | + { | ||
| 255 | + RoleId = roleId, | ||
| 256 | + MenuId = menuId | ||
| 257 | + }).ToList(); | ||
| 258 | + | ||
| 259 | + await _roleMenuRepository.InsertRangeAsync(entities); | ||
| 260 | + } | ||
| 261 | + | ||
| 262 | + private async Task<List<Guid>> ResolveMenuIdsFromAccessPermissionsAsync(string accessPermissions) | ||
| 263 | + { | ||
| 264 | + var codes = accessPermissions | ||
| 265 | + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) | ||
| 266 | + .Where(c => !string.IsNullOrWhiteSpace(c)) | ||
| 267 | + .Distinct(StringComparer.Ordinal) | ||
| 268 | + .ToList(); | ||
| 269 | + | ||
| 270 | + if (codes.Count == 0) | ||
| 271 | + { | ||
| 272 | + return new List<Guid>(); | ||
| 273 | + } | ||
| 274 | + | ||
| 275 | + var menus = await _menuRepository._DbQueryable | ||
| 276 | + .Where(m => m.IsDeleted == false && m.PermissionCode != null) | ||
| 277 | + .Where(m => codes.Contains(m.PermissionCode!)) | ||
| 278 | + .Select(m => m.Id) | ||
| 279 | + .ToListAsync(); | ||
| 280 | + | ||
| 281 | + return menus; | ||
| 282 | + } | ||
| 283 | + | ||
| 284 | + private async Task FillAccessPermissionsAsync(List<RbacRoleGetListOutputDto> items) | ||
| 285 | + { | ||
| 286 | + if (items.Count == 0) | ||
| 287 | + { | ||
| 288 | + return; | ||
| 289 | + } | ||
| 290 | + | ||
| 291 | + var map = await GetAccessPermissionsByRoleIdsAsync(items.Select(x => x.Id).ToList()); | ||
| 292 | + foreach (var item in items) | ||
| 293 | + { | ||
| 294 | + item.AccessPermissions = map.GetValueOrDefault(item.Id, string.Empty); | ||
| 295 | + } | ||
| 296 | + } | ||
| 297 | + | ||
| 298 | + /// <summary> | ||
| 299 | + /// 按角色汇总已绑定菜单上的 PermissionCode(去重、英文逗号+空格拼接) | ||
| 300 | + /// </summary> | ||
| 301 | + private async Task<Dictionary<Guid, string>> GetAccessPermissionsByRoleIdsAsync(List<Guid> roleIds) | ||
| 302 | + { | ||
| 303 | + var result = roleIds.Distinct().ToDictionary(id => id, _ => string.Empty); | ||
| 304 | + if (result.Count == 0) | ||
| 305 | + { | ||
| 306 | + return result; | ||
| 307 | + } | ||
| 308 | + | ||
| 309 | + var distinctRoleIds = result.Keys.ToList(); | ||
| 310 | + var links = await _roleMenuRepository._DbQueryable | ||
| 311 | + .Where(rm => distinctRoleIds.Contains(rm.RoleId)) | ||
| 312 | + .Select(rm => new { rm.RoleId, rm.MenuId }) | ||
| 313 | + .ToListAsync(); | ||
| 314 | + if (links.Count == 0) | ||
| 315 | + { | ||
| 316 | + return result; | ||
| 317 | + } | ||
| 318 | + | ||
| 319 | + var menuIds = links.Select(x => x.MenuId).Distinct().ToList(); | ||
| 320 | + var menus = await _menuRepository._DbQueryable | ||
| 321 | + .Where(m => menuIds.Contains(m.Id) && m.IsDeleted == false) | ||
| 322 | + .Select(m => new { m.Id, m.PermissionCode }) | ||
| 323 | + .ToListAsync(); | ||
| 324 | + var permByMenuId = menus.ToDictionary(x => x.Id, x => x.PermissionCode); | ||
| 325 | + | ||
| 326 | + var byRole = distinctRoleIds.ToDictionary(id => id, _ => new HashSet<string>(StringComparer.Ordinal)); | ||
| 327 | + foreach (var link in links) | ||
| 328 | + { | ||
| 329 | + if (!permByMenuId.TryGetValue(link.MenuId, out var code) || string.IsNullOrWhiteSpace(code)) | ||
| 330 | + { | ||
| 331 | + continue; | ||
| 332 | + } | ||
| 333 | + | ||
| 334 | + if (byRole.TryGetValue(link.RoleId, out var set)) | ||
| 335 | + { | ||
| 336 | + set.Add(code.Trim()); | ||
| 337 | + } | ||
| 338 | + } | ||
| 339 | + | ||
| 340 | + foreach (var kv in byRole) | ||
| 341 | + { | ||
| 342 | + if (kv.Value.Count == 0) | ||
| 343 | + { | ||
| 344 | + continue; | ||
| 345 | + } | ||
| 346 | + | ||
| 347 | + result[kv.Key] = string.Join(", ", kv.Value.OrderBy(x => x, StringComparer.Ordinal)); | ||
| 348 | + } | ||
| 349 | + | ||
| 350 | + return result; | ||
| 351 | + } | ||
| 352 | + | ||
| 201 | /// <inheritdoc /> | 353 | /// <inheritdoc /> |
| 202 | [UnitOfWork] | 354 | [UnitOfWork] |
| 203 | public async Task DeleteAsync([FromBody] List<Guid> ids) | 355 | public async Task DeleteAsync([FromBody] List<Guid> ids) |
美国版/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 | @@ -106,6 +106,11 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | ||
| 106 | 106 | ||
| 107 | var role = await _dbContext.SqlSugarClient.Queryable<UserRoleEntity>().FirstAsync(x => x.UserId == id); | 107 | var role = await _dbContext.SqlSugarClient.Queryable<UserRoleEntity>().FirstAsync(x => x.UserId == id); |
| 108 | 108 | ||
| 109 | + var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( | ||
| 110 | + _dbContext.SqlSugarClient, locationIds); | ||
| 111 | + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( | ||
| 112 | + _dbContext.SqlSugarClient, locationIds); | ||
| 113 | + | ||
| 109 | return new TeamMemberGetOutputDto | 114 | return new TeamMemberGetOutputDto |
| 110 | { | 115 | { |
| 111 | Id = user.Id, | 116 | Id = user.Id, |
| @@ -115,6 +120,9 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | @@ -115,6 +120,9 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | ||
| 115 | Phone = user.Phone, | 120 | Phone = user.Phone, |
| 116 | State = user.State, | 121 | State = user.State, |
| 117 | RoleId = role?.RoleId, | 122 | RoleId = role?.RoleId, |
| 123 | + PartnerIds = partnerIds, | ||
| 124 | + RegionIds = regionIds, | ||
| 125 | + GroupIds = regionIds, | ||
| 118 | LocationIds = locationIds, | 126 | LocationIds = locationIds, |
| 119 | AssignedLocations = assigned | 127 | AssignedLocations = assigned |
| 120 | }; | 128 | }; |
| @@ -123,10 +131,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | @@ -123,10 +131,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | ||
| 123 | /// <inheritdoc /> | 131 | /// <inheritdoc /> |
| 124 | public async Task<TeamMemberGetOutputDto> CreateAsync(TeamMemberCreateInputVo input) | 132 | public async Task<TeamMemberGetOutputDto> CreateAsync(TeamMemberCreateInputVo input) |
| 125 | { | 133 | { |
| 126 | - if (input.LocationIds is null || input.LocationIds.Count == 0) | ||
| 127 | - { | ||
| 128 | - throw new UserFriendlyException("成员必须至少分配一个门店"); | ||
| 129 | - } | 134 | + var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input); |
| 130 | 135 | ||
| 131 | var user = new UserAggregateRoot(input.UserName.Trim(), input.Password, input.Phone, input.FullName.Trim()) | 136 | var user = new UserAggregateRoot(input.UserName.Trim(), input.Password, input.Phone, input.FullName.Trim()) |
| 132 | { | 137 | { |
| @@ -145,7 +150,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | @@ -145,7 +150,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | ||
| 145 | await _userManager.GiveUserSetRoleAsync(new List<Guid> { user.Id }, new List<Guid> { input.RoleId.Value }); | 150 | await _userManager.GiveUserSetRoleAsync(new List<Guid> { user.Id }, new List<Guid> { input.RoleId.Value }); |
| 146 | } | 151 | } |
| 147 | 152 | ||
| 148 | - await UpsertUserLocationsAsync(user.Id, input.LocationIds); | 153 | + await UpsertUserLocationsAsync(user.Id, mergedLocationIds); |
| 149 | 154 | ||
| 150 | return await GetAsync(user.Id); | 155 | return await GetAsync(user.Id); |
| 151 | } | 156 | } |
| @@ -153,10 +158,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | @@ -153,10 +158,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | ||
| 153 | /// <inheritdoc /> | 158 | /// <inheritdoc /> |
| 154 | public async Task<TeamMemberGetOutputDto> UpdateAsync(Guid id, TeamMemberUpdateInputVo input) | 159 | public async Task<TeamMemberGetOutputDto> UpdateAsync(Guid id, TeamMemberUpdateInputVo input) |
| 155 | { | 160 | { |
| 156 | - if (input.LocationIds is null || input.LocationIds.Count == 0) | ||
| 157 | - { | ||
| 158 | - throw new UserFriendlyException("成员必须至少分配一个门店"); | ||
| 159 | - } | 161 | + var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input); |
| 160 | 162 | ||
| 161 | var user = await _userRepository.GetByIdAsync(id); | 163 | var user = await _userRepository.GetByIdAsync(id); |
| 162 | if (user is null || user.IsDeleted) | 164 | if (user is null || user.IsDeleted) |
| @@ -187,7 +189,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | @@ -187,7 +189,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | ||
| 187 | await _userManager.GiveUserSetRoleAsync(new List<Guid> { id }, new List<Guid>()); | 189 | await _userManager.GiveUserSetRoleAsync(new List<Guid> { id }, new List<Guid>()); |
| 188 | } | 190 | } |
| 189 | 191 | ||
| 190 | - await UpsertUserLocationsAsync(id, input.LocationIds); | 192 | + await UpsertUserLocationsAsync(id, mergedLocationIds); |
| 191 | 193 | ||
| 192 | return await GetAsync(id); | 194 | return await GetAsync(id); |
| 193 | } | 195 | } |
| @@ -584,10 +586,13 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | @@ -584,10 +586,13 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | ||
| 584 | return null; | 586 | return null; |
| 585 | }).Where(x => x != null).Cast<TeamMemberAssignedLocationDto>().ToList()); | 587 | }).Where(x => x != null).Cast<TeamMemberAssignedLocationDto>().ToList()); |
| 586 | 588 | ||
| 589 | + var scopeIdsMap = await BuildTeamMemberScopeIdsMapAsync(assignedMap); | ||
| 590 | + | ||
| 587 | return users.Select(u => | 591 | return users.Select(u => |
| 588 | { | 592 | { |
| 589 | roleMap.TryGetValue(u.Id, out var role); | 593 | roleMap.TryGetValue(u.Id, out var role); |
| 590 | assignedMap.TryGetValue(u.Id.ToString(), out var assigned); | 594 | assignedMap.TryGetValue(u.Id.ToString(), out var assigned); |
| 595 | + scopeIdsMap.TryGetValue(u.Id.ToString(), out var scopeIds); | ||
| 591 | 596 | ||
| 592 | return new TeamMemberGetListOutputDto | 597 | return new TeamMemberGetListOutputDto |
| 593 | { | 598 | { |
| @@ -599,11 +604,109 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | @@ -599,11 +604,109 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService | ||
| 599 | State = u.State, | 604 | State = u.State, |
| 600 | RoleId = role?.Id, | 605 | RoleId = role?.Id, |
| 601 | RoleName = role?.RoleName, | 606 | RoleName = role?.RoleName, |
| 607 | + PartnerIds = scopeIds?.PartnerIds ?? new List<string>(), | ||
| 608 | + RegionIds = scopeIds?.RegionIds ?? new List<string>(), | ||
| 602 | AssignedLocations = assigned ?? new List<TeamMemberAssignedLocationDto>() | 609 | AssignedLocations = assigned ?? new List<TeamMemberAssignedLocationDto>() |
| 603 | }; | 610 | }; |
| 604 | }).ToList(); | 611 | }).ToList(); |
| 605 | } | 612 | } |
| 606 | 613 | ||
| 614 | + private async Task<Dictionary<string, TeamMemberScopeIds>> BuildTeamMemberScopeIdsMapAsync( | ||
| 615 | + Dictionary<string, List<TeamMemberAssignedLocationDto>> assignedMap) | ||
| 616 | + { | ||
| 617 | + var result = new Dictionary<string, TeamMemberScopeIds>(StringComparer.Ordinal); | ||
| 618 | + foreach (var (userId, assigned) in assignedMap) | ||
| 619 | + { | ||
| 620 | + var locationIds = assigned | ||
| 621 | + .Select(x => x.Id) | ||
| 622 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | ||
| 623 | + .Select(x => x.Trim()) | ||
| 624 | + .Distinct(StringComparer.Ordinal) | ||
| 625 | + .ToList(); | ||
| 626 | + if (locationIds.Count == 0) | ||
| 627 | + { | ||
| 628 | + result[userId] = new TeamMemberScopeIds(); | ||
| 629 | + continue; | ||
| 630 | + } | ||
| 631 | + | ||
| 632 | + var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( | ||
| 633 | + _dbContext.SqlSugarClient, locationIds); | ||
| 634 | + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( | ||
| 635 | + _dbContext.SqlSugarClient, locationIds); | ||
| 636 | + result[userId] = new TeamMemberScopeIds | ||
| 637 | + { | ||
| 638 | + PartnerIds = partnerIds, | ||
| 639 | + RegionIds = regionIds | ||
| 640 | + }; | ||
| 641 | + } | ||
| 642 | + | ||
| 643 | + return result; | ||
| 644 | + } | ||
| 645 | + | ||
| 646 | + private sealed class TeamMemberScopeIds | ||
| 647 | + { | ||
| 648 | + public List<string> PartnerIds { get; init; } = new(); | ||
| 649 | + public List<string> RegionIds { get; init; } = new(); | ||
| 650 | + } | ||
| 651 | + | ||
| 652 | + private Task<List<string>> ResolveTeamMemberLocationIdsForSaveAsync(TeamMemberUpdateInputVo input) => | ||
| 653 | + ResolveTeamMemberLocationIdsForSaveAsync(new TeamMemberCreateInputVo | ||
| 654 | + { | ||
| 655 | + PartnerId = input.PartnerId, | ||
| 656 | + PartnerIds = input.PartnerIds, | ||
| 657 | + RegionIds = input.RegionIds, | ||
| 658 | + GroupIds = input.GroupIds, | ||
| 659 | + LocationIds = input.LocationIds | ||
| 660 | + }); | ||
| 661 | + | ||
| 662 | + private async Task<List<string>> ResolveTeamMemberLocationIdsForSaveAsync(TeamMemberCreateInputVo input) | ||
| 663 | + { | ||
| 664 | + var partnerIds = NormalizePartnerIds(input); | ||
| 665 | + var regionIds = NormalizeRegionIds(input); | ||
| 666 | + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | ||
| 667 | + _dbContext.SqlSugarClient, partnerIds, regionIds, input.LocationIds); | ||
| 668 | + | ||
| 669 | + if (merged.Count == 0) | ||
| 670 | + { | ||
| 671 | + throw new UserFriendlyException("成员必须至少分配一个门店(公司/区域/门店至少选一项)"); | ||
| 672 | + } | ||
| 673 | + | ||
| 674 | + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); | ||
| 675 | + return merged; | ||
| 676 | + } | ||
| 677 | + | ||
| 678 | + private static List<string> NormalizePartnerIds(TeamMemberCreateInputVo input) | ||
| 679 | + { | ||
| 680 | + var merged = new HashSet<string>(StringComparer.Ordinal); | ||
| 681 | + if (!string.IsNullOrWhiteSpace(input.PartnerId)) | ||
| 682 | + { | ||
| 683 | + merged.Add(input.PartnerId.Trim()); | ||
| 684 | + } | ||
| 685 | + | ||
| 686 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.PartnerIds)) | ||
| 687 | + { | ||
| 688 | + merged.Add(id); | ||
| 689 | + } | ||
| 690 | + | ||
| 691 | + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); | ||
| 692 | + } | ||
| 693 | + | ||
| 694 | + private static List<string> NormalizeRegionIds(TeamMemberCreateInputVo input) | ||
| 695 | + { | ||
| 696 | + var merged = new HashSet<string>(StringComparer.Ordinal); | ||
| 697 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds)) | ||
| 698 | + { | ||
| 699 | + merged.Add(id); | ||
| 700 | + } | ||
| 701 | + | ||
| 702 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds)) | ||
| 703 | + { | ||
| 704 | + merged.Add(id); | ||
| 705 | + } | ||
| 706 | + | ||
| 707 | + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); | ||
| 708 | + } | ||
| 709 | + | ||
| 607 | private async Task UpsertUserLocationsAsync(Guid userId, List<string> locationIds) | 710 | private async Task UpsertUserLocationsAsync(Guid userId, List<string> locationIds) |
| 608 | { | 711 | { |
| 609 | var now = DateTime.Now; | 712 | var now = DateTime.Now; |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs
| @@ -15,6 +15,7 @@ using Lazy.Captcha.Core; | @@ -15,6 +15,7 @@ using Lazy.Captcha.Core; | ||
| 15 | using Mapster; | 15 | using Mapster; |
| 16 | using Microsoft.AspNetCore.Authorization; | 16 | using Microsoft.AspNetCore.Authorization; |
| 17 | using Microsoft.AspNetCore.Http; | 17 | using Microsoft.AspNetCore.Http; |
| 18 | +using Microsoft.AspNetCore.Mvc; | ||
| 18 | using Microsoft.Extensions.Options; | 19 | using Microsoft.Extensions.Options; |
| 19 | using Microsoft.IdentityModel.Tokens; | 20 | using Microsoft.IdentityModel.Tokens; |
| 20 | using SqlSugar; | 21 | using SqlSugar; |
| @@ -25,6 +26,8 @@ using Volo.Abp.Security.Claims; | @@ -25,6 +26,8 @@ using Volo.Abp.Security.Claims; | ||
| 25 | using Volo.Abp.Uow; | 26 | using Volo.Abp.Uow; |
| 26 | using Volo.Abp.Users; | 27 | using Volo.Abp.Users; |
| 27 | using Yi.Framework.Core.Helper; | 28 | using Yi.Framework.Core.Helper; |
| 29 | +using Yi.Framework.Rbac.Application.Contracts.Dtos.Account; | ||
| 30 | +using Yi.Framework.Rbac.Application.Contracts.IServices; | ||
| 28 | using Yi.Framework.Rbac.Domain.Entities; | 31 | using Yi.Framework.Rbac.Domain.Entities; |
| 29 | using Yi.Framework.Rbac.Domain.Managers; | 32 | using Yi.Framework.Rbac.Domain.Managers; |
| 30 | using Yi.Framework.Rbac.Domain.Shared.Consts; | 33 | using Yi.Framework.Rbac.Domain.Shared.Consts; |
| @@ -47,6 +50,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService | @@ -47,6 +50,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService | ||
| 47 | private readonly ICaptcha _captcha; | 50 | private readonly ICaptcha _captcha; |
| 48 | private readonly RbacOptions _rbacOptions; | 51 | private readonly RbacOptions _rbacOptions; |
| 49 | private readonly JwtOptions _jwtOptions; | 52 | private readonly JwtOptions _jwtOptions; |
| 53 | + private readonly IForgotPasswordByEmailService _forgotPasswordByEmailService; | ||
| 50 | 54 | ||
| 51 | public UsAppAuthAppService( | 55 | public UsAppAuthAppService( |
| 52 | IAccountManager accountManager, | 56 | IAccountManager accountManager, |
| @@ -55,7 +59,8 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService | @@ -55,7 +59,8 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService | ||
| 55 | IHttpContextAccessor httpContextAccessor, | 59 | IHttpContextAccessor httpContextAccessor, |
| 56 | ICaptcha captcha, | 60 | ICaptcha captcha, |
| 57 | IOptions<JwtOptions> jwtOptions, | 61 | IOptions<JwtOptions> jwtOptions, |
| 58 | - IOptions<RbacOptions> rbacOptions) | 62 | + IOptions<RbacOptions> rbacOptions, |
| 63 | + IForgotPasswordByEmailService forgotPasswordByEmailService) | ||
| 59 | { | 64 | { |
| 60 | _accountManager = accountManager; | 65 | _accountManager = accountManager; |
| 61 | _userRepository = userRepository; | 66 | _userRepository = userRepository; |
| @@ -64,6 +69,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService | @@ -64,6 +69,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService | ||
| 64 | _captcha = captcha; | 69 | _captcha = captcha; |
| 65 | _jwtOptions = jwtOptions.Value; | 70 | _jwtOptions = jwtOptions.Value; |
| 66 | _rbacOptions = rbacOptions.Value; | 71 | _rbacOptions = rbacOptions.Value; |
| 72 | + _forgotPasswordByEmailService = forgotPasswordByEmailService; | ||
| 67 | } | 73 | } |
| 68 | 74 | ||
| 69 | protected ILocalEventBus LocalEventBus => LazyServiceProvider.LazyGetRequiredService<ILocalEventBus>(); | 75 | protected ILocalEventBus LocalEventBus => LazyServiceProvider.LazyGetRequiredService<ILocalEventBus>(); |
| @@ -124,6 +130,19 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService | @@ -124,6 +130,19 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService | ||
| 124 | }; | 130 | }; |
| 125 | } | 131 | } |
| 126 | 132 | ||
| 133 | + /// <inheritdoc /> | ||
| 134 | + [AllowAnonymous] | ||
| 135 | + [HttpPost("us-app-auth/forgot-password/email/send-code")] | ||
| 136 | + public virtual Task PostSendForgotPasswordCodeByEmailAsync(EmailCaptchaImageDto input) => | ||
| 137 | + _forgotPasswordByEmailService.SendForgotPasswordCodeAsync(input); | ||
| 138 | + | ||
| 139 | + /// <inheritdoc /> | ||
| 140 | + [AllowAnonymous] | ||
| 141 | + [UnitOfWork] | ||
| 142 | + [HttpPost("us-app-auth/forgot-password/email/reset")] | ||
| 143 | + public virtual Task<string> PostResetPasswordByEmailAsync(RetrievePasswordByEmailDto input) => | ||
| 144 | + _forgotPasswordByEmailService.ResetPasswordByEmailAsync(input); | ||
| 145 | + | ||
| 127 | /// <summary> | 146 | /// <summary> |
| 128 | /// 获取当前登录用户已绑定的门店(切换门店时可重新拉取) | 147 | /// 获取当前登录用户已绑定的门店(切换门店时可重新拉取) |
| 129 | /// </summary> | 148 | /// </summary> |
| @@ -147,7 +166,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService | @@ -147,7 +166,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService | ||
| 147 | /// 店长:在同店绑定用户中,取 <c>Role.RoleCode</c> 或 <c>Role.RoleName</c>(忽略大小写)包含 <c>manager</c> 的第一条; | 166 | /// 店长:在同店绑定用户中,取 <c>Role.RoleCode</c> 或 <c>Role.RoleName</c>(忽略大小写)包含 <c>manager</c> 的第一条; |
| 148 | /// 若无匹配则店长姓名与电话均为「无」。 | 167 | /// 若无匹配则店长姓名与电话均为「无」。 |
| 149 | /// | 168 | /// |
| 150 | - /// <c>OperatingHours</c>:当前 <c>location</c> 表无营业时间字段,固定返回「无」。 | 169 | + /// <c>OperatingHours</c>:读取 <c>location.OperatingHours</c>;为空时返回「无」。 |
| 151 | /// </remarks> | 170 | /// </remarks> |
| 152 | /// <param name="locationId">门店主键(Guid 字符串)</param> | 171 | /// <param name="locationId">门店主键(Guid 字符串)</param> |
| 153 | /// <returns>与原型一致的展示字段</returns> | 172 | /// <returns>与原型一致的展示字段</returns> |
| @@ -193,7 +212,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService | @@ -193,7 +212,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService | ||
| 193 | LocationName = string.IsNullOrWhiteSpace(loc.LocationName) ? "无" : loc.LocationName.Trim(), | 212 | LocationName = string.IsNullOrWhiteSpace(loc.LocationName) ? "无" : loc.LocationName.Trim(), |
| 194 | FullAddress = BuildFullAddress(loc), | 213 | FullAddress = BuildFullAddress(loc), |
| 195 | StorePhone = FormatStorePhoneDisplay(loc.Phone), | 214 | StorePhone = FormatStorePhoneDisplay(loc.Phone), |
| 196 | - OperatingHours = "无", | 215 | + OperatingHours = string.IsNullOrWhiteSpace(loc.OperatingHours) ? "无" : loc.OperatingHours.Trim(), |
| 197 | ManagerName = mgrName, | 216 | ManagerName = mgrName, |
| 198 | ManagerPhone = mgrPhone | 217 | ManagerPhone = mgrPhone |
| 199 | }; | 218 | }; |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Domain/Entities/LocationAggregateRoot.cs
| @@ -130,5 +130,11 @@ public class LocationAggregateRoot : AggregateRoot<Guid>, ISoftDelete, IAuditedO | @@ -130,5 +130,11 @@ public class LocationAggregateRoot : AggregateRoot<Guid>, ISoftDelete, IAuditedO | ||
| 130 | /// Longitude | 130 | /// Longitude |
| 131 | /// </summary> | 131 | /// </summary> |
| 132 | public decimal? Longitude { get; set; } | 132 | public decimal? Longitude { get; set; } |
| 133 | + | ||
| 134 | + /// <summary> | ||
| 135 | + /// 经营时间(自由文本,如 Mon–Fri 9:00 AM – 6:00 PM) | ||
| 136 | + /// </summary> | ||
| 137 | + [SugarColumn(ColumnName = "OperatingHours")] | ||
| 138 | + public string? OperatingHours { get; set; } | ||
| 133 | } | 139 | } |
| 134 | 140 |
美国版/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` ( | @@ -11,6 +11,11 @@ CREATE TABLE IF NOT EXISTS `fl_partner` ( | ||
| 11 | `PartnerName` varchar(256) NOT NULL COMMENT '合作伙伴名称', | 11 | `PartnerName` varchar(256) NOT NULL COMMENT '合作伙伴名称', |
| 12 | `ContactEmail` varchar(256) DEFAULT NULL COMMENT '联系邮箱', | 12 | `ContactEmail` varchar(256) DEFAULT NULL COMMENT '联系邮箱', |
| 13 | `PhoneNumber` varchar(64) DEFAULT NULL COMMENT '电话', | 13 | `PhoneNumber` varchar(64) DEFAULT NULL COMMENT '电话', |
| 14 | + `Street` varchar(512) DEFAULT NULL COMMENT '街道地址', | ||
| 15 | + `City` varchar(128) DEFAULT NULL COMMENT '城市', | ||
| 16 | + `StateCode` varchar(32) DEFAULT NULL COMMENT '州/省代码(如 NY)', | ||
| 17 | + `Country` varchar(128) DEFAULT NULL COMMENT '国家', | ||
| 18 | + `ZipCode` varchar(32) DEFAULT NULL COMMENT '邮编', | ||
| 14 | `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', | 19 | `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用', |
| 15 | PRIMARY KEY (`Id`), | 20 | PRIMARY KEY (`Id`), |
| 16 | KEY `IX_fl_partner_IsDeleted` (`IsDeleted`), | 21 | KEY `IX_fl_partner_IsDeleted` (`IsDeleted`), |
美国版/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 | @@ -14,5 +14,10 @@ namespace Yi.Framework.Rbac.Application.Contracts.Dtos.Role | ||
| 14 | public bool State { get; set; } | 14 | public bool State { get; set; } |
| 15 | 15 | ||
| 16 | public int OrderNum { get; set; } | 16 | public int OrderNum { get; set; } |
| 17 | + | ||
| 18 | + /// <summary> | ||
| 19 | + /// 访问权限:该角色在已绑定菜单上的 PermissionCode 汇总(去重、逗号+空格拼接;无则为空串) | ||
| 20 | + /// </summary> | ||
| 21 | + public string AccessPermissions { get; set; } = string.Empty; | ||
| 17 | } | 22 | } |
| 18 | } | 23 | } |
美国版/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 | @@ -14,5 +14,10 @@ namespace Yi.Framework.Rbac.Application.Contracts.Dtos.Role | ||
| 14 | public bool State { get; set; } | 14 | public bool State { get; set; } |
| 15 | 15 | ||
| 16 | public int OrderNum { get; set; } | 16 | public int OrderNum { get; set; } |
| 17 | + | ||
| 18 | + /// <summary> | ||
| 19 | + /// 访问权限:该角色在已绑定菜单上的 PermissionCode 汇总(去重、逗号+空格拼接;无则为空串) | ||
| 20 | + /// </summary> | ||
| 21 | + public string AccessPermissions { get; set; } = string.Empty; | ||
| 17 | } | 22 | } |
| 18 | } | 23 | } |
美国版/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 | @@ -37,6 +37,7 @@ namespace Yi.Framework.Rbac.Application.Services | ||
| 37 | private readonly IGuidGenerator _guidGenerator; | 37 | private readonly IGuidGenerator _guidGenerator; |
| 38 | private readonly RbacOptions _rbacOptions; | 38 | private readonly RbacOptions _rbacOptions; |
| 39 | private readonly IAliyunManger _aliyunManger; | 39 | private readonly IAliyunManger _aliyunManger; |
| 40 | + private readonly IForgotPasswordByEmailService _forgotPasswordByEmailService; | ||
| 40 | private IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> _userCache; | 41 | private IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> _userCache; |
| 41 | private UserManager _userManager; | 42 | private UserManager _userManager; |
| 42 | private IHttpContextAccessor _httpContextAccessor; | 43 | private IHttpContextAccessor _httpContextAccessor; |
| @@ -51,6 +52,7 @@ namespace Yi.Framework.Rbac.Application.Services | @@ -51,6 +52,7 @@ namespace Yi.Framework.Rbac.Application.Services | ||
| 51 | IGuidGenerator guidGenerator, | 52 | IGuidGenerator guidGenerator, |
| 52 | IOptions<RbacOptions> options, | 53 | IOptions<RbacOptions> options, |
| 53 | IAliyunManger aliyunManger, | 54 | IAliyunManger aliyunManger, |
| 55 | + IForgotPasswordByEmailService forgotPasswordByEmailService, | ||
| 54 | UserManager userManager, IHttpContextAccessor httpContextAccessor) | 56 | UserManager userManager, IHttpContextAccessor httpContextAccessor) |
| 55 | { | 57 | { |
| 56 | _userRepository = userRepository; | 58 | _userRepository = userRepository; |
| @@ -62,6 +64,7 @@ namespace Yi.Framework.Rbac.Application.Services | @@ -62,6 +64,7 @@ namespace Yi.Framework.Rbac.Application.Services | ||
| 62 | _guidGenerator = guidGenerator; | 64 | _guidGenerator = guidGenerator; |
| 63 | _rbacOptions = options.Value; | 65 | _rbacOptions = options.Value; |
| 64 | _aliyunManger = aliyunManger; | 66 | _aliyunManger = aliyunManger; |
| 67 | + _forgotPasswordByEmailService = forgotPasswordByEmailService; | ||
| 65 | _userCache = userCache; | 68 | _userCache = userCache; |
| 66 | _userManager = userManager; | 69 | _userManager = userManager; |
| 67 | _httpContextAccessor = httpContextAccessor; | 70 | _httpContextAccessor = httpContextAccessor; |
| @@ -328,6 +331,58 @@ namespace Yi.Framework.Rbac.Application.Services | @@ -328,6 +331,58 @@ namespace Yi.Framework.Rbac.Application.Services | ||
| 328 | return entity.UserName; | 331 | return entity.UserName; |
| 329 | } | 332 | } |
| 330 | 333 | ||
| 334 | + /// <summary> | ||
| 335 | + /// Send a forgot-password verification code to the account email (platform). | ||
| 336 | + /// </summary> | ||
| 337 | + /// <remarks> | ||
| 338 | + /// When <c>RbacOptions.EnableCaptcha</c> is true, pass <c>Uuid</c> and <c>Code</c> from <c>GetCaptchaImageAsync</c>. | ||
| 339 | + /// If the email is not registered, the API still returns 200 without sending mail (anti-enumeration). | ||
| 340 | + /// | ||
| 341 | + /// Example request: | ||
| 342 | + /// ```json | ||
| 343 | + /// { | ||
| 344 | + /// "email": "user@example.com", | ||
| 345 | + /// "uuid": "optional-captcha-uuid", | ||
| 346 | + /// "code": "optional-captcha-text" | ||
| 347 | + /// } | ||
| 348 | + /// ``` | ||
| 349 | + /// </remarks> | ||
| 350 | + /// <param name="input">Email and optional image captcha fields.</param> | ||
| 351 | + /// <returns>No payload when accepted.</returns> | ||
| 352 | + /// <response code="200">Accepted (email may or may not exist).</response> | ||
| 353 | + /// <response code="400">Invalid input, captcha, rate limit, or SMTP not configured.</response> | ||
| 354 | + /// <response code="500">Server error</response> | ||
| 355 | + [AllowAnonymous] | ||
| 356 | + [HttpPost("account/forgot-password/email/send-code")] | ||
| 357 | + public virtual Task PostSendForgotPasswordCodeByEmailAsync(EmailCaptchaImageDto input) => | ||
| 358 | + _forgotPasswordByEmailService.SendForgotPasswordCodeAsync(input); | ||
| 359 | + | ||
| 360 | + /// <summary> | ||
| 361 | + /// Reset password using the email verification code (platform). | ||
| 362 | + /// </summary> | ||
| 363 | + /// <remarks> | ||
| 364 | + /// New password rules: at least 8 characters; upper and lower case; digit; special character. | ||
| 365 | + /// | ||
| 366 | + /// Example request: | ||
| 367 | + /// ```json | ||
| 368 | + /// { | ||
| 369 | + /// "email": "user@example.com", | ||
| 370 | + /// "code": "ABCDEF", | ||
| 371 | + /// "password": "NewPassw0rd!" | ||
| 372 | + /// } | ||
| 373 | + /// ``` | ||
| 374 | + /// </remarks> | ||
| 375 | + /// <param name="input">Email, OTP, and new password.</param> | ||
| 376 | + /// <returns>Matched account user name.</returns> | ||
| 377 | + /// <response code="200">Password updated.</response> | ||
| 378 | + /// <response code="400">Invalid OTP, weak password, or account not found.</response> | ||
| 379 | + /// <response code="500">Server error</response> | ||
| 380 | + [AllowAnonymous] | ||
| 381 | + [UnitOfWork] | ||
| 382 | + [HttpPost("account/forgot-password/email/reset")] | ||
| 383 | + public virtual Task<string> PostResetPasswordByEmailAsync(RetrievePasswordByEmailDto input) => | ||
| 384 | + _forgotPasswordByEmailService.ResetPasswordByEmailAsync(input); | ||
| 385 | + | ||
| 331 | 386 | ||
| 332 | /// <summary> | 387 | /// <summary> |
| 333 | /// 注册,需要验证码通过 | 388 | /// 注册,需要验证码通过 |
美国版/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; | @@ -19,6 +19,7 @@ using Yi.Framework.Rbac.Domain.Managers; | ||
| 19 | using Yi.Framework.Rbac.Domain.Shared.Consts; | 19 | using Yi.Framework.Rbac.Domain.Shared.Consts; |
| 20 | using Yi.Framework.Rbac.Domain.Shared.Enums; | 20 | using Yi.Framework.Rbac.Domain.Shared.Enums; |
| 21 | using Yi.Framework.SqlSugarCore.Abstractions; | 21 | using Yi.Framework.SqlSugarCore.Abstractions; |
| 22 | +using System.Linq; | ||
| 22 | 23 | ||
| 23 | namespace Yi.Framework.Rbac.Application.Services.System | 24 | namespace Yi.Framework.Rbac.Application.Services.System |
| 24 | { | 25 | { |
| @@ -75,7 +76,17 @@ namespace Yi.Framework.Rbac.Application.Services.System | @@ -75,7 +76,17 @@ namespace Yi.Framework.Rbac.Application.Services.System | ||
| 75 | .WhereIF(!string.IsNullOrEmpty(input.RoleName), x => x.RoleName.Contains(input.RoleName!)) | 76 | .WhereIF(!string.IsNullOrEmpty(input.RoleName), x => x.RoleName.Contains(input.RoleName!)) |
| 76 | .WhereIF(input.State is not null, x => x.State == input.State) | 77 | .WhereIF(input.State is not null, x => x.State == input.State) |
| 77 | .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); | 78 | .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); |
| 78 | - return new PagedResultDto<RoleGetListOutputDto>(total, await MapToGetListOutputDtosAsync(entities)); | 79 | + var list = await MapToGetListOutputDtosAsync(entities); |
| 80 | + await FillAccessPermissionsForRoleListAsync(list); | ||
| 81 | + return new PagedResultDto<RoleGetListOutputDto>(total, list); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + /// <inheritdoc /> | ||
| 85 | + public override async Task<RoleGetOutputDto> GetAsync(Guid id) | ||
| 86 | + { | ||
| 87 | + var dto = await base.GetAsync(id); | ||
| 88 | + await FillAccessPermissionsForRoleGetAsync(dto); | ||
| 89 | + return dto; | ||
| 79 | } | 90 | } |
| 80 | 91 | ||
| 81 | /// <inheritdoc /> | 92 | /// <inheritdoc /> |
| @@ -165,9 +176,7 @@ namespace Yi.Framework.Rbac.Application.Services.System | @@ -165,9 +176,7 @@ namespace Yi.Framework.Rbac.Application.Services.System | ||
| 165 | 176 | ||
| 166 | var entity = await MapToEntityAsync(input); | 177 | var entity = await MapToEntityAsync(input); |
| 167 | await _repository.InsertAsync(entity); | 178 | await _repository.InsertAsync(entity); |
| 168 | - var outputDto = await MapToGetOutputDtoAsync(entity); | ||
| 169 | - | ||
| 170 | - return outputDto; | 179 | + return await MapToGetOutputDtoWithPermissionsAsync(entity); |
| 171 | } | 180 | } |
| 172 | 181 | ||
| 173 | /// <summary> | 182 | /// <summary> |
| @@ -191,8 +200,7 @@ namespace Yi.Framework.Rbac.Application.Services.System | @@ -191,8 +200,7 @@ namespace Yi.Framework.Rbac.Application.Services.System | ||
| 191 | 200 | ||
| 192 | await _roleManager.GiveRoleSetMenuAsync(new List<Guid> { id }, input.MenuIds); | 201 | await _roleManager.GiveRoleSetMenuAsync(new List<Guid> { id }, input.MenuIds); |
| 193 | 202 | ||
| 194 | - var dto = await MapToGetOutputDtoAsync(entity); | ||
| 195 | - return dto; | 203 | + return await MapToGetOutputDtoWithPermissionsAsync(entity); |
| 196 | } | 204 | } |
| 197 | 205 | ||
| 198 | 206 | ||
| @@ -213,7 +221,7 @@ namespace Yi.Framework.Rbac.Application.Services.System | @@ -213,7 +221,7 @@ namespace Yi.Framework.Rbac.Application.Services.System | ||
| 213 | 221 | ||
| 214 | entity.State = state; | 222 | entity.State = state; |
| 215 | await _repository.UpdateAsync(entity); | 223 | await _repository.UpdateAsync(entity); |
| 216 | - return await MapToGetOutputDtoAsync(entity); | 224 | + return await MapToGetOutputDtoWithPermissionsAsync(entity); |
| 217 | } | 225 | } |
| 218 | 226 | ||
| 219 | 227 | ||
| @@ -337,5 +345,92 @@ namespace Yi.Framework.Rbac.Application.Services.System | @@ -337,5 +345,92 @@ namespace Yi.Framework.Rbac.Application.Services.System | ||
| 337 | depts = deptList.DeptTreeBuild() | 345 | depts = deptList.DeptTreeBuild() |
| 338 | }); | 346 | }); |
| 339 | } | 347 | } |
| 348 | + | ||
| 349 | + private async Task<RoleGetOutputDto> MapToGetOutputDtoWithPermissionsAsync(RoleAggregateRoot entity) | ||
| 350 | + { | ||
| 351 | + var dto = await MapToGetOutputDtoAsync(entity); | ||
| 352 | + await FillAccessPermissionsForRoleGetAsync(dto); | ||
| 353 | + return dto; | ||
| 354 | + } | ||
| 355 | + | ||
| 356 | + private async Task FillAccessPermissionsForRoleListAsync(List<RoleGetListOutputDto> items) | ||
| 357 | + { | ||
| 358 | + if (items.Count == 0) | ||
| 359 | + { | ||
| 360 | + return; | ||
| 361 | + } | ||
| 362 | + | ||
| 363 | + var map = await GetAccessPermissionsByRoleIdsAsync(items.Select(x => x.Id).ToList()); | ||
| 364 | + foreach (var item in items) | ||
| 365 | + { | ||
| 366 | + item.AccessPermissions = map.GetValueOrDefault(item.Id, string.Empty); | ||
| 367 | + } | ||
| 368 | + } | ||
| 369 | + | ||
| 370 | + private async Task FillAccessPermissionsForRoleGetAsync(RoleGetOutputDto dto) | ||
| 371 | + { | ||
| 372 | + var map = await GetAccessPermissionsByRoleIdsAsync(new List<Guid> { dto.Id }); | ||
| 373 | + dto.AccessPermissions = map.GetValueOrDefault(dto.Id, string.Empty); | ||
| 374 | + } | ||
| 375 | + | ||
| 376 | + /// <summary> | ||
| 377 | + /// 按角色汇总已绑定菜单上的 PermissionCode(去重、英文逗号+空格拼接) | ||
| 378 | + /// </summary> | ||
| 379 | + private async Task<Dictionary<Guid, string>> GetAccessPermissionsByRoleIdsAsync(List<Guid> roleIds) | ||
| 380 | + { | ||
| 381 | + var result = roleIds.Distinct().ToDictionary(id => id, _ => string.Empty); | ||
| 382 | + if (result.Count == 0) | ||
| 383 | + { | ||
| 384 | + return result; | ||
| 385 | + } | ||
| 386 | + | ||
| 387 | + var distinctRoleIds = result.Keys.ToList(); | ||
| 388 | + var links = await _menuRepository._Db.Queryable<RoleMenuEntity>() | ||
| 389 | + .Where(rm => distinctRoleIds.Contains(rm.RoleId)) | ||
| 390 | + .Select(rm => new { rm.RoleId, rm.MenuId }) | ||
| 391 | + .ToListAsync(); | ||
| 392 | + if (links.Count == 0) | ||
| 393 | + { | ||
| 394 | + return result; | ||
| 395 | + } | ||
| 396 | + | ||
| 397 | + var menuIds = links.Select(x => x.MenuId).Distinct().ToList(); | ||
| 398 | + var menus = await _menuRepository._DbQueryable | ||
| 399 | + .Where(m => menuIds.Contains(m.Id) && m.IsDeleted == false) | ||
| 400 | + .Select(m => new { m.Id, m.PermissionCode }) | ||
| 401 | + .ToListAsync(); | ||
| 402 | + var permByMenuId = menus.ToDictionary(x => x.Id, x => x.PermissionCode); | ||
| 403 | + | ||
| 404 | + var byRole = new Dictionary<Guid, HashSet<string>>(); | ||
| 405 | + foreach (var rid in distinctRoleIds) | ||
| 406 | + { | ||
| 407 | + byRole[rid] = new HashSet<string>(StringComparer.Ordinal); | ||
| 408 | + } | ||
| 409 | + | ||
| 410 | + foreach (var link in links) | ||
| 411 | + { | ||
| 412 | + if (!permByMenuId.TryGetValue(link.MenuId, out var code) || string.IsNullOrWhiteSpace(code)) | ||
| 413 | + { | ||
| 414 | + continue; | ||
| 415 | + } | ||
| 416 | + | ||
| 417 | + if (byRole.TryGetValue(link.RoleId, out var set)) | ||
| 418 | + { | ||
| 419 | + set.Add(code.Trim()); | ||
| 420 | + } | ||
| 421 | + } | ||
| 422 | + | ||
| 423 | + foreach (var kv in byRole) | ||
| 424 | + { | ||
| 425 | + if (kv.Value.Count == 0) | ||
| 426 | + { | ||
| 427 | + continue; | ||
| 428 | + } | ||
| 429 | + | ||
| 430 | + result[kv.Key] = string.Join(", ", kv.Value.OrderBy(x => x, StringComparer.Ordinal)); | ||
| 431 | + } | ||
| 432 | + | ||
| 433 | + return result; | ||
| 434 | + } | ||
| 340 | } | 435 | } |
| 341 | } | 436 | } |
| 342 | \ No newline at end of file | 437 | \ No newline at end of file |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Yi.Framework.Rbac.Application.csproj
| @@ -8,6 +8,7 @@ | @@ -8,6 +8,7 @@ | ||
| 8 | 8 | ||
| 9 | 9 | ||
| 10 | <ItemGroup> | 10 | <ItemGroup> |
| 11 | + <PackageReference Include="MailKit" Version="4.8.0" /> | ||
| 11 | <PackageReference Include="Lazy.Captcha.Core" Version="2.0.7" /> | 12 | <PackageReference Include="Lazy.Captcha.Core" Version="2.0.7" /> |
| 12 | <PackageReference Include="QuestPDF" Version="2024.12.2" /> | 13 | <PackageReference Include="QuestPDF" Version="2024.12.2" /> |
| 13 | <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.7" /> | 14 | <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.7" /> |
美国版/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 | @@ -18,6 +18,8 @@ namespace Yi.Framework.Rbac.Domain.Shared | ||
| 18 | Configure<JwtOptions>(configuration.GetSection(nameof(JwtOptions))); | 18 | Configure<JwtOptions>(configuration.GetSection(nameof(JwtOptions))); |
| 19 | Configure<RefreshJwtOptions>(configuration.GetSection(nameof(RefreshJwtOptions))); | 19 | Configure<RefreshJwtOptions>(configuration.GetSection(nameof(RefreshJwtOptions))); |
| 20 | Configure<RbacOptions>(configuration.GetSection(nameof(RbacOptions))); | 20 | Configure<RbacOptions>(configuration.GetSection(nameof(RbacOptions))); |
| 21 | + Configure<ForgotPasswordEmailOptions>( | ||
| 22 | + configuration.GetSection(ForgotPasswordEmailOptions.SectionName)); | ||
| 21 | } | 23 | } |
| 22 | } | 24 | } |
| 23 | } | 25 | } |
| 24 | \ No newline at end of file | 26 | \ No newline at end of file |
美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/Yi.Abp.Web.csproj
| @@ -8,6 +8,8 @@ | @@ -8,6 +8,8 @@ | ||
| 8 | 8 | ||
| 9 | 9 | ||
| 10 | <ItemGroup> | 10 | <ItemGroup> |
| 11 | + <!-- 与 FoodLabeling.Application 中 Excel 导出一致;显式引用避免部分发布/拷贝场景下漏带 ClosedXML.dll --> | ||
| 12 | + <PackageReference Include="ClosedXML" Version="0.102.3" /> | ||
| 11 | <PackageReference Include="Hangfire.MemoryStorage" Version="1.8.1.1" /> | 13 | <PackageReference Include="Hangfire.MemoryStorage" Version="1.8.1.1" /> |
| 12 | <PackageReference Include="Hangfire.Redis.StackExchange" Version="1.9.4" /> | 14 | <PackageReference Include="Hangfire.Redis.StackExchange" Version="1.9.4" /> |
| 13 | <!-- 解决 docker 构建冲突问题,冲突版本信息如下 --> | 15 | <!-- 解决 docker 构建冲突问题,冲突版本信息如下 --> |
| @@ -67,4 +69,11 @@ | @@ -67,4 +69,11 @@ | ||
| 67 | </None> | 69 | </None> |
| 68 | </ItemGroup> | 70 | </ItemGroup> |
| 69 | 71 | ||
| 72 | + <!-- 避免「只拷业务 dll 覆盖线上」导致缺 ClosedXML;完整 dotnet publish 应始终包含该文件 --> | ||
| 73 | + <Target Name="VerifyClosedXmlInPublishOutput" AfterTargets="Publish"> | ||
| 74 | + <Error | ||
| 75 | + Condition="!Exists('$(PublishDir)ClosedXML.dll')" | ||
| 76 | + Text="发布目录缺少 ClosedXML.dll。请使用 dotnet publish Yi.Abp.Web 的完整输出(含全部依赖 dll 与 Yi.Abp.Web.deps.json)部署到服务器,勿仅用 bin 内部分文件做覆盖。" /> | ||
| 77 | + </Target> | ||
| 78 | + | ||
| 70 | </Project> | 79 | </Project> |
美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json
| @@ -18,10 +18,10 @@ | @@ -18,10 +18,10 @@ | ||
| 18 | "Microsoft.AspNetCore": "Warning" | 18 | "Microsoft.AspNetCore": "Warning" |
| 19 | } | 19 | } |
| 20 | }, | 20 | }, |
| 21 | - //应用启动 | 21 | + //应用启动:SelfUrl 供 Program.cs UseUrls 绑定;用 0.0.0.0 避免写成固定局域网 IP 在本机无该网卡时启动失败(WinError 10049) |
| 22 | "App": { | 22 | "App": { |
| 23 | "SelfUrl": "http://localhost:19001", | 23 | "SelfUrl": "http://localhost:19001", |
| 24 | - "CorsOrigins": "http://localhost:19001;http://localhost:18000;http://localhost:5666" | 24 | + "CorsOrigins": "http://localhost:19001;http://localhost:18000;http://localhost:5666;http://localhost:3000" |
| 25 | }, | 25 | }, |
| 26 | //配置 | 26 | //配置 |
| 27 | "Settings": { | 27 | "Settings": { |
| @@ -104,11 +104,39 @@ | @@ -104,11 +104,39 @@ | ||
| 104 | //开启定时数据库备份 | 104 | //开启定时数据库备份 |
| 105 | "EnableDataBaseBackup": false | 105 | "EnableDataBaseBackup": false |
| 106 | }, | 106 | }, |
| 107 | + | ||
| 108 | + // Forgot password: Microsoft 365 may return 535 until SMTP AUTH is enabled for the mailbox | ||
| 109 | + // (Exchange admin) and credentials are correct; use an app password if MFA is on. | ||
| 110 | + "ForgotPasswordEmail": { | ||
| 111 | + "IsEnabled": true, | ||
| 112 | + "SmtpHost": "smtp.office365.com", | ||
| 113 | + "SmtpPort": 587, | ||
| 114 | + "SecureSocketPreset": "StartTls", | ||
| 115 | + "UserName": "Sandi.ma@3ffoodsafety.com", | ||
| 116 | + "Password": "", | ||
| 117 | + "FromAddress": "Sandi.ma@3ffoodsafety.com", | ||
| 118 | + "FromDisplayName": "Food Labeling", | ||
| 119 | + "CodeLength": 6, | ||
| 120 | + "CacheExpirationMinutes": 10 | ||
| 121 | + }, | ||
| 107 | 122 | ||
| 108 | //语义内核 | 123 | //语义内核 |
| 109 | "SemanticKernel": { | 124 | "SemanticKernel": { |
| 110 | "ModelIds": ["gpt-4o"], | 125 | "ModelIds": ["gpt-4o"], |
| 111 | "Endpoint": "https://xxx.com/v1", | 126 | "Endpoint": "https://xxx.com/v1", |
| 112 | "ApiKey": "sk-xxxxxx" | 127 | "ApiKey": "sk-xxxxxx" |
| 128 | + }, | ||
| 129 | + | ||
| 130 | + "FoodLabeling": { | ||
| 131 | + "BatchImport": { | ||
| 132 | + "TemplateDirectory": "/www/wwwroot/FoodLabelingManagementUs/batchImportOfFiles", | ||
| 133 | + "LocationTemplateFileName": "Location-Manager-批量导入模板.xlsx", | ||
| 134 | + "TeamMemberTemplateFileName": "Team-Member-批量导入模板.xlsx", | ||
| 135 | + "ProductTemplateFileName": "Product-Manager-批量导入模板.xlsx", | ||
| 136 | + "TeamMemberImportDefaultPassword": "ChangeMe123!", | ||
| 137 | + "MaxImportRows": 5000, | ||
| 138 | + "MaxUploadBytes": 10485760, | ||
| 139 | + "MaxBulkUpdateItems": 500 | ||
| 140 | + } | ||
| 113 | } | 141 | } |
| 114 | } | 142 | } |
美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx
| @@ -1819,7 +1819,7 @@ function RoleDialog({ | @@ -1819,7 +1819,7 @@ function RoleDialog({ | ||
| 1819 | } | 1819 | } |
| 1820 | setSubmitting(true); | 1820 | setSubmitting(true); |
| 1821 | try { | 1821 | try { |
| 1822 | - const payload = { | 1822 | + const payload: RbacRoleUpsertInput = { |
| 1823 | roleName: roleName.trim(), | 1823 | roleName: roleName.trim(), |
| 1824 | roleCode: roleCode.trim(), | 1824 | roleCode: roleCode.trim(), |
| 1825 | remark: remark.trim() ? remark.trim() : null, | 1825 | remark: remark.trim() ? remark.trim() : null, |
美国版/Food Labeling Management Platform/src/types/authSession.ts
| @@ -27,5 +27,11 @@ export type CurrentUserMenuPermissionsOutputDto = { | @@ -27,5 +27,11 @@ export type CurrentUserMenuPermissionsOutputDto = { | ||
| 27 | roleCodes: string[]; | 27 | roleCodes: string[]; |
| 28 | permissionCodes: string[]; | 28 | permissionCodes: string[]; |
| 29 | menus: CurrentUserMenuNodeDto[]; | 29 | menus: CurrentUserMenuNodeDto[]; |
| 30 | + /** User.LastModificationTime(ISO 字符串或 null) */ | ||
| 31 | + lastUpdated?: string | null; | ||
| 32 | + /** 角色展示名,多角色英文逗号拼接 */ | ||
| 33 | + role?: string; | ||
| 34 | + /** 全名:姓名 > 昵称 > 用户名 */ | ||
| 35 | + fullName?: string; | ||
| 30 | }; | 36 | }; |
| 31 | 37 |
项目相关文档/批量导入导出接口说明.md
| @@ -224,12 +224,14 @@ | @@ -224,12 +224,14 @@ | ||
| 224 | 224 | ||
| 225 | **列表筛选字段**(导出与列表对齐):`Keyword`、`State`、`Sorting` — 与产品分页列表一致(`Keyword` 匹配产品编码、名称、分类名称)。 | 225 | **列表筛选字段**(导出与列表对齐):`Keyword`、`State`、`Sorting` — 与产品分页列表一致(`Keyword` 匹配产品编码、名称、分类名称)。 |
| 226 | 226 | ||
| 227 | +**路由说明**:单条 **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。 | ||
| 228 | + | ||
| 227 | ### 3.1 下载批量导入模板 | 229 | ### 3.1 下载批量导入模板 |
| 228 | 230 | ||
| 229 | | 项目 | 说明 | | 231 | | 项目 | 说明 | |
| 230 | |------|------| | 232 | |------|------| |
| 231 | | 方法 | `DownloadProductImportTemplateAsync` | | 233 | | 方法 | `DownloadProductImportTemplateAsync` | |
| 232 | -| HTTP | `GET` | | 234 | +| HTTP | `GET`(已标 `[HttpGet]`,与约定 `Download*` 可能判为 `POST` 的情况区分) | |
| 233 | | 常见路径 | `/api/app/product/download-product-import-template` | | 235 | | 常见路径 | `/api/app/product/download-product-import-template` | |
| 234 | | 作用 | 从 `TemplateDirectory` 读取 `ProductTemplateFileName` 指向的 xlsx 并下载(工作表名一般为 `Products`) | | 236 | | 作用 | 从 `TemplateDirectory` 读取 `ProductTemplateFileName` 指向的 xlsx 并下载(工作表名一般为 `Products`) | |
| 235 | 237 | ||
| @@ -238,14 +240,17 @@ | @@ -238,14 +240,17 @@ | ||
| 238 | | 项目 | 说明 | | 240 | | 项目 | 说明 | |
| 239 | |------|------| | 241 | |------|------| |
| 240 | | 方法 | `ExportProductsExcelAsync` | | 242 | | 方法 | `ExportProductsExcelAsync` | |
| 241 | -| HTTP | `GET` | | 243 | +| HTTP | `GET`(应用服务方法上已标 `[HttpGet]`;ABP 对 `Export*` 默认易判成 `POST`,用 GET 否则会 **405**) | |
| 242 | | 常见路径 | `/api/app/product/export-products-excel` | | 244 | | 常见路径 | `/api/app/product/export-products-excel` | |
| 245 | +| Postman | **不要** 填 Body;筛选用 **Params**(Query)或拼在 URL 上;导出**不需要**上传 `file`(上传文件是 **3.3 导入**) | | ||
| 243 | | Query | 与产品列表筛选一致:`Keyword`、`State`、`Sorting` | | 246 | | Query | 与产品列表筛选一致:`Keyword`、`State`、`Sorting` | |
| 244 | | 数据范围 | **全量**:符合筛选条件的全部产品;**不使用** `SkipCount` / `MaxResultCount` | | 247 | | 数据范围 | **全量**:符合筛选条件的全部产品;**不使用** `SkipCount` / `MaxResultCount` | |
| 245 | | 排序 | 有 `Sorting` 则按其排序,否则默认按 `ProductName` 降序 | | 248 | | 排序 | 有 `Sorting` 则按其排序,否则默认按 `ProductName` 降序 | |
| 246 | | 响应文件名示例 | `products-export-yyyyMMdd-HHmmss.xlsx` | | 249 | | 响应文件名示例 | `products-export-yyyyMMdd-HHmmss.xlsx` | |
| 247 | | 列(与导入模板一致) | **Location**(多门店英文逗号拼接门店名称)、**Product Category**(分类名称)、**Product**(产品名称)、**Product Code**(产品编码;可为空则导出为空单元格) | | 250 | | 列(与导入模板一致) | **Location**(多门店英文逗号拼接门店名称)、**Product Category**(分类名称)、**Product**(产品名称)、**Product Code**(产品编码;可为空则导出为空单元格) | |
| 248 | 251 | ||
| 252 | +**部署**:须使用 **`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` 结束时报错提示。 | ||
| 253 | + | ||
| 249 | ### 3.3 批量导入 Excel | 254 | ### 3.3 批量导入 Excel |
| 250 | 255 | ||
| 251 | | 项目 | 说明 | | 256 | | 项目 | 说明 | |
| @@ -289,7 +294,7 @@ | @@ -289,7 +294,7 @@ | ||
| 289 | 294 | ||
| 290 | ## 5 Account Management(Company / Region) | 295 | ## 5 Account Management(Company / Region) |
| 291 | 296 | ||
| 292 | -前端 **Account Management** 菜单中 **Company**、**Region** 两个页签的「Bulk Export (PDF)」对应后端已有接口:数据模型分别为 **`fl_partner`**(合作伙伴 / 公司)、**`fl_group`**(组织 / 大区);应用服务为 **`PartnerAppService`**、**`GroupAppService`**。导出为 **QuestPDF** 生成的 **PDF**,筛选条件与各自**分页列表**一致,**不使用** `SkipCount` / `MaxResultCount`(全量导出)。单次导出超过 **5000** 条时返回业务错误,需缩小筛选范围。 | 297 | +前端 **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]`**。 |
| 293 | 298 | ||
| 294 | ### 5.1 Company(合作伙伴 / `PartnerAppService`) | 299 | ### 5.1 Company(合作伙伴 / `PartnerAppService`) |
| 295 | 300 |