Commit 07d5dea2be04f05deb7e8bca2f1069de034600ab
1 parent
1aa1dca9
5-18代码优化
Showing
30 changed files
with
1360 additions
and
223 deletions
美国版/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 |
| 10 | 10 | |
| 11 | 11 | public bool State { get; set; } = true; |
| 12 | 12 | |
| 13 | + /// <summary> | |
| 14 | + /// 门店可用范围:ALL / SPECIFIED;传了 <see cref="RegionIds"/> 或 <see cref="LocationIds"/> 时自动为 SPECIFIED | |
| 15 | + /// </summary> | |
| 16 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 17 | + | |
| 18 | + /// <summary> | |
| 19 | + /// 适用 Region(多选),<c>fl_group.Id</c>;与 <see cref="GroupIds"/> 合并去重 | |
| 20 | + /// </summary> | |
| 21 | + public List<string>? RegionIds { get; set; } | |
| 22 | + | |
| 23 | + /// <summary> | |
| 24 | + /// 适用 Region(多选),与 <see cref="RegionIds"/> 相同 | |
| 25 | + /// </summary> | |
| 26 | + public List<string>? GroupIds { get; set; } | |
| 27 | + | |
| 28 | + /// <summary> | |
| 29 | + /// 适用门店(多选),<c>location.Id</c>;与 Region 合并后写入 <c>fl_label_multiple_option_location</c> | |
| 30 | + /// </summary> | |
| 31 | + public List<string>? LocationIds { get; set; } | |
| 32 | + | |
| 13 | 33 | public int OrderNum { get; set; } |
| 14 | 34 | } |
| 15 | 35 | ... | ... |
美国版/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 |
| 7 | 7 | public string? Keyword { get; set; } |
| 8 | 8 | |
| 9 | 9 | public bool? State { get; set; } |
| 10 | + | |
| 11 | + /// <summary> | |
| 12 | + /// Region 筛选(<c>fl_group.Id</c>) | |
| 13 | + /// </summary> | |
| 14 | + public string? GroupId { get; set; } | |
| 15 | + | |
| 16 | + /// <summary> | |
| 17 | + /// 门店筛选(<c>location.Id</c>);优先于 <see cref="GroupId"/> | |
| 18 | + /// </summary> | |
| 19 | + public string? LocationId { get; set; } | |
| 10 | 20 | } |
| 11 | 21 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListOutputDto.cs
| ... | ... | @@ -12,8 +12,20 @@ public class LabelMultipleOptionGetListOutputDto |
| 12 | 12 | |
| 13 | 13 | public bool State { get; set; } |
| 14 | 14 | |
| 15 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 16 | + | |
| 15 | 17 | public int OrderNum { get; set; } |
| 16 | 18 | |
| 17 | 19 | public DateTime LastEdited { get; set; } |
| 20 | + | |
| 21 | + /// <summary>列表列 Region</summary> | |
| 22 | + public string Region { get; set; } = string.Empty; | |
| 23 | + | |
| 24 | + /// <summary>列表列 Location</summary> | |
| 25 | + public string Location { get; set; } = string.Empty; | |
| 26 | + | |
| 27 | + public List<string> RegionIds { get; set; } = new(); | |
| 28 | + | |
| 29 | + public List<string> LocationIds { get; set; } = new(); | |
| 18 | 30 | } |
| 19 | 31 | ... | ... |
美国版/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 |
| 13 | 13 | public bool State { get; set; } |
| 14 | 14 | |
| 15 | 15 | public int OrderNum { get; set; } |
| 16 | + | |
| 17 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 18 | + | |
| 19 | + public List<string> RegionIds { get; set; } = new(); | |
| 20 | + | |
| 21 | + public List<string> GroupIds { get; set; } = new(); | |
| 22 | + | |
| 23 | + public List<string> LocationIds { get; set; } = new(); | |
| 16 | 24 | } |
| 17 | 25 | ... | ... |
美国版/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 |
| 8 | 8 | |
| 9 | 9 | public bool State { get; set; } = true; |
| 10 | 10 | |
| 11 | + /// <summary> | |
| 12 | + /// 门店可用范围:ALL / SPECIFIED;传了 <see cref="RegionIds"/> 或 <see cref="LocationIds"/> 时自动为 SPECIFIED | |
| 13 | + /// </summary> | |
| 14 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 15 | + | |
| 16 | + /// <summary> | |
| 17 | + /// 适用 Region(多选),<c>fl_group.Id</c>;与 <see cref="GroupIds"/> 合并去重 | |
| 18 | + /// </summary> | |
| 19 | + public List<string>? RegionIds { get; set; } | |
| 20 | + | |
| 21 | + /// <summary> | |
| 22 | + /// 适用 Region(多选),与 <see cref="RegionIds"/> 相同 | |
| 23 | + /// </summary> | |
| 24 | + public List<string>? GroupIds { get; set; } | |
| 25 | + | |
| 26 | + /// <summary> | |
| 27 | + /// 适用门店(多选),<c>location.Id</c>;与 Region 合并后写入 <c>fl_label_type_location</c> | |
| 28 | + /// </summary> | |
| 29 | + public List<string>? LocationIds { get; set; } | |
| 30 | + | |
| 11 | 31 | public int OrderNum { get; set; } |
| 12 | 32 | } |
| 13 | 33 | ... | ... |
美国版/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 |
| 7 | 7 | public string? Keyword { get; set; } |
| 8 | 8 | |
| 9 | 9 | public bool? State { get; set; } |
| 10 | + | |
| 11 | + /// <summary> | |
| 12 | + /// Region 筛选(<c>fl_group.Id</c>);仅返回在该 Region 下存在标签实例的类型 | |
| 13 | + /// </summary> | |
| 14 | + public string? GroupId { get; set; } | |
| 15 | + | |
| 16 | + /// <summary> | |
| 17 | + /// 门店筛选(<c>location.Id</c>);优先于 <see cref="GroupId"/> | |
| 18 | + /// </summary> | |
| 19 | + public string? LocationId { get; set; } | |
| 10 | 20 | } |
| 11 | 21 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListOutputDto.cs
| ... | ... | @@ -10,10 +10,25 @@ public class LabelTypeGetListOutputDto |
| 10 | 10 | |
| 11 | 11 | public bool State { get; set; } |
| 12 | 12 | |
| 13 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 14 | + | |
| 13 | 15 | public int OrderNum { get; set; } |
| 14 | 16 | |
| 17 | + /// <summary>列表列 No. of Labels:该类型下未删除标签数(统计 <c>fl_label</c>,非库表物理列)</summary> | |
| 15 | 18 | public long NoOfLabels { get; set; } |
| 16 | 19 | |
| 17 | 20 | public DateTime LastEdited { get; set; } |
| 21 | + | |
| 22 | + /// <summary>列表列 Region:由该类型下标签绑定的门店 <c>GroupName</c> 去重拼接</summary> | |
| 23 | + public string Region { get; set; } = string.Empty; | |
| 24 | + | |
| 25 | + /// <summary>列表列 Location:由该类型下标签绑定的门店名去重拼接</summary> | |
| 26 | + public string Location { get; set; } = string.Empty; | |
| 27 | + | |
| 28 | + /// <summary>Region Id(<c>fl_group.Id</c>,由标签门店反推)</summary> | |
| 29 | + public List<string> RegionIds { get; set; } = new(); | |
| 30 | + | |
| 31 | + /// <summary>门店 Id(<c>location.Id</c>,由该类型下标签汇总)</summary> | |
| 32 | + public List<string> LocationIds { get; set; } = new(); | |
| 18 | 33 | } |
| 19 | 34 | ... | ... |
美国版/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 |
| 11 | 11 | public bool State { get; set; } |
| 12 | 12 | |
| 13 | 13 | public int OrderNum { get; set; } |
| 14 | + | |
| 15 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 16 | + | |
| 17 | + public List<string> RegionIds { get; set; } = new(); | |
| 18 | + | |
| 19 | + public List<string> GroupIds { get; set; } = new(); | |
| 20 | + | |
| 21 | + public List<string> LocationIds { get; set; } = new(); | |
| 14 | 22 | } |
| 15 | 23 | ... | ... |
美国版/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 |
| 8 | 8 | /// <summary>打印任务 Id(fl_label_print_task.Id),重打时使用</summary> |
| 9 | 9 | public string TaskId { get; set; } = string.Empty; |
| 10 | 10 | |
| 11 | - /// <summary>标签编码(展示为 Label ID)</summary> | |
| 11 | + /// <summary> | |
| 12 | + /// 列表列 Label ID:门店当日打印序号(<c>yyyyMMdd-n</c>),非 <c>fl_label.LabelCode</c> | |
| 13 | + /// </summary> | |
| 12 | 14 | public string LabelCode { get; set; } = string.Empty; |
| 13 | 15 | |
| 14 | 16 | public string ProductName { get; set; } = "无"; | ... | ... |
美国版/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 |
| 18 | 18 | public Guid? RoleId { get; set; } |
| 19 | 19 | |
| 20 | 20 | /// <summary> |
| 21 | - /// 门店ID(可选) | |
| 21 | + /// Company 筛选(<c>fl_partner.Id</c>);与 <see cref="GroupId"/>、<see cref="LocationId"/> 按「门店优先」解析范围 | |
| 22 | + /// </summary> | |
| 23 | + public string? PartnerId { get; set; } | |
| 24 | + | |
| 25 | + /// <summary> | |
| 26 | + /// Region 筛选(<c>fl_group.Id</c>);未传 <see cref="LocationId"/> 时生效 | |
| 27 | + /// </summary> | |
| 28 | + public string? GroupId { get; set; } | |
| 29 | + | |
| 30 | + /// <summary> | |
| 31 | + /// 门店筛选(<c>location.Id</c>);最精确,传则忽略 <see cref="PartnerId"/> / <see cref="GroupId"/> | |
| 22 | 32 | /// </summary> |
| 23 | 33 | public string? LocationId { get; set; } |
| 24 | 34 | ... | ... |
美国版/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 |
| 17 | 17 | /// <summary>门店电话(来自 location.Phone;空为「无」)</summary> |
| 18 | 18 | public string StorePhone { get; set; } = string.Empty; |
| 19 | 19 | |
| 20 | - /// <summary>营业时间;当前库无字段,固定返回「无」直至业务落库</summary> | |
| 20 | + /// <summary>经营时间(<c>location.OperatingHours</c> 自由文本);库空或未维护时为「无」</summary> | |
| 21 | 21 | public string OperatingHours { get; set; } = string.Empty; |
| 22 | 22 | |
| 23 | 23 | /// <summary>店长姓名;优先取绑定本店且角色名/编码含 manager 的用户</summary> | ... | ... |
美国版/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 |
| 38 | 38 | /// <summary>打印时间(PrintedAt ?? CreationTime)</summary> |
| 39 | 39 | public DateTime PrintedAt { get; set; } |
| 40 | 40 | |
| 41 | - /// <summary>操作人姓名(当前登录账号 Name)</summary> | |
| 41 | + /// <summary>操作人姓名(任务 CreatedBy 对应用户;查看全店日志时为实际打印人)</summary> | |
| 42 | 42 | public string OperatorName { get; set; } = string.Empty; |
| 43 | 43 | |
| 44 | 44 | /// <summary>门店名称</summary> | ... | ... |
美国版/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; |
| 12 | 12 | public interface ILocationAppService : IApplicationService |
| 13 | 13 | { |
| 14 | 14 | /// <summary> |
| 15 | - /// 门店分页列表 | |
| 15 | + /// 门店分页列表;管理员返回全部门店,非管理员仅返回其绑定门店所属 Region(Partner + GroupName)下的门店。 | |
| 16 | 16 | /// </summary> |
| 17 | 17 | /// <param name="input">查询条件</param> |
| 18 | 18 | Task<PagedResultWithPageDto<LocationGetListOutputDto>> GetListAsync(LocationGetListInputVo input); | ... | ... |
美国版/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 |
| 32 | 32 | Task<UsAppLabelPrintOutputDto> ReprintPrintLogAsync(UsAppLabelReprintInputVo input); |
| 33 | 33 | |
| 34 | 34 | /// <summary> |
| 35 | - /// Label Report 统计(卡片 + 分类柱数据 + 7 日趋势 + Top 产品);<c>admin</c> 统计全部,否则仅当前用户。 | |
| 35 | + /// Label Report 统计(卡片 + 分类柱数据 + 7 日趋势 + Top 产品); | |
| 36 | + /// <c>admin</c> 统计全部门店;非管理员仅统计 <c>userlocation</c> 绑定门店内全部打印任务(不按 CreatedBy 过滤)。 | |
| 36 | 37 | /// </summary> |
| 37 | 38 | Task<ReportsLabelReportOutputDto> GetLabelReportAsync(ReportsLabelReportQueryInputVo input); |
| 38 | 39 | |
| ... | ... | @@ -40,4 +41,32 @@ public interface IReportsAppService : IApplicationService |
| 40 | 41 | /// Label Report 导出 PDF |
| 41 | 42 | /// </summary> |
| 42 | 43 | Task<IActionResult> ExportLabelReportPdfAsync(ReportsLabelReportQueryInputVo input); |
| 44 | + | |
| 45 | + /// <summary> | |
| 46 | + /// 按模板统计打印标签数量(分页列表:模板名称 + 打印数)。 | |
| 47 | + /// </summary> | |
| 48 | + /// <remarks> | |
| 49 | + /// 统计 <c>fl_label_print_task</c>,按 <c>TemplateId</c> 分组;数据范围与 <c>label-report</c> 一致: | |
| 50 | + /// 管理员可查全部门店(可按 Company/Region/Location 收窄);非管理员仅 <c>userlocation</c> 绑定门店内全部打印任务。 | |
| 51 | + /// | |
| 52 | + /// 示例请求: | |
| 53 | + /// ```http | |
| 54 | + /// GET /api/app/reports/template-print-stat-list?SkipCount=1&MaxResultCount=20&StartDate=2026-04-07&EndDate=2026-05-18 | |
| 55 | + /// Authorization: Bearer {token} | |
| 56 | + /// ``` | |
| 57 | + /// | |
| 58 | + /// 参数说明: | |
| 59 | + /// - SkipCount / MaxResultCount: 分页(SkipCount 为 1-based 页码约定,与项目其它列表一致) | |
| 60 | + /// - StartDate / EndDate: 统计区间(含起止日;默认近 30 天至今天) | |
| 61 | + /// - PartnerId / GroupId / LocationId: 组织范围筛选 | |
| 62 | + /// - Keyword: 模板名称模糊匹配 | |
| 63 | + /// - Sorting: 可选 <c>PrintedCount desc</c>(默认按打印数降序) | |
| 64 | + /// </remarks> | |
| 65 | + /// <param name="input">分页与筛选条件</param> | |
| 66 | + /// <returns>各模板打印数量列表</returns> | |
| 67 | + /// <response code="200">成功返回分页统计</response> | |
| 68 | + /// <response code="400">入参无效或未登录</response> | |
| 69 | + /// <response code="500">服务器错误</response> | |
| 70 | + Task<PagedResultWithPageDto<ReportsTemplatePrintStatListItemDto>> GetTemplatePrintStatListAsync( | |
| 71 | + ReportsTemplatePrintStatGetListInputVo input); | |
| 43 | 72 | } | ... | ... |
美国版/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 |
| 44 | 44 | /// 按门店 Id 查询 Location 详情(须为当前账号 userlocation 绑定门店) |
| 45 | 45 | /// </summary> |
| 46 | 46 | /// <param name="locationId">门店 Guid 字符串</param> |
| 47 | - /// <returns>店名、地址、电话、营业时间占位、店长信息</returns> | |
| 47 | + /// <returns>店名、地址、电话、经营时间(operatingHours)、店长信息</returns> | |
| 48 | 48 | /// <response code="200">成功</response> |
| 49 | 49 | /// <response code="400">参数非法、未绑定或无权限</response> |
| 50 | 50 | /// <response code="500">服务器错误</response> | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs
| 1 | 1 | using FoodLabeling.Application.Contracts.Dtos.Common; |
| 2 | +using FoodLabeling.Application.Contracts.Dtos.Reports; | |
| 2 | 3 | using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; |
| 3 | 4 | using Volo.Abp.Application.Services; |
| 4 | 5 | |
| ... | ... | @@ -30,7 +31,12 @@ public interface IUsAppLabelingAppService : IApplicationService |
| 30 | 31 | Task<UsAppLabelPrintOutputDto> ReprintAsync(UsAppLabelReprintInputVo input); |
| 31 | 32 | |
| 32 | 33 | /// <summary> |
| 33 | - /// App 打印日志:获取当前登录账号在当前门店打印的记录(分页,时间倒序) | |
| 34 | + /// App 打印日志:当前门店打印记录(分页)。管理员 / Partner 角色可见门店内全部;其它角色仅本人。 | |
| 34 | 35 | /// </summary> |
| 35 | 36 | Task<PagedResultWithPageDto<PrintLogItemDto>> GetPrintLogListAsync(PrintLogGetListInputVo input); |
| 37 | + | |
| 38 | + /// <summary> | |
| 39 | + /// App Label Report:当前门店统计。权限规则与 <see cref="GetPrintLogListAsync"/> 一致。 | |
| 40 | + /// </summary> | |
| 41 | + Task<ReportsLabelReportOutputDto> GetLabelReportAsync(UsAppLabelReportQueryInputVo input); | |
| 36 | 42 | } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelMultipleOptionDbEntity.cs
| ... | ... | @@ -29,5 +29,10 @@ public class FlLabelMultipleOptionDbEntity |
| 29 | 29 | public int OrderNum { get; set; } |
| 30 | 30 | |
| 31 | 31 | public bool State { get; set; } |
| 32 | + | |
| 33 | + /// <summary> | |
| 34 | + /// 门店可用范围:ALL / SPECIFIED | |
| 35 | + /// </summary> | |
| 36 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 32 | 37 | } |
| 33 | 38 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTypeDbEntity.cs
| ... | ... | @@ -27,5 +27,10 @@ public class FlLabelTypeDbEntity |
| 27 | 27 | public int OrderNum { get; set; } |
| 28 | 28 | |
| 29 | 29 | public bool State { get; set; } |
| 30 | + | |
| 31 | + /// <summary> | |
| 32 | + /// 门店可用范围:ALL / SPECIFIED | |
| 33 | + /// </summary> | |
| 34 | + public string AvailabilityType { get; set; } = "ALL"; | |
| 30 | 35 | } |
| 31 | 36 | ... | ... |
美国版/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; |
| 3 | 3 | using FoodLabeling.Application.Contracts.Dtos.LabelMultipleOption; |
| 4 | 4 | using FoodLabeling.Application.Contracts.IServices; |
| 5 | 5 | using FoodLabeling.Application.Services.DbModels; |
| 6 | +using FoodLabeling.Domain.Entities; | |
| 6 | 7 | using SqlSugar; |
| 7 | 8 | using Volo.Abp; |
| 8 | 9 | using Volo.Abp.Application.Services; |
| ... | ... | @@ -26,12 +27,16 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 26 | 27 | { |
| 27 | 28 | RefAsync<int> total = 0; |
| 28 | 29 | var keyword = input.Keyword?.Trim(); |
| 30 | + var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( | |
| 31 | + _dbContext.SqlSugarClient, input.GroupId, input.LocationId); | |
| 29 | 32 | |
| 30 | 33 | var query = _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionDbEntity>() |
| 31 | 34 | .Where(x => !x.IsDeleted) |
| 32 | 35 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.OptionCode.Contains(keyword!) || x.OptionName.Contains(keyword!)) |
| 33 | 36 | .WhereIF(input.State != null, x => x.State == input.State); |
| 34 | 37 | |
| 38 | + query = ApplyMultipleOptionScopeFilter(query, scopedLocationIds); | |
| 39 | + | |
| 35 | 40 | if (!string.IsNullOrWhiteSpace(input.Sorting)) |
| 36 | 41 | { |
| 37 | 42 | query = query.OrderBy(input.Sorting); |
| ... | ... | @@ -42,15 +47,26 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 42 | 47 | } |
| 43 | 48 | |
| 44 | 49 | var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); |
| 45 | - var items = entities.Select(x => new LabelMultipleOptionGetListOutputDto | |
| 50 | + var scopeMap = await BuildMultipleOptionScopeMapAsync(entities); | |
| 51 | + | |
| 52 | + var items = entities.Select(x => | |
| 46 | 53 | { |
| 47 | - Id = x.Id, | |
| 48 | - OptionCode = x.OptionCode, | |
| 49 | - OptionName = x.OptionName, | |
| 50 | - OptionValuesJson = x.OptionValuesJson, | |
| 51 | - State = x.State, | |
| 52 | - OrderNum = x.OrderNum, | |
| 53 | - LastEdited = x.LastModificationTime ?? x.CreationTime | |
| 54 | + scopeMap.TryGetValue(x.Id, out var scope); | |
| 55 | + return new LabelMultipleOptionGetListOutputDto | |
| 56 | + { | |
| 57 | + Id = x.Id, | |
| 58 | + OptionCode = x.OptionCode, | |
| 59 | + OptionName = x.OptionName, | |
| 60 | + OptionValuesJson = x.OptionValuesJson, | |
| 61 | + State = x.State, | |
| 62 | + AvailabilityType = x.AvailabilityType, | |
| 63 | + OrderNum = x.OrderNum, | |
| 64 | + LastEdited = x.LastModificationTime ?? x.CreationTime, | |
| 65 | + Region = scope?.Region ?? EmptyDisplay, | |
| 66 | + Location = scope?.Location ?? EmptyDisplay, | |
| 67 | + RegionIds = scope?.RegionIds ?? new List<string>(), | |
| 68 | + LocationIds = scope?.LocationIds ?? new List<string>() | |
| 69 | + }; | |
| 54 | 70 | }).ToList(); |
| 55 | 71 | |
| 56 | 72 | return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); |
| ... | ... | @@ -65,15 +81,21 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 65 | 81 | throw new UserFriendlyException("多选项不存在"); |
| 66 | 82 | } |
| 67 | 83 | |
| 68 | - return new LabelMultipleOptionGetOutputDto | |
| 84 | + var dto = MapToGetOutput(entity); | |
| 85 | + if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) | |
| 69 | 86 | { |
| 70 | - Id = entity.Id, | |
| 71 | - OptionCode = entity.OptionCode, | |
| 72 | - OptionName = entity.OptionName, | |
| 73 | - OptionValuesJson = entity.OptionValuesJson, | |
| 74 | - State = entity.State, | |
| 75 | - OrderNum = entity.OrderNum | |
| 76 | - }; | |
| 87 | + var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionLocationDbEntity>() | |
| 88 | + .Where(x => x.MultipleOptionId == entity.Id) | |
| 89 | + .Select(x => x.LocationId) | |
| 90 | + .ToListAsync(); | |
| 91 | + dto.LocationIds = LocationScopeBindingHelper.NormalizeIds(locationIds); | |
| 92 | + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( | |
| 93 | + _dbContext.SqlSugarClient, locationIds); | |
| 94 | + dto.RegionIds = regionIds; | |
| 95 | + dto.GroupIds = regionIds; | |
| 96 | + } | |
| 97 | + | |
| 98 | + return dto; | |
| 77 | 99 | } |
| 78 | 100 | |
| 79 | 101 | public async Task<LabelMultipleOptionGetOutputDto> CreateAsync(LabelMultipleOptionCreateInputVo input) |
| ... | ... | @@ -85,6 +107,8 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 85 | 107 | throw new UserFriendlyException("多选项编码和名称不能为空"); |
| 86 | 108 | } |
| 87 | 109 | |
| 110 | + var (availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input); | |
| 111 | + | |
| 88 | 112 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionDbEntity>() |
| 89 | 113 | .AnyAsync(x => !x.IsDeleted && (x.OptionCode == code || x.OptionName == name)); |
| 90 | 114 | if (duplicated) |
| ... | ... | @@ -92,17 +116,27 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 92 | 116 | throw new UserFriendlyException("多选项编码或名称已存在"); |
| 93 | 117 | } |
| 94 | 118 | |
| 119 | + var now = DateTime.Now; | |
| 120 | + var currentUserId = CurrentUser?.Id?.ToString(); | |
| 95 | 121 | var entity = new FlLabelMultipleOptionDbEntity |
| 96 | 122 | { |
| 97 | 123 | Id = _guidGenerator.Create().ToString(), |
| 124 | + IsDeleted = false, | |
| 125 | + CreationTime = now, | |
| 126 | + CreatorId = currentUserId, | |
| 127 | + LastModificationTime = now, | |
| 128 | + LastModifierId = currentUserId, | |
| 129 | + ConcurrencyStamp = _guidGenerator.Create().ToString("N"), | |
| 98 | 130 | OptionCode = code, |
| 99 | 131 | OptionName = name, |
| 100 | 132 | OptionValuesJson = input.OptionValuesJson?.Trim(), |
| 101 | 133 | State = input.State, |
| 134 | + AvailabilityType = availabilityType, | |
| 102 | 135 | OrderNum = input.OrderNum |
| 103 | 136 | }; |
| 104 | 137 | |
| 105 | 138 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); |
| 139 | + await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); | |
| 106 | 140 | return await GetAsync(entity.Id); |
| 107 | 141 | } |
| 108 | 142 | |
| ... | ... | @@ -122,6 +156,8 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 122 | 156 | throw new UserFriendlyException("多选项编码和名称不能为空"); |
| 123 | 157 | } |
| 124 | 158 | |
| 159 | + var (availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input); | |
| 160 | + | |
| 125 | 161 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionDbEntity>() |
| 126 | 162 | .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.OptionCode == code || x.OptionName == name)); |
| 127 | 163 | if (duplicated) |
| ... | ... | @@ -133,11 +169,14 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 133 | 169 | entity.OptionName = name; |
| 134 | 170 | entity.OptionValuesJson = input.OptionValuesJson?.Trim(); |
| 135 | 171 | entity.State = input.State; |
| 172 | + entity.AvailabilityType = availabilityType; | |
| 136 | 173 | entity.OrderNum = input.OrderNum; |
| 137 | 174 | entity.LastModificationTime = DateTime.Now; |
| 138 | 175 | entity.LastModifierId = CurrentUser?.Id?.ToString(); |
| 139 | 176 | |
| 140 | 177 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 178 | + await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, | |
| 179 | + entity.LastModificationTime ?? DateTime.Now); | |
| 141 | 180 | return await GetAsync(id); |
| 142 | 181 | } |
| 143 | 182 | |
| ... | ... | @@ -150,12 +189,266 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 150 | 189 | return; |
| 151 | 190 | } |
| 152 | 191 | |
| 192 | + await _dbContext.SqlSugarClient.Deleteable<FlLabelMultipleOptionLocationDbEntity>() | |
| 193 | + .Where(x => x.MultipleOptionId == id) | |
| 194 | + .ExecuteCommandAsync(); | |
| 195 | + | |
| 153 | 196 | entity.IsDeleted = true; |
| 154 | 197 | entity.LastModificationTime = DateTime.Now; |
| 155 | 198 | entity.LastModifierId = CurrentUser?.Id?.ToString(); |
| 156 | 199 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 157 | 200 | } |
| 158 | 201 | |
| 202 | + private const string EmptyDisplay = "无"; | |
| 203 | + private const string AllRegionsDisplay = "All Regions"; | |
| 204 | + private const string AllLocationsDisplay = "All Locations"; | |
| 205 | + | |
| 206 | + private async Task<(string AvailabilityType, List<string> LocationIds)> ResolveMultipleOptionScopeForSaveAsync( | |
| 207 | + LabelMultipleOptionCreateInputVo input) | |
| 208 | + { | |
| 209 | + var regionIds = NormalizeRegionIds(input); | |
| 210 | + var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); | |
| 211 | + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); | |
| 212 | + | |
| 213 | + var hasScopeArrays = input.RegionIds is not null || input.GroupIds is not null || input.LocationIds is not null; | |
| 214 | + if (regionIds.Count > 0 || explicitLocationIds.Count > 0) | |
| 215 | + { | |
| 216 | + availabilityType = "SPECIFIED"; | |
| 217 | + } | |
| 218 | + else if (hasScopeArrays && string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase)) | |
| 219 | + { | |
| 220 | + availabilityType = "ALL"; | |
| 221 | + } | |
| 222 | + | |
| 223 | + if (availabilityType != "ALL" && availabilityType != "SPECIFIED") | |
| 224 | + { | |
| 225 | + throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); | |
| 226 | + } | |
| 227 | + | |
| 228 | + if (availabilityType == "ALL") | |
| 229 | + { | |
| 230 | + return ("ALL", new List<string>()); | |
| 231 | + } | |
| 232 | + | |
| 233 | + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | |
| 234 | + _dbContext.SqlSugarClient, (IReadOnlyList<string>?)null, regionIds, explicitLocationIds); | |
| 235 | + if (merged.Count == 0) | |
| 236 | + { | |
| 237 | + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); | |
| 238 | + } | |
| 239 | + | |
| 240 | + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); | |
| 241 | + return ("SPECIFIED", merged); | |
| 242 | + } | |
| 243 | + | |
| 244 | + private static List<string> NormalizeRegionIds(LabelMultipleOptionCreateInputVo input) | |
| 245 | + { | |
| 246 | + var merged = new HashSet<string>(StringComparer.Ordinal); | |
| 247 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds)) | |
| 248 | + { | |
| 249 | + merged.Add(id); | |
| 250 | + } | |
| 251 | + | |
| 252 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds)) | |
| 253 | + { | |
| 254 | + merged.Add(id); | |
| 255 | + } | |
| 256 | + | |
| 257 | + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); | |
| 258 | + } | |
| 259 | + | |
| 260 | + private async Task SaveMultipleOptionLocationsAsync( | |
| 261 | + string multipleOptionId, | |
| 262 | + string availabilityType, | |
| 263 | + List<string> locationIds, | |
| 264 | + string? currentUserId, | |
| 265 | + DateTime now) | |
| 266 | + { | |
| 267 | + await _dbContext.SqlSugarClient.Deleteable<FlLabelMultipleOptionLocationDbEntity>() | |
| 268 | + .Where(x => x.MultipleOptionId == multipleOptionId) | |
| 269 | + .ExecuteCommandAsync(); | |
| 270 | + | |
| 271 | + if (availabilityType != "SPECIFIED" || locationIds.Count == 0) | |
| 272 | + { | |
| 273 | + return; | |
| 274 | + } | |
| 275 | + | |
| 276 | + var rows = locationIds.Select(locId => new FlLabelMultipleOptionLocationDbEntity | |
| 277 | + { | |
| 278 | + Id = _guidGenerator.Create().ToString(), | |
| 279 | + MultipleOptionId = multipleOptionId, | |
| 280 | + LocationId = locId, | |
| 281 | + CreationTime = now, | |
| 282 | + CreatorId = currentUserId | |
| 283 | + }).ToList(); | |
| 284 | + | |
| 285 | + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); | |
| 286 | + } | |
| 287 | + | |
| 288 | + private static LabelMultipleOptionGetOutputDto MapToGetOutput(FlLabelMultipleOptionDbEntity x) | |
| 289 | + { | |
| 290 | + return new LabelMultipleOptionGetOutputDto | |
| 291 | + { | |
| 292 | + Id = x.Id, | |
| 293 | + OptionCode = x.OptionCode, | |
| 294 | + OptionName = x.OptionName, | |
| 295 | + OptionValuesJson = x.OptionValuesJson, | |
| 296 | + State = x.State, | |
| 297 | + OrderNum = x.OrderNum, | |
| 298 | + AvailabilityType = x.AvailabilityType | |
| 299 | + }; | |
| 300 | + } | |
| 301 | + | |
| 302 | + private static ISugarQueryable<FlLabelMultipleOptionDbEntity> ApplyMultipleOptionScopeFilter( | |
| 303 | + ISugarQueryable<FlLabelMultipleOptionDbEntity> query, | |
| 304 | + List<string>? scopedLocationIds) | |
| 305 | + { | |
| 306 | + if (scopedLocationIds is null) | |
| 307 | + { | |
| 308 | + return query; | |
| 309 | + } | |
| 310 | + | |
| 311 | + if (scopedLocationIds.Count == 0) | |
| 312 | + { | |
| 313 | + return query.Where(o => o.AvailabilityType == "ALL"); | |
| 314 | + } | |
| 315 | + | |
| 316 | + return query.Where(o => | |
| 317 | + o.AvailabilityType == "ALL" || | |
| 318 | + SqlFunc.Subqueryable<FlLabelMultipleOptionLocationDbEntity>() | |
| 319 | + .Where(ol => ol.MultipleOptionId == o.Id && scopedLocationIds.Contains(ol.LocationId)) | |
| 320 | + .Any()); | |
| 321 | + } | |
| 322 | + | |
| 323 | + private async Task<Dictionary<string, MultipleOptionScopeData>> BuildMultipleOptionScopeMapAsync( | |
| 324 | + List<FlLabelMultipleOptionDbEntity> entities) | |
| 325 | + { | |
| 326 | + var result = new Dictionary<string, MultipleOptionScopeData>(StringComparer.Ordinal); | |
| 327 | + if (entities.Count == 0) | |
| 328 | + { | |
| 329 | + return result; | |
| 330 | + } | |
| 331 | + | |
| 332 | + foreach (var e in entities.Where(x => | |
| 333 | + !string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))) | |
| 334 | + { | |
| 335 | + result[e.Id] = new MultipleOptionScopeData | |
| 336 | + { | |
| 337 | + Region = AllRegionsDisplay, | |
| 338 | + Location = AllLocationsDisplay, | |
| 339 | + RegionIds = new List<string>(), | |
| 340 | + LocationIds = new List<string>() | |
| 341 | + }; | |
| 342 | + } | |
| 343 | + | |
| 344 | + var specifiedIds = entities | |
| 345 | + .Where(x => string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) | |
| 346 | + .Select(x => x.Id) | |
| 347 | + .ToList(); | |
| 348 | + if (specifiedIds.Count == 0) | |
| 349 | + { | |
| 350 | + return result; | |
| 351 | + } | |
| 352 | + | |
| 353 | + var links = await _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionLocationDbEntity>() | |
| 354 | + .Where(x => specifiedIds.Contains(x.MultipleOptionId)) | |
| 355 | + .ToListAsync(); | |
| 356 | + | |
| 357 | + var locIdSet = links | |
| 358 | + .Select(x => x.LocationId) | |
| 359 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 360 | + .Select(x => x.Trim()) | |
| 361 | + .Distinct(StringComparer.Ordinal) | |
| 362 | + .ToList(); | |
| 363 | + | |
| 364 | + var locById = new Dictionary<string, LocationAggregateRoot>(StringComparer.Ordinal); | |
| 365 | + if (locIdSet.Count > 0) | |
| 366 | + { | |
| 367 | + var guidList = locIdSet.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); | |
| 368 | + if (guidList.Count > 0) | |
| 369 | + { | |
| 370 | + var locs = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>() | |
| 371 | + .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) | |
| 372 | + .ToListAsync(); | |
| 373 | + foreach (var loc in locs) | |
| 374 | + { | |
| 375 | + locById[loc.Id.ToString()] = loc; | |
| 376 | + } | |
| 377 | + } | |
| 378 | + } | |
| 379 | + | |
| 380 | + foreach (var optionId in specifiedIds) | |
| 381 | + { | |
| 382 | + var optionLinks = links.Where(x => x.MultipleOptionId == optionId).ToList(); | |
| 383 | + var locationIds = LocationScopeBindingHelper.NormalizeIds( | |
| 384 | + optionLinks.Select(x => x.LocationId).ToList()); | |
| 385 | + | |
| 386 | + if (optionLinks.Count == 0) | |
| 387 | + { | |
| 388 | + result[optionId] = new MultipleOptionScopeData | |
| 389 | + { | |
| 390 | + Region = EmptyDisplay, | |
| 391 | + Location = EmptyDisplay, | |
| 392 | + RegionIds = new List<string>(), | |
| 393 | + LocationIds = new List<string>() | |
| 394 | + }; | |
| 395 | + continue; | |
| 396 | + } | |
| 397 | + | |
| 398 | + var regions = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | |
| 399 | + var locationNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | |
| 400 | + foreach (var lid in locationIds) | |
| 401 | + { | |
| 402 | + if (!locById.TryGetValue(lid, out var loc)) | |
| 403 | + { | |
| 404 | + continue; | |
| 405 | + } | |
| 406 | + | |
| 407 | + var groupName = loc.GroupName?.Trim(); | |
| 408 | + if (!string.IsNullOrEmpty(groupName)) | |
| 409 | + { | |
| 410 | + regions.Add(groupName); | |
| 411 | + } | |
| 412 | + | |
| 413 | + var locName = loc.LocationName?.Trim(); | |
| 414 | + if (string.IsNullOrEmpty(locName)) | |
| 415 | + { | |
| 416 | + locName = loc.LocationCode?.Trim(); | |
| 417 | + } | |
| 418 | + | |
| 419 | + if (!string.IsNullOrEmpty(locName)) | |
| 420 | + { | |
| 421 | + locationNames.Add(locName); | |
| 422 | + } | |
| 423 | + } | |
| 424 | + | |
| 425 | + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( | |
| 426 | + _dbContext.SqlSugarClient, locationIds); | |
| 427 | + | |
| 428 | + result[optionId] = new MultipleOptionScopeData | |
| 429 | + { | |
| 430 | + Region = regions.Count > 0 | |
| 431 | + ? string.Join(", ", regions.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) | |
| 432 | + : EmptyDisplay, | |
| 433 | + Location = locationNames.Count > 0 | |
| 434 | + ? string.Join(", ", locationNames.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) | |
| 435 | + : EmptyDisplay, | |
| 436 | + RegionIds = regionIds, | |
| 437 | + LocationIds = locationIds | |
| 438 | + }; | |
| 439 | + } | |
| 440 | + | |
| 441 | + return result; | |
| 442 | + } | |
| 443 | + | |
| 444 | + private sealed class MultipleOptionScopeData | |
| 445 | + { | |
| 446 | + public string Region { get; init; } = string.Empty; | |
| 447 | + public string Location { get; init; } = string.Empty; | |
| 448 | + public List<string> RegionIds { get; init; } = new(); | |
| 449 | + public List<string> LocationIds { get; init; } = new(); | |
| 450 | + } | |
| 451 | + | |
| 159 | 452 | private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items) |
| 160 | 453 | { |
| 161 | 454 | var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; |
| ... | ... | @@ -171,4 +464,3 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 171 | 464 | }; |
| 172 | 465 | } |
| 173 | 466 | } |
| 174 | - | ... | ... |
美国版/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; |
| 3 | 3 | using FoodLabeling.Application.Contracts.Dtos.LabelType; |
| 4 | 4 | using FoodLabeling.Application.Contracts.IServices; |
| 5 | 5 | using FoodLabeling.Application.Services.DbModels; |
| 6 | +using FoodLabeling.Domain.Entities; | |
| 6 | 7 | using SqlSugar; |
| 7 | 8 | using Volo.Abp; |
| 8 | 9 | using Volo.Abp.Application.Services; |
| ... | ... | @@ -26,12 +27,16 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 26 | 27 | { |
| 27 | 28 | RefAsync<int> total = 0; |
| 28 | 29 | var keyword = input.Keyword?.Trim(); |
| 30 | + var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( | |
| 31 | + _dbContext.SqlSugarClient, input.GroupId, input.LocationId); | |
| 29 | 32 | |
| 30 | 33 | var query = _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>() |
| 31 | 34 | .Where(x => !x.IsDeleted) |
| 32 | 35 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.TypeCode.Contains(keyword!) || x.TypeName.Contains(keyword!)) |
| 33 | 36 | .WhereIF(input.State != null, x => x.State == input.State); |
| 34 | 37 | |
| 38 | + query = ApplyLabelTypeScopeFilter(query, scopedLocationIds); | |
| 39 | + | |
| 35 | 40 | if (!string.IsNullOrWhiteSpace(input.Sorting)) |
| 36 | 41 | { |
| 37 | 42 | query = query.OrderBy(input.Sorting); |
| ... | ... | @@ -44,23 +49,28 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 44 | 49 | var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); |
| 45 | 50 | var ids = entities.Select(x => x.Id).ToList(); |
| 46 | 51 | |
| 47 | - var countRows = await _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>() | |
| 48 | - .Where(x => !x.IsDeleted) | |
| 49 | - .Where(x => x.LabelTypeId != null && ids.Contains(x.LabelTypeId)) | |
| 50 | - .GroupBy(x => x.LabelTypeId) | |
| 51 | - .Select(x => new { TypeId = x.LabelTypeId, Count = SqlFunc.AggregateCount(x.Id) }) | |
| 52 | - .ToListAsync(); | |
| 53 | - var countMap = countRows.ToDictionary(x => x.TypeId!, x => (long)x.Count); | |
| 52 | + var countMap = await BuildTypeLabelStatsMapAsync(ids, scopedLocationIds); | |
| 53 | + var scopeMap = await BuildTypeConfiguredScopeMapAsync(entities); | |
| 54 | 54 | |
| 55 | - var items = entities.Select(x => new LabelTypeGetListOutputDto | |
| 55 | + var items = entities.Select(x => | |
| 56 | 56 | { |
| 57 | - Id = x.Id, | |
| 58 | - TypeCode = x.TypeCode, | |
| 59 | - TypeName = x.TypeName, | |
| 60 | - State = x.State, | |
| 61 | - OrderNum = x.OrderNum, | |
| 62 | - NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0, | |
| 63 | - LastEdited = x.LastModificationTime ?? x.CreationTime | |
| 57 | + scopeMap.TryGetValue(x.Id, out var scope); | |
| 58 | + countMap.TryGetValue(x.Id, out var stats); | |
| 59 | + return new LabelTypeGetListOutputDto | |
| 60 | + { | |
| 61 | + Id = x.Id, | |
| 62 | + TypeCode = x.TypeCode, | |
| 63 | + TypeName = x.TypeName, | |
| 64 | + State = x.State, | |
| 65 | + AvailabilityType = x.AvailabilityType, | |
| 66 | + OrderNum = x.OrderNum, | |
| 67 | + NoOfLabels = stats?.Count ?? 0, | |
| 68 | + LastEdited = stats?.MaxEdited ?? x.LastModificationTime ?? x.CreationTime, | |
| 69 | + Region = scope?.Region ?? EmptyDisplay, | |
| 70 | + Location = scope?.Location ?? EmptyDisplay, | |
| 71 | + RegionIds = scope?.RegionIds ?? new List<string>(), | |
| 72 | + LocationIds = scope?.LocationIds ?? new List<string>() | |
| 73 | + }; | |
| 64 | 74 | }).ToList(); |
| 65 | 75 | |
| 66 | 76 | return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); |
| ... | ... | @@ -75,7 +85,21 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 75 | 85 | throw new UserFriendlyException("标签类型不存在"); |
| 76 | 86 | } |
| 77 | 87 | |
| 78 | - return MapToGetOutput(entity); | |
| 88 | + var dto = MapToGetOutput(entity); | |
| 89 | + if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) | |
| 90 | + { | |
| 91 | + var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeLocationDbEntity>() | |
| 92 | + .Where(x => x.LabelTypeId == entity.Id) | |
| 93 | + .Select(x => x.LocationId) | |
| 94 | + .ToListAsync(); | |
| 95 | + dto.LocationIds = LocationScopeBindingHelper.NormalizeIds(locationIds); | |
| 96 | + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( | |
| 97 | + _dbContext.SqlSugarClient, locationIds); | |
| 98 | + dto.RegionIds = regionIds; | |
| 99 | + dto.GroupIds = regionIds; | |
| 100 | + } | |
| 101 | + | |
| 102 | + return dto; | |
| 79 | 103 | } |
| 80 | 104 | |
| 81 | 105 | public async Task<LabelTypeGetOutputDto> CreateAsync(LabelTypeCreateInputVo input) |
| ... | ... | @@ -87,6 +111,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 87 | 111 | throw new UserFriendlyException("类型编码和名称不能为空"); |
| 88 | 112 | } |
| 89 | 113 | |
| 114 | + var (availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input); | |
| 115 | + | |
| 90 | 116 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>() |
| 91 | 117 | .AnyAsync(x => !x.IsDeleted && (x.TypeCode == code || x.TypeName == name)); |
| 92 | 118 | if (duplicated) |
| ... | ... | @@ -94,16 +120,26 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 94 | 120 | throw new UserFriendlyException("类型编码或名称已存在"); |
| 95 | 121 | } |
| 96 | 122 | |
| 123 | + var now = DateTime.Now; | |
| 124 | + var currentUserId = CurrentUser?.Id?.ToString(); | |
| 97 | 125 | var entity = new FlLabelTypeDbEntity |
| 98 | 126 | { |
| 99 | 127 | Id = _guidGenerator.Create().ToString(), |
| 128 | + IsDeleted = false, | |
| 129 | + CreationTime = now, | |
| 130 | + CreatorId = currentUserId, | |
| 131 | + LastModificationTime = now, | |
| 132 | + LastModifierId = currentUserId, | |
| 133 | + ConcurrencyStamp = _guidGenerator.Create().ToString("N"), | |
| 100 | 134 | TypeCode = code, |
| 101 | 135 | TypeName = name, |
| 102 | 136 | State = input.State, |
| 137 | + AvailabilityType = availabilityType, | |
| 103 | 138 | OrderNum = input.OrderNum |
| 104 | 139 | }; |
| 105 | 140 | |
| 106 | 141 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); |
| 142 | + await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); | |
| 107 | 143 | return await GetAsync(entity.Id); |
| 108 | 144 | } |
| 109 | 145 | |
| ... | ... | @@ -123,6 +159,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 123 | 159 | throw new UserFriendlyException("类型编码和名称不能为空"); |
| 124 | 160 | } |
| 125 | 161 | |
| 162 | + var (availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input); | |
| 163 | + | |
| 126 | 164 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>() |
| 127 | 165 | .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.TypeCode == code || x.TypeName == name)); |
| 128 | 166 | if (duplicated) |
| ... | ... | @@ -133,11 +171,14 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 133 | 171 | entity.TypeCode = code; |
| 134 | 172 | entity.TypeName = name; |
| 135 | 173 | entity.State = input.State; |
| 174 | + entity.AvailabilityType = availabilityType; | |
| 136 | 175 | entity.OrderNum = input.OrderNum; |
| 137 | 176 | entity.LastModificationTime = DateTime.Now; |
| 138 | 177 | entity.LastModifierId = CurrentUser?.Id?.ToString(); |
| 139 | 178 | |
| 140 | 179 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 180 | + await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, | |
| 181 | + entity.LastModificationTime ?? DateTime.Now); | |
| 141 | 182 | return await GetAsync(id); |
| 142 | 183 | } |
| 143 | 184 | |
| ... | ... | @@ -157,12 +198,304 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 157 | 198 | throw new UserFriendlyException("该标签类型已被标签引用,无法删除"); |
| 158 | 199 | } |
| 159 | 200 | |
| 201 | + await _dbContext.SqlSugarClient.Deleteable<FlLabelTypeLocationDbEntity>() | |
| 202 | + .Where(x => x.LabelTypeId == id) | |
| 203 | + .ExecuteCommandAsync(); | |
| 204 | + | |
| 160 | 205 | entity.IsDeleted = true; |
| 161 | 206 | entity.LastModificationTime = DateTime.Now; |
| 162 | 207 | entity.LastModifierId = CurrentUser?.Id?.ToString(); |
| 163 | 208 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 164 | 209 | } |
| 165 | 210 | |
| 211 | + private const string EmptyDisplay = "无"; | |
| 212 | + private const string AllRegionsDisplay = "All Regions"; | |
| 213 | + private const string AllLocationsDisplay = "All Locations"; | |
| 214 | + | |
| 215 | + private async Task<(string AvailabilityType, List<string> LocationIds)> ResolveTypeScopeForSaveAsync( | |
| 216 | + LabelTypeCreateInputVo input) | |
| 217 | + { | |
| 218 | + var regionIds = NormalizeRegionIds(input); | |
| 219 | + var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); | |
| 220 | + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); | |
| 221 | + | |
| 222 | + var hasScopeArrays = input.RegionIds is not null || input.GroupIds is not null || input.LocationIds is not null; | |
| 223 | + if (regionIds.Count > 0 || explicitLocationIds.Count > 0) | |
| 224 | + { | |
| 225 | + availabilityType = "SPECIFIED"; | |
| 226 | + } | |
| 227 | + else if (hasScopeArrays && string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase)) | |
| 228 | + { | |
| 229 | + availabilityType = "ALL"; | |
| 230 | + } | |
| 231 | + | |
| 232 | + if (availabilityType != "ALL" && availabilityType != "SPECIFIED") | |
| 233 | + { | |
| 234 | + throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); | |
| 235 | + } | |
| 236 | + | |
| 237 | + if (availabilityType == "ALL") | |
| 238 | + { | |
| 239 | + return ("ALL", new List<string>()); | |
| 240 | + } | |
| 241 | + | |
| 242 | + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | |
| 243 | + _dbContext.SqlSugarClient, (IReadOnlyList<string>?)null, regionIds, explicitLocationIds); | |
| 244 | + if (merged.Count == 0) | |
| 245 | + { | |
| 246 | + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); | |
| 247 | + } | |
| 248 | + | |
| 249 | + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); | |
| 250 | + return ("SPECIFIED", merged); | |
| 251 | + } | |
| 252 | + | |
| 253 | + private static List<string> NormalizeRegionIds(LabelTypeCreateInputVo input) | |
| 254 | + { | |
| 255 | + var merged = new HashSet<string>(StringComparer.Ordinal); | |
| 256 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds)) | |
| 257 | + { | |
| 258 | + merged.Add(id); | |
| 259 | + } | |
| 260 | + | |
| 261 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds)) | |
| 262 | + { | |
| 263 | + merged.Add(id); | |
| 264 | + } | |
| 265 | + | |
| 266 | + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); | |
| 267 | + } | |
| 268 | + | |
| 269 | + private static ISugarQueryable<FlLabelTypeDbEntity> ApplyLabelTypeScopeFilter( | |
| 270 | + ISugarQueryable<FlLabelTypeDbEntity> query, | |
| 271 | + List<string>? scopedLocationIds) | |
| 272 | + { | |
| 273 | + if (scopedLocationIds is null) | |
| 274 | + { | |
| 275 | + return query; | |
| 276 | + } | |
| 277 | + | |
| 278 | + if (scopedLocationIds.Count == 0) | |
| 279 | + { | |
| 280 | + return query.Where(t => t.AvailabilityType == "ALL"); | |
| 281 | + } | |
| 282 | + | |
| 283 | + return query.Where(t => | |
| 284 | + t.AvailabilityType == "ALL" || | |
| 285 | + SqlFunc.Subqueryable<FlLabelTypeLocationDbEntity>() | |
| 286 | + .Where(tl => tl.LabelTypeId == t.Id && scopedLocationIds.Contains(tl.LocationId)) | |
| 287 | + .Any()); | |
| 288 | + } | |
| 289 | + | |
| 290 | + private async Task SaveTypeLocationsAsync( | |
| 291 | + string labelTypeId, | |
| 292 | + string availabilityType, | |
| 293 | + List<string> locationIds, | |
| 294 | + string? currentUserId, | |
| 295 | + DateTime now) | |
| 296 | + { | |
| 297 | + await _dbContext.SqlSugarClient.Deleteable<FlLabelTypeLocationDbEntity>() | |
| 298 | + .Where(x => x.LabelTypeId == labelTypeId) | |
| 299 | + .ExecuteCommandAsync(); | |
| 300 | + | |
| 301 | + if (availabilityType != "SPECIFIED" || locationIds.Count == 0) | |
| 302 | + { | |
| 303 | + return; | |
| 304 | + } | |
| 305 | + | |
| 306 | + var rows = locationIds.Select(locId => new FlLabelTypeLocationDbEntity | |
| 307 | + { | |
| 308 | + Id = _guidGenerator.Create().ToString(), | |
| 309 | + LabelTypeId = labelTypeId, | |
| 310 | + LocationId = locId, | |
| 311 | + CreationTime = now, | |
| 312 | + CreatorId = currentUserId | |
| 313 | + }).ToList(); | |
| 314 | + | |
| 315 | + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync(); | |
| 316 | + } | |
| 317 | + | |
| 318 | + private async Task<Dictionary<string, TypeLabelStats>> BuildTypeLabelStatsMapAsync( | |
| 319 | + List<string> typeIds, | |
| 320 | + List<string>? scopedLocationIds) | |
| 321 | + { | |
| 322 | + var result = new Dictionary<string, TypeLabelStats>(StringComparer.Ordinal); | |
| 323 | + if (typeIds.Count == 0) | |
| 324 | + { | |
| 325 | + return result; | |
| 326 | + } | |
| 327 | + | |
| 328 | + var labelQuery = _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>() | |
| 329 | + .Where(x => !x.IsDeleted) | |
| 330 | + .Where(x => x.LabelTypeId != null && typeIds.Contains(x.LabelTypeId)); | |
| 331 | + | |
| 332 | + labelQuery = ApplyLabelScopeOnLabels(labelQuery, scopedLocationIds); | |
| 333 | + | |
| 334 | + var rows = await labelQuery | |
| 335 | + .Select(x => new { x.LabelTypeId, x.CreationTime, x.LastModificationTime }) | |
| 336 | + .ToListAsync(); | |
| 337 | + | |
| 338 | + foreach (var g in rows.GroupBy(x => x.LabelTypeId!)) | |
| 339 | + { | |
| 340 | + result[g.Key] = new TypeLabelStats | |
| 341 | + { | |
| 342 | + Count = g.Count(), | |
| 343 | + MaxEdited = g.Max(l => l.LastModificationTime ?? l.CreationTime) | |
| 344 | + }; | |
| 345 | + } | |
| 346 | + | |
| 347 | + return result; | |
| 348 | + } | |
| 349 | + | |
| 350 | + private async Task<Dictionary<string, TypeScopeData>> BuildTypeConfiguredScopeMapAsync( | |
| 351 | + List<FlLabelTypeDbEntity> entities) | |
| 352 | + { | |
| 353 | + var result = new Dictionary<string, TypeScopeData>(StringComparer.Ordinal); | |
| 354 | + if (entities.Count == 0) | |
| 355 | + { | |
| 356 | + return result; | |
| 357 | + } | |
| 358 | + | |
| 359 | + foreach (var e in entities.Where(x => | |
| 360 | + !string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))) | |
| 361 | + { | |
| 362 | + result[e.Id] = new TypeScopeData | |
| 363 | + { | |
| 364 | + Region = AllRegionsDisplay, | |
| 365 | + Location = AllLocationsDisplay, | |
| 366 | + RegionIds = new List<string>(), | |
| 367 | + LocationIds = new List<string>() | |
| 368 | + }; | |
| 369 | + } | |
| 370 | + | |
| 371 | + var specifiedIds = entities | |
| 372 | + .Where(x => string.Equals(x.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) | |
| 373 | + .Select(x => x.Id) | |
| 374 | + .ToList(); | |
| 375 | + if (specifiedIds.Count == 0) | |
| 376 | + { | |
| 377 | + return result; | |
| 378 | + } | |
| 379 | + | |
| 380 | + var links = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeLocationDbEntity>() | |
| 381 | + .Where(x => specifiedIds.Contains(x.LabelTypeId)) | |
| 382 | + .ToListAsync(); | |
| 383 | + | |
| 384 | + var locIdSet = links | |
| 385 | + .Select(x => x.LocationId) | |
| 386 | + .Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 387 | + .Select(x => x.Trim()) | |
| 388 | + .Distinct(StringComparer.Ordinal) | |
| 389 | + .ToList(); | |
| 390 | + | |
| 391 | + var locById = new Dictionary<string, LocationAggregateRoot>(StringComparer.Ordinal); | |
| 392 | + if (locIdSet.Count > 0) | |
| 393 | + { | |
| 394 | + var guidList = locIdSet.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); | |
| 395 | + if (guidList.Count > 0) | |
| 396 | + { | |
| 397 | + var locs = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>() | |
| 398 | + .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) | |
| 399 | + .ToListAsync(); | |
| 400 | + foreach (var loc in locs) | |
| 401 | + { | |
| 402 | + locById[loc.Id.ToString()] = loc; | |
| 403 | + } | |
| 404 | + } | |
| 405 | + } | |
| 406 | + | |
| 407 | + foreach (var typeId in specifiedIds) | |
| 408 | + { | |
| 409 | + var typeLinks = links.Where(x => x.LabelTypeId == typeId).ToList(); | |
| 410 | + var locationIds = LocationScopeBindingHelper.NormalizeIds( | |
| 411 | + typeLinks.Select(x => x.LocationId).ToList()); | |
| 412 | + | |
| 413 | + if (typeLinks.Count == 0) | |
| 414 | + { | |
| 415 | + result[typeId] = new TypeScopeData | |
| 416 | + { | |
| 417 | + Region = EmptyDisplay, | |
| 418 | + Location = EmptyDisplay, | |
| 419 | + RegionIds = new List<string>(), | |
| 420 | + LocationIds = new List<string>() | |
| 421 | + }; | |
| 422 | + continue; | |
| 423 | + } | |
| 424 | + | |
| 425 | + var regions = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | |
| 426 | + var locationNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | |
| 427 | + foreach (var lid in locationIds) | |
| 428 | + { | |
| 429 | + if (!locById.TryGetValue(lid, out var loc)) | |
| 430 | + { | |
| 431 | + continue; | |
| 432 | + } | |
| 433 | + | |
| 434 | + var groupName = loc.GroupName?.Trim(); | |
| 435 | + if (!string.IsNullOrEmpty(groupName)) | |
| 436 | + { | |
| 437 | + regions.Add(groupName); | |
| 438 | + } | |
| 439 | + | |
| 440 | + var locName = loc.LocationName?.Trim(); | |
| 441 | + if (string.IsNullOrEmpty(locName)) | |
| 442 | + { | |
| 443 | + locName = loc.LocationCode?.Trim(); | |
| 444 | + } | |
| 445 | + | |
| 446 | + if (!string.IsNullOrEmpty(locName)) | |
| 447 | + { | |
| 448 | + locationNames.Add(locName); | |
| 449 | + } | |
| 450 | + } | |
| 451 | + | |
| 452 | + var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( | |
| 453 | + _dbContext.SqlSugarClient, locationIds); | |
| 454 | + | |
| 455 | + result[typeId] = new TypeScopeData | |
| 456 | + { | |
| 457 | + Region = regions.Count > 0 | |
| 458 | + ? string.Join(", ", regions.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) | |
| 459 | + : EmptyDisplay, | |
| 460 | + Location = locationNames.Count > 0 | |
| 461 | + ? string.Join(", ", locationNames.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) | |
| 462 | + : EmptyDisplay, | |
| 463 | + RegionIds = regionIds, | |
| 464 | + LocationIds = locationIds | |
| 465 | + }; | |
| 466 | + } | |
| 467 | + | |
| 468 | + return result; | |
| 469 | + } | |
| 470 | + | |
| 471 | + private static ISugarQueryable<FlLabelDbEntity> ApplyLabelScopeOnLabels( | |
| 472 | + ISugarQueryable<FlLabelDbEntity> labelQuery, | |
| 473 | + List<string>? scopedLocationIds) | |
| 474 | + { | |
| 475 | + if (scopedLocationIds is null) | |
| 476 | + { | |
| 477 | + return labelQuery; | |
| 478 | + } | |
| 479 | + | |
| 480 | + return scopedLocationIds.Count == 0 | |
| 481 | + ? labelQuery.Where(_ => false) | |
| 482 | + : labelQuery.Where(l => scopedLocationIds.Contains(l.LocationId)); | |
| 483 | + } | |
| 484 | + | |
| 485 | + private sealed class TypeScopeData | |
| 486 | + { | |
| 487 | + public string Region { get; init; } = string.Empty; | |
| 488 | + public string Location { get; init; } = string.Empty; | |
| 489 | + public List<string> RegionIds { get; init; } = new(); | |
| 490 | + public List<string> LocationIds { get; init; } = new(); | |
| 491 | + } | |
| 492 | + | |
| 493 | + private sealed class TypeLabelStats | |
| 494 | + { | |
| 495 | + public long Count { get; init; } | |
| 496 | + public DateTime MaxEdited { get; init; } | |
| 497 | + } | |
| 498 | + | |
| 166 | 499 | private static LabelTypeGetOutputDto MapToGetOutput(FlLabelTypeDbEntity x) |
| 167 | 500 | { |
| 168 | 501 | return new LabelTypeGetOutputDto |
| ... | ... | @@ -171,7 +504,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 171 | 504 | TypeCode = x.TypeCode, |
| 172 | 505 | TypeName = x.TypeName, |
| 173 | 506 | State = x.State, |
| 174 | - OrderNum = x.OrderNum | |
| 507 | + OrderNum = x.OrderNum, | |
| 508 | + AvailabilityType = x.AvailabilityType | |
| 175 | 509 | }; |
| 176 | 510 | } |
| 177 | 511 | |
| ... | ... | @@ -190,4 +524,3 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 190 | 524 | }; |
| 191 | 525 | } |
| 192 | 526 | } |
| 193 | - | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationAppService.cs
| ... | ... | @@ -22,13 +22,16 @@ namespace FoodLabeling.Application.Services; |
| 22 | 22 | public class LocationAppService : ApplicationService, ILocationAppService |
| 23 | 23 | { |
| 24 | 24 | private readonly ISqlSugarRepository<LocationAggregateRoot, Guid> _locationRepository; |
| 25 | + private readonly ISqlSugarDbContext _dbContext; | |
| 25 | 26 | private readonly IOptionsSnapshot<FoodLabelingBatchImportOptions> _batchImportOptions; |
| 26 | 27 | |
| 27 | 28 | public LocationAppService( |
| 28 | 29 | ISqlSugarRepository<LocationAggregateRoot, Guid> locationRepository, |
| 30 | + ISqlSugarDbContext dbContext, | |
| 29 | 31 | IOptionsSnapshot<FoodLabelingBatchImportOptions> batchImportOptions) |
| 30 | 32 | { |
| 31 | 33 | _locationRepository = locationRepository; |
| 34 | + _dbContext = dbContext; | |
| 32 | 35 | _batchImportOptions = batchImportOptions; |
| 33 | 36 | } |
| 34 | 37 | |
| ... | ... | @@ -37,7 +40,7 @@ public class LocationAppService : ApplicationService, ILocationAppService |
| 37 | 40 | { |
| 38 | 41 | RefAsync<int> total = 0; |
| 39 | 42 | |
| 40 | - var query = BuildFilteredQuery(input); | |
| 43 | + var query = await BuildFilteredQueryAsync(input); | |
| 41 | 44 | if (!string.IsNullOrWhiteSpace(input.Sorting)) |
| 42 | 45 | { |
| 43 | 46 | query = query.OrderBy(input.Sorting); |
| ... | ... | @@ -200,7 +203,7 @@ public class LocationAppService : ApplicationService, ILocationAppService |
| 200 | 203 | State = input.State |
| 201 | 204 | }; |
| 202 | 205 | |
| 203 | - var query = BuildFilteredQuery(exportFilter); | |
| 206 | + var query = await BuildFilteredQueryAsync(exportFilter); | |
| 204 | 207 | if (!string.IsNullOrWhiteSpace(exportFilter.Sorting)) |
| 205 | 208 | { |
| 206 | 209 | query = query.OrderBy(exportFilter.Sorting); |
| ... | ... | @@ -328,14 +331,20 @@ public class LocationAppService : ApplicationService, ILocationAppService |
| 328 | 331 | return result; |
| 329 | 332 | } |
| 330 | 333 | |
| 331 | - private ISugarQueryable<LocationAggregateRoot> BuildFilteredQuery(LocationGetListInputVo input) | |
| 334 | + private async Task<ISugarQueryable<LocationAggregateRoot>> BuildFilteredQueryAsync(LocationGetListInputVo input) | |
| 332 | 335 | { |
| 336 | + var scope = await LocationRegionScopeHelper.ResolveLocationListScopeAsync(CurrentUser, _dbContext); | |
| 337 | + | |
| 333 | 338 | var keyword = input.Keyword?.Trim(); |
| 334 | 339 | var partner = input.Partner?.Trim(); |
| 335 | 340 | var groupName = input.GroupName?.Trim(); |
| 336 | 341 | |
| 337 | - return _locationRepository._DbQueryable | |
| 338 | - .Where(x => x.IsDeleted == false) | |
| 342 | + var query = _locationRepository._DbQueryable | |
| 343 | + .Where(x => x.IsDeleted == false); | |
| 344 | + | |
| 345 | + query = LocationRegionScopeHelper.ApplyLocationListScope(query, scope); | |
| 346 | + | |
| 347 | + return query | |
| 339 | 348 | .WhereIF(!string.IsNullOrEmpty(partner), x => x.Partner == partner) |
| 340 | 349 | .WhereIF(!string.IsNullOrEmpty(groupName), x => x.GroupName == groupName) |
| 341 | 350 | .WhereIF(input.State is not null, x => x.State == input.State) |
| ... | ... | @@ -350,8 +359,7 @@ public class LocationAppService : ApplicationService, ILocationAppService |
| 350 | 359 | (x.ZipCode != null && x.ZipCode.Contains(keyword!)) || |
| 351 | 360 | (x.Phone != null && x.Phone.Contains(keyword!)) || |
| 352 | 361 | (x.Email != null && x.Email.Contains(keyword!)) || |
| 353 | - (x.OperatingHours != null && x.OperatingHours.Contains(keyword!)) | |
| 354 | - ); | |
| 362 | + (x.OperatingHours != null && x.OperatingHours.Contains(keyword!))); | |
| 355 | 363 | } |
| 356 | 364 | |
| 357 | 365 | private static LocationGetListOutputDto ToListDto(LocationAggregateRoot x) => | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs
| 1 | 1 | using System.Text.Json; |
| 2 | 2 | using FoodLabeling.Application.Contracts.Constants; |
| 3 | -using FoodLabeling.Application.Contracts.Dtos.RbacRole; | |
| 4 | 3 | using FoodLabeling.Application.Helpers; |
| 4 | +using FoodLabeling.Application.Contracts.Dtos.RbacRole; | |
| 5 | 5 | using FoodLabeling.Application.Contracts.Dtos.Common; |
| 6 | 6 | using FoodLabeling.Application.Contracts.IServices; |
| 7 | -using FoodLabeling.Application.Services.DbModels; | |
| 8 | 7 | using Microsoft.AspNetCore.Mvc; |
| 9 | 8 | using SqlSugar; |
| 10 | 9 | using Volo.Abp; |
| ... | ... | @@ -103,8 +102,8 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService |
| 103 | 102 | throw new UserFriendlyException("角色不存在"); |
| 104 | 103 | } |
| 105 | 104 | |
| 106 | - var menuIds = await _dbContext.SqlSugarClient.Queryable<RoleMenuDbEntity>() | |
| 107 | - .Where(x => x.RoleId == id.ToString()) | |
| 105 | + var menuIds = await _roleMenuRepository._DbQueryable | |
| 106 | + .Where(x => x.RoleId == id) | |
| 108 | 107 | .Select(x => x.MenuId) |
| 109 | 108 | .ToListAsync(); |
| 110 | 109 | |
| ... | ... | @@ -118,7 +117,7 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService |
| 118 | 117 | State = entity.State, |
| 119 | 118 | OrderNum = entity.OrderNum, |
| 120 | 119 | AccessPermissionCodes = DeserializeAccessPermissionCodes(entity.AccessPermissionCodesJson), |
| 121 | - MenuIds = menuIds | |
| 120 | + MenuIds = menuIds.Select(x => x.ToString()).ToList() | |
| 122 | 121 | }; |
| 123 | 122 | await FillAccessPermissionsAsync(new List<RbacRoleGetListOutputDto> { dto }); |
| 124 | 123 | return dto; |
| ... | ... | @@ -212,20 +211,41 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService |
| 212 | 211 | } |
| 213 | 212 | |
| 214 | 213 | /// <summary> |
| 215 | - /// 新增/编辑时按 menuIds 或 accessPermissions 绑定角色菜单(二者都未传则不改绑定) | |
| 214 | + /// 新增/编辑时按 menuIds 或 accessPermissions 绑定角色菜单(RoleMenu 表)。 | |
| 216 | 215 | /// </summary> |
| 217 | 216 | private async Task ApplyRoleMenuBindingsAsync(Guid roleId, RbacRoleCreateInputVo input) |
| 218 | 217 | { |
| 219 | - if (input.MenuIds is not null) | |
| 218 | + var hasMenuIds = input.MenuIds is not null; | |
| 219 | + var hasAccessPermissions = input.AccessPermissions is not null; | |
| 220 | + | |
| 221 | + if (hasMenuIds && input.MenuIds!.Count > 0) | |
| 220 | 222 | { |
| 221 | 223 | await SetRoleMenusAsync(roleId, input.MenuIds); |
| 222 | 224 | return; |
| 223 | 225 | } |
| 224 | 226 | |
| 225 | - if (input.AccessPermissions is not null) | |
| 227 | + if (hasAccessPermissions) | |
| 226 | 228 | { |
| 229 | + if (string.IsNullOrWhiteSpace(input.AccessPermissions)) | |
| 230 | + { | |
| 231 | + await SetRoleMenusAsync(roleId, new List<Guid>()); | |
| 232 | + return; | |
| 233 | + } | |
| 234 | + | |
| 227 | 235 | var menuIds = await ResolveMenuIdsFromAccessPermissionsAsync(input.AccessPermissions); |
| 236 | + if (menuIds.Count == 0) | |
| 237 | + { | |
| 238 | + throw new UserFriendlyException( | |
| 239 | + "accessPermissions 未匹配到任何菜单,请确认 PermissionCode 与菜单一致,或先执行 menu_backfill_permission_code.sql 回填 Menu.PermissionCode"); | |
| 240 | + } | |
| 241 | + | |
| 228 | 242 | await SetRoleMenusAsync(roleId, menuIds); |
| 243 | + return; | |
| 244 | + } | |
| 245 | + | |
| 246 | + if (hasMenuIds && input.MenuIds!.Count == 0) | |
| 247 | + { | |
| 248 | + await SetRoleMenusAsync(roleId, new List<Guid>()); | |
| 229 | 249 | } |
| 230 | 250 | } |
| 231 | 251 | |
| ... | ... | @@ -250,10 +270,15 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService |
| 250 | 270 | return; |
| 251 | 271 | } |
| 252 | 272 | |
| 253 | - var entities = existMenuIds.Select(menuId => new RoleMenuEntity | |
| 273 | + var entities = existMenuIds.Select(menuId => | |
| 254 | 274 | { |
| 255 | - RoleId = roleId, | |
| 256 | - MenuId = menuId | |
| 275 | + var entity = new RoleMenuEntity | |
| 276 | + { | |
| 277 | + RoleId = roleId, | |
| 278 | + MenuId = menuId | |
| 279 | + }; | |
| 280 | + EntityHelper.TrySetId(entity, () => GuidGenerator.Create()); | |
| 281 | + return entity; | |
| 257 | 282 | }).ToList(); |
| 258 | 283 | |
| 259 | 284 | await _roleMenuRepository.InsertRangeAsync(entities); |
| ... | ... | @@ -261,24 +286,28 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService |
| 261 | 286 | |
| 262 | 287 | private async Task<List<Guid>> ResolveMenuIdsFromAccessPermissionsAsync(string accessPermissions) |
| 263 | 288 | { |
| 264 | - var codes = accessPermissions | |
| 265 | - .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) | |
| 266 | - .Where(c => !string.IsNullOrWhiteSpace(c)) | |
| 267 | - .Distinct(StringComparer.Ordinal) | |
| 268 | - .ToList(); | |
| 269 | - | |
| 289 | + var codes = RbacAccessPermissionHelper.ParseAccessPermissionCodes(accessPermissions); | |
| 270 | 290 | if (codes.Count == 0) |
| 271 | 291 | { |
| 272 | 292 | return new List<Guid>(); |
| 273 | 293 | } |
| 274 | 294 | |
| 295 | + var codeSet = new HashSet<string>(codes, StringComparer.OrdinalIgnoreCase); | |
| 296 | + | |
| 275 | 297 | var menus = await _menuRepository._DbQueryable |
| 276 | - .Where(m => m.IsDeleted == false && m.PermissionCode != null) | |
| 277 | - .Where(m => codes.Contains(m.PermissionCode!)) | |
| 278 | - .Select(m => m.Id) | |
| 298 | + .Where(m => m.IsDeleted == false) | |
| 299 | + .Select(m => new { m.Id, m.PermissionCode, m.Router }) | |
| 279 | 300 | .ToListAsync(); |
| 280 | 301 | |
| 281 | - return menus; | |
| 302 | + return menus | |
| 303 | + .Where(m => | |
| 304 | + { | |
| 305 | + var effective = RbacAccessPermissionHelper.GetEffectivePermissionCode(m.PermissionCode, m.Router); | |
| 306 | + return effective is not null && codeSet.Contains(effective); | |
| 307 | + }) | |
| 308 | + .Select(m => m.Id) | |
| 309 | + .Distinct() | |
| 310 | + .ToList(); | |
| 282 | 311 | } |
| 283 | 312 | |
| 284 | 313 | private async Task FillAccessPermissionsAsync(List<RbacRoleGetListOutputDto> items) |
| ... | ... | @@ -296,7 +325,7 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService |
| 296 | 325 | } |
| 297 | 326 | |
| 298 | 327 | /// <summary> |
| 299 | - /// 按角色汇总已绑定菜单上的 PermissionCode(去重、英文逗号+空格拼接) | |
| 328 | + /// Role → RoleMenu → Menu.PermissionCode(空则按 Router 推导)汇总 accessPermissions。 | |
| 300 | 329 | /// </summary> |
| 301 | 330 | private async Task<Dictionary<Guid, string>> GetAccessPermissionsByRoleIdsAsync(List<Guid> roleIds) |
| 302 | 331 | { |
| ... | ... | @@ -319,11 +348,13 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService |
| 319 | 348 | var menuIds = links.Select(x => x.MenuId).Distinct().ToList(); |
| 320 | 349 | var menus = await _menuRepository._DbQueryable |
| 321 | 350 | .Where(m => menuIds.Contains(m.Id) && m.IsDeleted == false) |
| 322 | - .Select(m => new { m.Id, m.PermissionCode }) | |
| 351 | + .Select(m => new { m.Id, m.PermissionCode, m.Router }) | |
| 323 | 352 | .ToListAsync(); |
| 324 | - var permByMenuId = menus.ToDictionary(x => x.Id, x => x.PermissionCode); | |
| 353 | + var permByMenuId = menus.ToDictionary( | |
| 354 | + x => x.Id, | |
| 355 | + x => RbacAccessPermissionHelper.GetEffectivePermissionCode(x.PermissionCode, x.Router)); | |
| 325 | 356 | |
| 326 | - var byRole = distinctRoleIds.ToDictionary(id => id, _ => new HashSet<string>(StringComparer.Ordinal)); | |
| 357 | + var byRole = distinctRoleIds.ToDictionary(id => id, _ => new HashSet<string>(StringComparer.OrdinalIgnoreCase)); | |
| 327 | 358 | foreach (var link in links) |
| 328 | 359 | { |
| 329 | 360 | if (!permByMenuId.TryGetValue(link.MenuId, out var code) || string.IsNullOrWhiteSpace(code)) |
| ... | ... | @@ -364,7 +395,6 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService |
| 364 | 395 | await _roleDeptRepository.DeleteAsync(x => idList.Contains(x.RoleId)); |
| 365 | 396 | await _userRoleRepository.DeleteAsync(x => idList.Contains(x.RoleId)); |
| 366 | 397 | |
| 367 | - // 角色表为软删(ISoftDelete) | |
| 368 | 398 | await _roleRepository.DeleteAsync(x => idList.Contains(x.Id)); |
| 369 | 399 | } |
| 370 | 400 | |
| ... | ... | @@ -426,4 +456,3 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService |
| 426 | 456 | .ToList(); |
| 427 | 457 | } |
| 428 | 458 | } |
| 429 | - | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleMenuAppService.cs
| ... | ... | @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; |
| 4 | 4 | using SqlSugar; |
| 5 | 5 | using Volo.Abp; |
| 6 | 6 | using Volo.Abp.Application.Services; |
| 7 | +using Volo.Abp.Domain.Entities; | |
| 7 | 8 | using Volo.Abp.Uow; |
| 8 | 9 | using Yi.Framework.Rbac.Domain.Entities; |
| 9 | 10 | using Yi.Framework.SqlSugarCore.Abstractions; |
| ... | ... | @@ -56,10 +57,15 @@ public class RbacRoleMenuAppService : ApplicationService, IRbacRoleMenuAppServic |
| 56 | 57 | |
| 57 | 58 | await _roleMenuRepository.DeleteAsync(x => x.RoleId == input.RoleId); |
| 58 | 59 | |
| 59 | - var entities = existMenuIds.Select(menuId => new RoleMenuEntity | |
| 60 | + var entities = existMenuIds.Select(menuId => | |
| 60 | 61 | { |
| 61 | - RoleId = input.RoleId, | |
| 62 | - MenuId = menuId | |
| 62 | + var entity = new RoleMenuEntity | |
| 63 | + { | |
| 64 | + RoleId = input.RoleId, | |
| 65 | + MenuId = menuId | |
| 66 | + }; | |
| 67 | + EntityHelper.TrySetId(entity, () => GuidGenerator.Create()); | |
| 68 | + return entity; | |
| 63 | 69 | }).ToList(); |
| 64 | 70 | |
| 65 | 71 | if (entities.Count > 0) | ... | ... |
美国版/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 |
| 106 | 106 | var userMap = await LoadUserNameMapAsync(pageRows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) |
| 107 | 107 | .Select(x => x!).Distinct().ToList()); |
| 108 | 108 | |
| 109 | + var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync( | |
| 110 | + _dbContext.SqlSugarClient, | |
| 111 | + pageRows.Select(x => new ReportsPrintLogDailyLabelIdHelper.PrintTaskScopeKey( | |
| 112 | + x.Id, | |
| 113 | + x.LocationId, | |
| 114 | + x.PrintedAt ?? DateTime.MinValue)).ToList()); | |
| 115 | + | |
| 109 | 116 | var items = pageRows.Select(x => |
| 110 | 117 | { |
| 111 | 118 | var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) |
| ... | ... | @@ -114,10 +121,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 114 | 121 | var templateText = FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName); |
| 115 | 122 | var locText = FormatLocationText(x.LocName, x.LocCode); |
| 116 | 123 | var printedAt = x.PrintedAt ?? DateTime.MinValue; |
| 124 | + var labelDisplayId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无"; | |
| 117 | 125 | return new ReportsPrintLogListItemDto |
| 118 | 126 | { |
| 119 | 127 | TaskId = x.Id, |
| 120 | - LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(), | |
| 128 | + LabelCode = labelDisplayId, | |
| 121 | 129 | ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), |
| 122 | 130 | ProductCategoryName = string.IsNullOrWhiteSpace(x.ProductCategoryName) |
| 123 | 131 | ? "无" |
| ... | ... | @@ -131,7 +139,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 131 | 139 | PrintedByName = ResolveUserName(userMap, x.CreatedBy), |
| 132 | 140 | LocationText = locText, |
| 133 | 141 | LocationId = x.LocationId?.Trim(), |
| 134 | - ExpiryDateText = TryExtractExpiryText(x.PrintInputJson) | |
| 142 | + ExpiryDateText = ReportsPrintLogExpiryHelper.ExtractExpiryText(x.PrintInputJson) | |
| 135 | 143 | }; |
| 136 | 144 | }).ToList(); |
| 137 | 145 | |
| ... | ... | @@ -191,6 +199,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 191 | 199 | t.PrintInputJson, |
| 192 | 200 | PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), |
| 193 | 201 | t.CreatedBy, |
| 202 | + t.LocationId, | |
| 194 | 203 | LocName = loc.LocationName, |
| 195 | 204 | LocCode = loc.LocationCode |
| 196 | 205 | }) |
| ... | ... | @@ -199,6 +208,13 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 199 | 208 | var userMap = await LoadUserNameMapAsync(rows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) |
| 200 | 209 | .Select(x => x!).Distinct().ToList()); |
| 201 | 210 | |
| 211 | + var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync( | |
| 212 | + _dbContext.SqlSugarClient, | |
| 213 | + rows.Select(x => new ReportsPrintLogDailyLabelIdHelper.PrintTaskScopeKey( | |
| 214 | + x.Id, | |
| 215 | + x.LocationId, | |
| 216 | + x.PrintedAt ?? DateTime.MinValue)).ToList()); | |
| 217 | + | |
| 202 | 218 | var fileName = $"print-log_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; |
| 203 | 219 | var document = Document.Create(container => |
| 204 | 220 | { |
| ... | ... | @@ -236,8 +252,9 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 236 | 252 | var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) |
| 237 | 253 | ? x.ProductCategoryName!.Trim() |
| 238 | 254 | : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim()); |
| 255 | + var labelDisplayId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无"; | |
| 239 | 256 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) |
| 240 | - .Text(string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim()); | |
| 257 | + .Text(labelDisplayId); | |
| 241 | 258 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) |
| 242 | 259 | .Text(string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim()); |
| 243 | 260 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3).Text(cat); |
| ... | ... | @@ -251,7 +268,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 251 | 268 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) |
| 252 | 269 | .Text(FormatLocationText(x.LocName, x.LocCode)); |
| 253 | 270 | table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) |
| 254 | - .Text(TryExtractExpiryText(x.PrintInputJson)); | |
| 271 | + .Text(ReportsPrintLogExpiryHelper.ExtractExpiryText(x.PrintInputJson)); | |
| 255 | 272 | } |
| 256 | 273 | }); |
| 257 | 274 | }); |
| ... | ... | @@ -338,7 +355,14 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 338 | 355 | var userMap = await LoadUserNameMapAsync(pageRows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) |
| 339 | 356 | .Select(x => x!).Distinct().ToList()); |
| 340 | 357 | |
| 341 | - var items = pageRows.Select(x => MapPrintLogExportRowToListItem(x, userMap)).ToList(); | |
| 358 | + var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync( | |
| 359 | + _dbContext.SqlSugarClient, | |
| 360 | + pageRows.Select(x => new ReportsPrintLogDailyLabelIdHelper.PrintTaskScopeKey( | |
| 361 | + x.Id, | |
| 362 | + x.LocationId, | |
| 363 | + x.PrintedAt ?? DateTime.MinValue)).ToList()); | |
| 364 | + | |
| 365 | + var items = pageRows.Select(x => MapPrintLogExportRowToListItem(x, userMap, dailyLabelIdMap)).ToList(); | |
| 342 | 366 | |
| 343 | 367 | var ms = ReportsPrintLogExcelHelper.BuildWorkbook(items); |
| 344 | 368 | var fileName = $"print-log_{Clock.Now:yyyyMMdd-HHmmss}.xlsx"; |
| ... | ... | @@ -351,6 +375,84 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 351 | 375 | _usAppLabelingAppService.ReprintAsync(input); |
| 352 | 376 | |
| 353 | 377 | /// <inheritdoc /> |
| 378 | + public async Task<PagedResultWithPageDto<ReportsTemplatePrintStatListItemDto>> GetTemplatePrintStatListAsync( | |
| 379 | + ReportsTemplatePrintStatGetListInputVo input) | |
| 380 | + { | |
| 381 | + if (input is null) | |
| 382 | + { | |
| 383 | + throw new UserFriendlyException("入参不能为空"); | |
| 384 | + } | |
| 385 | + | |
| 386 | + if (!CurrentUser.Id.HasValue) | |
| 387 | + { | |
| 388 | + throw new UserFriendlyException("用户未登录"); | |
| 389 | + } | |
| 390 | + | |
| 391 | + var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync( | |
| 392 | + CurrentUser, | |
| 393 | + _dbContext.SqlSugarClient, | |
| 394 | + input.PartnerId, | |
| 395 | + input.GroupId, | |
| 396 | + input.LocationId); | |
| 397 | + if (locationIds is not null && locationIds.Count == 0) | |
| 398 | + { | |
| 399 | + return EmptyTemplatePrintStatPage(input); | |
| 400 | + } | |
| 401 | + | |
| 402 | + var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); | |
| 403 | + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 404 | + var currentUserIdStr = CurrentUser.Id.Value.ToString(); | |
| 405 | + var templateKeyword = input.Keyword?.Trim(); | |
| 406 | + | |
| 407 | + var groupedRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword: null, | |
| 408 | + restrictToCreator: false) | |
| 409 | + .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) | |
| 410 | + .Where((t, l, p, lc, pc, loc, tpl) => | |
| 411 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && | |
| 412 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < rangeEndExcl) | |
| 413 | + .WhereIF(!string.IsNullOrWhiteSpace(templateKeyword), | |
| 414 | + (t, l, p, lc, pc, loc, tpl) => | |
| 415 | + tpl.TemplateName != null && tpl.TemplateName.Contains(templateKeyword!)) | |
| 416 | + .GroupBy((t, l, p, lc, pc, loc, tpl) => new { t.TemplateId, tpl.TemplateName }) | |
| 417 | + .Select((t, l, p, lc, pc, loc, tpl) => new | |
| 418 | + { | |
| 419 | + t.TemplateId, | |
| 420 | + tpl.TemplateName, | |
| 421 | + Cnt = SqlFunc.AggregateCount(t.Id) | |
| 422 | + }) | |
| 423 | + .ToListAsync(); | |
| 424 | + | |
| 425 | + var ordered = groupedRows | |
| 426 | + .Select(x => new ReportsTemplatePrintStatListItemDto | |
| 427 | + { | |
| 428 | + TemplateId = string.IsNullOrWhiteSpace(x.TemplateId) ? null : x.TemplateId.Trim(), | |
| 429 | + TemplateName = string.IsNullOrWhiteSpace(x.TemplateName) ? "无" : x.TemplateName.Trim(), | |
| 430 | + PrintedCount = x.Cnt | |
| 431 | + }) | |
| 432 | + .ToList(); | |
| 433 | + | |
| 434 | + if (!string.IsNullOrWhiteSpace(input.Sorting) && | |
| 435 | + input.Sorting.Trim().Equals("PrintedCount asc", StringComparison.OrdinalIgnoreCase)) | |
| 436 | + { | |
| 437 | + ordered = ordered.OrderBy(x => x.PrintedCount).ThenBy(x => x.TemplateName).ToList(); | |
| 438 | + } | |
| 439 | + else | |
| 440 | + { | |
| 441 | + ordered = ordered.OrderByDescending(x => x.PrintedCount).ThenBy(x => x.TemplateName).ToList(); | |
| 442 | + } | |
| 443 | + | |
| 444 | + var total = ordered.Count; | |
| 445 | + var pageIndex = PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); | |
| 446 | + var pageSize = input.MaxResultCount <= 0 ? total : input.MaxResultCount; | |
| 447 | + var offset = pageSize <= 0 ? 0 : (pageIndex - 1) * pageSize; | |
| 448 | + var pageItems = pageSize <= 0 | |
| 449 | + ? ordered | |
| 450 | + : ordered.Skip(offset).Take(pageSize).ToList(); | |
| 451 | + | |
| 452 | + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, pageItems); | |
| 453 | + } | |
| 454 | + | |
| 455 | + /// <inheritdoc /> | |
| 354 | 456 | public async Task<ReportsLabelReportOutputDto> GetLabelReportAsync(ReportsLabelReportQueryInputVo input) |
| 355 | 457 | { |
| 356 | 458 | if (input is null) |
| ... | ... | @@ -363,7 +465,12 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 363 | 465 | throw new UserFriendlyException("用户未登录"); |
| 364 | 466 | } |
| 365 | 467 | |
| 366 | - var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); | |
| 468 | + var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync( | |
| 469 | + CurrentUser, | |
| 470 | + _dbContext.SqlSugarClient, | |
| 471 | + input.PartnerId, | |
| 472 | + input.GroupId, | |
| 473 | + input.LocationId); | |
| 367 | 474 | if (locationIds is not null && locationIds.Count == 0) |
| 368 | 475 | { |
| 369 | 476 | return new ReportsLabelReportOutputDto(); |
| ... | ... | @@ -382,13 +489,13 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 382 | 489 | var currentUserIdStr = CurrentUser.Id.Value.ToString(); |
| 383 | 490 | var keyword = input.Keyword?.Trim(); |
| 384 | 491 | |
| 385 | - var totalCur = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 492 | + var totalCur = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) | |
| 386 | 493 | .Where((t, l, p, lc, pc, loc) => |
| 387 | 494 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && |
| 388 | 495 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) |
| 389 | 496 | .CountAsync(); |
| 390 | 497 | |
| 391 | - var totalPrev = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 498 | + var totalPrev = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) | |
| 392 | 499 | .Where((t, l, p, lc, pc, loc) => |
| 393 | 500 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart && |
| 394 | 501 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl) |
| ... | ... | @@ -399,7 +506,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 399 | 506 | var avgDaily = Math.Round((decimal)totalCur / dayCount, 2); |
| 400 | 507 | var avgDailyPrev = Math.Round((decimal)totalPrev / prevDayCount, 2); |
| 401 | 508 | |
| 402 | - var categoryRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 509 | + var categoryRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) | |
| 403 | 510 | .Where((t, l, p, lc, pc, loc) => |
| 404 | 511 | l.LabelCategoryId != null && |
| 405 | 512 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && |
| ... | ... | @@ -410,7 +517,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 410 | 517 | |
| 411 | 518 | var topCat = categoryRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); |
| 412 | 519 | |
| 413 | - var productRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 520 | + var productRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) | |
| 414 | 521 | .Where((t, l, p, lc, pc, loc) => |
| 415 | 522 | !string.IsNullOrEmpty(p.Id) && |
| 416 | 523 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && |
| ... | ... | @@ -431,7 +538,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 431 | 538 | |
| 432 | 539 | var trendEndExcl = trendEndDay.AddDays(1); |
| 433 | 540 | |
| 434 | - var trendRaw = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 541 | + var trendRaw = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) | |
| 435 | 542 | .Where((t, l, p, lc, pc, loc) => |
| 436 | 543 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay && |
| 437 | 544 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl) |
| ... | ... | @@ -585,7 +692,8 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 585 | 692 | List<string>? locationIds, |
| 586 | 693 | bool isAdmin, |
| 587 | 694 | string currentUserIdStr, |
| 588 | - string? keyword) | |
| 695 | + string? keyword, | |
| 696 | + bool restrictToCreator = true) | |
| 589 | 697 | { |
| 590 | 698 | return _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>() |
| 591 | 699 | .LeftJoin<FlLabelDbEntity>((t, l) => t.LabelId == l.Id) |
| ... | ... | @@ -595,7 +703,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 595 | 703 | .LeftJoin<LocationAggregateRoot>((t, l, p, lc, pc, loc) => |
| 596 | 704 | t.LocationId != null && SqlFunc.ToString(loc.Id) == t.LocationId) |
| 597 | 705 | .Where((t, l, p, lc, pc, loc) => !loc.IsDeleted) |
| 598 | - .WhereIF(!isAdmin, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr) | |
| 706 | + .WhereIF(restrictToCreator && !isAdmin, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr) | |
| 599 | 707 | .WhereIF(locationIds is not null, (t, l, p, lc, pc, loc) => locationIds!.Contains(t.LocationId!)) |
| 600 | 708 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), |
| 601 | 709 | (t, l, p, lc, pc, loc) => |
| ... | ... | @@ -697,6 +805,21 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 697 | 805 | }; |
| 698 | 806 | } |
| 699 | 807 | |
| 808 | + private static PagedResultWithPageDto<ReportsTemplatePrintStatListItemDto> EmptyTemplatePrintStatPage( | |
| 809 | + ReportsTemplatePrintStatGetListInputVo input) | |
| 810 | + { | |
| 811 | + var pageSize = input.MaxResultCount <= 0 ? 0 : input.MaxResultCount; | |
| 812 | + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount); | |
| 813 | + return new PagedResultWithPageDto<ReportsTemplatePrintStatListItemDto> | |
| 814 | + { | |
| 815 | + PageIndex = pageIndex, | |
| 816 | + PageSize = pageSize, | |
| 817 | + TotalCount = 0, | |
| 818 | + TotalPages = 0, | |
| 819 | + Items = new List<ReportsTemplatePrintStatListItemDto>() | |
| 820 | + }; | |
| 821 | + } | |
| 822 | + | |
| 700 | 823 | private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, |
| 701 | 824 | List<T> items) |
| 702 | 825 | { |
| ... | ... | @@ -799,54 +922,6 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 799 | 922 | return $"{ws}x{hs}{normalizedUnit}"; |
| 800 | 923 | } |
| 801 | 924 | |
| 802 | - private static string TryExtractExpiryText(string? printInputJson) | |
| 803 | - { | |
| 804 | - if (string.IsNullOrWhiteSpace(printInputJson)) | |
| 805 | - { | |
| 806 | - return "无"; | |
| 807 | - } | |
| 808 | - | |
| 809 | - try | |
| 810 | - { | |
| 811 | - using var doc = JsonDocument.Parse(printInputJson); | |
| 812 | - if (doc.RootElement.ValueKind != JsonValueKind.Object) | |
| 813 | - { | |
| 814 | - return "无"; | |
| 815 | - } | |
| 816 | - | |
| 817 | - foreach (var prop in doc.RootElement.EnumerateObject()) | |
| 818 | - { | |
| 819 | - var key = prop.Name.Trim(); | |
| 820 | - if (!key.Equals("expiryDate", StringComparison.OrdinalIgnoreCase) && | |
| 821 | - !key.Equals("expiry", StringComparison.OrdinalIgnoreCase) && | |
| 822 | - !key.Equals("expirationDate", StringComparison.OrdinalIgnoreCase)) | |
| 823 | - { | |
| 824 | - continue; | |
| 825 | - } | |
| 826 | - | |
| 827 | - var v = prop.Value; | |
| 828 | - if (v.ValueKind == JsonValueKind.String) | |
| 829 | - { | |
| 830 | - var s = v.GetString(); | |
| 831 | - return string.IsNullOrWhiteSpace(s) ? "无" : s.Trim(); | |
| 832 | - } | |
| 833 | - | |
| 834 | - if (v.ValueKind == JsonValueKind.Number && v.TryGetInt64(out var n)) | |
| 835 | - { | |
| 836 | - return n.ToString(CultureInfo.InvariantCulture); | |
| 837 | - } | |
| 838 | - | |
| 839 | - return v.ToString(); | |
| 840 | - } | |
| 841 | - } | |
| 842 | - catch | |
| 843 | - { | |
| 844 | - return "无"; | |
| 845 | - } | |
| 846 | - | |
| 847 | - return "无"; | |
| 848 | - } | |
| 849 | - | |
| 850 | 925 | private static IActionResult BuildEmptyPdf(string fileName) |
| 851 | 926 | { |
| 852 | 927 | QuestPDF.Settings.License = LicenseType.Community; |
| ... | ... | @@ -897,8 +972,10 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 897 | 972 | public string? LocCode { get; set; } |
| 898 | 973 | } |
| 899 | 974 | |
| 900 | - private static ReportsPrintLogListItemDto MapPrintLogExportRowToListItem(PrintLogExportRow x, | |
| 901 | - Dictionary<string, string> userMap) | |
| 975 | + private static ReportsPrintLogListItemDto MapPrintLogExportRowToListItem( | |
| 976 | + PrintLogExportRow x, | |
| 977 | + Dictionary<string, string> userMap, | |
| 978 | + IReadOnlyDictionary<string, string> dailyLabelIdMap) | |
| 902 | 979 | { |
| 903 | 980 | var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) |
| 904 | 981 | ? x.ProductCategoryName!.Trim() |
| ... | ... | @@ -906,10 +983,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 906 | 983 | var templateText = FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName); |
| 907 | 984 | var locText = FormatLocationText(x.LocName, x.LocCode); |
| 908 | 985 | var printedAt = x.PrintedAt ?? DateTime.MinValue; |
| 986 | + var labelDisplayId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无"; | |
| 909 | 987 | return new ReportsPrintLogListItemDto |
| 910 | 988 | { |
| 911 | 989 | TaskId = x.Id, |
| 912 | - LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(), | |
| 990 | + LabelCode = labelDisplayId, | |
| 913 | 991 | ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), |
| 914 | 992 | ProductCategoryName = string.IsNullOrWhiteSpace(x.ProductCategoryName) |
| 915 | 993 | ? "无" |
| ... | ... | @@ -923,7 +1001,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 923 | 1001 | PrintedByName = ResolveUserName(userMap, x.CreatedBy), |
| 924 | 1002 | LocationText = locText, |
| 925 | 1003 | LocationId = x.LocationId?.Trim(), |
| 926 | - ExpiryDateText = TryExtractExpiryText(x.PrintInputJson) | |
| 1004 | + ExpiryDateText = ReportsPrintLogExpiryHelper.ExtractExpiryText(x.PrintInputJson) | |
| 927 | 1005 | }; |
| 928 | 1006 | } |
| 929 | 1007 | } | ... | ... |
美国版/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; |
| 17 | 17 | using Volo.Abp.Domain.Entities; |
| 18 | 18 | using Volo.Abp.Guids; |
| 19 | 19 | using Yi.Framework.Rbac.Domain.Entities; |
| 20 | +using Yi.Framework.Rbac.Domain.Entities.ValueObjects; | |
| 21 | +using Yi.Framework.Rbac.Domain.Helpers; | |
| 20 | 22 | using Yi.Framework.Rbac.Domain.Managers; |
| 21 | 23 | using Yi.Framework.SqlSugarCore.Abstractions; |
| 22 | 24 | |
| ... | ... | @@ -54,7 +56,10 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 54 | 56 | var pageSize = input.MaxResultCount; |
| 55 | 57 | RefAsync<int> total = 0; |
| 56 | 58 | |
| 57 | - var query = await BuildFilteredUserQueryAsync(input); | |
| 59 | + var scopeLocationIds = await LocationScopeBindingHelper.ResolveFilteredLocationIdsForListAsync( | |
| 60 | + _dbContext.SqlSugarClient, input.PartnerId, input.GroupId, input.LocationId); | |
| 61 | + | |
| 62 | + var query = await BuildFilteredUserQueryAsync(input, scopeLocationIds); | |
| 58 | 63 | var users = await query |
| 59 | 64 | .OrderByIF(!string.IsNullOrWhiteSpace(input.Sorting), input.Sorting!) |
| 60 | 65 | .OrderByDescending(u => u.CreationTime) |
| ... | ... | @@ -62,8 +67,8 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 62 | 67 | |
| 63 | 68 | var items = await MapUsersToOutputAsync( |
| 64 | 69 | users, |
| 65 | - input.LocationId, | |
| 66 | - restrictAssignedLocationsToFilter: !string.IsNullOrWhiteSpace(input.LocationId)); | |
| 70 | + scopeLocationIds, | |
| 71 | + restrictAssignedLocationsToFilter: scopeLocationIds is not null); | |
| 67 | 72 | |
| 68 | 73 | var totalCount = (long)total; |
| 69 | 74 | return new PagedResultWithPageDto<TeamMemberGetListOutputDto> |
| ... | ... | @@ -133,11 +138,15 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 133 | 138 | { |
| 134 | 139 | var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input); |
| 135 | 140 | |
| 136 | - var user = new UserAggregateRoot(input.UserName.Trim(), input.Password, input.Phone, input.FullName.Trim()) | |
| 141 | + var user = new UserAggregateRoot | |
| 137 | 142 | { |
| 143 | + UserName = input.UserName.Trim(), | |
| 138 | 144 | Name = input.FullName.Trim(), |
| 145 | + Nick = input.FullName.Trim(), | |
| 139 | 146 | Email = input.Email?.Trim(), |
| 140 | - State = input.State | |
| 147 | + Phone = input.Phone, | |
| 148 | + State = input.State, | |
| 149 | + EncryPassword = new EncryPasswordValueObject(input.Password.Trim()) | |
| 141 | 150 | }; |
| 142 | 151 | |
| 143 | 152 | EntityHelper.TrySetId(user, _guidGenerator.Create); |
| ... | ... | @@ -172,13 +181,22 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 172 | 181 | user.Phone = input.Phone; |
| 173 | 182 | user.State = input.State; |
| 174 | 183 | |
| 184 | + var passwordChanged = false; | |
| 175 | 185 | if (!string.IsNullOrWhiteSpace(input.Password)) |
| 176 | 186 | { |
| 177 | - user.EncryPassword.Password = input.Password; | |
| 178 | - user.BuildPassword(); | |
| 187 | + UserPasswordHelper.ApplyPlainPassword(user, input.Password); | |
| 188 | + passwordChanged = true; | |
| 179 | 189 | } |
| 180 | 190 | |
| 181 | 191 | await _userRepository.UpdateAsync(user); |
| 192 | + if (passwordChanged) | |
| 193 | + { | |
| 194 | + await UserPasswordHelper.EnsurePasswordColumnsPersistedAsync( | |
| 195 | + _userRepository, | |
| 196 | + user.Id, | |
| 197 | + user.EncryPassword.Password, | |
| 198 | + user.EncryPassword.Salt); | |
| 199 | + } | |
| 182 | 200 | |
| 183 | 201 | if (input.RoleId != null) |
| 184 | 202 | { |
| ... | ... | @@ -254,13 +272,19 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 254 | 272 | { |
| 255 | 273 | QuestPDF.Settings.License = LicenseType.Community; |
| 256 | 274 | |
| 257 | - var query = await BuildFilteredUserQueryAsync(input); | |
| 275 | + var scopeLocationIds = await LocationScopeBindingHelper.ResolveFilteredLocationIdsForListAsync( | |
| 276 | + _dbContext.SqlSugarClient, input.PartnerId, input.GroupId, input.LocationId); | |
| 277 | + | |
| 278 | + var query = await BuildFilteredUserQueryAsync(input, scopeLocationIds); | |
| 258 | 279 | var users = await query |
| 259 | 280 | .OrderByIF(!string.IsNullOrWhiteSpace(input.Sorting), input.Sorting!) |
| 260 | 281 | .OrderByDescending(u => u.CreationTime) |
| 261 | 282 | .ToListAsync(); |
| 262 | 283 | |
| 263 | - var rows = await MapUsersToOutputAsync(users, locationFilter: null, restrictAssignedLocationsToFilter: false); | |
| 284 | + var rows = await MapUsersToOutputAsync( | |
| 285 | + users, | |
| 286 | + scopeLocationIds, | |
| 287 | + restrictAssignedLocationsToFilter: scopeLocationIds is not null); | |
| 264 | 288 | |
| 265 | 289 | var fileName = $"team-members_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; |
| 266 | 290 | var document = Document.Create(container => |
| ... | ... | @@ -492,7 +516,9 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 492 | 516 | return result.Distinct().ToList(); |
| 493 | 517 | } |
| 494 | 518 | |
| 495 | - private async Task<ISugarQueryable<UserAggregateRoot>> BuildFilteredUserQueryAsync(TeamMemberGetListInputVo input) | |
| 519 | + private async Task<ISugarQueryable<UserAggregateRoot>> BuildFilteredUserQueryAsync( | |
| 520 | + TeamMemberGetListInputVo input, | |
| 521 | + List<string>? scopeLocationIds) | |
| 496 | 522 | { |
| 497 | 523 | var keyword = input.Keyword?.Trim(); |
| 498 | 524 | var query = _userRepository._DbQueryable |
| ... | ... | @@ -513,15 +539,22 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 513 | 539 | query = query.Where(u => userIds.Contains(u.Id)); |
| 514 | 540 | } |
| 515 | 541 | |
| 516 | - if (!string.IsNullOrWhiteSpace(input.LocationId)) | |
| 542 | + if (scopeLocationIds is not null) | |
| 517 | 543 | { |
| 518 | - var locId = input.LocationId.Trim(); | |
| 519 | - var userIdStrs = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>() | |
| 520 | - .Where(x => !x.IsDeleted && x.LocationId == locId) | |
| 521 | - .Select(x => x.UserId) | |
| 522 | - .ToListAsync(); | |
| 523 | - var allowed = new HashSet<string>(userIdStrs); | |
| 524 | - query = query.Where(u => allowed.Contains(u.Id.ToString())); | |
| 544 | + if (scopeLocationIds.Count == 0) | |
| 545 | + { | |
| 546 | + query = query.Where(_ => false); | |
| 547 | + } | |
| 548 | + else | |
| 549 | + { | |
| 550 | + var scopeSet = new HashSet<string>(scopeLocationIds, StringComparer.Ordinal); | |
| 551 | + var userIdStrs = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>() | |
| 552 | + .Where(x => !x.IsDeleted && scopeSet.Contains(x.LocationId)) | |
| 553 | + .Select(x => x.UserId) | |
| 554 | + .ToListAsync(); | |
| 555 | + var allowed = new HashSet<string>(userIdStrs); | |
| 556 | + query = query.Where(u => allowed.Contains(u.Id.ToString())); | |
| 557 | + } | |
| 525 | 558 | } |
| 526 | 559 | |
| 527 | 560 | return query; |
| ... | ... | @@ -529,7 +562,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 529 | 562 | |
| 530 | 563 | private async Task<List<TeamMemberGetListOutputDto>> MapUsersToOutputAsync( |
| 531 | 564 | List<UserAggregateRoot> users, |
| 532 | - string? locationFilter, | |
| 565 | + List<string>? scopeLocationIds, | |
| 533 | 566 | bool restrictAssignedLocationsToFilter) |
| 534 | 567 | { |
| 535 | 568 | if (users.Count == 0) |
| ... | ... | @@ -552,9 +585,10 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService |
| 552 | 585 | var userLocQuery = _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>() |
| 553 | 586 | .Where(x => !x.IsDeleted) |
| 554 | 587 | .Where(x => userIdStrings.Contains(x.UserId)); |
| 555 | - if (restrictAssignedLocationsToFilter && !string.IsNullOrWhiteSpace(locationFilter)) | |
| 588 | + if (restrictAssignedLocationsToFilter && scopeLocationIds is { Count: > 0 }) | |
| 556 | 589 | { |
| 557 | - userLocQuery = userLocQuery.Where(x => x.LocationId == locationFilter.Trim()); | |
| 590 | + var scopeSet = new HashSet<string>(scopeLocationIds, StringComparer.Ordinal); | |
| 591 | + userLocQuery = userLocQuery.Where(x => scopeSet.Contains(x.LocationId)); | |
| 558 | 592 | } |
| 559 | 593 | |
| 560 | 594 | var userLocations = await userLocQuery.ToListAsync(); | ... | ... |
美国版/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; |
| 25 | 25 | using Volo.Abp.Security.Claims; |
| 26 | 26 | using Volo.Abp.Uow; |
| 27 | 27 | using Volo.Abp.Users; |
| 28 | -using Yi.Framework.Core.Helper; | |
| 29 | 28 | using Yi.Framework.Rbac.Application.Contracts.Dtos.Account; |
| 30 | 29 | using Yi.Framework.Rbac.Application.Contracts.IServices; |
| 31 | 30 | using Yi.Framework.Rbac.Domain.Entities; |
| 31 | +using Yi.Framework.Rbac.Domain.Helpers; | |
| 32 | 32 | using Yi.Framework.Rbac.Domain.Managers; |
| 33 | 33 | using Yi.Framework.Rbac.Domain.Shared.Consts; |
| 34 | 34 | using Yi.Framework.Rbac.Domain.Shared.Dtos; |
| ... | ... | @@ -102,7 +102,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService |
| 102 | 102 | throw new UserFriendlyException("登录失败!邮箱不存在!"); |
| 103 | 103 | } |
| 104 | 104 | |
| 105 | - if (user.EncryPassword.Password != MD5Helper.SHA2Encode(input.Password, user.EncryPassword.Salt)) | |
| 105 | + if (!UserPasswordHelper.VerifyPlainPassword(user, input.Password)) | |
| 106 | 106 | { |
| 107 | 107 | throw new UserFriendlyException(UserConst.Login_Error); |
| 108 | 108 | } |
| ... | ... | @@ -158,7 +158,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService |
| 158 | 158 | } |
| 159 | 159 | |
| 160 | 160 | /// <summary> |
| 161 | - /// 查询单个门店详情(Location 页):地址、门店电话、营业时间占位、店长(角色含 manager 的绑定用户) | |
| 161 | + /// 查询单个门店详情(Location 页):地址、门店电话、经营时间、店长(角色含 manager 的绑定用户) | |
| 162 | 162 | /// </summary> |
| 163 | 163 | /// <remarks> |
| 164 | 164 | /// 仅当当前登录用户在 <c>userlocation</c> 中绑定该 <c>locationId</c> 时可查;否则返回业务异常。 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs
| ... | ... | @@ -7,6 +7,7 @@ using System.Threading.Tasks; |
| 7 | 7 | using FoodLabeling.Application.Contracts.Dtos.Common; |
| 8 | 8 | using FoodLabeling.Application.Contracts.Dtos.Label; |
| 9 | 9 | using FoodLabeling.Application.Contracts.Dtos.LabelTemplate; |
| 10 | +using FoodLabeling.Application.Contracts.Dtos.Reports; | |
| 10 | 11 | using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; |
| 11 | 12 | using FoodLabeling.Application.Contracts.IServices; |
| 12 | 13 | using FoodLabeling.Application.Helpers; |
| ... | ... | @@ -53,7 +54,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 53 | 54 | /// L1 标签分类 fl_label_category(含 buttonAppearance;COLOR/IMAGE 展示值在 categoryPhotoUrl);仅对当前门店可用:ALL 或 SPECIFIED 且在 fl_label_category_location; |
| 54 | 55 | /// L2 产品分类 fl_product.CategoryId join fl_product_category(同上,展示值在 categoryPhotoUrl); |
| 55 | 56 | /// L3 产品卡片:按「产品 + 标签模板」拆分(同一 productId、不同 fl_label.TemplateId 为多张卡);L4 为该卡下与门店、标签分类、该产品、该模板关联的标签实例(fl_label + fl_label_type)。 |
| 56 | - /// L2 仅包含对当前门店可用的类别:AvailabilityType=ALL,或 SPECIFIED 且在 fl_product_category_location 存在该门店记录; | |
| 57 | + /// L2 产品分类展示名来自 fl_product_category;产品范围已由 fl_location_product 限定当前门店, | |
| 58 | + /// 不再因产品分类 SPECIFIED 未配 fl_product_category_location 而整行过滤(避免 App 全店空数据)。 | |
| 57 | 59 | /// 未归类或分类行未关联到 fl_product_category 时仍归入「无」节点。 |
| 58 | 60 | /// </remarks> |
| 59 | 61 | [Authorize] |
| ... | ... | @@ -68,6 +70,9 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 68 | 70 | var keyword = input.Keyword?.Trim(); |
| 69 | 71 | var filterCategoryId = input.LabelCategoryId?.Trim(); |
| 70 | 72 | |
| 73 | + await UsAppPrintLogScopeHelper.EnsureUserBoundToLocationAsync( | |
| 74 | + CurrentUser, _dbContext.SqlSugarClient, locationId); | |
| 75 | + | |
| 71 | 76 | var productIds = await _dbContext.SqlSugarClient.Queryable<FlLocationProductDbEntity>() |
| 72 | 77 | .Where(x => x.LocationId == locationId) |
| 73 | 78 | .Select(x => x.ProductId) |
| ... | ... | @@ -624,9 +629,10 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 624 | 629 | throw new UserFriendlyException("打印任务不存在"); |
| 625 | 630 | } |
| 626 | 631 | |
| 627 | - // 非 admin:仅允许重打自己在当前门店的任务;admin 可重打任意用户任务(仍须门店一致) | |
| 628 | - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 629 | - if (!isAdmin && !string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase)) | |
| 632 | + // 管理员 / Partner 角色:可重打当前门店任意用户任务;其它角色仅本人 | |
| 633 | + var canViewAll = await UsAppPrintLogScopeHelper.CanViewAllPrintsAtLocationAsync( | |
| 634 | + CurrentUser, _dbContext.SqlSugarClient); | |
| 635 | + if (!canViewAll && !string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase)) | |
| 630 | 636 | { |
| 631 | 637 | throw new UserFriendlyException("无权限重打该任务"); |
| 632 | 638 | } |
| ... | ... | @@ -726,13 +732,14 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 726 | 732 | } |
| 727 | 733 | |
| 728 | 734 | /// <summary> |
| 729 | - /// App 打印日志:获取当前登录账号在当前门店打印的记录(分页,时间倒序) | |
| 735 | + /// App 打印日志:当前门店打印记录(分页,时间倒序) | |
| 730 | 736 | /// </summary> |
| 731 | 737 | /// <remarks> |
| 732 | - /// 仅返回满足: | |
| 733 | - /// - CreatedBy == CurrentUser.Id | |
| 734 | - /// - LocationId == input.LocationId | |
| 735 | - /// 的打印任务记录(fl_label_print_task)。 | |
| 738 | + /// 数据范围(须已绑定 <c>input.locationId</c>): | |
| 739 | + /// <list type="bullet"> | |
| 740 | + /// <item><b>管理员</b>(<see cref="ReportsRoleHelper.IsAdminRole"/>)或角色码/名含 <c>partner</c>:该门店 <b>全部</b> 打印任务;</item> | |
| 741 | + /// <item>其它角色:仅 <c>CreatedBy == CurrentUser.Id</c>。</item> | |
| 742 | + /// </list> | |
| 736 | 743 | /// |
| 737 | 744 | /// 示例请求: |
| 738 | 745 | /// ```json |
| ... | ... | @@ -774,9 +781,12 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 774 | 781 | } |
| 775 | 782 | |
| 776 | 783 | var currentUserIdStr = CurrentUser.Id.Value.ToString(); |
| 784 | + await UsAppPrintLogScopeHelper.EnsureUserBoundToLocationAsync( | |
| 785 | + CurrentUser, _dbContext.SqlSugarClient, locationId); | |
| 777 | 786 | |
| 778 | - var currentUser = await _userRepository.GetByIdAsync(CurrentUser.Id.Value); | |
| 779 | - var operatorName = currentUser?.Name?.Trim() ?? string.Empty; | |
| 787 | + var canViewAll = await UsAppPrintLogScopeHelper.CanViewAllPrintsAtLocationAsync( | |
| 788 | + CurrentUser, _dbContext.SqlSugarClient); | |
| 789 | + var restrictToCreator = !canViewAll; | |
| 780 | 790 | |
| 781 | 791 | var locationName = "无"; |
| 782 | 792 | if (Guid.TryParse(locationId, out var locationGuid)) |
| ... | ... | @@ -797,13 +807,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 797 | 807 | |
| 798 | 808 | RefAsync<int> total = 0; |
| 799 | 809 | |
| 800 | - var query = _dbContext.SqlSugarClient | |
| 801 | - .Queryable<FlLabelPrintTaskDbEntity>() | |
| 802 | - .LeftJoin<FlLabelDbEntity>((t, l) => t.LabelId == l.Id) | |
| 803 | - .LeftJoin<FlProductDbEntity>((t, l, p) => t.ProductId == p.Id) | |
| 804 | - .LeftJoin<FlLabelTypeDbEntity>((t, l, p, lt) => t.LabelTypeId == lt.Id) | |
| 805 | - .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lt, tpl) => t.TemplateId == tpl.Id) | |
| 806 | - .Where((t, l, p, lt, tpl) => t.CreatedBy == currentUserIdStr && t.LocationId == locationId) | |
| 810 | + var query = UsAppPrintLogScopeHelper.BuildLocationPrintTaskQuery( | |
| 811 | + _dbContext.SqlSugarClient, locationId, restrictToCreator, currentUserIdStr) | |
| 807 | 812 | .OrderBy((t, l, p, lt, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc) |
| 808 | 813 | .OrderBy((t, l, p, lt, tpl) => t.CreationTime, OrderByType.Desc) |
| 809 | 814 | .Select((t, l, p, lt, tpl) => new |
| ... | ... | @@ -821,11 +826,16 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 821 | 826 | TemplateUnit = tpl.Unit, |
| 822 | 827 | t.PrintInputJson, |
| 823 | 828 | t.PrintedAt, |
| 824 | - t.CreationTime | |
| 829 | + t.CreationTime, | |
| 830 | + t.CreatedBy | |
| 825 | 831 | }); |
| 826 | 832 | |
| 827 | 833 | var pageRows = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); |
| 828 | 834 | |
| 835 | + var operatorMap = await UsAppPrintLogScopeHelper.LoadOperatorNameMapAsync( | |
| 836 | + _dbContext.SqlSugarClient, | |
| 837 | + pageRows.Select(x => x.CreatedBy)); | |
| 838 | + | |
| 829 | 839 | var items = pageRows.Select(x => new PrintLogItemDto |
| 830 | 840 | { |
| 831 | 841 | TaskId = x.Id, |
| ... | ... | @@ -839,7 +849,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 839 | 849 | LabelSizeText = FormatLabelSizeWithUnit(x.TemplateWidth, x.TemplateHeight, x.TemplateUnit), |
| 840 | 850 | PrintInputJson = x.PrintInputJson, |
| 841 | 851 | PrintedAt = x.PrintedAt ?? x.CreationTime, |
| 842 | - OperatorName = operatorName, | |
| 852 | + OperatorName = UsAppPrintLogScopeHelper.ResolveOperatorName(operatorMap, x.CreatedBy), | |
| 843 | 853 | LocationName = locationName |
| 844 | 854 | }).ToList(); |
| 845 | 855 | |
| ... | ... | @@ -858,6 +868,204 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 858 | 868 | }; |
| 859 | 869 | } |
| 860 | 870 | |
| 871 | + /// <summary> | |
| 872 | + /// App Label Report:当前门店打印统计(权限与 <see cref="GetPrintLogListAsync"/> 一致) | |
| 873 | + /// </summary> | |
| 874 | + /// <remarks> | |
| 875 | + /// 示例:<c>POST /api/app/us-app-labeling/get-label-report</c> | |
| 876 | + /// ```json | |
| 877 | + /// { "locationId": "3a21220f-db37-3e32-7390-d55f64cd62a8", "startDate": "2026-04-07", "endDate": "2026-05-18" } | |
| 878 | + /// ``` | |
| 879 | + /// </remarks> | |
| 880 | + [Authorize] | |
| 881 | + [HttpPost] | |
| 882 | + public virtual async Task<ReportsLabelReportOutputDto> GetLabelReportAsync(UsAppLabelReportQueryInputVo input) | |
| 883 | + { | |
| 884 | + if (input is null) | |
| 885 | + { | |
| 886 | + throw new UserFriendlyException("入参不能为空"); | |
| 887 | + } | |
| 888 | + | |
| 889 | + if (!CurrentUser.Id.HasValue) | |
| 890 | + { | |
| 891 | + throw new UserFriendlyException("用户未登录"); | |
| 892 | + } | |
| 893 | + | |
| 894 | + var locationId = input.LocationId?.Trim(); | |
| 895 | + if (string.IsNullOrWhiteSpace(locationId)) | |
| 896 | + { | |
| 897 | + throw new UserFriendlyException("门店Id不能为空"); | |
| 898 | + } | |
| 899 | + | |
| 900 | + await UsAppPrintLogScopeHelper.EnsureUserBoundToLocationAsync( | |
| 901 | + CurrentUser, _dbContext.SqlSugarClient, locationId); | |
| 902 | + | |
| 903 | + var canViewAll = await UsAppPrintLogScopeHelper.CanViewAllPrintsAtLocationAsync( | |
| 904 | + CurrentUser, _dbContext.SqlSugarClient); | |
| 905 | + var restrictToCreator = !canViewAll; | |
| 906 | + var currentUserIdStr = CurrentUser.Id.Value.ToString(); | |
| 907 | + var keyword = input.Keyword?.Trim(); | |
| 908 | + | |
| 909 | + var (curStart, curEndExcl) = ResolveAppDateRange(input.StartDate, input.EndDate); | |
| 910 | + var span = curEndExcl - curStart; | |
| 911 | + if (span.TotalDays < 1) | |
| 912 | + { | |
| 913 | + span = TimeSpan.FromDays(1); | |
| 914 | + } | |
| 915 | + | |
| 916 | + var prevEndExcl = curStart; | |
| 917 | + var prevStart = curStart - span; | |
| 918 | + var db = _dbContext.SqlSugarClient; | |
| 919 | + | |
| 920 | + ISugarQueryable<FlLabelPrintTaskDbEntity, FlLabelDbEntity, FlProductDbEntity, FlLabelCategoryDbEntity, | |
| 921 | + FlProductCategoryDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity> Core() => | |
| 922 | + UsAppPrintLogScopeHelper.BuildLocationPrintTaskReportQuery( | |
| 923 | + db, locationId, restrictToCreator, currentUserIdStr, keyword); | |
| 924 | + | |
| 925 | + var totalCur = await Core() | |
| 926 | + .Where((t, l, p, lc, pc, lt, tpl) => | |
| 927 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && | |
| 928 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) | |
| 929 | + .CountAsync(); | |
| 930 | + | |
| 931 | + var totalPrev = await Core() | |
| 932 | + .Where((t, l, p, lc, pc, lt, tpl) => | |
| 933 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart && | |
| 934 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl) | |
| 935 | + .CountAsync(); | |
| 936 | + | |
| 937 | + var dayCount = Math.Max(1, (int)Math.Ceiling((curEndExcl - curStart).TotalDays)); | |
| 938 | + var prevDayCount = Math.Max(1, (int)Math.Ceiling((prevEndExcl - prevStart).TotalDays)); | |
| 939 | + var avgDaily = Math.Round((decimal)totalCur / dayCount, 2); | |
| 940 | + var avgDailyPrev = Math.Round((decimal)totalPrev / prevDayCount, 2); | |
| 941 | + | |
| 942 | + var categoryRows = await Core() | |
| 943 | + .Where((t, l, p, lc, pc, lt, tpl) => | |
| 944 | + l.LabelCategoryId != null && | |
| 945 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && | |
| 946 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) | |
| 947 | + .GroupBy((t, l, p, lc, pc, lt, tpl) => new { lc.Id, lc.CategoryName }) | |
| 948 | + .Select((t, l, p, lc, pc, lt, tpl) => new { lc.Id, lc.CategoryName, Cnt = SqlFunc.AggregateCount(t.Id) }) | |
| 949 | + .ToListAsync(); | |
| 950 | + | |
| 951 | + var topCat = categoryRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); | |
| 952 | + | |
| 953 | + var productRows = await Core() | |
| 954 | + .Where((t, l, p, lc, pc, lt, tpl) => | |
| 955 | + !string.IsNullOrEmpty(p.Id) && | |
| 956 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && | |
| 957 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) | |
| 958 | + .GroupBy((t, l, p, lc, pc, lt, tpl) => new { p.Id, p.ProductName, Cat = pc.CategoryName }) | |
| 959 | + .Select((t, l, p, lc, pc, lt, tpl) => new | |
| 960 | + { | |
| 961 | + p.Id, | |
| 962 | + p.ProductName, | |
| 963 | + CategoryName = pc.CategoryName, | |
| 964 | + Cnt = SqlFunc.AggregateCount(t.Id) | |
| 965 | + }) | |
| 966 | + .ToListAsync(); | |
| 967 | + | |
| 968 | + var topProd = productRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); | |
| 969 | + var topList = productRows.OrderByDescending(x => x.Cnt).Take(20).ToList(); | |
| 970 | + | |
| 971 | + var trendEndDay = curEndExcl.Date.AddDays(-1); | |
| 972 | + var trendStartDay = trendEndDay.AddDays(-6); | |
| 973 | + if (trendStartDay < curStart.Date) | |
| 974 | + { | |
| 975 | + trendStartDay = curStart.Date; | |
| 976 | + } | |
| 977 | + | |
| 978 | + var trendEndExcl = trendEndDay.AddDays(1); | |
| 979 | + | |
| 980 | + var trendRaw = await Core() | |
| 981 | + .Where((t, l, p, lc, pc, lt, tpl) => | |
| 982 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay && | |
| 983 | + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl) | |
| 984 | + .Select((t, l, p, lc, pc, lt, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime)) | |
| 985 | + .ToListAsync(); | |
| 986 | + | |
| 987 | + var trendDict = trendRaw | |
| 988 | + .Where(x => x.HasValue) | |
| 989 | + .GroupBy(x => x!.Value.Date) | |
| 990 | + .ToDictionary(g => g.Key, g => g.Count()); | |
| 991 | + | |
| 992 | + var trend = new List<ReportsDailyCountDto>(); | |
| 993 | + for (var d = trendStartDay; d <= trendEndDay; d = d.AddDays(1)) | |
| 994 | + { | |
| 995 | + trend.Add(new ReportsDailyCountDto | |
| 996 | + { | |
| 997 | + Date = d.ToString("yyyy-MM-dd"), | |
| 998 | + Count = trendDict.TryGetValue(d, out var c) ? c : 0 | |
| 999 | + }); | |
| 1000 | + } | |
| 1001 | + | |
| 1002 | + var byCategory = categoryRows | |
| 1003 | + .OrderByDescending(x => x.Cnt) | |
| 1004 | + .Select(x => new ReportsCategoryCountDto | |
| 1005 | + { | |
| 1006 | + CategoryId = string.IsNullOrWhiteSpace(x.Id) ? null : x.Id.Trim(), | |
| 1007 | + CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? null : x.CategoryName.Trim(), | |
| 1008 | + Count = x.Cnt | |
| 1009 | + }) | |
| 1010 | + .ToList(); | |
| 1011 | + | |
| 1012 | + var mostUsed = topList.Select(x => | |
| 1013 | + { | |
| 1014 | + var pct = totalCur <= 0 ? 0m : Math.Round(x.Cnt * 100m / totalCur, 2); | |
| 1015 | + return new ReportsTopProductRowDto | |
| 1016 | + { | |
| 1017 | + ProductId = string.IsNullOrWhiteSpace(x.Id) ? null : x.Id.Trim(), | |
| 1018 | + ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? null : x.ProductName.Trim(), | |
| 1019 | + CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? null : x.CategoryName!.Trim(), | |
| 1020 | + TotalPrinted = x.Cnt, | |
| 1021 | + UsagePercent = pct | |
| 1022 | + }; | |
| 1023 | + }).ToList(); | |
| 1024 | + | |
| 1025 | + return new ReportsLabelReportOutputDto | |
| 1026 | + { | |
| 1027 | + Summary = new ReportsLabelReportSummaryDto | |
| 1028 | + { | |
| 1029 | + TotalLabelsPrinted = totalCur, | |
| 1030 | + TotalLabelsPrintedPrevPeriod = totalPrev, | |
| 1031 | + TotalLabelsPrintedChangeRate = CalcAppChangeRate(totalCur, totalPrev), | |
| 1032 | + MostPrintedCategoryName = string.IsNullOrWhiteSpace(topCat?.CategoryName) ? null : topCat.CategoryName.Trim(), | |
| 1033 | + MostPrintedCategoryCount = topCat?.Cnt ?? 0, | |
| 1034 | + TopProductName = string.IsNullOrWhiteSpace(topProd?.ProductName) ? null : topProd.ProductName.Trim(), | |
| 1035 | + TopProductCount = topProd?.Cnt ?? 0, | |
| 1036 | + AvgDailyPrints = avgDaily, | |
| 1037 | + AvgDailyPrintsPrevPeriod = avgDailyPrev, | |
| 1038 | + AvgDailyPrintsChangeRate = CalcAppChangeRate(avgDaily, avgDailyPrev) | |
| 1039 | + }, | |
| 1040 | + LabelsByCategory = byCategory, | |
| 1041 | + PrintVolumeTrend = trend, | |
| 1042 | + MostUsedProducts = mostUsed | |
| 1043 | + }; | |
| 1044 | + } | |
| 1045 | + | |
| 1046 | + private static (DateTime rangeStart, DateTime rangeEndExcl) ResolveAppDateRange(DateTime? startDate, DateTime? endDate) | |
| 1047 | + { | |
| 1048 | + var endDay = (endDate ?? DateTime.Today).Date; | |
| 1049 | + var endExcl = endDay.AddDays(1); | |
| 1050 | + var start = (startDate ?? endDay.AddDays(-29)).Date; | |
| 1051 | + if (start >= endExcl) | |
| 1052 | + { | |
| 1053 | + start = endExcl.AddDays(-1); | |
| 1054 | + } | |
| 1055 | + | |
| 1056 | + return (start, endExcl); | |
| 1057 | + } | |
| 1058 | + | |
| 1059 | + private static decimal CalcAppChangeRate(decimal current, decimal previous) | |
| 1060 | + { | |
| 1061 | + if (previous == 0) | |
| 1062 | + { | |
| 1063 | + return current > 0 ? 100m : 0m; | |
| 1064 | + } | |
| 1065 | + | |
| 1066 | + return Math.Round((current - previous) * 100m / previous, 2); | |
| 1067 | + } | |
| 1068 | + | |
| 861 | 1069 | private async Task<string?> ResolveTemplateProductDefaultValuesJsonAsync( |
| 862 | 1070 | string templateId, |
| 863 | 1071 | string? productId, |
| ... | ... | @@ -896,23 +1104,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 896 | 1104 | .Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId) |
| 897 | 1105 | .Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State) |
| 898 | 1106 | .Where((lp, l, p, c, t, tpl, pc) => !p.IsDeleted && p.State) |
| 899 | - .Where((lp, l, p, c, t, tpl, pc) => | |
| 900 | - !c.IsDeleted && c.State && | |
| 901 | - (c.AvailabilityType == "ALL" || | |
| 902 | - (c.AvailabilityType == "SPECIFIED" && | |
| 903 | - SqlFunc.Subqueryable<FlLabelCategoryLocationDbEntity>() | |
| 904 | - .Where(loc => loc.CategoryId == c.Id && loc.LocationId == locationId) | |
| 905 | - .Any()))) | |
| 1107 | + .Where((lp, l, p, c, t, tpl, pc) => !c.IsDeleted && c.State) | |
| 906 | 1108 | .Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State) |
| 907 | 1109 | .Where((lp, l, p, c, t, tpl, pc) => !tpl.IsDeleted) |
| 908 | 1110 | .Where((lp, l, p, c, t, tpl, pc) => |
| 909 | - pc.Id == null || | |
| 910 | - (!pc.IsDeleted && pc.State && | |
| 911 | - (pc.AvailabilityType == "ALL" || | |
| 912 | - (pc.AvailabilityType == "SPECIFIED" && | |
| 913 | - SqlFunc.Subqueryable<FlProductCategoryLocationDbEntity>() | |
| 914 | - .Where(loc => loc.CategoryId == pc.Id && loc.LocationId == locationId) | |
| 915 | - .Any())))) | |
| 1111 | + pc.Id == null || (!pc.IsDeleted && pc.State)) | |
| 916 | 1112 | .WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId) |
| 917 | 1113 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) => |
| 918 | 1114 | (l.LabelName != null && l.LabelName.Contains(keyword!)) || | ... | ... |
美国版/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; |
| 9 | 9 | using Yi.Framework.Rbac.Application.Contracts.IServices; |
| 10 | 10 | using Yi.Framework.Rbac.Domain.Authorization; |
| 11 | 11 | using Yi.Framework.Rbac.Domain.Entities; |
| 12 | +using Yi.Framework.Rbac.Domain.Helpers; | |
| 12 | 13 | using Yi.Framework.Rbac.Domain.Entities.ValueObjects; |
| 13 | 14 | using Yi.Framework.Rbac.Domain.Managers; |
| 14 | 15 | using Yi.Framework.Rbac.Domain.Repositories; |
| ... | ... | @@ -155,15 +156,21 @@ namespace Yi.Framework.Rbac.Application.Services.System |
| 155 | 156 | |
| 156 | 157 | var entity = await _repository.GetByIdAsync(id); |
| 157 | 158 | //更新密码,特殊处理 |
| 159 | + var passwordChanged = false; | |
| 158 | 160 | if (!string.IsNullOrWhiteSpace(input.Password)) |
| 159 | 161 | { |
| 160 | - entity.EncryPassword.Password = input.Password; | |
| 161 | - entity.BuildPassword(); | |
| 162 | + UserPasswordHelper.ApplyPlainPassword(entity, input.Password); | |
| 163 | + passwordChanged = true; | |
| 162 | 164 | } |
| 163 | 165 | |
| 164 | 166 | await MapToEntityAsync(input, entity); |
| 165 | 167 | |
| 166 | 168 | var res1 = await _repository.UpdateAsync(entity); |
| 169 | + if (passwordChanged) | |
| 170 | + { | |
| 171 | + await UserPasswordHelper.EnsurePasswordColumnsPersistedAsync( | |
| 172 | + _repository, id, entity.EncryPassword.Password, entity.EncryPassword.Salt); | |
| 173 | + } | |
| 167 | 174 | await _userManager.GiveUserSetRoleAsync(new List<Guid> { id }, input.RoleIds); |
| 168 | 175 | await _userManager.GiveUserSetPostAsync(new List<Guid> { id }, input.PostIds); |
| 169 | 176 | return await MapToGetOutputDtoAsync(entity); | ... | ... |
美国版/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; |
| 4 | 4 | using Yi.Framework.Core.Data; |
| 5 | 5 | using Yi.Framework.Core.Helper; |
| 6 | 6 | using Yi.Framework.Rbac.Domain.Entities.ValueObjects; |
| 7 | +using Yi.Framework.Rbac.Domain.Helpers; | |
| 7 | 8 | using Yi.Framework.Rbac.Domain.Shared.Enums; |
| 8 | 9 | |
| 9 | 10 | namespace Yi.Framework.Rbac.Domain.Entities |
| ... | ... | @@ -197,19 +198,8 @@ namespace Yi.Framework.Rbac.Domain.Entities |
| 197 | 198 | /// </summary> |
| 198 | 199 | /// <param name="password"></param> |
| 199 | 200 | /// <returns></returns> |
| 200 | - public bool JudgePassword(string password) | |
| 201 | - { | |
| 202 | - if (EncryPassword.Salt is null) | |
| 203 | - { | |
| 204 | - throw new ArgumentNullException(EncryPassword.Salt); | |
| 205 | - } | |
| 206 | - var p = MD5Helper.SHA2Encode(password, EncryPassword.Salt); | |
| 207 | - if (EncryPassword.Password == MD5Helper.SHA2Encode(password, EncryPassword.Salt)) | |
| 208 | - { | |
| 209 | - return true; | |
| 210 | - } | |
| 211 | - return false; | |
| 212 | - } | |
| 201 | + public bool JudgePassword(string password) => | |
| 202 | + UserPasswordHelper.VerifyPlainPassword(this, password); | |
| 213 | 203 | } |
| 214 | 204 | |
| 215 | 205 | ... | ... |
美国版/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; |
| 13 | 13 | using Volo.Abp.Security.Claims; |
| 14 | 14 | using Yi.Framework.Core.Helper; |
| 15 | 15 | using Yi.Framework.Rbac.Domain.Entities; |
| 16 | +using Yi.Framework.Rbac.Domain.Helpers; | |
| 16 | 17 | using Yi.Framework.Rbac.Domain.Repositories; |
| 17 | 18 | using Yi.Framework.Rbac.Domain.Shared.Caches; |
| 18 | 19 | using Yi.Framework.Rbac.Domain.Shared.Consts; |
| ... | ... | @@ -244,9 +245,10 @@ namespace Yi.Framework.Rbac.Domain.Managers |
| 244 | 245 | { |
| 245 | 246 | throw new UserFriendlyException("无效更新!原密码错误!"); |
| 246 | 247 | } |
| 247 | - user.EncryPassword.Password = newPassword; | |
| 248 | - user.BuildPassword(); | |
| 248 | + UserPasswordHelper.ApplyPlainPassword(user, newPassword); | |
| 249 | 249 | await _repository.UpdateAsync(user); |
| 250 | + await UserPasswordHelper.EnsurePasswordColumnsPersistedAsync( | |
| 251 | + _repository, userId, user.EncryPassword.Password, user.EncryPassword.Salt); | |
| 250 | 252 | } |
| 251 | 253 | |
| 252 | 254 | /// <summary> |
| ... | ... | @@ -258,9 +260,11 @@ namespace Yi.Framework.Rbac.Domain.Managers |
| 258 | 260 | public async Task<bool> RestPasswordAsync(Guid userId, string password) |
| 259 | 261 | { |
| 260 | 262 | var user = await _repository.GetByIdAsync(userId); |
| 261 | - user.EncryPassword.Password = password; | |
| 262 | - user.BuildPassword(); | |
| 263 | - return await _repository.UpdateAsync(user); | |
| 263 | + UserPasswordHelper.ApplyPlainPassword(user, password); | |
| 264 | + var updated = await _repository.UpdateAsync(user); | |
| 265 | + await UserPasswordHelper.EnsurePasswordColumnsPersistedAsync( | |
| 266 | + _repository, userId, user.EncryPassword.Password, user.EncryPassword.Salt); | |
| 267 | + return updated; | |
| 264 | 268 | } |
| 265 | 269 | |
| 266 | 270 | /// <summary> | ... | ... |