diff --git a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs index 08e7440..87581eb 100644 --- a/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs +++ b/泰额版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs @@ -1,3 +1,5 @@ +using System.Text.Json; + namespace FoodLabeling.Application.Helpers; /// @@ -45,11 +47,37 @@ public static class RbacAccessPermissionHelper } /// - /// 解析 accessPermissions 入参(英文逗号分隔,忽略大小写)。 + /// 解析 accessPermissions 入参:支持 JSON 数组字符串(如 ["manage_labels"])、英文逗号分隔。 /// public static List ParseAccessPermissionCodes(string accessPermissions) { - return accessPermissions + var raw = accessPermissions?.Trim(); + if (string.IsNullOrWhiteSpace(raw)) + { + return new List(); + } + + if (raw.StartsWith('[')) + { + try + { + var fromJson = JsonSerializer.Deserialize>(raw); + if (fromJson is { Count: > 0 }) + { + return fromJson + .Where(c => !string.IsNullOrWhiteSpace(c)) + .Select(c => c.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + } + catch + { + // fall through to delimiter split + } + } + + return raw .Split(new[] { ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Where(c => !string.IsNullOrWhiteSpace(c)) .Select(c => c.Trim()) diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardRecentLabelItemDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardRecentLabelItemDto.cs index a3a6de6..54d1856 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardRecentLabelItemDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardRecentLabelItemDto.cs @@ -8,7 +8,9 @@ public class DashboardRecentLabelItemDto /// 打印任务 Id(fl_label_print_task.Id) public string TaskId { get; set; } = string.Empty; - /// 标签编码(界面 Serial / Label ID,如 1-251201) + /// + /// 展示用 Label ID:门店当日打印序号 yyyyMMdd-n(与 preview / print-log 一致;非 fl_label.LabelCode) + /// public string LabelCode { get; set; } = string.Empty; /// 展示名称:优先产品名,否则标签名称 diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateGetListInputVo.cs index 731e532..796ff67 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateGetListInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateGetListInputVo.cs @@ -13,7 +13,12 @@ public class LabelTemplateGetListInputVo : PagedAndSortedResultRequestDto public string? Keyword { get; set; } /// - /// 按 Region 筛选(fl_group.Id):返回适用于该 Region 下任一门门店的模板,以及 appliedLocation=ALL 的模板 + /// 按 Company 筛选(fl_partner.Id);与 按「门店优先」解析 + /// + public string? PartnerId { get; set; } + + /// + /// 按 Region 筛选(fl_group.Id):返回适用于该 Region 下任一门门店的模板,以及全范围模板 /// public string? GroupId { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleCreateInputVo.cs index 41d380a..41bf2f0 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacRole/RbacRoleCreateInputVo.cs @@ -29,7 +29,7 @@ public class RbacRoleCreateInputVo public List? MenuIds { get; set; } /// - /// 按 PermissionCode 绑定菜单(英文逗号分隔);传空字符串表示清空绑定;不传则不修改已有绑定(仅编辑时) + /// 按 PermissionCode 绑定菜单:JSON 数组字符串(如 ["manage_labels"])、英文逗号分隔;传空字符串表示清空绑定;不传则不修改已有绑定(仅编辑时) /// public string? AccessPermissions { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs index b443b71..646bc84 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberCreateInputVo.cs @@ -28,7 +28,7 @@ public class TeamMemberCreateInputVo public List? PartnerIds { get; set; } /// - /// 适用 Region 多选(fl_group.Id);与 合并 + /// 适用 Region 多选(fl_group.Id);Company Admin 仅传 Company 时可省略,后端自动绑定该公司下全部 Region 与门店 /// public List? RegionIds { get; set; } @@ -38,7 +38,7 @@ public class TeamMemberCreateInputVo public List? GroupIds { get; set; } /// - /// 适用门店多选(location.Id);与 Company/Region 合并后写入 userlocation + /// 适用门店多选(location.Id);Company Admin 仅传 Company 时可省略,与 Region 合并后写入 userlocation /// public List? LocationIds { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs index e28a30a..29169e3 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListOutputDto.cs @@ -27,6 +27,9 @@ public class TeamMemberGetListOutputDto /// 适用 Region Id 列表(多选) public List RegionIds { get; set; } = new(); + /// 已绑定门店 Id 列表(location.Id + public List LocationIds { get; set; } = new(); + public List AssignedLocations { get; set; } = new(); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs index 98299e9..163b529 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberUpdateInputVo.cs @@ -28,7 +28,7 @@ public class TeamMemberUpdateInputVo public List? PartnerIds { get; set; } /// - /// 适用 Region 多选(fl_group.Id);与 合并 + /// 适用 Region 多选(fl_group.Id);Company Admin 仅传 Company 时可省略 /// public List? RegionIds { get; set; } @@ -38,7 +38,7 @@ public class TeamMemberUpdateInputVo public List? GroupIds { get; set; } /// - /// 适用门店多选(location.Id);与 Company/Region 合并后写入 userlocation + /// 适用门店多选(location.Id);Company Admin 仅传 Company 时可省略 /// public List? LocationIds { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeCompanyOptionDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeCompanyOptionDto.cs deleted file mode 100644 index 0ad3416..0000000 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeCompanyOptionDto.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; - -/// auth-scope / us-app-auth 公司下拉项 -public class AuthScopeCompanyOptionDto -{ - public string Id { get; set; } = string.Empty; - - public string PartnerName { get; set; } = string.Empty; - - public bool State { get; set; } = true; -} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeLocationOptionDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeLocationOptionDto.cs deleted file mode 100644 index 878d9cf..0000000 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeLocationOptionDto.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; - -/// auth-scope / us-app-auth 门店下拉项 -public class AuthScopeLocationOptionDto -{ - public string Id { get; set; } = string.Empty; - - public string LocationCode { get; set; } = string.Empty; - - public string LocationName { get; set; } = string.Empty; - - public string FullAddress { get; set; } = string.Empty; - - public bool State { get; set; } = true; - - public string PartnerId { get; set; } = string.Empty; - - public string GroupId { get; set; } = string.Empty; - - public string GroupName { get; set; } = string.Empty; -} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeRegionOptionDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeRegionOptionDto.cs deleted file mode 100644 index dac6d8e..0000000 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeRegionOptionDto.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; - -/// auth-scope / us-app-auth Region 下拉项 -public class AuthScopeRegionOptionDto -{ - public string Id { get; set; } = string.Empty; - - public string GroupName { get; set; } = string.Empty; - - public string PartnerId { get; set; } = string.Empty; - - public bool State { get; set; } = true; -} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeSelectLocationOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeSelectLocationOutputDto.cs deleted file mode 100644 index cc38312..0000000 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/AuthScopeSelectLocationOutputDto.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth; - -/// 确认选店返回 -public class AuthScopeSelectLocationOutputDto -{ - public string PartnerId { get; set; } = string.Empty; - - public string PartnerName { get; set; } = string.Empty; - - public string GroupId { get; set; } = string.Empty; - - public string GroupName { get; set; } = string.Empty; - - public UsAppBoundLocationDto Location { get; set; } = new(); -} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogGetListInputVo.cs index f819a65..20fe018 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogGetListInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogGetListInputVo.cs @@ -11,5 +11,15 @@ public class PrintLogGetListInputVo : PagedAndSortedResultRequestDto /// 当前门店 Id(location.Id,Guid 字符串) /// public string LocationId { get; set; } = string.Empty; -} + /// + /// 打印日期(自然日);按 PrintedAt ?? CreationTime 筛选该日记录。 + /// 未传且未传 时不按日过滤(返回该门店全部打印记录,分页)。 + /// + public DateTime? PrintDate { get; set; } + + /// + /// 打印日期字符串(yyyy-MM-dd);优先于 ,避免 JSON 仅日期时的 UTC 歧义。 + /// + public string? PrintDateDay { get; set; } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs index c55e7d3..e4ca62f 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs @@ -14,6 +14,7 @@ public interface IDashboardAppService : IApplicationService /// /// 按 Token 数据范围统计:系统管理员为全平台;其它账号仅统计其 userlocation 绑定门店所属公司范围内数据。 /// activeUsers / people 仅统计有门店绑定的成员,且不包含内置系统 admin(用户名 admin 或角色码 admin)。 + /// recentLabels[].labelCode 为门店当日打印序号 yyyyMMdd-n,规则与 POST /api/app/us-app-labeling/preview、print-log 一致。 /// Task GetOverviewAsync(); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelTemplateAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelTemplateAppService.cs index 3938824..8b015d3 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelTemplateAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelTemplateAppService.cs @@ -11,8 +11,9 @@ namespace FoodLabeling.Application.Contracts.IServices; public interface ILabelTemplateAppService : IApplicationService { /// - /// 标签模板分页列表;Query 支持 groupId(Region)、locationId(门店)筛选; - /// 出参含 region/locationitems/itemNames(模板控件名称,逗号拼接)。 + /// 标签模板分页列表;Query 支持 partnerId(Company)、groupId(Region)、locationId(门店)筛选; + /// 出参含 company/region/locationitems/itemNames(模板控件名称,逗号拼接)。 + /// SkipCount 为页码(从 1 起,第一页传 1)。 /// Task> GetListAsync(LabelTemplateGetListInputVo input); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs index 3de8e70..e49108e 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs @@ -92,16 +92,4 @@ public interface IUsAppAuthAppService : IApplicationService /// Invalid code or password policy. /// Server error Task PostResetPasswordByEmailAsync(RetrievePasswordByEmailDto input); - - /// App 管理员:可选公司列表 - Task> GetAdminScopeCompaniesAsync(); - - /// App 管理员:指定公司下 Region - Task> GetAdminScopeRegionsAsync(string partnerId); - - /// App 管理员:指定公司与 Region 下门店 - Task> GetAdminScopeLocationsAsync(string partnerId, string groupId); - - /// App 管理员:确认当前工作门店 - Task SelectAdminScopeLocationAsync(UsAppSelectAdminScopeLocationInputVo input); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs index 3a07f83..7c86d90 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs @@ -33,7 +33,7 @@ public interface IUsAppLabelingAppService : IApplicationService /// /// App 打印日志:当前门店打印记录(分页)。管理员 / Partner 角色可见门店内全部;其它角色仅本人。 - /// 出参 labelId 为门店当日打印序号(yyyyMMdd-n);labelEntityIdfl_label.IdlabelCode 为标签编码。 + /// 支持 printDate 按自然日筛选;出参 labelId 为门店当日打印序号(yyyyMMdd-n)。 /// Task> GetPrintLogListAsync(PrintLogGetListInputVo input); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateListItemsHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateListItemsHelper.cs index c637bc0..ec0cf8c 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateListItemsHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateListItemsHelper.cs @@ -29,7 +29,8 @@ public static class LabelTemplateListItemsHelper ElementName = x.ElementName, TypeAdd = x.TypeAdd, ElementType = x.ElementType, - OrderNum = x.OrderNum + OrderNum = x.OrderNum, + ElementKey = x.ElementKey }) .ToListAsync(); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateQueryHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateQueryHelper.cs new file mode 100644 index 0000000..1acd3ea --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateQueryHelper.cs @@ -0,0 +1,37 @@ +using FoodLabeling.Application.Services.DbModels; +using SqlSugar; + +namespace FoodLabeling.Application.Helpers; + +/// +/// 标签模板查询列投影:仅映射库内真实列,避免 ORM 元数据缓存或旧实体仍 SELECT 不存在的 scope 列。 +/// +public static class LabelTemplateQueryHelper +{ + /// + /// 列表/详情/引用校验等只读场景:强制 SQL 不含 AppliedPartnerType / AppliedRegionType。 + /// + public static ISugarQueryable ProjectListColumns( + ISugarQueryable query) => + query.Select(x => new FlLabelTemplateDbEntity + { + Id = x.Id, + IsDeleted = x.IsDeleted, + CreationTime = x.CreationTime, + CreatorId = x.CreatorId, + LastModifierId = x.LastModifierId, + LastModificationTime = x.LastModificationTime, + ConcurrencyStamp = x.ConcurrencyStamp, + TemplateCode = x.TemplateCode, + TemplateName = x.TemplateName, + LabelType = x.LabelType, + Unit = x.Unit, + Width = x.Width, + Height = x.Height, + AppliedLocationType = x.AppliedLocationType, + ShowRuler = x.ShowRuler, + ShowGrid = x.ShowGrid, + VersionNo = x.VersionNo, + State = x.State + }); +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeHelper.cs index 85cdb40..30532ec 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeHelper.cs @@ -150,17 +150,24 @@ public static class LabelTemplateScopeHelper string? currentUserId, DateTime now) { - await db.Deleteable() - .Where(x => x.TemplateId == templateId) - .ExecuteCommandAsync(); - await db.Deleteable() - .Where(x => x.TemplateId == templateId) - .ExecuteCommandAsync(); + var hasExtendedScope = await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(db); + + if (hasExtendedScope) + { + await db.Deleteable() + .Where(x => x.TemplateId == templateId) + .ExecuteCommandAsync(); + await db.Deleteable() + .Where(x => x.TemplateId == templateId) + .ExecuteCommandAsync(); + } + await db.Deleteable() .Where(x => x.TemplateId == templateId) .ExecuteCommandAsync(); - if (string.Equals(scope.AppliedPartnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) + if (hasExtendedScope + && string.Equals(scope.AppliedPartnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) && scope.PartnerIds.Count > 0) { var rows = scope.PartnerIds.Select(pid => new FlLabelTemplatePartnerDbEntity @@ -174,7 +181,8 @@ public static class LabelTemplateScopeHelper await db.Insertable(rows).ExecuteCommandAsync(); } - if (string.Equals(scope.AppliedRegionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) + if (hasExtendedScope + && string.Equals(scope.AppliedRegionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) && scope.RegionIds.Count > 0) { var rows = scope.RegionIds.Select(gid => new FlLabelTemplateRegionDbEntity @@ -216,12 +224,18 @@ public static class LabelTemplateScopeHelper var templateIds = templates.Select(x => x.Id).Distinct(StringComparer.Ordinal).ToList(); - var partnerLinks = await db.Queryable() - .Where(x => templateIds.Contains(x.TemplateId)) - .ToListAsync(); - var regionLinks = await db.Queryable() - .Where(x => templateIds.Contains(x.TemplateId)) - .ToListAsync(); + var hasExtendedScope = await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(db); + + var partnerLinks = hasExtendedScope + ? await db.Queryable() + .Where(x => templateIds.Contains(x.TemplateId)) + .ToListAsync() + : new List(); + var regionLinks = hasExtendedScope + ? await db.Queryable() + .Where(x => templateIds.Contains(x.TemplateId)) + .ToListAsync() + : new List(); var locationLinks = await db.Queryable() .Where(x => templateIds.Contains(x.TemplateId)) .ToListAsync(); @@ -274,8 +288,6 @@ public static class LabelTemplateScopeHelper foreach (var template in templates) { - var partnerType = NormalizeScopeType(template.AppliedPartnerType, ScopeAll); - var regionType = NormalizeScopeType(template.AppliedRegionType, ScopeAll); var locationType = NormalizeScopeType(template.AppliedLocationType, ScopeAll); var pIds = LocationScopeBindingHelper.NormalizeIds( @@ -285,34 +297,39 @@ public static class LabelTemplateScopeHelper var lIds = LocationScopeBindingHelper.NormalizeIds( locationLinks.Where(x => x.TemplateId == template.Id).Select(x => x.LocationId).ToList()); - if (pIds.Count == 0 - && string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) + var partnerType = pIds.Count > 0 ? ScopeSpecified : ScopeAll; + var regionType = rIds.Count > 0 ? ScopeSpecified : ScopeAll; + + if (hasExtendedScope + && pIds.Count == 0 && lIds.Count > 0) { pIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, lIds); + if (pIds.Count > 0) + { + partnerType = ScopeSpecified; + } } - if (rIds.Count == 0 - && string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) + if (hasExtendedScope + && rIds.Count == 0 && lIds.Count > 0) { rIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(db, lIds); - } - - if (pIds.Count == 0 && !string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)) - { - partnerType = ScopeAll; - } - - if (rIds.Count == 0 && !string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)) - { - regionType = ScopeAll; + if (rIds.Count > 0) + { + regionType = ScopeSpecified; + } } if (lIds.Count == 0 && !string.Equals(locationType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)) { locationType = ScopeAll; } + else if (lIds.Count > 0) + { + locationType = ScopeSpecified; + } var companyDisplay = string.Equals(partnerType, ScopeAll, StringComparison.OrdinalIgnoreCase) ? AllCompaniesDisplay @@ -354,12 +371,36 @@ public static class LabelTemplateScopeHelper return query; } + var hasExtendedScope = await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(db); + if (scopedLocationIds.Count == 0) { + return hasExtendedScope + ? query.Where(t => + t.AppliedLocationType == ScopeAll + && !SqlFunc.Subqueryable() + .Where(l => l.TemplateId == t.Id) + .Any() + && !SqlFunc.Subqueryable() + .Where(p => p.TemplateId == t.Id) + .Any() + && !SqlFunc.Subqueryable() + .Where(r => r.TemplateId == t.Id) + .Any()) + : query.Where(t => + t.AppliedLocationType == ScopeAll + && !SqlFunc.Subqueryable() + .Where(l => l.TemplateId == t.Id) + .Any()); + } + + if (!hasExtendedScope) + { return query.Where(t => - t.AppliedPartnerType == ScopeAll - && t.AppliedRegionType == ScopeAll - && t.AppliedLocationType == ScopeAll); + t.AppliedLocationType == ScopeAll + || SqlFunc.Subqueryable() + .Where(l => l.TemplateId == t.Id && scopedLocationIds.Contains(l.LocationId)) + .Any()); } var scopedPartnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( @@ -368,13 +409,28 @@ public static class LabelTemplateScopeHelper db, scopedLocationIds); return query.Where(t => - (t.AppliedPartnerType == ScopeAll && t.AppliedRegionType == ScopeAll && t.AppliedLocationType == ScopeAll) + ( + t.AppliedLocationType == ScopeAll + && !SqlFunc.Subqueryable() + .Where(l => l.TemplateId == t.Id) + .Any() + && !SqlFunc.Subqueryable() + .Where(p => p.TemplateId == t.Id) + .Any() + && !SqlFunc.Subqueryable() + .Where(r => r.TemplateId == t.Id) + .Any() + ) || ( - (t.AppliedPartnerType == ScopeAll + (!SqlFunc.Subqueryable() + .Where(p => p.TemplateId == t.Id) + .Any() || SqlFunc.Subqueryable() .Where(p => p.TemplateId == t.Id && scopedPartnerIds.Contains(p.PartnerId)) .Any()) - && (t.AppliedRegionType == ScopeAll + && (!SqlFunc.Subqueryable() + .Where(r => r.TemplateId == t.Id) + .Any() || SqlFunc.Subqueryable() .Where(r => r.TemplateId == t.Id && scopedGroupIds.Contains(r.GroupId)) .Any()) diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeSchemaHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeSchemaHelper.cs new file mode 100644 index 0000000..22c2998 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelTemplateScopeSchemaHelper.cs @@ -0,0 +1,156 @@ +using SqlSugar; + +namespace FoodLabeling.Application.Helpers; + +/// +/// 探测标签模板 Company/Region scope 列与关联表是否已迁移(fl_label_template_scope.sql)。 +/// 未迁移时 ORM 实体不含 AppliedPartnerType/AppliedRegionType,读写走 raw SQL 或关联表推断。 +/// +public static class LabelTemplateScopeSchemaHelper +{ + private static LabelTemplateScopeSchemaStatus? _cached; + + public sealed class LabelTemplateScopeSchemaStatus + { + public bool HasAppliedPartnerTypeColumn { get; init; } + + public bool HasAppliedRegionTypeColumn { get; init; } + + public bool HasPartnerScopeTable { get; init; } + + public bool HasRegionScopeTable { get; init; } + + /// partner/region 关联表均已创建(用于多选明细)。 + public bool HasPartnerRegionScopeTables => HasPartnerScopeTable && HasRegionScopeTable; + + /// 列 + 关联表均已迁移。 + public bool IsFullSchema => + HasAppliedPartnerTypeColumn + && HasAppliedRegionTypeColumn + && HasPartnerRegionScopeTables; + } + + public static async Task GetStatusAsync(ISqlSugarClient db) + { + if (_cached is not null) + { + return _cached; + } + + try + { + var partnerColCount = await db.Ado.GetIntAsync( + """ + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'fl_label_template' + AND COLUMN_NAME = 'AppliedPartnerType' + """); + + var regionColCount = await db.Ado.GetIntAsync( + """ + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'fl_label_template' + AND COLUMN_NAME = 'AppliedRegionType' + """); + + var partnerTableCount = await db.Ado.GetIntAsync( + """ + SELECT COUNT(*) + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'fl_label_template_partner' + """); + + var regionTableCount = await db.Ado.GetIntAsync( + """ + SELECT COUNT(*) + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'fl_label_template_region' + """); + + _cached = new LabelTemplateScopeSchemaStatus + { + HasAppliedPartnerTypeColumn = partnerColCount > 0, + HasAppliedRegionTypeColumn = regionColCount > 0, + HasPartnerScopeTable = partnerTableCount > 0, + HasRegionScopeTable = regionTableCount > 0 + }; + } + catch + { + _cached = new LabelTemplateScopeSchemaStatus(); + } + + return _cached; + } + + /// 单元测试或切换库后调用。 + public static void ResetCacheForTests() => _cached = null; + + /// + /// 是否已创建 fl_label_template_partnerfl_label_template_region。 + /// + public static async Task HasPartnerRegionScopeTablesAsync(ISqlSugarClient db) + { + var status = await GetStatusAsync(db); + return status.HasPartnerRegionScopeTables; + } + + /// + /// 已迁移 scope 列时,写入 AppliedPartnerType / AppliedRegionType(未迁移则 no-op)。 + /// + public static async Task SetAppliedScopeTypesAsync( + ISqlSugarClient db, + string templateId, + string appliedPartnerType, + string appliedRegionType) + { + if (string.IsNullOrWhiteSpace(templateId)) + { + return; + } + + var status = await GetStatusAsync(db); + if (!status.HasAppliedPartnerTypeColumn && !status.HasAppliedRegionTypeColumn) + { + return; + } + + if (status.HasAppliedPartnerTypeColumn && status.HasAppliedRegionTypeColumn) + { + await db.Ado.ExecuteCommandAsync( + """ + UPDATE fl_label_template + SET AppliedPartnerType = @partnerType, + AppliedRegionType = @regionType + WHERE Id = @id + """, + new + { + partnerType = appliedPartnerType.Trim(), + regionType = appliedRegionType.Trim(), + id = templateId.Trim() + }); + return; + } + + if (status.HasAppliedPartnerTypeColumn) + { + await db.Ado.ExecuteCommandAsync( + "UPDATE fl_label_template SET AppliedPartnerType = @type WHERE Id = @id", + new { type = appliedPartnerType.Trim(), id = templateId.Trim() }); + } + + if (status.HasAppliedRegionTypeColumn) + { + await db.Ado.ExecuteCommandAsync( + "UPDATE fl_label_template SET AppliedRegionType = @type WHERE Id = @id", + new { type = appliedRegionType.Trim(), id = templateId.Trim() }); + } + } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs index abb1491..76d9b60 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs @@ -2,6 +2,8 @@ using FoodLabeling.Application.Services.DbModels; using FoodLabeling.Domain.Entities; using SqlSugar; using Volo.Abp; +using Volo.Abp.Users; +using Yi.Framework.Rbac.Domain.Entities; using Yi.Framework.SqlSugarCore.Abstractions; namespace FoodLabeling.Application.Helpers; @@ -12,6 +14,87 @@ namespace FoodLabeling.Application.Helpers; public static class LocationScopeBindingHelper { /// + /// 读取用户 userlocation 绑定的门店 Id(Guid 归一化,避免大小写导致漏绑)。 + /// + public static async Task> ResolveBoundLocationIdsForUserAsync( + ISqlSugarClient db, + Guid userId) + { + var userKey = TeamMemberListScopeHelper.UserKey(userId); + var links = await db.Queryable() + .Where(x => !x.IsDeleted) + .ToListAsync(); + + var locationIds = new HashSet(StringComparer.Ordinal); + foreach (var link in links) + { + if (TeamMemberListScopeHelper.NormalizeScopeKey(link.UserId) != userKey) + { + continue; + } + + var locKey = TeamMemberListScopeHelper.NormalizeScopeKey(link.LocationId); + if (string.IsNullOrEmpty(locKey)) + { + continue; + } + + locationIds.Add(locKey); + } + + return locationIds.OrderBy(x => x, StringComparer.Ordinal).ToList(); + } + + /// + /// 与 Team Member 详情/编辑回显一致:Company Admin 展示绑定 Company 下全部 Region,其它角色按门店反推 Region。 + /// + public static async Task> ResolveDisplayRegionIdsForUserAsync( + ISqlSugarClient db, + Guid userId, + Guid? roleId) + { + var locationIds = await ResolveBoundLocationIdsForUserAsync(db, userId); + if (locationIds.Count == 0) + { + return new List(); + } + + var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds); + var regionIds = await ResolveGroupIdsFromLocationIdsAsync(db, locationIds); + + if (await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(db, roleId) && partnerIds.Count > 0) + { + var allRegions = await ResolveGroupIdsFromPartnerIdsAsync(db, partnerIds); + if (allRegions.Count > 0) + { + regionIds = allRegions; + } + } + + return regionIds; + } + + /// + /// 当前登录用户可见 Region Id(fl_group.Id),规则同 。 + /// + public static async Task> ResolveDisplayRegionIdsForCurrentUserAsync( + ISqlSugarClient db, + ICurrentUser currentUser) + { + if (currentUser.Id is null) + { + return new List(); + } + + var roleId = await db.Queryable() + .Where(ur => ur.UserId == currentUser.Id.Value) + .Select(ur => (Guid?)ur.RoleId) + .FirstAsync(); + + return await ResolveDisplayRegionIdsForUserAsync(db, currentUser.Id.Value, roleId); + } + + /// /// 列表筛选:门店 Id 优先,否则 Region(groupId),否则 Company(partnerId);均未传返回 null。 /// public static async Task?> ResolveFilteredLocationIdsForListAsync( @@ -55,7 +138,12 @@ public static class LocationScopeBindingHelper var gName = g.GroupName?.Trim() ?? string.Empty; var partner = await db.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == g.PartnerId); - var pName = partner?.PartnerName?.Trim() ?? string.Empty; + if (partner is null) + { + return new List(); + } + + var pName = partner.PartnerName?.Trim() ?? string.Empty; q = q.Where(x => x.GroupName == gName && x.Partner == pName); } else if (!string.IsNullOrWhiteSpace(pid)) @@ -358,6 +446,35 @@ public static class LocationScopeBindingHelper } /// + /// 解析 Company(fl_partner.Id)下全部 Region Id(fl_group.Id)。 + /// + public static async Task> ResolveGroupIdsFromPartnerIdsAsync( + ISqlSugarClient db, + IReadOnlyList? partnerIds) + { + var ids = NormalizeIds(partnerIds); + if (ids.Count == 0) + { + return new List(); + } + + var result = new HashSet(StringComparer.Ordinal); + foreach (var pid in ids) + { + var groupIds = await db.Queryable() + .Where(x => !x.IsDeleted && x.PartnerId == pid) + .Select(x => x.Id) + .ToListAsync(); + foreach (var gid in groupIds.Where(x => !string.IsNullOrWhiteSpace(x))) + { + result.Add(gid.Trim()); + } + } + + return result.OrderBy(x => x, StringComparer.Ordinal).ToList(); + } + + /// /// 校验合并后的门店 Id 均存在。 /// public static async Task ValidateLocationIdsExistAsync(ISqlSugarClient db, IReadOnlyList locationIds) diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/PartnerScopeHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/PartnerScopeHelper.cs index 67d04a9..32cb96f 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/PartnerScopeHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/PartnerScopeHelper.cs @@ -39,37 +39,16 @@ public static class PartnerScopeHelper return new PartnerScopeFilter(); } - var userId = currentUser.Id.Value.ToString(); - var locationIds = await dbContext.SqlSugarClient.Queryable() - .Where(x => !x.IsDeleted && x.UserId == userId) - .Select(x => x.LocationId) - .Distinct() - .ToListAsync(); - + var db = dbContext.SqlSugarClient; + var locationIds = await LocationScopeBindingHelper.ResolveBoundLocationIdsForUserAsync( + db, currentUser.Id.Value); if (locationIds.Count == 0) { return new PartnerScopeFilter(); } - var partnerKeys = await dbContext.SqlSugarClient.Queryable() - .Where(x => !x.IsDeleted && locationIds.Contains(x.Id.ToString())) - .Where(x => x.Partner != null && x.Partner != string.Empty) - .Select(x => x.Partner!) - .Distinct() - .ToListAsync(); - - if (partnerKeys.Count == 0) - { - return new PartnerScopeFilter(); - } - - var allowedIds = await dbContext.SqlSugarClient.Queryable() - .Where(x => !x.IsDeleted) - .Where(x => partnerKeys.Contains(x.PartnerName) || partnerKeys.Contains(x.Id)) - .Select(x => x.Id) - .Distinct() - .ToListAsync(); - + var allowedIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( + db, locationIds); return new PartnerScopeFilter { AllowedPartnerIds = allowedIds }; } @@ -95,7 +74,8 @@ public static class PartnerScopeHelper } /// - /// 组织(fl_group)数据范围:管理员可见全部;非管理员仅可见其 userlocation 绑定门店对应的 Region(公司 + 组织名与 location 一致)。 + /// 组织(fl_group)数据范围:管理员可见全部;Company Admin 可见绑定公司下全部 Region(与 Team Member 详情 regionIds 一致); + /// 其它非管理员仅可见其绑定门店反推的 Region。 /// public sealed class GroupScopeFilter { @@ -105,7 +85,7 @@ public static class PartnerScopeHelper } /// - /// 解析当前登录用户可查询的组织 Id 范围(与 Reports 按 location.Partner + location.GroupName 对齐)。 + /// 解析当前登录用户可查询的组织 Id 范围(与 Team Member 编辑回显 regionIds/groupIds 对齐)。 /// public static async Task ResolveGroupScopeAsync( ICurrentUser currentUser, @@ -121,62 +101,10 @@ public static class PartnerScopeHelper return new GroupScopeFilter(); } - var userId = currentUser.Id.Value.ToString(); - var locationRows = await dbContext.SqlSugarClient.Queryable() - .InnerJoin((ul, loc) => - !loc.IsDeleted && ul.LocationId == loc.Id.ToString()) - .Where(ul => !ul.IsDeleted && ul.UserId == userId) - .Select((ul, loc) => new { loc.Partner, loc.GroupName }) - .ToListAsync(); - - if (locationRows.Count == 0) - { - return new GroupScopeFilter(); - } + var regionIds = await LocationScopeBindingHelper.ResolveDisplayRegionIdsForCurrentUserAsync( + dbContext.SqlSugarClient, currentUser); - var pairs = locationRows - .Where(x => !string.IsNullOrWhiteSpace(x.GroupName)) - .Select(x => (Partner: (x.Partner ?? string.Empty).Trim(), GroupName: x.GroupName!.Trim())) - .Where(x => !string.IsNullOrEmpty(x.GroupName) && !string.IsNullOrEmpty(x.Partner)) - .Distinct() - .ToList(); - - if (pairs.Count == 0) - { - return new GroupScopeFilter(); - } - - var partnerKeys = pairs.Select(x => x.Partner).Distinct().ToList(); - var partners = await dbContext.SqlSugarClient.Queryable() - .Where(x => !x.IsDeleted) - .Where(x => partnerKeys.Contains(x.PartnerName) || partnerKeys.Contains(x.Id)) - .Select(x => new { x.Id, x.PartnerName }) - .ToListAsync(); - - var allowedGroupIds = new HashSet(StringComparer.Ordinal); - foreach (var pair in pairs) - { - var partnerId = partners - .FirstOrDefault(p => - string.Equals(p.PartnerName, pair.Partner, StringComparison.Ordinal) || - string.Equals(p.Id, pair.Partner, StringComparison.Ordinal)) - ?.Id; - if (string.IsNullOrEmpty(partnerId)) - { - continue; - } - - var ids = await dbContext.SqlSugarClient.Queryable() - .Where(g => !g.IsDeleted && g.PartnerId == partnerId && g.GroupName == pair.GroupName) - .Select(g => g.Id) - .ToListAsync(); - foreach (var id in ids) - { - allowedGroupIds.Add(id); - } - } - - return new GroupScopeFilter { AllowedGroupIds = allowedGroupIds.ToList() }; + return new GroupScopeFilter { AllowedGroupIds = regionIds }; } /// @@ -191,12 +119,13 @@ public static class PartnerScopeHelper return query; } - var ids = scope.AllowedGroupIds; - if (ids.Count == 0) + var allowedGuids = TeamMemberListScopeHelper.ParseGuidHashSet(scope.AllowedGroupIds); + if (allowedGuids.Count == 0) { return query.Where((g, p) => false); } - return query.Where((g, p) => ids.Contains(g.Id)); + var guidList = allowedGuids.ToList(); + return query.Where((g, p) => guidList.Contains(SqlFunc.ToGuid(g.Id))); } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs index 08e7440..87581eb 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/RbacAccessPermissionHelper.cs @@ -1,3 +1,5 @@ +using System.Text.Json; + namespace FoodLabeling.Application.Helpers; /// @@ -45,11 +47,37 @@ public static class RbacAccessPermissionHelper } /// - /// 解析 accessPermissions 入参(英文逗号分隔,忽略大小写)。 + /// 解析 accessPermissions 入参:支持 JSON 数组字符串(如 ["manage_labels"])、英文逗号分隔。 /// public static List ParseAccessPermissionCodes(string accessPermissions) { - return accessPermissions + var raw = accessPermissions?.Trim(); + if (string.IsNullOrWhiteSpace(raw)) + { + return new List(); + } + + if (raw.StartsWith('[')) + { + try + { + var fromJson = JsonSerializer.Deserialize>(raw); + if (fromJson is { Count: > 0 }) + { + return fromJson + .Where(c => !string.IsNullOrWhiteSpace(c)) + .Select(c => c.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + } + catch + { + // fall through to delimiter split + } + } + + return raw .Split(new[] { ',', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Where(c => !string.IsNullOrWhiteSpace(c)) .Select(c => c.Trim()) diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogDailyLabelIdHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogDailyLabelIdHelper.cs index 69fd9ce..34e7f35 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogDailyLabelIdHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogDailyLabelIdHelper.cs @@ -1,3 +1,4 @@ +using System.Globalization; using FoodLabeling.Application.Services.DbModels; using SqlSugar; @@ -128,6 +129,88 @@ public static class ReportsPrintLogDailyLabelIdHelper return result; } + /// + /// App Print Log 单日筛选:解析 / 为服务器本地自然日。 + /// 两者均未传时返回 null(不按日过滤,兼容 App 未传 printDate 的历史行为)。 + /// + public static DateTime? ResolvePrintLogFilterCalendarDay(DateTime? printDate, string? printDateDay) + { + var text = printDateDay?.Trim(); + if (!string.IsNullOrWhiteSpace(text)) + { + if (DateTime.TryParseExact( + text, + "yyyy-MM-dd", + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out var parsedExact)) + { + return parsedExact.Date; + } + + if (DateTime.TryParse( + text, + CultureInfo.InvariantCulture, + DateTimeStyles.AllowWhiteSpaces, + out var parsed)) + { + return parsed.Date; + } + + throw new Volo.Abp.UserFriendlyException("printDate 格式不正确,请使用 yyyy-MM-dd"); + } + + if (!printDate.HasValue) + { + return null; + } + + var value = printDate.Value; + return value.Kind == DateTimeKind.Utc ? value.ToLocalTime().Date : value.Date; + } + + /// + /// 按自然日过滤打印任务(MySQL DATE(IFNULL(PrintedAt, CreationTime)),避免 DateTime 区间比较时区偏差)。 + /// + public static ISugarQueryable ApplyPrintTaskCalendarDayFilter( + ISugarQueryable query, + DateTime filterDay) + { + var dayText = filterDay.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + return query.Where("DATE(IFNULL(PrintedAt, CreationTime)) = @filterDay", new { filterDay = dayText }); + } + + /// + /// 多表 Join 查询按自然日过滤(首表别名 t)。 + /// + public static ISugarQueryable ApplyPrintTaskCalendarDayFilter( + ISugarQueryable query, + DateTime filterDay) + { + var dayText = filterDay.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + return query.Where("DATE(IFNULL(t.PrintedAt, t.CreationTime)) = @filterDay", new { filterDay = dayText }); + } + + /// + /// 五表 Join(首表别名 t)。 + /// + public static ISugarQueryable ApplyPrintTaskCalendarDayFilter( + ISugarQueryable query, + DateTime filterDay) + { + var dayText = filterDay.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + return query.Where("DATE(IFNULL(t.PrintedAt, t.CreationTime)) = @filterDay", new { filterDay = dayText }); + } + + /// + /// App Print Log 单日筛选:printDate 的自然日区间;未传则默认服务器当天。 + /// + public static (DateTime dayStart, DateTime dayEndExcl) ResolvePrintLogFilterDayRange(DateTime? printDate) + { + var day = ResolvePrintLogFilterCalendarDay(printDate, null) ?? DateTime.Today; + return (day, day.AddDays(1)); + } + public readonly struct PrintTaskScopeKey { public PrintTaskScopeKey(string taskId, string? locationId, DateTime printedAt) diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExcelHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExcelHelper.cs index d752596..b49cfca 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExcelHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExcelHelper.cs @@ -36,7 +36,7 @@ public static class ReportsPrintLogExcelHelper ws.Cell(r, 3).Value = x.ProductCategoryName ?? string.Empty; ws.Cell(r, 4).Value = x.LabelCategoryName ?? string.Empty; ws.Cell(r, 5).Value = x.TemplateText ?? string.Empty; - ws.Cell(r, 6).Value = x.PrintedAt ?? string.Empty; + ws.Cell(r, 6).Value = ReportsDateTimeDisplayHelper.FormatPrintedAt(x.PrintedAt); ws.Cell(r, 7).Value = x.PrintedByName ?? string.Empty; ws.Cell(r, 8).Value = x.LocationText ?? string.Empty; ws.Cell(r, 9).Value = x.ExpiryDateText ?? string.Empty; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs index 5d0ed7f..e9e1a92 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs @@ -286,4 +286,8 @@ public static class ReportsPrintLogExpiryHelper return null; } + + /// 解析并格式化为 Print Log Expiration 列展示。 + public static string ExtractFormattedExpiryText(string? printInputJson) => + ReportsDateTimeDisplayHelper.FormatExpiryDisplay(ExtractExpiryText(printInputJson)); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberBatchExcelHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberBatchExcelHelper.cs index e2911cf..5c46776 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberBatchExcelHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberBatchExcelHelper.cs @@ -9,6 +9,54 @@ namespace FoodLabeling.Application.Helpers; /// public static class TeamMemberBatchExcelHelper { + /// 导入/下载模板表头(与 PDF 导出 Region 列对齐) + public static readonly string[] ImportTemplateHeaders = + { + "Name", + "User Name", + "Password", + "Email", + "Phone", + "Role Id", + "Role Name", + "Region", + "Assigned Location Ids", + "Status" + }; + + /// 生成批量导入模板 xlsx(含 Region 可选列说明行) + public static MemoryStream BuildImportTemplateWorkbook() + { + var ms = new MemoryStream(); + using var wb = new XLWorkbook(); + var ws = wb.AddWorksheet("TeamMembers"); + for (var i = 0; i < ImportTemplateHeaders.Length; i++) + { + ws.Cell(1, i + 1).Value = ImportTemplateHeaders[i]; + ws.Cell(1, i + 1).Style.Font.Bold = true; + } + + ws.Cell(2, 1).Value = "John Doe"; + ws.Cell(2, 2).Value = "john.doe"; + ws.Cell(2, 3).Value = "ChangeMe123!"; + ws.Cell(2, 4).Value = "john@example.com"; + ws.Cell(2, 5).Value = "789654444"; + ws.Cell(2, 6).Value = ""; + ws.Cell(2, 7).Value = "Staff"; + ws.Cell(2, 8).Value = ""; + ws.Cell(2, 9).Value = "LOC001;LOC002"; + ws.Cell(2, 10).Value = "TRUE"; + + ws.Cell(3, 7).Value = "(Role Name 与系统角色名一致;Company Admin 可只填 Region 或留空由 Company 规则处理)"; + ws.Cell(4, 8).Value = "(可选)Region 名称或 fl_group.Id,多个用 ; 分隔"; + ws.Cell(5, 9).Value = "(可选)门店 LocationCode 或 location.Id(Guid),多个用 ; 分隔;与 Region 至少填一项"; + + ws.Columns().AdjustToContents(); + wb.SaveAs(ms); + ms.Position = 0; + return ms; + } + /// /// 从上传的 Excel 解析为创建入参列表(行号从 2 起为数据行)。 /// @@ -162,7 +210,10 @@ public static class TeamMemberBatchExcelHelper "password" or "pwd" or "密码" => "password", "phone" or "mobile" or "电话" or "手机" => "phone", "role" or "rolename" or "角色" => "rolename", - "assignedlocations" or "locations" or "location" or "分配门店" or "门店" => "locations", + "roleid" or "角色id" => "roleid", + "region" or "regions" or "group" or "groupname" or "groupid" or "区域" => "regions", + "assignedlocations" or "assignedlocationids" or "locationids" or "locationcodes" or + "locations" or "location" or "分配门店" or "门店" or "门店id" => "locations", "status" or "active" or "state" or "启用" => "status", _ => null }; @@ -221,7 +272,9 @@ public static class TeamMemberBatchExcelHelper var userName = GetCellByField(colMap, ws, rowNum, "username"); var password = GetCellByField(colMap, ws, rowNum, "password"); var phoneStr = GetCellByField(colMap, ws, rowNum, "phone"); + var roleIdCell = GetCellByField(colMap, ws, rowNum, "roleid"); var roleName = GetCellByField(colMap, ws, rowNum, "rolename"); + var regionsCell = GetCellByField(colMap, ws, rowNum, "regions"); var locationsCell = GetCellByField(colMap, ws, rowNum, "locations"); var statusStr = GetCellByField(colMap, ws, rowNum, "status"); @@ -264,9 +317,13 @@ public static class TeamMemberBatchExcelHelper } Guid? roleIdResolved = null; - if (string.IsNullOrWhiteSpace(roleName)) + if (Guid.TryParse(roleIdCell?.Trim(), out var roleGuid)) { - errors.Add("Role 不能为空"); + roleIdResolved = roleGuid; + } + else if (string.IsNullOrWhiteSpace(roleName)) + { + errors.Add("Role Name 不能为空(或填写有效的 Role Id Guid)"); } else if (!roleNameToId.TryGetValue(NormalizeRoleKey(roleName), out var rid)) { @@ -277,10 +334,13 @@ public static class TeamMemberBatchExcelHelper roleIdResolved = rid; } - var locationTokens = SplitLocationTokens(locationsCell); - if (locationTokens.Count == 0) + var regionTokens = SplitMultiValueTokens(regionsCell); + var locationTokens = SplitMultiValueTokens(locationsCell); + var isCompanyAdmin = TeamMemberRoleHelper.IsCompanyAdminRoleName(roleName); + + if (regionTokens.Count == 0 && locationTokens.Count == 0 && !isCompanyAdmin) { - errors.Add("Assigned Locations 不能为空(多个门店可用分号、竖线或换行分隔)"); + errors.Add("Region 与 Assigned Location Ids 至少填一项(均可留空时仅适用于 Company Admin 且已在 Web 端配置 Company)"); } if (errors.Count > 0) @@ -297,6 +357,7 @@ public static class TeamMemberBatchExcelHelper Password = pwd, Phone = phone, RoleId = roleIdResolved, + RegionIds = regionTokens, LocationIds = locationTokens, State = state }; @@ -308,23 +369,27 @@ public static class TeamMemberBatchExcelHelper } /// - /// 拆分门店单元格为「待解析」片段(后续由服务层解析为 Location Id)。 + /// 拆分单元格为多个 token(门店/区域等;后续由服务层解析为 Id)。 /// - public static List SplitLocationTokens(string locationsCell) + public static List SplitMultiValueTokens(string cell) { - if (string.IsNullOrWhiteSpace(locationsCell)) + if (string.IsNullOrWhiteSpace(cell)) { return new List(); } - var parts = locationsCell + return cell .Split(new[] { ';', '|', '\n', '\r', ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(x => x.Trim()) .Where(x => !string.IsNullOrEmpty(x)) .ToList(); - return parts; } + /// + /// 兼容旧调用。 + /// + public static List SplitLocationTokens(string locationsCell) => SplitMultiValueTokens(locationsCell); + private static string RegexDigitsOnly(string s) { return new string(s.Where(char.IsDigit).ToArray()); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberListScopeHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberListScopeHelper.cs index 08eb869..057a0be 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberListScopeHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberListScopeHelper.cs @@ -9,6 +9,44 @@ namespace FoodLabeling.Application.Helpers; public static class TeamMemberListScopeHelper { /// + /// 统一 Guid 字符串键(小写 D 格式),避免 userlocation.UserId 与 User.Id.ToString() 大小写不一致导致漏人。 + /// + public static string NormalizeScopeKey(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + + return Guid.TryParse(value.Trim(), out var guid) + ? guid.ToString("D").ToLowerInvariant() + : value.Trim().ToLowerInvariant(); + } + + public static string UserKey(Guid userId) => userId.ToString("D").ToLowerInvariant(); + + public static HashSet ParseGuidHashSet(IEnumerable raw) + { + var set = new HashSet(); + foreach (var item in raw) + { + if (string.IsNullOrWhiteSpace(item)) + { + continue; + } + + if (Guid.TryParse(item.Trim(), out var guid)) + { + set.Add(guid); + } + } + + return set; + } + + public static List ParseGuidList(IEnumerable raw) => + ParseGuidHashSet(raw).ToList(); + /// /// 解析列表/导出适用的门店 Id 范围(用于 userlocation 命中成员)。 /// /// 管理员:仅受 Query 中 partnerId / groupId / locationId 约束;均未传则返回 null(不限制成员)。 diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberRoleHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberRoleHelper.cs new file mode 100644 index 0000000..e5663c1 --- /dev/null +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberRoleHelper.cs @@ -0,0 +1,95 @@ +using SqlSugar; +using Volo.Abp.Users; +using Yi.Framework.Rbac.Domain.Entities; + +namespace FoodLabeling.Application.Helpers; + +/// +/// Team Member 角色识别(Company Admin / Partner Admin 等)。 +/// +public static class TeamMemberRoleHelper +{ + /// + /// UI 展示为 Company Admin,库内常见为 Partner Admin 或 RoleCode 含 partner。 + /// + public static bool IsCompanyAdminRoleName(string? roleName) + { + if (string.IsNullOrWhiteSpace(roleName)) + { + return false; + } + + var name = roleName.Trim(); + if (string.Equals(name, "Company Admin", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (string.Equals(name, "Partner Admin", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return name.Contains("partner", StringComparison.OrdinalIgnoreCase) && + name.Contains("admin", StringComparison.OrdinalIgnoreCase); + } + + /// + /// 当前登录用户是否为 Company Admin 类角色。 + /// + public static async Task IsCompanyAdminUserAsync(ISqlSugarClient db, ICurrentUser currentUser) + { + if (currentUser.Id is null) + { + return false; + } + + var roleId = await db.Queryable() + .Where(ur => ur.UserId == currentUser.Id.Value) + .Select(ur => (Guid?)ur.RoleId) + .FirstAsync(); + + return await IsCompanyAdminRoleAsync(db, roleId); + } + + /// + /// 是否 Company Admin 类角色(按 RoleId 查库)。 + /// + public static async Task IsCompanyAdminRoleAsync(ISqlSugarClient db, Guid? roleId) + { + if (roleId is null) + { + return false; + } + + var role = await db.Queryable() + .FirstAsync(x => !x.IsDeleted && x.Id == roleId.Value); + if (role is null) + { + return false; + } + + if (IsCompanyAdminRoleName(role.RoleName)) + { + return true; + } + + var code = role.RoleCode?.Trim(); + return !string.IsNullOrEmpty(code) && + code.Contains("partner", StringComparison.OrdinalIgnoreCase); + } + + /// + /// 列表/详情出参:Partner Admin → Company Admin(与 Web UI 一致)。 + /// + public static string? FormatDisplayRoleName(string? roleName) + { + if (IsCompanyAdminRoleName(roleName) && + !string.Equals(roleName?.Trim(), "Company Admin", StringComparison.OrdinalIgnoreCase)) + { + return "Company Admin"; + } + + return roleName; + } +} diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppAuthScopeHelper.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppAuthScopeHelper.cs index 5222935..10f9c4e 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppAuthScopeHelper.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppAuthScopeHelper.cs @@ -1,5 +1,6 @@ using System.Text.Json; using FoodLabeling.Application.Contracts; +using FoodLabeling.Application.Contracts.Dtos.AuthScope; using FoodLabeling.Application.Contracts.Dtos.UsAppAuth; using FoodLabeling.Application.Services.DbModels; using FoodLabeling.Domain.Entities; @@ -94,11 +95,13 @@ public static class UsAppAuthScopeHelper var partner = await db.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == partnerId.Trim()); - var locations = await db.Queryable() + var locations = (await db.Queryable() .Where(x => !x.IsDeleted && ids.Contains(x.Id.ToString())) .OrderBy(x => x.OrderNum) - .ThenBy(x => x.LocationName) - .ToListAsync(); + .ToListAsync()) + .OrderBy(x => x.OrderNum) + .ThenBy(x => x.LocationName, StringComparer.OrdinalIgnoreCase) + .ToList(); return locations.Select(x => new AuthScopeLocationOptionDto { diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs index 674b99b..b05d2e9 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs @@ -54,9 +54,11 @@ public class DashboardAppService : ApplicationService, IDashboardAppService db.Queryable(), scopeLocationIds) .CountAsync(x => x.CreationTime >= yesterdayStart && x.CreationTime < todayStart); - var activeTemplates = await _dbContext.SqlSugarClient.Queryable() + var activeTemplates = await LabelTemplateQueryHelper.ProjectListColumns( + _dbContext.SqlSugarClient.Queryable()) .CountAsync(x => !x.IsDeleted && x.State); - var activeTemplatesPrevWeek = await _dbContext.SqlSugarClient.Queryable() + var activeTemplatesPrevWeek = await LabelTemplateQueryHelper.ProjectListColumns( + _dbContext.SqlSugarClient.Queryable()) .CountAsync(x => !x.IsDeleted && x.State && x.CreationTime < weekStart); var activeUsers = await DashboardScopeHelper.CountScopedTeamMembersAsync( @@ -166,7 +168,7 @@ public class DashboardAppService : ApplicationService, IDashboardAppService .Select((t, l, p, tpl) => new { t.Id, - LabelCode = l.LabelCode, + t.LocationId, LabelName = l.LabelName, ProductName = p.ProductName, tpl.Width, @@ -178,6 +180,13 @@ public class DashboardAppService : ApplicationService, IDashboardAppService }) .ToListAsync(); + var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync( + db, + recentRaw.Select(x => new ReportsPrintLogDailyLabelIdHelper.PrintTaskScopeKey( + x.Id, + x.LocationId, + x.PrintedAt ?? DateTime.MinValue)).ToList()); + var recentUserIds = recentRaw .Select(x => x.CreatedBy) .Where(x => !string.IsNullOrWhiteSpace(x)) @@ -209,10 +218,11 @@ public class DashboardAppService : ApplicationService, IDashboardAppService : (string.IsNullOrWhiteSpace(x.LabelName) ? "无" : x.LabelName.Trim()); var printedAt = x.PrintedAt ?? DateTime.MinValue; var status = ResolveRecentLabelStatus(x.PrintInputJson); + var labelDisplayId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无"; return new DashboardRecentLabelItemDto { TaskId = x.Id, - LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(), + LabelCode = labelDisplayId, DisplayName = displayName, PrintedByUserId = x.CreatedBy?.Trim(), PrintedByName = ResolveRecentUserName(recentUserMap, x.CreatedBy), diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTemplateDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTemplateDbEntity.cs index 5ff50b8..06031ad 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTemplateDbEntity.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTemplateDbEntity.cs @@ -34,12 +34,6 @@ public class FlLabelTemplateDbEntity public string AppliedLocationType { get; set; } = "ALL"; - /// 适用 Company 范围:ALL / SPECIFIED(见 fl_label_template_partner) - public string AppliedPartnerType { get; set; } = "ALL"; - - /// 适用 Region 范围:ALL / SPECIFIED(见 fl_label_template_region) - public string AppliedRegionType { get; set; } = "ALL"; - public bool ShowRuler { get; set; } public bool ShowGrid { get; set; } @@ -54,4 +48,3 @@ public class FlLabelTemplateDbEntity public bool State { get; set; } } - diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs index 9810f42..eddfd88 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs @@ -298,7 +298,8 @@ public class LabelAppService : ApplicationService, ILabelAppService throw new UserFriendlyException("标签不存在"); } - var template = await _dbContext.SqlSugarClient.Queryable() + var template = await LabelTemplateQueryHelper.ProjectListColumns( + _dbContext.SqlSugarClient.Queryable()) .FirstAsync(x => x.Id == label.TemplateId); var category = await _dbContext.SqlSugarClient.Queryable() .FirstAsync(x => x.Id == label.LabelCategoryId); @@ -404,7 +405,8 @@ public class LabelAppService : ApplicationService, ILabelAppService input.LocationId, input.LocationIds); - var template = await _dbContext.SqlSugarClient.Queryable() + var template = await LabelTemplateQueryHelper.ProjectListColumns( + _dbContext.SqlSugarClient.Queryable()) .FirstAsync(x => !x.IsDeleted && x.TemplateCode == input.TemplateCode.Trim()); if (template is null) { @@ -520,7 +522,8 @@ public class LabelAppService : ApplicationService, ILabelAppService input.LocationId, input.LocationIds); - var template = await _dbContext.SqlSugarClient.Queryable() + var template = await LabelTemplateQueryHelper.ProjectListColumns( + _dbContext.SqlSugarClient.Queryable()) .FirstAsync(x => !x.IsDeleted && x.TemplateCode == input.TemplateCode.Trim()); if (template is null) { @@ -671,7 +674,8 @@ public class LabelAppService : ApplicationService, ILabelAppService } // 取模板头 & elements - var template = await _dbContext.SqlSugarClient.Queryable() + var template = await LabelTemplateQueryHelper.ProjectListColumns( + _dbContext.SqlSugarClient.Queryable()) .FirstAsync(x => !x.IsDeleted && x.Id == label.TemplateId); if (template is null) { diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTemplateAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTemplateAppService.cs index 5b0df03..02534da 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTemplateAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTemplateAppService.cs @@ -29,10 +29,11 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ public async Task> GetListAsync(LabelTemplateGetListInputVo input) { + input ??= new LabelTemplateGetListInputVo(); RefAsync total = 0; var keyword = input.Keyword?.Trim(); - var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( - _dbContext.SqlSugarClient, input.GroupId, input.LocationId); + var scopedLocationIds = await LocationScopeBindingHelper.ResolveFilteredLocationIdsForListAsync( + _dbContext.SqlSugarClient, input.PartnerId, input.GroupId, input.LocationId); var query = _dbContext.SqlSugarClient.Queryable() .Where(x => !x.IsDeleted) @@ -44,25 +45,30 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ query = await LabelTemplateScopeHelper.ApplyTemplateScopeFilterAsync( _dbContext.SqlSugarClient, query, scopedLocationIds); - query = !string.IsNullOrWhiteSpace(input.Sorting) - ? query.OrderBy(input.Sorting) - : query.OrderByDescending(x => x.LastModificationTime ?? x.CreationTime); + query = ApplyLabelTemplateListSorting(query, input.Sorting); + query = LabelTemplateQueryHelper.ProjectListColumns(query); - var pageEntities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); + var pageSize = input.MaxResultCount <= 0 ? 10 : input.MaxResultCount; + var pageEntities = await query.ToPageListAsync(input.SkipCount, pageSize, total); var templateIds = pageEntities.Select(x => x.Id).ToList(); - // element count (Contents) - var elementCounts = await _dbContext.SqlSugarClient.Queryable() - .Where(x => templateIds.Contains(x.TemplateId)) - .GroupBy(x => x.TemplateId) - .Select(x => new { TemplateId = x.TemplateId, Count = SqlFunc.AggregateCount(x.Id) }) - .ToListAsync(); - var elementCountMap = elementCounts.ToDictionary(x => x.TemplateId, x => (int)x.Count); + var elementCountMap = new Dictionary(StringComparer.Ordinal); + if (templateIds.Count > 0) + { + var elementCounts = await _dbContext.SqlSugarClient.Queryable() + .Where(x => templateIds.Contains(x.TemplateId)) + .GroupBy(x => x.TemplateId) + .Select(x => new { TemplateId = x.TemplateId, Count = SqlFunc.AggregateCount(x.Id) }) + .ToListAsync(); + elementCountMap = elementCounts.ToDictionary(x => x.TemplateId, x => (int)x.Count); + } var scopeMap = await LabelTemplateScopeHelper.BuildScopeDisplayMapAsync( _dbContext.SqlSugarClient, pageEntities); - var itemsMap = await LabelTemplateListItemsHelper.ResolveTemplateItemsMapAsync( - _dbContext.SqlSugarClient, templateIds); + var itemsMap = templateIds.Count > 0 + ? await LabelTemplateListItemsHelper.ResolveTemplateItemsMapAsync( + _dbContext.SqlSugarClient, templateIds) + : new Dictionary(StringComparer.Ordinal); var items = pageEntities.Select(x => { @@ -97,12 +103,69 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ }; }).ToList(); - return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); + return BuildPagedResult(input.SkipCount, pageSize, total, items); + } + + /// + /// 列表排序:白名单字段 + IFNULL(LastModificationTime, CreationTime),避免 ?? 与原始 Sorting 拼接导致 SQL 异常。 + /// + private static ISugarQueryable ApplyLabelTemplateListSorting( + ISugarQueryable query, + string? sorting) + { + if (!string.IsNullOrWhiteSpace(sorting)) + { + var s = sorting.Trim(); + if (s.Equals("TemplateName asc", StringComparison.OrdinalIgnoreCase)) + { + return query.OrderBy(x => x.TemplateName); + } + + if (s.Equals("TemplateName desc", StringComparison.OrdinalIgnoreCase)) + { + return query.OrderByDescending(x => x.TemplateName); + } + + if (s.Equals("TemplateCode asc", StringComparison.OrdinalIgnoreCase)) + { + return query.OrderBy(x => x.TemplateCode); + } + + if (s.Equals("TemplateCode desc", StringComparison.OrdinalIgnoreCase)) + { + return query.OrderByDescending(x => x.TemplateCode); + } + + if (s.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase)) + { + return query.OrderBy(x => x.CreationTime); + } + + if (s.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase)) + { + return query.OrderByDescending(x => x.CreationTime); + } + + if (s.Equals("LastModificationTime asc", StringComparison.OrdinalIgnoreCase)) + { + return query.OrderBy(x => x.LastModificationTime); + } + + if (s.Equals("LastModificationTime desc", StringComparison.OrdinalIgnoreCase)) + { + return query.OrderByDescending(x => x.LastModificationTime); + } + } + + return query + .OrderBy(x => SqlFunc.IsNull(x.LastModificationTime, x.CreationTime), OrderByType.Desc) + .OrderBy(x => x.TemplateCode, OrderByType.Asc); } public async Task GetAsync(string id) { - var template = await _dbContext.SqlSugarClient.Queryable() + var template = await LabelTemplateQueryHelper.ProjectListColumns( + _dbContext.SqlSugarClient.Queryable()) .FirstAsync(x => !x.IsDeleted && x.TemplateCode == id); if (template is null) { @@ -180,8 +243,8 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ Width = template.Width, Height = template.Height, AppliedLocationType = template.AppliedLocationType, - AppliedPartnerType = template.AppliedPartnerType, - AppliedRegionType = template.AppliedRegionType, + AppliedPartnerType = LabelTemplateScopeHelper.ScopeAll, + AppliedRegionType = LabelTemplateScopeHelper.ScopeAll, ShowRuler = template.ShowRuler, ShowGrid = template.ShowGrid, BorderType = NormalizeTemplateBorderType(template.BorderType), @@ -210,7 +273,8 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ throw new UserFriendlyException("模板名称不能为空"); } - var duplicated = await _dbContext.SqlSugarClient.Queryable() + var duplicated = await LabelTemplateQueryHelper.ProjectListColumns( + _dbContext.SqlSugarClient.Queryable()) .AnyAsync(x => !x.IsDeleted && x.TemplateCode == code); if (duplicated) { @@ -237,8 +301,6 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ Width = input.Width, Height = input.Height, AppliedLocationType = scope.AppliedLocationType, - AppliedPartnerType = scope.AppliedPartnerType, - AppliedRegionType = scope.AppliedRegionType, ShowRuler = input.ShowRuler, ShowGrid = input.ShowGrid, BorderType = NormalizeTemplateBorderType(input.BorderType), @@ -262,13 +324,20 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ CurrentUser?.Id?.ToString(), now); + await LabelTemplateScopeSchemaHelper.SetAppliedScopeTypesAsync( + _dbContext.SqlSugarClient, + entity.Id, + scope.AppliedPartnerType, + scope.AppliedRegionType); + return await GetAsync(code); } [UnitOfWork] public async Task UpdateAsync(string id, LabelTemplateUpdateInputVo input) { - var template = await _dbContext.SqlSugarClient.Queryable() + var template = await LabelTemplateQueryHelper.ProjectListColumns( + _dbContext.SqlSugarClient.Queryable()) .FirstAsync(x => !x.IsDeleted && x.TemplateCode == id); if (template is null) { @@ -279,7 +348,8 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ var name = input.TemplateName?.Trim(); if (!string.IsNullOrWhiteSpace(code) && !string.Equals(code, template.TemplateCode, StringComparison.OrdinalIgnoreCase)) { - var duplicated = await _dbContext.SqlSugarClient.Queryable() + var duplicated = await LabelTemplateQueryHelper.ProjectListColumns( + _dbContext.SqlSugarClient.Queryable()) .AnyAsync(x => !x.IsDeleted && x.TemplateCode == code); if (duplicated) { @@ -295,8 +365,6 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ template.Width = input.Width; template.Height = input.Height; template.AppliedLocationType = scope.AppliedLocationType; - template.AppliedPartnerType = scope.AppliedPartnerType; - template.AppliedRegionType = scope.AppliedRegionType; template.ShowRuler = input.ShowRuler; template.ShowGrid = input.ShowGrid; template.BorderType = NormalizeTemplateBorderType(input.BorderType); @@ -325,13 +393,20 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ CurrentUser?.Id?.ToString(), template.LastModificationTime ?? DateTime.Now); + await LabelTemplateScopeSchemaHelper.SetAppliedScopeTypesAsync( + _dbContext.SqlSugarClient, + template.Id, + scope.AppliedPartnerType, + scope.AppliedRegionType); + return await GetAsync(template.TemplateCode); } [UnitOfWork] public async Task DeleteAsync(string id) { - var template = await _dbContext.SqlSugarClient.Queryable() + var template = await LabelTemplateQueryHelper.ProjectListColumns( + _dbContext.SqlSugarClient.Queryable()) .FirstAsync(x => !x.IsDeleted && x.TemplateCode == id); if (template is null) { @@ -358,12 +433,15 @@ public class LabelTemplateAppService : ApplicationService, ILabelTemplateAppServ await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.TemplateId == template.Id) .ExecuteCommandAsync(); - await _dbContext.SqlSugarClient.Deleteable() - .Where(x => x.TemplateId == template.Id) - .ExecuteCommandAsync(); - await _dbContext.SqlSugarClient.Deleteable() - .Where(x => x.TemplateId == template.Id) - .ExecuteCommandAsync(); + if (await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(_dbContext.SqlSugarClient)) + { + await _dbContext.SqlSugarClient.Deleteable() + .Where(x => x.TemplateId == template.Id) + .ExecuteCommandAsync(); + await _dbContext.SqlSugarClient.Deleteable() + .Where(x => x.TemplateId == template.Id) + .ExecuteCommandAsync(); + } await _dbContext.SqlSugarClient.Deleteable() .Where(x => x.TemplateId == template.Id) .ExecuteCommandAsync(); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs index cb373b3..b3ce8da 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs @@ -135,7 +135,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService : x.LabelCategoryName!.Trim(), CategoryName = string.IsNullOrWhiteSpace(cat) ? "无" : cat, TemplateText = string.IsNullOrWhiteSpace(templateText) ? "无" : templateText, - PrintedAt = ReportsDateTimeDisplayHelper.FormatPrintedAt(printedAt), + PrintedAt = printedAt, PrintedByName = ResolveUserName(userMap, x.CreatedBy), LocationText = locText, LocationId = x.LocationId?.Trim(), @@ -997,7 +997,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService : x.LabelCategoryName!.Trim(), CategoryName = string.IsNullOrWhiteSpace(cat) ? "无" : cat, TemplateText = string.IsNullOrWhiteSpace(templateText) ? "无" : templateText, - PrintedAt = ReportsDateTimeDisplayHelper.FormatPrintedAt(printedAt), + PrintedAt = printedAt, PrintedByName = ResolveUserName(userMap, x.CreatedBy), LocationText = locText, LocationId = x.LocationId?.Trim(), diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs index 44dc45c..905da64 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/TeamMemberAppService.cs @@ -120,6 +120,14 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( _dbContext.SqlSugarClient, locationIds); + if (await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(_dbContext.SqlSugarClient, role?.RoleId) && + partnerIds.Count > 0) + { + (regionIds, assigned) = await ApplyCompanyAdminDisplayScopeAsync( + role?.RoleId, partnerIds, regionIds, assigned); + locationIds = assigned.Select(x => x.Id).ToList(); + } + return new TeamMemberGetOutputDto { Id = user.Id, @@ -140,7 +148,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService /// public async Task CreateAsync(TeamMemberCreateInputVo input) { - var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input); + var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input, input.RoleId); var user = new UserAggregateRoot { @@ -245,25 +253,13 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService public Task DownloadTeamMemberImportTemplateAsync() { var opt = _batchImportOptions.Value; - var dir = opt.TemplateDirectory?.Trim(); - if (string.IsNullOrWhiteSpace(dir)) - { - throw new UserFriendlyException("未配置批量导入模板目录 FoodLabeling:BatchImport:TemplateDirectory"); - } - var fileName = opt.TeamMemberTemplateFileName?.Trim(); if (string.IsNullOrWhiteSpace(fileName)) { - throw new UserFriendlyException("未配置模板文件名 FoodLabeling:BatchImport:TeamMemberTemplateFileName"); + fileName = "Team-Member-批量导入模板.xlsx"; } - var fullPath = Path.Combine(dir, fileName); - if (!File.Exists(fullPath)) - { - throw new UserFriendlyException($"模板文件不存在:{fullPath}"); - } - - var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read); + var stream = TeamMemberBatchExcelHelper.BuildImportTemplateWorkbook(); const string contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; return Task.FromResult(new FileStreamResult(stream, contentType) { @@ -294,6 +290,9 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService scopeLocationIds, restrictAssignedLocationsToFilter: scopeLocationIds is not null); + var regionNameMap = await LoadRegionNameMapAsync( + rows.SelectMany(r => r.RegionIds).Distinct(StringComparer.Ordinal)); + var fileName = $"team-members_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; var document = Document.Create(container => { @@ -306,11 +305,12 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService { table.ColumnsDefinition(c => { + c.RelativeColumn(1.3f); + c.RelativeColumn(1.5f); + c.RelativeColumn(1.0f); + c.RelativeColumn(1.0f); c.RelativeColumn(1.4f); - c.RelativeColumn(1.6f); - c.RelativeColumn(1.1f); - c.RelativeColumn(1.1f); - c.RelativeColumn(2.2f); + c.RelativeColumn(2.0f); c.RelativeColumn(0.7f); }); @@ -321,11 +321,19 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService table.Cell().Element(CellHeader).Text("Email"); table.Cell().Element(CellHeader).Text("Phone"); table.Cell().Element(CellHeader).Text("Role"); + table.Cell().Element(CellHeader).Text("Region"); table.Cell().Element(CellHeader).Text("Assigned Locations"); table.Cell().Element(CellHeader).Text("Status"); foreach (var e in rows) { + var regionText = e.RegionIds.Count == 0 + ? "无" + : string.Join("; ", + e.RegionIds.Select(id => + regionNameMap.TryGetValue(id, out var name) && !string.IsNullOrWhiteSpace(name) + ? name + : id)); var locText = e.AssignedLocations.Count == 0 ? "无" : string.Join("; ", @@ -341,6 +349,8 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) .Text(string.IsNullOrWhiteSpace(e.RoleName) ? "无" : e.RoleName); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) + .Text(regionText); + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) .Text(locText); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) .Text(status); @@ -399,7 +409,16 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService { try { - vo.LocationIds = await ResolveLocationIdsFromImportTokensAsync(vo.LocationIds); + if (vo.RegionIds is { Count: > 0 }) + { + vo.RegionIds = await ResolveRegionIdsFromImportTokensAsync(vo.RegionIds); + } + + if (vo.LocationIds is { Count: > 0 }) + { + vo.LocationIds = await ResolveLocationIdsFromImportTokensAsync(vo.LocationIds); + } + await CreateAsync(vo); result.SuccessCount++; } @@ -515,7 +534,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService .FirstAsync(); if (byCode is null) { - throw new UserFriendlyException($"未找到门店编码:{key}"); + throw new UserFriendlyException($"未找到门店 LocationCode:{key}(亦可填 location.Id Guid)"); } result.Add(byCode.Id.ToString()); @@ -524,6 +543,83 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService return result.Distinct().ToList(); } + private async Task> ResolveRegionIdsFromImportTokensAsync(List tokens) + { + var result = new List(); + foreach (var raw in tokens) + { + var s = raw.Trim(); + if (string.IsNullOrEmpty(s)) + { + continue; + } + + var idx = s.IndexOf(" -", StringComparison.Ordinal); + var key = idx > 0 ? s[..idx].Trim() : s.Trim(); + + if (Guid.TryParse(key, out _)) + { + var byId = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted && x.Id == key) + .FirstAsync(); + if (byId is null) + { + throw new UserFriendlyException($"无效 Region Id:{key}"); + } + + result.Add(byId.Id.Trim()); + continue; + } + + var matches = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted) + .ToListAsync(); + matches = matches + .Where(x => string.Equals(x.GroupName?.Trim(), key, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (matches.Count == 0) + { + throw new UserFriendlyException($"未找到 Region:{key}(可填 fl_group.Id 或 GroupName)"); + } + + if (matches.Count > 1) + { + throw new UserFriendlyException( + $"Region 名称「{key}」存在多条记录,请改用 fl_group.Id(Guid)"); + } + + result.Add(matches[0].Id.Trim()); + } + + return result.Distinct(StringComparer.Ordinal).ToList(); + } + + private async Task> LoadRegionNameMapAsync(IEnumerable regionIds) + { + var ids = regionIds + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .Distinct(StringComparer.Ordinal) + .ToList(); + if (ids.Count == 0) + { + return new Dictionary(StringComparer.Ordinal); + } + + var groups = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted && ids.Contains(x.Id)) + .Select(x => new { x.Id, x.GroupName }) + .ToListAsync(); + + return groups + .Where(x => !string.IsNullOrWhiteSpace(x.Id)) + .ToDictionary( + x => x.Id.Trim(), + x => x.GroupName?.Trim() ?? x.Id.Trim(), + StringComparer.Ordinal); + } + private async Task> BuildFilteredUserQueryAsync( TeamMemberGetListInputVo input, List? scopeLocationIds) @@ -555,13 +651,30 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService } else { - var scopeSet = new HashSet(scopeLocationIds, StringComparer.Ordinal); - var userIdStrs = await _dbContext.SqlSugarClient.Queryable() - .Where(x => !x.IsDeleted && scopeSet.Contains(x.LocationId)) - .Select(x => x.UserId) + var scopeGuidSet = TeamMemberListScopeHelper.ParseGuidHashSet(scopeLocationIds); + var userLocationLinks = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted) + .Select(x => new { x.UserId, x.LocationId }) .ToListAsync(); - var allowed = new HashSet(userIdStrs); - query = query.Where(u => allowed.Contains(u.Id.ToString())); + + var allowedUserGuids = userLocationLinks + .Where(x => + Guid.TryParse(x.LocationId, out var locGuid) && scopeGuidSet.Contains(locGuid)) + .Select(x => x.UserId) + .Select(TeamMemberListScopeHelper.NormalizeScopeKey) + .Where(x => !string.IsNullOrEmpty(x)) + .Select(x => Guid.Parse(x)) + .Distinct() + .ToList(); + + if (allowedUserGuids.Count == 0) + { + query = query.Where(_ => false); + } + else + { + query = query.Where(u => allowedUserGuids.Contains(u.Id)); + } } } @@ -579,43 +692,62 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService } var userIds = users.Select(x => x.Id).ToList(); - var userIdStrings = userIds.Select(x => x.ToString()).ToList(); + var userGuidKeys = userIds.Select(TeamMemberListScopeHelper.UserKey).ToHashSet(StringComparer.Ordinal); var userRolePairs = await _dbContext.SqlSugarClient.Queryable((ur, r) => ur.RoleId == r.Id) .Where(ur => userIds.Contains(ur.UserId)) .Select((ur, r) => new { ur.UserId, r.Id, r.RoleName }) .ToListAsync(); - var roleMap = userRolePairs + var roleIdByUser = userRolePairs .GroupBy(x => x.UserId) - .ToDictionary(g => g.Key, g => g.FirstOrDefault()); + .ToDictionary(g => g.Key, g => (Guid?)g.First().Id); - var userLocQuery = _dbContext.SqlSugarClient.Queryable() + var allUserLocations = await _dbContext.SqlSugarClient.Queryable() .Where(x => !x.IsDeleted) - .Where(x => userIdStrings.Contains(x.UserId)); - if (restrictAssignedLocationsToFilter && scopeLocationIds is { Count: > 0 }) - { - var scopeSet = new HashSet(scopeLocationIds, StringComparer.Ordinal); - userLocQuery = userLocQuery.Where(x => scopeSet.Contains(x.LocationId)); - } + .Select(x => new { x.UserId, x.LocationId }) + .ToListAsync(); + + var scopeGuidSet = scopeLocationIds is { Count: > 0 } + ? TeamMemberListScopeHelper.ParseGuidHashSet(scopeLocationIds) + : null; - var userLocations = await userLocQuery.ToListAsync(); + var userLocations = allUserLocations + .Where(x => userGuidKeys.Contains(TeamMemberListScopeHelper.NormalizeScopeKey(x.UserId))) + .Where(x => + scopeGuidSet is null || + (Guid.TryParse(x.LocationId, out var locGuid) && scopeGuidSet.Contains(locGuid))) + .ToList(); var locationIds = userLocations.Select(x => x.LocationId).Distinct().ToList(); + var locationGuidList = TeamMemberListScopeHelper.ParseGuidList(locationIds); var locations = await _dbContext.SqlSugarClient.Queryable() .Where(x => !x.IsDeleted) - .WhereIF(locationIds.Count > 0, x => locationIds.Contains(x.Id.ToString())) + .WhereIF(locationGuidList.Count > 0, x => locationGuidList.Contains(x.Id)) .Select(x => new { x.Id, x.LocationCode, x.LocationName }) .ToListAsync(); - var locationMap = locations.ToDictionary(x => x.Id.ToString(), x => x); + var locationMap = locations.ToDictionary( + x => TeamMemberListScopeHelper.UserKey(x.Id), + x => x); var assignedMap = userLocations - .GroupBy(x => x.UserId) + .GroupBy(x => TeamMemberListScopeHelper.NormalizeScopeKey(x.UserId)) .ToDictionary( g => g.Key, g => g.Select(x => { - if (locationMap.TryGetValue(x.LocationId, out var loc)) + var locKey = TeamMemberListScopeHelper.NormalizeScopeKey(x.LocationId); + if (locationMap.TryGetValue(locKey, out var loc)) + { + return new TeamMemberAssignedLocationDto + { + Id = loc.Id.ToString(), + LocationCode = loc.LocationCode, + LocationName = loc.LocationName + }; + } + + if (locationMap.TryGetValue(x.LocationId, out loc)) { return new TeamMemberAssignedLocationDto { @@ -628,15 +760,31 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService return null; }).Where(x => x != null).Cast().ToList()); - var scopeIdsMap = await BuildTeamMemberScopeIdsMapAsync(assignedMap); + var scopeIdsMap = await BuildTeamMemberScopeIdsMapAsync(assignedMap, roleIdByUser); - return users.Select(u => + var items = new List(); + foreach (var u in users) { - roleMap.TryGetValue(u.Id, out var role); - assignedMap.TryGetValue(u.Id.ToString(), out var assigned); - scopeIdsMap.TryGetValue(u.Id.ToString(), out var scopeIds); + roleIdByUser.TryGetValue(u.Id, out var listRoleId); + var roleName = userRolePairs.FirstOrDefault(x => x.UserId == u.Id)?.RoleName; + var userKey = TeamMemberListScopeHelper.UserKey(u.Id); + assignedMap.TryGetValue(userKey, out var assigned); + scopeIdsMap.TryGetValue(userKey, out var scopeIds); + + var partnerIds = scopeIds?.PartnerIds ?? new List(); + var regionIds = scopeIds?.RegionIds ?? new List(); + var assignedLocations = assigned ?? new List(); + + (regionIds, assignedLocations) = await ApplyCompanyAdminDisplayScopeAsync( + listRoleId, partnerIds, regionIds, assignedLocations); + + var locationIdList = assignedLocations + .Select(x => x.Id) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); - return new TeamMemberGetListOutputDto + items.Add(new TeamMemberGetListOutputDto { Id = u.Id, FullName = u.Name ?? string.Empty, @@ -644,17 +792,21 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService Email = u.Email, Phone = u.Phone, State = u.State, - RoleId = role?.Id, - RoleName = role?.RoleName, - PartnerIds = scopeIds?.PartnerIds ?? new List(), - RegionIds = scopeIds?.RegionIds ?? new List(), - AssignedLocations = assigned ?? new List() - }; - }).ToList(); + RoleId = listRoleId, + RoleName = TeamMemberRoleHelper.FormatDisplayRoleName(roleName), + PartnerIds = partnerIds, + RegionIds = regionIds, + LocationIds = locationIdList, + AssignedLocations = assignedLocations + }); + } + + return items; } private async Task> BuildTeamMemberScopeIdsMapAsync( - Dictionary> assignedMap) + Dictionary> assignedMap, + Dictionary roleIdByUser) { var result = new Dictionary(StringComparer.Ordinal); foreach (var (userId, assigned) in assignedMap) @@ -665,16 +817,29 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService .Select(x => x.Trim()) .Distinct(StringComparer.Ordinal) .ToList(); - if (locationIds.Count == 0) + + var partnerIds = locationIds.Count == 0 + ? new List() + : await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIds); + var regionIds = locationIds.Count == 0 + ? new List() + : await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIds); + + if (Guid.TryParse(userId, out var userGuid) && + roleIdByUser.TryGetValue(userGuid, out var roleId) && + await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(_dbContext.SqlSugarClient, roleId) && + partnerIds.Count > 0) { - result[userId] = new TeamMemberScopeIds(); - continue; + var allRegions = await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync( + _dbContext.SqlSugarClient, partnerIds); + if (allRegions.Count > 0) + { + regionIds = allRegions; + } } - var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( - _dbContext.SqlSugarClient, locationIds); - var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( - _dbContext.SqlSugarClient, locationIds); result[userId] = new TeamMemberScopeIds { PartnerIds = partnerIds, @@ -685,6 +850,68 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService return result; } + /// + /// Company Admin 列表/详情:展示所选 Company 下全部 Region 与门店。 + /// + private async Task<(List RegionIds, List AssignedLocations)> + ApplyCompanyAdminDisplayScopeAsync( + Guid? roleId, + List partnerIds, + List regionIds, + List assigned) + { + if (!await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(_dbContext.SqlSugarClient, roleId) || + partnerIds.Count == 0) + { + return (regionIds, assigned); + } + + var allRegions = await LocationScopeBindingHelper.ResolveGroupIdsFromPartnerIdsAsync( + _dbContext.SqlSugarClient, partnerIds); + var allLocationIds = await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync( + _dbContext.SqlSugarClient, partnerIds); + var allAssigned = await BuildAssignedLocationDtosAsync(allLocationIds); + + return ( + allRegions.Count > 0 ? allRegions : regionIds, + allAssigned.Count > 0 ? allAssigned : assigned); + } + + private async Task> BuildAssignedLocationDtosAsync( + IReadOnlyList locationIds) + { + var ids = LocationScopeBindingHelper.NormalizeIds(locationIds); + if (ids.Count == 0) + { + return new List(); + } + + var guidList = ids + .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null) + .Where(x => x.HasValue) + .Select(x => x!.Value) + .ToList(); + if (guidList.Count == 0) + { + return new List(); + } + + var locations = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) + .Select(x => new { x.Id, x.LocationCode, x.LocationName }) + .ToListAsync(); + + return locations + .OrderBy(x => x.LocationCode) + .Select(x => new TeamMemberAssignedLocationDto + { + Id = x.Id.ToString(), + LocationCode = x.LocationCode, + LocationName = x.LocationName + }) + .ToList(); + } + private sealed class TeamMemberScopeIds { public List PartnerIds { get; init; } = new(); @@ -699,18 +926,41 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService RegionIds = input.RegionIds, GroupIds = input.GroupIds, LocationIds = input.LocationIds - }); + }, input.RoleId); - private async Task> ResolveTeamMemberLocationIdsForSaveAsync(TeamMemberCreateInputVo input) + private async Task> ResolveTeamMemberLocationIdsForSaveAsync( + TeamMemberCreateInputVo input, + Guid? roleId) { var partnerIds = NormalizePartnerIds(input); var regionIds = NormalizeRegionIds(input); + var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); + var isCompanyAdmin = await TeamMemberRoleHelper.IsCompanyAdminRoleAsync( + _dbContext.SqlSugarClient, roleId); + + if (isCompanyAdmin && partnerIds.Count > 0 && + regionIds.Count == 0 && explicitLocationIds.Count == 0) + { + var fromPartner = await LocationScopeBindingHelper.MergeToLocationIdsAsync( + _dbContext.SqlSugarClient, partnerIds, null, null); + if (fromPartner.Count == 0) + { + throw new UserFriendlyException("Company Admin 需绑定公司下门店,所选公司下暂无可用门店"); + } + + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, fromPartner); + return fromPartner; + } + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( _dbContext.SqlSugarClient, partnerIds, regionIds, input.LocationIds); if (merged.Count == 0) { - throw new UserFriendlyException("成员必须至少分配一个门店(公司/区域/门店至少选一项)"); + throw new UserFriendlyException( + isCompanyAdmin + ? "Company Admin 必须选择 Company,或指定 Region / 门店" + : "成员必须至少分配一个门店(公司/区域/门店至少选一项)"); } await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs index 5ef133b..704240c 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs @@ -7,6 +7,7 @@ using System.Security.Claims; using System.Text; using System.Threading.Tasks; using FoodLabeling.Application.Contracts; +using FoodLabeling.Application.Contracts.Dtos.AuthScope; using FoodLabeling.Application.Contracts.Dtos.UsAppAuth; using FoodLabeling.Application.Contracts.IServices; using FoodLabeling.Application.Helpers; @@ -209,7 +210,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService /// [Authorize] public virtual async Task SelectAdminScopeLocationAsync( - UsAppSelectAdminScopeLocationInputVo input) + AuthScopeSelectLocationInputVo input) { UsAppAuthScopeHelper.EnsureAdminAppToken(CurrentUser); if (!CurrentUser.Id.HasValue) @@ -221,7 +222,12 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService _dbContext.SqlSugarClient, _distributedCache, CurrentUser.Id.Value, - input); + new UsAppSelectAdminScopeLocationInputVo + { + PartnerId = input.PartnerId, + GroupId = input.GroupId, + LocationId = input.LocationId + }); } /// diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs index be42a9e..96a7f1e 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs @@ -396,12 +396,16 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ } var previewProductId = await ResolvePreviewProductIdAsync(labelRow.Id, input.ProductId); + var baseTime = input.BaseTime ?? DateTime.Now; + var dailyLabelId = await ReportsPrintLogDailyLabelIdHelper.ResolveNextDailyLabelIdAsync( + _dbContext.SqlSugarClient, locationId, baseTime); var template = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo { LabelCode = labelCode, ProductId = previewProductId, BaseTime = input.BaseTime, + LocationId = locationId, PrintInputJson = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value) }); @@ -465,7 +469,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ return new UsAppLabelPreviewDto { - LabelId = labelRow.Id, + LabelId = dailyLabelId, PrintLabelDisplayId = printLabelDisplayId, LocationId = locationId, LabelCode = labelCode, @@ -568,6 +572,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ LabelCode = labelCode, ProductId = previewProductId, BaseTime = input.BaseTime, + LocationId = locationId, PrintInputJson = normalizedPrintInput }); @@ -856,6 +861,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ /// ```json /// { /// "locationId": "11111111-1111-1111-1111-111111111111", + /// "printDate": "2026-06-16T00:00:00", /// "skipCount": 1, /// "maxResultCount": 20 /// } @@ -863,6 +869,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ /// /// 参数说明: /// - locationId: 当前门店 Id(必填) + /// - printDate: 打印日期(自然日,可选;未传默认当天;按 PrintedAt ?? CreationTime 筛选) /// - skipCount: 页码(从 1 开始,遵循本项目约定) /// - maxResultCount: 每页条数 /// @@ -918,8 +925,18 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ RefAsync total = 0; - var query = UsAppPrintLogScopeHelper.BuildLocationPrintTaskQuery( - _dbContext.SqlSugarClient, locationId, restrictToCreator, currentUserIdStr) + var filterDay = ReportsPrintLogDailyLabelIdHelper.ResolvePrintLogFilterCalendarDay( + input.PrintDate, input.PrintDateDay); + + var baseQuery = UsAppPrintLogScopeHelper.BuildLocationPrintTaskQuery( + _dbContext.SqlSugarClient, locationId, restrictToCreator, currentUserIdStr); + + if (filterDay.HasValue) + { + baseQuery = ReportsPrintLogDailyLabelIdHelper.ApplyPrintTaskCalendarDayFilter(baseQuery, filterDay.Value); + } + + var query = baseQuery .OrderBy((t, l, p, lt, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc) .OrderBy((t, l, p, lt, tpl) => t.CreationTime, OrderByType.Desc) .Select((t, l, p, lt, tpl) => new @@ -943,6 +960,13 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ var pageRows = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); + var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync( + _dbContext.SqlSugarClient, + pageRows.Select(x => new ReportsPrintLogDailyLabelIdHelper.PrintTaskScopeKey( + x.Id, + locationId, + x.PrintedAt ?? x.CreationTime)).ToList()); + var operatorMap = await UsAppPrintLogScopeHelper.LoadOperatorNameMapAsync( _dbContext.SqlSugarClient, pageRows.Select(x => x.CreatedBy)); @@ -952,7 +976,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ TaskId = x.Id, BatchId = x.BatchId, CopyIndex = x.CopyIndex, - LabelId = x.LabelId, + LabelId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无", + LabelEntityId = x.LabelId ?? string.Empty, LabelCode = x.LabelCode ?? string.Empty, ProductId = x.ProductId, ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json b/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json index 60e46e4..90eb624 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json @@ -20,7 +20,7 @@ }, //应用启动:SelfUrl 供 Program.cs UseUrls 绑定;用 0.0.0.0 避免写成固定局域网 IP 在本机无该网卡时启动失败(WinError 10049) "App": { - "SelfUrl": "http://localhost:19001", + "SelfUrl": "http://192.168.31.88:19001", "CorsOrigins": "http://localhost:19001;http://localhost:18000;http://localhost:5666;http://localhost:3000" }, //配置 diff --git a/美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx b/美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx index b0e3137..a4b96df 100755 --- a/美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx +++ b/美国版/Food Labeling Management Platform/src/components/people/PeopleView.tsx @@ -187,10 +187,19 @@ function normLocationId(id: string | null | undefined): string { return String(id ?? "").trim().toLowerCase(); } -function memberMatchesLocationScope(m: TeamMemberDto, allowedLocationIds: Set | null): boolean { +function memberMatchesLocationScope( + m: TeamMemberDto, + allowedLocationIds: Set | null, + allowedPartnerId?: string | null, +): boolean { + if (allowedPartnerId) { + const partnerKey = allowedPartnerId.trim().toLowerCase(); + const partnerIds = (m.partnerIds ?? []).map((x) => String(x).trim().toLowerCase()); + if (partnerIds.includes(partnerKey)) return true; + } if (allowedLocationIds === null) return true; if (allowedLocationIds.size === 0) return false; - const ids = (m.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean); + const ids = (m.locationIds ?? []).map((x) => normLocationId(x)).filter(Boolean); return ids.some((id) => allowedLocationIds.has(id)); } @@ -575,7 +584,7 @@ export function PeopleView({ if (memberLocationFilter !== "all" || !memberUsesClientScopeFilter) return null; const ids = new Set(); for (const loc of memberLocationsForSelect) { - if (loc.id) ids.add(loc.id); + if (loc.id) ids.add(normLocationId(loc.id)); } return ids; }, [memberLocationFilter, memberUsesClientScopeFilter, memberLocationsForSelect]); @@ -772,11 +781,14 @@ export function PeopleView({ skipCount: 1, maxResultCount: 500, keyword: debouncedMemberKeyword || undefined, + partnerId: memberPartnerFilter !== "all" ? memberPartnerFilter : undefined, + groupId: memberGroupFilter !== "all" ? memberGroupFilter : undefined, }, ac.signal, ); + const allowedPartnerId = memberPartnerFilter !== "all" ? memberPartnerFilter : null; const filtered = (res.items ?? []).filter((m) => - memberMatchesLocationScope(m, memberAllowedLocationIds), + memberMatchesLocationScope(m, memberAllowedLocationIds, allowedPartnerId), ); const total = filtered.length; const start = (memberPageIndex - 1) * memberPageSize; @@ -788,6 +800,8 @@ export function PeopleView({ skipCount: Math.max(1, memberPageIndex), maxResultCount: memberPageSize, keyword: debouncedMemberKeyword || undefined, + partnerId: memberPartnerFilter !== "all" ? memberPartnerFilter : undefined, + groupId: memberGroupFilter !== "all" ? memberGroupFilter : undefined, locationId: locationIdParam, }, ac.signal, diff --git a/美国版/Food Labeling Management Platform/src/services/teamMemberService.ts b/美国版/Food Labeling Management Platform/src/services/teamMemberService.ts index 1f4929b..71ee3b9 100644 --- a/美国版/Food Labeling Management Platform/src/services/teamMemberService.ts +++ b/美国版/Food Labeling Management Platform/src/services/teamMemberService.ts @@ -168,6 +168,8 @@ export async function getTeamMembers( MaxResultCount: input.maxResultCount, Keyword: input.keyword, RoleId: input.roleId, + PartnerId: input.partnerId, + GroupId: input.groupId, LocationId: input.locationId, State: input.state, Sorting: input.sorting, diff --git a/美国版/Food Labeling Management Platform/src/types/teamMember.ts b/美国版/Food Labeling Management Platform/src/types/teamMember.ts index 7f6731a..bf1e715 100644 --- a/美国版/Food Labeling Management Platform/src/types/teamMember.ts +++ b/美国版/Food Labeling Management Platform/src/types/teamMember.ts @@ -35,6 +35,8 @@ export type TeamMemberGetListInput = { maxResultCount: number; // pageSize keyword?: string; roleId?: string; + partnerId?: string; + groupId?: string; locationId?: string; state?: boolean; sorting?: string; diff --git a/项目相关文档/6-11代码优化.md b/项目相关文档/6-11代码优化.md index d33d378..683873c 100644 --- a/项目相关文档/6-11代码优化.md +++ b/项目相关文档/6-11代码优化.md @@ -1,10 +1,17 @@ # 6-11 代码优化 -本文档说明 **2026-06-11** 对美国版 App **`POST /api/app/us-app-labeling/preview`** 出参 **`labelId`** 的格式调整。 +本文档说明 **2026-06-11** 对美国版 App 标签预览/打印相关的两项**纯后端**改造(**不改 Web / App 前端**): + +1. **`POST /api/app/us-app-labeling/preview`** 出参 **`labelId`** 改为门店当日序号 `yyyyMMdd-n` +2. 模板 **Company 自动生成元素**:预览/打印时按门店从 **`fl_partner`** 填充公司名及可选地址字段 + +测试环境:`http://flus-test.3ffoodsafety.com` --- -## 背景 +## 一、Preview 出参 `labelId` 格式 + +### 背景 预览页「Label ID」原先返回 **`fl_label.Id`**(GUID,如 `3a2192be-7b8f-e3e8-db9c-3a5e627b9222`),与业务要求不符。 @@ -18,11 +25,7 @@ 格式:`{yyyyMMdd}-{n}`(`n` 从 1 递增,按 `PrintedAt ?? CreationTime` 所在自然日、同一 `locationId` 统计)。 -测试环境:`http://flus-test.3ffoodsafety.com` - ---- - -## 接口说明 +### 接口说明 | 项目 | 内容 | |------|------| @@ -30,7 +33,7 @@ | 路径 | `/api/app/us-app-labeling/preview` | | 鉴权 | Bearer Token | -### 入参(Body:`UsAppLabelPreviewInputVo`) +#### 入参(Body:`UsAppLabelPreviewInputVo`) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| @@ -40,7 +43,7 @@ | `baseTime` | DateTime | 否 | 基准时间;影响模板内日期/时间控件,也用于确定「哪一天的序号」;未传则用服务端当前时间 | | `printInputJson` | object | 否 | 打印时输入项 | -### 出参(`UsAppLabelPreviewDto`)变更 +#### 出参(`UsAppLabelPreviewDto`)变更 | 字段 | 变更前 | 变更后 | |------|--------|--------| @@ -48,7 +51,7 @@ 其余字段(`locationId`、`labelCode`、`template`、`labelLastEdited` 等)不变。 -### `labelId` 计算规则 +#### `labelId` 计算规则 1. 取 **`baseTime ?? 当前服务器时间`** 的日期部分 `yyyyMMdd`。 2. 统计该 **`locationId`** 在当日内已有打印任务数(`fl_label_print_task`,时间取 `PrintedAt ?? CreationTime`)。 @@ -57,9 +60,7 @@ > **注意**:`labelId` 不是 `fl_label.LabelCode`,也不是 `fl_label.Id`。标签主键如需内部关联,请使用打印任务创建后的 `taskId` 或 print-log 中的 `labelEntityId`。 ---- - -## 请求示例 +### 请求示例(labelId) ```bash curl -X POST "http://flus-test.3ffoodsafety.com/api/app/us-app-labeling/preview" \ @@ -87,9 +88,7 @@ curl -X POST "http://flus-test.3ffoodsafety.com/api/app/us-app-labeling/preview" 若该门店在 `2026-05-13` 已有 2 条打印记录,预览返回 `20260513-3`;当日首条预览为 `20260513-1`。 ---- - -## 涉及代码 +### 涉及代码(labelId) | 文件 | 说明 | |------|------| @@ -98,16 +97,14 @@ curl -X POST "http://flus-test.3ffoodsafety.com/api/app/us-app-labeling/preview" | `Dtos/UsAppLabeling/UsAppLabelPreviewDto.cs` | `labelId` XML 注释 | | `IServices/IUsAppLabelingAppService.cs` | 接口注释 | ---- - -## 验证步骤 +### 验证步骤(labelId) 1. 选定门店,确认当日已有 N 条 `fl_label_print_task`(可用 print-log 列表核对)。 2. 调用 **preview**,`labelId` 应为 `{今日yyyyMMdd}-{N+1}`。 3. 执行 **print** 创建新任务后,print-log 中该任务 `labelId` 与预览时一致(同一时刻连续预览+打印)。 4. 修改 `baseTime` 为历史日期,序号应按该日任务数计算,而非「今天」。 -### SQL 抽查 +#### SQL 抽查 ```sql SELECT Id, LocationId, PrintedAt, CreationTime @@ -122,7 +119,206 @@ ORDER BY IFNULL(PrintedAt, CreationTime), Id; --- +## 二、Company 自动生成元素(预览/打印填充) + +### 背景 + +模板编辑器中的 **Company** 控件属于「按门店自动取数」类型。用户点击打印时,后端应: + +- **始终**展示当前门店所属 **Company 名称**(`fl_partner.PartnerName`) +- **仅当模板元素 config 中勾选** Address / City / State / Zip / Email 时,才追加对应行;未勾选或库中无值则不展示该字段 + +勾选状态**不落独立表**,保存在 **`fl_label_template_element.ConfigJson`**(通过 `POST/PUT /api/app/label-template` 的 `elements[].config` 写入)。公司主数据来自 **`fl_partner`**。 + +> 本次**不提供** Web 属性面板 UI;需通过 **label-template 保存接口** 或 SQL 维护 `config`。 + +### 元素识别规则 + +满足以下任一条件即视为 Company 自动生成元素(`PartnerCompanyDisplayHelper.IsCompanyAutoElement`): + +| 条件 | 说明 | +|------|------| +| `typeAdd = "auto_Company"` | 标准 Company 控件 | +| `valueSourceType = "AUTO_DB"` 且 `typeAdd` 以 `auto_` 开头且含 `Company` | 兼容命名 | + +常见组合:`elementType = "TEXT_STATIC"`,`valueSourceType = "AUTO_DB"`,`typeAdd = "auto_Company"`。 + +### 模板 config(勾选字段) + +写入 `elements[].config`(JSON),支持两种等价写法(可混用): + +**方式 A:数组 `companyIncludeFields`** + +| 数组值 | 含义 | 数据来源(`fl_partner`) | +|--------|------|--------------------------| +| `address` | 街道地址 | `Street` | +| `city` | 城市 | `City` | +| `state` | 州/省 | `StateCode` | +| `zip` | 邮编 | `ZipCode` | +| `email` | 邮箱 | `ContactEmail` | + +**方式 B:布尔开关** + +`includeAddress` / `includeCity` / `includeState` / `includeZip` / `includeEmail`(`true` 表示勾选) + +**示例(保存模板时)** + +```json +{ + "elementType": "TEXT_STATIC", + "valueSourceType": "AUTO_DB", + "typeAdd": "auto_Company", + "name": "Company", + "config": { + "companyIncludeFields": ["address", "city", "state", "zip", "email"] + } +} +``` + +或: + +```json +"config": { + "includeAddress": true, + "includeCity": true, + "includeState": true, + "includeZip": true, + "includeEmail": false +} +``` + +### 展示文本格式 + +后端将解析结果写入 **`template.elements[].config.text`**(多行,`\n` 分隔): + +| 行序 | 内容 | 规则 | +|------|------|------| +| 第 1 行 | 公司名 | **固定输出**(有 `PartnerName` 时) | +| 第 2 行 | 街道 | 仅勾选 `address` 且 `Street` 非空 | +| 第 3 行 | 城市, 州, 邮编 | 勾选 city/state/zip 中任一项时,按「City, StateCode, ZipCode」逗号拼接(空段跳过) | +| 第 4 行 | 邮箱 | 仅勾选 `email` 且 `ContactEmail` 非空 | + +**渲染示例** + +``` +Acme Foods Inc +123 Main Street +New York, NY, 10001 +sales@acme.com +``` + +若仅勾选公司名(无任何 include),则 `text` 仅一行公司名。 + +### 门店 → Company 解析 + +1. 入参 **`locationId`**(`location.Id`,Guid 字符串) +2. 查 `location` 表取 **`Partner`** 字段 +3. 在 **`fl_partner`** 中按 **`Id = Partner`** 或 **`PartnerName = Partner`** 匹配(未删除) +4. 匹配失败时不抛错,`config.text` 保持原样(不填充) + +### 入参要求 + +| 场景 | `locationId` | +|------|----------------| +| 模板**不含** Company 自动生成元素 | 可选(App preview 仍必填门店,与原有逻辑一致) | +| 模板**含** Company 自动生成元素 | **`LabelPreviewResolveInputVo.locationId` 必填**;缺失返回 **400**:`预览/打印需要 locationId 以填充 Company 信息` | + +**App 调用链**:`UsAppLabelPreviewInputVo.locationId` → 内部 `LabelAppService.PreviewAsync(LabelPreviewResolveInputVo)` 时须**原样传入** `locationId`。 + +受影响接口(均通过 `LabelAppService.PreviewAsync` 渲染模板): + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/app/us-app-labeling/preview` | App 预览 | +| POST | `/api/app/us-app-labeling/print` | App 打印(落库前解析模板) | +| POST | `/api/app/label/preview`(或管理端等价预览) | Web 预览;含 Company 元素时 Body 须带 `locationId` | + +重打 **`reprint`** 使用历史任务 `RenderTemplateJson` 快照,**不再**重新解析 Company。 + +### 出参变更 + +无新增顶层字段;变更在 **`template.elements[]`** 内: + +| 字段 | 变更 | +|------|------| +| `elements[].config.text` | Company 元素由后端按上文规则写入多行文本 | + +### 数据库与脚本 + +| 项 | 说明 | +|----|------| +| `fl_partner.PartnerName` | 公司名(必有) | +| `fl_partner.Street` / `City` / `StateCode` / `ZipCode` / `ContactEmail` | 可选展示字段 | +| `location.Partner` | 门店归属 Company 键(Id 或名称) | +| DDL | `scripts/fl_partner_add_address_columns.sql`(缺地址列时执行) | + +模板适用范围(Company/Region/Location 三维 scope)见 **`项目相关文档/6-4代码优化.md`**,与本节「元素内 Company 自动填值」相互独立。 + +### 请求示例(含 Company 的 preview) + +```bash +curl -X POST "http://flus-test.3ffoodsafety.com/api/app/us-app-labeling/preview" \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "locationId": "550e8400-e29b-41d4-a716-446655440000", + "labelCode": "LBL0001", + "productId": "PROD001" + }' +``` + +响应 `template.elements` 中 Company 元素片段示例: + +```json +{ + "elementType": "TEXT_STATIC", + "typeAdd": "auto_Company", + "valueSourceType": "AUTO_DB", + "config": { + "companyIncludeFields": ["address", "city", "state", "zip"], + "text": "Acme Foods Inc\n123 Main Street\nNew York, NY, 10001" + } +} +``` + +### 涉及代码(Company) + +| 文件 | 说明 | +|------|------| +| `Helpers/PartnerCompanyDisplayHelper.cs` | 识别元素、解析 config、格式化文本、门店→Partner | +| `Services/LabelAppService.cs` | `PreviewAsync` 填充 Company 的 `config.text` | +| `Dtos/Label/LabelPreviewResolveInputVo.cs` | 新增 `locationId` | +| `Services/UsAppLabelingAppService.cs` | preview/print 调用预览时传入 `locationId` | + +### 验证步骤(Company) + +1. 确认门店 `location.Partner` 能关联到有效 `fl_partner` 记录(地址列已执行 DDL)。 +2. 模板含 `auto_Company` 元素,`config` 勾选若干 include 字段;保存后查 `fl_label_template_element.ConfigJson`。 +3. 调用 **preview**,Body 带正确 `locationId`:`template.elements` 中 Company 的 `config.text` 行数与勾选一致。 +4. 去掉 `locationId` 或传空 → 返回 400(仅当模板含 Company 元素时)。 +5. 执行 **print**,检查 `fl_label_print_task.RenderTemplateJson` 内 Company `text` 与预览一致。 + +#### SQL 抽查 + +```sql +-- 门店归属 Company +SELECT l.Id, l.Partner, p.PartnerName, p.Street, p.City, p.StateCode, p.ZipCode, p.ContactEmail +FROM location l +LEFT JOIN fl_partner p ON (p.Id = l.Partner OR p.PartnerName = l.Partner) AND p.IsDeleted = 0 +WHERE l.Id = '{locationId}' AND l.IsDeleted = 0; + +-- 模板 Company 元素 config +SELECT e.Id, e.TypeAdd, e.ValueSourceType, e.ConfigJson +FROM fl_label_template_element e +JOIN fl_label_template t ON t.Id = e.TemplateId +WHERE t.TemplateCode = '{templateCode}' AND e.IsDeleted = 0 + AND e.TypeAdd = 'auto_Company'; +``` + +--- + ## 关联文档 - 同序号规则:`项目相关文档/6-2代码优化.md` → **App `get-print-log-list` 的 Label ID** +- 模板三维 scope:`项目相关文档/6-4代码优化.md` → **`/api/app/label-template`** - App 预览页读取字段:`labelId` / `LabelId`(`preview.vue`) diff --git a/项目相关文档/6-16代码优化.md b/项目相关文档/6-16代码优化.md new file mode 100644 index 0000000..8e3a5ae --- /dev/null +++ b/项目相关文档/6-16代码优化.md @@ -0,0 +1,139 @@ +# 6-16 代码优化 + +本文档说明 **2026-06-16** 对美国版 Web 管理端 **`GET /api/app/dashboard/overview`** 中 **Recent Labels** 区块 **Label ID** 展示规则的同步改造。 + +测试环境:`http://flus-test.3ffoodsafety.com` + +--- + +## 背景 + +Dashboard「Recent Labels」列表原先 `recentLabels[].labelCode` 返回 **`fl_label.LabelCode`**(或标签业务编码),与业务要求的 **Label ID** 含义不一致。 + +业务规则(与 App preview、Print Log、报表一致): + +**Label ID = 某门店在某个自然日内,每次打印任务按时间递增的序号** + +| 示例 | 含义 | +|------|------| +| `20260513-1` | 该门店 2026-05-13 当日第 1 次打印 | +| `20260513-2` | 同日第 2 次 | +| `20260514-1` | 次日重新从 1 计数 | + +格式:`{yyyyMMdd}-{n}`(`n` 从 1 递增;按 **`PrintedAt ?? CreationTime`** 所在自然日、同一 **`locationId`** 统计)。 + +> **注意**:`labelCode` 字段名保持不变(前端 Recent Labels 已绑定该字段展示 Label ID),**语义**由「标签编码」改为「门店当日打印序号」。不是 `fl_label.Id`,也不是 `fl_label.LabelCode`。 + +--- + +## 接口说明 + +| 项目 | 内容 | +|------|------| +| 方法 | `GET` | +| 路径 | `/api/app/dashboard/overview` | +| 鉴权 | Bearer Token(Web 管理端登录 Token) | +| 入参 | 无 | + +### 出参变更(`DashboardOverviewOutputDto.recentLabels[]`) + +| 字段 | 变更前 | 变更后 | +|------|--------|--------| +| **`labelCode`** | `fl_label.LabelCode`(标签业务编码) | 该打印任务在所属门店、打印日内的序号 **`yyyyMMdd-n`** | + +`recentLabels` 其余字段(`taskId`、`displayName`、`printedByName`、`printedAt`、`status`、`labelTypeBadge`)不变。 + +Overview 其它区块(指标卡片、周趋势、分类分布等)不受影响。 + +### `labelCode`(Label ID)计算规则 + +与 **`POST /api/app/us-app-labeling/preview`**、**`GET /api/app/reports/print-log-list`** 共用 Helper:**`ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync`**。 + +1. 取打印任务 **`PrintedAt ?? CreationTime`** 的日期部分 `yyyyMMdd`。 +2. 在同一 **`locationId`**、同一自然日内,按 `PrintedAt ?? CreationTime` 升序、`Id` 升序对所有任务排序。 +3. 第 `i` 条任务的 Label ID = **`{yyyyMMdd}-{i}`**(`i` 从 1 开始)。 +4. Recent Labels 取权限范围内**最新 10 条**打印任务;每条任务的 `labelCode` 为其在**所属门店当日序列**中的序号(非全平台统一编号)。 + +多门店场景:不同门店各自从 `-1` 计数;同一 Dashboard 列表可混合展示多个门店的记录,每条记录的 `labelCode` 仅对应该任务的 `locationId` + 打印日。 + +--- + +## 请求示例 + +```bash +curl -X GET "http://flus-test.3ffoodsafety.com/api/app/dashboard/overview" \ + -H "Authorization: Bearer {token}" +``` + +### 响应片段(`recentLabels` 示例) + +```json +{ + "recentLabels": [ + { + "taskId": "1234567890123456789", + "labelCode": "20260604-3", + "displayName": "Organic Milk 1L", + "printedByName": "Alice", + "printedAt": "2026-06-04T14:22:00", + "status": "active", + "labelTypeBadge": "2\"x2\"" + }, + { + "taskId": "1234567890123456788", + "labelCode": "20260604-2", + "displayName": "Whole Wheat Bread", + "printedByName": "Bob", + "printedAt": "2026-06-04T11:05:00", + "status": "expired", + "labelTypeBadge": "4\"x2\"" + } + ], + "generatedAt": "2026-06-04T15:00:00" +} +``` + +若某任务无法解析门店或打印时间,则 `labelCode` 为 **`无`**。 + +--- + +## 涉及代码 + +| 文件 | 说明 | +|------|------| +| `Services/DashboardAppService.cs` | `GetOverviewAsync`:`recentLabels` 查询增加 `LocationId`,调用 `ResolveDailyLabelIdsAsync` 填充 `labelCode` | +| `Dtos/Dashboard/DashboardRecentLabelItemDto.cs` | `LabelCode` XML 注释更新为当日序号语义 | +| `IServices/IDashboardAppService.cs` | 接口 `remarks` 补充 `recentLabels[].labelCode` 规则说明 | +| `Helpers/ReportsPrintLogDailyLabelIdHelper.cs` | 共用(与 6-11 preview / 6-2 print-log 一致) | + +--- + +## 验证步骤 + +1. 登录 Web 管理端,获取 Bearer Token。 +2. 选定门店,确认某自然日已有 N 条 `fl_label_print_task`(可用 print-log 或 SQL 核对)。 +3. 调用 **`GET /api/app/dashboard/overview`**,`recentLabels` 中对应任务的 **`labelCode`** 应与 print-log 列表中同一 `taskId` 的 Label ID 一致。 +4. 跨日打印:次日首条应为 `{新日期}-1`。 +5. 多门店:两门店同日各打印 1 次,两条记录的 `labelCode` 末尾序号均可为 `-1`(各自门店独立计数)。 + +### SQL 抽查 + +```sql +SELECT Id, LocationId, PrintedAt, CreationTime, + IFNULL(PrintedAt, CreationTime) AS EffectiveTime +FROM fl_label_print_task +WHERE LocationId = '{locationId}' + AND IFNULL(PrintedAt, CreationTime) >= '{yyyy-MM-dd}' + AND IFNULL(PrintedAt, CreationTime) < DATE_ADD('{yyyy-MM-dd}', INTERVAL 1 DAY) +ORDER BY EffectiveTime, Id; +``` + +按行号 `1..N` 对应 `labelCode` 的 `-n` 部分;日期部分为 `yyyyMMdd`。 + +--- + +## 关联文档 + +- Label ID 通用规则与 preview:`项目相关文档/6-11代码优化.md` +- App print-log Label ID:`项目相关文档/6-2代码优化.md` +- Dashboard 数据范围(管理员 / 门店绑定):`DashboardScopeHelper` 及接口 `IDashboardAppService` 注释 diff --git a/项目相关文档/6-17代码优化.md b/项目相关文档/6-17代码优化.md new file mode 100644 index 0000000..337ab5a --- /dev/null +++ b/项目相关文档/6-17代码优化.md @@ -0,0 +1,336 @@ +# 6-17 代码优化 + +本文档说明 **2026-06-17** 对美国版的两项后端改造: + +1. **`GET /api/app/label-template`** 列表 500 修复(scope 库结构兼容) +2. **`POST /api/app/us-app-labeling/get-print-log-list`** 增加 **按自然日** 的打印时间筛选 + +测试环境:`http://flus-test.3ffoodsafety.com` + +--- + +## 一、label-template 列表修复 + +Web 管理端 **Label Templates** 页加载失败,Network 中: + +`GET /api/app/label-template?SkipCount=1&MaxResultCount=10` 返回错误,页面提示 **Failed to load label templates**。 + +### 根因 + +6-4 改造引入 **Company / Region / Location** 三维适用范围后,代码会: + +1. 查询 `fl_label_template.AppliedPartnerType` / `AppliedRegionType` +2. 关联 `fl_label_template_partner` / `fl_label_template_region` + +测试库若**尚未执行**迁移脚本 `fl_label_template_scope.sql`,上述列/表不存在,SqlSugar 生成 SQL 时报 **Unknown column / Table doesn't exist**,接口 500。 + +> `SkipCount=1` 表示**第 1 页**(页码从 1 起),不是报错原因。 + +--- + +### 修复说明 + +#### 1. 库结构兼容(未迁移仍可列表) + +| 改动 | 说明 | +|------|------| +| `LabelTemplateScopeSchemaHelper` | 启动后首次请求检测 partner/region 关联表是否存在 | +| `FlLabelTemplateDbEntity` | `AppliedPartnerType` / `AppliedRegionType` 标记 `IsIgnore`,避免 SELECT 不存在列 | +| `LabelTemplateScopeHelper` | 未迁移时列表/详情 **仅 Location 维度** 过滤与展示;Company/Region 显示 `All Companies` / `All Regions` | +| 已迁移库 | 行为与 6-4 一致,按关联表判断 ALL/SPECIFIED | + +#### 2. 列表筛选补全 + +`LabelTemplateGetListInputVo` 增加 **`partnerId`**(Company),与 `groupId`、`locationId` 按「门店优先」解析(`ResolveFilteredLocationIdsForListAsync`)。 + +#### 3. Items 列 + +`LabelTemplateListItemsHelper` 补全 `ElementKey` 查询字段,控件名称排序稳定。 + +--- + +### 接口说明(label-template) + +| 项目 | 内容 | +|------|------| +| 方法 | `GET` | +| 路径 | `/api/app/label-template` | +| 鉴权 | Bearer Token | + +### 入参(Query) + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `SkipCount` | int | 否 | **页码,从 1 起**;第一页传 `1`(与全平台列表约定一致) | +| `MaxResultCount` | int | 否 | 每页条数,默认 10 | +| `Keyword` | string | 否 | 模板名称 / 编码模糊搜索 | +| `PartnerId` | string | 否 | 按 Company(`fl_partner.Id`)筛选 | +| `GroupId` | string | 否 | 按 Region(`fl_group.Id`)筛选 | +| `LocationId` | string | 否 | 按门店(`location.Id`)筛选;**优先于** Partner/Region | +| `LabelType` | string | 否 | 如 `PRICE` / `NUTRITION` | +| `State` | bool | 否 | 启用状态 | +| `Sorting` | string | 否 | 排序字段 | + +筛选解析顺序:**LocationId → GroupId → PartnerId**;均未传则不按门店范围收窄(仍受登录账号数据权限约束)。 + +### 出参(`PagedResultWithPageDto`) + +| 字段 | 说明 | +|------|------| +| `pageIndex` | 当前页码 | +| `pageSize` | 每页条数 | +| `totalCount` | 总条数 | +| `totalPages` | 总页数 | +| `items[]` | 模板列表 | + +**`items[]` 主要字段** + +| 字段 | 说明 | +|------|------| +| `id` / `templateCode` | 模板编码(前端主键) | +| `templateName` | 模板名称 | +| `company` / `region` / `location` | 三维适用范围展示文案 | +| `partnerIds` / `regionIds` / `locationIds` | 对应 Id 数组(ALL 时为空) | +| `items` / `itemNames` | 模板内控件名称(逗号拼接 / 数组) | +| `contentsCount` | 控件数量 | +| `sizeText` | 如 `4x2inch` | +| `lastEdited` | 最近编辑时间 | + +--- + +### 请求示例(label-template) + +```bash +curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \ + -H "Authorization: Bearer {token}" \ + --data-urlencode "SkipCount=1" \ + --data-urlencode "MaxResultCount=10" +``` + +带筛选: + +```bash +curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \ + -H "Authorization: Bearer {token}" \ + --data-urlencode "SkipCount=1" \ + --data-urlencode "MaxResultCount=10" \ + --data-urlencode "PartnerId=1234567890123456789" \ + --data-urlencode "Keyword=Price" +``` + +### 响应片段(示例) + +```json +{ + "pageIndex": 1, + "pageSize": 10, + "totalCount": 2, + "totalPages": 1, + "items": [ + { + "id": "TPL-PRICE-01", + "templateCode": "TPL-PRICE-01", + "templateName": "Standard Price Label", + "company": "All Companies", + "region": "All Regions", + "location": "All Locations", + "items": "Label Name, Price, Barcode", + "itemNames": ["Label Name", "Price", "Barcode"], + "contentsCount": 3, + "sizeText": "2x2inch", + "lastEdited": "2026-06-04T10:00:00" + } + ] +} +``` + +--- + +## 数据库迁移(启用完整三维 scope) + +未执行前:列表可正常返回,但 **无法** 保存 Company/Region 多选明细。 + +**脚本(可重复执行)**: + +`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_template_scope.sql` + +| 对象 | 说明 | +|------|------| +| `fl_label_template.AppliedPartnerType` | Company:ALL / SPECIFIED | +| `fl_label_template.AppliedRegionType` | Region:ALL / SPECIFIED | +| `fl_label_template_partner` | Company 多选明细 | +| `fl_label_template_region` | Region 多选明细 | + +执行后重启应用,新建/编辑模板即可写入 Company/Region 范围。 + +### SQL 检查是否已迁移 + +```sql +SELECT COLUMN_NAME +FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'fl_label_template' + AND COLUMN_NAME IN ('AppliedPartnerType', 'AppliedRegionType'); + +SELECT TABLE_NAME +FROM information_schema.TABLES +WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME IN ('fl_label_template_partner', 'fl_label_template_region'); +``` + +--- + +### 涉及代码(label-template) + +| 文件 | 说明 | +|------|------| +| `Helpers/LabelTemplateScopeSchemaHelper.cs` | 检测 scope 表是否已迁移 | +| `Helpers/LabelTemplateScopeHelper.cs` | 兼容未迁移库的过滤/展示/保存 | +| `Helpers/LabelTemplateListItemsHelper.cs` | Items 列 ElementKey | +| `Services/DbModels/FlLabelTemplateDbEntity.cs` | Partner/Region 列 IsIgnore | +| `Services/LabelTemplateAppService.cs` | 列表筛选支持 `partnerId` | +| `Dtos/LabelTemplate/LabelTemplateGetListInputVo.cs` | 新增 `PartnerId` | +| `IServices/ILabelTemplateAppService.cs` | 接口注释 | + +### 验证步骤(label-template) + +1. **未迁移库**:仅执行后端部署,调用 `GET /api/app/label-template?SkipCount=1&MaxResultCount=10` 应 **200**,Web 列表可加载。 +2. **已迁移库**:列表 `company`/`region` 展示与 6-4 一致;指定 Company 保存后再查列表 Id 数组正确。 +3. 分页:第 1 页 `SkipCount=1`,第 2 页 `SkipCount=2`,`totalCount` 与 UI 分页一致。 +4. 筛选:传 `LocationId` 后仅返回该门店可见模板 + 全范围模板。 + +--- + +## 二、App Print Log 按日筛选(get-print-log-list) + +### 背景 + +App **Print Log** 页增加 **Date** 选择器(及 **Today** 快捷按钮),需按所选自然日只展示该门店当日的打印记录;无记录时提示如 `No print records for 06/16/2026`。 + +改造前接口仅按门店 + 权限分页,**无日期条件**,无法与 UI 日期筛选对齐。 + +### 接口说明 + +| 项目 | 内容 | +|------|------| +| 方法 | `POST` | +| 路径 | `/api/app/us-app-labeling/get-print-log-list` | +| 鉴权 | App Bearer Token | + +### 入参(Body:`PrintLogGetListInputVo`) + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `locationId` | string | 是 | 当前门店 Id(`location.Id`) | +| **`printDate`** | DateTime | 否 | **打印日期(自然日)**;按 `PrintedAt ?? CreationTime` 落在该日的任务筛选 | +| `skipCount` | int | 否 | 页码,从 **1** 起 | +| `maxResultCount` | int | 否 | 每页条数 | + +#### `printDate` 规则 + +1. **未传** → 默认 **服务器当天**(与 App 默认选中 Today 一致)。 +2. **传入任意时刻** → 仅取 **日期部分**(`Date`),区间为 `[当天 00:00:00, 次日 00:00:00)`。 +3. 有效时间字段:`fl_label_print_task.PrintedAt` 优先,为空则用 `CreationTime`(与 Label ID 序号、`6-2` / `6-11` 一致)。 +4. 权限不变:管理员 / Partner 看门店全部;其它角色仅本人(`CreatedBy`)。 + +### 出参 + +仍为 `PagedResultWithPageDto`;`items[]` 字段不变。`totalCount` 为**该日**符合条件的总数。 + +| 字段 | 说明 | +|------|------| +| `labelId` | 门店当日打印序号 `yyyyMMdd-n`(与 preview / 报表一致) | +| `printedAt` | 打印时间 | +| 其它 | 与改造前一致 | + +### 请求示例 + +**查询当天(显式传 printDate)** + +```bash +curl -X POST "http://flus-test.3ffoodsafety.com/api/app/us-app-labeling/get-print-log-list" \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "locationId": "550e8400-e29b-41d4-a716-446655440000", + "printDate": "2026-06-16T00:00:00", + "skipCount": 1, + "maxResultCount": 20 + }' +``` + +**Today 按钮(不传 printDate,后端默认当天)** + +```json +{ + "locationId": "550e8400-e29b-41d4-a716-446655440000", + "skipCount": 1, + "maxResultCount": 20 +} +``` + +### 响应片段(示例) + +```json +{ + "pageIndex": 1, + "pageSize": 20, + "totalCount": 2, + "totalPages": 1, + "items": [ + { + "taskId": "1234567890123456789", + "labelId": "20260616-1", + "productName": "Organic Milk", + "printedAt": "2026-06-16T09:15:00", + "operatorName": "Alice", + "locationName": "Ordos Airport" + }, + { + "taskId": "1234567890123456788", + "labelId": "20260616-2", + "printedAt": "2026-06-16T14:30:00" + } + ] +} +``` + +若 `totalCount = 0`,App 可展示 `No print records for MM/DD/YYYY`。 + +### 涉及代码(get-print-log-list) + +| 文件 | 说明 | +|------|------| +| `Dtos/UsAppLabeling/PrintLogGetListInputVo.cs` | 新增 `PrintDate` | +| `Services/UsAppLabelingAppService.cs` | `GetPrintLogListAsync` 增加日期 Where | +| `Helpers/ReportsPrintLogDailyLabelIdHelper.cs` | 新增 `ResolvePrintLogFilterDayRange` | +| `IServices/IUsAppLabelingAppService.cs` | 接口注释 | + +### 验证步骤(get-print-log-list) + +1. 门店在 `2026-06-16` 有 N 条打印任务,传 `printDate=2026-06-16` → `totalCount=N`,且每条 `printedAt` 落在该日。 +2. 传 `printDate=2026-06-15` 且无记录 → `totalCount=0`,`items=[]`。 +3. 不传 `printDate` → 等价于查询**服务器当天**。 +4. 同一 `printDate` 下 `labelId` 序号与 `6-2` 规则一致(`-1`、`-2` … 按当日时间升序)。 + +#### SQL 抽查 + +```sql +SELECT Id, LocationId, PrintedAt, CreationTime +FROM fl_label_print_task +WHERE LocationId = '{locationId}' + AND IFNULL(PrintedAt, CreationTime) >= '2026-06-16' + AND IFNULL(PrintedAt, CreationTime) < '2026-06-17' +ORDER BY IFNULL(PrintedAt, CreationTime), Id; +``` + +行数应与接口 `totalCount`(该日、该门店、权限范围内)一致。 + +--- + +## 关联文档 + +- 三维 scope 业务规则:`项目相关文档/6-4代码优化.md` +- App Print Log Label ID:`项目相关文档/6-2代码优化.md` +- Preview 当日序号:`项目相关文档/6-11代码优化.md` +- 分页约定(SkipCount = 页码):`Helpers/PagedQueryConvention.cs` diff --git a/项目相关文档/6-18代码优化.md b/项目相关文档/6-18代码优化.md new file mode 100644 index 0000000..9f5fe00 --- /dev/null +++ b/项目相关文档/6-18代码优化.md @@ -0,0 +1,650 @@ +# 6-18 代码优化 + +本文档说明 **2026-06-18** 对 **`GET /api/app/label-template`** 列表接口的第二轮修复。 + +6-17 已完成 scope 库结构兼容(未迁移 `fl_label_template_partner` / `fl_label_template_region` 时仍可查询),但测试环境 Web **Label Templates** 页仍返回 **500**,Network 中: + +`GET /api/app/label-template?SkipCount=1&MaxResultCount=10` + +测试环境:`http://flus-test.3ffoodsafety.com` + +--- + +## 一、现象 + +- 页面提示:**Failed to load label templates. Request failed.** +- 接口 HTTP **500**,响应体 `errors` 为空,无具体异常文案。 +- 同页 `partner` / `group` / `location` 下拉接口可正常加载。 + +> `SkipCount=1` 表示**第 1 页**(页码从 1 起),不是报错原因。详见 `Helpers/PagedQueryConvention.cs`。 + +--- + +## 二、根因 + +服务端日志已明确报错: + +```text +Unknown column 'AppliedPartnerType' in 'field list' +``` + +对应 SQL: + +```sql +SELECT ... `AppliedLocationType`,`AppliedPartnerType`,`AppliedRegionType`, ... +FROM `fl_label_template` +WHERE NOT ( `IsDeleted`=1 ) +ORDER BY IFNULL(`LastModificationTime`,`CreationTime`) DESC +LIMIT 0,10 +``` + +### 2.1 主因:ORM 实体仍映射不存在列 + +6-17 曾在 `FlLabelTemplateDbEntity` 上对 `AppliedPartnerType` / `AppliedRegionType` 标记 `[SugarColumn(IsIgnore = true)]`,但当前 SqlSugar 运行时**仍会**把这两列拼进 `SELECT`(与 `fl_label.AppliedRegionType` 的 `IsIgnore` 行为不一致),导致未执行 `fl_label_template_scope.sql` 的库直接 500。 + +**MCP 查库(测试库)**: + +- `fl_label_template` **无** `AppliedPartnerType` / `AppliedRegionType` 列 +- **无** `fl_label_template_partner` / `fl_label_template_region` 表 +- **有** `AppliedLocationType` 与 `fl_label_template_location` + +### 2.2 次因:列表 SQL 其它隐患(一并修复) + +| # | 问题 | 后果 | +|---|------|------| +| 1 | 默认排序曾用 `LastModificationTime ?? CreationTime` | SqlSugar 翻译失败 → 500 | +| 2 | `Sorting` 直接 `OrderBy(input.Sorting)` 拼 SQL | 非法字段 / 注入风险 | +| 3 | 空 `templateIds` 仍 `Contains` 查询 | 可能生成 `IN ()` | +| 4 | 无效 partner/group 筛选未安全降级 | 未捕获异常 | + +--- + +## 三、修复说明 + +### 1. 从 ORM 实体移除不存在列 + 强制列投影(核心) + +| 改动 | 说明 | +|------|------| +| `FlLabelTemplateDbEntity` | **删除** `AppliedPartnerType` / `AppliedRegionType` 属性 | +| **`LabelTemplateQueryHelper.ProjectListColumns`** | 列表/详情/重复校验等只读查询 **显式 Select** 真实列,SQL 不再出现 scope 列 | +| `LabelTemplateScopeSchemaHelper` | 探测列/表;已迁移时用 raw SQL 写入 scope 列 | +| `LabelTemplateAppService` | `GetListAsync` 在排序后调用 `ProjectListColumns` | + +> **重要**:若 Yi-SQL 日志仍出现 `AppliedPartnerType`,说明进程加载的是**旧 DLL**(`FoodLabeling.Application` 未重新编译或未重启)。请先 `dotnet build` 通过后再**完全停止并重启** `Yi.Abp.Web`。 + +### 2. 安全排序 + +新增 `ApplyLabelTemplateListSorting`: + +- 默认:`ORDER BY IFNULL(LastModificationTime, CreationTime) DESC, TemplateCode ASC` +- `Sorting` 白名单字段 + asc/desc + +### 3. 列表查询健壮性 + +| 改动 | 说明 | +|------|------| +| `input ??= new()`、`pageSize` 默认 10 | 入参/分页兜底 | +| `templateIds.Count > 0` 再查 elements/items | 避免空 `IN ()` | +| `ResolveFilteredLocationIdsForListAsync` | 无效 partner/group 返回空列表 | +| `DeleteAsync` | 仅当 scope 关联表存在时才删除 partner/region 行 | + +### 4. 与 6-17 的关系 + +6-17 用 `IsIgnore` 试图跳过列映射,**实测无效**;6-18 改为**实体不含列 + raw SQL 按需写入**,与 `fl_label.AppliedRegionType` 处理方式一致。 + +--- + +## 四、接口说明 + +| 项目 | 内容 | +|------|------| +| 方法 | `GET` | +| 路径 | `/api/app/label-template` | +| 鉴权 | Bearer Token(`Authorization: {data.token}`,`data.token` 已含 `Bearer ` 前缀) | + +### 入参(Query) + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `SkipCount` | int | 否 | **页码,从 1 起**;第一页传 `1` | +| `MaxResultCount` | int | 否 | 每页条数;`<=0` 时后端按 **10** 处理 | +| `Keyword` | string | 否 | 模板名称 / 编码模糊搜索 | +| `PartnerId` | string | 否 | 按 Company(`fl_partner.Id`)筛选 | +| `GroupId` | string | 否 | 按 Region(`fl_group.Id`)筛选 | +| `LocationId` | string | 否 | 按门店(`location.Id`)筛选;**优先于** Partner/Region | +| `LabelType` | string | 否 | 如 `PRICE` / `NUTRITION` | +| `State` | bool | 否 | 启用状态 | +| `Sorting` | string | 否 | 白名单:`TemplateName asc/desc`、`TemplateCode asc/desc`、`CreationTime asc/desc`、`LastModificationTime asc/desc`;其它值忽略并走默认排序 | + +筛选解析顺序:**LocationId → GroupId → PartnerId**;均未传则不按门店范围收窄(仍返回全部未删除模板,除非前端传了无效 Id 则返回空列表)。 + +### 出参(`PagedResultWithPageDto`) + +| 字段 | 说明 | +|------|------| +| `pageIndex` | 当前页码 | +| `pageSize` | 每页条数 | +| `totalCount` | 总条数 | +| `totalPages` | 总页数 | +| `items[]` | 模板列表 | + +**`items[]` 主要字段** + +| 字段 | 说明 | +|------|------| +| `id` / `templateCode` | 模板编码(前端主键) | +| `templateName` | 模板名称 | +| `company` / `region` / `location` | 适用范围展示;未迁移 scope 库时 Company/Region 为 `All Companies` / `All Regions` | +| `partnerIds` / `regionIds` / `locationIds` | 对应 Id 数组 | +| `items` / `itemNames` | 模板内控件名称 | +| `contentsCount` | 控件数量 | +| `sizeText` | 如 `2x2inch` | +| `lastEdited` | 最近编辑时间(`LastModificationTime ?? CreationTime`) | + +--- + +## 五、请求示例 + +### 登录获取 Token + +```bash +curl -X POST "http://flus-test.3ffoodsafety.com/api/oauth/Login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "userName=admin&password=123456" +``` + +### 列表(第一页,默认排序) + +```bash +curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \ + -H "Authorization: Bearer {token}" \ + --data-urlencode "SkipCount=1" \ + --data-urlencode "MaxResultCount=10" +``` + +### 带 Company 筛选 + +```bash +curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \ + -H "Authorization: Bearer {token}" \ + --data-urlencode "SkipCount=1" \ + --data-urlencode "MaxResultCount=10" \ + --data-urlencode "PartnerId={fl_partner.Id}" +``` + +### 指定排序 + +```bash +curl -G "http://flus-test.3ffoodsafety.com/api/app/label-template" \ + -H "Authorization: Bearer {token}" \ + --data-urlencode "SkipCount=1" \ + --data-urlencode "MaxResultCount=10" \ + --data-urlencode "Sorting=TemplateName asc" +``` + +### 响应片段(示例) + +```json +{ + "pageIndex": 1, + "pageSize": 10, + "totalCount": 3, + "totalPages": 1, + "items": [ + { + "id": "tpl_n0b5h9_mpyssitm", + "templateCode": "tpl_n0b5h9_mpyssitm", + "templateName": "Retail Label w/Price Copy", + "company": "All Companies", + "region": "All Regions", + "location": "Ordos Airport, Store B", + "items": "Label Name, Price, Barcode", + "contentsCount": 5, + "sizeText": "2x2inch", + "lastEdited": "2026-06-04T08:30:00" + } + ] +} +``` + +--- + +## 六、验证步骤 + +0. **重新编译并重启**(必做): + ```bash + cd "美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web" + dotnet build + ``` + 停止正在运行的 Web 进程后重新启动;确认 Yi-SQL 中 **不再出现** `AppliedPartnerType`。 + +1. **部署**包含 6-17 + 6-18 的后端并重启。 +2. 调用 `GET /api/app/label-template?SkipCount=1&MaxResultCount=10` → 应 **200**,`totalCount >= 0`。 +3. Web **Label Templates** 列表可加载,不再出现红色 **Failed to load label templates**。 +4. 传无效 `PartnerId` / `GroupId` → **200** 且 `items=[]`(非 500)。 +5. 不传 `Sorting` → 按最近编辑时间降序;传 `Sorting=TemplateName asc` → 按名称升序。 +6. 分页:第 2 页 `SkipCount=2`,`totalCount` 与 UI 一致。 + +### SQL 抽查(修复后 ORM 应生成的列) + +```sql +SELECT Id, TemplateCode, TemplateName, AppliedLocationType, + IFNULL(LastModificationTime, CreationTime) AS LastEdited +FROM fl_label_template +WHERE IsDeleted = 0 +ORDER BY IFNULL(LastModificationTime, CreationTime) DESC, TemplateCode ASC +LIMIT 10; +``` + +**不应再出现** `AppliedPartnerType` / `AppliedRegionType`。 + +### 检查 scope 是否已迁移 + +```sql +SELECT COUNT(*) AS scope_table_cnt +FROM information_schema.TABLES +WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME IN ('fl_label_template_partner', 'fl_label_template_region'); +``` + +`scope_table_cnt = 0` 时行为与 6-17 一致:仅 Location 维度落库与展示。 + +--- + +## 七、涉及代码 + +| 文件 | 说明 | +|------|------| +| `Helpers/LabelTemplateQueryHelper.cs` | **新增** `ProjectListColumns` 强制 SQL 列白名单 | +| `Services/DbModels/FlLabelTemplateDbEntity.cs` | **移除** Partner/Region 列属性 | +| `Helpers/LabelTemplateScopeSchemaHelper.cs` | 列/表探测 + `SetAppliedScopeTypesAsync` raw SQL | +| `Services/LabelTemplateAppService.cs` | 安全排序、Create/Update 写 scope 列、列表健壮性 | +| `Helpers/LabelTemplateScopeHelper.cs` | 未迁移库 scope 过滤/展示(6-17) | +| `Helpers/LocationScopeBindingHelper.cs` | `ResolveFilteredLocationIdsForListAsync` | +| `Helpers/LabelTemplateListItemsHelper.cs` | Items 列 | +| `Dtos/LabelTemplate/LabelTemplateGetListInputVo.cs` | `PartnerId` 筛选 | + +--- + +## 八、数据库迁移(可选) + +需完整 Company / Region 三维 scope 时,执行: + +`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_template_scope.sql` + +未执行前:**列表可正常返回**(6-17 + 6-18),但无法持久化 Company/Region 多选明细。 + +--- + +## 九、App 标签预览 `POST /api/app/us-app-labeling/preview` + +### 现象 + +App 标签预览页调用 preview 失败(500 或 400),常与 **label-template 列表** 同源:加载模板头时 ORM 仍 SELECT 不存在的 `AppliedPartnerType` / `AppliedRegionType`。 + +另有两项逻辑缺陷(与 `6-11` 文档不一致): + +| # | 问题 | 后果 | +|---|------|------| +| 1 | `LabelAppService.PreviewAsync` 全表查询 `FlLabelTemplateDbEntity` | 未迁移 scope 列时 **Unknown column** → 500 | +| 2 | `UsAppLabelingAppService.PreviewAsync` 未向 `_labelAppService.PreviewAsync` 传 **`locationId`** | 模板含 Company 自动生成元素时报 **「预览/打印需要 locationId 以填充 Company 信息」** | +| 3 | 出参 **`labelId`** 仍返回 `fl_label.Id`(GUID) | 与 Print Log 当日序号 `yyyyMMdd-n` 不一致 | + +### 修复说明 + +| 改动 | 说明 | +|------|------| +| `LabelAppService.PreviewAsync` | 模板头查询改用 `LabelTemplateQueryHelper.ProjectListColumns` | +| `UsAppLabelingAppService.PreviewAsync` | 传入 `LocationId`;`labelId` 改用 `ReportsPrintLogDailyLabelIdHelper.ResolveNextDailyLabelIdAsync` | +| `UsAppLabelingAppService.PrintAsync` | 解析模板时同步传入 `LocationId`(打印与预览 Company 填充一致) | +| `DashboardAppService` | 模板统计 Count 同样走 `ProjectListColumns` | + +### 接口说明 + +| 项目 | 内容 | +|------|------| +| 方法 | `POST` | +| 路径 | `/api/app/us-app-labeling/preview` | +| 鉴权 | App Bearer Token | + +#### 入参(Body:`UsAppLabelPreviewInputVo`) + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `locationId` | string | 是 | 当前门店 Id(`location.Id`) | +| `labelCode` | string | 是 | 标签编码(`fl_label.LabelCode`) | +| `productId` | string | 否 | 预览产品 Id;不传则取标签绑定第一个产品 | +| `baseTime` | DateTime | 否 | 日期/时间控件基准;也用于计算当日 `labelId` 序号;未传为服务器当前时间 | +| `printInputJson` | object | 否 | `PRINT_INPUT` 元素用户输入 | + +#### 出参(`UsAppLabelPreviewDto`) + +| 字段 | 说明 | +|------|------| +| **`labelId`** | 门店当日**下一个**打印序号 `yyyyMMdd-n`(预览不落库) | +| `locationId` / `labelCode` | 回传入参 | +| `template` | 已解析 AUTO_DB / PRINT_INPUT 的模板结构(含 Company 自动填充) | +| `labelLastEdited` | 标签最近编辑时间 | +| 其它 | `typeName`、`productName`、`templateProductDefaultValues` 等 | + +#### `labelId` 规则 + +与 `6-11`、`get-print-log-list` 一致:`{baseTime 日期 yyyyMMdd}-{当日已有打印任务数 + 1}`。 + +#### Company 自动元素 + +模板含 Company 自动生成控件时,后端根据 **`locationId`** 查 `fl_partner` 填充 `config.text`(详见 `6-11` 第二节)。 + +### 请求示例 + +```bash +curl -X POST "http://flus-test.3ffoodsafety.com/api/app/us-app-labeling/preview" \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "locationId": "550e8400-e29b-41d4-a716-446655440000", + "labelCode": "LBL0001", + "productId": "PROD001", + "baseTime": "2026-06-18T09:00:00" + }' +``` + +### 响应片段(示例) + +```json +{ + "labelId": "20260618-3", + "locationId": "550e8400-e29b-41d4-a716-446655440000", + "labelCode": "LBL0001", + "labelLastEdited": "2026-06-01T08:30:09", + "template": { + "id": "tpl_xxx", + "width": 2, + "height": 2, + "unit": "inch", + "elements": [] + } +} +``` + +### 验证步骤 + +1. 停服 → `dotnet build` → 重启(同 label-template 一节)。 +2. App 进入标签预览页,Network 中 preview 应 **200**。 +3. 响应 `labelId` 为 `yyyyMMdd-n`,非 GUID。 +4. 模板含 Company 元素时,`template.elements` 中对应 `config.text` 为门店所属公司名。 +5. 当日已有 N 条打印任务时,preview 返回 `-{N+1}`。 + +### 涉及代码(preview) + +| 文件 | 说明 | +|------|------| +| `Services/UsAppLabelingAppService.cs` | `PreviewAsync` / `PrintAsync` 传 `LocationId`、当日 `labelId` | +| `Services/LabelAppService.cs` | `PreviewAsync` 模板头 `ProjectListColumns` | +| `Helpers/LabelTemplateQueryHelper.cs` | 列投影 | +| `Helpers/ReportsPrintLogDailyLabelIdHelper.cs` | `ResolveNextDailyLabelIdAsync` | +| `Helpers/PartnerCompanyDisplayHelper.cs` | Company 自动填充 | + +--- + +## 十、App 打印日志 `POST /api/app/us-app-labeling/get-print-log-list` + +### 现象 + +Postman 传 `printDate: "2026-06-16"` 返回 `totalCount: 0`,误以为接口异常。 + +### 根因(查库核对) + +门店 `3a218397-8dda-a378-e024-ef89bcef8d24` 在测试库中: + +| 自然日(`DATE(IFNULL(PrintedAt, CreationTime))`) | 记录数 | +|---------------------------------------------------|--------| +| `2026-06-01` | **7** | +| `2026-05-31` | **3** | +| `2026-06-16` | **0** | + +**接口按入参日期筛选时,该日无数据则返回空列表,行为正确。** 文档/示例误用 `2026-06-16`,应改用 **`2026-06-01`** 验证。 + +另:`PrintedAt` 入库多为 `null`,筛选实际走 **`CreationTime`**。 + +### 本轮修复 + +| 改动 | 说明 | +|------|------| +| 日期条件 | 改用 MySQL `DATE(IFNULL(t.PrintedAt, t.CreationTime)) = 'yyyy-MM-dd'`,避免 DateTime 区间比较时区偏差 | +| `printDateDay` | 新增字符串入参(`yyyy-MM-dd`),优先于 `printDate`,避免 JSON 仅日期 UTC 歧义 | +| 未传日期 | **`printDate` 与 `printDateDay` 均未传时不按日过滤**(返回该门店全部打印记录,兼容 App 未传参) | +| `labelId` | 列表出参改为当日序号 `yyyyMMdd-n`;`labelEntityId` 为 `fl_label.Id` | + +### 请求示例(有数据的日期) + +```bash +curl -X POST "http://192.168.1.4:19001/api/app/us-app-labeling/get-print-log-list" \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "locationId": "3a218397-8dda-a378-e024-ef89bcef8d24", + "skipCount": 1, + "maxResultCount": 20, + "printDateDay": "2026-06-01" + }' +``` + +或使用 `"printDate": "2026-06-01"`(等价)。 + +### 权限说明 + +非 admin / 非 Partner 角色时,仅返回 **`CreatedBy = 当前用户`** 的记录;若 Token 用户不是打印人,即使日期正确也会空列表。 + +### 部署 + +修改在 `FoodLabeling.Application`,需 **停服 → dotnet build → 重启** 后 Postman 才生效。 + +### 涉及代码 + +| 文件 | 说明 | +|------|------| +| `Helpers/ReportsPrintLogDailyLabelIdHelper.cs` | `ResolvePrintLogFilterCalendarDay`、`ApplyPrintTaskCalendarDayFilter` | +| `Dtos/UsAppLabeling/PrintLogGetListInputVo.cs` | `PrintDateDay` | +| `Services/UsAppLabelingAppService.cs` | `GetPrintLogListAsync` | + +--- + +## 十一、角色编辑 `PUT /api/app/rbac-role/{id}` — accessPermissions + +### 现象 + +编辑角色传 `"accessPermissions": "[\"manage_labels\"]"` 后权限未生效或菜单绑定被清空。 + +### 根因 + +前端/Web 约定 `accessPermissions` 为 **JSON 数组字符串**(见 `rbacRoleService.ts` 的 `JSON.stringify(codes)`),后端 `ParseAccessPermissionCodes` 原先仅按逗号拆分,整段 `["manage_labels"]` 被当成一个非法 token,`NormalizeAccessPermissionCodes` 过滤后为空 → **RoleMenu 被清空**。 + +### 修复 + +`RbacAccessPermissionHelper.ParseAccessPermissionCodes`:若字符串以 `[` 开头,先 `JsonSerializer.Deserialize>`,再回退逗号分隔。 + +### 合法 accessPermissions 值 + +| 编码 | 说明 | +|------|------| +| `manage_labels` | 标签相关菜单(/labeling、/labels 等) | +| `manage_people` | 账号管理 | +| `edit_settings` | 菜单/多选项设置 | +| `view_reports` | 报表 | +| `manage_products` / `approve_batches` | 当前无独立菜单,不阻断其它权限 | + +### 请求示例 + +```json +{ + "roleName": "Staff", + "roleCode": "Staff", + "remark": null, + "dataScope": 0, + "state": true, + "orderNum": 0, + "accessPermissions": "[\"manage_labels\"]" +} +``` + +亦可传 `"accessPermissionCodes": ["manage_labels"]`(与 `accessPermissions` 合并去重)。 + +### 涉及代码 + +| 文件 | 说明 | +|------|------| +| `Helpers/RbacAccessPermissionHelper.cs` | JSON 数组解析 | +| `Helpers/RoleAccessPermissionMenuMapping.cs` | UI 权限码 → Menu.Router | +| `Services/RbacRoleAppService.cs` | `UpdateAsync` / `ApplyRoleMenuBindingsAsync` | + +--- + +## 十二、Team Member — Company Admin 范围绑定 + +### 规则 + +角色为 **Company Admin**(库内常为 **Partner Admin**,或 RoleCode 含 `partner`)时: + +| 字段 | 是否必填 | 说明 | +|------|----------|------| +| `partnerId` / `partnerIds` | **是** | 指定适用 Company | +| `regionIds` | **否** | 未传时后端自动绑定该公司下**全部 Region**(出参展示) | +| `locationIds` / `assignedLocations` | **否** | 未传时后端自动绑定该公司下**全部门店** 并写入 `userlocation` | + +其它角色仍须 Company / Region / 门店至少一项能解析出门店。 + +### 请求示例(Create / Update) + +```json +{ + "fullName": "Jane Doe", + "userName": "jane.doe", + "password": "123456", + "email": "jane@example.com", + "roleId": "{Partner Admin 角色 Guid}", + "partnerId": "{fl_partner.Id}", + "state": true +} +``` + +无需传 `regionIds`、`locationIds`。 + +### 列表出参 + +`GET /api/app/team-member` 对 Company Admin 成员: + +- `roleName` 统一为 **Company Admin**(库内 Partner Admin 也会转换) +- `regionIds` 展示该公司下**全部 Region** +- `assignedLocations` 展示该公司下**全部门店**(无需前端再选手动 Region/Locations) + +### 编辑弹窗(Web) + +选 **Company Admin** 时隐藏 Region / Locations 字段,仅保留 **Company**;保存时只传 `partnerId`,后端自动展开绑定。 + +### 涉及代码 + +| 文件 | 说明 | +|------|------| +| `Helpers/TeamMemberRoleHelper.cs` | Company Admin 角色识别 | +| `Helpers/LocationScopeBindingHelper.cs` | `ResolveGroupIdsFromPartnerIdsAsync` | +| `Services/TeamMemberAppService.cs` | 保存/列表/详情 scope 逻辑 | +| `PeopleView.tsx` | 前端 Company Admin 放宽 Region/Locations 必填 | + +--- + +## 十三、Team Member 批量导入 / PDF 导出 — Region 列 + +### 模板列(下载 `download-team-member-import-template`) + +| 列 | 必填 | 说明 | +|----|------|------| +| Name / Email / Role Name | 是 | 与单条创建一致 | +| **Region** | **否** | 可留空;填 `fl_group.Id` 或 Region 名称(GroupName),多个用 `;` 分隔 | +| **Assigned Location Ids** | **否*** | 与 Region **至少填一项**;见下 | + +\* Company Admin 在 Web 端可只选 Company;Excel 批量导入仍须 Region 或门店至少一项(或后续扩展 Company 列)。 + +### Assigned Location Ids 填什么? + +**优先填 `location.LocationCode`(门店编码)**,例如 `LOC001`、`33333`。 + +也支持: + +- **`location.Id`(Guid 字符串)** +- **`编码 - 门店名`** 格式(取 ` -` 前一段作为编码或 Guid) + +模板示例 `LOC001;LOC002` 即为 **LocationCode**,不是 Guid。 + +### 涉及代码 + +| 文件 | 说明 | +|------|------| +| `Helpers/TeamMemberBatchExcelHelper.cs` | Region 列解析、`BuildImportTemplateWorkbook` | +| `Services/TeamMemberAppService.cs` | 导入解析 Region/Location、PDF 增 Region 列 | + +--- + +## 十四、Team Member 列表只返回 1 条(应 3 条) + +### 现象 + +`GET /api/app/team-member?SkipCount=1&MaxResultCount=10` 在 Subway China 下应返回 **123 / Sandi / Tom** 共 3 人,实际 `totalCount=1`。 + +### 根因 + +1. **Guid 字符串大小写不一致**:`userlocation.UserId` / `LocationId` 与 `User.Id.ToString()` 比较时用字符串 `Contains`,大小写不同会漏关联,导致成员 scope 命中失败、`assignedLocations` 为空。 +2. **前端未传 `PartnerId`**:筛选 Company 时只靠客户端二次过滤,且 `locationIds` 为空时被误过滤。 +3. **列表出参缺 `locationIds`**:前端无法正确做门店 scope 匹配。 + +### 修复 + +| 改动 | 说明 | +|------|------| +| `TeamMemberListScopeHelper` | 统一 `NormalizeScopeKey` / `UserKey`,用 **Guid** 比较 | +| `BuildFilteredUserQueryAsync` | 按 Guid 解析 scope 与 userlocation | +| `MapUsersToOutputAsync` | assignedMap 用规范化 userKey;出参增加 **`locationIds`** | +| 前端 `getTeamMembers` | 传 **`PartnerId` / `GroupId`** | +| `memberMatchesLocationScope` | 支持 **partnerIds** 匹配 + locationId 小写归一 | + +### 验证(测试库 Subway China) + +| 用户 | 应出现在 Subway 范围列表 | +|------|--------------------------| +| 123 / Sandi / Tom | 是 | +| Nancy Lang(GongCha) | 否 | +| admin | 仅平台管理员无筛选时可见 | + +--- + +## 十五、Region(Group)列表 Company Admin 只返回 1 条 + +### 现象 + +`GET /api/app/group?SkipCount=1&MaxResultCount=10&Sorting=CreationTime+desc` 以 **Company Admin(Subway China / 用户 123)** 登录时,`totalCount=1`,仅 **Subway Beijing**;测试库 Subway China 下应有 **Beijing / Chendu / Shanghai** 共 3 条。 + +### 根因 + +`PartnerScopeHelper.ResolveGroupScopeAsync` 对非管理员按 **userlocation 绑定的门店** 推导 Region:只匹配 `location.Partner + location.GroupName` 对应的 `fl_group`。用户 123 仅绑定了 Beijing 门店,因此 scope 只有 **Subway Beijing** 一条,未展开到公司下全部 Region。 + +### 修复 + +| 文件 | 说明 | +|------|------| +| `Helpers/LocationScopeBindingHelper.cs` | 新增 `ResolveBoundLocationIdsForUserAsync`、`ResolveDisplayRegionIdsForUserAsync`(与 Team Member 详情 `regionIds` 同规则) | +| `Helpers/PartnerScopeHelper.cs` | `ResolveGroupScopeAsync` 改为调用上述方法;`ApplyGroupScope` 用 Guid 比较;`ResolvePartnerScopeAsync` 统一 Guid 读 `userlocation` | + +### 验证 + +- 用户 **123**(Company Admin)→ `totalCount=3`,`items` 与编辑接口 `regionIds` 三条一致 +- 平台 **admin** → 全部 4 条 +- **SkipCount=1** 表示第一页(页码从 1 起),不是 offset + +--- + +## 关联文档 + +- App Preview labelId / Company:`项目相关文档/6-11代码优化.md` +- 第一轮 scope 兼容:`项目相关文档/6-17代码优化.md`(第一节、第二节 get-print-log-list) +- 三维 scope 业务规则:`项目相关文档/6-4代码优化.md` +- 分页约定:`Helpers/PagedQueryConvention.cs` diff --git a/项目相关文档/Account-Management-批量导入模板/Team-Member-批量导入模板.csv b/项目相关文档/Account-Management-批量导入模板/Team-Member-批量导入模板.csv index 32ae262..86ba811 100644 --- a/项目相关文档/Account-Management-批量导入模板/Team-Member-批量导入模板.csv +++ b/项目相关文档/Account-Management-批量导入模板/Team-Member-批量导入模板.csv @@ -1,3 +1,3 @@ -Name,User Name (Login),Password,Email,Phone,Role Id,Role Name (仅说明勿删),Assigned Location Ids,Status -John Doe,john.doe,ChangeMe123!,john@123.com,789654444,"(必填)从系统角色列表复制 Role Id","Staff","门店Guid1;门店Guid2",TRUE +Name,User Name,Password,Email,Phone,Role Id,Role Name,Region,Assigned Location Ids,Status +John Doe,john.doe,ChangeMe123!,john@example.com,789654444,,Staff,,LOC001;LOC002,TRUE ,,,,,,,,, diff --git a/项目相关文档/批量导入导出接口说明.md b/项目相关文档/批量导入导出接口说明.md index 83e9853..453c539 100644 --- a/项目相关文档/批量导入导出接口说明.md +++ b/项目相关文档/批量导入导出接口说明.md @@ -178,7 +178,7 @@ | 数据范围 | **全量**:符合筛选条件的全部成员;**不使用** `SkipCount` / `MaxResultCount` | | 排序 | 有 `Sorting` 则按其排序,否则按创建时间降序 | | 响应 | `Content-Type: application/pdf`,文件名示例 `team-members_yyyy-MM-dd_HH-mm-ss.pdf` | -| PDF 列 | Name、Email、Phone、Role、Assigned Locations(多门店以分号拼接)、Status(Active/Inactive) | +| PDF 列 | Name、Email、Phone、Role、**Region**、Assigned Locations(多门店以分号拼接)、Status(Active/Inactive) | **说明**:PDF 中「Assigned Locations」展示该成员**全部**已分配门店(不受列表按门店筛选时「仅显示命中门店」的收缩影响),便于导出后审阅完整权限。 @@ -195,10 +195,14 @@ **表头识别(摘要)** -- **必填列**:`Name`(或 FullName)、`Email`;**Role**;**Assigned Locations**(至少一条)。 -- **可选列**:`UserName` / `Login`(不填则登录账号用 Email)、`Password`(不填则用配置 **`TeamMemberImportDefaultPassword`**)、`Phone`、`Status`。 -- **Assigned Locations**:多个门店可用 **`;`**、`|`、换行、中文 **`,`** 分隔;支持 `33333 - Central Park Store`(取 **` -`** 前为门店编码或 Guid)。 -- **Role**:与系统 **`Role.RoleName`** 一致(忽略大小写与中间空格);未匹配则该行失败。 +- **必填列**:`Name`(或 FullName)、`Email`;**Role Name**(或 **Role Id** Guid);**Region** 与 **Assigned Location Ids** **至少填一项**(均可留空仅适用于 Web 已支持的 Company Admin + Company 场景)。 +- **可选列**:`User Name` / `Login`(不填则登录账号用 Email)、`Password`(不填则用配置 **`TeamMemberImportDefaultPassword`**)、`Phone`、`Status`、**`Region`**(可留空)。 +- **Region**:多个可用 **`;`**、`|`, 换行分隔;支持 **`fl_group.Id`(Guid)** 或 **Region 名称(GroupName)**;仅填 Region 时会绑定该区域下全部门店。 +- **Assigned Location Ids**:多个可用 **`;`**、`|`, 换行、中文 **`,`** 分隔;支持 **`location.LocationCode`(门店编码,推荐)** 或 **`location.Id`(Guid)**;亦支持 `LOC001 - Store Name`(取 **` -`** 前为编码或 Guid)。 +- **Role**:与系统 **`Role.RoleName`** 一致(忽略大小写与中间空格);或填 **Role Id**(Guid)。 +- 下载模板由服务端 **动态生成**(含 **Region** 列),不再依赖服务器目录中的静态 xlsx。 + +**PDF 导出列**:Name、Email、Phone、Role、**Region**、Assigned Locations、Status。 内部对每行调用与单条创建相同的业务逻辑;**单行失败不影响其它行**。