Commit 0075d0e2744df78e66793194f230600b72ee382c
1
Showing
239 changed files
with
14786 additions
and
1727 deletions
Too many changes.
To preserve performance only 100 of 239 files are displayed.
泰额版/Food Labeling Management Code/Yi.Abp.Net8/framework/Yi.Framework.SqlSugarCore/SqlSugarDbContextFactory.cs
| @@ -21,10 +21,13 @@ namespace Yi.Framework.SqlSugarCore | @@ -21,10 +21,13 @@ namespace Yi.Framework.SqlSugarCore | ||
| 21 | { | 21 | { |
| 22 | #region Properties | 22 | #region Properties |
| 23 | 23 | ||
| 24 | + private ISqlSugarClient? _sqlSugarClient; | ||
| 25 | + private readonly object _clientLock = new(); | ||
| 26 | + | ||
| 24 | /// <summary> | 27 | /// <summary> |
| 25 | - /// SqlSugar客户端实例 | 28 | + /// SqlSugar 客户端(延迟按当前租户解析连接串,避免构造时 CurrentTenant 尚未就绪落到 host 库) |
| 26 | /// </summary> | 29 | /// </summary> |
| 27 | - public ISqlSugarClient SqlSugarClient { get; private set; } | 30 | + public ISqlSugarClient SqlSugarClient => GetOrCreateClient(); |
| 28 | 31 | ||
| 29 | /// <summary> | 32 | /// <summary> |
| 30 | /// 延迟服务提供者 | 33 | /// 延迟服务提供者 |
| @@ -75,22 +78,62 @@ namespace Yi.Framework.SqlSugarCore | @@ -75,22 +78,62 @@ namespace Yi.Framework.SqlSugarCore | ||
| 75 | public SqlSugarDbContextFactory(IAbpLazyServiceProvider lazyServiceProvider) | 78 | public SqlSugarDbContextFactory(IAbpLazyServiceProvider lazyServiceProvider) |
| 76 | { | 79 | { |
| 77 | LazyServiceProvider = lazyServiceProvider; | 80 | LazyServiceProvider = lazyServiceProvider; |
| 81 | + } | ||
| 78 | 82 | ||
| 79 | - // 异步获取租户配置 | ||
| 80 | - var tenantConfiguration = AsyncHelper.RunSync(async () => await TenantConfigurationWrapper.GetAsync()); | ||
| 81 | - | ||
| 82 | - // 构建数据库连接配置 | ||
| 83 | - var connectionConfig = BuildConnectionConfig(options => | 83 | + private ISqlSugarClient GetOrCreateClient() |
| 84 | + { | ||
| 85 | + // 必须每次按当前租户解析连接串:MultiTenancyMiddleware 查 YiTenant 时会先 Change(null) | ||
| 86 | + // 创建主库客户端;若缓存不切换,后续业务会误连 host。 | ||
| 87 | + var tenantConfiguration = | ||
| 88 | + AsyncHelper.RunSync(async () => await TenantConfigurationWrapper.GetAsync()); | ||
| 89 | + var connectionString = tenantConfiguration.GetCurrentConnectionString(); | ||
| 90 | + var dbType = GetCurrentDbType(tenantConfiguration.GetCurrentConnectionName()); | ||
| 91 | + | ||
| 92 | + if (_sqlSugarClient is not null && | ||
| 93 | + string.Equals( | ||
| 94 | + _sqlSugarClient.CurrentConnectionConfig?.ConnectionString, | ||
| 95 | + connectionString, | ||
| 96 | + StringComparison.Ordinal)) | ||
| 84 | { | 97 | { |
| 85 | - options.ConnectionString = tenantConfiguration.GetCurrentConnectionString(); | ||
| 86 | - options.DbType = GetCurrentDbType(tenantConfiguration.GetCurrentConnectionName()); | ||
| 87 | - }); | 98 | + return _sqlSugarClient; |
| 99 | + } | ||
| 88 | 100 | ||
| 89 | - // 创建SqlSugar客户端实例 | ||
| 90 | - SqlSugarClient = new SqlSugarClient(connectionConfig); | 101 | + lock (_clientLock) |
| 102 | + { | ||
| 103 | + if (_sqlSugarClient is not null && | ||
| 104 | + string.Equals( | ||
| 105 | + _sqlSugarClient.CurrentConnectionConfig?.ConnectionString, | ||
| 106 | + connectionString, | ||
| 107 | + StringComparison.Ordinal)) | ||
| 108 | + { | ||
| 109 | + return _sqlSugarClient; | ||
| 110 | + } | ||
| 91 | 111 | ||
| 92 | - // 配置数据库AOP | ||
| 93 | - ConfigureDbAop(SqlSugarClient); | 112 | + if (_sqlSugarClient is not null) |
| 113 | + { | ||
| 114 | + try | ||
| 115 | + { | ||
| 116 | + _sqlSugarClient.Dispose(); | ||
| 117 | + } | ||
| 118 | + catch | ||
| 119 | + { | ||
| 120 | + // ignore dispose race | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + _sqlSugarClient = null; | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + var connectionConfig = BuildConnectionConfig(options => | ||
| 127 | + { | ||
| 128 | + options.ConnectionString = connectionString; | ||
| 129 | + options.DbType = dbType; | ||
| 130 | + }); | ||
| 131 | + | ||
| 132 | + var client = new SqlSugarClient(connectionConfig); | ||
| 133 | + ConfigureDbAop(client); | ||
| 134 | + _sqlSugarClient = client; | ||
| 135 | + return _sqlSugarClient; | ||
| 136 | + } | ||
| 94 | } | 137 | } |
| 95 | 138 | ||
| 96 | /// <summary> | 139 | /// <summary> |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserBriefDto.cs
| @@ -5,6 +5,9 @@ namespace FoodLabeling.Application.Contracts.Dtos.AuthSession; | @@ -5,6 +5,9 @@ namespace FoodLabeling.Application.Contracts.Dtos.AuthSession; | ||
| 5 | /// </summary> | 5 | /// </summary> |
| 6 | public class CurrentUserBriefDto | 6 | public class CurrentUserBriefDto |
| 7 | { | 7 | { |
| 8 | + /// <summary> | ||
| 9 | + /// 当前登录用户 Id(非租户 Id)。 | ||
| 10 | + /// </summary> | ||
| 8 | public Guid Id { get; set; } | 11 | public Guid Id { get; set; } |
| 9 | 12 | ||
| 10 | public string UserName { get; set; } = string.Empty; | 13 | public string UserName { get; set; } = string.Empty; |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuPermissionsOutputDto.cs
| @@ -5,6 +5,11 @@ namespace FoodLabeling.Application.Contracts.Dtos.AuthSession; | @@ -5,6 +5,11 @@ namespace FoodLabeling.Application.Contracts.Dtos.AuthSession; | ||
| 5 | /// </summary> | 5 | /// </summary> |
| 6 | public class CurrentUserMenuPermissionsOutputDto | 6 | public class CurrentUserMenuPermissionsOutputDto |
| 7 | { | 7 | { |
| 8 | + /// <summary> | ||
| 9 | + /// 当前登录用户 Id(JSON: <c>userId</c>)。与 <see cref="User"/>.<see cref="CurrentUserBriefDto.Id"/> 一致,非租户 Id。 | ||
| 10 | + /// </summary> | ||
| 11 | + public Guid UserId { get; set; } | ||
| 12 | + | ||
| 8 | public CurrentUserBriefDto User { get; set; } = new(); | 13 | public CurrentUserBriefDto User { get; set; } = new(); |
| 9 | 14 | ||
| 10 | public List<string> RoleCodes { get; set; } = new(); | 15 | public List<string> RoleCodes { get; set; } = new(); |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Common/ITrainingFileScopeInput.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Common; | ||
| 2 | + | ||
| 3 | +/// <summary> | ||
| 4 | +/// 培训文件创建/编辑入参中的 Company / Region / Location 适用范围字段。 | ||
| 5 | +/// </summary> | ||
| 6 | +public interface ITrainingFileScopeInput : ILabelEntityPartnerScopeInput | ||
| 7 | +{ | ||
| 8 | + /// <summary>适用 Region:ALL / SPECIFIED</summary> | ||
| 9 | + string? AppliedRegionType { get; } | ||
| 10 | + | ||
| 11 | + /// <summary>适用 Region(<c>fl_group.Id</c>);与 <see cref="GroupIds"/> 合并;可含 <c>ALL</c></summary> | ||
| 12 | + List<string>? RegionIds { get; } | ||
| 13 | + | ||
| 14 | + /// <summary>与 <see cref="RegionIds"/> 相同;可含 <c>ALL</c></summary> | ||
| 15 | + List<string>? GroupIds { get; } | ||
| 16 | + | ||
| 17 | + /// <summary>适用 Location:ALL / SPECIFIED(与 <see cref="AppliedLocationType"/> 二选一)</summary> | ||
| 18 | + string? AvailabilityType { get; } | ||
| 19 | + | ||
| 20 | + /// <summary>适用 Location:ALL / SPECIFIED(<see cref="AvailabilityType"/> 别名)</summary> | ||
| 21 | + string? AppliedLocationType { get; } | ||
| 22 | + | ||
| 23 | + /// <summary>适用门店(<c>location.Id</c>);可含 <c>ALL</c></summary> | ||
| 24 | + List<string>? LocationIds { get; } | ||
| 25 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelBatchCreateItemInputVo.cs
| @@ -13,6 +13,9 @@ public class LabelBatchCreateItemInputVo | @@ -13,6 +13,9 @@ public class LabelBatchCreateItemInputVo | ||
| 13 | 13 | ||
| 14 | public List<string>? PartnerIds { get; set; } | 14 | public List<string>? PartnerIds { get; set; } |
| 15 | 15 | ||
| 16 | + /// <summary>适用 Company(单选)</summary> | ||
| 17 | + public List<string>? CompanyIds { get; set; } | ||
| 18 | + | ||
| 16 | public string? AppliedRegionType { get; set; } | 19 | public string? AppliedRegionType { get; set; } |
| 17 | 20 | ||
| 18 | public List<string>? RegionIds { get; set; } | 21 | public List<string>? RegionIds { get; set; } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelCreateInputVo.cs
| @@ -2,6 +2,7 @@ namespace FoodLabeling.Application.Contracts.Dtos.Label; | @@ -2,6 +2,7 @@ namespace FoodLabeling.Application.Contracts.Dtos.Label; | ||
| 2 | 2 | ||
| 3 | public class LabelCreateInputVo | 3 | public class LabelCreateInputVo |
| 4 | { | 4 | { |
| 5 | + /// <summary>标签编码(可选;未传或空字符串时由后端自动生成唯一编码)</summary> | ||
| 5 | public string? LabelCode { get; set; } | 6 | public string? LabelCode { get; set; } |
| 6 | 7 | ||
| 7 | public string LabelName { get; set; } = string.Empty; | 8 | public string LabelName { get; set; } = string.Empty; |
| @@ -9,32 +10,38 @@ public class LabelCreateInputVo | @@ -9,32 +10,38 @@ public class LabelCreateInputVo | ||
| 9 | public string TemplateCode { get; set; } = string.Empty; | 10 | public string TemplateCode { get; set; } = string.Empty; |
| 10 | 11 | ||
| 11 | /// <summary> | 12 | /// <summary> |
| 12 | - /// 适用 Company(<c>fl_partner.Id</c>);与 <see cref="PartnerIds"/> 合并,用于解析所属门店 | 13 | + /// 适用 Company(<c>fl_partner.Id</c>);与 <see cref="PartnerIds"/> / <see cref="CompanyIds"/> 合并,单选 |
| 13 | /// </summary> | 14 | /// </summary> |
| 14 | public string? PartnerId { get; set; } | 15 | public string? PartnerId { get; set; } |
| 15 | 16 | ||
| 16 | /// <summary> | 17 | /// <summary> |
| 17 | - /// 适用 Company 多选(<c>fl_partner.Id</c>) | 18 | + /// 适用 Company 多选字段(仅支持 1 个;与 <see cref="CompanyIds"/> 相同) |
| 18 | /// </summary> | 19 | /// </summary> |
| 19 | public List<string>? PartnerIds { get; set; } | 20 | public List<string>? PartnerIds { get; set; } |
| 20 | 21 | ||
| 21 | /// <summary> | 22 | /// <summary> |
| 22 | - /// 适用 Region 范围:<c>ALL</c>(全选)/ <c>SPECIFIED</c>(按 <see cref="RegionIds"/>)。传了有效 regionIds 时按 SPECIFIED 处理。 | 23 | + /// 适用 Company(单选,<c>fl_partner.Id</c>);与 <see cref="PartnerId"/> / <see cref="PartnerIds"/> 合并 |
| 24 | + /// </summary> | ||
| 25 | + public List<string>? CompanyIds { get; set; } | ||
| 26 | + | ||
| 27 | + /// <summary> | ||
| 28 | + /// 适用 Region 范围:<c>ALL</c> / <c>SPECIFIED</c>。 | ||
| 29 | + /// 亦可在 <see cref="RegionIds"/> / <see cref="LocationIds"/> 传哨兵 <c>["ALL"]</c>(即使本字段为 SPECIFIED 亦归档 ALL;POST/PUT 均支持)。 | ||
| 23 | /// </summary> | 30 | /// </summary> |
| 24 | public string? AppliedRegionType { get; set; } | 31 | public string? AppliedRegionType { get; set; } |
| 25 | 32 | ||
| 26 | /// <summary> | 33 | /// <summary> |
| 27 | - /// 适用 Region 多选(<c>fl_group.Id</c>);与 <see cref="GroupIds"/> 合并,落库 <c>fl_label_region</c> | 34 | + /// 适用 Region 多选(<c>fl_group.Id</c>);与 <see cref="GroupIds"/> 合并;可含 <c>ALL</c> |
| 28 | /// </summary> | 35 | /// </summary> |
| 29 | public List<string>? RegionIds { get; set; } | 36 | public List<string>? RegionIds { get; set; } |
| 30 | 37 | ||
| 31 | /// <summary> | 38 | /// <summary> |
| 32 | - /// 适用 Region 多选(与 <see cref="RegionIds"/> 相同) | 39 | + /// 适用 Region 多选(与 <see cref="RegionIds"/> 相同);可含 <c>ALL</c> |
| 33 | /// </summary> | 40 | /// </summary> |
| 34 | public List<string>? GroupIds { get; set; } | 41 | public List<string>? GroupIds { get; set; } |
| 35 | 42 | ||
| 36 | /// <summary> | 43 | /// <summary> |
| 37 | - /// 适用门店 Id 数组(<c>location.Id</c>,落库 <c>fl_label_location</c>);主字段 | 44 | + /// 适用门店 Id 数组(<c>location.Id</c>);可含 <c>ALL</c>;落库 <c>fl_label_location</c> |
| 38 | /// </summary> | 45 | /// </summary> |
| 39 | public List<string>? LocationIds { get; set; } | 46 | public List<string>? LocationIds { get; set; } |
| 40 | 47 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetOutputDto.cs
| @@ -17,11 +17,14 @@ public class LabelGetOutputDto | @@ -17,11 +17,14 @@ public class LabelGetOutputDto | ||
| 17 | 17 | ||
| 18 | public string LocationName { get; set; } = string.Empty; | 18 | public string LocationName { get; set; } = string.Empty; |
| 19 | 19 | ||
| 20 | - /// <summary>适用 Company Id(由所属门店反推)</summary> | 20 | + /// <summary>适用 Company Id(单选;优先落库 <c>fl_label.PartnerId</c>)</summary> |
| 21 | public string? PartnerId { get; set; } | 21 | public string? PartnerId { get; set; } |
| 22 | 22 | ||
| 23 | public List<string> PartnerIds { get; set; } = new(); | 23 | public List<string> PartnerIds { get; set; } = new(); |
| 24 | 24 | ||
| 25 | + /// <summary>与 <see cref="PartnerIds"/> 相同(兼容字段,单选)</summary> | ||
| 26 | + public List<string> CompanyIds { get; set; } = new(); | ||
| 27 | + | ||
| 25 | /// <summary>适用 Region 范围:ALL / SPECIFIED</summary> | 28 | /// <summary>适用 Region 范围:ALL / SPECIFIED</summary> |
| 26 | public string AppliedRegionType { get; set; } = "SPECIFIED"; | 29 | public string AppliedRegionType { get; set; } = "SPECIFIED"; |
| 27 | 30 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelUpdateInputVo.cs
| @@ -10,17 +10,23 @@ public class LabelUpdateInputVo | @@ -10,17 +10,23 @@ public class LabelUpdateInputVo | ||
| 10 | 10 | ||
| 11 | public List<string>? PartnerIds { get; set; } | 11 | public List<string>? PartnerIds { get; set; } |
| 12 | 12 | ||
| 13 | + /// <summary>适用 Company(单选);与 <see cref="PartnerId"/> / <see cref="PartnerIds"/> 合并</summary> | ||
| 14 | + public List<string>? CompanyIds { get; set; } | ||
| 15 | + | ||
| 13 | /// <summary> | 16 | /// <summary> |
| 14 | - /// 适用 Region 范围:<c>ALL</c> / <c>SPECIFIED</c>(见 <see cref="RegionIds"/>) | 17 | + /// 适用 Region 范围:<c>ALL</c> / <c>SPECIFIED</c>。 |
| 18 | + /// 亦可在 <see cref="RegionIds"/> / <see cref="LocationIds"/> 传哨兵 <c>["ALL"]</c>(与新增一致,PUT 支持)。 | ||
| 15 | /// </summary> | 19 | /// </summary> |
| 16 | public string? AppliedRegionType { get; set; } | 20 | public string? AppliedRegionType { get; set; } |
| 17 | 21 | ||
| 22 | + /// <summary>适用 Region 多选;可含 <c>ALL</c></summary> | ||
| 18 | public List<string>? RegionIds { get; set; } | 23 | public List<string>? RegionIds { get; set; } |
| 19 | 24 | ||
| 25 | + /// <summary>与 <see cref="RegionIds"/> 相同;可含 <c>ALL</c></summary> | ||
| 20 | public List<string>? GroupIds { get; set; } | 26 | public List<string>? GroupIds { get; set; } |
| 21 | 27 | ||
| 22 | /// <summary> | 28 | /// <summary> |
| 23 | - /// 适用门店 Id 数组(<c>location.Id</c>,落库 <c>fl_label_location</c>);主字段,全量覆盖 | 29 | + /// 适用门店 Id 数组;可含 <c>ALL</c>;落库 <c>fl_label_location</c>,全量覆盖 |
| 24 | /// </summary> | 30 | /// </summary> |
| 25 | public List<string>? LocationIds { get; set; } | 31 | public List<string>? LocationIds { get; set; } |
| 26 | 32 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelAlertTimer/LabelAlertTimerCheckExpiredInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.LabelAlertTimer; | ||
| 2 | + | ||
| 3 | +/// <summary> | ||
| 4 | +/// 检查告警计时器是否过期入参(至少提供一个标识) | ||
| 5 | +/// </summary> | ||
| 6 | +public class LabelAlertTimerCheckExpiredInputVo | ||
| 7 | +{ | ||
| 8 | + public string? TimerId { get; set; } | ||
| 9 | + | ||
| 10 | + public string? BatchId { get; set; } | ||
| 11 | + | ||
| 12 | + public string? PrintTaskId { get; set; } | ||
| 13 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelAlertTimer/LabelAlertTimerCheckExpiredOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.LabelAlertTimer; | ||
| 2 | + | ||
| 3 | +/// <summary> | ||
| 4 | +/// 检查告警计时器是否过期出参 | ||
| 5 | +/// </summary> | ||
| 6 | +public class LabelAlertTimerCheckExpiredOutputDto | ||
| 7 | +{ | ||
| 8 | + /// <summary>是否找到计时器记录</summary> | ||
| 9 | + public bool Found { get; set; } | ||
| 10 | + | ||
| 11 | + /// <summary>是否已过期;无记录时为 false(仅展示用,不用于拦截打印)</summary> | ||
| 12 | + public bool IsExpired { get; set; } | ||
| 13 | + | ||
| 14 | + public DateTime? ExpiresAt { get; set; } | ||
| 15 | + | ||
| 16 | + /// <summary>剩余秒数(已过期为 0)</summary> | ||
| 17 | + public int RemainingSeconds { get; set; } | ||
| 18 | + | ||
| 19 | + /// <summary>状态:<c>expired</c> 或 <c>running</c></summary> | ||
| 20 | + public string? Status { get; set; } | ||
| 21 | + | ||
| 22 | + public string? Title { get; set; } | ||
| 23 | + | ||
| 24 | + public string? Subtitle { get; set; } | ||
| 25 | + | ||
| 26 | + public string? TimerId { get; set; } | ||
| 27 | + | ||
| 28 | + public string? BatchId { get; set; } | ||
| 29 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelAlertTimer/LabelAlertTimerGetListInputVo.cs
0 → 100644
| 1 | +using Volo.Abp.Application.Dtos; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Contracts.Dtos.LabelAlertTimer; | ||
| 4 | + | ||
| 5 | +/// <summary> | ||
| 6 | +/// 告警计时器分页列表入参 | ||
| 7 | +/// </summary> | ||
| 8 | +public class LabelAlertTimerGetListInputVo : PagedAndSortedResultRequestDto | ||
| 9 | +{ | ||
| 10 | + /// <summary> | ||
| 11 | + /// 当前门店 Id(location.Id,Guid 字符串)。 | ||
| 12 | + /// <c>list</c> 必填;<c>app-list</c> 可空(空则取已选门店缓存)。 | ||
| 13 | + /// </summary> | ||
| 14 | + public string? LocationId { get; set; } | ||
| 15 | + | ||
| 16 | + /// <summary> | ||
| 17 | + /// 打印日期(<c>yyyy-MM-dd</c>);按 PrintedAt 筛选该日记录。 | ||
| 18 | + /// </summary> | ||
| 19 | + public string? DateDay { get; set; } | ||
| 20 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelAlertTimer/LabelAlertTimerListItemDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.LabelAlertTimer; | ||
| 2 | + | ||
| 3 | +/// <summary> | ||
| 4 | +/// 告警计时器列表项 | ||
| 5 | +/// </summary> | ||
| 6 | +public class LabelAlertTimerListItemDto | ||
| 7 | +{ | ||
| 8 | + public string Id { get; set; } = string.Empty; | ||
| 9 | + | ||
| 10 | + public string BatchId { get; set; } = string.Empty; | ||
| 11 | + | ||
| 12 | + public string PrintTaskId { get; set; } = string.Empty; | ||
| 13 | + | ||
| 14 | + public string LabelId { get; set; } = string.Empty; | ||
| 15 | + | ||
| 16 | + public string? LabelCode { get; set; } | ||
| 17 | + | ||
| 18 | + public string Title { get; set; } = string.Empty; | ||
| 19 | + | ||
| 20 | + public string Subtitle { get; set; } = string.Empty; | ||
| 21 | + | ||
| 22 | + /// <summary>总时长(秒),同 DurationSeconds</summary> | ||
| 23 | + public int TotalTime { get; set; } | ||
| 24 | + | ||
| 25 | + /// <summary>剩余时长(秒)</summary> | ||
| 26 | + public int RemainingTime { get; set; } | ||
| 27 | + | ||
| 28 | + /// <summary>状态:<c>expired</c> 或 <c>running</c></summary> | ||
| 29 | + public string Status { get; set; } = string.Empty; | ||
| 30 | + | ||
| 31 | + public DateTime ExpiresAt { get; set; } | ||
| 32 | + | ||
| 33 | + public DateTime PrintedAt { get; set; } | ||
| 34 | + | ||
| 35 | + public string LocationId { get; set; } = string.Empty; | ||
| 36 | + | ||
| 37 | + public string? ProductName { get; set; } | ||
| 38 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryCreateInputVo.cs
| @@ -4,7 +4,8 @@ namespace FoodLabeling.Application.Contracts.Dtos.LabelCategory; | @@ -4,7 +4,8 @@ namespace FoodLabeling.Application.Contracts.Dtos.LabelCategory; | ||
| 4 | 4 | ||
| 5 | public class LabelCategoryCreateInputVo : ILabelEntityPartnerScopeInput | 5 | public class LabelCategoryCreateInputVo : ILabelEntityPartnerScopeInput |
| 6 | { | 6 | { |
| 7 | - public string CategoryCode { get; set; } = string.Empty; | 7 | + /// <summary>分类编码(可选;未传或空字符串时由后端自动生成唯一编码)</summary> |
| 8 | + public string? CategoryCode { get; set; } | ||
| 8 | 9 | ||
| 9 | public string CategoryName { get; set; } = string.Empty; | 10 | public string CategoryName { get; set; } = string.Empty; |
| 10 | 11 | ||
| @@ -37,25 +38,28 @@ public class LabelCategoryCreateInputVo : ILabelEntityPartnerScopeInput | @@ -37,25 +38,28 @@ public class LabelCategoryCreateInputVo : ILabelEntityPartnerScopeInput | ||
| 37 | public List<string>? CompanyIds { get; set; } | 38 | public List<string>? CompanyIds { get; set; } |
| 38 | 39 | ||
| 39 | /// <summary> | 40 | /// <summary> |
| 40 | - /// 门店可用范围:ALL / SPECIFIED | 41 | + /// 门店可用范围:ALL / SPECIFIED。 |
| 42 | + /// <c>regionIds</c>/<c>groupIds</c>/<c>locationIds</c> 可传哨兵 <c>["ALL"]</c>(大小写不敏感); | ||
| 43 | + /// 即使本字段为 SPECIFIED,Id 数组为 ALL 时仍归档为 ALL(POST/PUT 均支持)。 | ||
| 41 | /// </summary> | 44 | /// </summary> |
| 42 | public string AvailabilityType { get; set; } = "ALL"; | 45 | public string AvailabilityType { get; set; } = "ALL"; |
| 43 | 46 | ||
| 44 | /// <summary> | 47 | /// <summary> |
| 45 | - /// 适用 Region(多选),<c>fl_group.Id</c>;与 <see cref="GroupIds"/> 合并去重 | 48 | + /// 适用 Region(多选),<c>fl_group.Id</c>;与 <see cref="GroupIds"/> 合并去重;可含 <c>ALL</c> |
| 46 | /// </summary> | 49 | /// </summary> |
| 47 | public List<string>? RegionIds { get; set; } | 50 | public List<string>? RegionIds { get; set; } |
| 48 | 51 | ||
| 49 | /// <summary> | 52 | /// <summary> |
| 50 | - /// 适用 Region(多选),与 <see cref="RegionIds"/> 相同 | 53 | + /// 适用 Region(多选),与 <see cref="RegionIds"/> 相同;可含 <c>ALL</c> |
| 51 | /// </summary> | 54 | /// </summary> |
| 52 | public List<string>? GroupIds { get; set; } | 55 | public List<string>? GroupIds { get; set; } |
| 53 | 56 | ||
| 54 | /// <summary> | 57 | /// <summary> |
| 55 | - /// 适用门店(多选),<c>location.Id</c>;与 Region 合并后写入 <c>fl_label_category_location</c> | 58 | + /// 适用门店(多选),<c>location.Id</c>;可含 <c>ALL</c>;与 Region 合并后写入 <c>fl_label_category_location</c> |
| 56 | /// </summary> | 59 | /// </summary> |
| 57 | public List<string>? LocationIds { get; set; } | 60 | public List<string>? LocationIds { get; set; } |
| 58 | 61 | ||
| 59 | - public int OrderNum { get; set; } | 62 | + /// <summary>排序;未传或 null 时默认 0</summary> |
| 63 | + public int? OrderNum { get; set; } | ||
| 60 | } | 64 | } |
| 61 | 65 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionCreateInputVo.cs
| @@ -4,7 +4,7 @@ namespace FoodLabeling.Application.Contracts.Dtos.LabelMultipleOption; | @@ -4,7 +4,7 @@ namespace FoodLabeling.Application.Contracts.Dtos.LabelMultipleOption; | ||
| 4 | 4 | ||
| 5 | public class LabelMultipleOptionCreateInputVo : ILabelEntityPartnerScopeInput | 5 | public class LabelMultipleOptionCreateInputVo : ILabelEntityPartnerScopeInput |
| 6 | { | 6 | { |
| 7 | - /// <summary>多选项编码(可选;未传或空字符串时存空,列表/详情出参为「无」)</summary> | 7 | + /// <summary>多选项编码(可选;未传或空字符串时由后端自动生成唯一编码)</summary> |
| 8 | public string? OptionCode { get; set; } | 8 | public string? OptionCode { get; set; } |
| 9 | 9 | ||
| 10 | public string OptionName { get; set; } = string.Empty; | 10 | public string OptionName { get; set; } = string.Empty; |
| @@ -25,25 +25,27 @@ public class LabelMultipleOptionCreateInputVo : ILabelEntityPartnerScopeInput | @@ -25,25 +25,27 @@ public class LabelMultipleOptionCreateInputVo : ILabelEntityPartnerScopeInput | ||
| 25 | public List<string>? CompanyIds { get; set; } | 25 | public List<string>? CompanyIds { get; set; } |
| 26 | 26 | ||
| 27 | /// <summary> | 27 | /// <summary> |
| 28 | - /// 门店可用范围:ALL / SPECIFIED;传了 <see cref="RegionIds"/> 或 <see cref="LocationIds"/> 时自动为 SPECIFIED | 28 | + /// 门店可用范围:ALL / SPECIFIED;传了 <see cref="RegionIds"/> 或 <see cref="LocationIds"/> 时自动为 SPECIFIED。 |
| 29 | + /// Id 数组可传哨兵 <c>ALL</c>(POST/PUT 新增与编辑均支持),归档为 <c>availabilityType=ALL</c> 且不写门店快照。 | ||
| 29 | /// </summary> | 30 | /// </summary> |
| 30 | public string AvailabilityType { get; set; } = "ALL"; | 31 | public string AvailabilityType { get; set; } = "ALL"; |
| 31 | 32 | ||
| 32 | /// <summary> | 33 | /// <summary> |
| 33 | - /// 适用 Region(多选),<c>fl_group.Id</c>;与 <see cref="GroupIds"/> 合并去重 | 34 | + /// 适用 Region(多选),<c>fl_group.Id</c>;与 <see cref="GroupIds"/> 合并去重。可传 <c>ALL</c>。 |
| 34 | /// </summary> | 35 | /// </summary> |
| 35 | public List<string>? RegionIds { get; set; } | 36 | public List<string>? RegionIds { get; set; } |
| 36 | 37 | ||
| 37 | /// <summary> | 38 | /// <summary> |
| 38 | - /// 适用 Region(多选),与 <see cref="RegionIds"/> 相同 | 39 | + /// 适用 Region(多选),与 <see cref="RegionIds"/> 相同。可传 <c>ALL</c>。 |
| 39 | /// </summary> | 40 | /// </summary> |
| 40 | public List<string>? GroupIds { get; set; } | 41 | public List<string>? GroupIds { get; set; } |
| 41 | 42 | ||
| 42 | /// <summary> | 43 | /// <summary> |
| 43 | - /// 适用门店(多选),<c>location.Id</c>;与 Region 合并后写入 <c>fl_label_multiple_option_location</c> | 44 | + /// 适用门店(多选),<c>location.Id</c>;与 Region 合并后写入 <c>fl_label_multiple_option_location</c>。可传 <c>ALL</c>。 |
| 44 | /// </summary> | 45 | /// </summary> |
| 45 | public List<string>? LocationIds { get; set; } | 46 | public List<string>? LocationIds { get; set; } |
| 46 | 47 | ||
| 47 | - public int OrderNum { get; set; } | 48 | + /// <summary>排序;未传或 null 时默认 0</summary> |
| 49 | + public int? OrderNum { get; set; } | ||
| 48 | } | 50 | } |
| 49 | 51 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeCreateInputVo.cs
| @@ -4,7 +4,8 @@ namespace FoodLabeling.Application.Contracts.Dtos.LabelType; | @@ -4,7 +4,8 @@ namespace FoodLabeling.Application.Contracts.Dtos.LabelType; | ||
| 4 | 4 | ||
| 5 | public class LabelTypeCreateInputVo : ILabelEntityPartnerScopeInput | 5 | public class LabelTypeCreateInputVo : ILabelEntityPartnerScopeInput |
| 6 | { | 6 | { |
| 7 | - public string TypeCode { get; set; } = string.Empty; | 7 | + /// <summary>类型编码;为空时由后端自动生成唯一编码</summary> |
| 8 | + public string? TypeCode { get; set; } | ||
| 8 | 9 | ||
| 9 | public string TypeName { get; set; } = string.Empty; | 10 | public string TypeName { get; set; } = string.Empty; |
| 10 | 11 | ||
| @@ -22,25 +23,28 @@ public class LabelTypeCreateInputVo : ILabelEntityPartnerScopeInput | @@ -22,25 +23,28 @@ public class LabelTypeCreateInputVo : ILabelEntityPartnerScopeInput | ||
| 22 | public List<string>? CompanyIds { get; set; } | 23 | public List<string>? CompanyIds { get; set; } |
| 23 | 24 | ||
| 24 | /// <summary> | 25 | /// <summary> |
| 25 | - /// 门店可用范围:ALL / SPECIFIED;传了 <see cref="RegionIds"/> 或 <see cref="LocationIds"/> 时自动为 SPECIFIED | 26 | + /// 门店可用范围:ALL / SPECIFIED。 |
| 27 | + /// <c>regionIds</c>/<c>groupIds</c>/<c>locationIds</c> 可传哨兵 <c>["ALL"]</c>(大小写不敏感); | ||
| 28 | + /// 即使本字段为 SPECIFIED,Id 数组为 ALL 时仍归档为 ALL(POST/PUT 均支持)。 | ||
| 26 | /// </summary> | 29 | /// </summary> |
| 27 | public string AvailabilityType { get; set; } = "ALL"; | 30 | public string AvailabilityType { get; set; } = "ALL"; |
| 28 | 31 | ||
| 29 | /// <summary> | 32 | /// <summary> |
| 30 | - /// 适用 Region(多选),<c>fl_group.Id</c>;与 <see cref="GroupIds"/> 合并去重 | 33 | + /// 适用 Region(多选),<c>fl_group.Id</c>;与 <see cref="GroupIds"/> 合并去重;可含 <c>ALL</c> |
| 31 | /// </summary> | 34 | /// </summary> |
| 32 | public List<string>? RegionIds { get; set; } | 35 | public List<string>? RegionIds { get; set; } |
| 33 | 36 | ||
| 34 | /// <summary> | 37 | /// <summary> |
| 35 | - /// 适用 Region(多选),与 <see cref="RegionIds"/> 相同 | 38 | + /// 适用 Region(多选),与 <see cref="RegionIds"/> 相同;可含 <c>ALL</c> |
| 36 | /// </summary> | 39 | /// </summary> |
| 37 | public List<string>? GroupIds { get; set; } | 40 | public List<string>? GroupIds { get; set; } |
| 38 | 41 | ||
| 39 | /// <summary> | 42 | /// <summary> |
| 40 | - /// 适用门店(多选),<c>location.Id</c>;与 Region 合并后写入 <c>fl_label_type_location</c> | 43 | + /// 适用门店(多选),<c>location.Id</c>;可含 <c>ALL</c>;与 Region 合并后写入 <c>fl_label_type_location</c> |
| 41 | /// </summary> | 44 | /// </summary> |
| 42 | public List<string>? LocationIds { get; set; } | 45 | public List<string>? LocationIds { get; set; } |
| 43 | 46 | ||
| 44 | - public int OrderNum { get; set; } | 47 | + /// <summary>排序;未传或 null 时默认 0</summary> |
| 48 | + public int? OrderNum { get; set; } | ||
| 45 | } | 49 | } |
| 46 | 50 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductBatchImportOnlineInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Product; | ||
| 2 | + | ||
| 3 | +/// <summary> | ||
| 4 | +/// 产品 JSON 在线批量导入请求体 | ||
| 5 | +/// </summary> | ||
| 6 | +public class ProductBatchImportOnlineInputVo | ||
| 7 | +{ | ||
| 8 | + /// <summary> | ||
| 9 | + /// 待导入行,每元素与单条 <c>POST /api/app/product</c> 的 <see cref="ProductCreateInputVo"/> 一致 | ||
| 10 | + /// </summary> | ||
| 11 | + public List<ProductCreateInputVo> Items { get; set; } = new(); | ||
| 12 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductBatchImportOnlineResultDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Product; | ||
| 2 | + | ||
| 3 | +/// <summary> | ||
| 4 | +/// 产品 JSON 在线批量导入结果 | ||
| 5 | +/// </summary> | ||
| 6 | +public class ProductBatchImportOnlineResultDto | ||
| 7 | +{ | ||
| 8 | + public int SuccessCount { get; set; } | ||
| 9 | + | ||
| 10 | + public int FailCount { get; set; } | ||
| 11 | + | ||
| 12 | + public List<ProductBatchImportOnlineErrorDto> Errors { get; set; } = new(); | ||
| 13 | +} | ||
| 14 | + | ||
| 15 | +/// <summary> | ||
| 16 | +/// 产品 JSON 在线批量导入单条失败信息 | ||
| 17 | +/// </summary> | ||
| 18 | +public class ProductBatchImportOnlineErrorDto | ||
| 19 | +{ | ||
| 20 | + /// <summary> | ||
| 21 | + /// 在请求 <c>items</c> 数组中的序号(从 0 开始) | ||
| 22 | + /// </summary> | ||
| 23 | + public int Index { get; set; } | ||
| 24 | + | ||
| 25 | + /// <summary> | ||
| 26 | + /// 产品名称(<c>productName</c>),便于定位失败行 | ||
| 27 | + /// </summary> | ||
| 28 | + public string? ProductName { get; set; } | ||
| 29 | + | ||
| 30 | + public string Message { get; set; } = string.Empty; | ||
| 31 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs
| 1 | using System.Collections.Generic; | 1 | using System.Collections.Generic; |
| 2 | +using System.Text.Json.Serialization; | ||
| 3 | +using FoodLabeling.Application.Contracts.Json; | ||
| 2 | 4 | ||
| 3 | namespace FoodLabeling.Application.Contracts.Dtos.Product; | 5 | namespace FoodLabeling.Application.Contracts.Dtos.Product; |
| 4 | 6 | ||
| @@ -35,18 +37,46 @@ public class ProductCreateInputVo | @@ -35,18 +37,46 @@ public class ProductCreateInputVo | ||
| 35 | public string? AvailabilityType { get; set; } | 37 | public string? AvailabilityType { get; set; } |
| 36 | 38 | ||
| 37 | /// <summary> | 39 | /// <summary> |
| 38 | - /// 适用 Company(<c>fl_partner.Id</c>,UI 称 Company);展开该公司下全部门店后与 Region/门店合并写入 <c>fl_location_product</c> | 40 | + /// 适用 Company:ALL / SPECIFIED(产品 Company 仅支持 SPECIFIED + 单选具体 Guid)。 |
| 39 | /// </summary> | 41 | /// </summary> |
| 42 | + public string? AppliedPartnerType { get; set; } | ||
| 43 | + | ||
| 44 | + /// <summary> | ||
| 45 | + /// 适用 Company(<c>fl_partner.Id</c>,UI 称 Company);展开该公司下全部门店后与 Region/门店合并写入 <c>fl_location_product</c>。 | ||
| 46 | + /// 与 <see cref="CompanyIds"/> 二选一或同值;产品 Company <b>仅支持单选</b>,<b>不支持 ALL</b>。 | ||
| 47 | + /// 兼容前端误传数组:<c>"partnerId":["{guid}"]</c> 与字符串等价。 | ||
| 48 | + /// </summary> | ||
| 49 | + [JsonConverter(typeof(StringOrFirstArrayItemJsonConverter))] | ||
| 40 | public string? PartnerId { get; set; } | 50 | public string? PartnerId { get; set; } |
| 41 | 51 | ||
| 42 | /// <summary> | 52 | /// <summary> |
| 43 | - /// 适用 Region(<c>fl_group.Id</c>,UI 称 Region;库字段为 <c>location.GroupName</c>) | 53 | + /// 适用 Company Id 列表(与 <see cref="CompanyIds"/> 同义,前端常用字段)。<b>仅支持单选</b>。 |
| 54 | + /// </summary> | ||
| 55 | + public List<string>? PartnerIds { get; set; } | ||
| 56 | + | ||
| 57 | + /// <summary> | ||
| 58 | + /// 适用 Company(推荐前端字段)。<b>仅支持单选</b>:数组最多 1 个具体 Guid;<b>不支持 ALL</b>。 | ||
| 59 | + /// 传多个 Guid 或含 <c>ALL</c> 将报错;与 <see cref="PartnerId"/> 同时传时须一致。 | ||
| 60 | + /// </summary> | ||
| 61 | + public List<string>? CompanyIds { get; set; } | ||
| 62 | + | ||
| 63 | + /// <summary>适用 Region:ALL / SPECIFIED(与 <see cref="AvailabilityType"/> 独立,用于前端显式声明)</summary> | ||
| 64 | + public string? AppliedRegionType { get; set; } | ||
| 65 | + | ||
| 66 | + /// <summary> | ||
| 67 | + /// 适用 Region(<c>fl_group.Id</c>,UI 称 Region;库字段为 <c>location.GroupName</c>)。 | ||
| 68 | + /// 与 <see cref="RegionIds"/> 合并;含 <c>ALL</c> 且无具体门店时,有具体 Company 则展开该公司门店,否则归档全局 ALL。 | ||
| 44 | /// </summary> | 69 | /// </summary> |
| 45 | public List<string>? GroupIds { get; set; } | 70 | public List<string>? GroupIds { get; set; } |
| 46 | 71 | ||
| 47 | /// <summary> | 72 | /// <summary> |
| 48 | - /// 适用门店 Id 列表;与 <see cref="GroupIds"/> 合并后写入 <c>fl_location_product</c>。 | ||
| 49 | - /// 不传则不在本接口写入门店关联。 | 73 | + /// 适用 Region(与 <see cref="GroupIds"/> 同义,前端常用字段)。 |
| 74 | + /// </summary> | ||
| 75 | + public List<string>? RegionIds { get; set; } | ||
| 76 | + | ||
| 77 | + /// <summary> | ||
| 78 | + /// 适用门店 Id 列表。有具体 Guid 时以门店为准;含 <c>ALL</c> 且已指定 Company/Region 时表示该范围内全选(SPECIFIED 快照); | ||
| 79 | + /// 无具体 Company/Region 时归档全局 ALL。 | ||
| 50 | /// </summary> | 80 | /// </summary> |
| 51 | public List<string>? LocationIds { get; set; } | 81 | public List<string>? LocationIds { get; set; } |
| 52 | } | 82 | } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductGetOutputDto.cs
| @@ -29,17 +29,25 @@ public class ProductGetOutputDto | @@ -29,17 +29,25 @@ public class ProductGetOutputDto | ||
| 29 | /// <summary>适用门店:ALL / SPECIFIED</summary> | 29 | /// <summary>适用门店:ALL / SPECIFIED</summary> |
| 30 | public string AvailabilityType { get; set; } = "SPECIFIED"; | 30 | public string AvailabilityType { get; set; } = "SPECIFIED"; |
| 31 | 31 | ||
| 32 | - /// <summary>适用 Company Id(<c>fl_partner.Id</c>,由关联门店反推;多公司时取第一个)</summary> | 32 | + /// <summary>适用 Company Id(<c>fl_partner.Id</c>,由关联门店反推;多公司时取第一个;ALL 时为 null)</summary> |
| 33 | public string? PartnerId { get; set; } | 33 | public string? PartnerId { get; set; } |
| 34 | 34 | ||
| 35 | - /// <summary>适用 Company Id 列表(去重)</summary> | 35 | + /// <summary>适用 Company Id 列表(去重;历史数据可能多条)</summary> |
| 36 | public List<string> PartnerIds { get; set; } = new(); | 36 | public List<string> PartnerIds { get; set; } = new(); |
| 37 | 37 | ||
| 38 | + /// <summary> | ||
| 39 | + /// 适用 Company(与编辑入参对齐)。单选:0~1 个具体 Guid;不支持 ALL。 | ||
| 40 | + /// </summary> | ||
| 41 | + public List<string> CompanyIds { get; set; } = new(); | ||
| 42 | + | ||
| 38 | /// <summary>适用 Region Id(<c>fl_group.Id</c>,由关联门店反推)</summary> | 43 | /// <summary>适用 Region Id(<c>fl_group.Id</c>,由关联门店反推)</summary> |
| 39 | public List<string> GroupIds { get; set; } = new(); | 44 | public List<string> GroupIds { get; set; } = new(); |
| 40 | 45 | ||
| 46 | + /// <summary>适用 Region(与 <see cref="GroupIds"/> 同义)</summary> | ||
| 47 | + public List<string> RegionIds { get; set; } = new(); | ||
| 48 | + | ||
| 41 | /// <summary> | 49 | /// <summary> |
| 42 | - /// 适用门店 Id 列表(来自 fl_location_product) | 50 | + /// 适用门店 Id 列表(来自 fl_location_product;覆盖 Region 全集时可折叠为 <c>["ALL"]</c>) |
| 43 | /// </summary> | 51 | /// </summary> |
| 44 | public List<string> LocationIds { get; set; } = new(); | 52 | public List<string> LocationIds { get; set; } = new(); |
| 45 | } | 53 | } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryCreateInputVo.cs
| 1 | namespace FoodLabeling.Application.Contracts.Dtos.ProductCategory; | 1 | namespace FoodLabeling.Application.Contracts.Dtos.ProductCategory; |
| 2 | 2 | ||
| 3 | +using FoodLabeling.Application.Contracts.Dtos.Common; | ||
| 4 | + | ||
| 3 | /// <summary> | 5 | /// <summary> |
| 4 | /// 产品模块:新增类别入参 | 6 | /// 产品模块:新增类别入参 |
| 5 | /// </summary> | 7 | /// </summary> |
| 6 | -public class ProductCategoryCreateInputVo | 8 | +public class ProductCategoryCreateInputVo : ILabelEntityPartnerScopeInput |
| 7 | { | 9 | { |
| 8 | - /// <summary> | ||
| 9 | - /// 类别编码(可选,不传或空字符串表示无编码) | ||
| 10 | - /// </summary> | 10 | + /// <summary>类别编码(可选;未传或空字符串时由后端自动生成唯一编码)</summary> |
| 11 | public string? CategoryCode { get; set; } | 11 | public string? CategoryCode { get; set; } |
| 12 | 12 | ||
| 13 | public string CategoryName { get; set; } = string.Empty; | 13 | public string CategoryName { get; set; } = string.Empty; |
| @@ -30,25 +30,40 @@ public class ProductCategoryCreateInputVo | @@ -30,25 +30,40 @@ public class ProductCategoryCreateInputVo | ||
| 30 | public bool State { get; set; } = true; | 30 | public bool State { get; set; } = true; |
| 31 | 31 | ||
| 32 | /// <summary> | 32 | /// <summary> |
| 33 | - /// 门店可用范围:ALL / SPECIFIED | 33 | + /// 适用 Company:ALL / SPECIFIED。 |
| 34 | + /// <c>partnerIds</c>/<c>companyIds</c> 可传哨兵 <c>["ALL"]</c>(大小写不敏感)。 | ||
| 35 | + /// </summary> | ||
| 36 | + public string? AppliedPartnerType { get; set; } | ||
| 37 | + | ||
| 38 | + /// <summary>适用 Company(<c>fl_partner.Id</c>)</summary> | ||
| 39 | + public List<string>? PartnerIds { get; set; } | ||
| 40 | + | ||
| 41 | + /// <summary>与 <see cref="PartnerIds"/> 相同(兼容字段)</summary> | ||
| 42 | + public List<string>? CompanyIds { get; set; } | ||
| 43 | + | ||
| 44 | + /// <summary> | ||
| 45 | + /// 门店可用范围:ALL / SPECIFIED。 | ||
| 46 | + /// <c>regionIds</c>/<c>groupIds</c>/<c>locationIds</c> 可传哨兵 <c>["ALL"]</c>(大小写不敏感); | ||
| 47 | + /// 即使本字段为 SPECIFIED,Id 数组为 ALL 时仍归档为 ALL(POST/PUT 均支持)。 | ||
| 34 | /// </summary> | 48 | /// </summary> |
| 35 | public string AvailabilityType { get; set; } = "ALL"; | 49 | public string AvailabilityType { get; set; } = "ALL"; |
| 36 | 50 | ||
| 37 | /// <summary> | 51 | /// <summary> |
| 38 | - /// 适用 Region(**多选**),<c>fl_group.Id</c> 数组;与 <see cref="GroupIds"/> 等价,推荐本字段。 | 52 | + /// 适用 Region(**多选**),<c>fl_group.Id</c>;可含 <c>ALL</c>;与 <see cref="GroupIds"/> 合并去重。 |
| 39 | /// </summary> | 53 | /// </summary> |
| 40 | public List<string>? RegionIds { get; set; } | 54 | public List<string>? RegionIds { get; set; } |
| 41 | 55 | ||
| 42 | /// <summary> | 56 | /// <summary> |
| 43 | - /// 适用 Region(多选),与 <see cref="RegionIds"/> 相同;传任一会合并去重。 | 57 | + /// 适用 Region(多选),与 <see cref="RegionIds"/> 相同;可含 <c>ALL</c>。 |
| 44 | /// </summary> | 58 | /// </summary> |
| 45 | public List<string>? GroupIds { get; set; } | 59 | public List<string>? GroupIds { get; set; } |
| 46 | 60 | ||
| 47 | /// <summary> | 61 | /// <summary> |
| 48 | - /// 适用门店(**多选**),<c>location.Id</c> 数组;与 Region 合并后写入 <c>fl_product_category_location</c>。 | 62 | + /// 适用门店(**多选**),<c>location.Id</c>;可含 <c>ALL</c>;与 Region 合并后写入 <c>fl_product_category_location</c>。 |
| 49 | /// </summary> | 63 | /// </summary> |
| 50 | public List<string>? LocationIds { get; set; } | 64 | public List<string>? LocationIds { get; set; } |
| 51 | 65 | ||
| 52 | - public int OrderNum { get; set; } = 0; | 66 | + /// <summary>排序;未传或 null 时默认 0</summary> |
| 67 | + public int? OrderNum { get; set; } | ||
| 53 | } | 68 | } |
| 54 | 69 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListOutputDto.cs
| @@ -21,6 +21,16 @@ public class ProductCategoryGetListOutputDto | @@ -21,6 +21,16 @@ public class ProductCategoryGetListOutputDto | ||
| 21 | 21 | ||
| 22 | public string AvailabilityType { get; set; } = "ALL"; | 22 | public string AvailabilityType { get; set; } = "ALL"; |
| 23 | 23 | ||
| 24 | + public string AppliedPartnerType { get; set; } = "ALL"; | ||
| 25 | + | ||
| 26 | + /// <summary>适用 Company 展示</summary> | ||
| 27 | + public string Company { get; set; } = string.Empty; | ||
| 28 | + | ||
| 29 | + public List<string> PartnerIds { get; set; } = new(); | ||
| 30 | + | ||
| 31 | + /// <summary>与 <see cref="PartnerIds"/> 相同</summary> | ||
| 32 | + public List<string> CompanyIds { get; set; } = new(); | ||
| 33 | + | ||
| 24 | public int OrderNum { get; set; } | 34 | public int OrderNum { get; set; } |
| 25 | 35 | ||
| 26 | public DateTime LastEdited { get; set; } | 36 | public DateTime LastEdited { get; set; } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs
| @@ -22,6 +22,16 @@ public class ProductCategoryGetOutputDto | @@ -22,6 +22,16 @@ public class ProductCategoryGetOutputDto | ||
| 22 | 22 | ||
| 23 | public string AvailabilityType { get; set; } = "ALL"; | 23 | public string AvailabilityType { get; set; } = "ALL"; |
| 24 | 24 | ||
| 25 | + public string AppliedPartnerType { get; set; } = "ALL"; | ||
| 26 | + | ||
| 27 | + /// <summary>列表/详情 Company 展示</summary> | ||
| 28 | + public string Company { get; set; } = string.Empty; | ||
| 29 | + | ||
| 30 | + public List<string> PartnerIds { get; set; } = new(); | ||
| 31 | + | ||
| 32 | + /// <summary>与 <see cref="PartnerIds"/> 相同</summary> | ||
| 33 | + public List<string> CompanyIds { get; set; } = new(); | ||
| 34 | + | ||
| 25 | /// <summary>适用 Region Id 列表(多选,<c>fl_group.Id</c>;由绑定门店反推)</summary> | 35 | /// <summary>适用 Region Id 列表(多选,<c>fl_group.Id</c>;由绑定门店反推)</summary> |
| 26 | public List<string> RegionIds { get; set; } = new(); | 36 | public List<string> RegionIds { get; set; } = new(); |
| 27 | 37 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductLocation/ProductLocationGetListInputVo.cs
| @@ -9,6 +9,12 @@ namespace FoodLabeling.Application.Contracts.Dtos.ProductLocation; | @@ -9,6 +9,12 @@ namespace FoodLabeling.Application.Contracts.Dtos.ProductLocation; | ||
| 9 | public class ProductLocationGetListInputVo : PagedAndSortedResultRequestDto | 9 | public class ProductLocationGetListInputVo : PagedAndSortedResultRequestDto |
| 10 | { | 10 | { |
| 11 | /// <summary> | 11 | /// <summary> |
| 12 | + /// Company Id(<c>fl_partner.Id</c>);未传 <see cref="LocationId"/> 时按该公司下门店展开, | ||
| 13 | + /// 并包含 <c>AvailabilityType=ALL</c> 的产品。 | ||
| 14 | + /// </summary> | ||
| 15 | + public string? PartnerId { get; set; } | ||
| 16 | + | ||
| 17 | + /// <summary> | ||
| 12 | /// 门店Id(location.Id,string 表示) | 18 | /// 门店Id(location.Id,string 表示) |
| 13 | /// </summary> | 19 | /// </summary> |
| 14 | public string? LocationId { get; set; } | 20 | public string? LocationId { get; set; } |
| @@ -18,4 +24,3 @@ public class ProductLocationGetListInputVo : PagedAndSortedResultRequestDto | @@ -18,4 +24,3 @@ public class ProductLocationGetListInputVo : PagedAndSortedResultRequestDto | ||
| 18 | /// </summary> | 24 | /// </summary> |
| 19 | public string? ProductId { get; set; } | 25 | public string? ProductId { get; set; } |
| 20 | } | 26 | } |
| 21 | - |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleCreateInputVo.cs
| @@ -24,11 +24,16 @@ public class RbacRoleCreateInputVo | @@ -24,11 +24,16 @@ public class RbacRoleCreateInputVo | ||
| 24 | public int? OrderNum { get; set; } | 24 | public int? OrderNum { get; set; } |
| 25 | 25 | ||
| 26 | /// <summary> | 26 | /// <summary> |
| 27 | - /// 绑定菜单 Id;与 accessPermissions 同时传时以本字段为准 | 27 | + /// 绑定菜单 Id;与 menuPermissionKeys / accessPermissions 同时传时以本字段为准 |
| 28 | /// </summary> | 28 | /// </summary> |
| 29 | public List<Guid>? MenuIds { get; set; } | 29 | public List<Guid>? MenuIds { get; set; } |
| 30 | 30 | ||
| 31 | /// <summary> | 31 | /// <summary> |
| 32 | + /// 绑定菜单 Id(字符串 Guid 列表);与 <see cref="MenuIds"/> 等价,<see cref="MenuIds"/> 优先 | ||
| 33 | + /// </summary> | ||
| 34 | + public List<string>? MenuPermissionKeys { get; set; } | ||
| 35 | + | ||
| 36 | + /// <summary> | ||
| 32 | /// 按 PermissionCode 绑定菜单:JSON 数组字符串(如 <c>["manage_labels"]</c>)、英文逗号分隔;传空字符串表示清空绑定;不传则不修改已有绑定(仅编辑时) | 37 | /// 按 PermissionCode 绑定菜单:JSON 数组字符串(如 <c>["manage_labels"]</c>)、英文逗号分隔;传空字符串表示清空绑定;不传则不修改已有绑定(仅编辑时) |
| 33 | /// </summary> | 38 | /// </summary> |
| 34 | public string? AccessPermissions { get; set; } | 39 | public string? AccessPermissions { get; set; } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleGetListOutputDto.cs
| @@ -28,5 +28,10 @@ public class RbacRoleGetListOutputDto | @@ -28,5 +28,10 @@ public class RbacRoleGetListOutputDto | ||
| 28 | /// 角色访问权限码列表(与库字段 AccessPermissionCodes 对应) | 28 | /// 角色访问权限码列表(与库字段 AccessPermissionCodes 对应) |
| 29 | /// </summary> | 29 | /// </summary> |
| 30 | public List<string> AccessPermissionCodes { get; set; } = new(); | 30 | public List<string> AccessPermissionCodes { get; set; } = new(); |
| 31 | + | ||
| 32 | + /// <summary> | ||
| 33 | + /// 已绑定菜单 Id 列表(字符串 Guid,与前端 menuPermissionKeys 字段一致) | ||
| 34 | + /// </summary> | ||
| 35 | + public List<string> MenuPermissionKeys { get; set; } = new(); | ||
| 31 | } | 36 | } |
| 32 | 37 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberBatchImportOnlineInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.TeamMember; | ||
| 2 | + | ||
| 3 | +/// <summary> | ||
| 4 | +/// 成员 JSON 在线批量导入请求体 | ||
| 5 | +/// </summary> | ||
| 6 | +public class TeamMemberBatchImportOnlineInputVo | ||
| 7 | +{ | ||
| 8 | + /// <summary> | ||
| 9 | + /// 待导入行,每元素与单条 <c>POST /api/app/team-member</c> 的 <see cref="TeamMemberCreateInputVo"/> 一致 | ||
| 10 | + /// </summary> | ||
| 11 | + public List<TeamMemberCreateInputVo> Items { get; set; } = new(); | ||
| 12 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberBatchImportOnlineResultDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.TeamMember; | ||
| 2 | + | ||
| 3 | +/// <summary> | ||
| 4 | +/// 成员 JSON 在线批量导入结果 | ||
| 5 | +/// </summary> | ||
| 6 | +public class TeamMemberBatchImportOnlineResultDto | ||
| 7 | +{ | ||
| 8 | + public int SuccessCount { get; set; } | ||
| 9 | + | ||
| 10 | + public int FailCount { get; set; } | ||
| 11 | + | ||
| 12 | + public List<TeamMemberBatchImportOnlineErrorDto> Errors { get; set; } = new(); | ||
| 13 | +} | ||
| 14 | + | ||
| 15 | +/// <summary> | ||
| 16 | +/// 成员 JSON 在线批量导入单条失败信息 | ||
| 17 | +/// </summary> | ||
| 18 | +public class TeamMemberBatchImportOnlineErrorDto | ||
| 19 | +{ | ||
| 20 | + /// <summary> | ||
| 21 | + /// 在请求 <c>items</c> 数组中的序号(从 0 开始) | ||
| 22 | + /// </summary> | ||
| 23 | + public int Index { get; set; } | ||
| 24 | + | ||
| 25 | + /// <summary> | ||
| 26 | + /// 登录账号(<c>userName</c>),便于定位失败行 | ||
| 27 | + /// </summary> | ||
| 28 | + public string? UserName { get; set; } | ||
| 29 | + | ||
| 30 | + public string Message { get; set; } = string.Empty; | ||
| 31 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs
| @@ -28,20 +28,28 @@ public class TeamMemberCreateInputVo | @@ -28,20 +28,28 @@ public class TeamMemberCreateInputVo | ||
| 28 | public List<string>? PartnerIds { get; set; } | 28 | public List<string>? PartnerIds { get; set; } |
| 29 | 29 | ||
| 30 | /// <summary> | 30 | /// <summary> |
| 31 | - /// 适用 Region 多选(<c>fl_group.Id</c>);Company Admin 仅传 Company 时可省略,后端自动绑定该公司下全部 Region 与门店 | 31 | + /// 适用 Region 多选(<c>fl_group.Id</c>);可含 <c>ALL</c> 哨兵(大小写不敏感,与具体 Guid 同传时以 ALL 为准)。 |
| 32 | + /// Company Admin 仅传 Company 时可省略;<c>locationIds</c> 为空且含 ALL 时展开该公司全部 Region 下门店。 | ||
| 32 | /// </summary> | 33 | /// </summary> |
| 33 | public List<string>? RegionIds { get; set; } | 34 | public List<string>? RegionIds { get; set; } |
| 34 | 35 | ||
| 35 | /// <summary> | 36 | /// <summary> |
| 36 | - /// 适用 Region 多选(与 <see cref="RegionIds"/> 相同) | 37 | + /// 适用 Region 多选(与 <see cref="RegionIds"/> 相同);可含 <c>ALL</c>。 |
| 37 | /// </summary> | 38 | /// </summary> |
| 38 | public List<string>? GroupIds { get; set; } | 39 | public List<string>? GroupIds { get; set; } |
| 39 | 40 | ||
| 40 | /// <summary> | 41 | /// <summary> |
| 41 | - /// 适用门店多选(<c>location.Id</c>);Company Admin 仅传 Company 时可省略,与 Region 合并后写入 <c>userlocation</c> | 42 | + /// 适用门店多选(<c>location.Id</c>);可含 <c>ALL</c> 哨兵。 |
| 43 | + /// 有具体 Region 时展开该 Region 下门店;仅有 Company 时展开该公司全部门店。 | ||
| 44 | + /// Company Admin 仅传 Company 时可省略。 | ||
| 42 | /// </summary> | 45 | /// </summary> |
| 43 | public List<string>? LocationIds { get; set; } | 46 | public List<string>? LocationIds { get; set; } |
| 44 | 47 | ||
| 48 | + /// <summary> | ||
| 49 | + /// 适用门店多选别名(与 <see cref="LocationIds"/> 合并解析);可含 <c>ALL</c> 或门店 Guid。 | ||
| 50 | + /// </summary> | ||
| 51 | + public List<string>? Locations { get; set; } | ||
| 52 | + | ||
| 45 | public bool State { get; set; } = true; | 53 | public bool State { get; set; } = true; |
| 46 | } | 54 | } |
| 47 | 55 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetOutputDto.cs
| @@ -19,12 +19,13 @@ public class TeamMemberGetOutputDto | @@ -19,12 +19,13 @@ public class TeamMemberGetOutputDto | ||
| 19 | /// <summary>适用 Company Id(多选,由绑定门店反推)</summary> | 19 | /// <summary>适用 Company Id(多选,由绑定门店反推)</summary> |
| 20 | public List<string> PartnerIds { get; set; } = new(); | 20 | public List<string> PartnerIds { get; set; } = new(); |
| 21 | 21 | ||
| 22 | - /// <summary>适用 Region Id(多选,<c>fl_group.Id</c>)</summary> | 22 | + /// <summary>适用 Region Id(多选,<c>fl_group.Id</c>;覆盖该公司全部 Region 时回显 <c>["ALL"]</c>)</summary> |
| 23 | public List<string> RegionIds { get; set; } = new(); | 23 | public List<string> RegionIds { get; set; } = new(); |
| 24 | 24 | ||
| 25 | /// <summary>与 <see cref="RegionIds"/> 相同</summary> | 25 | /// <summary>与 <see cref="RegionIds"/> 相同</summary> |
| 26 | public List<string> GroupIds { get; set; } = new(); | 26 | public List<string> GroupIds { get; set; } = new(); |
| 27 | 27 | ||
| 28 | + /// <summary>绑定门店 Id;覆盖该公司全部门店时回显 <c>["ALL"]</c>(库中仍存展开 Guid)</summary> | ||
| 28 | public List<string> LocationIds { get; set; } = new(); | 29 | public List<string> LocationIds { get; set; } = new(); |
| 29 | 30 | ||
| 30 | public List<TeamMemberAssignedLocationDto> AssignedLocations { get; set; } = new(); | 31 | 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
| @@ -28,20 +28,26 @@ public class TeamMemberUpdateInputVo | @@ -28,20 +28,26 @@ public class TeamMemberUpdateInputVo | ||
| 28 | public List<string>? PartnerIds { get; set; } | 28 | public List<string>? PartnerIds { get; set; } |
| 29 | 29 | ||
| 30 | /// <summary> | 30 | /// <summary> |
| 31 | - /// 适用 Region 多选(<c>fl_group.Id</c>);Company Admin 仅传 Company 时可省略 | 31 | + /// 适用 Region 多选(<c>fl_group.Id</c>);可含 <c>ALL</c> 哨兵(大小写不敏感,与具体 Guid 同传时以 ALL 为准)。 |
| 32 | /// </summary> | 32 | /// </summary> |
| 33 | public List<string>? RegionIds { get; set; } | 33 | public List<string>? RegionIds { get; set; } |
| 34 | 34 | ||
| 35 | /// <summary> | 35 | /// <summary> |
| 36 | - /// 适用 Region 多选(与 <see cref="RegionIds"/> 相同) | 36 | + /// 适用 Region 多选(与 <see cref="RegionIds"/> 相同);可含 <c>ALL</c>。 |
| 37 | /// </summary> | 37 | /// </summary> |
| 38 | public List<string>? GroupIds { get; set; } | 38 | public List<string>? GroupIds { get; set; } |
| 39 | 39 | ||
| 40 | /// <summary> | 40 | /// <summary> |
| 41 | - /// 适用门店多选(<c>location.Id</c>);Company Admin 仅传 Company 时可省略 | 41 | + /// 适用门店多选(<c>location.Id</c>);可含 <c>ALL</c> 哨兵。 |
| 42 | + /// 有具体 Region 时表示该 Region 下全部门店;仅有 Company 时按公司全部门店落库。 | ||
| 42 | /// </summary> | 43 | /// </summary> |
| 43 | public List<string>? LocationIds { get; set; } | 44 | public List<string>? LocationIds { get; set; } |
| 44 | 45 | ||
| 46 | + /// <summary> | ||
| 47 | + /// 适用门店多选别名(与 <see cref="LocationIds"/> 合并解析);可含 <c>ALL</c> 或门店 Guid。 | ||
| 48 | + /// </summary> | ||
| 49 | + public List<string>? Locations { get; set; } | ||
| 50 | + | ||
| 45 | public bool State { get; set; } = true; | 51 | public bool State { get; set; } = true; |
| 46 | } | 52 | } |
| 47 | 53 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingCategoryCreateInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Training; | ||
| 2 | + | ||
| 3 | +public class TrainingCategoryCreateInputVo | ||
| 4 | +{ | ||
| 5 | + public string CategoryName { get; set; } = string.Empty; | ||
| 6 | + | ||
| 7 | + /// <summary>空=一级分类;有值=二级分类</summary> | ||
| 8 | + public string? ParentId { get; set; } | ||
| 9 | + | ||
| 10 | + public int OrderNum { get; set; } | ||
| 11 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingCategoryGetOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Training; | ||
| 2 | + | ||
| 3 | +public class TrainingCategoryGetOutputDto | ||
| 4 | +{ | ||
| 5 | + public string Id { get; set; } = string.Empty; | ||
| 6 | + | ||
| 7 | + public string CategoryName { get; set; } = string.Empty; | ||
| 8 | + | ||
| 9 | + public string? ParentId { get; set; } | ||
| 10 | + | ||
| 11 | + public int OrderNum { get; set; } | ||
| 12 | + | ||
| 13 | + public DateTime CreationTime { get; set; } | ||
| 14 | + | ||
| 15 | + public DateTime? LastModificationTime { get; set; } | ||
| 16 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingCategoryTreeInputVo.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Training; | ||
| 2 | + | ||
| 3 | +public class TrainingCategoryTreeInputVo | ||
| 4 | +{ | ||
| 5 | + /// <summary>关键字(匹配分类名或文件名)</summary> | ||
| 6 | + public string? Keyword { get; set; } | ||
| 7 | + | ||
| 8 | + /// <summary>按门店筛选可见文件;不传则不过滤文件权限</summary> | ||
| 9 | + public string? LocationId { get; set; } | ||
| 10 | + | ||
| 11 | + /// <summary>是否包含文件列表,默认 true</summary> | ||
| 12 | + public bool IncludeFiles { get; set; } = true; | ||
| 13 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingCategoryTreeNodeDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Training; | ||
| 2 | + | ||
| 3 | +public class TrainingCategoryTreeNodeDto | ||
| 4 | +{ | ||
| 5 | + public string Id { get; set; } = string.Empty; | ||
| 6 | + | ||
| 7 | + public string CategoryName { get; set; } = string.Empty; | ||
| 8 | + | ||
| 9 | + public string? ParentId { get; set; } | ||
| 10 | + | ||
| 11 | + public int OrderNum { get; set; } | ||
| 12 | + | ||
| 13 | + public List<TrainingCategoryTreeNodeDto> Children { get; set; } = new(); | ||
| 14 | + | ||
| 15 | + public List<TrainingFileDto> Files { get; set; } = new(); | ||
| 16 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingCategoryUpdateInputVo.cs
0 → 100644
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Training; | ||
| 2 | + | ||
| 3 | +public class TrainingFileDto | ||
| 4 | +{ | ||
| 5 | + public string Id { get; set; } = string.Empty; | ||
| 6 | + | ||
| 7 | + public string CategoryId { get; set; } = string.Empty; | ||
| 8 | + | ||
| 9 | + public string FileName { get; set; } = string.Empty; | ||
| 10 | + | ||
| 11 | + public string FileUrl { get; set; } = string.Empty; | ||
| 12 | + | ||
| 13 | + /// <summary>image / doc / other</summary> | ||
| 14 | + public string FileType { get; set; } = "other"; | ||
| 15 | + | ||
| 16 | + public long FileSize { get; set; } | ||
| 17 | + | ||
| 18 | + public int OrderNum { get; set; } | ||
| 19 | + | ||
| 20 | + public string AppliedPartnerType { get; set; } = "ALL"; | ||
| 21 | + | ||
| 22 | + /// <summary>Company 展示(ALL 时为 All Companies)</summary> | ||
| 23 | + public string Company { get; set; } = string.Empty; | ||
| 24 | + | ||
| 25 | + public List<string> PartnerIds { get; set; } = new(); | ||
| 26 | + | ||
| 27 | + /// <summary>与 <see cref="PartnerIds"/> 相同</summary> | ||
| 28 | + public List<string> CompanyIds { get; set; } = new(); | ||
| 29 | + | ||
| 30 | + public string AppliedRegionType { get; set; } = "ALL"; | ||
| 31 | + | ||
| 32 | + /// <summary>Region 展示</summary> | ||
| 33 | + public string Region { get; set; } = string.Empty; | ||
| 34 | + | ||
| 35 | + public List<string> RegionIds { get; set; } = new(); | ||
| 36 | + | ||
| 37 | + /// <summary>与 <see cref="RegionIds"/> 相同</summary> | ||
| 38 | + public List<string> GroupIds { get; set; } = new(); | ||
| 39 | + | ||
| 40 | + public string AvailabilityType { get; set; } = "ALL"; | ||
| 41 | + | ||
| 42 | + /// <summary>Location 展示</summary> | ||
| 43 | + public string Location { get; set; } = string.Empty; | ||
| 44 | + | ||
| 45 | + public List<string> LocationIds { get; set; } = new(); | ||
| 46 | + | ||
| 47 | + public DateTime CreationTime { get; set; } | ||
| 48 | + | ||
| 49 | + public DateTime? LastModificationTime { get; set; } | ||
| 50 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileScopeInputVo.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Common; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Contracts.Dtos.Training; | ||
| 4 | + | ||
| 5 | +public class TrainingFileScopeInputVo : ITrainingFileScopeInput | ||
| 6 | +{ | ||
| 7 | + public string? AppliedPartnerType { get; set; } | ||
| 8 | + | ||
| 9 | + public List<string>? PartnerIds { get; set; } | ||
| 10 | + | ||
| 11 | + public List<string>? CompanyIds { get; set; } | ||
| 12 | + | ||
| 13 | + /// <summary>适用 Region:ALL / SPECIFIED</summary> | ||
| 14 | + public string? AppliedRegionType { get; set; } | ||
| 15 | + | ||
| 16 | + public List<string>? RegionIds { get; set; } | ||
| 17 | + | ||
| 18 | + public List<string>? GroupIds { get; set; } | ||
| 19 | + | ||
| 20 | + /// <summary>适用 Location:ALL / SPECIFIED</summary> | ||
| 21 | + public string? AvailabilityType { get; set; } | ||
| 22 | + | ||
| 23 | + /// <summary>适用 Location:ALL / SPECIFIED(<see cref="AvailabilityType"/> 别名)</summary> | ||
| 24 | + public string? AppliedLocationType { get; set; } | ||
| 25 | + | ||
| 26 | + public List<string>? LocationIds { get; set; } | ||
| 27 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileScopeOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Training; | ||
| 2 | + | ||
| 3 | +public class TrainingFileScopeOutputDto | ||
| 4 | +{ | ||
| 5 | + public string AppliedPartnerType { get; set; } = "ALL"; | ||
| 6 | + | ||
| 7 | + public string Company { get; set; } = string.Empty; | ||
| 8 | + | ||
| 9 | + public List<string> PartnerIds { get; set; } = new(); | ||
| 10 | + | ||
| 11 | + public List<string> CompanyIds { get; set; } = new(); | ||
| 12 | + | ||
| 13 | + public string AppliedRegionType { get; set; } = "ALL"; | ||
| 14 | + | ||
| 15 | + public string Region { get; set; } = string.Empty; | ||
| 16 | + | ||
| 17 | + public List<string> RegionIds { get; set; } = new(); | ||
| 18 | + | ||
| 19 | + public List<string> GroupIds { get; set; } = new(); | ||
| 20 | + | ||
| 21 | + public string AvailabilityType { get; set; } = "ALL"; | ||
| 22 | + | ||
| 23 | + public string Location { get; set; } = string.Empty; | ||
| 24 | + | ||
| 25 | + public List<string> LocationIds { get; set; } = new(); | ||
| 26 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileSortInputVo.cs
0 → 100644
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileSortItemVo.cs
0 → 100644
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileUpdateInputVo.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Common; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Contracts.Dtos.Training; | ||
| 4 | + | ||
| 5 | +public class TrainingFileUpdateInputVo : ITrainingFileScopeInput | ||
| 6 | +{ | ||
| 7 | + public string FileName { get; set; } = string.Empty; | ||
| 8 | + | ||
| 9 | + public int OrderNum { get; set; } | ||
| 10 | + | ||
| 11 | + /// <summary>适用 Company:ALL / SPECIFIED</summary> | ||
| 12 | + public string? AppliedPartnerType { get; set; } | ||
| 13 | + | ||
| 14 | + /// <summary>适用 Company(<c>fl_partner.Id</c>);可含 <c>ALL</c></summary> | ||
| 15 | + public List<string>? PartnerIds { get; set; } | ||
| 16 | + | ||
| 17 | + /// <summary>与 <see cref="PartnerIds"/> 相同;可含 <c>ALL</c></summary> | ||
| 18 | + public List<string>? CompanyIds { get; set; } | ||
| 19 | + | ||
| 20 | + /// <summary>适用 Region:ALL / SPECIFIED</summary> | ||
| 21 | + public string? AppliedRegionType { get; set; } | ||
| 22 | + | ||
| 23 | + /// <summary>适用 Region(<c>fl_group.Id</c>);与 <see cref="GroupIds"/> 合并;可含 <c>ALL</c></summary> | ||
| 24 | + public List<string>? RegionIds { get; set; } | ||
| 25 | + | ||
| 26 | + /// <summary>与 <see cref="RegionIds"/> 相同;可含 <c>ALL</c></summary> | ||
| 27 | + public List<string>? GroupIds { get; set; } | ||
| 28 | + | ||
| 29 | + /// <summary>适用 Location:ALL / SPECIFIED</summary> | ||
| 30 | + public string? AvailabilityType { get; set; } | ||
| 31 | + | ||
| 32 | + /// <summary>适用 Location:ALL / SPECIFIED(<see cref="AvailabilityType"/> 别名)</summary> | ||
| 33 | + public string? AppliedLocationType { get; set; } | ||
| 34 | + | ||
| 35 | + /// <summary>适用门店(<c>location.Id</c>);可含 <c>ALL</c></summary> | ||
| 36 | + public List<string>? LocationIds { get; set; } | ||
| 37 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileUploadInputVo.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Common; | ||
| 2 | +using Microsoft.AspNetCore.Http; | ||
| 3 | +using Microsoft.AspNetCore.Mvc; | ||
| 4 | + | ||
| 5 | +namespace FoodLabeling.Application.Contracts.Dtos.Training; | ||
| 6 | + | ||
| 7 | +public class TrainingFileUploadInputVo : ITrainingFileScopeInput | ||
| 8 | +{ | ||
| 9 | + [FromForm(Name = "file")] | ||
| 10 | + public IFormFile File { get; set; } = default!; | ||
| 11 | + | ||
| 12 | + [FromForm(Name = "categoryId")] | ||
| 13 | + public string CategoryId { get; set; } = string.Empty; | ||
| 14 | + | ||
| 15 | + [FromForm(Name = "orderNum")] | ||
| 16 | + public int OrderNum { get; set; } | ||
| 17 | + | ||
| 18 | + /// <summary>适用 Company:ALL / SPECIFIED</summary> | ||
| 19 | + [FromForm(Name = "appliedPartnerType")] | ||
| 20 | + public string? AppliedPartnerType { get; set; } | ||
| 21 | + | ||
| 22 | + /// <summary>适用 Company(<c>fl_partner.Id</c>);可含 <c>ALL</c></summary> | ||
| 23 | + [FromForm(Name = "partnerIds")] | ||
| 24 | + public List<string>? PartnerIds { get; set; } | ||
| 25 | + | ||
| 26 | + /// <summary>与 <see cref="PartnerIds"/> 相同;可含 <c>ALL</c></summary> | ||
| 27 | + [FromForm(Name = "companyIds")] | ||
| 28 | + public List<string>? CompanyIds { get; set; } | ||
| 29 | + | ||
| 30 | + /// <summary>适用 Region:ALL / SPECIFIED</summary> | ||
| 31 | + [FromForm(Name = "appliedRegionType")] | ||
| 32 | + public string? AppliedRegionType { get; set; } | ||
| 33 | + | ||
| 34 | + /// <summary>适用 Region(<c>fl_group.Id</c>);与 <see cref="GroupIds"/> 合并;可含 <c>ALL</c></summary> | ||
| 35 | + [FromForm(Name = "regionIds")] | ||
| 36 | + public List<string>? RegionIds { get; set; } | ||
| 37 | + | ||
| 38 | + /// <summary>与 <see cref="RegionIds"/> 相同;可含 <c>ALL</c></summary> | ||
| 39 | + [FromForm(Name = "groupIds")] | ||
| 40 | + public List<string>? GroupIds { get; set; } | ||
| 41 | + | ||
| 42 | + /// <summary>适用 Location:ALL / SPECIFIED</summary> | ||
| 43 | + [FromForm(Name = "availabilityType")] | ||
| 44 | + public string? AvailabilityType { get; set; } | ||
| 45 | + | ||
| 46 | + /// <summary>适用 Location:ALL / SPECIFIED(<see cref="AvailabilityType"/> 别名)</summary> | ||
| 47 | + [FromForm(Name = "appliedLocationType")] | ||
| 48 | + public string? AppliedLocationType { get; set; } | ||
| 49 | + | ||
| 50 | + /// <summary>适用门店(<c>location.Id</c>);可含 <c>ALL</c></summary> | ||
| 51 | + [FromForm(Name = "locationIds")] | ||
| 52 | + public List<string>? LocationIds { get; set; } | ||
| 53 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/TrainingFileUploadOutputDto.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Training; | ||
| 2 | + | ||
| 3 | +public class TrainingFileUploadOutputDto | ||
| 4 | +{ | ||
| 5 | + public string Id { get; set; } = string.Empty; | ||
| 6 | + | ||
| 7 | + public string FileName { get; set; } = string.Empty; | ||
| 8 | + | ||
| 9 | + public string FileUrl { get; set; } = string.Empty; | ||
| 10 | + | ||
| 11 | + public string FileType { get; set; } = "other"; | ||
| 12 | + | ||
| 13 | + public long FileSize { get; set; } | ||
| 14 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Training/UsAppTrainingTreeInputVo.cs
0 → 100644
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IAuthSessionAppService.cs
| @@ -12,8 +12,11 @@ public interface IAuthSessionAppService : IApplicationService | @@ -12,8 +12,11 @@ public interface IAuthSessionAppService : IApplicationService | ||
| 12 | /// 获取当前登录用户的角色编码、权限码与可见菜单树 | 12 | /// 获取当前登录用户的角色编码、权限码与可见菜单树 |
| 13 | /// </summary> | 13 | /// </summary> |
| 14 | /// <remarks> | 14 | /// <remarks> |
| 15 | - /// 与框架 <c>UserManager.GetInfoAsync</c> 一致;用户名为 <c>admin</c> 时返回全部未删除菜单(与 <c>AccountService.GetVue3Router</c> 行为对齐)。 | ||
| 16 | - /// 返回体额外包含:<c>lastUpdated</c>(系统编辑全局时间戳,与任意写接口成功联动;无戳时回退用户 LastModificationTime)、<c>role</c>(角色展示名,多角色英文逗号拼接)、<c>fullName</c>(姓名优先,其次昵称、用户名)。 | 15 | + /// 非 SaaS(<c>EnabledSaasMultiTenancy=false</c>)且用户名为 <c>admin</c> 时返回全部未删除菜单(与 <c>AccountService.GetVue3Router</c> 行为对齐)。 |
| 16 | + /// SaaS 开启时:平台主库登录(JWT / <c>__tenant</c> 无业务租户 Id,含全 0)即使用户名为 <c>admin</c> 也仅返回平台菜单 | ||
| 17 | + /// (<c>PermissionCode</c> 以 <c>menu.platform</c> 开头或 <c>Router</c> 以 <c>/platform</c> 开头,含祖先节点以成树); | ||
| 18 | + /// 公司业务租户登录时即使用户名为 <c>admin</c> 也按 RoleMenu 关联查询,与 <c>UpdateCompanyMenus</c> 同步的管理员菜单权限一致。 | ||
| 19 | + /// 返回体额外包含:<c>userId</c>(当前登录用户 Id,与 <c>user.id</c> 一致,非租户 Id)、<c>lastUpdated</c>(系统编辑全局时间戳,与任意写接口成功联动;无戳时回退用户 LastModificationTime)、<c>role</c>(角色展示名,多角色英文逗号拼接)、<c>fullName</c>(姓名优先,其次昵称、用户名)。 | ||
| 17 | /// 角色名通过 <c>Role</c> 表直查(<c>RoleDbEntity</c>),避免走仓储 IDataPermission。 | 20 | /// 角色名通过 <c>Role</c> 表直查(<c>RoleDbEntity</c>),避免走仓储 IDataPermission。 |
| 18 | /// </remarks> | 21 | /// </remarks> |
| 19 | /// <returns>用户简要信息、权限码与菜单树</returns> | 22 | /// <returns>用户简要信息、权限码与菜单树</returns> |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelAlertTimerAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Common; | ||
| 2 | +using FoodLabeling.Application.Contracts.Dtos.LabelAlertTimer; | ||
| 3 | +using Volo.Abp.Application.Services; | ||
| 4 | + | ||
| 5 | +namespace FoodLabeling.Application.Contracts.IServices; | ||
| 6 | + | ||
| 7 | +/// <summary> | ||
| 8 | +/// 标签告警计时器(App) | ||
| 9 | +/// </summary> | ||
| 10 | +public interface ILabelAlertTimerAppService : IApplicationService | ||
| 11 | +{ | ||
| 12 | + Task<PagedResultWithPageDto<LabelAlertTimerListItemDto>> GetListAsync(LabelAlertTimerGetListInputVo input); | ||
| 13 | + | ||
| 14 | + /// <summary> | ||
| 15 | + /// App:当前账号当前门店告警列表(含倒计时;locationId 可省略,走已选门店缓存) | ||
| 16 | + /// </summary> | ||
| 17 | + Task<PagedResultWithPageDto<LabelAlertTimerListItemDto>> GetAppListAsync(LabelAlertTimerGetListInputVo input); | ||
| 18 | + | ||
| 19 | + Task DeleteAsync(string id); | ||
| 20 | + | ||
| 21 | + Task<LabelAlertTimerCheckExpiredOutputDto> CheckExpiredAsync(LabelAlertTimerCheckExpiredInputVo input); | ||
| 22 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelAppService.cs
| @@ -21,7 +21,8 @@ public interface ILabelAppService : IApplicationService | @@ -21,7 +21,8 @@ public interface ILabelAppService : IApplicationService | ||
| 21 | Task<LabelGetOutputDto> GetAsync(string id); | 21 | Task<LabelGetOutputDto> GetAsync(string id); |
| 22 | 22 | ||
| 23 | /// <summary> | 23 | /// <summary> |
| 24 | - /// 新增标签。Body 支持 <c>appliedRegionType</c>(ALL/SPECIFIED)、<c>regionIds</c> / <c>groupIds</c>(落库 <c>fl_label_region</c>)、<c>locationIds</c>(主字段,落库 <c>fl_label_location</c>);<c>locationId</c> 与 <c>locationIds</c> 合并;<c>labelTypeId</c> 可选。 | 24 | + /// 新增标签。Body 支持 <c>appliedRegionType</c>(ALL/SPECIFIED)、 |
| 25 | + /// <c>regionIds</c> / <c>groupIds</c> / <c>locationIds</c>(可含哨兵 <c>ALL</c>,归档为 AppliedRegionType=ALL)。 | ||
| 25 | /// </summary> | 26 | /// </summary> |
| 26 | Task<LabelGetOutputDto> CreateAsync(LabelCreateInputVo input); | 27 | Task<LabelGetOutputDto> CreateAsync(LabelCreateInputVo input); |
| 27 | 28 | ||
| @@ -31,7 +32,7 @@ public interface ILabelAppService : IApplicationService | @@ -31,7 +32,7 @@ public interface ILabelAppService : IApplicationService | ||
| 31 | Task<LabelBatchCreateResultDto> BatchCreateAsync(LabelBatchCreateInputVo input); | 32 | Task<LabelBatchCreateResultDto> BatchCreateAsync(LabelBatchCreateInputVo input); |
| 32 | 33 | ||
| 33 | /// <summary> | 34 | /// <summary> |
| 34 | - /// 编辑标签(id=LabelCode)。适用 Region / 门店字段同 <see cref="CreateAsync"/>。 | 35 | + /// 编辑标签(id=LabelCode)。范围传参与 <see cref="CreateAsync"/> 一致,支持 Id 数组传 <c>ALL</c>。 |
| 35 | /// </summary> | 36 | /// </summary> |
| 36 | Task<LabelGetOutputDto> UpdateAsync(string id, LabelUpdateInputVo input); | 37 | Task<LabelGetOutputDto> UpdateAsync(string id, LabelUpdateInputVo input); |
| 37 | 38 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelCategoryAppService.cs
| @@ -9,8 +9,10 @@ public interface ILabelCategoryAppService | @@ -9,8 +9,10 @@ public interface ILabelCategoryAppService | ||
| 9 | 9 | ||
| 10 | Task<LabelCategoryGetOutputDto> GetAsync(string id); | 10 | Task<LabelCategoryGetOutputDto> GetAsync(string id); |
| 11 | 11 | ||
| 12 | + /// <summary>新增标签分类;<c>regionIds</c>/<c>groupIds</c>/<c>locationIds</c> 支持哨兵 <c>ALL</c>。</summary> | ||
| 12 | Task<LabelCategoryGetOutputDto> CreateAsync(LabelCategoryCreateInputVo input); | 13 | Task<LabelCategoryGetOutputDto> CreateAsync(LabelCategoryCreateInputVo input); |
| 13 | 14 | ||
| 15 | + /// <summary>编辑标签分类;范围传参与新增一致,支持 <c>ALL</c>。</summary> | ||
| 14 | Task<LabelCategoryGetOutputDto> UpdateAsync(string id, LabelCategoryUpdateInputVo input); | 16 | Task<LabelCategoryGetOutputDto> UpdateAsync(string id, LabelCategoryUpdateInputVo input); |
| 15 | 17 | ||
| 16 | Task DeleteAsync(string id); | 18 | Task DeleteAsync(string id); |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelMultipleOptionAppService.cs
| @@ -9,8 +9,14 @@ public interface ILabelMultipleOptionAppService | @@ -9,8 +9,14 @@ public interface ILabelMultipleOptionAppService | ||
| 9 | 9 | ||
| 10 | Task<LabelMultipleOptionGetOutputDto> GetAsync(string id); | 10 | Task<LabelMultipleOptionGetOutputDto> GetAsync(string id); |
| 11 | 11 | ||
| 12 | + /// <summary> | ||
| 13 | + /// 新增多选项;<c>regionIds</c>、<c>groupIds</c>、<c>locationIds</c> 可传 <c>ALL</c> 哨兵(POST)。 | ||
| 14 | + /// </summary> | ||
| 12 | Task<LabelMultipleOptionGetOutputDto> CreateAsync(LabelMultipleOptionCreateInputVo input); | 15 | Task<LabelMultipleOptionGetOutputDto> CreateAsync(LabelMultipleOptionCreateInputVo input); |
| 13 | 16 | ||
| 17 | + /// <summary> | ||
| 18 | + /// 编辑多选项;适用范围与新增相同,<c>regionIds</c>、<c>groupIds</c>、<c>locationIds</c> 可传 <c>ALL</c>(PUT)。 | ||
| 19 | + /// </summary> | ||
| 14 | Task<LabelMultipleOptionGetOutputDto> UpdateAsync(string id, LabelMultipleOptionUpdateInputVo input); | 20 | Task<LabelMultipleOptionGetOutputDto> UpdateAsync(string id, LabelMultipleOptionUpdateInputVo input); |
| 15 | 21 | ||
| 16 | Task DeleteAsync(string id); | 22 | Task DeleteAsync(string id); |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelTemplateAppService.cs
| @@ -25,12 +25,14 @@ public interface ILabelTemplateAppService : IApplicationService | @@ -25,12 +25,14 @@ public interface ILabelTemplateAppService : IApplicationService | ||
| 25 | 25 | ||
| 26 | /// <summary> | 26 | /// <summary> |
| 27 | /// 新增标签模板;body 支持 Company/Region/Location 三维范围(各维度 ALL/SPECIFIED + Id 数组)。 | 27 | /// 新增标签模板;body 支持 Company/Region/Location 三维范围(各维度 ALL/SPECIFIED + Id 数组)。 |
| 28 | + /// <c>regionIds</c>、<c>groupIds</c>、<c>locationIds</c>、<c>appliedLocationIds</c> 可传 <c>ALL</c> 哨兵(POST)。 | ||
| 28 | /// </summary> | 29 | /// </summary> |
| 29 | Task<LabelTemplateGetOutputDto> CreateAsync(LabelTemplateCreateInputVo input); | 30 | Task<LabelTemplateGetOutputDto> CreateAsync(LabelTemplateCreateInputVo input); |
| 30 | 31 | ||
| 31 | /// <summary> | 32 | /// <summary> |
| 32 | /// 编辑标签模板(版本号 +1,重建 elements);适用范围多选规则同新增。 | 33 | /// 编辑标签模板(版本号 +1,重建 elements);适用范围多选规则同新增。 |
| 33 | /// body 支持 <c>printOrientation</c>(<c>vertical</c> / <c>horizontal</c>,横打不交换 Width/Height)。 | 34 | /// body 支持 <c>printOrientation</c>(<c>vertical</c> / <c>horizontal</c>,横打不交换 Width/Height)。 |
| 35 | + /// <c>regionIds</c>、<c>groupIds</c>、<c>locationIds</c>、<c>appliedLocationIds</c> 可传 <c>ALL</c> 哨兵(PUT)。 | ||
| 34 | /// </summary> | 36 | /// </summary> |
| 35 | Task<LabelTemplateGetOutputDto> UpdateAsync(string id, LabelTemplateUpdateInputVo input); | 37 | Task<LabelTemplateGetOutputDto> UpdateAsync(string id, LabelTemplateUpdateInputVo input); |
| 36 | 38 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelTypeAppService.cs
| @@ -9,8 +9,10 @@ public interface ILabelTypeAppService | @@ -9,8 +9,10 @@ public interface ILabelTypeAppService | ||
| 9 | 9 | ||
| 10 | Task<LabelTypeGetOutputDto> GetAsync(string id); | 10 | Task<LabelTypeGetOutputDto> GetAsync(string id); |
| 11 | 11 | ||
| 12 | + /// <summary>新增标签类型;<c>regionIds</c>/<c>groupIds</c>/<c>locationIds</c> 支持哨兵 <c>ALL</c>。</summary> | ||
| 12 | Task<LabelTypeGetOutputDto> CreateAsync(LabelTypeCreateInputVo input); | 13 | Task<LabelTypeGetOutputDto> CreateAsync(LabelTypeCreateInputVo input); |
| 13 | 14 | ||
| 15 | + /// <summary>编辑标签类型;范围传参与新增一致,支持 <c>ALL</c>。</summary> | ||
| 14 | Task<LabelTypeGetOutputDto> UpdateAsync(string id, LabelTypeUpdateInputVo input); | 16 | Task<LabelTypeGetOutputDto> UpdateAsync(string id, LabelTypeUpdateInputVo input); |
| 15 | 17 | ||
| 16 | Task DeleteAsync(string id); | 18 | Task DeleteAsync(string id); |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs
| @@ -32,8 +32,10 @@ public interface IProductAppService : IApplicationService | @@ -32,8 +32,10 @@ public interface IProductAppService : IApplicationService | ||
| 32 | /// </summary> | 32 | /// </summary> |
| 33 | /// <remarks> | 33 | /// <remarks> |
| 34 | /// <see cref="ProductCreateInputVo.ProductCode"/> 可选;为空时后端生成唯一编码(如 PRD_ + Guid)。 | 34 | /// <see cref="ProductCreateInputVo.ProductCode"/> 可选;为空时后端生成唯一编码(如 PRD_ + Guid)。 |
| 35 | - /// 若传 <see cref="ProductCreateInputVo.PartnerId"/>(Company)、<see cref="ProductCreateInputVo.GroupIds"/>(Region) | ||
| 36 | - /// 和/或 <see cref="ProductCreateInputVo.LocationIds"/>,合并后写入 fl_location_product。 | 35 | + /// Company 传 <see cref="ProductCreateInputVo.CompanyIds"/>(推荐,仅单选具体 Guid,<b>不支持 ALL</b>) |
| 36 | + /// 或 <see cref="ProductCreateInputVo.PartnerId"/>; | ||
| 37 | + /// 再与 <see cref="ProductCreateInputVo.GroupIds"/> / <see cref="ProductCreateInputVo.LocationIds"/> 合并写入 fl_location_product。 | ||
| 38 | + /// Region/Location 数组含 <c>["ALL"]</c> 时,即使 AvailabilityType 为 SPECIFIED 也归档为 ALL,清空 fl_location_product 快照。 | ||
| 37 | /// </remarks> | 39 | /// </remarks> |
| 38 | Task<ProductGetOutputDto> CreateAsync(ProductCreateInputVo input); | 40 | Task<ProductGetOutputDto> CreateAsync(ProductCreateInputVo input); |
| 39 | 41 | ||
| @@ -41,8 +43,8 @@ public interface IProductAppService : IApplicationService | @@ -41,8 +43,8 @@ public interface IProductAppService : IApplicationService | ||
| 41 | /// 编辑产品 | 43 | /// 编辑产品 |
| 42 | /// </summary> | 44 | /// </summary> |
| 43 | /// <remarks> | 45 | /// <remarks> |
| 44 | - /// 当请求体包含 <see cref="ProductCreateInputVo.PartnerId"/>、<see cref="ProductCreateInputVo.GroupIds"/> | ||
| 45 | - /// 和/或 <see cref="ProductCreateInputVo.LocationIds"/> 时,合并后整表替换门店关联;均不传则不改。 | 46 | + /// 当请求体包含 <c>companyIds</c>/<c>partnerId</c>、<c>groupIds</c> 和/或 <c>locationIds</c> 时,合并后整表替换门店关联;均不传则不改。 |
| 47 | + /// Company <b>仅支持单选</b>(<c>companyIds</c> 最多 1 个具体 Guid,<b>不支持 ALL</b>)。Region/Location ALL 哨兵规则同 <see cref="CreateAsync"/>。 | ||
| 46 | /// </remarks> | 48 | /// </remarks> |
| 47 | Task<ProductGetOutputDto> UpdateAsync(Guid id, ProductUpdateInputVo input); | 49 | Task<ProductGetOutputDto> UpdateAsync(Guid id, ProductUpdateInputVo input); |
| 48 | 50 | ||
| @@ -74,5 +76,36 @@ public interface IProductAppService : IApplicationService | @@ -74,5 +76,36 @@ public interface IProductAppService : IApplicationService | ||
| 74 | /// 批量编辑产品(JSON 一次提交多行,与单条 <c>PUT</c> 字段一致) | 76 | /// 批量编辑产品(JSON 一次提交多行,与单条 <c>PUT</c> 字段一致) |
| 75 | /// </summary> | 77 | /// </summary> |
| 76 | Task<ProductBulkUpdateResultDto> UpdateProductsBulkAsync(ProductBulkUpdateInputVo input); | 78 | Task<ProductBulkUpdateResultDto> UpdateProductsBulkAsync(ProductBulkUpdateInputVo input); |
| 79 | + | ||
| 80 | + /// <summary> | ||
| 81 | + /// JSON 在线批量导入产品(逐行调用 <see cref="CreateAsync"/>,部分成功) | ||
| 82 | + /// </summary> | ||
| 83 | + /// <remarks> | ||
| 84 | + /// 请求体为 JSON,每行字段与单条新增 <see cref="ProductCreateInputVo"/> 一致。 | ||
| 85 | + /// | ||
| 86 | + /// 示例请求: | ||
| 87 | + /// ```json | ||
| 88 | + /// { | ||
| 89 | + /// "items": [ | ||
| 90 | + /// { | ||
| 91 | + /// "productName": "Tuna & Bacon Sub", | ||
| 92 | + /// "categoryId": "CATEGORY_ID", | ||
| 93 | + /// "productCode": "40001", | ||
| 94 | + /// "state": true, | ||
| 95 | + /// "locationIds": ["LOCATION_GUID"] | ||
| 96 | + /// } | ||
| 97 | + /// ] | ||
| 98 | + /// } | ||
| 99 | + /// ``` | ||
| 100 | + /// | ||
| 101 | + /// 参数说明: | ||
| 102 | + /// - items: 待导入行数组;<c>index</c> 从 0 起;单次最多 <c>MaxImportRows</c> 条(默认 5000) | ||
| 103 | + /// </remarks> | ||
| 104 | + /// <param name="input">批量导入请求体</param> | ||
| 105 | + /// <returns>成功数、失败数及失败明细(<c>index</c>、<c>productName</c>、<c>message</c>)</returns> | ||
| 106 | + /// <response code="200">全部或部分行处理完成,见返回体中的计数与 errors</response> | ||
| 107 | + /// <response code="400">整单校验失败(如 items 为空、超过单次条数上限)</response> | ||
| 108 | + /// <response code="500">服务器错误</response> | ||
| 109 | + Task<ProductBatchImportOnlineResultDto> BatchImportOnlineAsync(ProductBatchImportOnlineInputVo input); | ||
| 77 | } | 110 | } |
| 78 | 111 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductCategoryAppService.cs
| @@ -17,12 +17,12 @@ public interface IProductCategoryAppService : IApplicationService | @@ -17,12 +17,12 @@ public interface IProductCategoryAppService : IApplicationService | ||
| 17 | Task<ProductCategoryGetOutputDto> GetAsync(string id); | 17 | Task<ProductCategoryGetOutputDto> GetAsync(string id); |
| 18 | 18 | ||
| 19 | /// <summary> | 19 | /// <summary> |
| 20 | - /// 新增类别;<c>categoryCode</c> 可选;body 传 <c>regionIds</c>(Region 多选)与 <c>locationIds</c>(门店多选)绑定适用范围。 | 20 | + /// 新增类别;支持 <c>companyIds</c>/<c>partnerIds</c> 与 Region/Location 哨兵 <c>ALL</c>(POST)。 |
| 21 | /// </summary> | 21 | /// </summary> |
| 22 | Task<ProductCategoryGetOutputDto> CreateAsync(ProductCategoryCreateInputVo input); | 22 | Task<ProductCategoryGetOutputDto> CreateAsync(ProductCategoryCreateInputVo input); |
| 23 | 23 | ||
| 24 | /// <summary> | 24 | /// <summary> |
| 25 | - /// 编辑类别;<c>categoryCode</c> 可选;<c>regionIds</c>/<c>locationIds</c> 多选数组规则同新增;传空数组 <c>[]</c> 可清空对应范围。 | 25 | + /// 编辑类别;范围传参与新增一致,<c>companyIds</c> 全选回显/保存支持 <c>["ALL"]</c>(PUT)。 |
| 26 | /// </summary> | 26 | /// </summary> |
| 27 | Task<ProductCategoryGetOutputDto> UpdateAsync(string id, ProductCategoryUpdateInputVo input); | 27 | Task<ProductCategoryGetOutputDto> UpdateAsync(string id, ProductCategoryUpdateInputVo input); |
| 28 | 28 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITeamMemberAppService.cs
| @@ -14,8 +14,14 @@ public interface ITeamMemberAppService | @@ -14,8 +14,14 @@ public interface ITeamMemberAppService | ||
| 14 | 14 | ||
| 15 | Task<TeamMemberGetOutputDto> GetAsync(Guid id); | 15 | Task<TeamMemberGetOutputDto> GetAsync(Guid id); |
| 16 | 16 | ||
| 17 | + /// <summary> | ||
| 18 | + /// 新增成员(POST <c>/api/app/team-member</c>)。<c>locationIds</c> / <c>regionIds</c> / <c>groupIds</c> / <c>locations</c> 可传 <c>ALL</c> 哨兵。 | ||
| 19 | + /// </summary> | ||
| 17 | Task<TeamMemberGetOutputDto> CreateAsync(TeamMemberCreateInputVo input); | 20 | Task<TeamMemberGetOutputDto> CreateAsync(TeamMemberCreateInputVo input); |
| 18 | 21 | ||
| 22 | + /// <summary> | ||
| 23 | + /// 更新成员(PUT <c>/api/app/team-member/{id}</c>)。范围传参规则与 <see cref="CreateAsync"/> 相同。 | ||
| 24 | + /// </summary> | ||
| 19 | Task<TeamMemberGetOutputDto> UpdateAsync(Guid id, TeamMemberUpdateInputVo input); | 25 | Task<TeamMemberGetOutputDto> UpdateAsync(Guid id, TeamMemberUpdateInputVo input); |
| 20 | 26 | ||
| 21 | Task DeleteAsync(Guid id); | 27 | Task DeleteAsync(Guid id); |
| @@ -39,4 +45,37 @@ public interface ITeamMemberAppService | @@ -39,4 +45,37 @@ public interface ITeamMemberAppService | ||
| 39 | /// 批量编辑成员(JSON 一次提交多行) | 45 | /// 批量编辑成员(JSON 一次提交多行) |
| 40 | /// </summary> | 46 | /// </summary> |
| 41 | Task<TeamMemberBulkUpdateResultDto> UpdateTeamMembersBulkAsync(TeamMemberBulkUpdateInputVo input); | 47 | Task<TeamMemberBulkUpdateResultDto> UpdateTeamMembersBulkAsync(TeamMemberBulkUpdateInputVo input); |
| 48 | + | ||
| 49 | + /// <summary> | ||
| 50 | + /// JSON 在线批量导入成员(逐行调用 <see cref="CreateAsync"/>,部分成功) | ||
| 51 | + /// </summary> | ||
| 52 | + /// <remarks> | ||
| 53 | + /// 请求体为 JSON,每行字段与单条新增 <see cref="TeamMemberCreateInputVo"/> 一致; | ||
| 54 | + /// <c>password</c> 为空时使用配置 <c>TeamMemberImportDefaultPassword</c>。 | ||
| 55 | + /// | ||
| 56 | + /// 示例请求: | ||
| 57 | + /// ```json | ||
| 58 | + /// { | ||
| 59 | + /// "items": [ | ||
| 60 | + /// { | ||
| 61 | + /// "fullName": "John Doe", | ||
| 62 | + /// "userName": "john@example.com", | ||
| 63 | + /// "email": "john@example.com", | ||
| 64 | + /// "roleId": "ROLE_GUID", | ||
| 65 | + /// "locationIds": ["LOCATION_GUID"], | ||
| 66 | + /// "state": true | ||
| 67 | + /// } | ||
| 68 | + /// ] | ||
| 69 | + /// } | ||
| 70 | + /// ``` | ||
| 71 | + /// | ||
| 72 | + /// 参数说明: | ||
| 73 | + /// - items: 待导入行数组;<c>index</c> 从 0 起;单次最多 <c>MaxImportRows</c> 条(默认 5000) | ||
| 74 | + /// </remarks> | ||
| 75 | + /// <param name="input">批量导入请求体</param> | ||
| 76 | + /// <returns>成功数、失败数及失败明细(<c>index</c>、<c>userName</c>、<c>message</c>)</returns> | ||
| 77 | + /// <response code="200">全部或部分行处理完成,见返回体中的计数与 errors</response> | ||
| 78 | + /// <response code="400">整单校验失败(如 items 为空、超过单次条数上限)</response> | ||
| 79 | + /// <response code="500">服务器错误</response> | ||
| 80 | + Task<TeamMemberBatchImportOnlineResultDto> BatchImportOnlineAsync(TeamMemberBatchImportOnlineInputVo input); | ||
| 42 | } | 81 | } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ITrainingAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Training; | ||
| 2 | +using Volo.Abp.Application.Services; | ||
| 3 | + | ||
| 4 | +namespace FoodLabeling.Application.Contracts.IServices; | ||
| 5 | + | ||
| 6 | +public interface ITrainingAppService : IApplicationService | ||
| 7 | +{ | ||
| 8 | + Task<List<TrainingCategoryTreeNodeDto>> GetCategoryTreeAsync(TrainingCategoryTreeInputVo input); | ||
| 9 | + | ||
| 10 | + Task<TrainingCategoryGetOutputDto> CreateCategoryAsync(TrainingCategoryCreateInputVo input); | ||
| 11 | + | ||
| 12 | + Task<TrainingCategoryGetOutputDto> UpdateCategoryAsync(string id, TrainingCategoryUpdateInputVo input); | ||
| 13 | + | ||
| 14 | + Task DeleteCategoryAsync(string id); | ||
| 15 | + | ||
| 16 | + Task<TrainingFileDto> UploadFileAsync(TrainingFileUploadInputVo input); | ||
| 17 | + | ||
| 18 | + Task<TrainingFileDto> UpdateFileAsync(string id, TrainingFileUpdateInputVo input); | ||
| 19 | + | ||
| 20 | + Task DeleteFileAsync(string id); | ||
| 21 | + | ||
| 22 | + Task SortFilesAsync(TrainingFileSortInputVo input); | ||
| 23 | + | ||
| 24 | + Task<TrainingFileScopeOutputDto> GetFileScopeAsync(string id); | ||
| 25 | + | ||
| 26 | + Task<TrainingFileScopeOutputDto> SetFileScopeAsync(string id, TrainingFileScopeInputVo input); | ||
| 27 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppTrainingAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Training; | ||
| 2 | +using Volo.Abp.Application.Services; | ||
| 3 | + | ||
| 4 | +namespace FoodLabeling.Application.Contracts.IServices; | ||
| 5 | + | ||
| 6 | +/// <summary> | ||
| 7 | +/// App 培训 / 资料中心 | ||
| 8 | +/// </summary> | ||
| 9 | +public interface IUsAppTrainingAppService : IApplicationService | ||
| 10 | +{ | ||
| 11 | + Task<List<TrainingCategoryTreeNodeDto>> GetTreeAsync(UsAppTrainingTreeInputVo input); | ||
| 12 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Json/StringOrFirstArrayItemJsonConverter.cs
0 → 100644
| 1 | +using System.Text.Json; | ||
| 2 | +using System.Text.Json.Serialization; | ||
| 3 | + | ||
| 4 | +namespace FoodLabeling.Application.Contracts.Json; | ||
| 5 | + | ||
| 6 | +/// <summary> | ||
| 7 | +/// 反序列化时兼容 JSON 字符串或字符串数组(取首个非空元素),用于前端误把单选字段传成数组的场景(如 <c>partnerId</c>)。 | ||
| 8 | +/// </summary> | ||
| 9 | +public sealed class StringOrFirstArrayItemJsonConverter : JsonConverter<string?> | ||
| 10 | +{ | ||
| 11 | + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
| 12 | + { | ||
| 13 | + switch (reader.TokenType) | ||
| 14 | + { | ||
| 15 | + case JsonTokenType.Null: | ||
| 16 | + return null; | ||
| 17 | + case JsonTokenType.String: | ||
| 18 | + return reader.GetString(); | ||
| 19 | + case JsonTokenType.StartArray: | ||
| 20 | + { | ||
| 21 | + string? first = null; | ||
| 22 | + while (reader.Read()) | ||
| 23 | + { | ||
| 24 | + if (reader.TokenType == JsonTokenType.EndArray) | ||
| 25 | + { | ||
| 26 | + break; | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + if (reader.TokenType == JsonTokenType.Null) | ||
| 30 | + { | ||
| 31 | + continue; | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + if (reader.TokenType != JsonTokenType.String) | ||
| 35 | + { | ||
| 36 | + throw new JsonException("partnerId 数组元素必须是字符串"); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + var item = reader.GetString()?.Trim(); | ||
| 40 | + if (first is null && !string.IsNullOrWhiteSpace(item)) | ||
| 41 | + { | ||
| 42 | + first = item; | ||
| 43 | + } | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + return first; | ||
| 47 | + } | ||
| 48 | + default: | ||
| 49 | + throw new JsonException($"无法将 JSON token {reader.TokenType} 转为字符串(partnerId)"); | ||
| 50 | + } | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) | ||
| 54 | + { | ||
| 55 | + if (value is null) | ||
| 56 | + { | ||
| 57 | + writer.WriteNullValue(); | ||
| 58 | + } | ||
| 59 | + else | ||
| 60 | + { | ||
| 61 | + writer.WriteStringValue(value); | ||
| 62 | + } | ||
| 63 | + } | ||
| 64 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/UsAppJwtClaims.cs
| @@ -10,4 +10,7 @@ public static class UsAppJwtClaims | @@ -10,4 +10,7 @@ public static class UsAppJwtClaims | ||
| 10 | 10 | ||
| 11 | /// <summary>美国版移动端 App</summary> | 11 | /// <summary>美国版移动端 App</summary> |
| 12 | public const string ClientKindUsApp = "us-app"; | 12 | public const string ClientKindUsApp = "us-app"; |
| 13 | + | ||
| 14 | + /// <summary>泰额版 App(us-app-auth 转发 th-app-auth 签发)</summary> | ||
| 15 | + public const string ClientKindThApp = "th_app"; | ||
| 13 | } | 16 | } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj
| @@ -4,10 +4,35 @@ | @@ -4,10 +4,35 @@ | ||
| 4 | <ItemGroup> | 4 | <ItemGroup> |
| 5 | <PackageReference Include="ClosedXML" Version="0.102.3" /> | 5 | <PackageReference Include="ClosedXML" Version="0.102.3" /> |
| 6 | <PackageReference Include="Lazy.Captcha.Core" Version="2.0.7" /> | 6 | <PackageReference Include="Lazy.Captcha.Core" Version="2.0.7" /> |
| 7 | + <PackageReference Include="MySqlConnector" Version="2.4.0" /> | ||
| 7 | <PackageReference Include="QuestPDF" Version="2024.12.2" /> | 8 | <PackageReference Include="QuestPDF" Version="2024.12.2" /> |
| 8 | </ItemGroup> | 9 | </ItemGroup> |
| 9 | 10 | ||
| 10 | <ItemGroup> | 11 | <ItemGroup> |
| 12 | + <EmbeddedResource Include="..\scripts\fl_entity_applied_region_type.sql"> | ||
| 13 | + <LogicalName>FoodLabeling.TenantMigrations.fl_entity_applied_region_type.sql</LogicalName> | ||
| 14 | + </EmbeddedResource> | ||
| 15 | + <EmbeddedResource Include="..\scripts\fl_label_partner_id.sql"> | ||
| 16 | + <LogicalName>FoodLabeling.TenantMigrations.fl_label_partner_id.sql</LogicalName> | ||
| 17 | + </EmbeddedResource> | ||
| 18 | + <EmbeddedResource Include="..\scripts\fl_product_category_partner_scope.sql"> | ||
| 19 | + <LogicalName>FoodLabeling.TenantMigrations.fl_product_category_partner_scope.sql</LogicalName> | ||
| 20 | + </EmbeddedResource> | ||
| 21 | + <EmbeddedResource Include="..\scripts\fl_userlocation.sql"> | ||
| 22 | + <LogicalName>FoodLabeling.TenantMigrations.fl_userlocation.sql</LogicalName> | ||
| 23 | + </EmbeddedResource> | ||
| 24 | + <EmbeddedResource Include="..\scripts\fl_team_member_scope.sql"> | ||
| 25 | + <LogicalName>FoodLabeling.TenantMigrations.fl_team_member_scope.sql</LogicalName> | ||
| 26 | + </EmbeddedResource> | ||
| 27 | + <EmbeddedResource Include="..\scripts\fl_training.sql"> | ||
| 28 | + <LogicalName>FoodLabeling.TenantMigrations.fl_training.sql</LogicalName> | ||
| 29 | + </EmbeddedResource> | ||
| 30 | + <EmbeddedResource Include="..\scripts\fl_label_alert_timer.sql"> | ||
| 31 | + <LogicalName>FoodLabeling.TenantMigrations.fl_label_alert_timer.sql</LogicalName> | ||
| 32 | + </EmbeddedResource> | ||
| 33 | + </ItemGroup> | ||
| 34 | + | ||
| 35 | + <ItemGroup> | ||
| 11 | <ProjectReference Include="..\..\..\framework\Yi.Framework.Ddd.Application\Yi.Framework.Ddd.Application.csproj" /> | 36 | <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" /> | 37 | <ProjectReference Include="..\..\rbac\Yi.Framework.Rbac.Application.Contracts\Yi.Framework.Rbac.Application.Contracts.csproj" /> |
| 13 | <ProjectReference Include="..\FoodLabeling.Application.Contracts\FoodLabeling.Application.Contracts.csproj" /> | 38 | <ProjectReference Include="..\FoodLabeling.Application.Contracts\FoodLabeling.Application.Contracts.csproj" /> |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/AllScopeBindingHelper.cs
| @@ -23,22 +23,36 @@ public static class AllScopeBindingHelper | @@ -23,22 +23,36 @@ public static class AllScopeBindingHelper | ||
| 23 | return selected.Count == 0; | 23 | return selected.Count == 0; |
| 24 | } | 24 | } |
| 25 | 25 | ||
| 26 | - if (selected.Count != universe.Count) | 26 | + var universeSet = new HashSet<string>( |
| 27 | + universe.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()), | ||
| 28 | + StringComparer.OrdinalIgnoreCase); | ||
| 29 | + var selectedSet = new HashSet<string>( | ||
| 30 | + selected.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()), | ||
| 31 | + StringComparer.OrdinalIgnoreCase); | ||
| 32 | + | ||
| 33 | + if (selectedSet.Count != universeSet.Count) | ||
| 27 | { | 34 | { |
| 28 | return false; | 35 | return false; |
| 29 | } | 36 | } |
| 30 | 37 | ||
| 31 | - var set = new HashSet<string>(universe, StringComparer.Ordinal); | ||
| 32 | - return selected.All(id => set.Contains(id)); | 38 | + return selectedSet.SetEquals(universeSet); |
| 33 | } | 39 | } |
| 34 | 40 | ||
| 35 | - /// <summary>解析维度类型:显式 ALL、空数组 ALL、或 Id 列表全选时视为 ALL。</summary> | 41 | + /// <summary> |
| 42 | + /// 解析维度类型:显式 ALL、空数组 ALL、或 Id 列表全选时视为 ALL。 | ||
| 43 | + /// 入参显式 <see cref="ScopeSpecified"/> 时保留 SPECIFIED 并落库全部 Id,不因「当前上下文全选」折叠为 ALL。 | ||
| 44 | + /// </summary> | ||
| 36 | public static string ResolveDimensionType( | 45 | public static string ResolveDimensionType( |
| 37 | string? declaredType, | 46 | string? declaredType, |
| 38 | IReadOnlyList<string> ids, | 47 | IReadOnlyList<string> ids, |
| 39 | bool hasArrayInPayload, | 48 | bool hasArrayInPayload, |
| 40 | bool isFullSelection) | 49 | bool isFullSelection) |
| 41 | { | 50 | { |
| 51 | + if (IsDeclaredSpecified(declaredType)) | ||
| 52 | + { | ||
| 53 | + return ScopeSpecified; | ||
| 54 | + } | ||
| 55 | + | ||
| 42 | if (isFullSelection || (ids.Count == 0 && IsDeclaredAll(declaredType))) | 56 | if (isFullSelection || (ids.Count == 0 && IsDeclaredAll(declaredType))) |
| 43 | { | 57 | { |
| 44 | return ScopeAll; | 58 | return ScopeAll; |
| @@ -61,6 +75,9 @@ public static class AllScopeBindingHelper | @@ -61,6 +75,9 @@ public static class AllScopeBindingHelper | ||
| 61 | public static bool IsDeclaredAll(string? type) => | 75 | public static bool IsDeclaredAll(string? type) => |
| 62 | string.Equals((type ?? ScopeAll).Trim(), ScopeAll, StringComparison.OrdinalIgnoreCase); | 76 | string.Equals((type ?? ScopeAll).Trim(), ScopeAll, StringComparison.OrdinalIgnoreCase); |
| 63 | 77 | ||
| 78 | + public static bool IsDeclaredSpecified(string? type) => | ||
| 79 | + string.Equals((type ?? string.Empty).Trim(), ScopeSpecified, StringComparison.OrdinalIgnoreCase); | ||
| 80 | + | ||
| 64 | /// <summary>全部 Company(<c>fl_partner.Id</c>)。</summary> | 81 | /// <summary>全部 Company(<c>fl_partner.Id</c>)。</summary> |
| 65 | public static async Task<List<string>> ResolveAllPartnerIdsAsync(ISqlSugarClient db) | 82 | public static async Task<List<string>> ResolveAllPartnerIdsAsync(ISqlSugarClient db) |
| 66 | { | 83 | { |
| @@ -87,20 +104,51 @@ public static class AllScopeBindingHelper | @@ -87,20 +104,51 @@ public static class AllScopeBindingHelper | ||
| 87 | return LocationScopeBindingHelper.NormalizeIds(rows); | 104 | return LocationScopeBindingHelper.NormalizeIds(rows); |
| 88 | } | 105 | } |
| 89 | 106 | ||
| 90 | - /// <summary>全部门店;可按 Company / Region 限定。</summary> | 107 | + /// <summary> |
| 108 | + /// 全部门店;可按 Company / Region 限定。 | ||
| 109 | + /// 同时传 Company 与 Region 时取<strong>交集</strong>(该 Region 下且属于这些 Company 的门店), | ||
| 110 | + /// 禁止并集——否则回显「区内全选」无法折叠为 <c>locationIds:["ALL"]</c>。 | ||
| 111 | + /// </summary> | ||
| 91 | public static async Task<List<string>> ResolveAllLocationIdsAsync( | 112 | public static async Task<List<string>> ResolveAllLocationIdsAsync( |
| 92 | ISqlSugarClient db, | 113 | ISqlSugarClient db, |
| 93 | IReadOnlyList<string>? partnerIds, | 114 | IReadOnlyList<string>? partnerIds, |
| 94 | IReadOnlyList<string>? regionIds) | 115 | IReadOnlyList<string>? regionIds) |
| 95 | { | 116 | { |
| 117 | + var partners = LocationScopeBindingHelper.NormalizeIds(partnerIds); | ||
| 118 | + var regions = LocationScopeBindingHelper.FilterConcreteScopeIds(regionIds); | ||
| 119 | + | ||
| 120 | + if (regions.Count > 0) | ||
| 121 | + { | ||
| 122 | + var fromRegions = await LocationScopeBindingHelper.ResolveLocationIdsFromGroupIdsAsync(db, regions); | ||
| 123 | + if (partners.Count == 0) | ||
| 124 | + { | ||
| 125 | + return LocationScopeBindingHelper.NormalizeIds(fromRegions); | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + var partnerLocSet = new HashSet<string>( | ||
| 129 | + await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(db, partners), | ||
| 130 | + StringComparer.OrdinalIgnoreCase); | ||
| 131 | + return LocationScopeBindingHelper.NormalizeIds( | ||
| 132 | + fromRegions.Where(id => partnerLocSet.Contains(id)).ToList()); | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + if (partners.Count > 0) | ||
| 136 | + { | ||
| 137 | + return await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(db, partners); | ||
| 138 | + } | ||
| 139 | + | ||
| 96 | var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | 140 | var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( |
| 97 | db, | 141 | db, |
| 98 | - partnerIds, | ||
| 99 | - regionIds, | 142 | + (IReadOnlyList<string>?)null, |
| 143 | + null, | ||
| 100 | null); | 144 | null); |
| 101 | return LocationScopeBindingHelper.NormalizeIds(merged); | 145 | return LocationScopeBindingHelper.NormalizeIds(merged); |
| 102 | } | 146 | } |
| 103 | 147 | ||
| 148 | + /// <summary>Id 数组是否含 <c>ALL</c> 哨兵(大小写不敏感;与具体 Guid 同传时以 ALL 为准)。</summary> | ||
| 149 | + public static bool HasAllScopeSentinelSelection(IReadOnlyList<string>? ids) => | ||
| 150 | + LocationScopeBindingHelper.ContainsAllScopeSentinel(ids); | ||
| 151 | + | ||
| 104 | /// <summary>Company 维度是否应存为 ALL(含「勾选了全部 Company」)。</summary> | 152 | /// <summary>Company 维度是否应存为 ALL(含「勾选了全部 Company」)。</summary> |
| 105 | public static async Task<(string Type, List<string> Ids)> NormalizePartnerScopeAsync( | 153 | public static async Task<(string Type, List<string> Ids)> NormalizePartnerScopeAsync( |
| 106 | ISqlSugarClient db, | 154 | ISqlSugarClient db, |
| @@ -110,6 +158,10 @@ public static class AllScopeBindingHelper | @@ -110,6 +158,10 @@ public static class AllScopeBindingHelper | ||
| 110 | bool hasArrayInPayload) | 158 | bool hasArrayInPayload) |
| 111 | { | 159 | { |
| 112 | var ids = LabelEntityPartnerScopeHelper.NormalizePartnerIds(partnerIds, companyIds); | 160 | var ids = LabelEntityPartnerScopeHelper.NormalizePartnerIds(partnerIds, companyIds); |
| 161 | + if (HasAllScopeSentinelSelection(ids)) | ||
| 162 | + { | ||
| 163 | + return (ScopeAll, new List<string>()); | ||
| 164 | + } | ||
| 113 | var allPartners = await ResolveAllPartnerIdsAsync(db); | 165 | var allPartners = await ResolveAllPartnerIdsAsync(db); |
| 114 | // 勾选当前全部 Company(含前端仍传 SPECIFIED + 全量 Id)→ 归档 ALL,后续新增 Company 动态适用 | 166 | // 勾选当前全部 Company(含前端仍传 SPECIFIED + 全量 Id)→ 归档 ALL,后续新增 Company 动态适用 |
| 115 | var isFull = ids.Count > 0 && IsFullIdSelection(ids, allPartners); | 167 | var isFull = ids.Count > 0 && IsFullIdSelection(ids, allPartners); |
| @@ -125,7 +177,12 @@ public static class AllScopeBindingHelper | @@ -125,7 +177,12 @@ public static class AllScopeBindingHelper | ||
| 125 | bool hasArrayInPayload, | 177 | bool hasArrayInPayload, |
| 126 | IReadOnlyList<string>? partnerIdsForContext) | 178 | IReadOnlyList<string>? partnerIdsForContext) |
| 127 | { | 179 | { |
| 128 | - var ids = LocationScopeBindingHelper.NormalizeIds(regionIds); | 180 | + if (HasAllScopeSentinelSelection(regionIds)) |
| 181 | + { | ||
| 182 | + return (ScopeAll, new List<string>()); | ||
| 183 | + } | ||
| 184 | + | ||
| 185 | + var ids = LocationScopeBindingHelper.FilterConcreteScopeIds(regionIds); | ||
| 129 | var allRegions = await ResolveAllRegionIdsAsync(db, partnerIdsForContext); | 186 | var allRegions = await ResolveAllRegionIdsAsync(db, partnerIdsForContext); |
| 130 | var isFull = ids.Count > 0 && IsFullIdSelection(ids, allRegions); | 187 | var isFull = ids.Count > 0 && IsFullIdSelection(ids, allRegions); |
| 131 | var type = ResolveDimensionType(declaredType, ids, hasArrayInPayload, isFull); | 188 | var type = ResolveDimensionType(declaredType, ids, hasArrayInPayload, isFull); |
| @@ -141,7 +198,12 @@ public static class AllScopeBindingHelper | @@ -141,7 +198,12 @@ public static class AllScopeBindingHelper | ||
| 141 | IReadOnlyList<string>? partnerIdsForContext, | 198 | IReadOnlyList<string>? partnerIdsForContext, |
| 142 | IReadOnlyList<string>? regionIdsForContext) | 199 | IReadOnlyList<string>? regionIdsForContext) |
| 143 | { | 200 | { |
| 144 | - var ids = LocationScopeBindingHelper.NormalizeIds(locationIds); | 201 | + if (HasAllScopeSentinelSelection(locationIds)) |
| 202 | + { | ||
| 203 | + return (ScopeAll, new List<string>()); | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + var ids = LocationScopeBindingHelper.FilterConcreteScopeIds(locationIds); | ||
| 145 | var allLocations = await ResolveAllLocationIdsAsync(db, partnerIdsForContext, regionIdsForContext); | 207 | var allLocations = await ResolveAllLocationIdsAsync(db, partnerIdsForContext, regionIdsForContext); |
| 146 | var isFull = ids.Count > 0 && IsFullIdSelection(ids, allLocations); | 208 | var isFull = ids.Count > 0 && IsFullIdSelection(ids, allLocations); |
| 147 | var type = ResolveDimensionType(declaredType, ids, hasArrayInPayload, isFull); | 209 | var type = ResolveDimensionType(declaredType, ids, hasArrayInPayload, isFull); |
| @@ -162,7 +224,7 @@ public static class AllScopeBindingHelper | @@ -162,7 +224,7 @@ public static class AllScopeBindingHelper | ||
| 162 | return new List<string> { partnerId.Trim() }; | 224 | return new List<string> { partnerId.Trim() }; |
| 163 | } | 225 | } |
| 164 | 226 | ||
| 165 | - var regionIds = LocationScopeBindingHelper.NormalizeIds(regionOrGroupIds); | 227 | + var regionIds = LocationScopeBindingHelper.FilterConcreteScopeIds(regionOrGroupIds); |
| 166 | if (regionIds.Count > 0) | 228 | if (regionIds.Count > 0) |
| 167 | { | 229 | { |
| 168 | var rows = await db.Queryable<FlGroupDbEntity>() | 230 | var rows = await db.Queryable<FlGroupDbEntity>() |
| @@ -173,7 +235,7 @@ public static class AllScopeBindingHelper | @@ -173,7 +235,7 @@ public static class AllScopeBindingHelper | ||
| 173 | return normalized.Count > 0 ? normalized : null; | 235 | return normalized.Count > 0 ? normalized : null; |
| 174 | } | 236 | } |
| 175 | 237 | ||
| 176 | - var locIds = LocationScopeBindingHelper.NormalizeIds(locationIds); | 238 | + var locIds = LocationScopeBindingHelper.FilterConcreteScopeIds(locationIds); |
| 177 | if (locIds.Count == 0) | 239 | if (locIds.Count == 0) |
| 178 | { | 240 | { |
| 179 | return null; | 241 | return null; |
| @@ -195,27 +257,55 @@ public static class AllScopeBindingHelper | @@ -195,27 +257,55 @@ public static class AllScopeBindingHelper | ||
| 195 | bool hasScopeArrays, | 257 | bool hasScopeArrays, |
| 196 | IReadOnlyList<string>? partnerIdsForContext) | 258 | IReadOnlyList<string>? partnerIdsForContext) |
| 197 | { | 259 | { |
| 260 | + var concreteLocations = LocationScopeBindingHelper.FilterConcreteScopeIds(locationIds); | ||
| 261 | + var concreteRegions = LocationScopeBindingHelper.FilterConcreteScopeIds(regionIds); | ||
| 262 | + | ||
| 263 | + // 具体 Region(非全局 ALL)下即使覆盖该区全部门店,也只存 SPECIFIED 快照,不升全局 AvailabilityType=ALL | ||
| 264 | + if (concreteRegions.Count > 0) | ||
| 265 | + { | ||
| 266 | + return false; | ||
| 267 | + } | ||
| 268 | + | ||
| 269 | + // locationIds 含 ALL 且无具体 Region:由调用方按 Company 展开或归档全局 ALL | ||
| 270 | + if (HasAllScopeSentinelSelection(locationIds)) | ||
| 271 | + { | ||
| 272 | + return true; | ||
| 273 | + } | ||
| 274 | + | ||
| 275 | + if (HasAllScopeSentinelSelection(regionIds) && concreteLocations.Count == 0) | ||
| 276 | + { | ||
| 277 | + return true; | ||
| 278 | + } | ||
| 279 | + | ||
| 198 | if (IsDeclaredAll(declaredAvailabilityType) | 280 | if (IsDeclaredAll(declaredAvailabilityType) |
| 199 | - && LocationScopeBindingHelper.NormalizeIds(regionIds).Count == 0 | ||
| 200 | - && LocationScopeBindingHelper.NormalizeIds(locationIds).Count == 0) | 281 | + && concreteRegions.Count == 0 |
| 282 | + && concreteLocations.Count == 0) | ||
| 201 | { | 283 | { |
| 202 | return true; | 284 | return true; |
| 203 | } | 285 | } |
| 204 | 286 | ||
| 205 | if (hasScopeArrays | 287 | if (hasScopeArrays |
| 206 | - && LocationScopeBindingHelper.NormalizeIds(regionIds).Count == 0 | ||
| 207 | - && LocationScopeBindingHelper.NormalizeIds(locationIds).Count == 0 | 288 | + && concreteRegions.Count == 0 |
| 289 | + && concreteLocations.Count == 0 | ||
| 208 | && IsDeclaredAll(declaredAvailabilityType)) | 290 | && IsDeclaredAll(declaredAvailabilityType)) |
| 209 | { | 291 | { |
| 210 | return true; | 292 | return true; |
| 211 | } | 293 | } |
| 212 | 294 | ||
| 213 | - // 前端 Select All 常传 SPECIFIED + 当前全量 Id:覆盖 Company 上下文全部门店时归档 ALL | ||
| 214 | - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | 295 | + // 前端 Select All 常传 SPECIFIED + 当前全量 Id:覆盖上下文全部门店时归档 ALL。 |
| 296 | + // 有具体 locationIds 时:只按这些门店落 SPECIFIED 快照,禁止再升成 AvailabilityType=ALL | ||
| 297 | + // (否则「Region=ALL + 选 1 店」在公司仅 1 店或误判全选时会清空快照并回显成全局 ALL)。 | ||
| 298 | + if (concreteLocations.Count > 0) | ||
| 299 | + { | ||
| 300 | + return false; | ||
| 301 | + } | ||
| 302 | + | ||
| 303 | + List<string> merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | ||
| 215 | db, | 304 | db, |
| 216 | (IReadOnlyList<string>?)null, | 305 | (IReadOnlyList<string>?)null, |
| 217 | - regionIds, | ||
| 218 | - locationIds); | 306 | + concreteRegions, |
| 307 | + concreteLocations); | ||
| 308 | + | ||
| 219 | if (merged.Count == 0) | 309 | if (merged.Count == 0) |
| 220 | { | 310 | { |
| 221 | return false; | 311 | return false; |
| @@ -224,14 +314,14 @@ public static class AllScopeBindingHelper | @@ -224,14 +314,14 @@ public static class AllScopeBindingHelper | ||
| 224 | var partnerContext = LocationScopeBindingHelper.NormalizeIds(partnerIdsForContext); | 314 | var partnerContext = LocationScopeBindingHelper.NormalizeIds(partnerIdsForContext); |
| 225 | if (partnerContext.Count == 0) | 315 | if (partnerContext.Count == 0) |
| 226 | { | 316 | { |
| 227 | - partnerContext = await ResolvePartnerContextFromScopeAsync(db, null, regionIds, locationIds) | 317 | + partnerContext = await ResolvePartnerContextFromScopeAsync(db, null, concreteRegions, concreteLocations) |
| 228 | ?? new List<string>(); | 318 | ?? new List<string>(); |
| 229 | } | 319 | } |
| 230 | 320 | ||
| 231 | var allLocations = await ResolveAllLocationIdsAsync( | 321 | var allLocations = await ResolveAllLocationIdsAsync( |
| 232 | db, | 322 | db, |
| 233 | partnerContext.Count > 0 ? partnerContext : null, | 323 | partnerContext.Count > 0 ? partnerContext : null, |
| 234 | - null); | 324 | + concreteRegions.Count > 0 ? concreteRegions : null); |
| 235 | return IsFullIdSelection(merged, allLocations); | 325 | return IsFullIdSelection(merged, allLocations); |
| 236 | } | 326 | } |
| 237 | 327 | ||
| @@ -244,10 +334,35 @@ public static class AllScopeBindingHelper | @@ -244,10 +334,35 @@ public static class AllScopeBindingHelper | ||
| 244 | IReadOnlyList<string>? locationIds, | 334 | IReadOnlyList<string>? locationIds, |
| 245 | bool hasScopePayload) | 335 | bool hasScopePayload) |
| 246 | { | 336 | { |
| 337 | + // partnerId 字符串为 ALL 哨兵:全选 Company,归档 ALL(禁止把 ALL 当 Guid 查库) | ||
| 338 | + if (LocationScopeBindingHelper.IsAllScopeSentinel(partnerId)) | ||
| 339 | + { | ||
| 340 | + return true; | ||
| 341 | + } | ||
| 342 | + | ||
| 343 | + var concreteLocations = LocationScopeBindingHelper.FilterConcreteScopeIds(locationIds); | ||
| 344 | + var concreteGroups = LocationScopeBindingHelper.FilterConcreteScopeIds(groupIds); | ||
| 345 | + | ||
| 346 | + // 具体 Region 下不升全局 AvailabilityType=ALL(与 ShouldTreatMergedLocationScopeAsAllAsync 一致) | ||
| 347 | + if (concreteGroups.Count > 0) | ||
| 348 | + { | ||
| 349 | + return false; | ||
| 350 | + } | ||
| 351 | + | ||
| 352 | + if (HasAllScopeSentinelSelection(locationIds)) | ||
| 353 | + { | ||
| 354 | + return true; | ||
| 355 | + } | ||
| 356 | + | ||
| 357 | + if (HasAllScopeSentinelSelection(groupIds) && concreteLocations.Count == 0) | ||
| 358 | + { | ||
| 359 | + return true; | ||
| 360 | + } | ||
| 361 | + | ||
| 247 | if (IsDeclaredAll(declaredAvailabilityType) | 362 | if (IsDeclaredAll(declaredAvailabilityType) |
| 248 | && string.IsNullOrWhiteSpace(partnerId) | 363 | && string.IsNullOrWhiteSpace(partnerId) |
| 249 | - && LocationScopeBindingHelper.NormalizeIds(groupIds).Count == 0 | ||
| 250 | - && LocationScopeBindingHelper.NormalizeIds(locationIds).Count == 0) | 364 | + && concreteGroups.Count == 0 |
| 365 | + && concreteLocations.Count == 0) | ||
| 251 | { | 366 | { |
| 252 | return true; | 367 | return true; |
| 253 | } | 368 | } |
| @@ -257,20 +372,29 @@ public static class AllScopeBindingHelper | @@ -257,20 +372,29 @@ public static class AllScopeBindingHelper | ||
| 257 | return false; | 372 | return false; |
| 258 | } | 373 | } |
| 259 | 374 | ||
| 375 | + // 有具体门店时不升全局 ALL | ||
| 376 | + if (concreteLocations.Count > 0) | ||
| 377 | + { | ||
| 378 | + return false; | ||
| 379 | + } | ||
| 380 | + | ||
| 260 | var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | 381 | var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( |
| 261 | db, | 382 | db, |
| 262 | partnerId, | 383 | partnerId, |
| 263 | - groupIds, | ||
| 264 | - locationIds); | 384 | + concreteGroups, |
| 385 | + concreteLocations); | ||
| 386 | + | ||
| 265 | if (merged.Count == 0) | 387 | if (merged.Count == 0) |
| 266 | { | 388 | { |
| 267 | return false; | 389 | return false; |
| 268 | } | 390 | } |
| 269 | 391 | ||
| 270 | - // 未传 partnerId 时从 Region/门店反推 Company,避免用「全系统门店」误判导致无法归档 ALL | ||
| 271 | var partnerContext = await ResolvePartnerContextFromScopeAsync( | 392 | var partnerContext = await ResolvePartnerContextFromScopeAsync( |
| 272 | - db, partnerId, groupIds, locationIds); | ||
| 273 | - var allLocations = await ResolveAllLocationIdsAsync(db, partnerContext, null); | 393 | + db, partnerId, concreteGroups, concreteLocations); |
| 394 | + var allLocations = await ResolveAllLocationIdsAsync( | ||
| 395 | + db, | ||
| 396 | + partnerContext, | ||
| 397 | + concreteGroups.Count > 0 ? concreteGroups : null); | ||
| 274 | return IsFullIdSelection(merged, allLocations); | 398 | return IsFullIdSelection(merged, allLocations); |
| 275 | } | 399 | } |
| 276 | 400 | ||
| @@ -302,4 +426,169 @@ public static class AllScopeBindingHelper | @@ -302,4 +426,169 @@ public static class AllScopeBindingHelper | ||
| 302 | : await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, locs); | 426 | : await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, locs); |
| 303 | return (partnerIds, regionIds, locs); | 427 | return (partnerIds, regionIds, locs); |
| 304 | } | 428 | } |
| 429 | + | ||
| 430 | + /// <summary> | ||
| 431 | + /// 标签/产品分类等:解析 Region + Location 落库(对齐 Label:Region=ALL 时可保留具体门店快照)。 | ||
| 432 | + /// </summary> | ||
| 433 | + public sealed class LabelEntityRegionLocationSaveResult | ||
| 434 | + { | ||
| 435 | + public string AvailabilityType { get; init; } = ScopeSpecified; | ||
| 436 | + | ||
| 437 | + public string AppliedRegionType { get; init; } = ScopeAll; | ||
| 438 | + | ||
| 439 | + public List<string> LocationIds { get; init; } = new(); | ||
| 440 | + } | ||
| 441 | + | ||
| 442 | + public static async Task<LabelEntityRegionLocationSaveResult> ResolveLabelEntityRegionLocationForSaveAsync( | ||
| 443 | + ISqlSugarClient db, | ||
| 444 | + string? declaredAvailabilityType, | ||
| 445 | + IReadOnlyList<string>? partnerIdsForContext, | ||
| 446 | + IReadOnlyList<string>? mergedRegionIdsRaw, | ||
| 447 | + IReadOnlyList<string>? locationIdsRaw, | ||
| 448 | + bool hasScopeArrays) | ||
| 449 | + { | ||
| 450 | + var normalizedLocationIds = LocationScopeBindingHelper.NormalizeIds(locationIdsRaw); | ||
| 451 | + var mergedRegionIds = LocationScopeBindingHelper.NormalizeIds(mergedRegionIdsRaw); | ||
| 452 | + var regionIds = LocationScopeBindingHelper.FilterConcreteScopeIds(mergedRegionIds); | ||
| 453 | + var explicitLocationIds = LocationScopeBindingHelper.FilterConcreteScopeIds(normalizedLocationIds); | ||
| 454 | + var locationHasAll = HasAllScopeSentinelSelection(normalizedLocationIds); | ||
| 455 | + var regionHasAllSentinel = HasAllScopeSentinelSelection(mergedRegionIds); | ||
| 456 | + var hasConcretePartner = LocationScopeBindingHelper.NormalizeIds(partnerIdsForContext).Count > 0; | ||
| 457 | + // 用户显式传具体门店 Id 时一律 SPECIFIED,不因「恰好覆盖 Region 全集」归档 ALL | ||
| 458 | + var userExplicitLocations = explicitLocationIds.Count > 0 && !locationHasAll; | ||
| 459 | + | ||
| 460 | + // 前端 Select All Region 常展开为全量 Guid,需识别为 Region=ALL | ||
| 461 | + var regionHasAll = regionHasAllSentinel; | ||
| 462 | + if (!regionHasAll && regionIds.Count > 0) | ||
| 463 | + { | ||
| 464 | + var allRegions = await ResolveAllRegionIdsAsync( | ||
| 465 | + db, | ||
| 466 | + hasConcretePartner ? partnerIdsForContext : null); | ||
| 467 | + regionHasAll = allRegions.Count > 0 && IsFullIdSelection(regionIds, allRegions); | ||
| 468 | + } | ||
| 469 | + | ||
| 470 | + // location ALL 哨兵,或具体 Region 下「空选/全选门店」:有 Company/Region 上下文则展开 SPECIFIED | ||
| 471 | + var locationCoversConcreteRegions = false; | ||
| 472 | + if (!locationHasAll && !userExplicitLocations && regionIds.Count > 0 && !regionHasAll) | ||
| 473 | + { | ||
| 474 | + if (explicitLocationIds.Count == 0) | ||
| 475 | + { | ||
| 476 | + locationCoversConcreteRegions = true; | ||
| 477 | + } | ||
| 478 | + else | ||
| 479 | + { | ||
| 480 | + var regionUniverse = await ResolveAllLocationIdsAsync( | ||
| 481 | + db, | ||
| 482 | + hasConcretePartner ? partnerIdsForContext : null, | ||
| 483 | + regionIds); | ||
| 484 | + locationCoversConcreteRegions = regionUniverse.Count > 0 | ||
| 485 | + && IsFullIdSelection(explicitLocationIds, regionUniverse); | ||
| 486 | + } | ||
| 487 | + } | ||
| 488 | + | ||
| 489 | + if (!userExplicitLocations && (locationHasAll || locationCoversConcreteRegions)) | ||
| 490 | + { | ||
| 491 | + var expandRegions = regionHasAll ? null : (regionIds.Count > 0 ? regionIds : null); | ||
| 492 | + var expanded = await LocationScopeBindingHelper.ExpandScopedAllLocationsForSaveAsync( | ||
| 493 | + db, | ||
| 494 | + hasConcretePartner ? partnerIdsForContext : null, | ||
| 495 | + expandRegions); | ||
| 496 | + if (expanded is not null) | ||
| 497 | + { | ||
| 498 | + return new LabelEntityRegionLocationSaveResult | ||
| 499 | + { | ||
| 500 | + AvailabilityType = ScopeSpecified, | ||
| 501 | + AppliedRegionType = regionHasAll || regionIds.Count == 0 ? ScopeAll : ScopeSpecified, | ||
| 502 | + LocationIds = expanded | ||
| 503 | + }; | ||
| 504 | + } | ||
| 505 | + | ||
| 506 | + return new LabelEntityRegionLocationSaveResult | ||
| 507 | + { | ||
| 508 | + AvailabilityType = ScopeAll, | ||
| 509 | + AppliedRegionType = ScopeAll, | ||
| 510 | + LocationIds = new List<string>() | ||
| 511 | + }; | ||
| 512 | + } | ||
| 513 | + | ||
| 514 | + // Region=ALL + 具体门店:Region 存 ALL,门店 SPECIFIED 快照(可回显 regionIds=["ALL"] + 单个 locationId) | ||
| 515 | + if (regionHasAll && explicitLocationIds.Count > 0) | ||
| 516 | + { | ||
| 517 | + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(db, explicitLocationIds); | ||
| 518 | + return new LabelEntityRegionLocationSaveResult | ||
| 519 | + { | ||
| 520 | + AvailabilityType = ScopeSpecified, | ||
| 521 | + AppliedRegionType = ScopeAll, | ||
| 522 | + LocationIds = explicitLocationIds | ||
| 523 | + }; | ||
| 524 | + } | ||
| 525 | + | ||
| 526 | + // Region=ALL 且无具体门店 → 双 ALL | ||
| 527 | + if (regionHasAll) | ||
| 528 | + { | ||
| 529 | + return new LabelEntityRegionLocationSaveResult | ||
| 530 | + { | ||
| 531 | + AvailabilityType = ScopeAll, | ||
| 532 | + AppliedRegionType = ScopeAll, | ||
| 533 | + LocationIds = new List<string>() | ||
| 534 | + }; | ||
| 535 | + } | ||
| 536 | + | ||
| 537 | + // 有具体门店(可同传具体 Region):以门店为准 | ||
| 538 | + if (explicitLocationIds.Count > 0) | ||
| 539 | + { | ||
| 540 | + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(db, explicitLocationIds); | ||
| 541 | + return new LabelEntityRegionLocationSaveResult | ||
| 542 | + { | ||
| 543 | + AvailabilityType = ScopeSpecified, | ||
| 544 | + AppliedRegionType = ScopeSpecified, | ||
| 545 | + LocationIds = explicitLocationIds | ||
| 546 | + }; | ||
| 547 | + } | ||
| 548 | + | ||
| 549 | + if (await ShouldTreatMergedLocationScopeAsAllAsync( | ||
| 550 | + db, | ||
| 551 | + declaredAvailabilityType, | ||
| 552 | + regionIds, | ||
| 553 | + explicitLocationIds, | ||
| 554 | + hasScopeArrays, | ||
| 555 | + partnerIdsForContext)) | ||
| 556 | + { | ||
| 557 | + return new LabelEntityRegionLocationSaveResult | ||
| 558 | + { | ||
| 559 | + AvailabilityType = ScopeAll, | ||
| 560 | + AppliedRegionType = ScopeAll, | ||
| 561 | + LocationIds = new List<string>() | ||
| 562 | + }; | ||
| 563 | + } | ||
| 564 | + | ||
| 565 | + var availabilityType = (declaredAvailabilityType ?? ScopeAll).Trim().ToUpperInvariant(); | ||
| 566 | + if (regionIds.Count > 0) | ||
| 567 | + { | ||
| 568 | + availabilityType = ScopeSpecified; | ||
| 569 | + } | ||
| 570 | + else if (hasScopeArrays && IsDeclaredAll(availabilityType)) | ||
| 571 | + { | ||
| 572 | + availabilityType = ScopeAll; | ||
| 573 | + } | ||
| 574 | + | ||
| 575 | + if (availabilityType != ScopeAll && availabilityType != ScopeSpecified) | ||
| 576 | + { | ||
| 577 | + throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); | ||
| 578 | + } | ||
| 579 | + | ||
| 580 | + var locationSpecified = string.Equals(availabilityType, ScopeSpecified, StringComparison.OrdinalIgnoreCase); | ||
| 581 | + var savedLocationIds = await LocationScopeBindingHelper.ResolveEntityLocationIdsForSaveAsync( | ||
| 582 | + db, | ||
| 583 | + locationSpecified, | ||
| 584 | + regionIds, | ||
| 585 | + explicitLocationIds); | ||
| 586 | + | ||
| 587 | + return new LabelEntityRegionLocationSaveResult | ||
| 588 | + { | ||
| 589 | + AvailabilityType = locationSpecified ? ScopeSpecified : ScopeAll, | ||
| 590 | + AppliedRegionType = locationSpecified ? ScopeSpecified : ScopeAll, | ||
| 591 | + LocationIds = savedLocationIds | ||
| 592 | + }; | ||
| 593 | + } | ||
| 305 | } | 594 | } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/EntityLocationScopeDisplayHelper.cs
| @@ -11,6 +11,10 @@ public static class EntityLocationScopeDisplayHelper | @@ -11,6 +11,10 @@ public static class EntityLocationScopeDisplayHelper | ||
| 11 | /// <summary> | 11 | /// <summary> |
| 12 | /// 根据可用范围类型与已解析的 Region/Location Id、名称,生成列表展示文案。 | 12 | /// 根据可用范围类型与已解析的 Region/Location Id、名称,生成列表展示文案。 |
| 13 | /// </summary> | 13 | /// </summary> |
| 14 | + /// <remarks> | ||
| 15 | + /// 有具体门店 Guid 时始终展示门店名称,不因「覆盖某 Region 下全部门店」误显示 All Location | ||
| 16 | + /// (常见于 <c>AppliedRegionType=ALL</c> + 单个 <c>locationIds</c>)。 | ||
| 17 | + /// </remarks> | ||
| 14 | public static async Task<(string Region, string Location)> BuildListDisplayAsync( | 18 | public static async Task<(string Region, string Location)> BuildListDisplayAsync( |
| 15 | ISqlSugarClient db, | 19 | ISqlSugarClient db, |
| 16 | string? availabilityType, | 20 | string? availabilityType, |
| @@ -18,7 +22,8 @@ public static class EntityLocationScopeDisplayHelper | @@ -18,7 +22,8 @@ public static class EntityLocationScopeDisplayHelper | ||
| 18 | IReadOnlyList<string> locationIds, | 22 | IReadOnlyList<string> locationIds, |
| 19 | IEnumerable<string> regionNames, | 23 | IEnumerable<string> regionNames, |
| 20 | IEnumerable<string> locationNames, | 24 | IEnumerable<string> locationNames, |
| 21 | - IReadOnlyList<string>? partnerIdsForContext) | 25 | + IReadOnlyList<string>? partnerIdsForContext, |
| 26 | + string? appliedRegionType = null) | ||
| 22 | { | 27 | { |
| 23 | if (AllScopeBindingHelper.IsDeclaredAll(availabilityType)) | 28 | if (AllScopeBindingHelper.IsDeclaredAll(availabilityType)) |
| 24 | { | 29 | { |
| @@ -29,27 +34,33 @@ public static class EntityLocationScopeDisplayHelper | @@ -29,27 +34,33 @@ public static class EntityLocationScopeDisplayHelper | ||
| 29 | var normLocationIds = LocationScopeBindingHelper.NormalizeIds(locationIds); | 34 | var normLocationIds = LocationScopeBindingHelper.NormalizeIds(locationIds); |
| 30 | var partnerContext = partnerIdsForContext is { Count: > 0 } ? partnerIdsForContext : null; | 35 | var partnerContext = partnerIdsForContext is { Count: > 0 } ? partnerIdsForContext : null; |
| 31 | 36 | ||
| 32 | - var allRegionIds = await AllScopeBindingHelper.ResolveAllRegionIdsAsync(db, partnerContext); | ||
| 33 | - var regionIsAll = normRegionIds.Count > 0 | ||
| 34 | - && allRegionIds.Count > 0 | ||
| 35 | - && AllScopeBindingHelper.IsFullIdSelection(normRegionIds, allRegionIds); | ||
| 36 | - | ||
| 37 | - var regionContextForLocations = regionIsAll ? null : normRegionIds.Count > 0 ? normRegionIds : null; | ||
| 38 | - var allLocationIds = await AllScopeBindingHelper.ResolveAllLocationIdsAsync( | ||
| 39 | - db, | ||
| 40 | - partnerContext, | ||
| 41 | - regionContextForLocations); | ||
| 42 | - var locationIsAll = normLocationIds.Count > 0 | ||
| 43 | - && allLocationIds.Count > 0 | ||
| 44 | - && AllScopeBindingHelper.IsFullIdSelection(normLocationIds, allLocationIds); | ||
| 45 | - | ||
| 46 | - var regionDisplay = regionIsAll | ||
| 47 | - ? AllScopeBindingHelper.AllRegionsDisplay | ||
| 48 | - : JoinDistinctNames(regionNames); | 37 | + string regionDisplay; |
| 38 | + if (AllScopeBindingHelper.IsDeclaredAll(appliedRegionType) | ||
| 39 | + || AllScopeBindingHelper.HasAllScopeSentinelSelection(normRegionIds)) | ||
| 40 | + { | ||
| 41 | + regionDisplay = AllScopeBindingHelper.AllRegionsDisplay; | ||
| 42 | + } | ||
| 43 | + else | ||
| 44 | + { | ||
| 45 | + var allRegionIds = await AllScopeBindingHelper.ResolveAllRegionIdsAsync(db, partnerContext); | ||
| 46 | + var regionIsAll = normRegionIds.Count > 0 | ||
| 47 | + && allRegionIds.Count > 0 | ||
| 48 | + && AllScopeBindingHelper.IsFullIdSelection(normRegionIds, allRegionIds); | ||
| 49 | + regionDisplay = regionIsAll | ||
| 50 | + ? AllScopeBindingHelper.AllRegionsDisplay | ||
| 51 | + : JoinDistinctNames(regionNames); | ||
| 52 | + } | ||
| 49 | 53 | ||
| 50 | - var locationDisplay = locationIsAll | ||
| 51 | - ? AllScopeBindingHelper.AllLocationsDisplay | ||
| 52 | - : JoinDistinctNames(locationNames); | 54 | + // 具体门店 Guid → 显示门店名;仅 AvailabilityType=ALL 或 locationIds 含 ALL 哨兵才显示 All Location |
| 55 | + string locationDisplay; | ||
| 56 | + if (AllScopeBindingHelper.HasAllScopeSentinelSelection(normLocationIds)) | ||
| 57 | + { | ||
| 58 | + locationDisplay = AllScopeBindingHelper.AllLocationsDisplay; | ||
| 59 | + } | ||
| 60 | + else | ||
| 61 | + { | ||
| 62 | + locationDisplay = JoinDistinctNames(locationNames); | ||
| 63 | + } | ||
| 53 | 64 | ||
| 54 | return (regionDisplay, locationDisplay); | 65 | return (regionDisplay, locationDisplay); |
| 55 | } | 66 | } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelAlertTimerWriteHelper.cs
0 → 100644
| 1 | +using System.Globalization; | ||
| 2 | +using FoodLabeling.Application.Services.DbModels; | ||
| 3 | +using FoodLabeling.Domain.Shared.Helpers; | ||
| 4 | +using SqlSugar; | ||
| 5 | + | ||
| 6 | +namespace FoodLabeling.Application.Helpers; | ||
| 7 | + | ||
| 8 | +/// <summary> | ||
| 9 | +/// 打印成功后写入告警计时器(按 BatchId 幂等,一条批次一条计时器)。 | ||
| 10 | +/// </summary> | ||
| 11 | +public static class LabelAlertTimerWriteHelper | ||
| 12 | +{ | ||
| 13 | + /// <summary> | ||
| 14 | + /// 根据打印批次创建告警计时器;已存在未删除记录或无有效过期时刻时直接返回。 | ||
| 15 | + /// </summary> | ||
| 16 | + public static async Task TryCreateFromPrintBatchAsync( | ||
| 17 | + ISqlSugarClient db, | ||
| 18 | + string batchId, | ||
| 19 | + string? createdBy, | ||
| 20 | + CancellationToken ct = default) | ||
| 21 | + { | ||
| 22 | + var bid = batchId?.Trim(); | ||
| 23 | + if (string.IsNullOrWhiteSpace(bid)) | ||
| 24 | + { | ||
| 25 | + return; | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + ct.ThrowIfCancellationRequested(); | ||
| 29 | + | ||
| 30 | + // BatchId 有唯一索引:含软删记录也不再插入,避免幂等补写撞唯一约束 | ||
| 31 | + var exists = await db.Queryable<FlLabelAlertTimerDbEntity>() | ||
| 32 | + .AnyAsync(x => x.BatchId == bid); | ||
| 33 | + if (exists) | ||
| 34 | + { | ||
| 35 | + return; | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + var task = (await db.Queryable<FlLabelPrintTaskDbEntity>() | ||
| 39 | + .Where(x => x.BatchId == bid) | ||
| 40 | + .OrderBy(x => x.CopyIndex) | ||
| 41 | + .Take(1) | ||
| 42 | + .ToListAsync(ct)) | ||
| 43 | + .FirstOrDefault(); | ||
| 44 | + if (task is null) | ||
| 45 | + { | ||
| 46 | + return; | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + var printedAt = task.PrintedAt ?? task.BaseTime ?? task.CreationTime; | ||
| 50 | + if (!ReportsPrintLogExpiryHelper.TryResolveExpiryDateTime( | ||
| 51 | + task.PrintInputJson, | ||
| 52 | + task.RenderTemplateJson, | ||
| 53 | + task.BaseTime, | ||
| 54 | + printedAt, | ||
| 55 | + out var expiresAt)) | ||
| 56 | + { | ||
| 57 | + return; | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + var label = (await db.Queryable<FlLabelDbEntity>() | ||
| 61 | + .Where(x => x.Id == task.LabelId) | ||
| 62 | + .Select(x => new { x.LabelName, x.LabelCode }) | ||
| 63 | + .Take(1) | ||
| 64 | + .ToListAsync(ct)) | ||
| 65 | + .FirstOrDefault(); | ||
| 66 | + | ||
| 67 | + var labelName = label?.LabelName?.Trim(); | ||
| 68 | + if (string.IsNullOrWhiteSpace(labelName)) | ||
| 69 | + { | ||
| 70 | + labelName = FoodLabelingDisplayConsts.NotAvailable; | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + string? productName = null; | ||
| 74 | + if (!string.IsNullOrWhiteSpace(task.ProductId)) | ||
| 75 | + { | ||
| 76 | + productName = (await db.Queryable<FlProductDbEntity>() | ||
| 77 | + .Where(x => x.Id == task.ProductId && !x.IsDeleted) | ||
| 78 | + .Select(x => x.ProductName) | ||
| 79 | + .Take(1) | ||
| 80 | + .ToListAsync(ct)) | ||
| 81 | + .FirstOrDefault(); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + var durationSeconds = Math.Max(0, (int)(expiresAt - printedAt).TotalSeconds); | ||
| 85 | + var title = BuildTitle(labelName, durationSeconds); | ||
| 86 | + var subtitle = BuildSubtitle(durationSeconds, expiresAt); | ||
| 87 | + var now = DateTime.Now; | ||
| 88 | + | ||
| 89 | + var entity = new FlLabelAlertTimerDbEntity | ||
| 90 | + { | ||
| 91 | + Id = YitIdHelper.NextId().ToString(), | ||
| 92 | + BatchId = bid, | ||
| 93 | + PrintTaskId = task.Id, | ||
| 94 | + LabelId = task.LabelId, | ||
| 95 | + LabelCode = label?.LabelCode?.Trim(), | ||
| 96 | + LabelName = labelName, | ||
| 97 | + ProductId = task.ProductId, | ||
| 98 | + ProductName = string.IsNullOrWhiteSpace(productName) ? null : productName.Trim(), | ||
| 99 | + LocationId = task.LocationId?.Trim() ?? string.Empty, | ||
| 100 | + PrintedAt = printedAt, | ||
| 101 | + BaseTime = task.BaseTime, | ||
| 102 | + ExpiresAt = expiresAt, | ||
| 103 | + DurationSeconds = durationSeconds, | ||
| 104 | + Title = title, | ||
| 105 | + Subtitle = subtitle, | ||
| 106 | + IsDeleted = false, | ||
| 107 | + DeletionTime = null, | ||
| 108 | + CreatedBy = createdBy, | ||
| 109 | + CreationTime = now | ||
| 110 | + }; | ||
| 111 | + | ||
| 112 | + await db.Insertable(entity).ExecuteCommandAsync(); | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + private static string BuildTitle(string labelName, int durationSeconds) | ||
| 116 | + { | ||
| 117 | + var hoursText = FormatDurationHoursLabel(durationSeconds); | ||
| 118 | + return string.IsNullOrWhiteSpace(hoursText) ? labelName : $"{labelName} ({hoursText})"; | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + private static string BuildSubtitle(int durationSeconds, DateTime expiresAt) | ||
| 122 | + { | ||
| 123 | + var hoursText = FormatDurationHoursLabel(durationSeconds); | ||
| 124 | + var timeText = expiresAt.ToString("h:mm tt", CultureInfo.CurrentCulture); | ||
| 125 | + return string.IsNullOrWhiteSpace(hoursText) | ||
| 126 | + ? $"Completes at {timeText}" | ||
| 127 | + : $"{hoursText} Completes at {timeText}"; | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + private static string FormatDurationHoursLabel(int durationSeconds) | ||
| 131 | + { | ||
| 132 | + if (durationSeconds <= 0) | ||
| 133 | + { | ||
| 134 | + return string.Empty; | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + var totalHours = durationSeconds / 3600.0; | ||
| 138 | + if (totalHours >= 1) | ||
| 139 | + { | ||
| 140 | + var rounded = (int)Math.Round(totalHours, MidpointRounding.AwayFromZero); | ||
| 141 | + rounded = Math.Max(1, rounded); | ||
| 142 | + return rounded == 1 ? "1 hour" : $"{rounded} hours"; | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + var minutes = Math.Max(1, (int)Math.Ceiling(durationSeconds / 60.0)); | ||
| 146 | + return minutes == 1 ? "1 minute" : $"{minutes} minutes"; | ||
| 147 | + } | ||
| 148 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelEntityListScopeHelper.cs
| @@ -4,12 +4,37 @@ using SqlSugar; | @@ -4,12 +4,37 @@ using SqlSugar; | ||
| 4 | namespace FoodLabeling.Application.Helpers; | 4 | namespace FoodLabeling.Application.Helpers; |
| 5 | 5 | ||
| 6 | /// <summary> | 6 | /// <summary> |
| 7 | -/// 标签类型/分类/多选项列表:非平台管理员仅可见其绑定 Company 范围内数据。 | 7 | +/// 标签类型/分类/多选项列表:按可见门店范围过滤。 |
| 8 | +/// AvailabilityType=ALL 时传任意 Region/Location 筛选均命中。 | ||
| 8 | /// </summary> | 9 | /// </summary> |
| 9 | public static class LabelEntityListScopeHelper | 10 | public static class LabelEntityListScopeHelper |
| 10 | { | 11 | { |
| 11 | /// <summary> | 12 | /// <summary> |
| 12 | - /// 标签类型列表:按 Company + Region/Location 范围过滤(Company Admin 不可见 All Companies 数据)。 | 13 | + /// 是否应用门店 Availability 过滤。 |
| 14 | + /// 仅传 <c>partnerId</c>(未传 groupId/locationId)时返回 false:只按 Company 维度筛, | ||
| 15 | + /// 避免「多公司绑定」在第二家公司暂无门店或门店未落入关联表时被误杀。 | ||
| 16 | + /// </summary> | ||
| 17 | + public static bool ShouldApplyLocationAvailabilityFilter( | ||
| 18 | + string? partnerId, | ||
| 19 | + string? groupId, | ||
| 20 | + string? locationId) | ||
| 21 | + { | ||
| 22 | + if (!string.IsNullOrWhiteSpace(groupId) || !string.IsNullOrWhiteSpace(locationId)) | ||
| 23 | + { | ||
| 24 | + return true; | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + // 仅 PartnerId:Company 过滤由 Apply*PartnerListFilter 负责 | ||
| 28 | + if (!string.IsNullOrWhiteSpace(partnerId)) | ||
| 29 | + { | ||
| 30 | + return false; | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + return true; | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + /// <summary> | ||
| 37 | + /// 标签类型列表:门店可用范围过滤(ALL 命中任意门店筛选)。 | ||
| 13 | /// </summary> | 38 | /// </summary> |
| 14 | public static ISugarQueryable<FlLabelTypeDbEntity> ApplyTypeLocationAvailabilityFilter( | 39 | public static ISugarQueryable<FlLabelTypeDbEntity> ApplyTypeLocationAvailabilityFilter( |
| 15 | ISugarQueryable<FlLabelTypeDbEntity> query, | 40 | ISugarQueryable<FlLabelTypeDbEntity> query, |
| @@ -22,12 +47,12 @@ public static class LabelEntityListScopeHelper | @@ -22,12 +47,12 @@ public static class LabelEntityListScopeHelper | ||
| 22 | 47 | ||
| 23 | if (scopedLocationIds.Count == 0) | 48 | if (scopedLocationIds.Count == 0) |
| 24 | { | 49 | { |
| 25 | - return query.Where(_ => false); | 50 | + // 无可见门店时仍保留 AvailabilityType=ALL(仅 Company 筛选时公司可能暂无门店) |
| 51 | + return query.Where(t => t.AvailabilityType == AllScopeBindingHelper.ScopeAll); | ||
| 26 | } | 52 | } |
| 27 | 53 | ||
| 28 | return query.Where(t => | 54 | return query.Where(t => |
| 29 | - (t.AvailabilityType == AllScopeBindingHelper.ScopeAll | ||
| 30 | - && t.AppliedPartnerType == LabelEntityPartnerScopeHelper.ScopeSpecified) | 55 | + t.AvailabilityType == AllScopeBindingHelper.ScopeAll |
| 31 | || (t.AvailabilityType == AllScopeBindingHelper.ScopeSpecified | 56 | || (t.AvailabilityType == AllScopeBindingHelper.ScopeSpecified |
| 32 | && SqlFunc.Subqueryable<FlLabelTypeLocationDbEntity>() | 57 | && SqlFunc.Subqueryable<FlLabelTypeLocationDbEntity>() |
| 33 | .Where(tl => tl.LabelTypeId == t.Id && scopedLocationIds.Contains(tl.LocationId)) | 58 | .Where(tl => tl.LabelTypeId == t.Id && scopedLocationIds.Contains(tl.LocationId)) |
| @@ -35,7 +60,7 @@ public static class LabelEntityListScopeHelper | @@ -35,7 +60,7 @@ public static class LabelEntityListScopeHelper | ||
| 35 | } | 60 | } |
| 36 | 61 | ||
| 37 | /// <summary> | 62 | /// <summary> |
| 38 | - /// 标签分类列表:按 Company + Region/Location 范围过滤。 | 63 | + /// 标签分类列表:门店可用范围过滤(ALL 命中任意门店筛选)。 |
| 39 | /// </summary> | 64 | /// </summary> |
| 40 | public static ISugarQueryable<FlLabelCategoryDbEntity> ApplyCategoryLocationAvailabilityFilter( | 65 | public static ISugarQueryable<FlLabelCategoryDbEntity> ApplyCategoryLocationAvailabilityFilter( |
| 41 | ISugarQueryable<FlLabelCategoryDbEntity> query, | 66 | ISugarQueryable<FlLabelCategoryDbEntity> query, |
| @@ -48,12 +73,11 @@ public static class LabelEntityListScopeHelper | @@ -48,12 +73,11 @@ public static class LabelEntityListScopeHelper | ||
| 48 | 73 | ||
| 49 | if (scopedLocationIds.Count == 0) | 74 | if (scopedLocationIds.Count == 0) |
| 50 | { | 75 | { |
| 51 | - return query.Where(_ => false); | 76 | + return query.Where(c => c.AvailabilityType == AllScopeBindingHelper.ScopeAll); |
| 52 | } | 77 | } |
| 53 | 78 | ||
| 54 | return query.Where(c => | 79 | return query.Where(c => |
| 55 | - (c.AvailabilityType == AllScopeBindingHelper.ScopeAll | ||
| 56 | - && c.AppliedPartnerType == LabelEntityPartnerScopeHelper.ScopeSpecified) | 80 | + c.AvailabilityType == AllScopeBindingHelper.ScopeAll |
| 57 | || (c.AvailabilityType == AllScopeBindingHelper.ScopeSpecified | 81 | || (c.AvailabilityType == AllScopeBindingHelper.ScopeSpecified |
| 58 | && SqlFunc.Subqueryable<FlLabelCategoryLocationDbEntity>() | 82 | && SqlFunc.Subqueryable<FlLabelCategoryLocationDbEntity>() |
| 59 | .Where(cl => cl.CategoryId == c.Id && scopedLocationIds.Contains(cl.LocationId)) | 83 | .Where(cl => cl.CategoryId == c.Id && scopedLocationIds.Contains(cl.LocationId)) |
| @@ -61,7 +85,7 @@ public static class LabelEntityListScopeHelper | @@ -61,7 +85,7 @@ public static class LabelEntityListScopeHelper | ||
| 61 | } | 85 | } |
| 62 | 86 | ||
| 63 | /// <summary> | 87 | /// <summary> |
| 64 | - /// 标签多选项列表:按 Company + Region/Location 范围过滤。 | 88 | + /// 标签多选项列表:门店可用范围过滤(ALL 命中任意门店筛选)。 |
| 65 | /// </summary> | 89 | /// </summary> |
| 66 | public static ISugarQueryable<FlLabelMultipleOptionDbEntity> ApplyMultipleOptionLocationAvailabilityFilter( | 90 | public static ISugarQueryable<FlLabelMultipleOptionDbEntity> ApplyMultipleOptionLocationAvailabilityFilter( |
| 67 | ISugarQueryable<FlLabelMultipleOptionDbEntity> query, | 91 | ISugarQueryable<FlLabelMultipleOptionDbEntity> query, |
| @@ -74,12 +98,11 @@ public static class LabelEntityListScopeHelper | @@ -74,12 +98,11 @@ public static class LabelEntityListScopeHelper | ||
| 74 | 98 | ||
| 75 | if (scopedLocationIds.Count == 0) | 99 | if (scopedLocationIds.Count == 0) |
| 76 | { | 100 | { |
| 77 | - return query.Where(_ => false); | 101 | + return query.Where(o => o.AvailabilityType == AllScopeBindingHelper.ScopeAll); |
| 78 | } | 102 | } |
| 79 | 103 | ||
| 80 | return query.Where(o => | 104 | return query.Where(o => |
| 81 | - (o.AvailabilityType == AllScopeBindingHelper.ScopeAll | ||
| 82 | - && o.AppliedPartnerType == LabelEntityPartnerScopeHelper.ScopeSpecified) | 105 | + o.AvailabilityType == AllScopeBindingHelper.ScopeAll |
| 83 | || (o.AvailabilityType == AllScopeBindingHelper.ScopeSpecified | 106 | || (o.AvailabilityType == AllScopeBindingHelper.ScopeSpecified |
| 84 | && SqlFunc.Subqueryable<FlLabelMultipleOptionLocationDbEntity>() | 107 | && SqlFunc.Subqueryable<FlLabelMultipleOptionLocationDbEntity>() |
| 85 | .Where(ol => ol.MultipleOptionId == o.Id && scopedLocationIds.Contains(ol.LocationId)) | 108 | .Where(ol => ol.MultipleOptionId == o.Id && scopedLocationIds.Contains(ol.LocationId)) |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelEntityPartnerScopeHelper.cs
| @@ -26,7 +26,10 @@ public static class LabelEntityPartnerScopeHelper | @@ -26,7 +26,10 @@ public static class LabelEntityPartnerScopeHelper | ||
| 26 | Category, | 26 | Category, |
| 27 | 27 | ||
| 28 | /// <summary>标签多选项 <c>fl_label_multiple_option</c></summary> | 28 | /// <summary>标签多选项 <c>fl_label_multiple_option</c></summary> |
| 29 | - MultipleOption | 29 | + MultipleOption, |
| 30 | + | ||
| 31 | + /// <summary>产品分类 <c>fl_product_category</c></summary> | ||
| 32 | + ProductCategory | ||
| 30 | } | 33 | } |
| 31 | 34 | ||
| 32 | public sealed class LabelEntityPartnerScopeSaveResult | 35 | public sealed class LabelEntityPartnerScopeSaveResult |
| @@ -60,6 +63,10 @@ public static class LabelEntityPartnerScopeHelper | @@ -60,6 +63,10 @@ public static class LabelEntityPartnerScopeHelper | ||
| 60 | public bool HasCategoryPartnerTable { get; init; } | 63 | public bool HasCategoryPartnerTable { get; init; } |
| 61 | 64 | ||
| 62 | public bool HasMultipleOptionPartnerTable { get; init; } | 65 | public bool HasMultipleOptionPartnerTable { get; init; } |
| 66 | + | ||
| 67 | + public bool HasProductCategoryPartnerColumn { get; init; } | ||
| 68 | + | ||
| 69 | + public bool HasProductCategoryPartnerTable { get; init; } | ||
| 63 | } | 70 | } |
| 64 | 71 | ||
| 65 | /// <summary> | 72 | /// <summary> |
| @@ -185,7 +192,7 @@ public static class LabelEntityPartnerScopeHelper | @@ -185,7 +192,7 @@ public static class LabelEntityPartnerScopeHelper | ||
| 185 | if (needsPartnerRows && !hasTable) | 192 | if (needsPartnerRows && !hasTable) |
| 186 | { | 193 | { |
| 187 | throw new UserFriendlyException( | 194 | throw new UserFriendlyException( |
| 188 | - "Company 适用范围关联表尚未就绪,请联系管理员执行数据库迁移(fl_label_entity_partner_scope.sql)后重试"); | 195 | + "Company 适用范围关联表尚未就绪,请联系管理员执行数据库迁移(fl_label_entity_partner_scope.sql 或 fl_product_category_partner_scope.sql)后重试"); |
| 189 | } | 196 | } |
| 190 | 197 | ||
| 191 | if (hasTable) | 198 | if (hasTable) |
| @@ -325,11 +332,30 @@ public static class LabelEntityPartnerScopeHelper | @@ -325,11 +332,30 @@ public static class LabelEntityPartnerScopeHelper | ||
| 325 | ? AllCompaniesDisplay | 332 | ? AllCompaniesDisplay |
| 326 | : BuildCompanyDisplay(pIds, partnerNameById); | 333 | : BuildCompanyDisplay(pIds, partnerNameById); |
| 327 | 334 | ||
| 335 | + List<string> displayPartnerIds; | ||
| 336 | + if (string.Equals(partnerType, ScopeAll, StringComparison.OrdinalIgnoreCase)) | ||
| 337 | + { | ||
| 338 | + displayPartnerIds = new List<string> { AllScopeBindingHelper.ScopeAll }; | ||
| 339 | + } | ||
| 340 | + else | ||
| 341 | + { | ||
| 342 | + var collapsed = await ScopeAllEchoHelper.CollapseScopeIdsToAllSentinelAsync( | ||
| 343 | + db, | ||
| 344 | + pIds, | ||
| 345 | + null, | ||
| 346 | + null, | ||
| 347 | + new ScopeAllEchoHelper.ScopeAllEchoOptions | ||
| 348 | + { | ||
| 349 | + AppliedPartnerType = partnerType | ||
| 350 | + }); | ||
| 351 | + displayPartnerIds = collapsed.PartnerIds; | ||
| 352 | + } | ||
| 353 | + | ||
| 328 | result[entityId] = new LabelEntityPartnerScopeDisplay | 354 | result[entityId] = new LabelEntityPartnerScopeDisplay |
| 329 | { | 355 | { |
| 330 | Company = companyDisplay, | 356 | Company = companyDisplay, |
| 331 | AppliedPartnerType = partnerType, | 357 | AppliedPartnerType = partnerType, |
| 332 | - PartnerIds = pIds | 358 | + PartnerIds = displayPartnerIds |
| 333 | }; | 359 | }; |
| 334 | } | 360 | } |
| 335 | 361 | ||
| @@ -383,11 +409,13 @@ public static class LabelEntityPartnerScopeHelper | @@ -383,11 +409,13 @@ public static class LabelEntityPartnerScopeHelper | ||
| 383 | return query.Where(_ => false); | 409 | return query.Where(_ => false); |
| 384 | } | 410 | } |
| 385 | 411 | ||
| 412 | + // Company=ALL:传任意 partnerId 均命中;SPECIFIED:须与关联表有交集 | ||
| 386 | return query.Where(t => | 413 | return query.Where(t => |
| 387 | - t.AppliedPartnerType == ScopeSpecified | ||
| 388 | - && SqlFunc.Subqueryable<FlLabelTypePartnerDbEntity>() | ||
| 389 | - .Where(p => p.LabelTypeId == t.Id && scopedPartnerIds.Contains(p.PartnerId)) | ||
| 390 | - .Any()); | 414 | + t.AppliedPartnerType == ScopeAll |
| 415 | + || (t.AppliedPartnerType == ScopeSpecified | ||
| 416 | + && SqlFunc.Subqueryable<FlLabelTypePartnerDbEntity>() | ||
| 417 | + .Where(p => p.LabelTypeId == t.Id && scopedPartnerIds.Contains(p.PartnerId)) | ||
| 418 | + .Any())); | ||
| 391 | } | 419 | } |
| 392 | 420 | ||
| 393 | /// <summary> | 421 | /// <summary> |
| @@ -415,10 +443,11 @@ public static class LabelEntityPartnerScopeHelper | @@ -415,10 +443,11 @@ public static class LabelEntityPartnerScopeHelper | ||
| 415 | } | 443 | } |
| 416 | 444 | ||
| 417 | return query.Where(c => | 445 | return query.Where(c => |
| 418 | - c.AppliedPartnerType == ScopeSpecified | ||
| 419 | - && SqlFunc.Subqueryable<FlLabelCategoryPartnerDbEntity>() | ||
| 420 | - .Where(p => p.CategoryId == c.Id && scopedPartnerIds.Contains(p.PartnerId)) | ||
| 421 | - .Any()); | 446 | + c.AppliedPartnerType == ScopeAll |
| 447 | + || (c.AppliedPartnerType == ScopeSpecified | ||
| 448 | + && SqlFunc.Subqueryable<FlLabelCategoryPartnerDbEntity>() | ||
| 449 | + .Where(p => p.CategoryId == c.Id && scopedPartnerIds.Contains(p.PartnerId)) | ||
| 450 | + .Any())); | ||
| 422 | } | 451 | } |
| 423 | 452 | ||
| 424 | /// <summary> | 453 | /// <summary> |
| @@ -446,14 +475,48 @@ public static class LabelEntityPartnerScopeHelper | @@ -446,14 +475,48 @@ public static class LabelEntityPartnerScopeHelper | ||
| 446 | } | 475 | } |
| 447 | 476 | ||
| 448 | return query.Where(o => | 477 | return query.Where(o => |
| 449 | - o.AppliedPartnerType == ScopeSpecified | ||
| 450 | - && SqlFunc.Subqueryable<FlLabelMultipleOptionPartnerDbEntity>() | ||
| 451 | - .Where(p => p.MultipleOptionId == o.Id && scopedPartnerIds.Contains(p.PartnerId)) | ||
| 452 | - .Any()); | 478 | + o.AppliedPartnerType == ScopeAll |
| 479 | + || (o.AppliedPartnerType == ScopeSpecified | ||
| 480 | + && SqlFunc.Subqueryable<FlLabelMultipleOptionPartnerDbEntity>() | ||
| 481 | + .Where(p => p.MultipleOptionId == o.Id && scopedPartnerIds.Contains(p.PartnerId)) | ||
| 482 | + .Any())); | ||
| 483 | + } | ||
| 484 | + | ||
| 485 | + /// <summary> | ||
| 486 | + /// 列表筛选:按可见 Company 过滤产品分类。 | ||
| 487 | + /// </summary> | ||
| 488 | + public static async Task<ISugarQueryable<FlProductCategoryDbEntity>> ApplyProductCategoryPartnerListFilterAsync( | ||
| 489 | + ISqlSugarClient db, | ||
| 490 | + ISugarQueryable<FlProductCategoryDbEntity> query, | ||
| 491 | + IReadOnlyList<string>? scopedPartnerIds) | ||
| 492 | + { | ||
| 493 | + if (scopedPartnerIds is null) | ||
| 494 | + { | ||
| 495 | + return query; | ||
| 496 | + } | ||
| 497 | + | ||
| 498 | + var schema = await GetSchemaStatusAsync(db); | ||
| 499 | + if (!HasPartnerTableForKind(schema, LabelEntityPartnerKind.ProductCategory)) | ||
| 500 | + { | ||
| 501 | + return query; | ||
| 502 | + } | ||
| 503 | + | ||
| 504 | + if (scopedPartnerIds.Count == 0) | ||
| 505 | + { | ||
| 506 | + return query.Where(_ => false); | ||
| 507 | + } | ||
| 508 | + | ||
| 509 | + return query.Where(c => | ||
| 510 | + c.AppliedPartnerType == ScopeAll | ||
| 511 | + || (c.AppliedPartnerType == ScopeSpecified | ||
| 512 | + && SqlFunc.Subqueryable<FlProductCategoryPartnerDbEntity>() | ||
| 513 | + .Where(p => p.CategoryId == c.Id && scopedPartnerIds.Contains(p.PartnerId)) | ||
| 514 | + .Any())); | ||
| 453 | } | 515 | } |
| 454 | 516 | ||
| 455 | /// <summary> | 517 | /// <summary> |
| 456 | /// 由 Query <c>partnerId</c> 解析列表 Company 筛选 Id;未传则返回 <c>null</c>。 | 518 | /// 由 Query <c>partnerId</c> 解析列表 Company 筛选 Id;未传则返回 <c>null</c>。 |
| 519 | + /// Id 统一为小写 Guid,避免与关联表大小写不一致导致 IN 匹配失败。 | ||
| 457 | /// </summary> | 520 | /// </summary> |
| 458 | public static async Task<List<string>?> ResolveScopedPartnerIdsForListAsync( | 521 | public static async Task<List<string>?> ResolveScopedPartnerIdsForListAsync( |
| 459 | ISqlSugarClient db, | 522 | ISqlSugarClient db, |
| @@ -467,7 +530,13 @@ public static class LabelEntityPartnerScopeHelper | @@ -467,7 +530,13 @@ public static class LabelEntityPartnerScopeHelper | ||
| 467 | 530 | ||
| 468 | var exists = await db.Queryable<FlPartnerDbEntity>() | 531 | var exists = await db.Queryable<FlPartnerDbEntity>() |
| 469 | .AnyAsync(x => !x.IsDeleted && x.Id == pid); | 532 | .AnyAsync(x => !x.IsDeleted && x.Id == pid); |
| 470 | - return exists ? new List<string> { pid } : new List<string>(); | 533 | + if (!exists) |
| 534 | + { | ||
| 535 | + return new List<string>(); | ||
| 536 | + } | ||
| 537 | + | ||
| 538 | + var key = TeamMemberListScopeHelper.NormalizeScopeKey(pid); | ||
| 539 | + return string.IsNullOrEmpty(key) ? new List<string> { pid } : new List<string> { key }; | ||
| 471 | } | 540 | } |
| 472 | 541 | ||
| 473 | /// <summary> | 542 | /// <summary> |
| @@ -514,6 +583,11 @@ public static class LabelEntityPartnerScopeHelper | @@ -514,6 +583,11 @@ public static class LabelEntityPartnerScopeHelper | ||
| 514 | .Where(x => x.MultipleOptionId == entityId) | 583 | .Where(x => x.MultipleOptionId == entityId) |
| 515 | .ExecuteCommandAsync(); | 584 | .ExecuteCommandAsync(); |
| 516 | break; | 585 | break; |
| 586 | + case LabelEntityPartnerKind.ProductCategory: | ||
| 587 | + await db.Deleteable<FlProductCategoryPartnerDbEntity>() | ||
| 588 | + .Where(x => x.CategoryId == entityId) | ||
| 589 | + .ExecuteCommandAsync(); | ||
| 590 | + break; | ||
| 517 | } | 591 | } |
| 518 | } | 592 | } |
| 519 | 593 | ||
| @@ -564,7 +638,10 @@ public static class LabelEntityPartnerScopeHelper | @@ -564,7 +638,10 @@ public static class LabelEntityPartnerScopeHelper | ||
| 564 | db, "fl_label_multiple_option", "AppliedPartnerType"), | 638 | db, "fl_label_multiple_option", "AppliedPartnerType"), |
| 565 | HasTypePartnerTable = await TableExistsAsync(db, "fl_label_type_partner"), | 639 | HasTypePartnerTable = await TableExistsAsync(db, "fl_label_type_partner"), |
| 566 | HasCategoryPartnerTable = await TableExistsAsync(db, "fl_label_category_partner"), | 640 | HasCategoryPartnerTable = await TableExistsAsync(db, "fl_label_category_partner"), |
| 567 | - HasMultipleOptionPartnerTable = await TableExistsAsync(db, "fl_label_multiple_option_partner") | 641 | + HasMultipleOptionPartnerTable = await TableExistsAsync(db, "fl_label_multiple_option_partner"), |
| 642 | + HasProductCategoryPartnerColumn = await ColumnExistsAsync( | ||
| 643 | + db, "fl_product_category", "AppliedPartnerType"), | ||
| 644 | + HasProductCategoryPartnerTable = await TableExistsAsync(db, "fl_product_category_partner") | ||
| 568 | }; | 645 | }; |
| 569 | } | 646 | } |
| 570 | catch | 647 | catch |
| @@ -581,6 +658,7 @@ public static class LabelEntityPartnerScopeHelper | @@ -581,6 +658,7 @@ public static class LabelEntityPartnerScopeHelper | ||
| 581 | LabelEntityPartnerKind.Type => schema.HasTypePartnerTable, | 658 | LabelEntityPartnerKind.Type => schema.HasTypePartnerTable, |
| 582 | LabelEntityPartnerKind.Category => schema.HasCategoryPartnerTable, | 659 | LabelEntityPartnerKind.Category => schema.HasCategoryPartnerTable, |
| 583 | LabelEntityPartnerKind.MultipleOption => schema.HasMultipleOptionPartnerTable, | 660 | LabelEntityPartnerKind.MultipleOption => schema.HasMultipleOptionPartnerTable, |
| 661 | + LabelEntityPartnerKind.ProductCategory => schema.HasProductCategoryPartnerTable, | ||
| 584 | _ => false | 662 | _ => false |
| 585 | }; | 663 | }; |
| 586 | 664 | ||
| @@ -590,6 +668,7 @@ public static class LabelEntityPartnerScopeHelper | @@ -590,6 +668,7 @@ public static class LabelEntityPartnerScopeHelper | ||
| 590 | LabelEntityPartnerKind.Type => schema.HasTypePartnerColumn, | 668 | LabelEntityPartnerKind.Type => schema.HasTypePartnerColumn, |
| 591 | LabelEntityPartnerKind.Category => schema.HasCategoryPartnerColumn, | 669 | LabelEntityPartnerKind.Category => schema.HasCategoryPartnerColumn, |
| 592 | LabelEntityPartnerKind.MultipleOption => schema.HasMultipleOptionPartnerColumn, | 670 | LabelEntityPartnerKind.MultipleOption => schema.HasMultipleOptionPartnerColumn, |
| 671 | + LabelEntityPartnerKind.ProductCategory => schema.HasProductCategoryPartnerColumn, | ||
| 593 | _ => false | 672 | _ => false |
| 594 | }; | 673 | }; |
| 595 | 674 | ||
| @@ -637,6 +716,7 @@ public static class LabelEntityPartnerScopeHelper | @@ -637,6 +716,7 @@ public static class LabelEntityPartnerScopeHelper | ||
| 637 | LabelEntityPartnerKind.Type => "fl_label_type", | 716 | LabelEntityPartnerKind.Type => "fl_label_type", |
| 638 | LabelEntityPartnerKind.Category => "fl_label_category", | 717 | LabelEntityPartnerKind.Category => "fl_label_category", |
| 639 | LabelEntityPartnerKind.MultipleOption => "fl_label_multiple_option", | 718 | LabelEntityPartnerKind.MultipleOption => "fl_label_multiple_option", |
| 719 | + LabelEntityPartnerKind.ProductCategory => "fl_product_category", | ||
| 640 | _ => string.Empty | 720 | _ => string.Empty |
| 641 | }; | 721 | }; |
| 642 | 722 | ||
| @@ -666,6 +746,7 @@ public static class LabelEntityPartnerScopeHelper | @@ -666,6 +746,7 @@ public static class LabelEntityPartnerScopeHelper | ||
| 666 | LabelEntityPartnerKind.Type => "fl_label_type", | 746 | LabelEntityPartnerKind.Type => "fl_label_type", |
| 667 | LabelEntityPartnerKind.Category => "fl_label_category", | 747 | LabelEntityPartnerKind.Category => "fl_label_category", |
| 668 | LabelEntityPartnerKind.MultipleOption => "fl_label_multiple_option", | 748 | LabelEntityPartnerKind.MultipleOption => "fl_label_multiple_option", |
| 749 | + LabelEntityPartnerKind.ProductCategory => "fl_product_category", | ||
| 669 | _ => string.Empty | 750 | _ => string.Empty |
| 670 | }; | 751 | }; |
| 671 | 752 | ||
| @@ -709,6 +790,13 @@ public static class LabelEntityPartnerScopeHelper | @@ -709,6 +790,13 @@ public static class LabelEntityPartnerScopeHelper | ||
| 709 | .ToListAsync(); | 790 | .ToListAsync(); |
| 710 | return rows.Select(x => (x.MultipleOptionId, x.PartnerId)).ToList(); | 791 | return rows.Select(x => (x.MultipleOptionId, x.PartnerId)).ToList(); |
| 711 | } | 792 | } |
| 793 | + case LabelEntityPartnerKind.ProductCategory: | ||
| 794 | + { | ||
| 795 | + var rows = await db.Queryable<FlProductCategoryPartnerDbEntity>() | ||
| 796 | + .Where(x => entityIds.Contains(x.CategoryId)) | ||
| 797 | + .ToListAsync(); | ||
| 798 | + return rows.Select(x => (x.CategoryId, x.PartnerId)).ToList(); | ||
| 799 | + } | ||
| 712 | default: | 800 | default: |
| 713 | return new List<(string EntityId, string PartnerId)>(); | 801 | return new List<(string EntityId, string PartnerId)>(); |
| 714 | } | 802 | } |
| @@ -764,6 +852,19 @@ public static class LabelEntityPartnerScopeHelper | @@ -764,6 +852,19 @@ public static class LabelEntityPartnerScopeHelper | ||
| 764 | await db.Insertable(rows).ExecuteCommandAsync(); | 852 | await db.Insertable(rows).ExecuteCommandAsync(); |
| 765 | break; | 853 | break; |
| 766 | } | 854 | } |
| 855 | + case LabelEntityPartnerKind.ProductCategory: | ||
| 856 | + { | ||
| 857 | + var rows = partnerIds.Select(pid => new FlProductCategoryPartnerDbEntity | ||
| 858 | + { | ||
| 859 | + Id = guidGenerator.Create().ToString(), | ||
| 860 | + CategoryId = entityId, | ||
| 861 | + PartnerId = pid, | ||
| 862 | + CreationTime = now, | ||
| 863 | + CreatorId = currentUserId | ||
| 864 | + }).ToList(); | ||
| 865 | + await db.Insertable(rows).ExecuteCommandAsync(); | ||
| 866 | + break; | ||
| 867 | + } | ||
| 767 | } | 868 | } |
| 768 | } | 869 | } |
| 769 | 870 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelQueryHelper.cs
| @@ -28,6 +28,7 @@ public static class LabelQueryHelper | @@ -28,6 +28,7 @@ public static class LabelQueryHelper | ||
| 28 | LabelName = x.LabelName, | 28 | LabelName = x.LabelName, |
| 29 | TemplateId = x.TemplateId, | 29 | TemplateId = x.TemplateId, |
| 30 | LocationId = x.LocationId, | 30 | LocationId = x.LocationId, |
| 31 | + PartnerId = x.PartnerId, | ||
| 31 | LabelCategoryId = x.LabelCategoryId, | 32 | LabelCategoryId = x.LabelCategoryId, |
| 32 | LabelTypeId = x.LabelTypeId, | 33 | LabelTypeId = x.LabelTypeId, |
| 33 | LabelType = x.LabelType, | 34 | LabelType = x.LabelType, |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelRegionScopeHelper.cs
| @@ -58,12 +58,40 @@ public static class LabelRegionScopeHelper | @@ -58,12 +58,40 @@ public static class LabelRegionScopeHelper | ||
| 58 | IReadOnlyList<string>? regionIds, | 58 | IReadOnlyList<string>? regionIds, |
| 59 | IReadOnlyList<string>? groupIds, | 59 | IReadOnlyList<string>? groupIds, |
| 60 | string? locationId, | 60 | string? locationId, |
| 61 | - IReadOnlyList<string>? locationIds) | 61 | + IReadOnlyList<string>? locationIds, |
| 62 | + IReadOnlyList<string>? partnerIdsForContext = null) | ||
| 62 | { | 63 | { |
| 63 | - var mergedRegionIds = NormalizeRegionIds(regionIds, groupIds); | ||
| 64 | - var explicitLocationIds = MergeExplicitLocationIds(locationId, locationIds); | 64 | + var mergedRegionIdsRaw = NormalizeRegionIds(regionIds, groupIds); |
| 65 | + var explicitLocationIdsRaw = MergeExplicitLocationIds(locationId, locationIds); | ||
| 66 | + var regionHasAll = LocationScopeBindingHelper.ContainsAllScopeSentinel(mergedRegionIdsRaw); | ||
| 67 | + var locationHasAll = LocationScopeBindingHelper.ContainsAllScopeSentinel(explicitLocationIdsRaw); | ||
| 68 | + var mergedRegionIds = LocationScopeBindingHelper.FilterConcreteScopeIds(mergedRegionIdsRaw); | ||
| 69 | + var explicitLocationIds = LocationScopeBindingHelper.FilterConcreteScopeIds(explicitLocationIdsRaw); | ||
| 65 | var type = (appliedRegionType ?? AppliedRegionAll).Trim().ToUpperInvariant(); | 70 | var type = (appliedRegionType ?? AppliedRegionAll).Trim().ToUpperInvariant(); |
| 66 | var hasScopeArrays = regionIds is not null || groupIds is not null || locationIds is not null; | 71 | var hasScopeArrays = regionIds is not null || groupIds is not null || locationIds is not null; |
| 72 | + var partnerContext = LocationScopeBindingHelper.NormalizeIds(partnerIdsForContext); | ||
| 73 | + | ||
| 74 | + // locationIds / regionIds 含 ALL 哨兵:不归档为 Guid 校验,按全选处理(Company 由 fl_label.PartnerId 落库) | ||
| 75 | + if (locationHasAll || (regionHasAll && explicitLocationIds.Count == 0)) | ||
| 76 | + { | ||
| 77 | + return new LabelRegionScopeSaveResult | ||
| 78 | + { | ||
| 79 | + AppliedRegionType = AppliedRegionAll, | ||
| 80 | + RegionIds = new List<string>(), | ||
| 81 | + LocationIds = new List<string>() | ||
| 82 | + }; | ||
| 83 | + } | ||
| 84 | + | ||
| 85 | + if (regionHasAll && explicitLocationIds.Count > 0) | ||
| 86 | + { | ||
| 87 | + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(db, explicitLocationIds); | ||
| 88 | + return new LabelRegionScopeSaveResult | ||
| 89 | + { | ||
| 90 | + AppliedRegionType = AppliedRegionAll, | ||
| 91 | + RegionIds = new List<string>(), | ||
| 92 | + LocationIds = explicitLocationIds | ||
| 93 | + }; | ||
| 94 | + } | ||
| 67 | 95 | ||
| 68 | if (mergedRegionIds.Count > 0) | 96 | if (mergedRegionIds.Count > 0) |
| 69 | { | 97 | { |
| @@ -95,8 +123,10 @@ public static class LabelRegionScopeHelper | @@ -95,8 +123,10 @@ public static class LabelRegionScopeHelper | ||
| 95 | await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(db, explicitLocationIds); | 123 | await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(db, explicitLocationIds); |
| 96 | } | 124 | } |
| 97 | 125 | ||
| 98 | - var partnerContext = await ResolvePartnerContextAsync(db, mergedRegionIds, explicitLocationIds); | ||
| 99 | - var allRegions = await AllScopeBindingHelper.ResolveAllRegionIdsAsync(db, partnerContext); | 126 | + var resolvedPartnerContext = partnerContext.Count > 0 |
| 127 | + ? partnerContext | ||
| 128 | + : await ResolvePartnerContextAsync(db, mergedRegionIds, explicitLocationIds); | ||
| 129 | + var allRegions = await AllScopeBindingHelper.ResolveAllRegionIdsAsync(db, resolvedPartnerContext); | ||
| 100 | var regionsFull = mergedRegionIds.Count > 0 | 130 | var regionsFull = mergedRegionIds.Count > 0 |
| 101 | && AllScopeBindingHelper.IsFullIdSelection(mergedRegionIds, allRegions); | 131 | && AllScopeBindingHelper.IsFullIdSelection(mergedRegionIds, allRegions); |
| 102 | 132 | ||
| @@ -107,7 +137,7 @@ public static class LabelRegionScopeHelper | @@ -107,7 +137,7 @@ public static class LabelRegionScopeHelper | ||
| 107 | if (declaredOrFullAll) | 137 | if (declaredOrFullAll) |
| 108 | { | 138 | { |
| 109 | var allLocationsForPartner = await AllScopeBindingHelper.ResolveAllLocationIdsAsync( | 139 | var allLocationsForPartner = await AllScopeBindingHelper.ResolveAllLocationIdsAsync( |
| 110 | - db, partnerContext, null); | 140 | + db, resolvedPartnerContext, null); |
| 111 | var locationsFullForPartner = explicitLocationIds.Count == 0 | 141 | var locationsFullForPartner = explicitLocationIds.Count == 0 |
| 112 | || (allLocationsForPartner.Count > 0 | 142 | || (allLocationsForPartner.Count > 0 |
| 113 | && AllScopeBindingHelper.IsFullIdSelection(explicitLocationIds, allLocationsForPartner)); | 143 | && AllScopeBindingHelper.IsFullIdSelection(explicitLocationIds, allLocationsForPartner)); |
| @@ -135,7 +165,7 @@ public static class LabelRegionScopeHelper | @@ -135,7 +165,7 @@ public static class LabelRegionScopeHelper | ||
| 135 | if (mergedRegionIds.Count == 0 && explicitLocationIds.Count > 0) | 165 | if (mergedRegionIds.Count == 0 && explicitLocationIds.Count > 0) |
| 136 | { | 166 | { |
| 137 | var allLocations = await AllScopeBindingHelper.ResolveAllLocationIdsAsync( | 167 | var allLocations = await AllScopeBindingHelper.ResolveAllLocationIdsAsync( |
| 138 | - db, partnerContext, null); | 168 | + db, resolvedPartnerContext, null); |
| 139 | if (allLocations.Count > 0 | 169 | if (allLocations.Count > 0 |
| 140 | && AllScopeBindingHelper.IsFullIdSelection(explicitLocationIds, allLocations)) | 170 | && AllScopeBindingHelper.IsFullIdSelection(explicitLocationIds, allLocations)) |
| 141 | { | 171 | { |
| @@ -157,7 +187,7 @@ public static class LabelRegionScopeHelper | @@ -157,7 +187,7 @@ public static class LabelRegionScopeHelper | ||
| 157 | 187 | ||
| 158 | // 部分 Region:门店 Select All(空或覆盖该区域全集)→ 只落 Region,不写门店快照(新区店靠 Region 动态匹配) | 188 | // 部分 Region:门店 Select All(空或覆盖该区域全集)→ 只落 Region,不写门店快照(新区店靠 Region 动态匹配) |
| 159 | var allLocationsInRegions = await AllScopeBindingHelper.ResolveAllLocationIdsAsync( | 189 | var allLocationsInRegions = await AllScopeBindingHelper.ResolveAllLocationIdsAsync( |
| 160 | - db, null, mergedRegionIds); | 190 | + db, resolvedPartnerContext, mergedRegionIds); |
| 161 | var locationsFullInRegions = explicitLocationIds.Count == 0 | 191 | var locationsFullInRegions = explicitLocationIds.Count == 0 |
| 162 | || (allLocationsInRegions.Count > 0 | 192 | || (allLocationsInRegions.Count > 0 |
| 163 | && AllScopeBindingHelper.IsFullIdSelection(explicitLocationIds, allLocationsInRegions)); | 193 | && AllScopeBindingHelper.IsFullIdSelection(explicitLocationIds, allLocationsInRegions)); |
| @@ -179,7 +209,7 @@ public static class LabelRegionScopeHelper | @@ -179,7 +209,7 @@ public static class LabelRegionScopeHelper | ||
| 179 | 209 | ||
| 180 | // 部分 Region + 部分门店 → 校验门店属于所选 Region 后落快照 | 210 | // 部分 Region + 部分门店 → 校验门店属于所选 Region 后落快照 |
| 181 | var mergedLocations = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | 211 | var mergedLocations = await LocationScopeBindingHelper.MergeToLocationIdsAsync( |
| 182 | - db, (IReadOnlyList<string>?)null, mergedRegionIds, explicitLocationIds); | 212 | + db, resolvedPartnerContext, mergedRegionIds, explicitLocationIds); |
| 183 | if (mergedLocations.Count == 0) | 213 | if (mergedLocations.Count == 0) |
| 184 | { | 214 | { |
| 185 | throw new UserFriendlyException("指定 Region 下未匹配到有效门店,请检查 Region 或 locationIds"); | 215 | throw new UserFriendlyException("指定 Region 下未匹配到有效门店,请检查 Region 或 locationIds"); |
| @@ -405,8 +435,8 @@ public static class LabelRegionScopeHelper | @@ -405,8 +435,8 @@ public static class LabelRegionScopeHelper | ||
| 405 | return new LabelRegionScopeDisplay | 435 | return new LabelRegionScopeDisplay |
| 406 | { | 436 | { |
| 407 | Region = AllRegionsDisplay, | 437 | Region = AllRegionsDisplay, |
| 408 | - RegionIds = new List<string>(), | ||
| 409 | - GroupIds = new List<string>() | 438 | + RegionIds = new List<string> { AllScopeBindingHelper.ScopeAll }, |
| 439 | + GroupIds = new List<string> { AllScopeBindingHelper.ScopeAll } | ||
| 410 | }; | 440 | }; |
| 411 | } | 441 | } |
| 412 | 442 | ||
| @@ -435,11 +465,18 @@ public static class LabelRegionScopeHelper | @@ -435,11 +465,18 @@ public static class LabelRegionScopeHelper | ||
| 435 | ? string.Join(", ", names.Where(n => !string.IsNullOrWhiteSpace(n)).Select(n => n!.Trim()).Distinct()) | 465 | ? string.Join(", ", names.Where(n => !string.IsNullOrWhiteSpace(n)).Select(n => n!.Trim()).Distinct()) |
| 436 | : "?"; | 466 | : "?"; |
| 437 | 467 | ||
| 468 | + var collapsed = await ScopeAllEchoHelper.CollapseScopeIdsToAllSentinelAsync( | ||
| 469 | + db, | ||
| 470 | + null, | ||
| 471 | + regionIds, | ||
| 472 | + locationIds, | ||
| 473 | + ScopeAllEchoHelper.ForLabelRegionScope(appliedRegionType, locationIds)); | ||
| 474 | + | ||
| 438 | return new LabelRegionScopeDisplay | 475 | return new LabelRegionScopeDisplay |
| 439 | { | 476 | { |
| 440 | Region = regionText, | 477 | Region = regionText, |
| 441 | - RegionIds = regionIds, | ||
| 442 | - GroupIds = regionIds | 478 | + RegionIds = collapsed.RegionIds, |
| 479 | + GroupIds = collapsed.RegionIds | ||
| 443 | }; | 480 | }; |
| 444 | } | 481 | } |
| 445 | 482 | ||
| @@ -454,7 +491,7 @@ public static class LabelRegionScopeHelper | @@ -454,7 +491,7 @@ public static class LabelRegionScopeHelper | ||
| 454 | return new LabelLocationScopeDisplay | 491 | return new LabelLocationScopeDisplay |
| 455 | { | 492 | { |
| 456 | Location = AllLocationsDisplay, | 493 | Location = AllLocationsDisplay, |
| 457 | - LocationIds = new List<string>() | 494 | + LocationIds = new List<string> { AllScopeBindingHelper.ScopeAll } |
| 458 | }; | 495 | }; |
| 459 | } | 496 | } |
| 460 | 497 | ||
| @@ -482,10 +519,17 @@ public static class LabelRegionScopeHelper | @@ -482,10 +519,17 @@ public static class LabelRegionScopeHelper | ||
| 482 | ? string.Join(", ", names.Where(n => !string.IsNullOrWhiteSpace(n)).Select(n => n!.Trim()).Distinct()) | 519 | ? string.Join(", ", names.Where(n => !string.IsNullOrWhiteSpace(n)).Select(n => n!.Trim()).Distinct()) |
| 483 | : "?"; | 520 | : "?"; |
| 484 | 521 | ||
| 522 | + var collapsed = await ScopeAllEchoHelper.CollapseScopeIdsToAllSentinelAsync( | ||
| 523 | + db, | ||
| 524 | + null, | ||
| 525 | + null, | ||
| 526 | + locationIds, | ||
| 527 | + ScopeAllEchoHelper.ForLabelRegionScope(appliedRegionType, locationIds)); | ||
| 528 | + | ||
| 485 | return new LabelLocationScopeDisplay | 529 | return new LabelLocationScopeDisplay |
| 486 | { | 530 | { |
| 487 | Location = locationText, | 531 | Location = locationText, |
| 488 | - LocationIds = locationIds.ToList() | 532 | + LocationIds = collapsed.LocationIds |
| 489 | }; | 533 | }; |
| 490 | } | 534 | } |
| 491 | 535 | ||
| @@ -593,7 +637,24 @@ public static class LabelRegionScopeHelper | @@ -593,7 +637,24 @@ public static class LabelRegionScopeHelper | ||
| 593 | new { all = AppliedRegionAll, gid }); | 637 | new { all = AppliedRegionAll, gid }); |
| 594 | } | 638 | } |
| 595 | 639 | ||
| 596 | - /// <summary>? scoped ?????<c>fl_label_location</c> ???? <c>LocationId</c>??</summary> | 640 | + /// <summary> |
| 641 | + /// 无可见门店时:仅保留 <c>AppliedRegionType=ALL</c> 的标签(用于 PartnerId 筛选但该公司暂无门店)。 | ||
| 642 | + /// </summary> | ||
| 643 | + public static ISugarQueryable<FlLabelDbEntity> ApplyLabelAllRegionOnlyFilter( | ||
| 644 | + ISqlSugarClient db, | ||
| 645 | + ISugarQueryable<FlLabelDbEntity> query, | ||
| 646 | + LabelRegionSchemaHelper.LabelRegionSchemaStatus schema) | ||
| 647 | + { | ||
| 648 | + if (!schema.HasAppliedRegionTypeColumn) | ||
| 649 | + { | ||
| 650 | + // 未迁移列时无法识别 ALL,保守返回空 | ||
| 651 | + return query.Where(_ => false); | ||
| 652 | + } | ||
| 653 | + | ||
| 654 | + return query.Where("AppliedRegionType = @all", new { all = AppliedRegionAll }); | ||
| 655 | + } | ||
| 656 | + | ||
| 657 | + /// <summary>按 scoped 门店过滤;AppliedRegionType=ALL 对任意门店筛选均命中。</summary> | ||
| 597 | public static ISugarQueryable<FlLabelDbEntity> ApplyLabelLocationListFilter( | 658 | public static ISugarQueryable<FlLabelDbEntity> ApplyLabelLocationListFilter( |
| 598 | ISqlSugarClient db, | 659 | ISqlSugarClient db, |
| 599 | ISugarQueryable<FlLabelDbEntity> query, | 660 | ISugarQueryable<FlLabelDbEntity> query, |
| @@ -602,7 +663,7 @@ public static class LabelRegionScopeHelper | @@ -602,7 +663,7 @@ public static class LabelRegionScopeHelper | ||
| 602 | { | 663 | { |
| 603 | if (scopedLocationIds.Count == 0) | 664 | if (scopedLocationIds.Count == 0) |
| 604 | { | 665 | { |
| 605 | - return query.Where(_ => false); | 666 | + return ApplyLabelAllRegionOnlyFilter(db, query, schema); |
| 606 | } | 667 | } |
| 607 | 668 | ||
| 608 | return ApplyLabelLocationMatchFilter(query, scopedLocationIds, schema); | 669 | return ApplyLabelLocationMatchFilter(query, scopedLocationIds, schema); |
| @@ -648,6 +709,14 @@ public static class LabelRegionScopeHelper | @@ -648,6 +709,14 @@ public static class LabelRegionScopeHelper | ||
| 648 | var locationIds = await GetLocationIdsForLabelAsync(db, label.Id, label.LocationId); | 709 | var locationIds = await GetLocationIdsForLabelAsync(db, label.Id, label.LocationId); |
| 649 | if (locationIds.Count == 0) | 710 | if (locationIds.Count == 0) |
| 650 | { | 711 | { |
| 712 | + // 指定 Company 的 ALL:仅该公司下门店可用 | ||
| 713 | + if (!string.IsNullOrWhiteSpace(label.PartnerId)) | ||
| 714 | + { | ||
| 715 | + var locPartners = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( | ||
| 716 | + db, new List<string> { locationId.Trim() }); | ||
| 717 | + return locPartners.Contains(label.PartnerId.Trim(), StringComparer.OrdinalIgnoreCase); | ||
| 718 | + } | ||
| 719 | + | ||
| 651 | return true; | 720 | return true; |
| 652 | } | 721 | } |
| 653 | 722 | ||
| @@ -784,6 +853,26 @@ public static class LabelRegionScopeHelper | @@ -784,6 +853,26 @@ public static class LabelRegionScopeHelper | ||
| 784 | List<string> scopedLocationIds, | 853 | List<string> scopedLocationIds, |
| 785 | LabelRegionSchemaHelper.LabelRegionSchemaStatus schema) | 854 | LabelRegionSchemaHelper.LabelRegionSchemaStatus schema) |
| 786 | { | 855 | { |
| 856 | + if (schema.HasAppliedRegionTypeColumn && schema.HasLabelLocationTable) | ||
| 857 | + { | ||
| 858 | + return query.Where( | ||
| 859 | + """ | ||
| 860 | + (AppliedRegionType = @all | ||
| 861 | + OR LocationId IN (@locs) | ||
| 862 | + OR EXISTS (SELECT 1 FROM fl_label_location ll WHERE ll.LabelId = fl_label.Id AND ll.LocationId IN (@locs))) | ||
| 863 | + """, | ||
| 864 | + new { all = AppliedRegionAll, locs = scopedLocationIds }); | ||
| 865 | + } | ||
| 866 | + | ||
| 867 | + if (schema.HasAppliedRegionTypeColumn) | ||
| 868 | + { | ||
| 869 | + return query.Where( | ||
| 870 | + """ | ||
| 871 | + (AppliedRegionType = @all OR LocationId IN (@locs)) | ||
| 872 | + """, | ||
| 873 | + new { all = AppliedRegionAll, locs = scopedLocationIds }); | ||
| 874 | + } | ||
| 875 | + | ||
| 787 | if (schema.HasLabelLocationTable) | 876 | if (schema.HasLabelLocationTable) |
| 788 | { | 877 | { |
| 789 | return query.Where( | 878 | return query.Where( |
| @@ -829,17 +918,18 @@ public static class LabelRegionScopeHelper | @@ -829,17 +918,18 @@ public static class LabelRegionScopeHelper | ||
| 829 | 918 | ||
| 830 | private static async Task ValidateRegionIdsExistAsync(ISqlSugarClient db, List<string> regionIds) | 919 | private static async Task ValidateRegionIdsExistAsync(ISqlSugarClient db, List<string> regionIds) |
| 831 | { | 920 | { |
| 832 | - if (regionIds.Count == 0) | 921 | + var ids = LocationScopeBindingHelper.FilterConcreteScopeIds(regionIds); |
| 922 | + if (ids.Count == 0) | ||
| 833 | { | 923 | { |
| 834 | return; | 924 | return; |
| 835 | } | 925 | } |
| 836 | 926 | ||
| 837 | var count = await db.Queryable<FlGroupDbEntity>() | 927 | var count = await db.Queryable<FlGroupDbEntity>() |
| 838 | - .Where(g => !g.IsDeleted && regionIds.Contains(g.Id)) | 928 | + .Where(g => !g.IsDeleted && ids.Contains(g.Id)) |
| 839 | .CountAsync(); | 929 | .CountAsync(); |
| 840 | - if (count != regionIds.Count) | 930 | + if (count != ids.Count) |
| 841 | { | 931 | { |
| 842 | - throw new UserFriendlyException("????? Region Id???????"); | 932 | + throw new UserFriendlyException("存在无效的 Region Id,请刷新后重试"); |
| 843 | } | 933 | } |
| 844 | } | 934 | } |
| 845 | } | 935 | } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeHelper.cs
| @@ -58,43 +58,73 @@ public static class LabelTemplateScopeHelper | @@ -58,43 +58,73 @@ public static class LabelTemplateScopeHelper | ||
| 58 | 58 | ||
| 59 | /// <summary> | 59 | /// <summary> |
| 60 | /// 解析新增/编辑入参中的 Company / Region / Location 范围。 | 60 | /// 解析新增/编辑入参中的 Company / Region / Location 范围。 |
| 61 | + /// Create/Update 共用;<c>regionIds</c>、<c>groupIds</c>、<c>locationIds</c>、<c>appliedLocationIds</c> 可传哨兵 <c>ALL</c>, | ||
| 62 | + /// 即使 <c>appliedRegionType</c> / <c>appliedLocation</c> 为 <c>SPECIFIED</c> 也会归档为对应维度 <c>ALL</c> 且不写关联快照。 | ||
| 61 | /// </summary> | 63 | /// </summary> |
| 62 | public static async Task<LabelTemplateScopeSaveResult> ResolveScopeForSaveAsync( | 64 | public static async Task<LabelTemplateScopeSaveResult> ResolveScopeForSaveAsync( |
| 63 | ISqlSugarClient db, | 65 | ISqlSugarClient db, |
| 64 | LabelTemplateCreateInputVo input) | 66 | LabelTemplateCreateInputVo input) |
| 65 | { | 67 | { |
| 66 | - var partnerIds = NormalizePartnerIds(input); | ||
| 67 | - var regionIds = NormalizeRegionIds(input); | ||
| 68 | - var locationIds = MergeExplicitLocationIds(input); | 68 | + var mergedRegionIds = NormalizeRegionIds(input); |
| 69 | + var mergedLocationIds = MergeExplicitLocationIds(input); | ||
| 70 | + var hasRegionArray = input.RegionIds is not null || input.GroupIds is not null; | ||
| 71 | + var hasLocationArray = input.LocationIds is not null || input.AppliedLocationIds is not null; | ||
| 72 | + | ||
| 73 | + // Create/Update 共用:优先识别 ALL 哨兵,避免后续 Count>0 或 Guid 存在性校验误伤编辑回显的 ["ALL"] | ||
| 74 | + var locationHasAll = AllScopeBindingHelper.HasAllScopeSentinelSelection(mergedLocationIds); | ||
| 75 | + var regionHasAll = AllScopeBindingHelper.HasAllScopeSentinelSelection(mergedRegionIds); | ||
| 76 | + var concreteLocations = LocationScopeBindingHelper.FilterConcreteScopeIds(mergedLocationIds); | ||
| 69 | 77 | ||
| 70 | var (partnerType, partnerIdsForSave) = await AllScopeBindingHelper.NormalizePartnerScopeAsync( | 78 | var (partnerType, partnerIdsForSave) = await AllScopeBindingHelper.NormalizePartnerScopeAsync( |
| 71 | db, | 79 | db, |
| 72 | input.AppliedPartnerType, | 80 | input.AppliedPartnerType, |
| 73 | - partnerIds, | ||
| 74 | - null, | 81 | + input.PartnerIds, |
| 82 | + input.CompanyIds, | ||
| 75 | input.PartnerIds is not null || input.CompanyIds is not null); | 83 | input.PartnerIds is not null || input.CompanyIds is not null); |
| 76 | - partnerIds = partnerIdsForSave; | 84 | + var partnerIds = partnerIdsForSave; |
| 77 | 85 | ||
| 78 | var partnerContext = string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) | 86 | var partnerContext = string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) |
| 79 | ? partnerIds | 87 | ? partnerIds |
| 80 | : null; | 88 | : null; |
| 81 | 89 | ||
| 82 | - var (regionType, regionIdsForSave) = await AllScopeBindingHelper.NormalizeRegionScopeAsync( | ||
| 83 | - db, | ||
| 84 | - input.AppliedRegionType, | ||
| 85 | - regionIds, | ||
| 86 | - input.RegionIds is not null || input.GroupIds is not null, | ||
| 87 | - partnerContext); | ||
| 88 | - regionIds = regionIdsForSave; | 90 | + string regionType; |
| 91 | + List<string> regionIds; | ||
| 92 | + if (regionHasAll && concreteLocations.Count == 0) | ||
| 93 | + { | ||
| 94 | + regionType = ScopeAll; | ||
| 95 | + regionIds = new List<string>(); | ||
| 96 | + } | ||
| 97 | + else | ||
| 98 | + { | ||
| 99 | + var regionResult = await AllScopeBindingHelper.NormalizeRegionScopeAsync( | ||
| 100 | + db, | ||
| 101 | + input.AppliedRegionType, | ||
| 102 | + LocationScopeBindingHelper.FilterConcreteScopeIds(mergedRegionIds), | ||
| 103 | + hasRegionArray, | ||
| 104 | + partnerContext); | ||
| 105 | + regionType = regionResult.Type; | ||
| 106 | + regionIds = regionResult.Ids; | ||
| 107 | + } | ||
| 89 | 108 | ||
| 90 | - var (locationType, locationIdsForSave) = await AllScopeBindingHelper.NormalizeLocationScopeAsync( | ||
| 91 | - db, | ||
| 92 | - input.AppliedLocationType, | ||
| 93 | - locationIds, | ||
| 94 | - input.LocationIds is not null || input.AppliedLocationIds is not null, | ||
| 95 | - partnerContext, | ||
| 96 | - string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) ? regionIds : null); | ||
| 97 | - locationIds = locationIdsForSave; | 109 | + string locationType; |
| 110 | + List<string> locationIds; | ||
| 111 | + if (locationHasAll) | ||
| 112 | + { | ||
| 113 | + locationType = ScopeAll; | ||
| 114 | + locationIds = new List<string>(); | ||
| 115 | + } | ||
| 116 | + else | ||
| 117 | + { | ||
| 118 | + var locationResult = await AllScopeBindingHelper.NormalizeLocationScopeAsync( | ||
| 119 | + db, | ||
| 120 | + input.AppliedLocationType, | ||
| 121 | + concreteLocations, | ||
| 122 | + hasLocationArray, | ||
| 123 | + partnerContext, | ||
| 124 | + string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) ? regionIds : null); | ||
| 125 | + locationType = locationResult.Type; | ||
| 126 | + locationIds = locationResult.Ids; | ||
| 127 | + } | ||
| 98 | 128 | ||
| 99 | ValidateDimensionType("Company", partnerType); | 129 | ValidateDimensionType("Company", partnerType); |
| 100 | ValidateDimensionType("Region", regionType); | 130 | ValidateDimensionType("Region", regionType); |
| @@ -134,7 +164,9 @@ public static class LabelTemplateScopeHelper | @@ -134,7 +164,9 @@ public static class LabelTemplateScopeHelper | ||
| 134 | || string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) | 164 | || string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) |
| 135 | || string.Equals(locationType, ScopeSpecified, StringComparison.OrdinalIgnoreCase); | 165 | || string.Equals(locationType, ScopeSpecified, StringComparison.OrdinalIgnoreCase); |
| 136 | 166 | ||
| 137 | - if (anySpecified) | 167 | + if (anySpecified |
| 168 | + && (string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) | ||
| 169 | + || string.Equals(locationType, ScopeSpecified, StringComparison.OrdinalIgnoreCase))) | ||
| 138 | { | 170 | { |
| 139 | var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | 171 | var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( |
| 140 | db, | 172 | db, |
| @@ -300,6 +332,8 @@ public static class LabelTemplateScopeHelper | @@ -300,6 +332,8 @@ public static class LabelTemplateScopeHelper | ||
| 300 | .Where(x => templateIds.Contains(x.TemplateId)) | 332 | .Where(x => templateIds.Contains(x.TemplateId)) |
| 301 | .ToListAsync(); | 333 | .ToListAsync(); |
| 302 | 334 | ||
| 335 | + var storedScopeTypes = await LabelTemplateScopeSchemaHelper.GetAppliedScopeTypesMapAsync(db, templateIds); | ||
| 336 | + | ||
| 303 | var partnerIdSet = partnerLinks.Select(x => x.PartnerId).Distinct(StringComparer.Ordinal).ToList(); | 337 | var partnerIdSet = partnerLinks.Select(x => x.PartnerId).Distinct(StringComparer.Ordinal).ToList(); |
| 304 | var regionIdSet = regionLinks.Select(x => x.GroupId).Distinct(StringComparer.Ordinal).ToList(); | 338 | var regionIdSet = regionLinks.Select(x => x.GroupId).Distinct(StringComparer.Ordinal).ToList(); |
| 305 | var locationIdSet = locationLinks.Select(x => x.LocationId).Distinct(StringComparer.Ordinal).ToList(); | 339 | var locationIdSet = locationLinks.Select(x => x.LocationId).Distinct(StringComparer.Ordinal).ToList(); |
| @@ -360,9 +394,16 @@ public static class LabelTemplateScopeHelper | @@ -360,9 +394,16 @@ public static class LabelTemplateScopeHelper | ||
| 360 | var partnerType = pIds.Count > 0 ? ScopeSpecified : ScopeAll; | 394 | var partnerType = pIds.Count > 0 ? ScopeSpecified : ScopeAll; |
| 361 | var regionType = rIds.Count > 0 ? ScopeSpecified : ScopeAll; | 395 | var regionType = rIds.Count > 0 ? ScopeSpecified : ScopeAll; |
| 362 | 396 | ||
| 397 | + if (storedScopeTypes.TryGetValue(template.Id, out var storedTypes)) | ||
| 398 | + { | ||
| 399 | + partnerType = NormalizeScopeType(storedTypes.PartnerType, partnerType); | ||
| 400 | + regionType = NormalizeScopeType(storedTypes.RegionType, regionType); | ||
| 401 | + } | ||
| 402 | + | ||
| 363 | if (hasExtendedScope | 403 | if (hasExtendedScope |
| 364 | && pIds.Count == 0 | 404 | && pIds.Count == 0 |
| 365 | - && lIds.Count > 0) | 405 | + && lIds.Count > 0 |
| 406 | + && !string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)) | ||
| 366 | { | 407 | { |
| 367 | pIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, lIds); | 408 | pIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, lIds); |
| 368 | if (pIds.Count > 0) | 409 | if (pIds.Count > 0) |
| @@ -373,7 +414,8 @@ public static class LabelTemplateScopeHelper | @@ -373,7 +414,8 @@ public static class LabelTemplateScopeHelper | ||
| 373 | 414 | ||
| 374 | if (hasExtendedScope | 415 | if (hasExtendedScope |
| 375 | && rIds.Count == 0 | 416 | && rIds.Count == 0 |
| 376 | - && lIds.Count > 0) | 417 | + && lIds.Count > 0 |
| 418 | + && !string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)) | ||
| 377 | { | 419 | { |
| 378 | rIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(db, lIds); | 420 | rIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(db, lIds); |
| 379 | if (rIds.Count > 0) | 421 | if (rIds.Count > 0) |
| @@ -401,6 +443,13 @@ public static class LabelTemplateScopeHelper | @@ -401,6 +443,13 @@ public static class LabelTemplateScopeHelper | ||
| 401 | ? AllLocationsDisplay | 443 | ? AllLocationsDisplay |
| 402 | : FormatLocationNames(lIds, locById); | 444 | : FormatLocationNames(lIds, locById); |
| 403 | 445 | ||
| 446 | + var collapsed = await ScopeAllEchoHelper.CollapseScopeIdsToAllSentinelAsync( | ||
| 447 | + db, | ||
| 448 | + pIds, | ||
| 449 | + rIds, | ||
| 450 | + lIds, | ||
| 451 | + ScopeAllEchoHelper.ForLabelTemplateDimensions(partnerType, regionType, locationType)); | ||
| 452 | + | ||
| 404 | result[template.Id] = new LabelTemplateScopeDisplay | 453 | result[template.Id] = new LabelTemplateScopeDisplay |
| 405 | { | 454 | { |
| 406 | Company = companyDisplay, | 455 | Company = companyDisplay, |
| @@ -409,9 +458,9 @@ public static class LabelTemplateScopeHelper | @@ -409,9 +458,9 @@ public static class LabelTemplateScopeHelper | ||
| 409 | AppliedPartnerType = partnerType, | 458 | AppliedPartnerType = partnerType, |
| 410 | AppliedRegionType = regionType, | 459 | AppliedRegionType = regionType, |
| 411 | AppliedLocationType = locationType, | 460 | AppliedLocationType = locationType, |
| 412 | - PartnerIds = pIds, | ||
| 413 | - RegionIds = rIds, | ||
| 414 | - LocationIds = lIds | 461 | + PartnerIds = collapsed.PartnerIds, |
| 462 | + RegionIds = collapsed.RegionIds, | ||
| 463 | + LocationIds = collapsed.LocationIds | ||
| 415 | }; | 464 | }; |
| 416 | } | 465 | } |
| 417 | 466 | ||
| @@ -419,31 +468,58 @@ public static class LabelTemplateScopeHelper | @@ -419,31 +468,58 @@ public static class LabelTemplateScopeHelper | ||
| 419 | } | 468 | } |
| 420 | 469 | ||
| 421 | /// <summary> | 470 | /// <summary> |
| 422 | - /// 列表权限:按当前用户可见门店筛选模板(各维度 AND)。 | ||
| 423 | - /// <paramref name="scopedLocationIds"/> 为 <c>null</c> 时不限制(管理员且未传 Query 筛选); | ||
| 424 | - /// 非空时仅返回与可见 Company/Region/Location 匹配的模板,不包含未绑定任何范围的「全局」模板。 | 471 | + /// 列表筛选:按当前用户可见范围筛选模板。 |
| 472 | + /// 仅传 <c>partnerId</c> 时只按 Company 维度过滤(多公司绑定不会被门店 AND 误杀)。 | ||
| 425 | /// </summary> | 473 | /// </summary> |
| 426 | public static async Task<ISugarQueryable<FlLabelTemplateDbEntity>> ApplyTemplateScopeFilterAsync( | 474 | public static async Task<ISugarQueryable<FlLabelTemplateDbEntity>> ApplyTemplateScopeFilterAsync( |
| 427 | ISqlSugarClient db, | 475 | ISqlSugarClient db, |
| 428 | ISugarQueryable<FlLabelTemplateDbEntity> query, | 476 | ISugarQueryable<FlLabelTemplateDbEntity> query, |
| 429 | - List<string>? scopedLocationIds) | 477 | + List<string>? scopedLocationIds, |
| 478 | + string? partnerId = null, | ||
| 479 | + string? groupId = null, | ||
| 480 | + string? locationId = null) | ||
| 430 | { | 481 | { |
| 482 | + var hasExtendedScope = await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(db); | ||
| 483 | + var schema = await LabelTemplateScopeSchemaHelper.GetStatusAsync(db); | ||
| 484 | + | ||
| 485 | + // 仅 PartnerId:只返回 fl_label_template_partner 含该公司的模板。 | ||
| 486 | + // 不含 AppliedPartnerType=ALL(全公司模板在未传 PartnerId 时可见); | ||
| 487 | + // 也不把「无关联行」当成 ALL,避免其他公司数据漏出。 | ||
| 488 | + if (!LabelEntityListScopeHelper.ShouldApplyLocationAvailabilityFilter(partnerId, groupId, locationId) | ||
| 489 | + && !string.IsNullOrWhiteSpace(partnerId)) | ||
| 490 | + { | ||
| 491 | + var pid = partnerId.Trim(); | ||
| 492 | + if (!schema.HasPartnerScopeTable) | ||
| 493 | + { | ||
| 494 | + return query.Where(_ => false); | ||
| 495 | + } | ||
| 496 | + | ||
| 497 | + return query.Where(t => | ||
| 498 | + SqlFunc.Subqueryable<FlLabelTemplatePartnerDbEntity>() | ||
| 499 | + .Where(p => p.TemplateId == t.Id && p.PartnerId == pid) | ||
| 500 | + .Any()); | ||
| 501 | + } | ||
| 502 | + | ||
| 431 | if (scopedLocationIds is null) | 503 | if (scopedLocationIds is null) |
| 432 | { | 504 | { |
| 433 | return query; | 505 | return query; |
| 434 | } | 506 | } |
| 435 | 507 | ||
| 436 | - var hasExtendedScope = await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(db); | ||
| 437 | - | ||
| 438 | if (scopedLocationIds.Count == 0) | 508 | if (scopedLocationIds.Count == 0) |
| 439 | { | 509 | { |
| 440 | - return query.Where(_ => false); | 510 | + // 无可见门店:保留 Location=ALL(及无 location 关联)的模板 |
| 511 | + return query.Where(t => | ||
| 512 | + t.AppliedLocationType == ScopeAll | ||
| 513 | + || !SqlFunc.Subqueryable<FlLabelTemplateLocationDbEntity>() | ||
| 514 | + .Where(l => l.TemplateId == t.Id) | ||
| 515 | + .Any()); | ||
| 441 | } | 516 | } |
| 442 | 517 | ||
| 443 | if (!hasExtendedScope) | 518 | if (!hasExtendedScope) |
| 444 | { | 519 | { |
| 445 | return query.Where(t => | 520 | return query.Where(t => |
| 446 | - SqlFunc.Subqueryable<FlLabelTemplateLocationDbEntity>() | 521 | + t.AppliedLocationType == ScopeAll |
| 522 | + || SqlFunc.Subqueryable<FlLabelTemplateLocationDbEntity>() | ||
| 447 | .Where(l => l.TemplateId == t.Id && scopedLocationIds.Contains(l.LocationId)) | 523 | .Where(l => l.TemplateId == t.Id && scopedLocationIds.Contains(l.LocationId)) |
| 448 | .Any()); | 524 | .Any()); |
| 449 | } | 525 | } |
| @@ -453,6 +529,30 @@ public static class LabelTemplateScopeHelper | @@ -453,6 +529,30 @@ public static class LabelTemplateScopeHelper | ||
| 453 | var scopedGroupIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( | 529 | var scopedGroupIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( |
| 454 | db, scopedLocationIds); | 530 | db, scopedLocationIds); |
| 455 | 531 | ||
| 532 | + // Company:AppliedPartnerType=ALL 或关联命中(不用「无行=ALL」) | ||
| 533 | + if (schema.HasAppliedPartnerTypeColumn) | ||
| 534 | + { | ||
| 535 | + return query.Where( | ||
| 536 | + """ | ||
| 537 | + (AppliedPartnerType = @all | ||
| 538 | + OR EXISTS (SELECT 1 FROM fl_label_template_partner p | ||
| 539 | + WHERE p.TemplateId = fl_label_template.Id AND p.PartnerId IN (@partnerIds))) | ||
| 540 | + AND (NOT EXISTS (SELECT 1 FROM fl_label_template_region r WHERE r.TemplateId = fl_label_template.Id) | ||
| 541 | + OR EXISTS (SELECT 1 FROM fl_label_template_region r | ||
| 542 | + WHERE r.TemplateId = fl_label_template.Id AND r.GroupId IN (@groupIds))) | ||
| 543 | + AND (AppliedLocationType = @all | ||
| 544 | + OR EXISTS (SELECT 1 FROM fl_label_template_location l | ||
| 545 | + WHERE l.TemplateId = fl_label_template.Id AND l.LocationId IN (@locs))) | ||
| 546 | + """, | ||
| 547 | + new | ||
| 548 | + { | ||
| 549 | + all = ScopeAll, | ||
| 550 | + partnerIds = scopedPartnerIds, | ||
| 551 | + groupIds = scopedGroupIds.Count > 0 ? scopedGroupIds : new List<string> { "__none__" }, | ||
| 552 | + locs = scopedLocationIds | ||
| 553 | + }); | ||
| 554 | + } | ||
| 555 | + | ||
| 456 | return query.Where(t => | 556 | return query.Where(t => |
| 457 | SqlFunc.Subqueryable<FlLabelTemplatePartnerDbEntity>() | 557 | SqlFunc.Subqueryable<FlLabelTemplatePartnerDbEntity>() |
| 458 | .Where(p => p.TemplateId == t.Id && scopedPartnerIds.Contains(p.PartnerId)) | 558 | .Where(p => p.TemplateId == t.Id && scopedPartnerIds.Contains(p.PartnerId)) |
| @@ -567,15 +667,16 @@ public static class LabelTemplateScopeHelper | @@ -567,15 +667,16 @@ public static class LabelTemplateScopeHelper | ||
| 567 | 667 | ||
| 568 | private static async Task ValidateRegionIdsExistAsync(ISqlSugarClient db, List<string> regionIds) | 668 | private static async Task ValidateRegionIdsExistAsync(ISqlSugarClient db, List<string> regionIds) |
| 569 | { | 669 | { |
| 570 | - if (regionIds.Count == 0) | 670 | + var ids = LocationScopeBindingHelper.FilterConcreteScopeIds(regionIds); |
| 671 | + if (ids.Count == 0) | ||
| 571 | { | 672 | { |
| 572 | return; | 673 | return; |
| 573 | } | 674 | } |
| 574 | 675 | ||
| 575 | var count = await db.Queryable<FlGroupDbEntity>() | 676 | var count = await db.Queryable<FlGroupDbEntity>() |
| 576 | - .Where(x => !x.IsDeleted && regionIds.Contains(x.Id)) | 677 | + .Where(x => !x.IsDeleted && ids.Contains(x.Id)) |
| 577 | .CountAsync(); | 678 | .CountAsync(); |
| 578 | - if (count != regionIds.Count) | 679 | + if (count != ids.Count) |
| 579 | { | 680 | { |
| 580 | throw new UserFriendlyException("存在无效的 Region(regionIds/groupIds),请刷新后重试"); | 681 | throw new UserFriendlyException("存在无效的 Region(regionIds/groupIds),请刷新后重试"); |
| 581 | } | 682 | } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeSchemaHelper.cs
| @@ -315,6 +315,77 @@ public static class LabelTemplateScopeSchemaHelper | @@ -315,6 +315,77 @@ public static class LabelTemplateScopeSchemaHelper | ||
| 315 | return map.TryGetValue(templateId.Trim(), out var text) ? text : null; | 315 | return map.TryGetValue(templateId.Trim(), out var text) ? text : null; |
| 316 | } | 316 | } |
| 317 | 317 | ||
| 318 | + /// <summary>批量读取模板 Company/Region 维度 ALL/SPECIFIED;列不存在时返回空字典。</summary> | ||
| 319 | + public static async Task<Dictionary<string, (string PartnerType, string RegionType)>> GetAppliedScopeTypesMapAsync( | ||
| 320 | + ISqlSugarClient db, | ||
| 321 | + IReadOnlyList<string> templateIds) | ||
| 322 | + { | ||
| 323 | + var result = new Dictionary<string, (string PartnerType, string RegionType)>(StringComparer.Ordinal); | ||
| 324 | + if (templateIds.Count == 0) | ||
| 325 | + { | ||
| 326 | + return result; | ||
| 327 | + } | ||
| 328 | + | ||
| 329 | + var status = await GetStatusAsync(db); | ||
| 330 | + if (!status.HasAppliedPartnerTypeColumn && !status.HasAppliedRegionTypeColumn) | ||
| 331 | + { | ||
| 332 | + return result; | ||
| 333 | + } | ||
| 334 | + | ||
| 335 | + var ids = templateIds.Where(x => !string.IsNullOrWhiteSpace(x)) | ||
| 336 | + .Select(x => x.Trim()) | ||
| 337 | + .Distinct(StringComparer.Ordinal) | ||
| 338 | + .ToArray(); | ||
| 339 | + if (ids.Length == 0) | ||
| 340 | + { | ||
| 341 | + return result; | ||
| 342 | + } | ||
| 343 | + | ||
| 344 | + var rows = await db.Ado.SqlQueryAsync<AppliedScopeTypesRow>( | ||
| 345 | + """ | ||
| 346 | + SELECT Id, AppliedPartnerType, AppliedRegionType | ||
| 347 | + FROM fl_label_template | ||
| 348 | + WHERE Id IN (@ids) | ||
| 349 | + """, | ||
| 350 | + new { ids }); | ||
| 351 | + | ||
| 352 | + foreach (var row in rows) | ||
| 353 | + { | ||
| 354 | + if (string.IsNullOrWhiteSpace(row.Id)) | ||
| 355 | + { | ||
| 356 | + continue; | ||
| 357 | + } | ||
| 358 | + | ||
| 359 | + var partnerType = status.HasAppliedPartnerTypeColumn | ||
| 360 | + ? NormalizeScopeTypeValue(row.AppliedPartnerType) | ||
| 361 | + : ScopeAll; | ||
| 362 | + var regionType = status.HasAppliedRegionTypeColumn | ||
| 363 | + ? NormalizeScopeTypeValue(row.AppliedRegionType) | ||
| 364 | + : ScopeAll; | ||
| 365 | + result[row.Id.Trim()] = (partnerType, regionType); | ||
| 366 | + } | ||
| 367 | + | ||
| 368 | + return result; | ||
| 369 | + } | ||
| 370 | + | ||
| 371 | + private static string NormalizeScopeTypeValue(string? type) | ||
| 372 | + { | ||
| 373 | + var normalized = (type ?? ScopeAll).Trim().ToUpperInvariant(); | ||
| 374 | + return normalized == ScopeSpecified ? ScopeSpecified : ScopeAll; | ||
| 375 | + } | ||
| 376 | + | ||
| 377 | + private const string ScopeAll = "ALL"; | ||
| 378 | + private const string ScopeSpecified = "SPECIFIED"; | ||
| 379 | + | ||
| 380 | + private sealed class AppliedScopeTypesRow | ||
| 381 | + { | ||
| 382 | + public string Id { get; init; } = string.Empty; | ||
| 383 | + | ||
| 384 | + public string? AppliedPartnerType { get; init; } | ||
| 385 | + | ||
| 386 | + public string? AppliedRegionType { get; init; } | ||
| 387 | + } | ||
| 388 | + | ||
| 318 | /// <summary>已迁移 <c>Contents</c> 列时写入(未迁移则 no-op)。</summary> | 389 | /// <summary>已迁移 <c>Contents</c> 列时写入(未迁移则 no-op)。</summary> |
| 319 | public static async Task SetContentsAsync(ISqlSugarClient db, string templateId, string? contents) | 390 | public static async Task SetContentsAsync(ISqlSugarClient db, string templateId, string? contents) |
| 320 | { | 391 | { |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs
| @@ -306,13 +306,14 @@ public static class LocationScopeBindingHelper | @@ -306,13 +306,14 @@ public static class LocationScopeBindingHelper | ||
| 306 | merged.Add(id); | 306 | merged.Add(id); |
| 307 | } | 307 | } |
| 308 | 308 | ||
| 309 | - var fromGroups = await ResolveLocationIdsFromGroupIdsAsync(db, groupIds); | 309 | + var concreteGroupIds = FilterConcreteScopeIds(groupIds); |
| 310 | + var fromGroups = await ResolveLocationIdsFromGroupIdsAsync(db, concreteGroupIds); | ||
| 310 | foreach (var id in fromGroups) | 311 | foreach (var id in fromGroups) |
| 311 | { | 312 | { |
| 312 | merged.Add(id); | 313 | merged.Add(id); |
| 313 | } | 314 | } |
| 314 | 315 | ||
| 315 | - foreach (var id in NormalizeIds(locationIds)) | 316 | + foreach (var id in FilterConcreteScopeIds(locationIds)) |
| 316 | { | 317 | { |
| 317 | merged.Add(id); | 318 | merged.Add(id); |
| 318 | } | 319 | } |
| @@ -321,6 +322,113 @@ public static class LocationScopeBindingHelper | @@ -321,6 +322,113 @@ public static class LocationScopeBindingHelper | ||
| 321 | } | 322 | } |
| 322 | 323 | ||
| 323 | /// <summary> | 324 | /// <summary> |
| 325 | + /// 多选 Id 是否含 <c>ALL</c> 哨兵(大小写不敏感)。与具体 Guid 同传时以 ALL 为准(全选)。 | ||
| 326 | + /// </summary> | ||
| 327 | + public static bool IsAllScopeSentinel(string? value) => | ||
| 328 | + string.Equals(value?.Trim(), AllScopeBindingHelper.ScopeAll, StringComparison.OrdinalIgnoreCase); | ||
| 329 | + | ||
| 330 | + /// <summary>Id 列表是否含 <c>ALL</c> 哨兵。</summary> | ||
| 331 | + public static bool ContainsAllScopeSentinel(IReadOnlyList<string>? ids) => | ||
| 332 | + ids?.Any(IsAllScopeSentinel) == true; | ||
| 333 | + | ||
| 334 | + /// <summary>去掉 ALL 哨兵,仅保留具体 Id。</summary> | ||
| 335 | + public static List<string> FilterConcreteScopeIds(IReadOnlyList<string>? ids) => | ||
| 336 | + NormalizeIds(ids).Where(x => !IsAllScopeSentinel(x)).ToList(); | ||
| 337 | + | ||
| 338 | + /// <summary> | ||
| 339 | + /// Team Member 门店范围落库(不做 partner+region+location 并集)。 | ||
| 340 | + /// 优先级: | ||
| 341 | + /// 1) <c>locationIds</c> 含 ALL → 公司全部门店; | ||
| 342 | + /// 2) 有具体 <c>locationIds</c>,且 <c>regionIds</c> 为空或为 ALL → 只绑这些门店(UI 在 Region=ALL 下再收窄门店); | ||
| 343 | + /// 3) <c>regionIds</c> 含 ALL → 公司全部 Region 下门店; | ||
| 344 | + /// 4) 具体 <c>regionIds</c> → 按 Region 展开(忽略同传 locationIds,保证多区域能落库); | ||
| 345 | + /// 5) 仅 <paramref name="partnerIds"/> → 公司全部门店。 | ||
| 346 | + /// </summary> | ||
| 347 | + public static async Task<List<string>> ResolveTeamMemberLocationIdsForSaveAsync( | ||
| 348 | + ISqlSugarClient db, | ||
| 349 | + IReadOnlyList<string>? partnerIds, | ||
| 350 | + IReadOnlyList<string>? regionIds, | ||
| 351 | + IReadOnlyList<string>? locationIds) | ||
| 352 | + { | ||
| 353 | + var normalizedPartners = NormalizeIds(partnerIds); | ||
| 354 | + var locationHasAll = ContainsAllScopeSentinel(locationIds); | ||
| 355 | + var concreteLocations = FilterConcreteScopeIds(locationIds); | ||
| 356 | + var regionHasAll = ContainsAllScopeSentinel(regionIds); | ||
| 357 | + var concreteRegions = FilterConcreteScopeIds(regionIds); | ||
| 358 | + | ||
| 359 | + // 1. locationIds 含 ALL:有具体 Region 时展开该 Region;否则按 Company 全部门店 | ||
| 360 | + if (locationHasAll) | ||
| 361 | + { | ||
| 362 | + var expanded = await ExpandScopedAllLocationsForSaveAsync( | ||
| 363 | + db, | ||
| 364 | + normalizedPartners.Count > 0 ? normalizedPartners : null, | ||
| 365 | + concreteRegions.Count > 0 ? concreteRegions : null); | ||
| 366 | + if (expanded is not null) | ||
| 367 | + { | ||
| 368 | + return expanded; | ||
| 369 | + } | ||
| 370 | + | ||
| 371 | + throw new UserFriendlyException("选择全部门店时需指定 Company(partnerId / partnerIds)或 Region"); | ||
| 372 | + } | ||
| 373 | + | ||
| 374 | + // 2. 具体门店 +(无区域 / 区域为 ALL)→ 只绑这些门店,避免 regionIds=ALL 盖掉单店 | ||
| 375 | + if (concreteLocations.Count > 0 && (regionHasAll || concreteRegions.Count == 0)) | ||
| 376 | + { | ||
| 377 | + return concreteLocations; | ||
| 378 | + } | ||
| 379 | + | ||
| 380 | + // 3. regionIds 含 ALL(且无具体门店)→ 按 partner 展开全部 Region 下门店 | ||
| 381 | + if (regionHasAll) | ||
| 382 | + { | ||
| 383 | + if (normalizedPartners.Count == 0) | ||
| 384 | + { | ||
| 385 | + throw new UserFriendlyException("选择全部 Region 时需指定 Company(partnerId / partnerIds)"); | ||
| 386 | + } | ||
| 387 | + | ||
| 388 | + return await AllScopeBindingHelper.ResolveAllLocationIdsAsync(db, normalizedPartners, null); | ||
| 389 | + } | ||
| 390 | + | ||
| 391 | + // 4. 具体 regionIds | ||
| 392 | + if (concreteRegions.Count > 0) | ||
| 393 | + { | ||
| 394 | + var fromRegions = await ExpandScopedAllLocationsForSaveAsync( | ||
| 395 | + db, | ||
| 396 | + normalizedPartners.Count > 0 ? normalizedPartners : null, | ||
| 397 | + concreteRegions); | ||
| 398 | + var regionLocs = fromRegions ?? new List<string>(); | ||
| 399 | + | ||
| 400 | + // 同传具体门店:若已覆盖该区全集(前端 Location=ALL 常展开 Guid)→ 用区内全部门店; | ||
| 401 | + // 否则保留子集(Region 具体 + 部分门店) | ||
| 402 | + if (concreteLocations.Count > 0) | ||
| 403 | + { | ||
| 404 | + if (regionLocs.Count > 0 | ||
| 405 | + && AllScopeBindingHelper.IsFullIdSelection(concreteLocations, regionLocs)) | ||
| 406 | + { | ||
| 407 | + return regionLocs; | ||
| 408 | + } | ||
| 409 | + | ||
| 410 | + return concreteLocations; | ||
| 411 | + } | ||
| 412 | + | ||
| 413 | + return regionLocs; | ||
| 414 | + } | ||
| 415 | + | ||
| 416 | + // 5. 仅有具体 locationIds → 只绑这些门店 | ||
| 417 | + if (concreteLocations.Count > 0) | ||
| 418 | + { | ||
| 419 | + return concreteLocations; | ||
| 420 | + } | ||
| 421 | + | ||
| 422 | + // 6. 仅有 partner → 绑该公司全部门店 | ||
| 423 | + if (normalizedPartners.Count > 0) | ||
| 424 | + { | ||
| 425 | + return await MergeToLocationIdsAsync(db, normalizedPartners, null, null); | ||
| 426 | + } | ||
| 427 | + | ||
| 428 | + return new List<string>(); | ||
| 429 | + } | ||
| 430 | + | ||
| 431 | + /// <summary> | ||
| 324 | /// 标签类型/分类/多选项门店范围落库:仅按 Region/Location 合并,不因 Company(partner)展开并集。 | 432 | /// 标签类型/分类/多选项门店范围落库:仅按 Region/Location 合并,不因 Company(partner)展开并集。 |
| 325 | /// </summary> | 433 | /// </summary> |
| 326 | public static async Task<List<string>> ResolveEntityLocationIdsForSaveAsync( | 434 | public static async Task<List<string>> ResolveEntityLocationIdsForSaveAsync( |
| @@ -334,11 +442,28 @@ public static class LocationScopeBindingHelper | @@ -334,11 +442,28 @@ public static class LocationScopeBindingHelper | ||
| 334 | return new List<string>(); | 442 | return new List<string>(); |
| 335 | } | 443 | } |
| 336 | 444 | ||
| 445 | + if (ContainsAllScopeSentinel(locationIds) | ||
| 446 | + || (ContainsAllScopeSentinel(regionIds) | ||
| 447 | + && FilterConcreteScopeIds(locationIds).Count == 0)) | ||
| 448 | + { | ||
| 449 | + throw new UserFriendlyException("门店范围含 ALL 哨兵时应归档为 ALL,不应进入 SPECIFIED 落库校验"); | ||
| 450 | + } | ||
| 451 | + | ||
| 452 | + var concreteRegions = FilterConcreteScopeIds(regionIds); | ||
| 453 | + var concreteLocations = FilterConcreteScopeIds(locationIds); | ||
| 454 | + | ||
| 455 | + // 有具体门店时以门店为准(不再与 Region 并集展开),避免「选 1 个 Location + 同传 Region」落成整 Region 再回显成 ALL | ||
| 456 | + if (concreteLocations.Count > 0) | ||
| 457 | + { | ||
| 458 | + await ValidateLocationIdsExistAsync(db, concreteLocations); | ||
| 459 | + return concreteLocations; | ||
| 460 | + } | ||
| 461 | + | ||
| 337 | var merged = await MergeToLocationIdsAsync( | 462 | var merged = await MergeToLocationIdsAsync( |
| 338 | db, | 463 | db, |
| 339 | (IReadOnlyList<string>?)null, | 464 | (IReadOnlyList<string>?)null, |
| 340 | - regionIds, | ||
| 341 | - locationIds); | 465 | + concreteRegions, |
| 466 | + concreteLocations); | ||
| 342 | if (merged.Count == 0) | 467 | if (merged.Count == 0) |
| 343 | { | 468 | { |
| 344 | throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); | 469 | throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); |
| @@ -349,6 +474,52 @@ public static class LocationScopeBindingHelper | @@ -349,6 +474,52 @@ public static class LocationScopeBindingHelper | ||
| 349 | } | 474 | } |
| 350 | 475 | ||
| 351 | /// <summary> | 476 | /// <summary> |
| 477 | + /// 将「范围内 ALL」展开为门店 Id:优先按具体 Region,其次按具体 Company;两者皆无则返回 null(调用方归档全局 ALL)。 | ||
| 478 | + /// </summary> | ||
| 479 | + public static async Task<List<string>?> ExpandScopedAllLocationsForSaveAsync( | ||
| 480 | + ISqlSugarClient db, | ||
| 481 | + IReadOnlyList<string>? partnerIds, | ||
| 482 | + IReadOnlyList<string>? regionIds) | ||
| 483 | + { | ||
| 484 | + var concreteRegions = FilterConcreteScopeIds(regionIds); | ||
| 485 | + if (concreteRegions.Count > 0) | ||
| 486 | + { | ||
| 487 | + var fromRegions = await ResolveLocationIdsFromGroupIdsAsync(db, concreteRegions); | ||
| 488 | + var partners = NormalizeIds(partnerIds); | ||
| 489 | + if (partners.Count > 0) | ||
| 490 | + { | ||
| 491 | + var partnerLocSet = new HashSet<string>( | ||
| 492 | + await ResolveLocationIdsFromPartnerIdsAsync(db, partners), | ||
| 493 | + StringComparer.OrdinalIgnoreCase); | ||
| 494 | + fromRegions = fromRegions.Where(id => partnerLocSet.Contains(id)).ToList(); | ||
| 495 | + } | ||
| 496 | + | ||
| 497 | + if (fromRegions.Count == 0) | ||
| 498 | + { | ||
| 499 | + throw new UserFriendlyException("指定 Region 下未匹配到有效门店"); | ||
| 500 | + } | ||
| 501 | + | ||
| 502 | + await ValidateLocationIdsExistAsync(db, fromRegions); | ||
| 503 | + return fromRegions; | ||
| 504 | + } | ||
| 505 | + | ||
| 506 | + var concretePartners = NormalizeIds(partnerIds); | ||
| 507 | + if (concretePartners.Count > 0) | ||
| 508 | + { | ||
| 509 | + var fromPartners = await ResolveLocationIdsFromPartnerIdsAsync(db, concretePartners); | ||
| 510 | + if (fromPartners.Count == 0) | ||
| 511 | + { | ||
| 512 | + throw new UserFriendlyException("指定 Company 下未匹配到有效门店"); | ||
| 513 | + } | ||
| 514 | + | ||
| 515 | + await ValidateLocationIdsExistAsync(db, fromPartners); | ||
| 516 | + return fromPartners; | ||
| 517 | + } | ||
| 518 | + | ||
| 519 | + return null; | ||
| 520 | + } | ||
| 521 | + | ||
| 522 | + /// <summary> | ||
| 352 | /// 根据已绑定门店反推适用的 Company Id(<c>fl_partner.Id</c>)。 | 523 | /// 根据已绑定门店反推适用的 Company Id(<c>fl_partner.Id</c>)。 |
| 353 | /// </summary> | 524 | /// </summary> |
| 354 | public static async Task<List<string>> ResolvePartnerIdsFromLocationIdsAsync( | 525 | public static async Task<List<string>> ResolvePartnerIdsFromLocationIdsAsync( |
| @@ -591,7 +762,7 @@ public static class LocationScopeBindingHelper | @@ -591,7 +762,7 @@ public static class LocationScopeBindingHelper | ||
| 591 | /// </summary> | 762 | /// </summary> |
| 592 | public static async Task ValidateLocationIdsExistAsync(ISqlSugarClient db, IReadOnlyList<string> locationIds) | 763 | public static async Task ValidateLocationIdsExistAsync(ISqlSugarClient db, IReadOnlyList<string> locationIds) |
| 593 | { | 764 | { |
| 594 | - var ids = NormalizeIds(locationIds); | 765 | + var ids = FilterConcreteScopeIds(locationIds); |
| 595 | if (ids.Count == 0) | 766 | if (ids.Count == 0) |
| 596 | { | 767 | { |
| 597 | return; | 768 | return; |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/PlatformMenuHelper.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Services.DbModels; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Helpers; | ||
| 4 | + | ||
| 5 | +/// <summary> | ||
| 6 | +/// 平台端菜单判定(与 ThSaasMenuPermissionCatalog.IsPlatformOnlyMenu 一致,避免 Application 依赖 Th 程序集) | ||
| 7 | +/// </summary> | ||
| 8 | +public static class PlatformMenuHelper | ||
| 9 | +{ | ||
| 10 | + /// <summary> | ||
| 11 | + /// 是否为仅平台端菜单(不可分配给公司) | ||
| 12 | + /// </summary> | ||
| 13 | + public static bool IsPlatformOnlyMenu(MenuDbEntity? menu) | ||
| 14 | + { | ||
| 15 | + if (menu == null) | ||
| 16 | + { | ||
| 17 | + return true; | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + var code = menu.PermissionCode?.Trim() ?? string.Empty; | ||
| 21 | + if (code.StartsWith("menu.platform", StringComparison.OrdinalIgnoreCase)) | ||
| 22 | + { | ||
| 23 | + return true; | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + var router = menu.Router?.Trim() ?? string.Empty; | ||
| 27 | + return router.StartsWith("/platform", StringComparison.OrdinalIgnoreCase); | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + /// <summary> | ||
| 31 | + /// 从全量菜单中保留平台菜单及其祖先节点,以便组装菜单树 | ||
| 32 | + /// </summary> | ||
| 33 | + public static List<MenuDbEntity> FilterPlatformMenusWithAncestors(IReadOnlyList<MenuDbEntity> allMenus) | ||
| 34 | + { | ||
| 35 | + if (allMenus.Count == 0) | ||
| 36 | + { | ||
| 37 | + return new List<MenuDbEntity>(); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + var byId = allMenus | ||
| 41 | + .GroupBy(m => m.Id, StringComparer.OrdinalIgnoreCase) | ||
| 42 | + .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase); | ||
| 43 | + | ||
| 44 | + var keepIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
| 45 | + foreach (var menu in allMenus.Where(IsPlatformOnlyMenu)) | ||
| 46 | + { | ||
| 47 | + IncludeWithAncestors(menu.Id, byId, keepIds); | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + return allMenus.Where(m => keepIds.Contains(m.Id)).ToList(); | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + private static void IncludeWithAncestors( | ||
| 54 | + string menuId, | ||
| 55 | + IReadOnlyDictionary<string, MenuDbEntity> byId, | ||
| 56 | + HashSet<string> keepIds) | ||
| 57 | + { | ||
| 58 | + if (!byId.TryGetValue(menuId, out var current)) | ||
| 59 | + { | ||
| 60 | + return; | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + if (!keepIds.Add(current.Id)) | ||
| 64 | + { | ||
| 65 | + return; | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + var parentId = current.ParentId?.Trim() ?? "0"; | ||
| 69 | + if (parentId == "0" || parentId == Guid.Empty.ToString()) | ||
| 70 | + { | ||
| 71 | + return; | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + IncludeWithAncestors(parentId, byId, keepIds); | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + /// <summary> | ||
| 78 | + /// 仅保留公司已开通菜单及其祖先节点(租户业务库 menu 表可能含 Seed 全量菜单)。 | ||
| 79 | + /// </summary> | ||
| 80 | + public static List<MenuDbEntity> FilterCompanyMenusWithAncestors( | ||
| 81 | + IReadOnlyList<MenuDbEntity> allMenus, | ||
| 82 | + IReadOnlySet<string> enabledMenuIds) | ||
| 83 | + { | ||
| 84 | + if (allMenus.Count == 0 || enabledMenuIds.Count == 0) | ||
| 85 | + { | ||
| 86 | + return new List<MenuDbEntity>(); | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + var byId = allMenus | ||
| 90 | + .Where(m => !m.IsDeleted && !IsPlatformOnlyMenu(m)) | ||
| 91 | + .GroupBy(m => m.Id, StringComparer.OrdinalIgnoreCase) | ||
| 92 | + .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase); | ||
| 93 | + | ||
| 94 | + var keepIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
| 95 | + foreach (var menuId in enabledMenuIds) | ||
| 96 | + { | ||
| 97 | + IncludeWithAncestors(menuId?.Trim() ?? string.Empty, byId, keepIds); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + return allMenus.Where(m => keepIds.Contains(m.Id)).ToList(); | ||
| 101 | + } | ||
| 102 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs
| @@ -67,6 +67,18 @@ public static class ReportsPrintLogExpiryHelper | @@ -67,6 +67,18 @@ public static class ReportsPrintLogExpiryHelper | ||
| 67 | public static string ExtractFormattedExpiryText(string? printInputJson) => | 67 | public static string ExtractFormattedExpiryText(string? printInputJson) => |
| 68 | ExtractFormattedExpiryText(printInputJson, null, null, null); | 68 | ExtractFormattedExpiryText(printInputJson, null, null, null); |
| 69 | 69 | ||
| 70 | + /// <summary>与 Print Log Expiration 同源:解析绝对过期时刻。</summary> | ||
| 71 | + public static bool TryResolveExpiryDateTime( | ||
| 72 | + string? printInputJson, | ||
| 73 | + string? renderTemplateJson, | ||
| 74 | + DateTime? baseTime, | ||
| 75 | + DateTime? printedAt, | ||
| 76 | + out DateTime expiresAt) | ||
| 77 | + { | ||
| 78 | + var reference = baseTime ?? printedAt ?? DateTime.Now; | ||
| 79 | + return TryResolveExpiryDateTime(printInputJson, renderTemplateJson, reference, out expiresAt); | ||
| 80 | + } | ||
| 81 | + | ||
| 70 | private static bool TryResolveExpiryDateTime( | 82 | private static bool TryResolveExpiryDateTime( |
| 71 | string? printInputJson, | 83 | string? printInputJson, |
| 72 | string? renderTemplateJson, | 84 | string? renderTemplateJson, |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs
| 1 | +using SqlSugar; | ||
| 1 | using Volo.Abp.Users; | 2 | using Volo.Abp.Users; |
| 2 | using Yi.Framework.Rbac.Domain.Shared.Consts; | 3 | using Yi.Framework.Rbac.Domain.Shared.Consts; |
| 3 | 4 | ||
| @@ -66,4 +67,28 @@ public static class ReportsRoleHelper | @@ -66,4 +67,28 @@ public static class ReportsRoleHelper | ||
| 66 | 67 | ||
| 67 | return false; | 68 | return false; |
| 68 | } | 69 | } |
| 70 | + | ||
| 71 | + /// <summary> | ||
| 72 | + /// App JWT 未写入角色声明时,按业务库 UserRole/Role 判断是否管理员。 | ||
| 73 | + /// </summary> | ||
| 74 | + public static async Task<bool> IsAdminRoleFromDbAsync(ISqlSugarClient db, Guid userId) | ||
| 75 | + { | ||
| 76 | + var rows = await db.Ado.SqlQueryAsync<AdminRoleCodeRow>( | ||
| 77 | + """ | ||
| 78 | + SELECT r.RoleCode | ||
| 79 | + FROM UserRole ur | ||
| 80 | + INNER JOIN Role r ON ur.RoleId = r.Id | ||
| 81 | + WHERE ur.UserId = @UserId AND r.IsDeleted = 0 AND r.State = 1 | ||
| 82 | + """, | ||
| 83 | + new { UserId = userId }); | ||
| 84 | + | ||
| 85 | + return rows.Any(r => | ||
| 86 | + !string.IsNullOrWhiteSpace(r.RoleCode) && | ||
| 87 | + string.Equals(r.RoleCode.Trim(), UserConst.AdminRolesCode, StringComparison.OrdinalIgnoreCase)); | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + private sealed class AdminRoleCodeRow | ||
| 91 | + { | ||
| 92 | + public string? RoleCode { get; set; } | ||
| 93 | + } | ||
| 69 | } | 94 | } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ScopeAllEchoHelper.cs
0 → 100644
| 1 | +using SqlSugar; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Helpers; | ||
| 4 | + | ||
| 5 | +/// <summary> | ||
| 6 | +/// 编辑/列表回显:库中可存展开 Guid;若绑定覆盖全集则将 Id 数组折叠为 <c>["ALL"]</c> 哨兵(对齐 Team Member)。 | ||
| 7 | +/// </summary> | ||
| 8 | +public static class ScopeAllEchoHelper | ||
| 9 | +{ | ||
| 10 | + public sealed class ScopeAllEchoOptions | ||
| 11 | + { | ||
| 12 | + public bool IsPartnerAll { get; init; } | ||
| 13 | + | ||
| 14 | + public bool IsRegionAll { get; init; } | ||
| 15 | + | ||
| 16 | + public bool IsLocationAll { get; init; } | ||
| 17 | + | ||
| 18 | + /// <summary>落库维度为 SPECIFIED 时禁止再按「恰好全选」折叠为 ALL 哨兵。</summary> | ||
| 19 | + public string? AppliedPartnerType { get; init; } | ||
| 20 | + | ||
| 21 | + public string? AppliedRegionType { get; init; } | ||
| 22 | + | ||
| 23 | + public string? AppliedLocationType { get; init; } | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + /// <summary> | ||
| 27 | + /// 将 partnerIds / regionIds / locationIds 折叠为 ALL 哨兵(编辑弹窗与列表 Id 数组回显)。 | ||
| 28 | + /// </summary> | ||
| 29 | + public static async Task<(List<string> PartnerIds, List<string> RegionIds, List<string> LocationIds)> | ||
| 30 | + CollapseScopeIdsToAllSentinelAsync( | ||
| 31 | + ISqlSugarClient db, | ||
| 32 | + IReadOnlyList<string>? partnerIds, | ||
| 33 | + IReadOnlyList<string>? regionIds, | ||
| 34 | + IReadOnlyList<string>? locationIds, | ||
| 35 | + ScopeAllEchoOptions? options = null) | ||
| 36 | + { | ||
| 37 | + options ??= new ScopeAllEchoOptions(); | ||
| 38 | + var partners = LocationScopeBindingHelper.NormalizeIds(partnerIds); | ||
| 39 | + var regions = LocationScopeBindingHelper.NormalizeIds(regionIds); | ||
| 40 | + var locations = LocationScopeBindingHelper.NormalizeIds(locationIds); | ||
| 41 | + | ||
| 42 | + if (options.IsPartnerAll) | ||
| 43 | + { | ||
| 44 | + partners = new List<string> { AllScopeBindingHelper.ScopeAll }; | ||
| 45 | + } | ||
| 46 | + else if (!AllScopeBindingHelper.IsDeclaredSpecified(options.AppliedPartnerType) && partners.Count > 0) | ||
| 47 | + { | ||
| 48 | + var allPartners = await AllScopeBindingHelper.ResolveAllPartnerIdsAsync(db); | ||
| 49 | + if (allPartners.Count > 0 && AllScopeBindingHelper.IsFullIdSelection(partners, allPartners)) | ||
| 50 | + { | ||
| 51 | + partners = new List<string> { AllScopeBindingHelper.ScopeAll }; | ||
| 52 | + } | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + if (options.IsRegionAll) | ||
| 56 | + { | ||
| 57 | + regions = new List<string> { AllScopeBindingHelper.ScopeAll }; | ||
| 58 | + } | ||
| 59 | + else if (!AllScopeBindingHelper.IsDeclaredSpecified(options.AppliedRegionType) && regions.Count > 0) | ||
| 60 | + { | ||
| 61 | + regions = await CollapseRegionsIfFullAsync(db, partners, regions); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + if (options.IsLocationAll) | ||
| 65 | + { | ||
| 66 | + locations = new List<string> { AllScopeBindingHelper.ScopeAll }; | ||
| 67 | + } | ||
| 68 | + else if (!AllScopeBindingHelper.IsDeclaredSpecified(options.AppliedLocationType) && locations.Count > 0) | ||
| 69 | + { | ||
| 70 | + locations = await CollapseLocationsIfFullAsync(db, partners, regions, locations); | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + return (partners, regions, locations); | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + /// <summary>AvailabilityType=ALL 时 Region 与 Location 均为 ALL。</summary> | ||
| 77 | + public static ScopeAllEchoOptions ForAvailabilityAll(bool isAll) => | ||
| 78 | + new() { IsRegionAll = isAll, IsLocationAll = isAll }; | ||
| 79 | + | ||
| 80 | + /// <summary>LabelTemplate 各维度独立 ALL 标记。</summary> | ||
| 81 | + public static ScopeAllEchoOptions ForLabelTemplateDimensions( | ||
| 82 | + string? appliedPartnerType, | ||
| 83 | + string? appliedRegionType, | ||
| 84 | + string? appliedLocationType) => | ||
| 85 | + new() | ||
| 86 | + { | ||
| 87 | + IsPartnerAll = AllScopeBindingHelper.IsDeclaredAll(appliedPartnerType), | ||
| 88 | + IsRegionAll = AllScopeBindingHelper.IsDeclaredAll(appliedRegionType), | ||
| 89 | + IsLocationAll = AllScopeBindingHelper.IsDeclaredAll(appliedLocationType), | ||
| 90 | + AppliedPartnerType = appliedPartnerType, | ||
| 91 | + AppliedRegionType = appliedRegionType, | ||
| 92 | + AppliedLocationType = appliedLocationType | ||
| 93 | + }; | ||
| 94 | + | ||
| 95 | + /// <summary>标签类型/分类/多选项:Company + Region + Location 可用范围。</summary> | ||
| 96 | + public static ScopeAllEchoOptions ForLabelEntityScope( | ||
| 97 | + string? appliedPartnerType, | ||
| 98 | + string? appliedRegionType, | ||
| 99 | + string? availabilityType) => | ||
| 100 | + new() | ||
| 101 | + { | ||
| 102 | + IsPartnerAll = AllScopeBindingHelper.IsDeclaredAll(appliedPartnerType), | ||
| 103 | + IsRegionAll = AllScopeBindingHelper.IsDeclaredAll(appliedRegionType), | ||
| 104 | + IsLocationAll = AllScopeBindingHelper.IsDeclaredAll(availabilityType), | ||
| 105 | + AppliedPartnerType = appliedPartnerType, | ||
| 106 | + AppliedRegionType = appliedRegionType, | ||
| 107 | + AppliedLocationType = availabilityType | ||
| 108 | + }; | ||
| 109 | + | ||
| 110 | + /// <summary>兼容旧调用:无 Region 类型时用 AvailabilityType 同时驱动 Region/Location。</summary> | ||
| 111 | + public static ScopeAllEchoOptions ForLabelEntityScope( | ||
| 112 | + string? appliedPartnerType, | ||
| 113 | + string? availabilityType) => | ||
| 114 | + ForLabelEntityScope(appliedPartnerType, availabilityType, availabilityType); | ||
| 115 | + | ||
| 116 | + /// <summary>Label:AppliedRegionType=ALL 且未落门店快照时 Region/Location 均为 ALL。</summary> | ||
| 117 | + public static ScopeAllEchoOptions ForLabelRegionScope( | ||
| 118 | + string? appliedRegionType, | ||
| 119 | + IReadOnlyList<string> locationIds) | ||
| 120 | + { | ||
| 121 | + var isRegionAll = AllScopeBindingHelper.IsDeclaredAll(appliedRegionType); | ||
| 122 | + var normalizedLocations = LocationScopeBindingHelper.NormalizeIds(locationIds); | ||
| 123 | + var isLocationAll = isRegionAll && normalizedLocations.Count == 0; | ||
| 124 | + return new ScopeAllEchoOptions | ||
| 125 | + { | ||
| 126 | + IsRegionAll = isRegionAll, | ||
| 127 | + IsLocationAll = isLocationAll, | ||
| 128 | + AppliedRegionType = appliedRegionType, | ||
| 129 | + AppliedLocationType = isLocationAll | ||
| 130 | + ? AllScopeBindingHelper.ScopeAll | ||
| 131 | + : AllScopeBindingHelper.ScopeSpecified | ||
| 132 | + }; | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + private static async Task<List<string>> CollapseRegionsIfFullAsync( | ||
| 136 | + ISqlSugarClient db, | ||
| 137 | + IReadOnlyList<string> partners, | ||
| 138 | + IReadOnlyList<string> regions) | ||
| 139 | + { | ||
| 140 | + var partnerContext = ResolvePartnerContextForCollapse(partners); | ||
| 141 | + var allRegionIds = partnerContext is { Count: > 0 } | ||
| 142 | + ? await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync(db, partnerContext) | ||
| 143 | + : await AllScopeBindingHelper.ResolveAllRegionIdsAsync(db, null); | ||
| 144 | + | ||
| 145 | + if (allRegionIds.Count > 0 && AllScopeBindingHelper.IsFullIdSelection(regions, allRegionIds)) | ||
| 146 | + { | ||
| 147 | + return new List<string> { AllScopeBindingHelper.ScopeAll }; | ||
| 148 | + } | ||
| 149 | + | ||
| 150 | + return regions.ToList(); | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + private static async Task<List<string>> CollapseLocationsIfFullAsync( | ||
| 154 | + ISqlSugarClient db, | ||
| 155 | + IReadOnlyList<string> partners, | ||
| 156 | + IReadOnlyList<string> regions, | ||
| 157 | + IReadOnlyList<string> locations) | ||
| 158 | + { | ||
| 159 | + var partnerContext = ResolvePartnerContextForCollapse(partners); | ||
| 160 | + List<string> allLocationIds; | ||
| 161 | + | ||
| 162 | + if (regions.Count == 1 && AllScopeBindingHelper.IsDeclaredAll(regions[0])) | ||
| 163 | + { | ||
| 164 | + allLocationIds = partnerContext is { Count: > 0 } | ||
| 165 | + ? await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(db, partnerContext) | ||
| 166 | + : await AllScopeBindingHelper.ResolveAllLocationIdsAsync(db, null, null); | ||
| 167 | + } | ||
| 168 | + else if (regions.Count > 0 && !regions.Any(r => AllScopeBindingHelper.IsDeclaredAll(r))) | ||
| 169 | + { | ||
| 170 | + allLocationIds = await AllScopeBindingHelper.ResolveAllLocationIdsAsync(db, partnerContext, regions); | ||
| 171 | + } | ||
| 172 | + else if (partnerContext is { Count: > 0 }) | ||
| 173 | + { | ||
| 174 | + allLocationIds = await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(db, partnerContext); | ||
| 175 | + } | ||
| 176 | + else | ||
| 177 | + { | ||
| 178 | + allLocationIds = await AllScopeBindingHelper.ResolveAllLocationIdsAsync(db, null, null); | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + if (allLocationIds.Count > 0 && AllScopeBindingHelper.IsFullIdSelection(locations, allLocationIds)) | ||
| 182 | + { | ||
| 183 | + // 具体 Region:覆盖该区全部门店时一律回显 ["ALL"](含区内仅 1 店;前端 ALL 常展开为具体 Guid) | ||
| 184 | + var hasConcreteRegions = regions.Count > 0 | ||
| 185 | + && !regions.Any(AllScopeBindingHelper.IsDeclaredAll); | ||
| 186 | + if (hasConcreteRegions) | ||
| 187 | + { | ||
| 188 | + return new List<string> { AllScopeBindingHelper.ScopeAll }; | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + // Region=ALL(或无具体 Region)时:仅多店才折叠,避免「Region=ALL + 选 1 店」误成 Location ALL | ||
| 192 | + if (locations.Count > 1 || allLocationIds.Count > 1) | ||
| 193 | + { | ||
| 194 | + return new List<string> { AllScopeBindingHelper.ScopeAll }; | ||
| 195 | + } | ||
| 196 | + } | ||
| 197 | + | ||
| 198 | + return locations.ToList(); | ||
| 199 | + } | ||
| 200 | + | ||
| 201 | + private static List<string>? ResolvePartnerContextForCollapse(IReadOnlyList<string> partners) | ||
| 202 | + { | ||
| 203 | + if (partners.Count == 0 || AllScopeBindingHelper.IsDeclaredAll(partners[0])) | ||
| 204 | + { | ||
| 205 | + return null; | ||
| 206 | + } | ||
| 207 | + | ||
| 208 | + return partners.ToList(); | ||
| 209 | + } | ||
| 210 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberScopeDisplayHelper.cs
| @@ -26,7 +26,8 @@ public static class TeamMemberScopeDisplayHelper | @@ -26,7 +26,8 @@ public static class TeamMemberScopeDisplayHelper | ||
| 26 | Guid? roleId, | 26 | Guid? roleId, |
| 27 | IReadOnlyList<string> partnerIds, | 27 | IReadOnlyList<string> partnerIds, |
| 28 | IReadOnlyList<string> regionIds, | 28 | IReadOnlyList<string> regionIds, |
| 29 | - IReadOnlyList<TeamMemberAssignedLocationDto> assigned) | 29 | + IReadOnlyList<TeamMemberAssignedLocationDto> assigned, |
| 30 | + string? appliedLocationType = null) | ||
| 30 | { | 31 | { |
| 31 | var assignedIds = assigned | 32 | var assignedIds = assigned |
| 32 | .Select(x => x.Id) | 33 | .Select(x => x.Id) |
| @@ -35,30 +36,43 @@ public static class TeamMemberScopeDisplayHelper | @@ -35,30 +36,43 @@ public static class TeamMemberScopeDisplayHelper | ||
| 35 | .Distinct(StringComparer.Ordinal) | 36 | .Distinct(StringComparer.Ordinal) |
| 36 | .ToList(); | 37 | .ToList(); |
| 37 | 38 | ||
| 39 | + if (AllScopeBindingHelper.IsDeclaredAll(appliedLocationType)) | ||
| 40 | + { | ||
| 41 | + if (partnerIds.Count > 0) | ||
| 42 | + { | ||
| 43 | + var concreteRegions = LocationScopeBindingHelper.FilterConcreteScopeIds(regionIds); | ||
| 44 | + var universe = concreteRegions.Count > 0 | ||
| 45 | + ? await AllScopeBindingHelper.ResolveAllLocationIdsAsync(db, partnerIds, concreteRegions) | ||
| 46 | + : await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(db, partnerIds); | ||
| 47 | + if (universe.Count > 0 | ||
| 48 | + && !AllScopeBindingHelper.IsFullIdSelection(assignedIds, universe)) | ||
| 49 | + { | ||
| 50 | + return assigned.ToList(); | ||
| 51 | + } | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + return AllLocationDisplay; | ||
| 55 | + } | ||
| 56 | + | ||
| 38 | if (assignedIds.Count == 0) | 57 | if (assignedIds.Count == 0) |
| 39 | { | 58 | { |
| 40 | return assigned.ToList(); | 59 | return assigned.ToList(); |
| 41 | } | 60 | } |
| 42 | 61 | ||
| 43 | - if (await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(db, roleId) && partnerIds.Count > 0) | 62 | + if (partnerIds.Count > 0) |
| 44 | { | 63 | { |
| 45 | - var allPartnerLocationIds = await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync( | ||
| 46 | - db, partnerIds); | ||
| 47 | - if (allPartnerLocationIds.Count > 0 && | ||
| 48 | - AllScopeBindingHelper.IsFullIdSelection(assignedIds, allPartnerLocationIds)) | 64 | + // 有具体 Region 时按 Region 范围内全选判断;否则按 Company 全部门店 |
| 65 | + var concreteRegions = LocationScopeBindingHelper.FilterConcreteScopeIds(regionIds); | ||
| 66 | + var universe = concreteRegions.Count > 0 | ||
| 67 | + ? await AllScopeBindingHelper.ResolveAllLocationIdsAsync(db, partnerIds, concreteRegions) | ||
| 68 | + : await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(db, partnerIds); | ||
| 69 | + if (universe.Count > 0 && | ||
| 70 | + AllScopeBindingHelper.IsFullIdSelection(assignedIds, universe)) | ||
| 49 | { | 71 | { |
| 50 | return AllLocationDisplay; | 72 | return AllLocationDisplay; |
| 51 | } | 73 | } |
| 52 | } | 74 | } |
| 53 | 75 | ||
| 54 | - var universeLocationIds = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | ||
| 55 | - db, partnerIds, regionIds, null); | ||
| 56 | - if (universeLocationIds.Count > 0 && | ||
| 57 | - AllScopeBindingHelper.IsFullIdSelection(assignedIds, universeLocationIds)) | ||
| 58 | - { | ||
| 59 | - return AllLocationDisplay; | ||
| 60 | - } | ||
| 61 | - | ||
| 62 | return assigned.ToList(); | 76 | return assigned.ToList(); |
| 63 | } | 77 | } |
| 64 | 78 | ||
| @@ -69,8 +83,15 @@ public static class TeamMemberScopeDisplayHelper | @@ -69,8 +83,15 @@ public static class TeamMemberScopeDisplayHelper | ||
| 69 | ISqlSugarClient db, | 83 | ISqlSugarClient db, |
| 70 | IReadOnlyList<string> partnerIds, | 84 | IReadOnlyList<string> partnerIds, |
| 71 | IReadOnlyList<string> regionIds, | 85 | IReadOnlyList<string> regionIds, |
| 72 | - IReadOnlyDictionary<string, string> regionNameMap) | 86 | + IReadOnlyDictionary<string, string> regionNameMap, |
| 87 | + string? appliedRegionType = null) | ||
| 73 | { | 88 | { |
| 89 | + if (AllScopeBindingHelper.IsDeclaredAll(appliedRegionType) | ||
| 90 | + || (regionIds.Count == 1 && AllScopeBindingHelper.IsDeclaredAll(regionIds[0]))) | ||
| 91 | + { | ||
| 92 | + return FoodLabelingDisplayConsts.AllRegion; | ||
| 93 | + } | ||
| 94 | + | ||
| 74 | if (regionIds.Count == 0) | 95 | if (regionIds.Count == 0) |
| 75 | { | 96 | { |
| 76 | return FoodLabelingDisplayConsts.NotAvailable; | 97 | return FoodLabelingDisplayConsts.NotAvailable; |
| @@ -92,6 +113,118 @@ public static class TeamMemberScopeDisplayHelper | @@ -92,6 +113,118 @@ public static class TeamMemberScopeDisplayHelper | ||
| 92 | : id)); | 113 | : id)); |
| 93 | } | 114 | } |
| 94 | 115 | ||
| 116 | + /// <summary> | ||
| 117 | + /// 编辑回显:库中存展开后的 Guid;结合 AppliedRegionType/AppliedLocationType 折叠为 <c>["ALL"]</c>。 | ||
| 118 | + /// </summary> | ||
| 119 | + public static async Task<(List<string> RegionIds, List<string> LocationIds, List<TeamMemberAssignedLocationDto> Assigned)> | ||
| 120 | + CollapseScopeIdsToAllSentinelForEditAsync( | ||
| 121 | + ISqlSugarClient db, | ||
| 122 | + IReadOnlyList<string> partnerIds, | ||
| 123 | + IReadOnlyList<string> regionIds, | ||
| 124 | + IReadOnlyList<string> locationIds, | ||
| 125 | + IReadOnlyList<TeamMemberAssignedLocationDto> assigned, | ||
| 126 | + string? appliedRegionType = null, | ||
| 127 | + string? appliedLocationType = null) | ||
| 128 | + { | ||
| 129 | + var partners = LocationScopeBindingHelper.NormalizeIds(partnerIds); | ||
| 130 | + var regions = LocationScopeBindingHelper.NormalizeIds(regionIds); | ||
| 131 | + var locations = LocationScopeBindingHelper.NormalizeIds(locationIds); | ||
| 132 | + var assignedList = assigned?.ToList() ?? new List<TeamMemberAssignedLocationDto>(); | ||
| 133 | + | ||
| 134 | + if (AllScopeBindingHelper.IsDeclaredAll(appliedRegionType)) | ||
| 135 | + { | ||
| 136 | + regions = new List<string> { AllScopeBindingHelper.ScopeAll }; | ||
| 137 | + } | ||
| 138 | + | ||
| 139 | + if (AllScopeBindingHelper.IsDeclaredAll(appliedLocationType)) | ||
| 140 | + { | ||
| 141 | + // 误标 ALL 但库中仅为部分门店时,回显具体 Id(与新增传参对称) | ||
| 142 | + if (locations.Count > 0 && partners.Count > 0) | ||
| 143 | + { | ||
| 144 | + var scopedRegions = LocationScopeBindingHelper.FilterConcreteScopeIds(regions); | ||
| 145 | + var universe = scopedRegions.Count > 0 | ||
| 146 | + ? await AllScopeBindingHelper.ResolveAllLocationIdsAsync(db, partners, scopedRegions) | ||
| 147 | + : await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(db, partners); | ||
| 148 | + if (!AllScopeBindingHelper.IsFullIdSelection(locations, universe)) | ||
| 149 | + { | ||
| 150 | + return (regions, locations, assignedList); | ||
| 151 | + } | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + locations = new List<string> { AllScopeBindingHelper.ScopeAll }; | ||
| 155 | + assignedList = AllLocationDisplay; | ||
| 156 | + return (regions, locations, assignedList); | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + // 落库为 SPECIFIED(显式选店):禁止再按「区内恰好全选」折叠成 ALL | ||
| 160 | + if (AllScopeBindingHelper.IsDeclaredSpecified(appliedLocationType)) | ||
| 161 | + { | ||
| 162 | + if (partners.Count == 0) | ||
| 163 | + { | ||
| 164 | + return (regions, locations, assignedList); | ||
| 165 | + } | ||
| 166 | + | ||
| 167 | + if (!AllScopeBindingHelper.IsDeclaredAll(appliedRegionType)) | ||
| 168 | + { | ||
| 169 | + var allRegionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync(db, partners); | ||
| 170 | + if (allRegionIds.Count > 0 && AllScopeBindingHelper.IsFullIdSelection(regions, allRegionIds)) | ||
| 171 | + { | ||
| 172 | + regions = new List<string> { AllScopeBindingHelper.ScopeAll }; | ||
| 173 | + } | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + return (regions, locations, assignedList); | ||
| 177 | + } | ||
| 178 | + | ||
| 179 | + if (partners.Count == 0) | ||
| 180 | + { | ||
| 181 | + return (regions, locations, assignedList); | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + if (!AllScopeBindingHelper.IsDeclaredAll(appliedRegionType)) | ||
| 185 | + { | ||
| 186 | + var allRegionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync(db, partners); | ||
| 187 | + if (allRegionIds.Count > 0 && AllScopeBindingHelper.IsFullIdSelection(regions, allRegionIds)) | ||
| 188 | + { | ||
| 189 | + regions = new List<string> { AllScopeBindingHelper.ScopeAll }; | ||
| 190 | + } | ||
| 191 | + } | ||
| 192 | + | ||
| 193 | + // Region=ALL + 具体门店:禁止再按「派生单 Region 区内全选」把 Location 折成 ALL | ||
| 194 | + if (AllScopeBindingHelper.IsDeclaredAll(appliedRegionType) | ||
| 195 | + || (regions.Count == 1 && AllScopeBindingHelper.IsDeclaredAll(regions[0]))) | ||
| 196 | + { | ||
| 197 | + var partnerUniverse = await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync( | ||
| 198 | + db, partners); | ||
| 199 | + if (partnerUniverse.Count > 1 | ||
| 200 | + && locations.Count > 1 | ||
| 201 | + && AllScopeBindingHelper.IsFullIdSelection(locations, partnerUniverse)) | ||
| 202 | + { | ||
| 203 | + locations = new List<string> { AllScopeBindingHelper.ScopeAll }; | ||
| 204 | + assignedList = AllLocationDisplay; | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + return (regions, locations, assignedList); | ||
| 208 | + } | ||
| 209 | + | ||
| 210 | + // 具体 Region:按区内门店全集判断是否折叠 locationIds | ||
| 211 | + var concreteRegions = LocationScopeBindingHelper.FilterConcreteScopeIds(regions); | ||
| 212 | + var allLocationIds = concreteRegions.Count > 0 | ||
| 213 | + ? await AllScopeBindingHelper.ResolveAllLocationIdsAsync(db, partners, concreteRegions) | ||
| 214 | + : await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(db, partners); | ||
| 215 | + if (allLocationIds.Count > 0 && AllScopeBindingHelper.IsFullIdSelection(locations, allLocationIds)) | ||
| 216 | + { | ||
| 217 | + var hasConcreteRegions = concreteRegions.Count > 0; | ||
| 218 | + if (hasConcreteRegions || locations.Count > 1 || allLocationIds.Count > 1) | ||
| 219 | + { | ||
| 220 | + locations = new List<string> { AllScopeBindingHelper.ScopeAll }; | ||
| 221 | + assignedList = AllLocationDisplay; | ||
| 222 | + } | ||
| 223 | + } | ||
| 224 | + | ||
| 225 | + return (regions, locations, assignedList); | ||
| 226 | + } | ||
| 227 | + | ||
| 95 | public static string FormatLocationTextForList(IReadOnlyList<TeamMemberAssignedLocationDto> assigned) | 228 | public static string FormatLocationTextForList(IReadOnlyList<TeamMemberAssignedLocationDto> assigned) |
| 96 | { | 229 | { |
| 97 | if (assigned.Count == 0) | 230 | if (assigned.Count == 0) |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TenantBusinessContextHelper.cs
0 → 100644
| 1 | +using System.IdentityModel.Tokens.Jwt; | ||
| 2 | +using System.Security.Claims; | ||
| 3 | +using Microsoft.AspNetCore.Http; | ||
| 4 | +using Volo.Abp.MultiTenancy; | ||
| 5 | +using Volo.Abp.Security.Claims; | ||
| 6 | +using Yi.Framework.Rbac.Domain.Shared.Consts; | ||
| 7 | +using Yi.Framework.SqlSugarCore.Abstractions; | ||
| 8 | + | ||
| 9 | +namespace FoodLabeling.Application.Helpers; | ||
| 10 | + | ||
| 11 | +/// <summary> | ||
| 12 | +/// SaaS 业务租户上下文判定(平台主库 / Default 租户不走公司菜单子集过滤)。 | ||
| 13 | +/// </summary> | ||
| 14 | +public static class TenantBusinessContextHelper | ||
| 15 | +{ | ||
| 16 | + private static readonly Guid ProtectedDefaultTenantId = Guid.Parse("11111111-1111-1111-1111-111111111111"); | ||
| 17 | + | ||
| 18 | + /// <summary> | ||
| 19 | + /// 解析当前请求的业务租户 Id:优先 <see cref="ICurrentTenant"/>,其次 JWT / Authorization Claim。 | ||
| 20 | + /// </summary> | ||
| 21 | + public static Guid? ResolveBusinessTenantId(ICurrentTenant? currentTenant, HttpContext? httpContext) | ||
| 22 | + { | ||
| 23 | + if (currentTenant?.Id is { } fromContext | ||
| 24 | + && fromContext != Guid.Empty | ||
| 25 | + && fromContext != ProtectedDefaultTenantId) | ||
| 26 | + { | ||
| 27 | + return fromContext; | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + var fromJwt = TryGetTenantIdFromHttpContext(httpContext); | ||
| 31 | + if (fromJwt.HasValue | ||
| 32 | + && fromJwt.Value != Guid.Empty | ||
| 33 | + && fromJwt.Value != ProtectedDefaultTenantId) | ||
| 34 | + { | ||
| 35 | + return fromJwt; | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + return null; | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + /// <summary> | ||
| 42 | + /// 当前请求是否处于真实公司业务租户上下文(须按 fl_th_tenant_menu_permission 限定可分配菜单)。 | ||
| 43 | + /// </summary> | ||
| 44 | + public static bool ShouldScopeMenusByCompany( | ||
| 45 | + DbConnOptions options, | ||
| 46 | + ICurrentTenant? currentTenant, | ||
| 47 | + HttpContext? httpContext) | ||
| 48 | + { | ||
| 49 | + return ShouldScopeMenusByCompany(options, ResolveBusinessTenantId(currentTenant, httpContext)); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + /// <summary> | ||
| 53 | + /// 当前请求是否处于真实公司业务租户上下文(须按 fl_th_tenant_menu_permission 限定可分配菜单)。 | ||
| 54 | + /// </summary> | ||
| 55 | + public static bool ShouldScopeMenusByCompany(DbConnOptions options, Guid? tenantId) | ||
| 56 | + { | ||
| 57 | + return options.EnabledSaasMultiTenancy | ||
| 58 | + && tenantId.HasValue | ||
| 59 | + && tenantId.Value != Guid.Empty | ||
| 60 | + && tenantId.Value != ProtectedDefaultTenantId; | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + /// <summary> | ||
| 64 | + /// 从 HttpContext Principal 或 Authorization Bearer JWT 读取 TenantId Claim 原始值。 | ||
| 65 | + /// </summary> | ||
| 66 | + public static string? TryGetTenantIdClaimValue(HttpContext? httpContext) | ||
| 67 | + { | ||
| 68 | + if (httpContext is null) | ||
| 69 | + { | ||
| 70 | + return null; | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + return TryGetTenantIdFromPrincipal(httpContext.User) | ||
| 74 | + ?? TryGetTenantIdFromAuthorizationHeader(httpContext.Request.Headers.Authorization.ToString()); | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + private static Guid? TryGetTenantIdFromHttpContext(HttpContext? httpContext) | ||
| 78 | + { | ||
| 79 | + var tenantClaim = TryGetTenantIdClaimValue(httpContext); | ||
| 80 | + if (string.IsNullOrWhiteSpace(tenantClaim) | ||
| 81 | + || !Guid.TryParse(tenantClaim, out var tenantGuid) | ||
| 82 | + || tenantGuid == Guid.Empty) | ||
| 83 | + { | ||
| 84 | + return null; | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + return tenantGuid; | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + private static string? TryGetTenantIdFromPrincipal(ClaimsPrincipal? user) | ||
| 91 | + { | ||
| 92 | + if (user?.Identity?.IsAuthenticated != true) | ||
| 93 | + { | ||
| 94 | + return null; | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + return user.FindFirst(TokenTypeConst.TenantId)?.Value | ||
| 98 | + ?? user.FindFirst(AbpClaimTypes.TenantId)?.Value | ||
| 99 | + ?? user.Claims.FirstOrDefault(c => | ||
| 100 | + c.Type.EndsWith("tenantId", StringComparison.OrdinalIgnoreCase) | ||
| 101 | + || c.Type.Equals("tenantid", StringComparison.OrdinalIgnoreCase))?.Value; | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + /// <summary> | ||
| 105 | + /// 多租户中间件可能早于 JWT Principal 就绪;直接从 Authorization 解析 TenantId Claim。 | ||
| 106 | + /// </summary> | ||
| 107 | + private static string? TryGetTenantIdFromAuthorizationHeader(string? authorization) | ||
| 108 | + { | ||
| 109 | + if (string.IsNullOrWhiteSpace(authorization)) | ||
| 110 | + { | ||
| 111 | + return null; | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + const string bearerPrefix = "Bearer "; | ||
| 115 | + if (!authorization.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase)) | ||
| 116 | + { | ||
| 117 | + return null; | ||
| 118 | + } | ||
| 119 | + | ||
| 120 | + var jwt = authorization[bearerPrefix.Length..].Trim(); | ||
| 121 | + if (string.IsNullOrWhiteSpace(jwt)) | ||
| 122 | + { | ||
| 123 | + return null; | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + try | ||
| 127 | + { | ||
| 128 | + var token = new JwtSecurityTokenHandler().ReadJwtToken(jwt); | ||
| 129 | + return token.Claims.FirstOrDefault(c => | ||
| 130 | + c.Type == TokenTypeConst.TenantId | ||
| 131 | + || c.Type == AbpClaimTypes.TenantId | ||
| 132 | + || c.Type.EndsWith("tenantId", StringComparison.OrdinalIgnoreCase)) | ||
| 133 | + ?.Value; | ||
| 134 | + } | ||
| 135 | + catch (Exception) | ||
| 136 | + { | ||
| 137 | + return null; | ||
| 138 | + } | ||
| 139 | + } | ||
| 140 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TenantContextGuard.cs
| 1 | using Volo.Abp; | 1 | using Volo.Abp; |
| 2 | using Volo.Abp.MultiTenancy; | 2 | using Volo.Abp.MultiTenancy; |
| 3 | +using Yi.Framework.SqlSugarCore.Abstractions; | ||
| 3 | 4 | ||
| 4 | namespace FoodLabeling.Application.Helpers; | 5 | namespace FoodLabeling.Application.Helpers; |
| 5 | 6 | ||
| @@ -8,9 +9,15 @@ namespace FoodLabeling.Application.Helpers; | @@ -8,9 +9,15 @@ namespace FoodLabeling.Application.Helpers; | ||
| 8 | /// </summary> | 9 | /// </summary> |
| 9 | public static class TenantContextGuard | 10 | public static class TenantContextGuard |
| 10 | { | 11 | { |
| 12 | + /// <summary> | ||
| 13 | + /// 平台主库登录(JWT/__tenant 均无业务租户)时访问 fl_* 等业务表的友好提示。 | ||
| 14 | + /// </summary> | ||
| 15 | + public const string PlatformCannotAccessBusinessDataMessage = | ||
| 16 | + "当前为平台主库登录,无法访问公司业务数据(标签/产品/成员等)。请选择具体公司登录,或使用平台「公司管理」相关接口。"; | ||
| 17 | + | ||
| 11 | public static void EnsureTenantResolved(ICurrentTenant currentTenant, string? operation = null) | 18 | public static void EnsureTenantResolved(ICurrentTenant currentTenant, string? operation = null) |
| 12 | { | 19 | { |
| 13 | - if (currentTenant.Id.HasValue) | 20 | + if (currentTenant.Id.HasValue && currentTenant.Id.Value != Guid.Empty) |
| 14 | { | 21 | { |
| 15 | return; | 22 | return; |
| 16 | } | 23 | } |
| @@ -19,6 +26,78 @@ public static class TenantContextGuard | @@ -19,6 +26,78 @@ public static class TenantContextGuard | ||
| 19 | ? "未识别租户上下文" | 26 | ? "未识别租户上下文" |
| 20 | : $"{operation}:未识别租户上下文"; | 27 | : $"{operation}:未识别租户上下文"; |
| 21 | throw new UserFriendlyException( | 28 | throw new UserFriendlyException( |
| 22 | - $"{hint}。请使用泰额登录接口(th-web-auth / th-app-auth)或请求头 __tenant 携带租户 Id。"); | 29 | + $"{hint}。请使用泰额登录接口(th-web-auth / th-app-auth)选择具体公司登录,或请求头 __tenant 携带租户 Id。"); |
| 30 | + } | ||
| 31 | + | ||
| 32 | + /// <summary> | ||
| 33 | + /// 泰额 SaaS 多租户开启时,业务表(fl_* / location 等)必须走租户库,禁止落到 host。 | ||
| 34 | + /// </summary> | ||
| 35 | + public static void EnsureBusinessTenantIfSaas( | ||
| 36 | + ICurrentTenant currentTenant, | ||
| 37 | + DbConnOptions? dbConnOptions, | ||
| 38 | + string? operation = null) | ||
| 39 | + { | ||
| 40 | + if (dbConnOptions is null || !dbConnOptions.EnabledSaasMultiTenancy) | ||
| 41 | + { | ||
| 42 | + return; | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + EnsureTenantResolved(currentTenant, operation); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + /// <summary> | ||
| 49 | + /// 判断当前 DbContext 是否连到平台主库(antis-foodlabeling-host)。 | ||
| 50 | + /// </summary> | ||
| 51 | + public static bool IsConnectedToHostDatabase(ISqlSugarDbContext dbContext, DbConnOptions dbConnOptions) | ||
| 52 | + { | ||
| 53 | + var dbName = dbContext.SqlSugarClient.Ado.Connection.Database; | ||
| 54 | + var hostDbName = TryExtractDatabaseName(dbConnOptions.Url); | ||
| 55 | + return !string.IsNullOrWhiteSpace(hostDbName) | ||
| 56 | + && string.Equals(dbName, hostDbName, StringComparison.OrdinalIgnoreCase); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + /// <summary> | ||
| 60 | + /// SaaS 模式下校验 DbContext 未落到 host 主库(双保险,避免缺表 500)。 | ||
| 61 | + /// </summary> | ||
| 62 | + public static void EnsureNotHostDatabaseIfSaas( | ||
| 63 | + ISqlSugarDbContext dbContext, | ||
| 64 | + DbConnOptions dbConnOptions, | ||
| 65 | + string? operation = null) | ||
| 66 | + { | ||
| 67 | + if (!dbConnOptions.EnabledSaasMultiTenancy) | ||
| 68 | + { | ||
| 69 | + return; | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + if (!IsConnectedToHostDatabase(dbContext, dbConnOptions)) | ||
| 73 | + { | ||
| 74 | + return; | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + var hint = string.IsNullOrWhiteSpace(operation) | ||
| 78 | + ? PlatformCannotAccessBusinessDataMessage | ||
| 79 | + : $"{operation}:{PlatformCannotAccessBusinessDataMessage}"; | ||
| 80 | + throw new UserFriendlyException(hint); | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + private static string? TryExtractDatabaseName(string? connectionString) | ||
| 84 | + { | ||
| 85 | + if (string.IsNullOrWhiteSpace(connectionString)) | ||
| 86 | + { | ||
| 87 | + return null; | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + foreach (var part in connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries)) | ||
| 91 | + { | ||
| 92 | + var kv = part.Split('=', 2, StringSplitOptions.TrimEntries); | ||
| 93 | + if (kv.Length == 2 | ||
| 94 | + && (kv[0].Equals("database", StringComparison.OrdinalIgnoreCase) | ||
| 95 | + || kv[0].Equals("Database", StringComparison.OrdinalIgnoreCase))) | ||
| 96 | + { | ||
| 97 | + return kv[1].Trim(); | ||
| 98 | + } | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + return null; | ||
| 23 | } | 102 | } |
| 24 | } | 103 | } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TrainingFileScopeHelper.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Services.DbModels; | ||
| 2 | +using FoodLabeling.Domain.Shared.Helpers; | ||
| 3 | +using SqlSugar; | ||
| 4 | +using Volo.Abp; | ||
| 5 | + | ||
| 6 | +namespace FoodLabeling.Application.Helpers; | ||
| 7 | + | ||
| 8 | +/// <summary> | ||
| 9 | +/// 培训文件适用范围:Company / Region / Location 三维度独立 ALL/SPECIFIED。 | ||
| 10 | +/// </summary> | ||
| 11 | +public static class TrainingFileScopeHelper | ||
| 12 | +{ | ||
| 13 | + public sealed class TrainingFileScopeSaveResult | ||
| 14 | + { | ||
| 15 | + public string AppliedPartnerType { get; init; } = AllScopeBindingHelper.ScopeAll; | ||
| 16 | + | ||
| 17 | + public List<string> PartnerIds { get; init; } = new(); | ||
| 18 | + | ||
| 19 | + public string AppliedRegionType { get; init; } = AllScopeBindingHelper.ScopeAll; | ||
| 20 | + | ||
| 21 | + public List<string> RegionIds { get; init; } = new(); | ||
| 22 | + | ||
| 23 | + public string AvailabilityType { get; init; } = AllScopeBindingHelper.ScopeAll; | ||
| 24 | + | ||
| 25 | + public List<string> LocationIds { get; init; } = new(); | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + public sealed class TrainingFileScopeDisplay | ||
| 29 | + { | ||
| 30 | + public string AppliedPartnerType { get; init; } = AllScopeBindingHelper.ScopeAll; | ||
| 31 | + | ||
| 32 | + public string Company { get; init; } = LabelEntityPartnerScopeHelper.AllCompaniesDisplay; | ||
| 33 | + | ||
| 34 | + public List<string> PartnerIds { get; init; } = new(); | ||
| 35 | + | ||
| 36 | + public string AppliedRegionType { get; init; } = AllScopeBindingHelper.ScopeAll; | ||
| 37 | + | ||
| 38 | + public string Region { get; init; } = FoodLabelingDisplayConsts.AllRegion; | ||
| 39 | + | ||
| 40 | + public List<string> RegionIds { get; init; } = new(); | ||
| 41 | + | ||
| 42 | + public string AvailabilityType { get; init; } = AllScopeBindingHelper.ScopeAll; | ||
| 43 | + | ||
| 44 | + public string Location { get; init; } = FoodLabelingDisplayConsts.AllLocation; | ||
| 45 | + | ||
| 46 | + public List<string> LocationIds { get; init; } = new(); | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + public sealed class LocationScopeContext | ||
| 50 | + { | ||
| 51 | + public string LocationId { get; init; } = string.Empty; | ||
| 52 | + | ||
| 53 | + public string? PartnerId { get; init; } | ||
| 54 | + | ||
| 55 | + public string? GroupId { get; init; } | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + /// <summary> | ||
| 59 | + /// 解析保存入参中的三维度范围。 | ||
| 60 | + /// </summary> | ||
| 61 | + public static async Task<TrainingFileScopeSaveResult> ResolveScopeForSaveAsync( | ||
| 62 | + ISqlSugarClient db, | ||
| 63 | + string? appliedPartnerType, | ||
| 64 | + IReadOnlyList<string>? partnerIds, | ||
| 65 | + IReadOnlyList<string>? companyIds, | ||
| 66 | + string? appliedRegionType, | ||
| 67 | + IReadOnlyList<string>? regionIds, | ||
| 68 | + IReadOnlyList<string>? groupIds, | ||
| 69 | + string? availabilityType, | ||
| 70 | + IReadOnlyList<string>? locationIds) | ||
| 71 | + { | ||
| 72 | + var partnerScope = await LabelEntityPartnerScopeHelper.ResolvePartnerScopeForSaveAsync( | ||
| 73 | + db, | ||
| 74 | + appliedPartnerType, | ||
| 75 | + partnerIds, | ||
| 76 | + companyIds); | ||
| 77 | + | ||
| 78 | + var partnerContext = string.Equals( | ||
| 79 | + partnerScope.AppliedPartnerType, | ||
| 80 | + AllScopeBindingHelper.ScopeSpecified, | ||
| 81 | + StringComparison.OrdinalIgnoreCase) | ||
| 82 | + ? partnerScope.PartnerIds | ||
| 83 | + : null; | ||
| 84 | + | ||
| 85 | + var mergedRegionIds = NormalizeRegionIds(regionIds, groupIds); | ||
| 86 | + var hasRegionArray = regionIds is not null || groupIds is not null; | ||
| 87 | + var (regionType, regionIdsForSave) = await AllScopeBindingHelper.NormalizeRegionScopeAsync( | ||
| 88 | + db, | ||
| 89 | + appliedRegionType, | ||
| 90 | + mergedRegionIds, | ||
| 91 | + hasRegionArray, | ||
| 92 | + partnerContext); | ||
| 93 | + | ||
| 94 | + if (string.Equals(regionType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase) | ||
| 95 | + && regionIdsForSave.Count > 0) | ||
| 96 | + { | ||
| 97 | + await ValidateGroupIdsExistAsync(db, regionIdsForSave); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + var hasLocationArray = locationIds is not null; | ||
| 101 | + var (locationType, locationIdsForSave) = await AllScopeBindingHelper.NormalizeLocationScopeAsync( | ||
| 102 | + db, | ||
| 103 | + availabilityType, | ||
| 104 | + locationIds ?? new List<string>(), | ||
| 105 | + hasLocationArray, | ||
| 106 | + partnerContext, | ||
| 107 | + regionIdsForSave); | ||
| 108 | + | ||
| 109 | + if (string.Equals(locationType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase) | ||
| 110 | + && locationIdsForSave.Count > 0) | ||
| 111 | + { | ||
| 112 | + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(db, locationIdsForSave); | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + return new TrainingFileScopeSaveResult | ||
| 116 | + { | ||
| 117 | + AppliedPartnerType = partnerScope.AppliedPartnerType, | ||
| 118 | + PartnerIds = partnerScope.PartnerIds, | ||
| 119 | + AppliedRegionType = regionType, | ||
| 120 | + RegionIds = regionIdsForSave, | ||
| 121 | + AvailabilityType = locationType, | ||
| 122 | + LocationIds = locationIdsForSave | ||
| 123 | + }; | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + /// <summary> | ||
| 127 | + /// 保存培训文件三维度范围关联。 | ||
| 128 | + /// </summary> | ||
| 129 | + public static async Task SaveScopeAsync( | ||
| 130 | + ISqlSugarClient db, | ||
| 131 | + string trainingFileId, | ||
| 132 | + TrainingFileScopeSaveResult scope, | ||
| 133 | + string? currentUserId, | ||
| 134 | + DateTime now) | ||
| 135 | + { | ||
| 136 | + await db.Deleteable<FlTrainingFilePartnerDbEntity>() | ||
| 137 | + .Where(x => x.TrainingFileId == trainingFileId) | ||
| 138 | + .ExecuteCommandAsync(); | ||
| 139 | + await db.Deleteable<FlTrainingFileRegionDbEntity>() | ||
| 140 | + .Where(x => x.TrainingFileId == trainingFileId) | ||
| 141 | + .ExecuteCommandAsync(); | ||
| 142 | + await db.Deleteable<FlTrainingFileLocationDbEntity>() | ||
| 143 | + .Where(x => x.TrainingFileId == trainingFileId) | ||
| 144 | + .ExecuteCommandAsync(); | ||
| 145 | + | ||
| 146 | + if (string.Equals(scope.AppliedPartnerType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase) | ||
| 147 | + && scope.PartnerIds.Count > 0) | ||
| 148 | + { | ||
| 149 | + var rows = scope.PartnerIds.Select(pid => new FlTrainingFilePartnerDbEntity | ||
| 150 | + { | ||
| 151 | + Id = YitIdHelper.NextId().ToString(), | ||
| 152 | + TrainingFileId = trainingFileId, | ||
| 153 | + PartnerId = pid, | ||
| 154 | + CreationTime = now, | ||
| 155 | + CreatorId = currentUserId | ||
| 156 | + }).ToList(); | ||
| 157 | + await db.Insertable(rows).ExecuteCommandAsync(); | ||
| 158 | + } | ||
| 159 | + | ||
| 160 | + if (string.Equals(scope.AppliedRegionType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase) | ||
| 161 | + && scope.RegionIds.Count > 0) | ||
| 162 | + { | ||
| 163 | + var rows = scope.RegionIds.Select(gid => new FlTrainingFileRegionDbEntity | ||
| 164 | + { | ||
| 165 | + Id = YitIdHelper.NextId().ToString(), | ||
| 166 | + TrainingFileId = trainingFileId, | ||
| 167 | + GroupId = gid, | ||
| 168 | + CreationTime = now, | ||
| 169 | + CreatorId = currentUserId | ||
| 170 | + }).ToList(); | ||
| 171 | + await db.Insertable(rows).ExecuteCommandAsync(); | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + if (string.Equals(scope.AvailabilityType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase) | ||
| 175 | + && scope.LocationIds.Count > 0) | ||
| 176 | + { | ||
| 177 | + var rows = scope.LocationIds.Select(lid => new FlTrainingFileLocationDbEntity | ||
| 178 | + { | ||
| 179 | + Id = YitIdHelper.NextId().ToString(), | ||
| 180 | + TrainingFileId = trainingFileId, | ||
| 181 | + LocationId = lid, | ||
| 182 | + CreationTime = now, | ||
| 183 | + CreatorId = currentUserId | ||
| 184 | + }).ToList(); | ||
| 185 | + await db.Insertable(rows).ExecuteCommandAsync(); | ||
| 186 | + } | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + /// <summary> | ||
| 190 | + /// 构建多个培训文件的适用范围回显(列表/树批量用)。 | ||
| 191 | + /// </summary> | ||
| 192 | + public static async Task<Dictionary<string, TrainingFileScopeDisplay>> BuildScopeDisplayMapAsync( | ||
| 193 | + ISqlSugarClient db, | ||
| 194 | + IReadOnlyList<FlTrainingFileDbEntity> files) | ||
| 195 | + { | ||
| 196 | + if (files.Count == 0) | ||
| 197 | + { | ||
| 198 | + return new Dictionary<string, TrainingFileScopeDisplay>(StringComparer.Ordinal); | ||
| 199 | + } | ||
| 200 | + | ||
| 201 | + var tasks = files.Select(async file => | ||
| 202 | + (file.Id, Display: await BuildScopeDisplayAsync(db, file))); | ||
| 203 | + var results = await Task.WhenAll(tasks); | ||
| 204 | + return results.ToDictionary(x => x.Id, x => x.Display, StringComparer.Ordinal); | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + /// <summary> | ||
| 208 | + /// 删除培训文件全部范围关联。 | ||
| 209 | + /// </summary> | ||
| 210 | + public static async Task DeleteScopeRowsAsync(ISqlSugarClient db, string trainingFileId) | ||
| 211 | + { | ||
| 212 | + await db.Deleteable<FlTrainingFilePartnerDbEntity>() | ||
| 213 | + .Where(x => x.TrainingFileId == trainingFileId) | ||
| 214 | + .ExecuteCommandAsync(); | ||
| 215 | + await db.Deleteable<FlTrainingFileRegionDbEntity>() | ||
| 216 | + .Where(x => x.TrainingFileId == trainingFileId) | ||
| 217 | + .ExecuteCommandAsync(); | ||
| 218 | + await db.Deleteable<FlTrainingFileLocationDbEntity>() | ||
| 219 | + .Where(x => x.TrainingFileId == trainingFileId) | ||
| 220 | + .ExecuteCommandAsync(); | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + /// <summary> | ||
| 224 | + /// 构建详情/编辑回显用的范围数据。 | ||
| 225 | + /// </summary> | ||
| 226 | + public static async Task<TrainingFileScopeDisplay> BuildScopeDisplayAsync( | ||
| 227 | + ISqlSugarClient db, | ||
| 228 | + FlTrainingFileDbEntity file) | ||
| 229 | + { | ||
| 230 | + var partnerIds = string.Equals( | ||
| 231 | + file.AppliedPartnerType, | ||
| 232 | + AllScopeBindingHelper.ScopeSpecified, | ||
| 233 | + StringComparison.OrdinalIgnoreCase) | ||
| 234 | + ? await db.Queryable<FlTrainingFilePartnerDbEntity>() | ||
| 235 | + .Where(x => x.TrainingFileId == file.Id) | ||
| 236 | + .Select(x => x.PartnerId) | ||
| 237 | + .ToListAsync() | ||
| 238 | + : new List<string>(); | ||
| 239 | + | ||
| 240 | + var regionIds = string.Equals( | ||
| 241 | + file.AppliedRegionType, | ||
| 242 | + AllScopeBindingHelper.ScopeSpecified, | ||
| 243 | + StringComparison.OrdinalIgnoreCase) | ||
| 244 | + ? await db.Queryable<FlTrainingFileRegionDbEntity>() | ||
| 245 | + .Where(x => x.TrainingFileId == file.Id) | ||
| 246 | + .Select(x => x.GroupId) | ||
| 247 | + .ToListAsync() | ||
| 248 | + : new List<string>(); | ||
| 249 | + | ||
| 250 | + var locationIds = string.Equals( | ||
| 251 | + file.AvailabilityType, | ||
| 252 | + AllScopeBindingHelper.ScopeSpecified, | ||
| 253 | + StringComparison.OrdinalIgnoreCase) | ||
| 254 | + ? await db.Queryable<FlTrainingFileLocationDbEntity>() | ||
| 255 | + .Where(x => x.TrainingFileId == file.Id) | ||
| 256 | + .Select(x => x.LocationId) | ||
| 257 | + .ToListAsync() | ||
| 258 | + : new List<string>(); | ||
| 259 | + | ||
| 260 | + partnerIds = LocationScopeBindingHelper.NormalizeIds(partnerIds); | ||
| 261 | + regionIds = LocationScopeBindingHelper.NormalizeIds(regionIds); | ||
| 262 | + locationIds = LocationScopeBindingHelper.NormalizeIds(locationIds); | ||
| 263 | + | ||
| 264 | + var collapsed = await ScopeAllEchoHelper.CollapseScopeIdsToAllSentinelAsync( | ||
| 265 | + db, | ||
| 266 | + partnerIds, | ||
| 267 | + regionIds, | ||
| 268 | + locationIds, | ||
| 269 | + ScopeAllEchoHelper.ForLabelTemplateDimensions( | ||
| 270 | + file.AppliedPartnerType, | ||
| 271 | + file.AppliedRegionType, | ||
| 272 | + file.AvailabilityType)); | ||
| 273 | + | ||
| 274 | + return new TrainingFileScopeDisplay | ||
| 275 | + { | ||
| 276 | + AppliedPartnerType = file.AppliedPartnerType, | ||
| 277 | + Company = await BuildPartnerDisplayAsync(db, file.AppliedPartnerType, collapsed.PartnerIds), | ||
| 278 | + PartnerIds = collapsed.PartnerIds, | ||
| 279 | + AppliedRegionType = file.AppliedRegionType, | ||
| 280 | + Region = await BuildRegionDisplayAsync(db, file.AppliedRegionType, collapsed.RegionIds), | ||
| 281 | + RegionIds = collapsed.RegionIds, | ||
| 282 | + AvailabilityType = file.AvailabilityType, | ||
| 283 | + Location = await BuildLocationDisplayAsync(db, file.AvailabilityType, collapsed.LocationIds), | ||
| 284 | + LocationIds = collapsed.LocationIds | ||
| 285 | + }; | ||
| 286 | + } | ||
| 287 | + | ||
| 288 | + /// <summary> | ||
| 289 | + /// 按门店上下文过滤可见文件(Company + Region + Location 三维度 AND)。 | ||
| 290 | + /// </summary> | ||
| 291 | + public static ISugarQueryable<FlTrainingFileDbEntity> ApplyLocationVisibilityFilter( | ||
| 292 | + ISugarQueryable<FlTrainingFileDbEntity> query, | ||
| 293 | + LocationScopeContext? context) | ||
| 294 | + { | ||
| 295 | + if (context is null || string.IsNullOrWhiteSpace(context.LocationId)) | ||
| 296 | + { | ||
| 297 | + return query; | ||
| 298 | + } | ||
| 299 | + | ||
| 300 | + var locationId = context.LocationId.Trim(); | ||
| 301 | + var partnerId = context.PartnerId?.Trim(); | ||
| 302 | + var groupId = context.GroupId?.Trim(); | ||
| 303 | + | ||
| 304 | + return query.Where(f => | ||
| 305 | + f.AppliedPartnerType == AllScopeBindingHelper.ScopeAll | ||
| 306 | + || (partnerId != null | ||
| 307 | + && SqlFunc.Subqueryable<FlTrainingFilePartnerDbEntity>() | ||
| 308 | + .Where(p => p.TrainingFileId == f.Id && p.PartnerId == partnerId) | ||
| 309 | + .Any())) | ||
| 310 | + .Where(f => | ||
| 311 | + f.AppliedRegionType == AllScopeBindingHelper.ScopeAll | ||
| 312 | + || (groupId != null | ||
| 313 | + && SqlFunc.Subqueryable<FlTrainingFileRegionDbEntity>() | ||
| 314 | + .Where(r => r.TrainingFileId == f.Id && r.GroupId == groupId) | ||
| 315 | + .Any())) | ||
| 316 | + .Where(f => | ||
| 317 | + f.AvailabilityType == AllScopeBindingHelper.ScopeAll | ||
| 318 | + || SqlFunc.Subqueryable<FlTrainingFileLocationDbEntity>() | ||
| 319 | + .Where(l => l.TrainingFileId == f.Id && l.LocationId == locationId) | ||
| 320 | + .Any()); | ||
| 321 | + } | ||
| 322 | + | ||
| 323 | + /// <summary> | ||
| 324 | + /// 解析门店对应的 Company / Region 上下文。 | ||
| 325 | + /// </summary> | ||
| 326 | + public static async Task<LocationScopeContext?> ResolveLocationScopeContextAsync( | ||
| 327 | + ISqlSugarClient db, | ||
| 328 | + string locationId) | ||
| 329 | + { | ||
| 330 | + if (string.IsNullOrWhiteSpace(locationId)) | ||
| 331 | + { | ||
| 332 | + return null; | ||
| 333 | + } | ||
| 334 | + | ||
| 335 | + var normalized = locationId.Trim(); | ||
| 336 | + var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, new[] { normalized }); | ||
| 337 | + var groupIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(db, new[] { normalized }); | ||
| 338 | + | ||
| 339 | + return new LocationScopeContext | ||
| 340 | + { | ||
| 341 | + LocationId = normalized, | ||
| 342 | + PartnerId = partnerIds.FirstOrDefault(), | ||
| 343 | + GroupId = groupIds.FirstOrDefault() | ||
| 344 | + }; | ||
| 345 | + } | ||
| 346 | + | ||
| 347 | + private static List<string> NormalizeRegionIds( | ||
| 348 | + IReadOnlyList<string>? regionIds, | ||
| 349 | + IReadOnlyList<string>? groupIds) | ||
| 350 | + { | ||
| 351 | + var merged = new HashSet<string>(StringComparer.Ordinal); | ||
| 352 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(regionIds)) | ||
| 353 | + { | ||
| 354 | + merged.Add(id); | ||
| 355 | + } | ||
| 356 | + | ||
| 357 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(groupIds)) | ||
| 358 | + { | ||
| 359 | + merged.Add(id); | ||
| 360 | + } | ||
| 361 | + | ||
| 362 | + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); | ||
| 363 | + } | ||
| 364 | + | ||
| 365 | + private static async Task ValidateGroupIdsExistAsync(ISqlSugarClient db, List<string> groupIds) | ||
| 366 | + { | ||
| 367 | + if (groupIds.Count == 0) | ||
| 368 | + { | ||
| 369 | + return; | ||
| 370 | + } | ||
| 371 | + | ||
| 372 | + var existing = await db.Queryable<FlGroupDbEntity>() | ||
| 373 | + .Where(x => !x.IsDeleted && groupIds.Contains(x.Id)) | ||
| 374 | + .Select(x => x.Id) | ||
| 375 | + .ToListAsync(); | ||
| 376 | + var existingSet = new HashSet<string>(existing, StringComparer.OrdinalIgnoreCase); | ||
| 377 | + var missing = groupIds.Where(id => !existingSet.Contains(id)).ToList(); | ||
| 378 | + if (missing.Count > 0) | ||
| 379 | + { | ||
| 380 | + throw new UserFriendlyException("存在无效的 Region Id"); | ||
| 381 | + } | ||
| 382 | + } | ||
| 383 | + | ||
| 384 | + private static async Task<string> BuildPartnerDisplayAsync( | ||
| 385 | + ISqlSugarClient db, | ||
| 386 | + string appliedPartnerType, | ||
| 387 | + IReadOnlyList<string> partnerIds) | ||
| 388 | + { | ||
| 389 | + if (!string.Equals(appliedPartnerType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase)) | ||
| 390 | + { | ||
| 391 | + return LabelEntityPartnerScopeHelper.AllCompaniesDisplay; | ||
| 392 | + } | ||
| 393 | + | ||
| 394 | + if (LocationScopeBindingHelper.ContainsAllScopeSentinel(partnerIds)) | ||
| 395 | + { | ||
| 396 | + return LabelEntityPartnerScopeHelper.AllCompaniesDisplay; | ||
| 397 | + } | ||
| 398 | + | ||
| 399 | + if (partnerIds.Count == 0) | ||
| 400 | + { | ||
| 401 | + return FoodLabelingDisplayConsts.NotAvailable; | ||
| 402 | + } | ||
| 403 | + | ||
| 404 | + var rows = await db.Queryable<FlPartnerDbEntity>() | ||
| 405 | + .Where(x => !x.IsDeleted && partnerIds.Contains(x.Id)) | ||
| 406 | + .Select(x => x.PartnerName) | ||
| 407 | + .ToListAsync(); | ||
| 408 | + var names = rows.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x!.Trim()).Distinct().OrderBy(x => x).ToList(); | ||
| 409 | + return names.Count > 0 ? string.Join(", ", names) : FoodLabelingDisplayConsts.NotAvailable; | ||
| 410 | + } | ||
| 411 | + | ||
| 412 | + private static async Task<string> BuildRegionDisplayAsync( | ||
| 413 | + ISqlSugarClient db, | ||
| 414 | + string appliedRegionType, | ||
| 415 | + IReadOnlyList<string> regionIds) | ||
| 416 | + { | ||
| 417 | + if (!string.Equals(appliedRegionType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase)) | ||
| 418 | + { | ||
| 419 | + return FoodLabelingDisplayConsts.AllRegion; | ||
| 420 | + } | ||
| 421 | + | ||
| 422 | + if (LocationScopeBindingHelper.ContainsAllScopeSentinel(regionIds) || regionIds.Count == 0) | ||
| 423 | + { | ||
| 424 | + return regionIds.Count == 0 | ||
| 425 | + ? FoodLabelingDisplayConsts.NotAvailable | ||
| 426 | + : FoodLabelingDisplayConsts.AllRegion; | ||
| 427 | + } | ||
| 428 | + | ||
| 429 | + var rows = await db.Queryable<FlGroupDbEntity>() | ||
| 430 | + .Where(x => !x.IsDeleted && regionIds.Contains(x.Id)) | ||
| 431 | + .Select(x => x.GroupName) | ||
| 432 | + .ToListAsync(); | ||
| 433 | + var names = rows.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x!.Trim()).Distinct().OrderBy(x => x).ToList(); | ||
| 434 | + return names.Count > 0 ? string.Join(", ", names) : FoodLabelingDisplayConsts.NotAvailable; | ||
| 435 | + } | ||
| 436 | + | ||
| 437 | + private static async Task<string> BuildLocationDisplayAsync( | ||
| 438 | + ISqlSugarClient db, | ||
| 439 | + string availabilityType, | ||
| 440 | + IReadOnlyList<string> locationIds) | ||
| 441 | + { | ||
| 442 | + if (!string.Equals(availabilityType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase)) | ||
| 443 | + { | ||
| 444 | + return FoodLabelingDisplayConsts.AllLocation; | ||
| 445 | + } | ||
| 446 | + | ||
| 447 | + if (LocationScopeBindingHelper.ContainsAllScopeSentinel(locationIds) || locationIds.Count == 0) | ||
| 448 | + { | ||
| 449 | + return locationIds.Count == 0 | ||
| 450 | + ? FoodLabelingDisplayConsts.NotAvailable | ||
| 451 | + : FoodLabelingDisplayConsts.AllLocation; | ||
| 452 | + } | ||
| 453 | + | ||
| 454 | + var guidList = locationIds.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); | ||
| 455 | + if (guidList.Count == 0) | ||
| 456 | + { | ||
| 457 | + return FoodLabelingDisplayConsts.NotAvailable; | ||
| 458 | + } | ||
| 459 | + | ||
| 460 | + var rows = await db.Queryable<FoodLabeling.Domain.Entities.LocationAggregateRoot>() | ||
| 461 | + .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) | ||
| 462 | + .Select(x => x.LocationName) | ||
| 463 | + .ToListAsync(); | ||
| 464 | + var names = rows.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x!.Trim()).Distinct().OrderBy(x => x).ToList(); | ||
| 465 | + return names.Count > 0 ? string.Join(", ", names) : FoodLabelingDisplayConsts.NotAvailable; | ||
| 466 | + } | ||
| 467 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppAuthScopeHelper.cs
| @@ -27,7 +27,10 @@ public static class UsAppAuthScopeHelper | @@ -27,7 +27,10 @@ public static class UsAppAuthScopeHelper | ||
| 27 | } | 27 | } |
| 28 | 28 | ||
| 29 | var kind = currentUser.FindClaim(UsAppJwtClaims.ClientKind)?.Value; | 29 | var kind = currentUser.FindClaim(UsAppJwtClaims.ClientKind)?.Value; |
| 30 | - if (!string.Equals(kind, UsAppJwtClaims.ClientKindUsApp, StringComparison.Ordinal)) | 30 | + // 美国版 us-app;泰额版 us-app-auth 转发 th-app-auth 后签发 th_app |
| 31 | + var isAppToken = string.Equals(kind, UsAppJwtClaims.ClientKindUsApp, StringComparison.Ordinal) | ||
| 32 | + || string.Equals(kind, UsAppJwtClaims.ClientKindThApp, StringComparison.Ordinal); | ||
| 33 | + if (!isAppToken) | ||
| 31 | { | 34 | { |
| 32 | throw new UserFriendlyException("请使用 App 登录令牌调用该接口"); | 35 | throw new UserFriendlyException("请使用 App 登录令牌调用该接口"); |
| 33 | } | 36 | } |
| @@ -53,7 +56,13 @@ public static class UsAppAuthScopeHelper | @@ -53,7 +56,13 @@ public static class UsAppAuthScopeHelper | ||
| 53 | return true; | 56 | return true; |
| 54 | } | 57 | } |
| 55 | 58 | ||
| 56 | - return await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser); | 59 | + if (await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser)) |
| 60 | + { | ||
| 61 | + return true; | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + // App JWT 可能未写 role claim:回退查库 RoleCode=admin | ||
| 65 | + return await ReportsRoleHelper.IsAdminRoleFromDbAsync(db, currentUser.Id.Value); | ||
| 57 | } | 66 | } |
| 58 | 67 | ||
| 59 | public static async Task<List<AuthScopeCompanyOptionDto>> ListCompaniesAsync(ISqlSugarClient db) | 68 | public static async Task<List<AuthScopeCompanyOptionDto>> ListCompaniesAsync(ISqlSugarClient db) |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/MultiTenancy/EmbeddedTenantSqlScripts.cs
0 → 100644
| 1 | +using System.Reflection; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.MultiTenancy; | ||
| 4 | + | ||
| 5 | +/// <summary> | ||
| 6 | +/// 从程序集嵌入资源读取租户初始化 SQL 脚本。 | ||
| 7 | +/// </summary> | ||
| 8 | +public static class EmbeddedTenantSqlScripts | ||
| 9 | +{ | ||
| 10 | + /// <summary> | ||
| 11 | + /// 读取指定嵌入资源中的 SQL 文本。 | ||
| 12 | + /// </summary> | ||
| 13 | + /// <param name="assembly">包含 EmbeddedResource 的程序集</param> | ||
| 14 | + /// <param name="resourceName">LogicalName 或完整资源名</param> | ||
| 15 | + public static string Read(Assembly assembly, string resourceName) | ||
| 16 | + { | ||
| 17 | + var stream = assembly.GetManifestResourceStream(resourceName); | ||
| 18 | + if (stream == null) | ||
| 19 | + { | ||
| 20 | + var available = string.Join(", ", assembly.GetManifestResourceNames()); | ||
| 21 | + throw new InvalidOperationException( | ||
| 22 | + $"未找到嵌入 SQL 资源「{resourceName}」。可用资源:{available}"); | ||
| 23 | + } | ||
| 24 | + | ||
| 25 | + using (stream) | ||
| 26 | + using (var reader = new StreamReader(stream)) | ||
| 27 | + { | ||
| 28 | + return reader.ReadToEnd(); | ||
| 29 | + } | ||
| 30 | + } | ||
| 31 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/MultiTenancy/FoodLabelingTenantMigrationScriptNames.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.MultiTenancy; | ||
| 2 | + | ||
| 3 | +/// <summary> | ||
| 4 | +/// 泰额版新租户业务库初始化时自动执行的嵌入 SQL 资源名(顺序即执行顺序)。 | ||
| 5 | +/// </summary> | ||
| 6 | +public static class FoodLabelingTenantMigrationScriptNames | ||
| 7 | +{ | ||
| 8 | + public const string AppliedRegionType = "FoodLabeling.TenantMigrations.fl_entity_applied_region_type.sql"; | ||
| 9 | + | ||
| 10 | + public const string LabelPartnerId = "FoodLabeling.TenantMigrations.fl_label_partner_id.sql"; | ||
| 11 | + | ||
| 12 | + public const string ProductCategoryPartnerScope = | ||
| 13 | + "FoodLabeling.TenantMigrations.fl_product_category_partner_scope.sql"; | ||
| 14 | + | ||
| 15 | + public const string UserLocation = "FoodLabeling.TenantMigrations.fl_userlocation.sql"; | ||
| 16 | + | ||
| 17 | + public const string TeamMemberScope = "FoodLabeling.TenantMigrations.fl_team_member_scope.sql"; | ||
| 18 | + | ||
| 19 | + public const string Training = "FoodLabeling.TenantMigrations.fl_training.sql"; | ||
| 20 | + | ||
| 21 | + public const string LabelAlertTimer = "FoodLabeling.TenantMigrations.fl_label_alert_timer.sql"; | ||
| 22 | + | ||
| 23 | + /// <summary>CodeFirst 之后按序执行的脚本资源名。</summary> | ||
| 24 | + public static readonly IReadOnlyList<string> TenantInitializationOrder = | ||
| 25 | + [ | ||
| 26 | + AppliedRegionType, | ||
| 27 | + LabelPartnerId, | ||
| 28 | + ProductCategoryPartnerScope, | ||
| 29 | + UserLocation, | ||
| 30 | + TeamMemberScope, | ||
| 31 | + Training, | ||
| 32 | + LabelAlertTimer | ||
| 33 | + ]; | ||
| 34 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/MultiTenancy/JwtClaimTenantResolveContributor.cs
| 1 | +using FoodLabeling.Application.Helpers; | ||
| 1 | using Microsoft.AspNetCore.Http; | 2 | using Microsoft.AspNetCore.Http; |
| 2 | using Volo.Abp.MultiTenancy; | 3 | using Volo.Abp.MultiTenancy; |
| 3 | -using Volo.Abp.Security.Claims; | ||
| 4 | -using Yi.Framework.Rbac.Domain.Shared.Consts; | ||
| 5 | 4 | ||
| 6 | namespace FoodLabeling.Application.MultiTenancy; | 5 | namespace FoodLabeling.Application.MultiTenancy; |
| 7 | 6 | ||
| 8 | /// <summary> | 7 | /// <summary> |
| 9 | -/// 从 JWT Claim <see cref="TokenTypeConst.TenantId"/> 解析当前租户(泰额版登录写入) | 8 | +/// 从 JWT Claim TenantId 解析当前租户(泰额版登录写入) |
| 10 | /// </summary> | 9 | /// </summary> |
| 11 | public class JwtClaimTenantResolveContributor : TenantResolveContributorBase | 10 | public class JwtClaimTenantResolveContributor : TenantResolveContributorBase |
| 12 | { | 11 | { |
| @@ -16,16 +15,17 @@ public class JwtClaimTenantResolveContributor : TenantResolveContributorBase | @@ -16,16 +15,17 @@ public class JwtClaimTenantResolveContributor : TenantResolveContributorBase | ||
| 16 | 15 | ||
| 17 | public override Task ResolveAsync(ITenantResolveContext context) | 16 | public override Task ResolveAsync(ITenantResolveContext context) |
| 18 | { | 17 | { |
| 19 | - var httpContext = context.ServiceProvider.GetService(typeof(IHttpContextAccessor)) as IHttpContextAccessor; | ||
| 20 | - var user = httpContext?.HttpContext?.User; | ||
| 21 | - if (user?.Identity?.IsAuthenticated != true) | 18 | + var httpContext = (context.ServiceProvider.GetService(typeof(IHttpContextAccessor)) as IHttpContextAccessor) |
| 19 | + ?.HttpContext; | ||
| 20 | + if (httpContext is null) | ||
| 22 | { | 21 | { |
| 23 | return Task.CompletedTask; | 22 | return Task.CompletedTask; |
| 24 | } | 23 | } |
| 25 | 24 | ||
| 26 | - var tenantClaim = user.FindFirst(TokenTypeConst.TenantId)?.Value | ||
| 27 | - ?? user.FindFirst(AbpClaimTypes.TenantId)?.Value; | ||
| 28 | - if (!string.IsNullOrWhiteSpace(tenantClaim)) | 25 | + var tenantClaim = TenantBusinessContextHelper.TryGetTenantIdClaimValue(httpContext); |
| 26 | + if (!string.IsNullOrWhiteSpace(tenantClaim) | ||
| 27 | + && Guid.TryParse(tenantClaim, out var tenantGuid) | ||
| 28 | + && tenantGuid != Guid.Empty) | ||
| 29 | { | 29 | { |
| 30 | context.TenantIdOrName = tenantClaim; | 30 | context.TenantIdOrName = tenantClaim; |
| 31 | } | 31 | } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/MultiTenancy/TenantSqlScriptExecutor.cs
0 → 100644
| 1 | +using System.Text; | ||
| 2 | +using System.Text.RegularExpressions; | ||
| 3 | +using MySqlConnector; | ||
| 4 | +using SqlSugar; | ||
| 5 | + | ||
| 6 | +namespace FoodLabeling.Application.MultiTenancy; | ||
| 7 | + | ||
| 8 | +/// <summary> | ||
| 9 | +/// 在 MySQL 租户库上执行幂等 DDL/DML 脚本(支持 PREPARE / 用户变量等多语句批次)。 | ||
| 10 | +/// </summary> | ||
| 11 | +public static class TenantSqlScriptExecutor | ||
| 12 | +{ | ||
| 13 | + private static readonly Regex PrepareBlockEndRegex = | ||
| 14 | + new(@"DEALLOCATE\s+PREPARE\s+\w+\s*;\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline); | ||
| 15 | + | ||
| 16 | + private static readonly Regex CreateTableEndRegex = | ||
| 17 | + new(@"\)\s*ENGINE\s*=.+;\s*$", RegexOptions.IgnoreCase | RegexOptions.Compiled); | ||
| 18 | + | ||
| 19 | + /// <summary> | ||
| 20 | + /// 执行完整 SQL 脚本;仅支持 MySQL。 | ||
| 21 | + /// </summary> | ||
| 22 | + public static async Task ExecuteMySqlScriptAsync( | ||
| 23 | + string connectionString, | ||
| 24 | + string script, | ||
| 25 | + CancellationToken cancellationToken = default) | ||
| 26 | + { | ||
| 27 | + if (string.IsNullOrWhiteSpace(script)) | ||
| 28 | + { | ||
| 29 | + return; | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + var normalizedConnectionString = EnsureMySqlScriptConnectionOptions(connectionString); | ||
| 33 | + await using var connection = new MySqlConnection(normalizedConnectionString); | ||
| 34 | + await connection.OpenAsync(cancellationToken); | ||
| 35 | + | ||
| 36 | + foreach (var batch in SplitIntoBatches(script)) | ||
| 37 | + { | ||
| 38 | + await using var command = new MySqlCommand(batch, connection); | ||
| 39 | + await command.ExecuteNonQueryAsync(cancellationToken); | ||
| 40 | + } | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + /// <summary> | ||
| 44 | + /// 非 MySQL 租户库跳过脚本(当前脚本均为 MySQL 方言)。 | ||
| 45 | + /// </summary> | ||
| 46 | + public static bool Supports(DbType dbType) => dbType == DbType.MySql; | ||
| 47 | + | ||
| 48 | + /// <summary> | ||
| 49 | + /// 将脚本拆成可独立提交的批次:PREPARE 块、CREATE TABLE、UPDATE 等。 | ||
| 50 | + /// </summary> | ||
| 51 | + internal static IReadOnlyList<string> SplitIntoBatches(string script) | ||
| 52 | + { | ||
| 53 | + var lines = script.Replace("\r\n", "\n").Split('\n'); | ||
| 54 | + var batches = new List<string>(); | ||
| 55 | + var current = new StringBuilder(); | ||
| 56 | + | ||
| 57 | + foreach (var rawLine in lines) | ||
| 58 | + { | ||
| 59 | + var line = rawLine.TrimEnd(); | ||
| 60 | + if (string.IsNullOrWhiteSpace(line) || line.TrimStart().StartsWith("--", StringComparison.Ordinal)) | ||
| 61 | + { | ||
| 62 | + continue; | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + current.AppendLine(line); | ||
| 66 | + | ||
| 67 | + var trimmed = line.Trim(); | ||
| 68 | + if (PrepareBlockEndRegex.IsMatch(trimmed) | ||
| 69 | + || CreateTableEndRegex.IsMatch(trimmed) | ||
| 70 | + || (trimmed.StartsWith("UPDATE ", StringComparison.OrdinalIgnoreCase) && trimmed.EndsWith(';'))) | ||
| 71 | + { | ||
| 72 | + var batch = current.ToString().Trim(); | ||
| 73 | + if (!string.IsNullOrEmpty(batch)) | ||
| 74 | + { | ||
| 75 | + batches.Add(batch); | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + current.Clear(); | ||
| 79 | + } | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + var tail = current.ToString().Trim(); | ||
| 83 | + if (!string.IsNullOrEmpty(tail)) | ||
| 84 | + { | ||
| 85 | + batches.Add(tail); | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + return batches; | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + private static string EnsureMySqlScriptConnectionOptions(string connectionString) | ||
| 92 | + { | ||
| 93 | + var builder = new MySqlConnectionStringBuilder(connectionString) | ||
| 94 | + { | ||
| 95 | + AllowUserVariables = true | ||
| 96 | + }; | ||
| 97 | + | ||
| 98 | + return builder.ConnectionString; | ||
| 99 | + } | ||
| 100 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs
| @@ -3,6 +3,7 @@ using FoodLabeling.Application.Contracts.Dtos.AuthSession; | @@ -3,6 +3,7 @@ using FoodLabeling.Application.Contracts.Dtos.AuthSession; | ||
| 3 | using FoodLabeling.Application.Contracts.IServices; | 3 | using FoodLabeling.Application.Contracts.IServices; |
| 4 | using Microsoft.AspNetCore.Authorization; | 4 | using Microsoft.AspNetCore.Authorization; |
| 5 | using Microsoft.AspNetCore.Mvc; | 5 | using Microsoft.AspNetCore.Mvc; |
| 6 | +using Microsoft.Extensions.Options; | ||
| 6 | using Volo.Abp; | 7 | using Volo.Abp; |
| 7 | using Volo.Abp.Application.Services; | 8 | using Volo.Abp.Application.Services; |
| 8 | using Volo.Abp.Caching; | 9 | using Volo.Abp.Caching; |
| @@ -24,24 +25,28 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | @@ -24,24 +25,28 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | ||
| 24 | private readonly IDistributedCache<SystemEditStampCacheItem, SystemEditStampCacheKey> _systemEditStampCache; | 25 | private readonly IDistributedCache<SystemEditStampCacheItem, SystemEditStampCacheKey> _systemEditStampCache; |
| 25 | private readonly ISqlSugarDbContext _dbContext; | 26 | private readonly ISqlSugarDbContext _dbContext; |
| 26 | private readonly ISqlSugarRepository<UserAggregateRoot, Guid> _userRepository; | 27 | private readonly ISqlSugarRepository<UserAggregateRoot, Guid> _userRepository; |
| 28 | + private readonly DbConnOptions _dbConnOptions; | ||
| 27 | 29 | ||
| 28 | public AuthSessionAppService( | 30 | public AuthSessionAppService( |
| 29 | ISqlSugarDbContext dbContext, | 31 | ISqlSugarDbContext dbContext, |
| 30 | ISqlSugarRepository<UserAggregateRoot, Guid> userRepository, | 32 | ISqlSugarRepository<UserAggregateRoot, Guid> userRepository, |
| 31 | IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> userCache, | 33 | IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> userCache, |
| 32 | - IDistributedCache<SystemEditStampCacheItem, SystemEditStampCacheKey> systemEditStampCache) | 34 | + IDistributedCache<SystemEditStampCacheItem, SystemEditStampCacheKey> systemEditStampCache, |
| 35 | + IOptions<DbConnOptions> dbConnOptions) | ||
| 33 | { | 36 | { |
| 34 | _dbContext = dbContext; | 37 | _dbContext = dbContext; |
| 35 | _userRepository = userRepository; | 38 | _userRepository = userRepository; |
| 36 | _userCache = userCache; | 39 | _userCache = userCache; |
| 37 | _systemEditStampCache = systemEditStampCache; | 40 | _systemEditStampCache = systemEditStampCache; |
| 41 | + _dbConnOptions = dbConnOptions.Value; | ||
| 38 | } | 42 | } |
| 39 | 43 | ||
| 40 | /// <inheritdoc /> | 44 | /// <inheritdoc /> |
| 41 | public virtual async Task<CurrentUserMenuPermissionsOutputDto> GetMyMenusAsync() | 45 | public virtual async Task<CurrentUserMenuPermissionsOutputDto> GetMyMenusAsync() |
| 42 | { | 46 | { |
| 43 | - // 平台 Token(JWT 无 TenantId)读主库;公司 Token 须已解析租户上下文 | ||
| 44 | - if (CurrentTenant.Id.HasValue) | 47 | + // 平台 Token(JWT 无 TenantId,或前端误传 __tenant=全 0)读主库; |
| 48 | + // 仅真实业务租户 Id 才要求已解析租户上下文 | ||
| 49 | + if (CurrentTenant.Id.HasValue && CurrentTenant.Id.Value != Guid.Empty) | ||
| 45 | { | 50 | { |
| 46 | TenantContextGuard.EnsureTenantResolved(CurrentTenant, "获取菜单权限"); | 51 | TenantContextGuard.EnsureTenantResolved(CurrentTenant, "获取菜单权限"); |
| 47 | } | 52 | } |
| @@ -66,35 +71,33 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | @@ -66,35 +71,33 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | ||
| 66 | .ToListAsync(); | 71 | .ToListAsync(); |
| 67 | var distinctUserRoleIds = userRoleIds.Distinct().ToList(); | 72 | var distinctUserRoleIds = userRoleIds.Distinct().ToList(); |
| 68 | 73 | ||
| 74 | + var isAdmin = UserConst.Admin.Equals(user.UserName); | ||
| 75 | + var isSaasEnabled = _dbConnOptions.EnabledSaasMultiTenancy; | ||
| 76 | + var isPlatformLogin = !CurrentTenant.Id.HasValue || CurrentTenant.Id.Value == Guid.Empty; | ||
| 77 | + | ||
| 69 | List<MenuDbEntity> menus; | 78 | List<MenuDbEntity> menus; |
| 70 | - if (UserConst.Admin.Equals(user.UserName)) | 79 | + // 非 SaaS:admin 返回全部;SaaS 公司业务租户:一律走 RoleMenu(含公司 admin) |
| 80 | + // SaaS 平台登录:下方再统一过滤为仅平台菜单 | ||
| 81 | + if (isAdmin && !isSaasEnabled) | ||
| 82 | + { | ||
| 83 | + menus = await _dbContext.SqlSugarClient.Queryable<MenuDbEntity>() | ||
| 84 | + .Where(x => x.IsDeleted == false) | ||
| 85 | + .ToListAsync(); | ||
| 86 | + } | ||
| 87 | + else if (isAdmin && isSaasEnabled && isPlatformLogin) | ||
| 71 | { | 88 | { |
| 72 | - // MenuAggregateRoot(ParentId 为 Guid) 无法兼容 menu.ParentId=0/字符串:这里统一用 MenuDbEntity | ||
| 73 | menus = await _dbContext.SqlSugarClient.Queryable<MenuDbEntity>() | 89 | menus = await _dbContext.SqlSugarClient.Queryable<MenuDbEntity>() |
| 74 | .Where(x => x.IsDeleted == false) | 90 | .Where(x => x.IsDeleted == false) |
| 75 | .ToListAsync(); | 91 | .ToListAsync(); |
| 76 | } | 92 | } |
| 77 | else | 93 | else |
| 78 | { | 94 | { |
| 79 | - var roleIdStrs = distinctUserRoleIds.Select(x => x.ToString()).Distinct().ToList(); | ||
| 80 | - if (roleIdStrs.Count == 0) | ||
| 81 | - { | ||
| 82 | - menus = new List<MenuDbEntity>(); | ||
| 83 | - } | ||
| 84 | - else | ||
| 85 | - { | ||
| 86 | - var menuIds = await _dbContext.SqlSugarClient.Queryable<RoleMenuDbEntity>() | ||
| 87 | - .Where(x => roleIdStrs.Contains(x.RoleId)) | ||
| 88 | - .Select(x => x.MenuId) | ||
| 89 | - .Distinct() | ||
| 90 | - .ToListAsync(); | ||
| 91 | - | ||
| 92 | - menus = menuIds.Count == 0 | ||
| 93 | - ? new List<MenuDbEntity>() | ||
| 94 | - : await _dbContext.SqlSugarClient.Queryable<MenuDbEntity>() | ||
| 95 | - .Where(x => x.IsDeleted == false && menuIds.Contains(x.Id)) | ||
| 96 | - .ToListAsync(); | ||
| 97 | - } | 95 | + menus = await LoadMenusByRoleIdsAsync(distinctUserRoleIds); |
| 96 | + } | ||
| 97 | + | ||
| 98 | + if (isSaasEnabled && isPlatformLogin) | ||
| 99 | + { | ||
| 100 | + menus = PlatformMenuHelper.FilterPlatformMenusWithAncestors(menus); | ||
| 98 | } | 101 | } |
| 99 | 102 | ||
| 100 | var menuNodes = menus | 103 | var menuNodes = menus |
| @@ -126,6 +129,7 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | @@ -126,6 +129,7 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | ||
| 126 | 129 | ||
| 127 | return new CurrentUserMenuPermissionsOutputDto | 130 | return new CurrentUserMenuPermissionsOutputDto |
| 128 | { | 131 | { |
| 132 | + UserId = user.Id, | ||
| 129 | User = new CurrentUserBriefDto | 133 | User = new CurrentUserBriefDto |
| 130 | { | 134 | { |
| 131 | Id = user.Id, | 135 | Id = user.Id, |
| @@ -145,6 +149,30 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | @@ -145,6 +149,30 @@ public class AuthSessionAppService : ApplicationService, IAuthSessionAppService | ||
| 145 | } | 149 | } |
| 146 | 150 | ||
| 147 | /// <summary> | 151 | /// <summary> |
| 152 | + /// 按用户角色关联 RoleMenu 查询可见菜单(SaaS 公司业务租户 admin 与普通用户相同路径) | ||
| 153 | + /// </summary> | ||
| 154 | + private async Task<List<MenuDbEntity>> LoadMenusByRoleIdsAsync(IReadOnlyList<Guid> userRoleIds) | ||
| 155 | + { | ||
| 156 | + var roleIdStrs = userRoleIds.Select(x => x.ToString()).Distinct().ToList(); | ||
| 157 | + if (roleIdStrs.Count == 0) | ||
| 158 | + { | ||
| 159 | + return new List<MenuDbEntity>(); | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + var menuIds = await _dbContext.SqlSugarClient.Queryable<RoleMenuDbEntity>() | ||
| 163 | + .Where(x => roleIdStrs.Contains(x.RoleId)) | ||
| 164 | + .Select(x => x.MenuId) | ||
| 165 | + .Distinct() | ||
| 166 | + .ToListAsync(); | ||
| 167 | + | ||
| 168 | + return menuIds.Count == 0 | ||
| 169 | + ? new List<MenuDbEntity>() | ||
| 170 | + : await _dbContext.SqlSugarClient.Queryable<MenuDbEntity>() | ||
| 171 | + .Where(x => x.IsDeleted == false && menuIds.Contains(x.Id)) | ||
| 172 | + .ToListAsync(); | ||
| 173 | + } | ||
| 174 | + | ||
| 175 | + /// <summary> | ||
| 148 | /// 缓存未命中时,取主要业务表最近修改时间,避免 Last Updated 长期停在用户资料时间。 | 176 | /// 缓存未命中时,取主要业务表最近修改时间,避免 Last Updated 长期停在用户资料时间。 |
| 149 | /// </summary> | 177 | /// </summary> |
| 150 | private async Task<DateTime?> ResolveRecentBusinessEditTimeAsync() | 178 | private async Task<DateTime?> ResolveRecentBusinessEditTimeAsync() |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs
| 1 | -using System.Globalization; | 1 | +using System.Globalization; |
| 2 | using System.Text.Json; | 2 | using System.Text.Json; |
| 3 | using FoodLabeling.Application.Contracts.Dtos.Dashboard; | 3 | using FoodLabeling.Application.Contracts.Dtos.Dashboard; |
| 4 | using FoodLabeling.Application.Contracts.IServices; | 4 | using FoodLabeling.Application.Contracts.IServices; |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelAlertTimerDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | ||
| 4 | + | ||
| 5 | +/// <summary> | ||
| 6 | +/// 标签告警计时器(对应表:fl_label_alert_timer) | ||
| 7 | +/// </summary> | ||
| 8 | +[SugarTable("fl_label_alert_timer")] | ||
| 9 | +public class FlLabelAlertTimerDbEntity | ||
| 10 | +{ | ||
| 11 | + [SugarColumn(IsPrimaryKey = true)] | ||
| 12 | + public string Id { get; set; } = string.Empty; | ||
| 13 | + | ||
| 14 | + public string BatchId { get; set; } = string.Empty; | ||
| 15 | + | ||
| 16 | + public string PrintTaskId { get; set; } = string.Empty; | ||
| 17 | + | ||
| 18 | + public string LabelId { get; set; } = string.Empty; | ||
| 19 | + | ||
| 20 | + public string? LabelCode { get; set; } | ||
| 21 | + | ||
| 22 | + public string LabelName { get; set; } = string.Empty; | ||
| 23 | + | ||
| 24 | + public string? ProductId { get; set; } | ||
| 25 | + | ||
| 26 | + public string? ProductName { get; set; } | ||
| 27 | + | ||
| 28 | + public string LocationId { get; set; } = string.Empty; | ||
| 29 | + | ||
| 30 | + public DateTime PrintedAt { get; set; } | ||
| 31 | + | ||
| 32 | + public DateTime? BaseTime { get; set; } | ||
| 33 | + | ||
| 34 | + public DateTime ExpiresAt { get; set; } | ||
| 35 | + | ||
| 36 | + public int DurationSeconds { get; set; } | ||
| 37 | + | ||
| 38 | + public string Title { get; set; } = string.Empty; | ||
| 39 | + | ||
| 40 | + public string Subtitle { get; set; } = string.Empty; | ||
| 41 | + | ||
| 42 | + public bool IsDeleted { get; set; } | ||
| 43 | + | ||
| 44 | + public DateTime? DeletionTime { get; set; } | ||
| 45 | + | ||
| 46 | + public string? CreatedBy { get; set; } | ||
| 47 | + | ||
| 48 | + public DateTime CreationTime { get; set; } | ||
| 49 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryDbEntity.cs
| @@ -49,6 +49,11 @@ public class FlLabelCategoryDbEntity | @@ -49,6 +49,11 @@ public class FlLabelCategoryDbEntity | ||
| 49 | public string AvailabilityType { get; set; } = "ALL"; | 49 | public string AvailabilityType { get; set; } = "ALL"; |
| 50 | 50 | ||
| 51 | /// <summary> | 51 | /// <summary> |
| 52 | + /// 适用 Region 范围:ALL / SPECIFIED(ALL 时可同时 SPECIFIED 门店快照,表示全区下指定门店) | ||
| 53 | + /// </summary> | ||
| 54 | + public string AppliedRegionType { get; set; } = "ALL"; | ||
| 55 | + | ||
| 56 | + /// <summary> | ||
| 52 | /// 适用 Company 范围:ALL / SPECIFIED | 57 | /// 适用 Company 范围:ALL / SPECIFIED |
| 53 | /// </summary> | 58 | /// </summary> |
| 54 | public string AppliedPartnerType { get; set; } = "ALL"; | 59 | public string AppliedPartnerType { get; set; } = "ALL"; |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelDbEntity.cs
| @@ -28,6 +28,11 @@ public class FlLabelDbEntity | @@ -28,6 +28,11 @@ public class FlLabelDbEntity | ||
| 28 | 28 | ||
| 29 | public string? LocationId { get; set; } | 29 | public string? LocationId { get; set; } |
| 30 | 30 | ||
| 31 | + /// <summary> | ||
| 32 | + /// 适用 Company(<c>fl_partner.Id</c>,单选);Region/Location 为 ALL 时用于回显与范围校验。 | ||
| 33 | + /// </summary> | ||
| 34 | + public string? PartnerId { get; set; } | ||
| 35 | + | ||
| 31 | public string? LabelCategoryId { get; set; } | 36 | public string? LabelCategoryId { get; set; } |
| 32 | 37 | ||
| 33 | public string? LabelTypeId { get; set; } | 38 | public string? LabelTypeId { get; set; } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelMultipleOptionDbEntity.cs
| @@ -36,6 +36,11 @@ public class FlLabelMultipleOptionDbEntity | @@ -36,6 +36,11 @@ public class FlLabelMultipleOptionDbEntity | ||
| 36 | public string AvailabilityType { get; set; } = "ALL"; | 36 | public string AvailabilityType { get; set; } = "ALL"; |
| 37 | 37 | ||
| 38 | /// <summary> | 38 | /// <summary> |
| 39 | + /// 适用 Region 范围:ALL / SPECIFIED | ||
| 40 | + /// </summary> | ||
| 41 | + public string AppliedRegionType { get; set; } = "ALL"; | ||
| 42 | + | ||
| 43 | + /// <summary> | ||
| 39 | /// 适用 Company 范围:ALL / SPECIFIED | 44 | /// 适用 Company 范围:ALL / SPECIFIED |
| 40 | /// </summary> | 45 | /// </summary> |
| 41 | public string AppliedPartnerType { get; set; } = "ALL"; | 46 | public string AppliedPartnerType { get; set; } = "ALL"; |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTypeDbEntity.cs
| @@ -34,6 +34,11 @@ public class FlLabelTypeDbEntity | @@ -34,6 +34,11 @@ public class FlLabelTypeDbEntity | ||
| 34 | public string AvailabilityType { get; set; } = "ALL"; | 34 | public string AvailabilityType { get; set; } = "ALL"; |
| 35 | 35 | ||
| 36 | /// <summary> | 36 | /// <summary> |
| 37 | + /// 适用 Region 范围:ALL / SPECIFIED | ||
| 38 | + /// </summary> | ||
| 39 | + public string AppliedRegionType { get; set; } = "ALL"; | ||
| 40 | + | ||
| 41 | + /// <summary> | ||
| 37 | /// 适用 Company 范围:ALL / SPECIFIED | 42 | /// 适用 Company 范围:ALL / SPECIFIED |
| 38 | /// </summary> | 43 | /// </summary> |
| 39 | public string AppliedPartnerType { get; set; } = "ALL"; | 44 | public string AppliedPartnerType { get; set; } = "ALL"; |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryDbEntity.cs
| @@ -46,6 +46,16 @@ public class FlProductCategoryDbEntity | @@ -46,6 +46,16 @@ public class FlProductCategoryDbEntity | ||
| 46 | /// </summary> | 46 | /// </summary> |
| 47 | public string AvailabilityType { get; set; } = "ALL"; | 47 | public string AvailabilityType { get; set; } = "ALL"; |
| 48 | 48 | ||
| 49 | + /// <summary> | ||
| 50 | + /// 适用 Region 范围:ALL / SPECIFIED(ALL 时可同时 SPECIFIED 门店快照) | ||
| 51 | + /// </summary> | ||
| 52 | + public string AppliedRegionType { get; set; } = "ALL"; | ||
| 53 | + | ||
| 54 | + /// <summary> | ||
| 55 | + /// 适用 Company 范围:ALL / SPECIFIED | ||
| 56 | + /// </summary> | ||
| 57 | + public string AppliedPartnerType { get; set; } = "ALL"; | ||
| 58 | + | ||
| 49 | public int OrderNum { get; set; } | 59 | public int OrderNum { get; set; } |
| 50 | } | 60 | } |
| 51 | 61 |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryPartnerDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | ||
| 4 | + | ||
| 5 | +[SugarTable("fl_product_category_partner")] | ||
| 6 | +public class FlProductCategoryPartnerDbEntity | ||
| 7 | +{ | ||
| 8 | + [SugarColumn(IsPrimaryKey = true)] | ||
| 9 | + public string Id { get; set; } = string.Empty; | ||
| 10 | + | ||
| 11 | + public string CategoryId { get; set; } = string.Empty; | ||
| 12 | + | ||
| 13 | + public string PartnerId { get; set; } = string.Empty; | ||
| 14 | + | ||
| 15 | + public DateTime CreationTime { get; set; } | ||
| 16 | + | ||
| 17 | + public string? CreatorId { get; set; } | ||
| 18 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductDbEntity.cs
| @@ -36,4 +36,9 @@ public class FlProductDbEntity | @@ -36,4 +36,9 @@ public class FlProductDbEntity | ||
| 36 | /// 适用门店:ALL / SPECIFIED(ALL 时动态包含后续新增门店) | 36 | /// 适用门店:ALL / SPECIFIED(ALL 时动态包含后续新增门店) |
| 37 | /// </summary> | 37 | /// </summary> |
| 38 | public string AvailabilityType { get; set; } = "SPECIFIED"; | 38 | public string AvailabilityType { get; set; } = "SPECIFIED"; |
| 39 | + | ||
| 40 | + /// <summary> | ||
| 41 | + /// 适用 Region:ALL / SPECIFIED(支持 Region=ALL + 指定门店) | ||
| 42 | + /// </summary> | ||
| 43 | + public string AppliedRegionType { get; set; } = "ALL"; | ||
| 39 | } | 44 | } |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlTeamMemberScopeDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | ||
| 2 | +using Yi.Framework.SqlSugarCore.Abstractions; | ||
| 3 | + | ||
| 4 | +namespace FoodLabeling.Application.Services.DbModels; | ||
| 5 | + | ||
| 6 | +/// <summary> | ||
| 7 | +/// Team Member 适用范围维度(Region/Location 的 ALL/SPECIFIED),与 userlocation 快照配合回显。 | ||
| 8 | +/// </summary> | ||
| 9 | +[IgnoreCodeFirst] | ||
| 10 | +[SugarTable("fl_team_member_scope")] | ||
| 11 | +public class FlTeamMemberScopeDbEntity | ||
| 12 | +{ | ||
| 13 | + [SugarColumn(IsPrimaryKey = true, Length = 36)] | ||
| 14 | + public string UserId { get; set; } = string.Empty; | ||
| 15 | + | ||
| 16 | + /// <summary>适用 Region:ALL / SPECIFIED</summary> | ||
| 17 | + [SugarColumn(Length = 20)] | ||
| 18 | + public string AppliedRegionType { get; set; } = "SPECIFIED"; | ||
| 19 | + | ||
| 20 | + /// <summary>适用 Location:ALL / SPECIFIED</summary> | ||
| 21 | + [SugarColumn(Length = 20)] | ||
| 22 | + public string AppliedLocationType { get; set; } = "SPECIFIED"; | ||
| 23 | + | ||
| 24 | + public DateTime? CreationTime { get; set; } | ||
| 25 | + | ||
| 26 | + public DateTime? LastModificationTime { get; set; } | ||
| 27 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlTrainingCategoryDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | ||
| 4 | + | ||
| 5 | +[SugarTable("fl_training_category")] | ||
| 6 | +public class FlTrainingCategoryDbEntity | ||
| 7 | +{ | ||
| 8 | + [SugarColumn(IsPrimaryKey = true)] | ||
| 9 | + public string Id { get; set; } = string.Empty; | ||
| 10 | + | ||
| 11 | + public string CategoryName { get; set; } = string.Empty; | ||
| 12 | + | ||
| 13 | + public string? ParentId { get; set; } | ||
| 14 | + | ||
| 15 | + public int OrderNum { get; set; } | ||
| 16 | + | ||
| 17 | + public bool IsDeleted { get; set; } | ||
| 18 | + | ||
| 19 | + public DateTime CreationTime { get; set; } | ||
| 20 | + | ||
| 21 | + public string? CreatorId { get; set; } | ||
| 22 | + | ||
| 23 | + public DateTime? LastModificationTime { get; set; } | ||
| 24 | + | ||
| 25 | + public string? LastModifierId { get; set; } | ||
| 26 | + | ||
| 27 | + public string ConcurrencyStamp { get; set; } = string.Empty; | ||
| 28 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlTrainingFileDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | ||
| 4 | + | ||
| 5 | +[SugarTable("fl_training_file")] | ||
| 6 | +public class FlTrainingFileDbEntity | ||
| 7 | +{ | ||
| 8 | + [SugarColumn(IsPrimaryKey = true)] | ||
| 9 | + public string Id { get; set; } = string.Empty; | ||
| 10 | + | ||
| 11 | + public string CategoryId { get; set; } = string.Empty; | ||
| 12 | + | ||
| 13 | + public string FileName { get; set; } = string.Empty; | ||
| 14 | + | ||
| 15 | + public string FileUrl { get; set; } = string.Empty; | ||
| 16 | + | ||
| 17 | + /// <summary>image / doc / other</summary> | ||
| 18 | + public string FileType { get; set; } = "other"; | ||
| 19 | + | ||
| 20 | + public long FileSize { get; set; } | ||
| 21 | + | ||
| 22 | + public int OrderNum { get; set; } | ||
| 23 | + | ||
| 24 | + public string AppliedPartnerType { get; set; } = "ALL"; | ||
| 25 | + | ||
| 26 | + public string AppliedRegionType { get; set; } = "ALL"; | ||
| 27 | + | ||
| 28 | + public string AvailabilityType { get; set; } = "ALL"; | ||
| 29 | + | ||
| 30 | + public bool IsDeleted { get; set; } | ||
| 31 | + | ||
| 32 | + public DateTime CreationTime { get; set; } | ||
| 33 | + | ||
| 34 | + public string? CreatorId { get; set; } | ||
| 35 | + | ||
| 36 | + public DateTime? LastModificationTime { get; set; } | ||
| 37 | + | ||
| 38 | + public string? LastModifierId { get; set; } | ||
| 39 | + | ||
| 40 | + public string ConcurrencyStamp { get; set; } = string.Empty; | ||
| 41 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlTrainingFileLocationDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | ||
| 4 | + | ||
| 5 | +[SugarTable("fl_training_file_location")] | ||
| 6 | +public class FlTrainingFileLocationDbEntity | ||
| 7 | +{ | ||
| 8 | + [SugarColumn(IsPrimaryKey = true)] | ||
| 9 | + public string Id { get; set; } = string.Empty; | ||
| 10 | + | ||
| 11 | + public string TrainingFileId { get; set; } = string.Empty; | ||
| 12 | + | ||
| 13 | + public string LocationId { get; set; } = string.Empty; | ||
| 14 | + | ||
| 15 | + public DateTime CreationTime { get; set; } | ||
| 16 | + | ||
| 17 | + public string? CreatorId { get; set; } | ||
| 18 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlTrainingFilePartnerDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | ||
| 4 | + | ||
| 5 | +[SugarTable("fl_training_file_partner")] | ||
| 6 | +public class FlTrainingFilePartnerDbEntity | ||
| 7 | +{ | ||
| 8 | + [SugarColumn(IsPrimaryKey = true)] | ||
| 9 | + public string Id { get; set; } = string.Empty; | ||
| 10 | + | ||
| 11 | + public string TrainingFileId { get; set; } = string.Empty; | ||
| 12 | + | ||
| 13 | + public string PartnerId { get; set; } = string.Empty; | ||
| 14 | + | ||
| 15 | + public DateTime CreationTime { get; set; } | ||
| 16 | + | ||
| 17 | + public string? CreatorId { get; set; } | ||
| 18 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlTrainingFileRegionDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | ||
| 2 | + | ||
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | ||
| 4 | + | ||
| 5 | +[SugarTable("fl_training_file_region")] | ||
| 6 | +public class FlTrainingFileRegionDbEntity | ||
| 7 | +{ | ||
| 8 | + [SugarColumn(IsPrimaryKey = true)] | ||
| 9 | + public string Id { get; set; } = string.Empty; | ||
| 10 | + | ||
| 11 | + public string TrainingFileId { get; set; } = string.Empty; | ||
| 12 | + | ||
| 13 | + public string GroupId { get; set; } = string.Empty; | ||
| 14 | + | ||
| 15 | + public DateTime CreationTime { get; set; } | ||
| 16 | + | ||
| 17 | + public string? CreatorId { get; set; } | ||
| 18 | +} |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs
| @@ -4,6 +4,7 @@ using FoodLabeling.Application.Contracts.Dtos.Group; | @@ -4,6 +4,7 @@ using FoodLabeling.Application.Contracts.Dtos.Group; | ||
| 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 Microsoft.AspNetCore.Mvc; | 6 | using Microsoft.AspNetCore.Mvc; |
| 7 | +using Microsoft.Extensions.Options; | ||
| 7 | using QuestPDF.Fluent; | 8 | using QuestPDF.Fluent; |
| 8 | using QuestPDF.Helpers; | 9 | using QuestPDF.Helpers; |
| 9 | using QuestPDF.Infrastructure; | 10 | using QuestPDF.Infrastructure; |
| @@ -25,16 +26,22 @@ public class GroupAppService : ApplicationService, IGroupAppService | @@ -25,16 +26,22 @@ public class GroupAppService : ApplicationService, IGroupAppService | ||
| 25 | 26 | ||
| 26 | private readonly ISqlSugarDbContext _dbContext; | 27 | private readonly ISqlSugarDbContext _dbContext; |
| 27 | private readonly IGuidGenerator _guidGenerator; | 28 | private readonly IGuidGenerator _guidGenerator; |
| 29 | + private readonly DbConnOptions _dbConnOptions; | ||
| 28 | 30 | ||
| 29 | - public GroupAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator) | 31 | + public GroupAppService( |
| 32 | + ISqlSugarDbContext dbContext, | ||
| 33 | + IGuidGenerator guidGenerator, | ||
| 34 | + IOptions<DbConnOptions> dbConnOptions) | ||
| 30 | { | 35 | { |
| 31 | _dbContext = dbContext; | 36 | _dbContext = dbContext; |
| 32 | _guidGenerator = guidGenerator; | 37 | _guidGenerator = guidGenerator; |
| 38 | + _dbConnOptions = dbConnOptions.Value; | ||
| 33 | } | 39 | } |
| 34 | 40 | ||
| 35 | /// <inheritdoc /> | 41 | /// <inheritdoc /> |
| 36 | public async Task<PagedResultWithPageDto<GroupGetListOutputDto>> GetListAsync(GroupGetListInputVo input) | 42 | public async Task<PagedResultWithPageDto<GroupGetListOutputDto>> GetListAsync(GroupGetListInputVo input) |
| 37 | { | 43 | { |
| 44 | + TenantContextGuard.EnsureBusinessTenantIfSaas(CurrentTenant, _dbConnOptions, "查询 Region"); | ||
| 38 | RefAsync<int> total = 0; | 45 | RefAsync<int> total = 0; |
| 39 | var query = await BuildGroupJoinedQueryAsync(input); | 46 | var query = await BuildGroupJoinedQueryAsync(input); |
| 40 | var projected = query.Select((g, p) => new GroupGetListOutputDto | 47 | var projected = query.Select((g, p) => new GroupGetListOutputDto |
泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAlertTimerAppService.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Common; | ||
| 2 | +using FoodLabeling.Application.Contracts.Dtos.LabelAlertTimer; | ||
| 3 | +using FoodLabeling.Application.Contracts.IServices; | ||
| 4 | +using FoodLabeling.Application.Helpers; | ||
| 5 | +using FoodLabeling.Application.Services.DbModels; | ||
| 6 | +using Microsoft.AspNetCore.Authorization; | ||
| 7 | +using Microsoft.AspNetCore.Mvc; | ||
| 8 | +using Microsoft.Extensions.Caching.Distributed; | ||
| 9 | +using SqlSugar; | ||
| 10 | +using Volo.Abp; | ||
| 11 | +using Volo.Abp.Application.Services; | ||
| 12 | +using Yi.Framework.SqlSugarCore.Abstractions; | ||
| 13 | + | ||
| 14 | +namespace FoodLabeling.Application.Services; | ||
| 15 | + | ||
| 16 | +/// <summary> | ||
| 17 | +/// 标签告警计时器(App):跟踪已打印标签的过期倒计时;与可否打印无关。 | ||
| 18 | +/// </summary> | ||
| 19 | +public class LabelAlertTimerAppService : ApplicationService, ILabelAlertTimerAppService | ||
| 20 | +{ | ||
| 21 | + private const string StatusRunning = "running"; | ||
| 22 | + private const string StatusExpired = "expired"; | ||
| 23 | + | ||
| 24 | + private readonly ISqlSugarDbContext _dbContext; | ||
| 25 | + private readonly IDistributedCache _distributedCache; | ||
| 26 | + | ||
| 27 | + public LabelAlertTimerAppService(ISqlSugarDbContext dbContext, IDistributedCache distributedCache) | ||
| 28 | + { | ||
| 29 | + _dbContext = dbContext; | ||
| 30 | + _distributedCache = distributedCache; | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + /// <summary> | ||
| 34 | + /// 分页查询当前门店告警计时器列表(含倒计时) | ||
| 35 | + /// </summary> | ||
| 36 | + /// <remarks> | ||
| 37 | + /// 仅展示已打印标签的过期倒计时,<b>不</b>用于判断能否打印。 | ||
| 38 | + /// 过期时刻与 Print Log「Expiration」列同源(<c>ReportsPrintLogExpiryHelper</c>)。 | ||
| 39 | + /// 同一打印批次(<c>BatchId</c>)无论打印多少张标签,仅一条计时器(取 CopyIndex 最小任务)。 | ||
| 40 | + /// | ||
| 41 | + /// 示例请求: | ||
| 42 | + /// ```json | ||
| 43 | + /// { | ||
| 44 | + /// "locationId": "11111111-1111-1111-1111-111111111111", | ||
| 45 | + /// "skipCount": 1, | ||
| 46 | + /// "maxResultCount": 20, | ||
| 47 | + /// "dateDay": "2026-08-07" | ||
| 48 | + /// } | ||
| 49 | + /// ``` | ||
| 50 | + /// | ||
| 51 | + /// 参数说明: | ||
| 52 | + /// - locationId: 当前门店 Id(必填,须已绑定) | ||
| 53 | + /// - skipCount: 页码(从 1 开始) | ||
| 54 | + /// - maxResultCount: 每页条数 | ||
| 55 | + /// - dateDay: 可选,按 PrintedAt 自然日筛选(yyyy-MM-dd) | ||
| 56 | + /// </remarks> | ||
| 57 | + /// <param name="input">分页查询入参</param> | ||
| 58 | + /// <returns>分页计时器列表(含 remainingTime 倒计时秒数)</returns> | ||
| 59 | + /// <response code="200">成功返回分页列表</response> | ||
| 60 | + /// <response code="400">参数错误或未登录/无门店权限</response> | ||
| 61 | + /// <response code="500">服务器错误</response> | ||
| 62 | + [Authorize] | ||
| 63 | + [HttpPost("label-alert-timer/list")] | ||
| 64 | + public virtual async Task<PagedResultWithPageDto<LabelAlertTimerListItemDto>> GetListAsync( | ||
| 65 | + LabelAlertTimerGetListInputVo input) | ||
| 66 | + { | ||
| 67 | + if (input is null) | ||
| 68 | + { | ||
| 69 | + throw new UserFriendlyException("入参不能为空"); | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + if (!CurrentUser.Id.HasValue) | ||
| 73 | + { | ||
| 74 | + throw new UserFriendlyException("用户未登录"); | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + var locationId = input.LocationId?.Trim(); | ||
| 78 | + if (string.IsNullOrWhiteSpace(locationId)) | ||
| 79 | + { | ||
| 80 | + throw new UserFriendlyException("门店Id不能为空"); | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + return await QueryListByLocationAsync(locationId, input); | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + /// <summary> | ||
| 87 | + /// App:当前账号当前门店告警列表(含倒计时) | ||
| 88 | + /// </summary> | ||
| 89 | + /// <remarks> | ||
| 90 | + /// 供 App 警告页使用:按当前登录账号可访问的门店查询已打印标签的告警倒计时。 | ||
| 91 | + /// <c>locationId</c> 可省略,省略时使用管理员已选门店缓存(<c>select-admin-scope-location</c>); | ||
| 92 | + /// 仍无门店时返回友好错误。过期状态仅用于展示,与可否打印无关。 | ||
| 93 | + /// | ||
| 94 | + /// 示例请求: | ||
| 95 | + /// ```json | ||
| 96 | + /// { | ||
| 97 | + /// "locationId": "11111111-1111-1111-1111-111111111111", | ||
| 98 | + /// "skipCount": 1, | ||
| 99 | + /// "maxResultCount": 50 | ||
| 100 | + /// } | ||
| 101 | + /// ``` | ||
| 102 | + /// | ||
| 103 | + /// 参数说明: | ||
| 104 | + /// - locationId: 当前门店 Id(可选;空则取已选门店缓存) | ||
| 105 | + /// - skipCount: 页码(从 1 开始) | ||
| 106 | + /// - maxResultCount: 每页条数 | ||
| 107 | + /// - dateDay: 可选,按 PrintedAt 自然日筛选(yyyy-MM-dd) | ||
| 108 | + /// | ||
| 109 | + /// 出参倒计时字段: | ||
| 110 | + /// - remainingTime: 剩余秒数,App 可直接做倒计时 | ||
| 111 | + /// - totalTime: 总时长(秒) | ||
| 112 | + /// - status: running / expired | ||
| 113 | + /// - expiresAt: 过期时刻 | ||
| 114 | + /// </remarks> | ||
| 115 | + /// <param name="input">分页查询入参</param> | ||
| 116 | + /// <returns>分页告警列表(含倒计时)</returns> | ||
| 117 | + /// <response code="200">成功返回分页列表</response> | ||
| 118 | + /// <response code="400">未登录、无门店或无权限</response> | ||
| 119 | + /// <response code="500">服务器错误</response> | ||
| 120 | + [Authorize] | ||
| 121 | + [HttpPost("label-alert-timer/app-list")] | ||
| 122 | + public virtual async Task<PagedResultWithPageDto<LabelAlertTimerListItemDto>> GetAppListAsync( | ||
| 123 | + LabelAlertTimerGetListInputVo input) | ||
| 124 | + { | ||
| 125 | + if (input is null) | ||
| 126 | + { | ||
| 127 | + throw new UserFriendlyException("入参不能为空"); | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + if (!CurrentUser.Id.HasValue) | ||
| 131 | + { | ||
| 132 | + throw new UserFriendlyException("用户未登录"); | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + var locationId = input.LocationId?.Trim(); | ||
| 136 | + if (string.IsNullOrWhiteSpace(locationId)) | ||
| 137 | + { | ||
| 138 | + var cache = await UsAppAuthScopeHelper.GetAdminScopeCacheAsync( | ||
| 139 | + _distributedCache, | ||
| 140 | + CurrentUser.Id.Value); | ||
| 141 | + locationId = cache?.Location?.Id?.Trim(); | ||
| 142 | + } | ||
| 143 | + | ||
| 144 | + if (string.IsNullOrWhiteSpace(locationId)) | ||
| 145 | + { | ||
| 146 | + throw new UserFriendlyException("请先选择门店或传入 locationId"); | ||
| 147 | + } | ||
| 148 | + | ||
| 149 | + return await QueryListByLocationAsync(locationId, input); | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + /// <summary> | ||
| 153 | + /// 软删除告警计时器 | ||
| 154 | + /// </summary> | ||
| 155 | + /// <remarks> | ||
| 156 | + /// 删除前校验当前用户可访问该计时器所属门店。 | ||
| 157 | + /// </remarks> | ||
| 158 | + /// <param name="id">计时器 Id</param> | ||
| 159 | + /// <response code="200">删除成功</response> | ||
| 160 | + /// <response code="400">记录不存在或无门店权限</response> | ||
| 161 | + /// <response code="500">服务器错误</response> | ||
| 162 | + [Authorize] | ||
| 163 | + [HttpDelete("label-alert-timer/{id}")] | ||
| 164 | + public virtual async Task DeleteAsync(string id) | ||
| 165 | + { | ||
| 166 | + var timerId = id?.Trim(); | ||
| 167 | + if (string.IsNullOrWhiteSpace(timerId)) | ||
| 168 | + { | ||
| 169 | + throw new UserFriendlyException("计时器Id不能为空"); | ||
| 170 | + } | ||
| 171 | + | ||
| 172 | + if (!CurrentUser.Id.HasValue) | ||
| 173 | + { | ||
| 174 | + throw new UserFriendlyException("用户未登录"); | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + var db = _dbContext.SqlSugarClient; | ||
| 178 | + var row = (await db.Queryable<FlLabelAlertTimerDbEntity>() | ||
| 179 | + .Where(x => x.Id == timerId && !x.IsDeleted) | ||
| 180 | + .Take(1) | ||
| 181 | + .ToListAsync()) | ||
| 182 | + .FirstOrDefault(); | ||
| 183 | + if (row is null) | ||
| 184 | + { | ||
| 185 | + throw new UserFriendlyException("计时器不存在或已删除"); | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + await UsAppPrintLogScopeHelper.EnsureUserCanAccessLocationAsync( | ||
| 189 | + CurrentUser, db, row.LocationId); | ||
| 190 | + | ||
| 191 | + var now = DateTime.Now; | ||
| 192 | + await db.Updateable<FlLabelAlertTimerDbEntity>() | ||
| 193 | + .SetColumns(x => x.IsDeleted == true) | ||
| 194 | + .SetColumns(x => x.DeletionTime == now) | ||
| 195 | + .Where(x => x.Id == timerId && !x.IsDeleted) | ||
| 196 | + .ExecuteCommandAsync(); | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + /// <summary> | ||
| 200 | + /// 查询已打印标签告警的过期/倒计时状态(不拦截打印) | ||
| 201 | + /// </summary> | ||
| 202 | + /// <remarks> | ||
| 203 | + /// 至少提供 <c>timerId</c>、<c>batchId</c>、<c>printTaskId</c> 之一。 | ||
| 204 | + /// 本接口仅返回已打印批次的过期状态与剩余秒数,供展示倒计时; | ||
| 205 | + /// <b>绝不</b>用于判断「能不能打印」——打印流程不得依赖本接口结果做拦截。 | ||
| 206 | + /// | ||
| 207 | + /// 示例请求: | ||
| 208 | + /// ```json | ||
| 209 | + /// { | ||
| 210 | + /// "batchId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" | ||
| 211 | + /// } | ||
| 212 | + /// ``` | ||
| 213 | + /// | ||
| 214 | + /// 参数说明: | ||
| 215 | + /// - timerId: 计时器 Id | ||
| 216 | + /// - batchId: 打印批次 Id | ||
| 217 | + /// - printTaskId: 打印任务 Id(同批次任意任务均可) | ||
| 218 | + /// </remarks> | ||
| 219 | + /// <param name="input">查询入参</param> | ||
| 220 | + /// <returns>过期/倒计时状态(展示用)</returns> | ||
| 221 | + /// <response code="200">成功(含未找到记录的情况)</response> | ||
| 222 | + /// <response code="400">未提供任何标识</response> | ||
| 223 | + /// <response code="500">服务器错误</response> | ||
| 224 | + [Authorize] | ||
| 225 | + [HttpPost("label-alert-timer/check-expired")] | ||
| 226 | + public virtual async Task<LabelAlertTimerCheckExpiredOutputDto> CheckExpiredAsync( | ||
| 227 | + LabelAlertTimerCheckExpiredInputVo input) | ||
| 228 | + { | ||
| 229 | + if (input is null) | ||
| 230 | + { | ||
| 231 | + throw new UserFriendlyException("入参不能为空"); | ||
| 232 | + } | ||
| 233 | + | ||
| 234 | + var timerId = input.TimerId?.Trim(); | ||
| 235 | + var batchId = input.BatchId?.Trim(); | ||
| 236 | + var printTaskId = input.PrintTaskId?.Trim(); | ||
| 237 | + if (string.IsNullOrWhiteSpace(timerId) && | ||
| 238 | + string.IsNullOrWhiteSpace(batchId) && | ||
| 239 | + string.IsNullOrWhiteSpace(printTaskId)) | ||
| 240 | + { | ||
| 241 | + throw new UserFriendlyException("请至少提供 timerId、batchId 或 printTaskId 之一"); | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + var db = _dbContext.SqlSugarClient; | ||
| 245 | + FlLabelAlertTimerDbEntity? row = null; | ||
| 246 | + | ||
| 247 | + if (!string.IsNullOrWhiteSpace(timerId)) | ||
| 248 | + { | ||
| 249 | + row = (await db.Queryable<FlLabelAlertTimerDbEntity>() | ||
| 250 | + .Where(x => x.Id == timerId && !x.IsDeleted) | ||
| 251 | + .Take(1) | ||
| 252 | + .ToListAsync()) | ||
| 253 | + .FirstOrDefault(); | ||
| 254 | + } | ||
| 255 | + else if (!string.IsNullOrWhiteSpace(batchId)) | ||
| 256 | + { | ||
| 257 | + row = (await db.Queryable<FlLabelAlertTimerDbEntity>() | ||
| 258 | + .Where(x => x.BatchId == batchId && !x.IsDeleted) | ||
| 259 | + .Take(1) | ||
| 260 | + .ToListAsync()) | ||
| 261 | + .FirstOrDefault(); | ||
| 262 | + } | ||
| 263 | + else if (!string.IsNullOrWhiteSpace(printTaskId)) | ||
| 264 | + { | ||
| 265 | + var task = (await db.Queryable<FlLabelPrintTaskDbEntity>() | ||
| 266 | + .Where(x => x.Id == printTaskId) | ||
| 267 | + .Take(1) | ||
| 268 | + .ToListAsync()) | ||
| 269 | + .FirstOrDefault(); | ||
| 270 | + if (task is not null && !string.IsNullOrWhiteSpace(task.BatchId)) | ||
| 271 | + { | ||
| 272 | + row = (await db.Queryable<FlLabelAlertTimerDbEntity>() | ||
| 273 | + .Where(x => x.BatchId == task.BatchId && !x.IsDeleted) | ||
| 274 | + .Take(1) | ||
| 275 | + .ToListAsync()) | ||
| 276 | + .FirstOrDefault(); | ||
| 277 | + } | ||
| 278 | + } | ||
| 279 | + | ||
| 280 | + if (row is null) | ||
| 281 | + { | ||
| 282 | + return new LabelAlertTimerCheckExpiredOutputDto | ||
| 283 | + { | ||
| 284 | + Found = false, | ||
| 285 | + IsExpired = false, | ||
| 286 | + RemainingSeconds = 0 | ||
| 287 | + }; | ||
| 288 | + } | ||
| 289 | + | ||
| 290 | + if (!CurrentUser.Id.HasValue) | ||
| 291 | + { | ||
| 292 | + throw new UserFriendlyException("用户未登录"); | ||
| 293 | + } | ||
| 294 | + | ||
| 295 | + await UsAppPrintLogScopeHelper.EnsureUserCanAccessLocationAsync( | ||
| 296 | + CurrentUser, db, row.LocationId); | ||
| 297 | + | ||
| 298 | + var now = DateTime.Now; | ||
| 299 | + var remaining = Math.Max(0, (int)(row.ExpiresAt - now).TotalSeconds); | ||
| 300 | + var isExpired = row.ExpiresAt <= now; | ||
| 301 | + | ||
| 302 | + return new LabelAlertTimerCheckExpiredOutputDto | ||
| 303 | + { | ||
| 304 | + Found = true, | ||
| 305 | + IsExpired = isExpired, | ||
| 306 | + ExpiresAt = row.ExpiresAt, | ||
| 307 | + RemainingSeconds = remaining, | ||
| 308 | + Status = isExpired ? StatusExpired : StatusRunning, | ||
| 309 | + Title = row.Title, | ||
| 310 | + Subtitle = row.Subtitle, | ||
| 311 | + TimerId = row.Id, | ||
| 312 | + BatchId = row.BatchId | ||
| 313 | + }; | ||
| 314 | + } | ||
| 315 | + | ||
| 316 | + private async Task<PagedResultWithPageDto<LabelAlertTimerListItemDto>> QueryListByLocationAsync( | ||
| 317 | + string locationId, | ||
| 318 | + LabelAlertTimerGetListInputVo input) | ||
| 319 | + { | ||
| 320 | + await UsAppPrintLogScopeHelper.EnsureUserCanAccessLocationAsync( | ||
| 321 | + CurrentUser, _dbContext.SqlSugarClient, locationId); | ||
| 322 | + | ||
| 323 | + var db = _dbContext.SqlSugarClient; | ||
| 324 | + RefAsync<int> total = 0; | ||
| 325 | + var query = db.Queryable<FlLabelAlertTimerDbEntity>() | ||
| 326 | + .Where(x => !x.IsDeleted && x.LocationId == locationId); | ||
| 327 | + | ||
| 328 | + var (dayStart, dayEndExcl) = ResolveDateDayFilter(input.DateDay); | ||
| 329 | + if (dayStart.HasValue && dayEndExcl.HasValue) | ||
| 330 | + { | ||
| 331 | + var start = dayStart.Value; | ||
| 332 | + var endExcl = dayEndExcl.Value; | ||
| 333 | + query = query.Where(x => x.PrintedAt >= start && x.PrintedAt < endExcl); | ||
| 334 | + } | ||
| 335 | + | ||
| 336 | + var pageRows = await query | ||
| 337 | + .OrderBy(x => x.ExpiresAt, OrderByType.Desc) | ||
| 338 | + .OrderBy(x => x.PrintedAt, OrderByType.Desc) | ||
| 339 | + .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); | ||
| 340 | + | ||
| 341 | + var now = DateTime.Now; | ||
| 342 | + var items = pageRows.Select(x => MapListItem(x, now)).ToList(); | ||
| 343 | + | ||
| 344 | + var pageSize = input.MaxResultCount <= 0 ? items.Count : input.MaxResultCount; | ||
| 345 | + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); | ||
| 346 | + var totalCount = (long)total; | ||
| 347 | + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(totalCount / (double)pageSize); | ||
| 348 | + | ||
| 349 | + return new PagedResultWithPageDto<LabelAlertTimerListItemDto> | ||
| 350 | + { | ||
| 351 | + PageIndex = pageIndex, | ||
| 352 | + PageSize = pageSize, | ||
| 353 | + TotalCount = totalCount, | ||
| 354 | + TotalPages = totalPages, | ||
| 355 | + Items = items | ||
| 356 | + }; | ||
| 357 | + } | ||
| 358 | + | ||
| 359 | + private static LabelAlertTimerListItemDto MapListItem(FlLabelAlertTimerDbEntity row, DateTime now) | ||
| 360 | + { | ||
| 361 | + var remaining = Math.Max(0, (int)(row.ExpiresAt - now).TotalSeconds); | ||
| 362 | + var isExpired = row.ExpiresAt <= now; | ||
| 363 | + | ||
| 364 | + return new LabelAlertTimerListItemDto | ||
| 365 | + { | ||
| 366 | + Id = row.Id, | ||
| 367 | + BatchId = row.BatchId, | ||
| 368 | + PrintTaskId = row.PrintTaskId, | ||
| 369 | + LabelId = row.LabelId, | ||
| 370 | + LabelCode = row.LabelCode, | ||
| 371 | + Title = row.Title, | ||
| 372 | + Subtitle = row.Subtitle, | ||
| 373 | + TotalTime = row.DurationSeconds, | ||
| 374 | + RemainingTime = remaining, | ||
| 375 | + Status = isExpired ? StatusExpired : StatusRunning, | ||
| 376 | + ExpiresAt = row.ExpiresAt, | ||
| 377 | + PrintedAt = row.PrintedAt, | ||
| 378 | + LocationId = row.LocationId, | ||
| 379 | + ProductName = row.ProductName | ||
| 380 | + }; | ||
| 381 | + } | ||
| 382 | + | ||
| 383 | + private static (DateTime? DayStart, DateTime? DayEndExcl) ResolveDateDayFilter(string? dateDay) | ||
| 384 | + { | ||
| 385 | + if (string.IsNullOrWhiteSpace(dateDay)) | ||
| 386 | + { | ||
| 387 | + return (null, null); | ||
| 388 | + } | ||
| 389 | + | ||
| 390 | + if (DateTime.TryParse(dateDay.Trim(), out var parsedDay)) | ||
| 391 | + { | ||
| 392 | + var day = parsedDay.Date; | ||
| 393 | + return (day, day.AddDays(1)); | ||
| 394 | + } | ||
| 395 | + | ||
| 396 | + return (null, null); | ||
| 397 | + } | ||
| 398 | +} |