diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionCreateInputVo.cs index 52a8cf9..6c779db 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionCreateInputVo.cs @@ -10,6 +10,26 @@ public class LabelMultipleOptionCreateInputVo public bool State { get; set; } = true; + /// + /// 门店可用范围:ALL / SPECIFIED;传了 时自动为 SPECIFIED + /// + public string AvailabilityType { get; set; } = "ALL"; + + /// + /// 适用 Region(多选),fl_group.Id;与 合并去重 + /// + public List? RegionIds { get; set; } + + /// + /// 适用 Region(多选),与 相同 + /// + public List? GroupIds { get; set; } + + /// + /// 适用门店(多选),location.Id;与 Region 合并后写入 fl_label_multiple_option_location + /// + public List? LocationIds { get; set; } + public int OrderNum { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListInputVo.cs index dbf8365..718e484 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListInputVo.cs @@ -7,5 +7,15 @@ public class LabelMultipleOptionGetListInputVo : PagedAndSortedResultRequestDto public string? Keyword { get; set; } public bool? State { get; set; } + + /// + /// Region 筛选(fl_group.Id) + /// + public string? GroupId { get; set; } + + /// + /// 门店筛选(location.Id);优先于 + /// + public string? LocationId { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListOutputDto.cs index f7af6bc..e5011fa 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListOutputDto.cs @@ -12,8 +12,20 @@ public class LabelMultipleOptionGetListOutputDto public bool State { get; set; } + public string AvailabilityType { get; set; } = "ALL"; + public int OrderNum { get; set; } public DateTime LastEdited { get; set; } + + /// 列表列 Region + public string Region { get; set; } = string.Empty; + + /// 列表列 Location + public string Location { get; set; } = string.Empty; + + public List RegionIds { get; set; } = new(); + + public List LocationIds { get; set; } = new(); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetOutputDto.cs index 0b181c6..23d92fd 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetOutputDto.cs @@ -13,5 +13,13 @@ public class LabelMultipleOptionGetOutputDto public bool State { get; set; } public int OrderNum { get; set; } + + public string AvailabilityType { get; set; } = "ALL"; + + public List RegionIds { get; set; } = new(); + + public List GroupIds { get; set; } = new(); + + public List LocationIds { get; set; } = new(); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeCreateInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeCreateInputVo.cs index 27e3863..084c6ce 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeCreateInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeCreateInputVo.cs @@ -8,6 +8,26 @@ public class LabelTypeCreateInputVo public bool State { get; set; } = true; + /// + /// 门店可用范围:ALL / SPECIFIED;传了 时自动为 SPECIFIED + /// + public string AvailabilityType { get; set; } = "ALL"; + + /// + /// 适用 Region(多选),fl_group.Id;与 合并去重 + /// + public List? RegionIds { get; set; } + + /// + /// 适用 Region(多选),与 相同 + /// + public List? GroupIds { get; set; } + + /// + /// 适用门店(多选),location.Id;与 Region 合并后写入 fl_label_type_location + /// + public List? LocationIds { get; set; } + public int OrderNum { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListInputVo.cs index af2f04c..1441853 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListInputVo.cs @@ -7,5 +7,15 @@ public class LabelTypeGetListInputVo : PagedAndSortedResultRequestDto public string? Keyword { get; set; } public bool? State { get; set; } + + /// + /// Region 筛选(fl_group.Id);仅返回在该 Region 下存在标签实例的类型 + /// + public string? GroupId { get; set; } + + /// + /// 门店筛选(location.Id);优先于 + /// + public string? LocationId { get; set; } } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListOutputDto.cs index 1a63ae4..8914e18 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListOutputDto.cs @@ -10,10 +10,25 @@ public class LabelTypeGetListOutputDto public bool State { get; set; } + public string AvailabilityType { get; set; } = "ALL"; + public int OrderNum { get; set; } + /// 列表列 No. of Labels:该类型下未删除标签数(统计 fl_label,非库表物理列) public long NoOfLabels { get; set; } public DateTime LastEdited { get; set; } + + /// 列表列 Region:由该类型下标签绑定的门店 GroupName 去重拼接 + public string Region { get; set; } = string.Empty; + + /// 列表列 Location:由该类型下标签绑定的门店名去重拼接 + public string Location { get; set; } = string.Empty; + + /// Region Id(fl_group.Id,由标签门店反推) + public List RegionIds { get; set; } = new(); + + /// 门店 Id(location.Id,由该类型下标签汇总) + public List LocationIds { get; set; } = new(); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetOutputDto.cs index 0763156..54440ba 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetOutputDto.cs @@ -11,5 +11,13 @@ public class LabelTypeGetOutputDto public bool State { get; set; } public int OrderNum { get; set; } + + public string AvailabilityType { get; set; } = "ALL"; + + public List RegionIds { get; set; } = new(); + + public List GroupIds { get; set; } = new(); + + public List LocationIds { get; set; } = new(); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogListItemDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogListItemDto.cs index c1857d1..2822a19 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogListItemDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogListItemDto.cs @@ -8,7 +8,9 @@ public class ReportsPrintLogListItemDto /// 打印任务 Id(fl_label_print_task.Id),重打时使用 public string TaskId { get; set; } = string.Empty; - /// 标签编码(展示为 Label ID) + /// + /// 列表列 Label ID:门店当日打印序号(yyyyMMdd-n),非 fl_label.LabelCode + /// public string LabelCode { get; set; } = string.Empty; public string ProductName { get; set; } = "无"; diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListInputVo.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListInputVo.cs index 55f2c29..8c7697e 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListInputVo.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/TeamMember/TeamMemberGetListInputVo.cs @@ -18,7 +18,17 @@ public class TeamMemberGetListInputVo : PagedAndSortedResultRequestDto public Guid? RoleId { get; set; } /// - /// 门店ID(可选) + /// Company 筛选(fl_partner.Id);与 按「门店优先」解析范围 + /// + public string? PartnerId { get; set; } + + /// + /// Region 筛选(fl_group.Id);未传 时生效 + /// + public string? GroupId { get; set; } + + /// + /// 门店筛选(location.Id);最精确,传则忽略 / /// public string? LocationId { get; set; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLocationDetailOutputDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLocationDetailOutputDto.cs index d684a8b..6c90125 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLocationDetailOutputDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLocationDetailOutputDto.cs @@ -17,7 +17,7 @@ public class UsAppLocationDetailOutputDto /// 门店电话(来自 location.Phone;空为「无」) public string StorePhone { get; set; } = string.Empty; - /// 营业时间;当前库无字段,固定返回「无」直至业务落库 + /// 经营时间(location.OperatingHours 自由文本);库空或未维护时为「无」 public string OperatingHours { get; set; } = string.Empty; /// 店长姓名;优先取绑定本店且角色名/编码含 manager 的用户 diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogItemDto.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogItemDto.cs index f006a1f..c4c6936 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogItemDto.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/PrintLogItemDto.cs @@ -38,7 +38,7 @@ public class PrintLogItemDto /// 打印时间(PrintedAt ?? CreationTime) public DateTime PrintedAt { get; set; } - /// 操作人姓名(当前登录账号 Name) + /// 操作人姓名(任务 CreatedBy 对应用户;查看全店日志时为实际打印人) public string OperatorName { get; set; } = string.Empty; /// 门店名称 diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationAppService.cs index 7dd222b..0933892 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationAppService.cs @@ -12,7 +12,7 @@ namespace FoodLabeling.Application.Contracts.IServices; public interface ILocationAppService : IApplicationService { /// - /// 门店分页列表 + /// 门店分页列表;管理员返回全部门店,非管理员仅返回其绑定门店所属 Region(Partner + GroupName)下的门店。 /// /// 查询条件 Task> GetListAsync(LocationGetListInputVo input); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IReportsAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IReportsAppService.cs index d4536e0..08fae68 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IReportsAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IReportsAppService.cs @@ -32,7 +32,8 @@ public interface IReportsAppService : IApplicationService Task ReprintPrintLogAsync(UsAppLabelReprintInputVo input); /// - /// Label Report 统计(卡片 + 分类柱数据 + 7 日趋势 + Top 产品);admin 统计全部,否则仅当前用户。 + /// Label Report 统计(卡片 + 分类柱数据 + 7 日趋势 + Top 产品); + /// admin 统计全部门店;非管理员仅统计 userlocation 绑定门店内全部打印任务(不按 CreatedBy 过滤)。 /// Task GetLabelReportAsync(ReportsLabelReportQueryInputVo input); @@ -40,4 +41,32 @@ public interface IReportsAppService : IApplicationService /// Label Report 导出 PDF /// Task ExportLabelReportPdfAsync(ReportsLabelReportQueryInputVo input); + + /// + /// 按模板统计打印标签数量(分页列表:模板名称 + 打印数)。 + /// + /// + /// 统计 fl_label_print_task,按 TemplateId 分组;数据范围与 label-report 一致: + /// 管理员可查全部门店(可按 Company/Region/Location 收窄);非管理员仅 userlocation 绑定门店内全部打印任务。 + /// + /// 示例请求: + /// ```http + /// GET /api/app/reports/template-print-stat-list?SkipCount=1&MaxResultCount=20&StartDate=2026-04-07&EndDate=2026-05-18 + /// Authorization: Bearer {token} + /// ``` + /// + /// 参数说明: + /// - SkipCount / MaxResultCount: 分页(SkipCount 为 1-based 页码约定,与项目其它列表一致) + /// - StartDate / EndDate: 统计区间(含起止日;默认近 30 天至今天) + /// - PartnerId / GroupId / LocationId: 组织范围筛选 + /// - Keyword: 模板名称模糊匹配 + /// - Sorting: 可选 PrintedCount desc(默认按打印数降序) + /// + /// 分页与筛选条件 + /// 各模板打印数量列表 + /// 成功返回分页统计 + /// 入参无效或未登录 + /// 服务器错误 + Task> GetTemplatePrintStatListAsync( + ReportsTemplatePrintStatGetListInputVo 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 8a764c2..7048114 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 @@ -44,7 +44,7 @@ public interface IUsAppAuthAppService : IApplicationService /// 按门店 Id 查询 Location 详情(须为当前账号 userlocation 绑定门店) /// /// 门店 Guid 字符串 - /// 店名、地址、电话、营业时间占位、店长信息 + /// 店名、地址、电话、经营时间(operatingHours)、店长信息 /// 成功 /// 参数非法、未绑定或无权限 /// 服务器错误 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 c97133f..124a380 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 @@ -1,4 +1,5 @@ using FoodLabeling.Application.Contracts.Dtos.Common; +using FoodLabeling.Application.Contracts.Dtos.Reports; using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; using Volo.Abp.Application.Services; @@ -30,7 +31,12 @@ public interface IUsAppLabelingAppService : IApplicationService Task ReprintAsync(UsAppLabelReprintInputVo input); /// - /// App 打印日志:获取当前登录账号在当前门店打印的记录(分页,时间倒序) + /// App 打印日志:当前门店打印记录(分页)。管理员 / Partner 角色可见门店内全部;其它角色仅本人。 /// Task> GetPrintLogListAsync(PrintLogGetListInputVo input); + + /// + /// App Label Report:当前门店统计。权限规则与 一致。 + /// + Task GetLabelReportAsync(UsAppLabelReportQueryInputVo input); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelMultipleOptionDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelMultipleOptionDbEntity.cs index b6fe0f4..231f746 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelMultipleOptionDbEntity.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelMultipleOptionDbEntity.cs @@ -29,5 +29,10 @@ public class FlLabelMultipleOptionDbEntity public int OrderNum { get; set; } public bool State { get; set; } + + /// + /// 门店可用范围:ALL / SPECIFIED + /// + public string AvailabilityType { get; set; } = "ALL"; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTypeDbEntity.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTypeDbEntity.cs index 875724a..aa92f95 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTypeDbEntity.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTypeDbEntity.cs @@ -27,5 +27,10 @@ public class FlLabelTypeDbEntity public int OrderNum { get; set; } public bool State { get; set; } + + /// + /// 门店可用范围:ALL / SPECIFIED + /// + public string AvailabilityType { get; set; } = "ALL"; } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelMultipleOptionAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelMultipleOptionAppService.cs index 8cc24fa..d012d5c 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelMultipleOptionAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelMultipleOptionAppService.cs @@ -3,6 +3,7 @@ using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Contracts.Dtos.LabelMultipleOption; using FoodLabeling.Application.Contracts.IServices; using FoodLabeling.Application.Services.DbModels; +using FoodLabeling.Domain.Entities; using SqlSugar; using Volo.Abp; using Volo.Abp.Application.Services; @@ -26,12 +27,16 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO { RefAsync total = 0; var keyword = input.Keyword?.Trim(); + var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( + _dbContext.SqlSugarClient, input.GroupId, input.LocationId); var query = _dbContext.SqlSugarClient.Queryable() .Where(x => !x.IsDeleted) .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.OptionCode.Contains(keyword!) || x.OptionName.Contains(keyword!)) .WhereIF(input.State != null, x => x.State == input.State); + query = ApplyMultipleOptionScopeFilter(query, scopedLocationIds); + if (!string.IsNullOrWhiteSpace(input.Sorting)) { query = query.OrderBy(input.Sorting); @@ -42,15 +47,26 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO } var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); - var items = entities.Select(x => new LabelMultipleOptionGetListOutputDto + var scopeMap = await BuildMultipleOptionScopeMapAsync(entities); + + var items = entities.Select(x => { - Id = x.Id, - OptionCode = x.OptionCode, - OptionName = x.OptionName, - OptionValuesJson = x.OptionValuesJson, - State = x.State, - OrderNum = x.OrderNum, - LastEdited = x.LastModificationTime ?? x.CreationTime + scopeMap.TryGetValue(x.Id, out var scope); + return new LabelMultipleOptionGetListOutputDto + { + Id = x.Id, + OptionCode = x.OptionCode, + OptionName = x.OptionName, + OptionValuesJson = x.OptionValuesJson, + State = x.State, + AvailabilityType = x.AvailabilityType, + OrderNum = x.OrderNum, + LastEdited = x.LastModificationTime ?? x.CreationTime, + Region = scope?.Region ?? EmptyDisplay, + Location = scope?.Location ?? EmptyDisplay, + RegionIds = scope?.RegionIds ?? new List(), + LocationIds = scope?.LocationIds ?? new List() + }; }).ToList(); return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); @@ -65,15 +81,21 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO throw new UserFriendlyException("多选项不存在"); } - return new LabelMultipleOptionGetOutputDto + var dto = MapToGetOutput(entity); + if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) { - Id = entity.Id, - OptionCode = entity.OptionCode, - OptionName = entity.OptionName, - OptionValuesJson = entity.OptionValuesJson, - State = entity.State, - OrderNum = entity.OrderNum - }; + var locationIds = await _dbContext.SqlSugarClient.Queryable() + .Where(x => x.MultipleOptionId == entity.Id) + .Select(x => x.LocationId) + .ToListAsync(); + dto.LocationIds = LocationScopeBindingHelper.NormalizeIds(locationIds); + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIds); + dto.RegionIds = regionIds; + dto.GroupIds = regionIds; + } + + return dto; } public async Task CreateAsync(LabelMultipleOptionCreateInputVo input) @@ -85,6 +107,8 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO throw new UserFriendlyException("多选项编码和名称不能为空"); } + var (availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input); + var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && (x.OptionCode == code || x.OptionName == name)); if (duplicated) @@ -92,17 +116,27 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO throw new UserFriendlyException("多选项编码或名称已存在"); } + var now = DateTime.Now; + var currentUserId = CurrentUser?.Id?.ToString(); var entity = new FlLabelMultipleOptionDbEntity { Id = _guidGenerator.Create().ToString(), + IsDeleted = false, + CreationTime = now, + CreatorId = currentUserId, + LastModificationTime = now, + LastModifierId = currentUserId, + ConcurrencyStamp = _guidGenerator.Create().ToString("N"), OptionCode = code, OptionName = name, OptionValuesJson = input.OptionValuesJson?.Trim(), State = input.State, + AvailabilityType = availabilityType, OrderNum = input.OrderNum }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); + await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); return await GetAsync(entity.Id); } @@ -122,6 +156,8 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO throw new UserFriendlyException("多选项编码和名称不能为空"); } + var (availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input); + var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.OptionCode == code || x.OptionName == name)); if (duplicated) @@ -133,11 +169,14 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO entity.OptionName = name; entity.OptionValuesJson = input.OptionValuesJson?.Trim(); entity.State = input.State; + entity.AvailabilityType = availabilityType; entity.OrderNum = input.OrderNum; entity.LastModificationTime = DateTime.Now; entity.LastModifierId = CurrentUser?.Id?.ToString(); await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, + entity.LastModificationTime ?? DateTime.Now); return await GetAsync(id); } @@ -150,12 +189,266 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO return; } + await _dbContext.SqlSugarClient.Deleteable() + .Where(x => x.MultipleOptionId == id) + .ExecuteCommandAsync(); + entity.IsDeleted = true; entity.LastModificationTime = DateTime.Now; entity.LastModifierId = CurrentUser?.Id?.ToString(); await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); } + private const string EmptyDisplay = "无"; + private const string AllRegionsDisplay = "All Regions"; + private const string AllLocationsDisplay = "All Locations"; + + private async Task<(string AvailabilityType, List LocationIds)> ResolveMultipleOptionScopeForSaveAsync( + LabelMultipleOptionCreateInputVo input) + { + var regionIds = NormalizeRegionIds(input); + var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); + + var hasScopeArrays = input.RegionIds is not null || input.GroupIds is not null || input.LocationIds is not null; + if (regionIds.Count > 0 || explicitLocationIds.Count > 0) + { + availabilityType = "SPECIFIED"; + } + else if (hasScopeArrays && string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase)) + { + availabilityType = "ALL"; + } + + if (availabilityType != "ALL" && availabilityType != "SPECIFIED") + { + throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); + } + + if (availabilityType == "ALL") + { + return ("ALL", new List()); + } + + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( + _dbContext.SqlSugarClient, (IReadOnlyList?)null, regionIds, explicitLocationIds); + if (merged.Count == 0) + { + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); + } + + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); + return ("SPECIFIED", merged); + } + + private static List NormalizeRegionIds(LabelMultipleOptionCreateInputVo input) + { + var merged = new HashSet(StringComparer.Ordinal); + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds)) + { + merged.Add(id); + } + + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds)) + { + merged.Add(id); + } + + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); + } + + private async Task SaveMultipleOptionLocationsAsync( + string multipleOptionId, + string availabilityType, + List locationIds, + string? currentUserId, + DateTime now) + { + await _dbContext.SqlSugarClient.Deleteable() + .Where(x => x.MultipleOptionId == multipleOptionId) + .ExecuteCommandAsync(); + + if (availabilityType != "SPECIFIED" || locationIds.Count == 0) + { + return; + } + + var rows = locationIds.Select(locId => new FlLabelMultipleOptionLocationDbEntity + { + Id = _guidGenerator.Create().ToString(), + MultipleOptionId = multipleOptionId, + LocationId = locId, + CreationTime = now, + CreatorId = currentUserId + }).ToList(); + + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); + } + + private static LabelMultipleOptionGetOutputDto MapToGetOutput(FlLabelMultipleOptionDbEntity x) + { + return new LabelMultipleOptionGetOutputDto + { + Id = x.Id, + OptionCode = x.OptionCode, + OptionName = x.OptionName, + OptionValuesJson = x.OptionValuesJson, + State = x.State, + OrderNum = x.OrderNum, + AvailabilityType = x.AvailabilityType + }; + } + + private static ISugarQueryable ApplyMultipleOptionScopeFilter( + ISugarQueryable query, + List? scopedLocationIds) + { + if (scopedLocationIds is null) + { + return query; + } + + if (scopedLocationIds.Count == 0) + { + return query.Where(o => o.AvailabilityType == "ALL"); + } + + return query.Where(o => + o.AvailabilityType == "ALL" || + SqlFunc.Subqueryable() + .Where(ol => ol.MultipleOptionId == o.Id && scopedLocationIds.Contains(ol.LocationId)) + .Any()); + } + + private async Task> BuildMultipleOptionScopeMapAsync( + List entities) + { + var result = new Dictionary(StringComparer.Ordinal); + if (entities.Count == 0) + { + return result; + } + + foreach (var e in entities.Where(x => + !string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))) + { + result[e.Id] = new MultipleOptionScopeData + { + Region = AllRegionsDisplay, + Location = AllLocationsDisplay, + RegionIds = new List(), + LocationIds = new List() + }; + } + + var specifiedIds = entities + .Where(x => string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Id) + .ToList(); + if (specifiedIds.Count == 0) + { + return result; + } + + var links = await _dbContext.SqlSugarClient.Queryable() + .Where(x => specifiedIds.Contains(x.MultipleOptionId)) + .ToListAsync(); + + var locIdSet = links + .Select(x => x.LocationId) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .Distinct(StringComparer.Ordinal) + .ToList(); + + var locById = new Dictionary(StringComparer.Ordinal); + if (locIdSet.Count > 0) + { + var guidList = locIdSet.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); + if (guidList.Count > 0) + { + var locs = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) + .ToListAsync(); + foreach (var loc in locs) + { + locById[loc.Id.ToString()] = loc; + } + } + } + + foreach (var optionId in specifiedIds) + { + var optionLinks = links.Where(x => x.MultipleOptionId == optionId).ToList(); + var locationIds = LocationScopeBindingHelper.NormalizeIds( + optionLinks.Select(x => x.LocationId).ToList()); + + if (optionLinks.Count == 0) + { + result[optionId] = new MultipleOptionScopeData + { + Region = EmptyDisplay, + Location = EmptyDisplay, + RegionIds = new List(), + LocationIds = new List() + }; + continue; + } + + var regions = new HashSet(StringComparer.OrdinalIgnoreCase); + var locationNames = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var lid in locationIds) + { + if (!locById.TryGetValue(lid, out var loc)) + { + continue; + } + + var groupName = loc.GroupName?.Trim(); + if (!string.IsNullOrEmpty(groupName)) + { + regions.Add(groupName); + } + + var locName = loc.LocationName?.Trim(); + if (string.IsNullOrEmpty(locName)) + { + locName = loc.LocationCode?.Trim(); + } + + if (!string.IsNullOrEmpty(locName)) + { + locationNames.Add(locName); + } + } + + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIds); + + result[optionId] = new MultipleOptionScopeData + { + Region = regions.Count > 0 + ? string.Join(", ", regions.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + : EmptyDisplay, + Location = locationNames.Count > 0 + ? string.Join(", ", locationNames.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + : EmptyDisplay, + RegionIds = regionIds, + LocationIds = locationIds + }; + } + + return result; + } + + private sealed class MultipleOptionScopeData + { + public string Region { get; init; } = string.Empty; + public string Location { get; init; } = string.Empty; + public List RegionIds { get; init; } = new(); + public List LocationIds { get; init; } = new(); + } + private static PagedResultWithPageDto BuildPagedResult(int skipCount, int maxResultCount, int total, List items) { var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; @@ -171,4 +464,3 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO }; } } - diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTypeAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTypeAppService.cs index ac82144..29838ee 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTypeAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTypeAppService.cs @@ -3,6 +3,7 @@ using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Contracts.Dtos.LabelType; using FoodLabeling.Application.Contracts.IServices; using FoodLabeling.Application.Services.DbModels; +using FoodLabeling.Domain.Entities; using SqlSugar; using Volo.Abp; using Volo.Abp.Application.Services; @@ -26,12 +27,16 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService { RefAsync total = 0; var keyword = input.Keyword?.Trim(); + var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( + _dbContext.SqlSugarClient, input.GroupId, input.LocationId); var query = _dbContext.SqlSugarClient.Queryable() .Where(x => !x.IsDeleted) .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.TypeCode.Contains(keyword!) || x.TypeName.Contains(keyword!)) .WhereIF(input.State != null, x => x.State == input.State); + query = ApplyLabelTypeScopeFilter(query, scopedLocationIds); + if (!string.IsNullOrWhiteSpace(input.Sorting)) { query = query.OrderBy(input.Sorting); @@ -44,23 +49,28 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); var ids = entities.Select(x => x.Id).ToList(); - var countRows = await _dbContext.SqlSugarClient.Queryable() - .Where(x => !x.IsDeleted) - .Where(x => x.LabelTypeId != null && ids.Contains(x.LabelTypeId)) - .GroupBy(x => x.LabelTypeId) - .Select(x => new { TypeId = x.LabelTypeId, Count = SqlFunc.AggregateCount(x.Id) }) - .ToListAsync(); - var countMap = countRows.ToDictionary(x => x.TypeId!, x => (long)x.Count); + var countMap = await BuildTypeLabelStatsMapAsync(ids, scopedLocationIds); + var scopeMap = await BuildTypeConfiguredScopeMapAsync(entities); - var items = entities.Select(x => new LabelTypeGetListOutputDto + var items = entities.Select(x => { - Id = x.Id, - TypeCode = x.TypeCode, - TypeName = x.TypeName, - State = x.State, - OrderNum = x.OrderNum, - NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0, - LastEdited = x.LastModificationTime ?? x.CreationTime + scopeMap.TryGetValue(x.Id, out var scope); + countMap.TryGetValue(x.Id, out var stats); + return new LabelTypeGetListOutputDto + { + Id = x.Id, + TypeCode = x.TypeCode, + TypeName = x.TypeName, + State = x.State, + AvailabilityType = x.AvailabilityType, + OrderNum = x.OrderNum, + NoOfLabels = stats?.Count ?? 0, + LastEdited = stats?.MaxEdited ?? x.LastModificationTime ?? x.CreationTime, + Region = scope?.Region ?? EmptyDisplay, + Location = scope?.Location ?? EmptyDisplay, + RegionIds = scope?.RegionIds ?? new List(), + LocationIds = scope?.LocationIds ?? new List() + }; }).ToList(); return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); @@ -75,7 +85,21 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService throw new UserFriendlyException("标签类型不存在"); } - return MapToGetOutput(entity); + var dto = MapToGetOutput(entity); + if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) + { + var locationIds = await _dbContext.SqlSugarClient.Queryable() + .Where(x => x.LabelTypeId == entity.Id) + .Select(x => x.LocationId) + .ToListAsync(); + dto.LocationIds = LocationScopeBindingHelper.NormalizeIds(locationIds); + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIds); + dto.RegionIds = regionIds; + dto.GroupIds = regionIds; + } + + return dto; } public async Task CreateAsync(LabelTypeCreateInputVo input) @@ -87,6 +111,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService throw new UserFriendlyException("类型编码和名称不能为空"); } + var (availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input); + var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && (x.TypeCode == code || x.TypeName == name)); if (duplicated) @@ -94,16 +120,26 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService throw new UserFriendlyException("类型编码或名称已存在"); } + var now = DateTime.Now; + var currentUserId = CurrentUser?.Id?.ToString(); var entity = new FlLabelTypeDbEntity { Id = _guidGenerator.Create().ToString(), + IsDeleted = false, + CreationTime = now, + CreatorId = currentUserId, + LastModificationTime = now, + LastModifierId = currentUserId, + ConcurrencyStamp = _guidGenerator.Create().ToString("N"), TypeCode = code, TypeName = name, State = input.State, + AvailabilityType = availabilityType, OrderNum = input.OrderNum }; await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); + await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); return await GetAsync(entity.Id); } @@ -123,6 +159,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService throw new UserFriendlyException("类型编码和名称不能为空"); } + var (availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input); + var duplicated = await _dbContext.SqlSugarClient.Queryable() .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.TypeCode == code || x.TypeName == name)); if (duplicated) @@ -133,11 +171,14 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService entity.TypeCode = code; entity.TypeName = name; entity.State = input.State; + entity.AvailabilityType = availabilityType; entity.OrderNum = input.OrderNum; entity.LastModificationTime = DateTime.Now; entity.LastModifierId = CurrentUser?.Id?.ToString(); await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); + await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, + entity.LastModificationTime ?? DateTime.Now); return await GetAsync(id); } @@ -157,12 +198,304 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService throw new UserFriendlyException("该标签类型已被标签引用,无法删除"); } + await _dbContext.SqlSugarClient.Deleteable() + .Where(x => x.LabelTypeId == id) + .ExecuteCommandAsync(); + entity.IsDeleted = true; entity.LastModificationTime = DateTime.Now; entity.LastModifierId = CurrentUser?.Id?.ToString(); await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); } + private const string EmptyDisplay = "无"; + private const string AllRegionsDisplay = "All Regions"; + private const string AllLocationsDisplay = "All Locations"; + + private async Task<(string AvailabilityType, List LocationIds)> ResolveTypeScopeForSaveAsync( + LabelTypeCreateInputVo input) + { + var regionIds = NormalizeRegionIds(input); + var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); + + var hasScopeArrays = input.RegionIds is not null || input.GroupIds is not null || input.LocationIds is not null; + if (regionIds.Count > 0 || explicitLocationIds.Count > 0) + { + availabilityType = "SPECIFIED"; + } + else if (hasScopeArrays && string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase)) + { + availabilityType = "ALL"; + } + + if (availabilityType != "ALL" && availabilityType != "SPECIFIED") + { + throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); + } + + if (availabilityType == "ALL") + { + return ("ALL", new List()); + } + + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( + _dbContext.SqlSugarClient, (IReadOnlyList?)null, regionIds, explicitLocationIds); + if (merged.Count == 0) + { + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); + } + + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); + return ("SPECIFIED", merged); + } + + private static List NormalizeRegionIds(LabelTypeCreateInputVo input) + { + var merged = new HashSet(StringComparer.Ordinal); + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds)) + { + merged.Add(id); + } + + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds)) + { + merged.Add(id); + } + + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); + } + + private static ISugarQueryable ApplyLabelTypeScopeFilter( + ISugarQueryable query, + List? scopedLocationIds) + { + if (scopedLocationIds is null) + { + return query; + } + + if (scopedLocationIds.Count == 0) + { + return query.Where(t => t.AvailabilityType == "ALL"); + } + + return query.Where(t => + t.AvailabilityType == "ALL" || + SqlFunc.Subqueryable() + .Where(tl => tl.LabelTypeId == t.Id && scopedLocationIds.Contains(tl.LocationId)) + .Any()); + } + + private async Task SaveTypeLocationsAsync( + string labelTypeId, + string availabilityType, + List locationIds, + string? currentUserId, + DateTime now) + { + await _dbContext.SqlSugarClient.Deleteable() + .Where(x => x.LabelTypeId == labelTypeId) + .ExecuteCommandAsync(); + + if (availabilityType != "SPECIFIED" || locationIds.Count == 0) + { + return; + } + + var rows = locationIds.Select(locId => new FlLabelTypeLocationDbEntity + { + Id = _guidGenerator.Create().ToString(), + LabelTypeId = labelTypeId, + LocationId = locId, + CreationTime = now, + CreatorId = currentUserId + }).ToList(); + + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); + } + + private async Task> BuildTypeLabelStatsMapAsync( + List typeIds, + List? scopedLocationIds) + { + var result = new Dictionary(StringComparer.Ordinal); + if (typeIds.Count == 0) + { + return result; + } + + var labelQuery = _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted) + .Where(x => x.LabelTypeId != null && typeIds.Contains(x.LabelTypeId)); + + labelQuery = ApplyLabelScopeOnLabels(labelQuery, scopedLocationIds); + + var rows = await labelQuery + .Select(x => new { x.LabelTypeId, x.CreationTime, x.LastModificationTime }) + .ToListAsync(); + + foreach (var g in rows.GroupBy(x => x.LabelTypeId!)) + { + result[g.Key] = new TypeLabelStats + { + Count = g.Count(), + MaxEdited = g.Max(l => l.LastModificationTime ?? l.CreationTime) + }; + } + + return result; + } + + private async Task> BuildTypeConfiguredScopeMapAsync( + List entities) + { + var result = new Dictionary(StringComparer.Ordinal); + if (entities.Count == 0) + { + return result; + } + + foreach (var e in entities.Where(x => + !string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))) + { + result[e.Id] = new TypeScopeData + { + Region = AllRegionsDisplay, + Location = AllLocationsDisplay, + RegionIds = new List(), + LocationIds = new List() + }; + } + + var specifiedIds = entities + .Where(x => string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Id) + .ToList(); + if (specifiedIds.Count == 0) + { + return result; + } + + var links = await _dbContext.SqlSugarClient.Queryable() + .Where(x => specifiedIds.Contains(x.LabelTypeId)) + .ToListAsync(); + + var locIdSet = links + .Select(x => x.LocationId) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .Distinct(StringComparer.Ordinal) + .ToList(); + + var locById = new Dictionary(StringComparer.Ordinal); + if (locIdSet.Count > 0) + { + var guidList = locIdSet.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); + if (guidList.Count > 0) + { + var locs = await _dbContext.SqlSugarClient.Queryable() + .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) + .ToListAsync(); + foreach (var loc in locs) + { + locById[loc.Id.ToString()] = loc; + } + } + } + + foreach (var typeId in specifiedIds) + { + var typeLinks = links.Where(x => x.LabelTypeId == typeId).ToList(); + var locationIds = LocationScopeBindingHelper.NormalizeIds( + typeLinks.Select(x => x.LocationId).ToList()); + + if (typeLinks.Count == 0) + { + result[typeId] = new TypeScopeData + { + Region = EmptyDisplay, + Location = EmptyDisplay, + RegionIds = new List(), + LocationIds = new List() + }; + continue; + } + + var regions = new HashSet(StringComparer.OrdinalIgnoreCase); + var locationNames = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var lid in locationIds) + { + if (!locById.TryGetValue(lid, out var loc)) + { + continue; + } + + var groupName = loc.GroupName?.Trim(); + if (!string.IsNullOrEmpty(groupName)) + { + regions.Add(groupName); + } + + var locName = loc.LocationName?.Trim(); + if (string.IsNullOrEmpty(locName)) + { + locName = loc.LocationCode?.Trim(); + } + + if (!string.IsNullOrEmpty(locName)) + { + locationNames.Add(locName); + } + } + + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( + _dbContext.SqlSugarClient, locationIds); + + result[typeId] = new TypeScopeData + { + Region = regions.Count > 0 + ? string.Join(", ", regions.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + : EmptyDisplay, + Location = locationNames.Count > 0 + ? string.Join(", ", locationNames.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + : EmptyDisplay, + RegionIds = regionIds, + LocationIds = locationIds + }; + } + + return result; + } + + private static ISugarQueryable ApplyLabelScopeOnLabels( + ISugarQueryable labelQuery, + List? scopedLocationIds) + { + if (scopedLocationIds is null) + { + return labelQuery; + } + + return scopedLocationIds.Count == 0 + ? labelQuery.Where(_ => false) + : labelQuery.Where(l => scopedLocationIds.Contains(l.LocationId)); + } + + private sealed class TypeScopeData + { + public string Region { get; init; } = string.Empty; + public string Location { get; init; } = string.Empty; + public List RegionIds { get; init; } = new(); + public List LocationIds { get; init; } = new(); + } + + private sealed class TypeLabelStats + { + public long Count { get; init; } + public DateTime MaxEdited { get; init; } + } + private static LabelTypeGetOutputDto MapToGetOutput(FlLabelTypeDbEntity x) { return new LabelTypeGetOutputDto @@ -171,7 +504,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService TypeCode = x.TypeCode, TypeName = x.TypeName, State = x.State, - OrderNum = x.OrderNum + OrderNum = x.OrderNum, + AvailabilityType = x.AvailabilityType }; } @@ -190,4 +524,3 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService }; } } - diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationAppService.cs index 80b3956..355f984 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationAppService.cs @@ -22,13 +22,16 @@ namespace FoodLabeling.Application.Services; public class LocationAppService : ApplicationService, ILocationAppService { private readonly ISqlSugarRepository _locationRepository; + private readonly ISqlSugarDbContext _dbContext; private readonly IOptionsSnapshot _batchImportOptions; public LocationAppService( ISqlSugarRepository locationRepository, + ISqlSugarDbContext dbContext, IOptionsSnapshot batchImportOptions) { _locationRepository = locationRepository; + _dbContext = dbContext; _batchImportOptions = batchImportOptions; } @@ -37,7 +40,7 @@ public class LocationAppService : ApplicationService, ILocationAppService { RefAsync total = 0; - var query = BuildFilteredQuery(input); + var query = await BuildFilteredQueryAsync(input); if (!string.IsNullOrWhiteSpace(input.Sorting)) { query = query.OrderBy(input.Sorting); @@ -200,7 +203,7 @@ public class LocationAppService : ApplicationService, ILocationAppService State = input.State }; - var query = BuildFilteredQuery(exportFilter); + var query = await BuildFilteredQueryAsync(exportFilter); if (!string.IsNullOrWhiteSpace(exportFilter.Sorting)) { query = query.OrderBy(exportFilter.Sorting); @@ -328,14 +331,20 @@ public class LocationAppService : ApplicationService, ILocationAppService return result; } - private ISugarQueryable BuildFilteredQuery(LocationGetListInputVo input) + private async Task> BuildFilteredQueryAsync(LocationGetListInputVo input) { + var scope = await LocationRegionScopeHelper.ResolveLocationListScopeAsync(CurrentUser, _dbContext); + var keyword = input.Keyword?.Trim(); var partner = input.Partner?.Trim(); var groupName = input.GroupName?.Trim(); - return _locationRepository._DbQueryable - .Where(x => x.IsDeleted == false) + var query = _locationRepository._DbQueryable + .Where(x => x.IsDeleted == false); + + query = LocationRegionScopeHelper.ApplyLocationListScope(query, scope); + + return query .WhereIF(!string.IsNullOrEmpty(partner), x => x.Partner == partner) .WhereIF(!string.IsNullOrEmpty(groupName), x => x.GroupName == groupName) .WhereIF(input.State is not null, x => x.State == input.State) @@ -350,8 +359,7 @@ public class LocationAppService : ApplicationService, ILocationAppService (x.ZipCode != null && x.ZipCode.Contains(keyword!)) || (x.Phone != null && x.Phone.Contains(keyword!)) || (x.Email != null && x.Email.Contains(keyword!)) || - (x.OperatingHours != null && x.OperatingHours.Contains(keyword!)) - ); + (x.OperatingHours != null && x.OperatingHours.Contains(keyword!))); } private static LocationGetListOutputDto ToListDto(LocationAggregateRoot x) => diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs index b2f7950..05908a6 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs @@ -1,10 +1,9 @@ using System.Text.Json; using FoodLabeling.Application.Contracts.Constants; -using FoodLabeling.Application.Contracts.Dtos.RbacRole; using FoodLabeling.Application.Helpers; +using FoodLabeling.Application.Contracts.Dtos.RbacRole; using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Contracts.IServices; -using FoodLabeling.Application.Services.DbModels; using Microsoft.AspNetCore.Mvc; using SqlSugar; using Volo.Abp; @@ -103,8 +102,8 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService throw new UserFriendlyException("角色不存在"); } - var menuIds = await _dbContext.SqlSugarClient.Queryable() - .Where(x => x.RoleId == id.ToString()) + var menuIds = await _roleMenuRepository._DbQueryable + .Where(x => x.RoleId == id) .Select(x => x.MenuId) .ToListAsync(); @@ -118,7 +117,7 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService State = entity.State, OrderNum = entity.OrderNum, AccessPermissionCodes = DeserializeAccessPermissionCodes(entity.AccessPermissionCodesJson), - MenuIds = menuIds + MenuIds = menuIds.Select(x => x.ToString()).ToList() }; await FillAccessPermissionsAsync(new List { dto }); return dto; @@ -212,20 +211,41 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService } /// - /// 新增/编辑时按 menuIds 或 accessPermissions 绑定角色菜单(二者都未传则不改绑定) + /// 新增/编辑时按 menuIds 或 accessPermissions 绑定角色菜单(RoleMenu 表)。 /// private async Task ApplyRoleMenuBindingsAsync(Guid roleId, RbacRoleCreateInputVo input) { - if (input.MenuIds is not null) + var hasMenuIds = input.MenuIds is not null; + var hasAccessPermissions = input.AccessPermissions is not null; + + if (hasMenuIds && input.MenuIds!.Count > 0) { await SetRoleMenusAsync(roleId, input.MenuIds); return; } - if (input.AccessPermissions is not null) + if (hasAccessPermissions) { + if (string.IsNullOrWhiteSpace(input.AccessPermissions)) + { + await SetRoleMenusAsync(roleId, new List()); + return; + } + var menuIds = await ResolveMenuIdsFromAccessPermissionsAsync(input.AccessPermissions); + if (menuIds.Count == 0) + { + throw new UserFriendlyException( + "accessPermissions 未匹配到任何菜单,请确认 PermissionCode 与菜单一致,或先执行 menu_backfill_permission_code.sql 回填 Menu.PermissionCode"); + } + await SetRoleMenusAsync(roleId, menuIds); + return; + } + + if (hasMenuIds && input.MenuIds!.Count == 0) + { + await SetRoleMenusAsync(roleId, new List()); } } @@ -250,10 +270,15 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService return; } - var entities = existMenuIds.Select(menuId => new RoleMenuEntity + var entities = existMenuIds.Select(menuId => { - RoleId = roleId, - MenuId = menuId + var entity = new RoleMenuEntity + { + RoleId = roleId, + MenuId = menuId + }; + EntityHelper.TrySetId(entity, () => GuidGenerator.Create()); + return entity; }).ToList(); await _roleMenuRepository.InsertRangeAsync(entities); @@ -261,24 +286,28 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService private async Task> ResolveMenuIdsFromAccessPermissionsAsync(string accessPermissions) { - var codes = accessPermissions - .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Where(c => !string.IsNullOrWhiteSpace(c)) - .Distinct(StringComparer.Ordinal) - .ToList(); - + var codes = RbacAccessPermissionHelper.ParseAccessPermissionCodes(accessPermissions); if (codes.Count == 0) { return new List(); } + var codeSet = new HashSet(codes, StringComparer.OrdinalIgnoreCase); + var menus = await _menuRepository._DbQueryable - .Where(m => m.IsDeleted == false && m.PermissionCode != null) - .Where(m => codes.Contains(m.PermissionCode!)) - .Select(m => m.Id) + .Where(m => m.IsDeleted == false) + .Select(m => new { m.Id, m.PermissionCode, m.Router }) .ToListAsync(); - return menus; + return menus + .Where(m => + { + var effective = RbacAccessPermissionHelper.GetEffectivePermissionCode(m.PermissionCode, m.Router); + return effective is not null && codeSet.Contains(effective); + }) + .Select(m => m.Id) + .Distinct() + .ToList(); } private async Task FillAccessPermissionsAsync(List items) @@ -296,7 +325,7 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService } /// - /// 按角色汇总已绑定菜单上的 PermissionCode(去重、英文逗号+空格拼接) + /// Role → RoleMenu → Menu.PermissionCode(空则按 Router 推导)汇总 accessPermissions。 /// private async Task> GetAccessPermissionsByRoleIdsAsync(List roleIds) { @@ -319,11 +348,13 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService var menuIds = links.Select(x => x.MenuId).Distinct().ToList(); var menus = await _menuRepository._DbQueryable .Where(m => menuIds.Contains(m.Id) && m.IsDeleted == false) - .Select(m => new { m.Id, m.PermissionCode }) + .Select(m => new { m.Id, m.PermissionCode, m.Router }) .ToListAsync(); - var permByMenuId = menus.ToDictionary(x => x.Id, x => x.PermissionCode); + var permByMenuId = menus.ToDictionary( + x => x.Id, + x => RbacAccessPermissionHelper.GetEffectivePermissionCode(x.PermissionCode, x.Router)); - var byRole = distinctRoleIds.ToDictionary(id => id, _ => new HashSet(StringComparer.Ordinal)); + var byRole = distinctRoleIds.ToDictionary(id => id, _ => new HashSet(StringComparer.OrdinalIgnoreCase)); foreach (var link in links) { if (!permByMenuId.TryGetValue(link.MenuId, out var code) || string.IsNullOrWhiteSpace(code)) @@ -364,7 +395,6 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService await _roleDeptRepository.DeleteAsync(x => idList.Contains(x.RoleId)); await _userRoleRepository.DeleteAsync(x => idList.Contains(x.RoleId)); - // 角色表为软删(ISoftDelete) await _roleRepository.DeleteAsync(x => idList.Contains(x.Id)); } @@ -426,4 +456,3 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService .ToList(); } } - diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleMenuAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleMenuAppService.cs index e096e27..ff3dd07 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleMenuAppService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleMenuAppService.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using SqlSugar; using Volo.Abp; using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Entities; using Volo.Abp.Uow; using Yi.Framework.Rbac.Domain.Entities; using Yi.Framework.SqlSugarCore.Abstractions; @@ -56,10 +57,15 @@ public class RbacRoleMenuAppService : ApplicationService, IRbacRoleMenuAppServic await _roleMenuRepository.DeleteAsync(x => x.RoleId == input.RoleId); - var entities = existMenuIds.Select(menuId => new RoleMenuEntity + var entities = existMenuIds.Select(menuId => { - RoleId = input.RoleId, - MenuId = menuId + var entity = new RoleMenuEntity + { + RoleId = input.RoleId, + MenuId = menuId + }; + EntityHelper.TrySetId(entity, () => GuidGenerator.Create()); + return entity; }).ToList(); if (entities.Count > 0) 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 c9012f5..0baef6a 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 @@ -106,6 +106,13 @@ public class ReportsAppService : ApplicationService, IReportsAppService var userMap = await LoadUserNameMapAsync(pageRows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x!).Distinct().ToList()); + var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync( + _dbContext.SqlSugarClient, + pageRows.Select(x => new ReportsPrintLogDailyLabelIdHelper.PrintTaskScopeKey( + x.Id, + x.LocationId, + x.PrintedAt ?? DateTime.MinValue)).ToList()); + var items = pageRows.Select(x => { var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) @@ -114,10 +121,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService var templateText = FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName); var locText = FormatLocationText(x.LocName, x.LocCode); var printedAt = x.PrintedAt ?? DateTime.MinValue; + var labelDisplayId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无"; return new ReportsPrintLogListItemDto { TaskId = x.Id, - LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(), + LabelCode = labelDisplayId, ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), ProductCategoryName = string.IsNullOrWhiteSpace(x.ProductCategoryName) ? "无" @@ -131,7 +139,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService PrintedByName = ResolveUserName(userMap, x.CreatedBy), LocationText = locText, LocationId = x.LocationId?.Trim(), - ExpiryDateText = TryExtractExpiryText(x.PrintInputJson) + ExpiryDateText = ReportsPrintLogExpiryHelper.ExtractExpiryText(x.PrintInputJson) }; }).ToList(); @@ -191,6 +199,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService t.PrintInputJson, PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), t.CreatedBy, + t.LocationId, LocName = loc.LocationName, LocCode = loc.LocationCode }) @@ -199,6 +208,13 @@ public class ReportsAppService : ApplicationService, IReportsAppService var userMap = await LoadUserNameMapAsync(rows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x!).Distinct().ToList()); + var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync( + _dbContext.SqlSugarClient, + rows.Select(x => new ReportsPrintLogDailyLabelIdHelper.PrintTaskScopeKey( + x.Id, + x.LocationId, + x.PrintedAt ?? DateTime.MinValue)).ToList()); + var fileName = $"print-log_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; var document = Document.Create(container => { @@ -236,8 +252,9 @@ public class ReportsAppService : ApplicationService, IReportsAppService var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) ? x.ProductCategoryName!.Trim() : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim()); + var labelDisplayId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无"; table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) - .Text(string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim()); + .Text(labelDisplayId); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) .Text(string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim()); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3).Text(cat); @@ -251,7 +268,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) .Text(FormatLocationText(x.LocName, x.LocCode)); table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) - .Text(TryExtractExpiryText(x.PrintInputJson)); + .Text(ReportsPrintLogExpiryHelper.ExtractExpiryText(x.PrintInputJson)); } }); }); @@ -338,7 +355,14 @@ public class ReportsAppService : ApplicationService, IReportsAppService var userMap = await LoadUserNameMapAsync(pageRows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x!).Distinct().ToList()); - var items = pageRows.Select(x => MapPrintLogExportRowToListItem(x, userMap)).ToList(); + var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync( + _dbContext.SqlSugarClient, + pageRows.Select(x => new ReportsPrintLogDailyLabelIdHelper.PrintTaskScopeKey( + x.Id, + x.LocationId, + x.PrintedAt ?? DateTime.MinValue)).ToList()); + + var items = pageRows.Select(x => MapPrintLogExportRowToListItem(x, userMap, dailyLabelIdMap)).ToList(); var ms = ReportsPrintLogExcelHelper.BuildWorkbook(items); var fileName = $"print-log_{Clock.Now:yyyyMMdd-HHmmss}.xlsx"; @@ -351,6 +375,84 @@ public class ReportsAppService : ApplicationService, IReportsAppService _usAppLabelingAppService.ReprintAsync(input); /// + public async Task> GetTemplatePrintStatListAsync( + ReportsTemplatePrintStatGetListInputVo input) + { + if (input is null) + { + throw new UserFriendlyException("入参不能为空"); + } + + if (!CurrentUser.Id.HasValue) + { + throw new UserFriendlyException("用户未登录"); + } + + var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync( + CurrentUser, + _dbContext.SqlSugarClient, + input.PartnerId, + input.GroupId, + input.LocationId); + if (locationIds is not null && locationIds.Count == 0) + { + return EmptyTemplatePrintStatPage(input); + } + + var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); + var currentUserIdStr = CurrentUser.Id.Value.ToString(); + var templateKeyword = input.Keyword?.Trim(); + + var groupedRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword: null, + restrictToCreator: false) + .LeftJoin((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) + .Where((t, l, p, lc, pc, loc, tpl) => + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < rangeEndExcl) + .WhereIF(!string.IsNullOrWhiteSpace(templateKeyword), + (t, l, p, lc, pc, loc, tpl) => + tpl.TemplateName != null && tpl.TemplateName.Contains(templateKeyword!)) + .GroupBy((t, l, p, lc, pc, loc, tpl) => new { t.TemplateId, tpl.TemplateName }) + .Select((t, l, p, lc, pc, loc, tpl) => new + { + t.TemplateId, + tpl.TemplateName, + Cnt = SqlFunc.AggregateCount(t.Id) + }) + .ToListAsync(); + + var ordered = groupedRows + .Select(x => new ReportsTemplatePrintStatListItemDto + { + TemplateId = string.IsNullOrWhiteSpace(x.TemplateId) ? null : x.TemplateId.Trim(), + TemplateName = string.IsNullOrWhiteSpace(x.TemplateName) ? "无" : x.TemplateName.Trim(), + PrintedCount = x.Cnt + }) + .ToList(); + + if (!string.IsNullOrWhiteSpace(input.Sorting) && + input.Sorting.Trim().Equals("PrintedCount asc", StringComparison.OrdinalIgnoreCase)) + { + ordered = ordered.OrderBy(x => x.PrintedCount).ThenBy(x => x.TemplateName).ToList(); + } + else + { + ordered = ordered.OrderByDescending(x => x.PrintedCount).ThenBy(x => x.TemplateName).ToList(); + } + + var total = ordered.Count; + var pageIndex = PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); + var pageSize = input.MaxResultCount <= 0 ? total : input.MaxResultCount; + var offset = pageSize <= 0 ? 0 : (pageIndex - 1) * pageSize; + var pageItems = pageSize <= 0 + ? ordered + : ordered.Skip(offset).Take(pageSize).ToList(); + + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, pageItems); + } + + /// public async Task GetLabelReportAsync(ReportsLabelReportQueryInputVo input) { if (input is null) @@ -363,7 +465,12 @@ public class ReportsAppService : ApplicationService, IReportsAppService throw new UserFriendlyException("用户未登录"); } - var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); + var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync( + CurrentUser, + _dbContext.SqlSugarClient, + input.PartnerId, + input.GroupId, + input.LocationId); if (locationIds is not null && locationIds.Count == 0) { return new ReportsLabelReportOutputDto(); @@ -382,13 +489,13 @@ public class ReportsAppService : ApplicationService, IReportsAppService var currentUserIdStr = CurrentUser.Id.Value.ToString(); var keyword = input.Keyword?.Trim(); - var totalCur = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + var totalCur = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) .CountAsync(); - var totalPrev = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + var totalPrev = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl) @@ -399,7 +506,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService var avgDaily = Math.Round((decimal)totalCur / dayCount, 2); var avgDailyPrev = Math.Round((decimal)totalPrev / prevDayCount, 2); - var categoryRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + var categoryRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => l.LabelCategoryId != null && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && @@ -410,7 +517,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService var topCat = categoryRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); - var productRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + var productRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => !string.IsNullOrEmpty(p.Id) && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && @@ -431,7 +538,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService var trendEndExcl = trendEndDay.AddDays(1); - var trendRaw = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) + var trendRaw = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) .Where((t, l, p, lc, pc, loc) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay && SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl) @@ -585,7 +692,8 @@ public class ReportsAppService : ApplicationService, IReportsAppService List? locationIds, bool isAdmin, string currentUserIdStr, - string? keyword) + string? keyword, + bool restrictToCreator = true) { return _dbContext.SqlSugarClient.Queryable() .LeftJoin((t, l) => t.LabelId == l.Id) @@ -595,7 +703,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService .LeftJoin((t, l, p, lc, pc, loc) => t.LocationId != null && SqlFunc.ToString(loc.Id) == t.LocationId) .Where((t, l, p, lc, pc, loc) => !loc.IsDeleted) - .WhereIF(!isAdmin, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr) + .WhereIF(restrictToCreator && !isAdmin, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr) .WhereIF(locationIds is not null, (t, l, p, lc, pc, loc) => locationIds!.Contains(t.LocationId!)) .WhereIF(!string.IsNullOrWhiteSpace(keyword), (t, l, p, lc, pc, loc) => @@ -697,6 +805,21 @@ public class ReportsAppService : ApplicationService, IReportsAppService }; } + private static PagedResultWithPageDto EmptyTemplatePrintStatPage( + ReportsTemplatePrintStatGetListInputVo input) + { + var pageSize = input.MaxResultCount <= 0 ? 0 : input.MaxResultCount; + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); + return new PagedResultWithPageDto + { + PageIndex = pageIndex, + PageSize = pageSize, + TotalCount = 0, + TotalPages = 0, + Items = new List() + }; + } + private static PagedResultWithPageDto BuildPagedResult(int skipCount, int maxResultCount, int total, List items) { @@ -799,54 +922,6 @@ public class ReportsAppService : ApplicationService, IReportsAppService return $"{ws}x{hs}{normalizedUnit}"; } - private static string TryExtractExpiryText(string? printInputJson) - { - if (string.IsNullOrWhiteSpace(printInputJson)) - { - return "无"; - } - - try - { - using var doc = JsonDocument.Parse(printInputJson); - if (doc.RootElement.ValueKind != JsonValueKind.Object) - { - return "无"; - } - - foreach (var prop in doc.RootElement.EnumerateObject()) - { - var key = prop.Name.Trim(); - if (!key.Equals("expiryDate", StringComparison.OrdinalIgnoreCase) && - !key.Equals("expiry", StringComparison.OrdinalIgnoreCase) && - !key.Equals("expirationDate", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var v = prop.Value; - if (v.ValueKind == JsonValueKind.String) - { - var s = v.GetString(); - return string.IsNullOrWhiteSpace(s) ? "无" : s.Trim(); - } - - if (v.ValueKind == JsonValueKind.Number && v.TryGetInt64(out var n)) - { - return n.ToString(CultureInfo.InvariantCulture); - } - - return v.ToString(); - } - } - catch - { - return "无"; - } - - return "无"; - } - private static IActionResult BuildEmptyPdf(string fileName) { QuestPDF.Settings.License = LicenseType.Community; @@ -897,8 +972,10 @@ public class ReportsAppService : ApplicationService, IReportsAppService public string? LocCode { get; set; } } - private static ReportsPrintLogListItemDto MapPrintLogExportRowToListItem(PrintLogExportRow x, - Dictionary userMap) + private static ReportsPrintLogListItemDto MapPrintLogExportRowToListItem( + PrintLogExportRow x, + Dictionary userMap, + IReadOnlyDictionary dailyLabelIdMap) { var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) ? x.ProductCategoryName!.Trim() @@ -906,10 +983,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService var templateText = FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName); var locText = FormatLocationText(x.LocName, x.LocCode); var printedAt = x.PrintedAt ?? DateTime.MinValue; + var labelDisplayId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无"; return new ReportsPrintLogListItemDto { TaskId = x.Id, - LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(), + LabelCode = labelDisplayId, ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), ProductCategoryName = string.IsNullOrWhiteSpace(x.ProductCategoryName) ? "无" @@ -923,7 +1001,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService PrintedByName = ResolveUserName(userMap, x.CreatedBy), LocationText = locText, LocationId = x.LocationId?.Trim(), - ExpiryDateText = TryExtractExpiryText(x.PrintInputJson) + ExpiryDateText = ReportsPrintLogExpiryHelper.ExtractExpiryText(x.PrintInputJson) }; } } 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 5f35724..4d18690 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 @@ -17,6 +17,8 @@ using Volo.Abp.Application.Services; using Volo.Abp.Domain.Entities; using Volo.Abp.Guids; using Yi.Framework.Rbac.Domain.Entities; +using Yi.Framework.Rbac.Domain.Entities.ValueObjects; +using Yi.Framework.Rbac.Domain.Helpers; using Yi.Framework.Rbac.Domain.Managers; using Yi.Framework.SqlSugarCore.Abstractions; @@ -54,7 +56,10 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService var pageSize = input.MaxResultCount; RefAsync total = 0; - var query = await BuildFilteredUserQueryAsync(input); + var scopeLocationIds = await LocationScopeBindingHelper.ResolveFilteredLocationIdsForListAsync( + _dbContext.SqlSugarClient, input.PartnerId, input.GroupId, input.LocationId); + + var query = await BuildFilteredUserQueryAsync(input, scopeLocationIds); var users = await query .OrderByIF(!string.IsNullOrWhiteSpace(input.Sorting), input.Sorting!) .OrderByDescending(u => u.CreationTime) @@ -62,8 +67,8 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService var items = await MapUsersToOutputAsync( users, - input.LocationId, - restrictAssignedLocationsToFilter: !string.IsNullOrWhiteSpace(input.LocationId)); + scopeLocationIds, + restrictAssignedLocationsToFilter: scopeLocationIds is not null); var totalCount = (long)total; return new PagedResultWithPageDto @@ -133,11 +138,15 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService { var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input); - var user = new UserAggregateRoot(input.UserName.Trim(), input.Password, input.Phone, input.FullName.Trim()) + var user = new UserAggregateRoot { + UserName = input.UserName.Trim(), Name = input.FullName.Trim(), + Nick = input.FullName.Trim(), Email = input.Email?.Trim(), - State = input.State + Phone = input.Phone, + State = input.State, + EncryPassword = new EncryPasswordValueObject(input.Password.Trim()) }; EntityHelper.TrySetId(user, _guidGenerator.Create); @@ -172,13 +181,22 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService user.Phone = input.Phone; user.State = input.State; + var passwordChanged = false; if (!string.IsNullOrWhiteSpace(input.Password)) { - user.EncryPassword.Password = input.Password; - user.BuildPassword(); + UserPasswordHelper.ApplyPlainPassword(user, input.Password); + passwordChanged = true; } await _userRepository.UpdateAsync(user); + if (passwordChanged) + { + await UserPasswordHelper.EnsurePasswordColumnsPersistedAsync( + _userRepository, + user.Id, + user.EncryPassword.Password, + user.EncryPassword.Salt); + } if (input.RoleId != null) { @@ -254,13 +272,19 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService { QuestPDF.Settings.License = LicenseType.Community; - var query = await BuildFilteredUserQueryAsync(input); + var scopeLocationIds = await LocationScopeBindingHelper.ResolveFilteredLocationIdsForListAsync( + _dbContext.SqlSugarClient, input.PartnerId, input.GroupId, input.LocationId); + + var query = await BuildFilteredUserQueryAsync(input, scopeLocationIds); var users = await query .OrderByIF(!string.IsNullOrWhiteSpace(input.Sorting), input.Sorting!) .OrderByDescending(u => u.CreationTime) .ToListAsync(); - var rows = await MapUsersToOutputAsync(users, locationFilter: null, restrictAssignedLocationsToFilter: false); + var rows = await MapUsersToOutputAsync( + users, + scopeLocationIds, + restrictAssignedLocationsToFilter: scopeLocationIds is not null); var fileName = $"team-members_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; var document = Document.Create(container => @@ -492,7 +516,9 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService return result.Distinct().ToList(); } - private async Task> BuildFilteredUserQueryAsync(TeamMemberGetListInputVo input) + private async Task> BuildFilteredUserQueryAsync( + TeamMemberGetListInputVo input, + List? scopeLocationIds) { var keyword = input.Keyword?.Trim(); var query = _userRepository._DbQueryable @@ -513,15 +539,22 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService query = query.Where(u => userIds.Contains(u.Id)); } - if (!string.IsNullOrWhiteSpace(input.LocationId)) + if (scopeLocationIds is not null) { - var locId = input.LocationId.Trim(); - var userIdStrs = await _dbContext.SqlSugarClient.Queryable() - .Where(x => !x.IsDeleted && x.LocationId == locId) - .Select(x => x.UserId) - .ToListAsync(); - var allowed = new HashSet(userIdStrs); - query = query.Where(u => allowed.Contains(u.Id.ToString())); + if (scopeLocationIds.Count == 0) + { + query = query.Where(_ => false); + } + 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) + .ToListAsync(); + var allowed = new HashSet(userIdStrs); + query = query.Where(u => allowed.Contains(u.Id.ToString())); + } } return query; @@ -529,7 +562,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService private async Task> MapUsersToOutputAsync( List users, - string? locationFilter, + List? scopeLocationIds, bool restrictAssignedLocationsToFilter) { if (users.Count == 0) @@ -552,9 +585,10 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService var userLocQuery = _dbContext.SqlSugarClient.Queryable() .Where(x => !x.IsDeleted) .Where(x => userIdStrings.Contains(x.UserId)); - if (restrictAssignedLocationsToFilter && !string.IsNullOrWhiteSpace(locationFilter)) + if (restrictAssignedLocationsToFilter && scopeLocationIds is { Count: > 0 }) { - userLocQuery = userLocQuery.Where(x => x.LocationId == locationFilter.Trim()); + var scopeSet = new HashSet(scopeLocationIds, StringComparer.Ordinal); + userLocQuery = userLocQuery.Where(x => scopeSet.Contains(x.LocationId)); } var userLocations = await userLocQuery.ToListAsync(); 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 53aeea3..c2c5aee 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 @@ -25,10 +25,10 @@ using Volo.Abp.EventBus.Local; using Volo.Abp.Security.Claims; using Volo.Abp.Uow; using Volo.Abp.Users; -using Yi.Framework.Core.Helper; using Yi.Framework.Rbac.Application.Contracts.Dtos.Account; using Yi.Framework.Rbac.Application.Contracts.IServices; using Yi.Framework.Rbac.Domain.Entities; +using Yi.Framework.Rbac.Domain.Helpers; using Yi.Framework.Rbac.Domain.Managers; using Yi.Framework.Rbac.Domain.Shared.Consts; using Yi.Framework.Rbac.Domain.Shared.Dtos; @@ -102,7 +102,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService throw new UserFriendlyException("登录失败!邮箱不存在!"); } - if (user.EncryPassword.Password != MD5Helper.SHA2Encode(input.Password, user.EncryPassword.Salt)) + if (!UserPasswordHelper.VerifyPlainPassword(user, input.Password)) { throw new UserFriendlyException(UserConst.Login_Error); } @@ -158,7 +158,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService } /// - /// 查询单个门店详情(Location 页):地址、门店电话、营业时间占位、店长(角色含 manager 的绑定用户) + /// 查询单个门店详情(Location 页):地址、门店电话、经营时间、店长(角色含 manager 的绑定用户) /// /// /// 仅当当前登录用户在 userlocation 中绑定该 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 65a6bad..4237c31 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 @@ -7,6 +7,7 @@ using System.Threading.Tasks; using FoodLabeling.Application.Contracts.Dtos.Common; using FoodLabeling.Application.Contracts.Dtos.Label; using FoodLabeling.Application.Contracts.Dtos.LabelTemplate; +using FoodLabeling.Application.Contracts.Dtos.Reports; using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; using FoodLabeling.Application.Contracts.IServices; using FoodLabeling.Application.Helpers; @@ -53,7 +54,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ /// L1 标签分类 fl_label_category(含 buttonAppearance;COLOR/IMAGE 展示值在 categoryPhotoUrl);仅对当前门店可用:ALL 或 SPECIFIED 且在 fl_label_category_location; /// L2 产品分类 fl_product.CategoryId join fl_product_category(同上,展示值在 categoryPhotoUrl); /// L3 产品卡片:按「产品 + 标签模板」拆分(同一 productId、不同 fl_label.TemplateId 为多张卡);L4 为该卡下与门店、标签分类、该产品、该模板关联的标签实例(fl_label + fl_label_type)。 - /// L2 仅包含对当前门店可用的类别:AvailabilityType=ALL,或 SPECIFIED 且在 fl_product_category_location 存在该门店记录; + /// L2 产品分类展示名来自 fl_product_category;产品范围已由 fl_location_product 限定当前门店, + /// 不再因产品分类 SPECIFIED 未配 fl_product_category_location 而整行过滤(避免 App 全店空数据)。 /// 未归类或分类行未关联到 fl_product_category 时仍归入「无」节点。 /// [Authorize] @@ -68,6 +70,9 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ var keyword = input.Keyword?.Trim(); var filterCategoryId = input.LabelCategoryId?.Trim(); + await UsAppPrintLogScopeHelper.EnsureUserBoundToLocationAsync( + CurrentUser, _dbContext.SqlSugarClient, locationId); + var productIds = await _dbContext.SqlSugarClient.Queryable() .Where(x => x.LocationId == locationId) .Select(x => x.ProductId) @@ -624,9 +629,10 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ throw new UserFriendlyException("打印任务不存在"); } - // 非 admin:仅允许重打自己在当前门店的任务;admin 可重打任意用户任务(仍须门店一致) - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); - if (!isAdmin && !string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase)) + // 管理员 / Partner 角色:可重打当前门店任意用户任务;其它角色仅本人 + var canViewAll = await UsAppPrintLogScopeHelper.CanViewAllPrintsAtLocationAsync( + CurrentUser, _dbContext.SqlSugarClient); + if (!canViewAll && !string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase)) { throw new UserFriendlyException("无权限重打该任务"); } @@ -726,13 +732,14 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ } /// - /// App 打印日志:获取当前登录账号在当前门店打印的记录(分页,时间倒序) + /// App 打印日志:当前门店打印记录(分页,时间倒序) /// /// - /// 仅返回满足: - /// - CreatedBy == CurrentUser.Id - /// - LocationId == input.LocationId - /// 的打印任务记录(fl_label_print_task)。 + /// 数据范围(须已绑定 input.locationId): + /// + /// 管理员)或角色码/名含 partner:该门店 全部 打印任务; + /// 其它角色:仅 CreatedBy == CurrentUser.Id + /// /// /// 示例请求: /// ```json @@ -774,9 +781,12 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ } var currentUserIdStr = CurrentUser.Id.Value.ToString(); + await UsAppPrintLogScopeHelper.EnsureUserBoundToLocationAsync( + CurrentUser, _dbContext.SqlSugarClient, locationId); - var currentUser = await _userRepository.GetByIdAsync(CurrentUser.Id.Value); - var operatorName = currentUser?.Name?.Trim() ?? string.Empty; + var canViewAll = await UsAppPrintLogScopeHelper.CanViewAllPrintsAtLocationAsync( + CurrentUser, _dbContext.SqlSugarClient); + var restrictToCreator = !canViewAll; var locationName = "无"; if (Guid.TryParse(locationId, out var locationGuid)) @@ -797,13 +807,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ RefAsync total = 0; - var query = _dbContext.SqlSugarClient - .Queryable() - .LeftJoin((t, l) => t.LabelId == l.Id) - .LeftJoin((t, l, p) => t.ProductId == p.Id) - .LeftJoin((t, l, p, lt) => t.LabelTypeId == lt.Id) - .LeftJoin((t, l, p, lt, tpl) => t.TemplateId == tpl.Id) - .Where((t, l, p, lt, tpl) => t.CreatedBy == currentUserIdStr && t.LocationId == locationId) + var query = UsAppPrintLogScopeHelper.BuildLocationPrintTaskQuery( + _dbContext.SqlSugarClient, locationId, restrictToCreator, currentUserIdStr) .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 @@ -821,11 +826,16 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ TemplateUnit = tpl.Unit, t.PrintInputJson, t.PrintedAt, - t.CreationTime + t.CreationTime, + t.CreatedBy }); var pageRows = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); + var operatorMap = await UsAppPrintLogScopeHelper.LoadOperatorNameMapAsync( + _dbContext.SqlSugarClient, + pageRows.Select(x => x.CreatedBy)); + var items = pageRows.Select(x => new PrintLogItemDto { TaskId = x.Id, @@ -839,7 +849,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ LabelSizeText = FormatLabelSizeWithUnit(x.TemplateWidth, x.TemplateHeight, x.TemplateUnit), PrintInputJson = x.PrintInputJson, PrintedAt = x.PrintedAt ?? x.CreationTime, - OperatorName = operatorName, + OperatorName = UsAppPrintLogScopeHelper.ResolveOperatorName(operatorMap, x.CreatedBy), LocationName = locationName }).ToList(); @@ -858,6 +868,204 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ }; } + /// + /// App Label Report:当前门店打印统计(权限与 一致) + /// + /// + /// 示例:POST /api/app/us-app-labeling/get-label-report + /// ```json + /// { "locationId": "3a21220f-db37-3e32-7390-d55f64cd62a8", "startDate": "2026-04-07", "endDate": "2026-05-18" } + /// ``` + /// + [Authorize] + [HttpPost] + public virtual async Task GetLabelReportAsync(UsAppLabelReportQueryInputVo input) + { + if (input is null) + { + throw new UserFriendlyException("入参不能为空"); + } + + if (!CurrentUser.Id.HasValue) + { + throw new UserFriendlyException("用户未登录"); + } + + var locationId = input.LocationId?.Trim(); + if (string.IsNullOrWhiteSpace(locationId)) + { + throw new UserFriendlyException("门店Id不能为空"); + } + + await UsAppPrintLogScopeHelper.EnsureUserBoundToLocationAsync( + CurrentUser, _dbContext.SqlSugarClient, locationId); + + var canViewAll = await UsAppPrintLogScopeHelper.CanViewAllPrintsAtLocationAsync( + CurrentUser, _dbContext.SqlSugarClient); + var restrictToCreator = !canViewAll; + var currentUserIdStr = CurrentUser.Id.Value.ToString(); + var keyword = input.Keyword?.Trim(); + + var (curStart, curEndExcl) = ResolveAppDateRange(input.StartDate, input.EndDate); + var span = curEndExcl - curStart; + if (span.TotalDays < 1) + { + span = TimeSpan.FromDays(1); + } + + var prevEndExcl = curStart; + var prevStart = curStart - span; + var db = _dbContext.SqlSugarClient; + + ISugarQueryable Core() => + UsAppPrintLogScopeHelper.BuildLocationPrintTaskReportQuery( + db, locationId, restrictToCreator, currentUserIdStr, keyword); + + var totalCur = await Core() + .Where((t, l, p, lc, pc, lt, tpl) => + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) + .CountAsync(); + + var totalPrev = await Core() + .Where((t, l, p, lc, pc, lt, tpl) => + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl) + .CountAsync(); + + var dayCount = Math.Max(1, (int)Math.Ceiling((curEndExcl - curStart).TotalDays)); + var prevDayCount = Math.Max(1, (int)Math.Ceiling((prevEndExcl - prevStart).TotalDays)); + var avgDaily = Math.Round((decimal)totalCur / dayCount, 2); + var avgDailyPrev = Math.Round((decimal)totalPrev / prevDayCount, 2); + + var categoryRows = await Core() + .Where((t, l, p, lc, pc, lt, tpl) => + l.LabelCategoryId != null && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) + .GroupBy((t, l, p, lc, pc, lt, tpl) => new { lc.Id, lc.CategoryName }) + .Select((t, l, p, lc, pc, lt, tpl) => new { lc.Id, lc.CategoryName, Cnt = SqlFunc.AggregateCount(t.Id) }) + .ToListAsync(); + + var topCat = categoryRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); + + var productRows = await Core() + .Where((t, l, p, lc, pc, lt, tpl) => + !string.IsNullOrEmpty(p.Id) && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) + .GroupBy((t, l, p, lc, pc, lt, tpl) => new { p.Id, p.ProductName, Cat = pc.CategoryName }) + .Select((t, l, p, lc, pc, lt, tpl) => new + { + p.Id, + p.ProductName, + CategoryName = pc.CategoryName, + Cnt = SqlFunc.AggregateCount(t.Id) + }) + .ToListAsync(); + + var topProd = productRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); + var topList = productRows.OrderByDescending(x => x.Cnt).Take(20).ToList(); + + var trendEndDay = curEndExcl.Date.AddDays(-1); + var trendStartDay = trendEndDay.AddDays(-6); + if (trendStartDay < curStart.Date) + { + trendStartDay = curStart.Date; + } + + var trendEndExcl = trendEndDay.AddDays(1); + + var trendRaw = await Core() + .Where((t, l, p, lc, pc, lt, tpl) => + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay && + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl) + .Select((t, l, p, lc, pc, lt, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime)) + .ToListAsync(); + + var trendDict = trendRaw + .Where(x => x.HasValue) + .GroupBy(x => x!.Value.Date) + .ToDictionary(g => g.Key, g => g.Count()); + + var trend = new List(); + for (var d = trendStartDay; d <= trendEndDay; d = d.AddDays(1)) + { + trend.Add(new ReportsDailyCountDto + { + Date = d.ToString("yyyy-MM-dd"), + Count = trendDict.TryGetValue(d, out var c) ? c : 0 + }); + } + + var byCategory = categoryRows + .OrderByDescending(x => x.Cnt) + .Select(x => new ReportsCategoryCountDto + { + CategoryId = string.IsNullOrWhiteSpace(x.Id) ? null : x.Id.Trim(), + CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? null : x.CategoryName.Trim(), + Count = x.Cnt + }) + .ToList(); + + var mostUsed = topList.Select(x => + { + var pct = totalCur <= 0 ? 0m : Math.Round(x.Cnt * 100m / totalCur, 2); + return new ReportsTopProductRowDto + { + ProductId = string.IsNullOrWhiteSpace(x.Id) ? null : x.Id.Trim(), + ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? null : x.ProductName.Trim(), + CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? null : x.CategoryName!.Trim(), + TotalPrinted = x.Cnt, + UsagePercent = pct + }; + }).ToList(); + + return new ReportsLabelReportOutputDto + { + Summary = new ReportsLabelReportSummaryDto + { + TotalLabelsPrinted = totalCur, + TotalLabelsPrintedPrevPeriod = totalPrev, + TotalLabelsPrintedChangeRate = CalcAppChangeRate(totalCur, totalPrev), + MostPrintedCategoryName = string.IsNullOrWhiteSpace(topCat?.CategoryName) ? null : topCat.CategoryName.Trim(), + MostPrintedCategoryCount = topCat?.Cnt ?? 0, + TopProductName = string.IsNullOrWhiteSpace(topProd?.ProductName) ? null : topProd.ProductName.Trim(), + TopProductCount = topProd?.Cnt ?? 0, + AvgDailyPrints = avgDaily, + AvgDailyPrintsPrevPeriod = avgDailyPrev, + AvgDailyPrintsChangeRate = CalcAppChangeRate(avgDaily, avgDailyPrev) + }, + LabelsByCategory = byCategory, + PrintVolumeTrend = trend, + MostUsedProducts = mostUsed + }; + } + + private static (DateTime rangeStart, DateTime rangeEndExcl) ResolveAppDateRange(DateTime? startDate, DateTime? endDate) + { + var endDay = (endDate ?? DateTime.Today).Date; + var endExcl = endDay.AddDays(1); + var start = (startDate ?? endDay.AddDays(-29)).Date; + if (start >= endExcl) + { + start = endExcl.AddDays(-1); + } + + return (start, endExcl); + } + + private static decimal CalcAppChangeRate(decimal current, decimal previous) + { + if (previous == 0) + { + return current > 0 ? 100m : 0m; + } + + return Math.Round((current - previous) * 100m / previous, 2); + } + private async Task ResolveTemplateProductDefaultValuesJsonAsync( string templateId, string? productId, @@ -896,23 +1104,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ .Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId) .Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State) .Where((lp, l, p, c, t, tpl, pc) => !p.IsDeleted && p.State) - .Where((lp, l, p, c, t, tpl, pc) => - !c.IsDeleted && c.State && - (c.AvailabilityType == "ALL" || - (c.AvailabilityType == "SPECIFIED" && - SqlFunc.Subqueryable() - .Where(loc => loc.CategoryId == c.Id && loc.LocationId == locationId) - .Any()))) + .Where((lp, l, p, c, t, tpl, pc) => !c.IsDeleted && c.State) .Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State) .Where((lp, l, p, c, t, tpl, pc) => !tpl.IsDeleted) .Where((lp, l, p, c, t, tpl, pc) => - pc.Id == null || - (!pc.IsDeleted && pc.State && - (pc.AvailabilityType == "ALL" || - (pc.AvailabilityType == "SPECIFIED" && - SqlFunc.Subqueryable() - .Where(loc => loc.CategoryId == pc.Id && loc.LocationId == locationId) - .Any())))) + pc.Id == null || (!pc.IsDeleted && pc.State)) .WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId) .WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) => (l.LabelName != null && l.LabelName.Contains(keyword!)) || diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/System/UserService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/System/UserService.cs index 51146ff..d6c4072 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/System/UserService.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/System/UserService.cs @@ -9,6 +9,7 @@ using Yi.Framework.Rbac.Application.Contracts.Dtos.User; using Yi.Framework.Rbac.Application.Contracts.IServices; using Yi.Framework.Rbac.Domain.Authorization; using Yi.Framework.Rbac.Domain.Entities; +using Yi.Framework.Rbac.Domain.Helpers; using Yi.Framework.Rbac.Domain.Entities.ValueObjects; using Yi.Framework.Rbac.Domain.Managers; using Yi.Framework.Rbac.Domain.Repositories; @@ -155,15 +156,21 @@ namespace Yi.Framework.Rbac.Application.Services.System var entity = await _repository.GetByIdAsync(id); //更新密码,特殊处理 + var passwordChanged = false; if (!string.IsNullOrWhiteSpace(input.Password)) { - entity.EncryPassword.Password = input.Password; - entity.BuildPassword(); + UserPasswordHelper.ApplyPlainPassword(entity, input.Password); + passwordChanged = true; } await MapToEntityAsync(input, entity); var res1 = await _repository.UpdateAsync(entity); + if (passwordChanged) + { + await UserPasswordHelper.EnsurePasswordColumnsPersistedAsync( + _repository, id, entity.EncryPassword.Password, entity.EncryPassword.Salt); + } await _userManager.GiveUserSetRoleAsync(new List { id }, input.RoleIds); await _userManager.GiveUserSetPostAsync(new List { id }, input.PostIds); return await MapToGetOutputDtoAsync(entity); diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Entities/UserAggregateRoot.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Entities/UserAggregateRoot.cs index 3f00e04..0f5c0b2 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Entities/UserAggregateRoot.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Entities/UserAggregateRoot.cs @@ -4,6 +4,7 @@ using Volo.Abp.Domain.Entities; using Yi.Framework.Core.Data; using Yi.Framework.Core.Helper; using Yi.Framework.Rbac.Domain.Entities.ValueObjects; +using Yi.Framework.Rbac.Domain.Helpers; using Yi.Framework.Rbac.Domain.Shared.Enums; namespace Yi.Framework.Rbac.Domain.Entities @@ -197,19 +198,8 @@ namespace Yi.Framework.Rbac.Domain.Entities /// /// /// - public bool JudgePassword(string password) - { - if (EncryPassword.Salt is null) - { - throw new ArgumentNullException(EncryPassword.Salt); - } - var p = MD5Helper.SHA2Encode(password, EncryPassword.Salt); - if (EncryPassword.Password == MD5Helper.SHA2Encode(password, EncryPassword.Salt)) - { - return true; - } - return false; - } + public bool JudgePassword(string password) => + UserPasswordHelper.VerifyPlainPassword(this, password); } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs index 1a9193b..2a766a1 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs +++ b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs @@ -13,6 +13,7 @@ using Volo.Abp.EventBus.Local; using Volo.Abp.Security.Claims; using Yi.Framework.Core.Helper; using Yi.Framework.Rbac.Domain.Entities; +using Yi.Framework.Rbac.Domain.Helpers; using Yi.Framework.Rbac.Domain.Repositories; using Yi.Framework.Rbac.Domain.Shared.Caches; using Yi.Framework.Rbac.Domain.Shared.Consts; @@ -244,9 +245,10 @@ namespace Yi.Framework.Rbac.Domain.Managers { throw new UserFriendlyException("无效更新!原密码错误!"); } - user.EncryPassword.Password = newPassword; - user.BuildPassword(); + UserPasswordHelper.ApplyPlainPassword(user, newPassword); await _repository.UpdateAsync(user); + await UserPasswordHelper.EnsurePasswordColumnsPersistedAsync( + _repository, userId, user.EncryPassword.Password, user.EncryPassword.Salt); } /// @@ -258,9 +260,11 @@ namespace Yi.Framework.Rbac.Domain.Managers public async Task RestPasswordAsync(Guid userId, string password) { var user = await _repository.GetByIdAsync(userId); - user.EncryPassword.Password = password; - user.BuildPassword(); - return await _repository.UpdateAsync(user); + UserPasswordHelper.ApplyPlainPassword(user, password); + var updated = await _repository.UpdateAsync(user); + await UserPasswordHelper.EnsurePasswordColumnsPersistedAsync( + _repository, userId, user.EncryPassword.Password, user.EncryPassword.Salt); + return updated; } ///