Commit 07d5dea2be04f05deb7e8bca2f1069de034600ab

Authored by 李曜臣
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,6 +10,26 @@ public class LabelMultipleOptionCreateInputVo
10 10
11 public bool State { get; set; } = true; 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 public int OrderNum { get; set; } 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,5 +7,15 @@ public class LabelMultipleOptionGetListInputVo : PagedAndSortedResultRequestDto
7 public string? Keyword { get; set; } 7 public string? Keyword { get; set; }
8 8
9 public bool? State { get; set; } 9 public bool? State { get; set; }
  10 +
  11 + /// <summary>
  12 + /// Region 筛选(<c>fl_group.Id</c>)
  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,8 +12,20 @@ public class LabelMultipleOptionGetListOutputDto
12 12
13 public bool State { get; set; } 13 public bool State { get; set; }
14 14
  15 + public string AvailabilityType { get; set; } = "ALL";
  16 +
15 public int OrderNum { get; set; } 17 public int OrderNum { get; set; }
16 18
17 public DateTime LastEdited { get; set; } 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,5 +13,13 @@ public class LabelMultipleOptionGetOutputDto
13 public bool State { get; set; } 13 public bool State { get; set; }
14 14
15 public int OrderNum { get; set; } 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,6 +8,26 @@ public class LabelTypeCreateInputVo
8 8
9 public bool State { get; set; } = true; 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 public int OrderNum { get; set; } 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,5 +7,15 @@ public class LabelTypeGetListInputVo : PagedAndSortedResultRequestDto
7 public string? Keyword { get; set; } 7 public string? Keyword { get; set; }
8 8
9 public bool? State { get; set; } 9 public bool? State { get; set; }
  10 +
  11 + /// <summary>
  12 + /// Region 筛选(<c>fl_group.Id</c>);仅返回在该 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 +10,25 @@ public class LabelTypeGetListOutputDto
10 10
11 public bool State { get; set; } 11 public bool State { get; set; }
12 12
  13 + public string AvailabilityType { get; set; } = "ALL";
  14 +
13 public int OrderNum { get; set; } 15 public int OrderNum { get; set; }
14 16
  17 + /// <summary>列表列 No. of Labels:该类型下未删除标签数(统计 <c>fl_label</c>,非库表物理列)</summary>
15 public long NoOfLabels { get; set; } 18 public long NoOfLabels { get; set; }
16 19
17 public DateTime LastEdited { get; set; } 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,5 +11,13 @@ public class LabelTypeGetOutputDto
11 public bool State { get; set; } 11 public bool State { get; set; }
12 12
13 public int OrderNum { get; set; } 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,7 +8,9 @@ public class ReportsPrintLogListItemDto
8 /// <summary>打印任务 Id(fl_label_print_task.Id),重打时使用</summary> 8 /// <summary>打印任务 Id(fl_label_print_task.Id),重打时使用</summary>
9 public string TaskId { get; set; } = string.Empty; 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 public string LabelCode { get; set; } = string.Empty; 14 public string LabelCode { get; set; } = string.Empty;
13 15
14 public string ProductName { get; set; } = "无"; 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,7 +18,17 @@ public class TeamMemberGetListInputVo : PagedAndSortedResultRequestDto
18 public Guid? RoleId { get; set; } 18 public Guid? RoleId { get; set; }
19 19
20 /// <summary> 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 /// </summary> 32 /// </summary>
23 public string? LocationId { get; set; } 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,7 +17,7 @@ public class UsAppLocationDetailOutputDto
17 /// <summary>门店电话(来自 location.Phone;空为「无」)</summary> 17 /// <summary>门店电话(来自 location.Phone;空为「无」)</summary>
18 public string StorePhone { get; set; } = string.Empty; 18 public string StorePhone { get; set; } = string.Empty;
19 19
20 - /// <summary>营业时间;当前库无字段,固定返回「无」直至业务落库</summary> 20 + /// <summary>经营时间(<c>location.OperatingHours</c> 自由文本);库空或未维护时为「无」</summary>
21 public string OperatingHours { get; set; } = string.Empty; 21 public string OperatingHours { get; set; } = string.Empty;
22 22
23 /// <summary>店长姓名;优先取绑定本店且角色名/编码含 manager 的用户</summary> 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,7 +38,7 @@ public class PrintLogItemDto
38 /// <summary>打印时间(PrintedAt ?? CreationTime)</summary> 38 /// <summary>打印时间(PrintedAt ?? CreationTime)</summary>
39 public DateTime PrintedAt { get; set; } 39 public DateTime PrintedAt { get; set; }
40 40
41 - /// <summary>操作人姓名(当前登录账号 Name)</summary> 41 + /// <summary>操作人姓名(任务 CreatedBy 对应用户;查看全店日志时为实际打印人)</summary>
42 public string OperatorName { get; set; } = string.Empty; 42 public string OperatorName { get; set; } = string.Empty;
43 43
44 /// <summary>门店名称</summary> 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,7 +12,7 @@ namespace FoodLabeling.Application.Contracts.IServices;
12 public interface ILocationAppService : IApplicationService 12 public interface ILocationAppService : IApplicationService
13 { 13 {
14 /// <summary> 14 /// <summary>
15 - /// 门店分页列表 15 + /// 门店分页列表;管理员返回全部门店,非管理员仅返回其绑定门店所属 Region(Partner + GroupName)下的门店。
16 /// </summary> 16 /// </summary>
17 /// <param name="input">查询条件</param> 17 /// <param name="input">查询条件</param>
18 Task<PagedResultWithPageDto<LocationGetListOutputDto>> GetListAsync(LocationGetListInputVo input); 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,7 +32,8 @@ public interface IReportsAppService : IApplicationService
32 Task<UsAppLabelPrintOutputDto> ReprintPrintLogAsync(UsAppLabelReprintInputVo input); 32 Task<UsAppLabelPrintOutputDto> ReprintPrintLogAsync(UsAppLabelReprintInputVo input);
33 33
34 /// <summary> 34 /// <summary>
35 - /// Label Report 统计(卡片 + 分类柱数据 + 7 日趋势 + Top 产品);<c>admin</c> 统计全部,否则仅当前用户。 35 + /// Label Report 统计(卡片 + 分类柱数据 + 7 日趋势 + Top 产品);
  36 + /// <c>admin</c> 统计全部门店;非管理员仅统计 <c>userlocation</c> 绑定门店内全部打印任务(不按 CreatedBy 过滤)。
36 /// </summary> 37 /// </summary>
37 Task<ReportsLabelReportOutputDto> GetLabelReportAsync(ReportsLabelReportQueryInputVo input); 38 Task<ReportsLabelReportOutputDto> GetLabelReportAsync(ReportsLabelReportQueryInputVo input);
38 39
@@ -40,4 +41,32 @@ public interface IReportsAppService : IApplicationService @@ -40,4 +41,32 @@ public interface IReportsAppService : IApplicationService
40 /// Label Report 导出 PDF 41 /// Label Report 导出 PDF
41 /// </summary> 42 /// </summary>
42 Task<IActionResult> ExportLabelReportPdfAsync(ReportsLabelReportQueryInputVo input); 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&amp;MaxResultCount=20&amp;StartDate=2026-04-07&amp;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,7 +44,7 @@ public interface IUsAppAuthAppService : IApplicationService
44 /// 按门店 Id 查询 Location 详情(须为当前账号 userlocation 绑定门店) 44 /// 按门店 Id 查询 Location 详情(须为当前账号 userlocation 绑定门店)
45 /// </summary> 45 /// </summary>
46 /// <param name="locationId">门店 Guid 字符串</param> 46 /// <param name="locationId">门店 Guid 字符串</param>
47 - /// <returns>店名、地址、电话、营业时间占位、店长信息</returns> 47 + /// <returns>店名、地址、电话、经营时间(operatingHours)、店长信息</returns>
48 /// <response code="200">成功</response> 48 /// <response code="200">成功</response>
49 /// <response code="400">参数非法、未绑定或无权限</response> 49 /// <response code="400">参数非法、未绑定或无权限</response>
50 /// <response code="500">服务器错误</response> 50 /// <response code="500">服务器错误</response>
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppLabelingAppService.cs
1 using FoodLabeling.Application.Contracts.Dtos.Common; 1 using FoodLabeling.Application.Contracts.Dtos.Common;
  2 +using FoodLabeling.Application.Contracts.Dtos.Reports;
2 using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; 3 using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
3 using Volo.Abp.Application.Services; 4 using Volo.Abp.Application.Services;
4 5
@@ -30,7 +31,12 @@ public interface IUsAppLabelingAppService : IApplicationService @@ -30,7 +31,12 @@ public interface IUsAppLabelingAppService : IApplicationService
30 Task<UsAppLabelPrintOutputDto> ReprintAsync(UsAppLabelReprintInputVo input); 31 Task<UsAppLabelPrintOutputDto> ReprintAsync(UsAppLabelReprintInputVo input);
31 32
32 /// <summary> 33 /// <summary>
33 - /// App 打印日志:获取当前登录账号在当前门店打印的记录(分页,时间倒序) 34 + /// App 打印日志:当前门店打印记录(分页)。管理员 / Partner 角色可见门店内全部;其它角色仅本人。
34 /// </summary> 35 /// </summary>
35 Task<PagedResultWithPageDto<PrintLogItemDto>> GetPrintLogListAsync(PrintLogGetListInputVo input); 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,5 +29,10 @@ public class FlLabelMultipleOptionDbEntity
29 public int OrderNum { get; set; } 29 public int OrderNum { get; set; }
30 30
31 public bool State { get; set; } 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,5 +27,10 @@ public class FlLabelTypeDbEntity
27 public int OrderNum { get; set; } 27 public int OrderNum { get; set; }
28 28
29 public bool State { get; set; } 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,6 +3,7 @@ using FoodLabeling.Application.Contracts.Dtos.Common;
3 using FoodLabeling.Application.Contracts.Dtos.LabelMultipleOption; 3 using FoodLabeling.Application.Contracts.Dtos.LabelMultipleOption;
4 using FoodLabeling.Application.Contracts.IServices; 4 using FoodLabeling.Application.Contracts.IServices;
5 using FoodLabeling.Application.Services.DbModels; 5 using FoodLabeling.Application.Services.DbModels;
  6 +using FoodLabeling.Domain.Entities;
6 using SqlSugar; 7 using SqlSugar;
7 using Volo.Abp; 8 using Volo.Abp;
8 using Volo.Abp.Application.Services; 9 using Volo.Abp.Application.Services;
@@ -26,12 +27,16 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -26,12 +27,16 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
26 { 27 {
27 RefAsync<int> total = 0; 28 RefAsync<int> total = 0;
28 var keyword = input.Keyword?.Trim(); 29 var keyword = input.Keyword?.Trim();
  30 + var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync(
  31 + _dbContext.SqlSugarClient, input.GroupId, input.LocationId);
29 32
30 var query = _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionDbEntity>() 33 var query = _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionDbEntity>()
31 .Where(x => !x.IsDeleted) 34 .Where(x => !x.IsDeleted)
32 .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.OptionCode.Contains(keyword!) || x.OptionName.Contains(keyword!)) 35 .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.OptionCode.Contains(keyword!) || x.OptionName.Contains(keyword!))
33 .WhereIF(input.State != null, x => x.State == input.State); 36 .WhereIF(input.State != null, x => x.State == input.State);
34 37
  38 + query = ApplyMultipleOptionScopeFilter(query, scopedLocationIds);
  39 +
35 if (!string.IsNullOrWhiteSpace(input.Sorting)) 40 if (!string.IsNullOrWhiteSpace(input.Sorting))
36 { 41 {
37 query = query.OrderBy(input.Sorting); 42 query = query.OrderBy(input.Sorting);
@@ -42,15 +47,26 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -42,15 +47,26 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
42 } 47 }
43 48
44 var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); 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 }).ToList(); 70 }).ToList();
55 71
56 return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); 72 return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items);
@@ -65,15 +81,21 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -65,15 +81,21 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
65 throw new UserFriendlyException("多选项不存在"); 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 public async Task<LabelMultipleOptionGetOutputDto> CreateAsync(LabelMultipleOptionCreateInputVo input) 101 public async Task<LabelMultipleOptionGetOutputDto> CreateAsync(LabelMultipleOptionCreateInputVo input)
@@ -85,6 +107,8 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -85,6 +107,8 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
85 throw new UserFriendlyException("多选项编码和名称不能为空"); 107 throw new UserFriendlyException("多选项编码和名称不能为空");
86 } 108 }
87 109
  110 + var (availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input);
  111 +
88 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionDbEntity>() 112 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionDbEntity>()
89 .AnyAsync(x => !x.IsDeleted && (x.OptionCode == code || x.OptionName == name)); 113 .AnyAsync(x => !x.IsDeleted && (x.OptionCode == code || x.OptionName == name));
90 if (duplicated) 114 if (duplicated)
@@ -92,17 +116,27 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -92,17 +116,27 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
92 throw new UserFriendlyException("多选项编码或名称已存在"); 116 throw new UserFriendlyException("多选项编码或名称已存在");
93 } 117 }
94 118
  119 + var now = DateTime.Now;
  120 + var currentUserId = CurrentUser?.Id?.ToString();
95 var entity = new FlLabelMultipleOptionDbEntity 121 var entity = new FlLabelMultipleOptionDbEntity
96 { 122 {
97 Id = _guidGenerator.Create().ToString(), 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 OptionCode = code, 130 OptionCode = code,
99 OptionName = name, 131 OptionName = name,
100 OptionValuesJson = input.OptionValuesJson?.Trim(), 132 OptionValuesJson = input.OptionValuesJson?.Trim(),
101 State = input.State, 133 State = input.State,
  134 + AvailabilityType = availabilityType,
102 OrderNum = input.OrderNum 135 OrderNum = input.OrderNum
103 }; 136 };
104 137
105 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); 138 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  139 + await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now);
106 return await GetAsync(entity.Id); 140 return await GetAsync(entity.Id);
107 } 141 }
108 142
@@ -122,6 +156,8 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -122,6 +156,8 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
122 throw new UserFriendlyException("多选项编码和名称不能为空"); 156 throw new UserFriendlyException("多选项编码和名称不能为空");
123 } 157 }
124 158
  159 + var (availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input);
  160 +
125 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionDbEntity>() 161 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionDbEntity>()
126 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.OptionCode == code || x.OptionName == name)); 162 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.OptionCode == code || x.OptionName == name));
127 if (duplicated) 163 if (duplicated)
@@ -133,11 +169,14 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -133,11 +169,14 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
133 entity.OptionName = name; 169 entity.OptionName = name;
134 entity.OptionValuesJson = input.OptionValuesJson?.Trim(); 170 entity.OptionValuesJson = input.OptionValuesJson?.Trim();
135 entity.State = input.State; 171 entity.State = input.State;
  172 + entity.AvailabilityType = availabilityType;
136 entity.OrderNum = input.OrderNum; 173 entity.OrderNum = input.OrderNum;
137 entity.LastModificationTime = DateTime.Now; 174 entity.LastModificationTime = DateTime.Now;
138 entity.LastModifierId = CurrentUser?.Id?.ToString(); 175 entity.LastModifierId = CurrentUser?.Id?.ToString();
139 176
140 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); 177 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  178 + await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId,
  179 + entity.LastModificationTime ?? DateTime.Now);
141 return await GetAsync(id); 180 return await GetAsync(id);
142 } 181 }
143 182
@@ -150,12 +189,266 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -150,12 +189,266 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
150 return; 189 return;
151 } 190 }
152 191
  192 + await _dbContext.SqlSugarClient.Deleteable<FlLabelMultipleOptionLocationDbEntity>()
  193 + .Where(x => x.MultipleOptionId == id)
  194 + .ExecuteCommandAsync();
  195 +
153 entity.IsDeleted = true; 196 entity.IsDeleted = true;
154 entity.LastModificationTime = DateTime.Now; 197 entity.LastModificationTime = DateTime.Now;
155 entity.LastModifierId = CurrentUser?.Id?.ToString(); 198 entity.LastModifierId = CurrentUser?.Id?.ToString();
156 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); 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 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items) 452 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items)
160 { 453 {
161 var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount; 454 var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount;
@@ -171,4 +464,3 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -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,6 +3,7 @@ using FoodLabeling.Application.Contracts.Dtos.Common;
3 using FoodLabeling.Application.Contracts.Dtos.LabelType; 3 using FoodLabeling.Application.Contracts.Dtos.LabelType;
4 using FoodLabeling.Application.Contracts.IServices; 4 using FoodLabeling.Application.Contracts.IServices;
5 using FoodLabeling.Application.Services.DbModels; 5 using FoodLabeling.Application.Services.DbModels;
  6 +using FoodLabeling.Domain.Entities;
6 using SqlSugar; 7 using SqlSugar;
7 using Volo.Abp; 8 using Volo.Abp;
8 using Volo.Abp.Application.Services; 9 using Volo.Abp.Application.Services;
@@ -26,12 +27,16 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -26,12 +27,16 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
26 { 27 {
27 RefAsync<int> total = 0; 28 RefAsync<int> total = 0;
28 var keyword = input.Keyword?.Trim(); 29 var keyword = input.Keyword?.Trim();
  30 + var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync(
  31 + _dbContext.SqlSugarClient, input.GroupId, input.LocationId);
29 32
30 var query = _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>() 33 var query = _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>()
31 .Where(x => !x.IsDeleted) 34 .Where(x => !x.IsDeleted)
32 .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.TypeCode.Contains(keyword!) || x.TypeName.Contains(keyword!)) 35 .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.TypeCode.Contains(keyword!) || x.TypeName.Contains(keyword!))
33 .WhereIF(input.State != null, x => x.State == input.State); 36 .WhereIF(input.State != null, x => x.State == input.State);
34 37
  38 + query = ApplyLabelTypeScopeFilter(query, scopedLocationIds);
  39 +
35 if (!string.IsNullOrWhiteSpace(input.Sorting)) 40 if (!string.IsNullOrWhiteSpace(input.Sorting))
36 { 41 {
37 query = query.OrderBy(input.Sorting); 42 query = query.OrderBy(input.Sorting);
@@ -44,23 +49,28 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -44,23 +49,28 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
44 var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); 49 var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
45 var ids = entities.Select(x => x.Id).ToList(); 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 }).ToList(); 74 }).ToList();
65 75
66 return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items); 76 return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items);
@@ -75,7 +85,21 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -75,7 +85,21 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
75 throw new UserFriendlyException("标签类型不存在"); 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 public async Task<LabelTypeGetOutputDto> CreateAsync(LabelTypeCreateInputVo input) 105 public async Task<LabelTypeGetOutputDto> CreateAsync(LabelTypeCreateInputVo input)
@@ -87,6 +111,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -87,6 +111,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
87 throw new UserFriendlyException("类型编码和名称不能为空"); 111 throw new UserFriendlyException("类型编码和名称不能为空");
88 } 112 }
89 113
  114 + var (availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input);
  115 +
90 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>() 116 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>()
91 .AnyAsync(x => !x.IsDeleted && (x.TypeCode == code || x.TypeName == name)); 117 .AnyAsync(x => !x.IsDeleted && (x.TypeCode == code || x.TypeName == name));
92 if (duplicated) 118 if (duplicated)
@@ -94,16 +120,26 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -94,16 +120,26 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
94 throw new UserFriendlyException("类型编码或名称已存在"); 120 throw new UserFriendlyException("类型编码或名称已存在");
95 } 121 }
96 122
  123 + var now = DateTime.Now;
  124 + var currentUserId = CurrentUser?.Id?.ToString();
97 var entity = new FlLabelTypeDbEntity 125 var entity = new FlLabelTypeDbEntity
98 { 126 {
99 Id = _guidGenerator.Create().ToString(), 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 TypeCode = code, 134 TypeCode = code,
101 TypeName = name, 135 TypeName = name,
102 State = input.State, 136 State = input.State,
  137 + AvailabilityType = availabilityType,
103 OrderNum = input.OrderNum 138 OrderNum = input.OrderNum
104 }; 139 };
105 140
106 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); 141 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  142 + await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now);
107 return await GetAsync(entity.Id); 143 return await GetAsync(entity.Id);
108 } 144 }
109 145
@@ -123,6 +159,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -123,6 +159,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
123 throw new UserFriendlyException("类型编码和名称不能为空"); 159 throw new UserFriendlyException("类型编码和名称不能为空");
124 } 160 }
125 161
  162 + var (availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input);
  163 +
126 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>() 164 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>()
127 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.TypeCode == code || x.TypeName == name)); 165 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.TypeCode == code || x.TypeName == name));
128 if (duplicated) 166 if (duplicated)
@@ -133,11 +171,14 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -133,11 +171,14 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
133 entity.TypeCode = code; 171 entity.TypeCode = code;
134 entity.TypeName = name; 172 entity.TypeName = name;
135 entity.State = input.State; 173 entity.State = input.State;
  174 + entity.AvailabilityType = availabilityType;
136 entity.OrderNum = input.OrderNum; 175 entity.OrderNum = input.OrderNum;
137 entity.LastModificationTime = DateTime.Now; 176 entity.LastModificationTime = DateTime.Now;
138 entity.LastModifierId = CurrentUser?.Id?.ToString(); 177 entity.LastModifierId = CurrentUser?.Id?.ToString();
139 178
140 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); 179 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  180 + await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId,
  181 + entity.LastModificationTime ?? DateTime.Now);
141 return await GetAsync(id); 182 return await GetAsync(id);
142 } 183 }
143 184
@@ -157,12 +198,304 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -157,12 +198,304 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
157 throw new UserFriendlyException("该标签类型已被标签引用,无法删除"); 198 throw new UserFriendlyException("该标签类型已被标签引用,无法删除");
158 } 199 }
159 200
  201 + await _dbContext.SqlSugarClient.Deleteable<FlLabelTypeLocationDbEntity>()
  202 + .Where(x => x.LabelTypeId == id)
  203 + .ExecuteCommandAsync();
  204 +
160 entity.IsDeleted = true; 205 entity.IsDeleted = true;
161 entity.LastModificationTime = DateTime.Now; 206 entity.LastModificationTime = DateTime.Now;
162 entity.LastModifierId = CurrentUser?.Id?.ToString(); 207 entity.LastModifierId = CurrentUser?.Id?.ToString();
163 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); 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 private static LabelTypeGetOutputDto MapToGetOutput(FlLabelTypeDbEntity x) 499 private static LabelTypeGetOutputDto MapToGetOutput(FlLabelTypeDbEntity x)
167 { 500 {
168 return new LabelTypeGetOutputDto 501 return new LabelTypeGetOutputDto
@@ -171,7 +504,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -171,7 +504,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
171 TypeCode = x.TypeCode, 504 TypeCode = x.TypeCode,
172 TypeName = x.TypeName, 505 TypeName = x.TypeName,
173 State = x.State, 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,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,13 +22,16 @@ namespace FoodLabeling.Application.Services;
22 public class LocationAppService : ApplicationService, ILocationAppService 22 public class LocationAppService : ApplicationService, ILocationAppService
23 { 23 {
24 private readonly ISqlSugarRepository<LocationAggregateRoot, Guid> _locationRepository; 24 private readonly ISqlSugarRepository<LocationAggregateRoot, Guid> _locationRepository;
  25 + private readonly ISqlSugarDbContext _dbContext;
25 private readonly IOptionsSnapshot<FoodLabelingBatchImportOptions> _batchImportOptions; 26 private readonly IOptionsSnapshot<FoodLabelingBatchImportOptions> _batchImportOptions;
26 27
27 public LocationAppService( 28 public LocationAppService(
28 ISqlSugarRepository<LocationAggregateRoot, Guid> locationRepository, 29 ISqlSugarRepository<LocationAggregateRoot, Guid> locationRepository,
  30 + ISqlSugarDbContext dbContext,
29 IOptionsSnapshot<FoodLabelingBatchImportOptions> batchImportOptions) 31 IOptionsSnapshot<FoodLabelingBatchImportOptions> batchImportOptions)
30 { 32 {
31 _locationRepository = locationRepository; 33 _locationRepository = locationRepository;
  34 + _dbContext = dbContext;
32 _batchImportOptions = batchImportOptions; 35 _batchImportOptions = batchImportOptions;
33 } 36 }
34 37
@@ -37,7 +40,7 @@ public class LocationAppService : ApplicationService, ILocationAppService @@ -37,7 +40,7 @@ public class LocationAppService : ApplicationService, ILocationAppService
37 { 40 {
38 RefAsync<int> total = 0; 41 RefAsync<int> total = 0;
39 42
40 - var query = BuildFilteredQuery(input); 43 + var query = await BuildFilteredQueryAsync(input);
41 if (!string.IsNullOrWhiteSpace(input.Sorting)) 44 if (!string.IsNullOrWhiteSpace(input.Sorting))
42 { 45 {
43 query = query.OrderBy(input.Sorting); 46 query = query.OrderBy(input.Sorting);
@@ -200,7 +203,7 @@ public class LocationAppService : ApplicationService, ILocationAppService @@ -200,7 +203,7 @@ public class LocationAppService : ApplicationService, ILocationAppService
200 State = input.State 203 State = input.State
201 }; 204 };
202 205
203 - var query = BuildFilteredQuery(exportFilter); 206 + var query = await BuildFilteredQueryAsync(exportFilter);
204 if (!string.IsNullOrWhiteSpace(exportFilter.Sorting)) 207 if (!string.IsNullOrWhiteSpace(exportFilter.Sorting))
205 { 208 {
206 query = query.OrderBy(exportFilter.Sorting); 209 query = query.OrderBy(exportFilter.Sorting);
@@ -328,14 +331,20 @@ public class LocationAppService : ApplicationService, ILocationAppService @@ -328,14 +331,20 @@ public class LocationAppService : ApplicationService, ILocationAppService
328 return result; 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 var keyword = input.Keyword?.Trim(); 338 var keyword = input.Keyword?.Trim();
334 var partner = input.Partner?.Trim(); 339 var partner = input.Partner?.Trim();
335 var groupName = input.GroupName?.Trim(); 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 .WhereIF(!string.IsNullOrEmpty(partner), x => x.Partner == partner) 348 .WhereIF(!string.IsNullOrEmpty(partner), x => x.Partner == partner)
340 .WhereIF(!string.IsNullOrEmpty(groupName), x => x.GroupName == groupName) 349 .WhereIF(!string.IsNullOrEmpty(groupName), x => x.GroupName == groupName)
341 .WhereIF(input.State is not null, x => x.State == input.State) 350 .WhereIF(input.State is not null, x => x.State == input.State)
@@ -350,8 +359,7 @@ public class LocationAppService : ApplicationService, ILocationAppService @@ -350,8 +359,7 @@ public class LocationAppService : ApplicationService, ILocationAppService
350 (x.ZipCode != null && x.ZipCode.Contains(keyword!)) || 359 (x.ZipCode != null && x.ZipCode.Contains(keyword!)) ||
351 (x.Phone != null && x.Phone.Contains(keyword!)) || 360 (x.Phone != null && x.Phone.Contains(keyword!)) ||
352 (x.Email != null && x.Email.Contains(keyword!)) || 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 private static LocationGetListOutputDto ToListDto(LocationAggregateRoot x) => 365 private static LocationGetListOutputDto ToListDto(LocationAggregateRoot x) =>
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacRoleAppService.cs
1 using System.Text.Json; 1 using System.Text.Json;
2 using FoodLabeling.Application.Contracts.Constants; 2 using FoodLabeling.Application.Contracts.Constants;
3 -using FoodLabeling.Application.Contracts.Dtos.RbacRole;  
4 using FoodLabeling.Application.Helpers; 3 using FoodLabeling.Application.Helpers;
  4 +using FoodLabeling.Application.Contracts.Dtos.RbacRole;
5 using FoodLabeling.Application.Contracts.Dtos.Common; 5 using FoodLabeling.Application.Contracts.Dtos.Common;
6 using FoodLabeling.Application.Contracts.IServices; 6 using FoodLabeling.Application.Contracts.IServices;
7 -using FoodLabeling.Application.Services.DbModels;  
8 using Microsoft.AspNetCore.Mvc; 7 using Microsoft.AspNetCore.Mvc;
9 using SqlSugar; 8 using SqlSugar;
10 using Volo.Abp; 9 using Volo.Abp;
@@ -103,8 +102,8 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService @@ -103,8 +102,8 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
103 throw new UserFriendlyException("角色不存在"); 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 .Select(x => x.MenuId) 107 .Select(x => x.MenuId)
109 .ToListAsync(); 108 .ToListAsync();
110 109
@@ -118,7 +117,7 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService @@ -118,7 +117,7 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
118 State = entity.State, 117 State = entity.State,
119 OrderNum = entity.OrderNum, 118 OrderNum = entity.OrderNum,
120 AccessPermissionCodes = DeserializeAccessPermissionCodes(entity.AccessPermissionCodesJson), 119 AccessPermissionCodes = DeserializeAccessPermissionCodes(entity.AccessPermissionCodesJson),
121 - MenuIds = menuIds 120 + MenuIds = menuIds.Select(x => x.ToString()).ToList()
122 }; 121 };
123 await FillAccessPermissionsAsync(new List<RbacRoleGetListOutputDto> { dto }); 122 await FillAccessPermissionsAsync(new List<RbacRoleGetListOutputDto> { dto });
124 return dto; 123 return dto;
@@ -212,20 +211,41 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService @@ -212,20 +211,41 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
212 } 211 }
213 212
214 /// <summary> 213 /// <summary>
215 - /// 新增/编辑时按 menuIds 或 accessPermissions 绑定角色菜单(二者都未传则不改绑定) 214 + /// 新增/编辑时按 menuIds 或 accessPermissions 绑定角色菜单(RoleMenu 表)。
216 /// </summary> 215 /// </summary>
217 private async Task ApplyRoleMenuBindingsAsync(Guid roleId, RbacRoleCreateInputVo input) 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 await SetRoleMenusAsync(roleId, input.MenuIds); 223 await SetRoleMenusAsync(roleId, input.MenuIds);
222 return; 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 var menuIds = await ResolveMenuIdsFromAccessPermissionsAsync(input.AccessPermissions); 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 await SetRoleMenusAsync(roleId, menuIds); 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,10 +270,15 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
250 return; 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 }).ToList(); 282 }).ToList();
258 283
259 await _roleMenuRepository.InsertRangeAsync(entities); 284 await _roleMenuRepository.InsertRangeAsync(entities);
@@ -261,24 +286,28 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService @@ -261,24 +286,28 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
261 286
262 private async Task<List<Guid>> ResolveMenuIdsFromAccessPermissionsAsync(string accessPermissions) 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 if (codes.Count == 0) 290 if (codes.Count == 0)
271 { 291 {
272 return new List<Guid>(); 292 return new List<Guid>();
273 } 293 }
274 294
  295 + var codeSet = new HashSet<string>(codes, StringComparer.OrdinalIgnoreCase);
  296 +
275 var menus = await _menuRepository._DbQueryable 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 .ToListAsync(); 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 private async Task FillAccessPermissionsAsync(List<RbacRoleGetListOutputDto> items) 313 private async Task FillAccessPermissionsAsync(List<RbacRoleGetListOutputDto> items)
@@ -296,7 +325,7 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService @@ -296,7 +325,7 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
296 } 325 }
297 326
298 /// <summary> 327 /// <summary>
299 - /// 按角色汇总已绑定菜单上的 PermissionCode(去重、英文逗号+空格拼接) 328 + /// Role → RoleMenu → Menu.PermissionCode(空则按 Router 推导)汇总 accessPermissions。
300 /// </summary> 329 /// </summary>
301 private async Task<Dictionary<Guid, string>> GetAccessPermissionsByRoleIdsAsync(List<Guid> roleIds) 330 private async Task<Dictionary<Guid, string>> GetAccessPermissionsByRoleIdsAsync(List<Guid> roleIds)
302 { 331 {
@@ -319,11 +348,13 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService @@ -319,11 +348,13 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
319 var menuIds = links.Select(x => x.MenuId).Distinct().ToList(); 348 var menuIds = links.Select(x => x.MenuId).Distinct().ToList();
320 var menus = await _menuRepository._DbQueryable 349 var menus = await _menuRepository._DbQueryable
321 .Where(m => menuIds.Contains(m.Id) && m.IsDeleted == false) 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 .ToListAsync(); 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 foreach (var link in links) 358 foreach (var link in links)
328 { 359 {
329 if (!permByMenuId.TryGetValue(link.MenuId, out var code) || string.IsNullOrWhiteSpace(code)) 360 if (!permByMenuId.TryGetValue(link.MenuId, out var code) || string.IsNullOrWhiteSpace(code))
@@ -364,7 +395,6 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService @@ -364,7 +395,6 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
364 await _roleDeptRepository.DeleteAsync(x => idList.Contains(x.RoleId)); 395 await _roleDeptRepository.DeleteAsync(x => idList.Contains(x.RoleId));
365 await _userRoleRepository.DeleteAsync(x => idList.Contains(x.RoleId)); 396 await _userRoleRepository.DeleteAsync(x => idList.Contains(x.RoleId));
366 397
367 - // 角色表为软删(ISoftDelete)  
368 await _roleRepository.DeleteAsync(x => idList.Contains(x.Id)); 398 await _roleRepository.DeleteAsync(x => idList.Contains(x.Id));
369 } 399 }
370 400
@@ -426,4 +456,3 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService @@ -426,4 +456,3 @@ public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
426 .ToList(); 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,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
4 using SqlSugar; 4 using SqlSugar;
5 using Volo.Abp; 5 using Volo.Abp;
6 using Volo.Abp.Application.Services; 6 using Volo.Abp.Application.Services;
  7 +using Volo.Abp.Domain.Entities;
7 using Volo.Abp.Uow; 8 using Volo.Abp.Uow;
8 using Yi.Framework.Rbac.Domain.Entities; 9 using Yi.Framework.Rbac.Domain.Entities;
9 using Yi.Framework.SqlSugarCore.Abstractions; 10 using Yi.Framework.SqlSugarCore.Abstractions;
@@ -56,10 +57,15 @@ public class RbacRoleMenuAppService : ApplicationService, IRbacRoleMenuAppServic @@ -56,10 +57,15 @@ public class RbacRoleMenuAppService : ApplicationService, IRbacRoleMenuAppServic
56 57
57 await _roleMenuRepository.DeleteAsync(x => x.RoleId == input.RoleId); 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 }).ToList(); 69 }).ToList();
64 70
65 if (entities.Count > 0) 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,6 +106,13 @@ public class ReportsAppService : ApplicationService, IReportsAppService
106 var userMap = await LoadUserNameMapAsync(pageRows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) 106 var userMap = await LoadUserNameMapAsync(pageRows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x))
107 .Select(x => x!).Distinct().ToList()); 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 var items = pageRows.Select(x => 116 var items = pageRows.Select(x =>
110 { 117 {
111 var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) 118 var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName)
@@ -114,10 +121,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -114,10 +121,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService
114 var templateText = FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName); 121 var templateText = FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName);
115 var locText = FormatLocationText(x.LocName, x.LocCode); 122 var locText = FormatLocationText(x.LocName, x.LocCode);
116 var printedAt = x.PrintedAt ?? DateTime.MinValue; 123 var printedAt = x.PrintedAt ?? DateTime.MinValue;
  124 + var labelDisplayId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无";
117 return new ReportsPrintLogListItemDto 125 return new ReportsPrintLogListItemDto
118 { 126 {
119 TaskId = x.Id, 127 TaskId = x.Id,
120 - LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(), 128 + LabelCode = labelDisplayId,
121 ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), 129 ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(),
122 ProductCategoryName = string.IsNullOrWhiteSpace(x.ProductCategoryName) 130 ProductCategoryName = string.IsNullOrWhiteSpace(x.ProductCategoryName)
123 ? "无" 131 ? "无"
@@ -131,7 +139,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -131,7 +139,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
131 PrintedByName = ResolveUserName(userMap, x.CreatedBy), 139 PrintedByName = ResolveUserName(userMap, x.CreatedBy),
132 LocationText = locText, 140 LocationText = locText,
133 LocationId = x.LocationId?.Trim(), 141 LocationId = x.LocationId?.Trim(),
134 - ExpiryDateText = TryExtractExpiryText(x.PrintInputJson) 142 + ExpiryDateText = ReportsPrintLogExpiryHelper.ExtractExpiryText(x.PrintInputJson)
135 }; 143 };
136 }).ToList(); 144 }).ToList();
137 145
@@ -191,6 +199,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -191,6 +199,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
191 t.PrintInputJson, 199 t.PrintInputJson,
192 PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime), 200 PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime),
193 t.CreatedBy, 201 t.CreatedBy,
  202 + t.LocationId,
194 LocName = loc.LocationName, 203 LocName = loc.LocationName,
195 LocCode = loc.LocationCode 204 LocCode = loc.LocationCode
196 }) 205 })
@@ -199,6 +208,13 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -199,6 +208,13 @@ public class ReportsAppService : ApplicationService, IReportsAppService
199 var userMap = await LoadUserNameMapAsync(rows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) 208 var userMap = await LoadUserNameMapAsync(rows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x))
200 .Select(x => x!).Distinct().ToList()); 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 var fileName = $"print-log_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; 218 var fileName = $"print-log_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf";
203 var document = Document.Create(container => 219 var document = Document.Create(container =>
204 { 220 {
@@ -236,8 +252,9 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -236,8 +252,9 @@ public class ReportsAppService : ApplicationService, IReportsAppService
236 var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) 252 var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName)
237 ? x.ProductCategoryName!.Trim() 253 ? x.ProductCategoryName!.Trim()
238 : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim()); 254 : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim());
  255 + var labelDisplayId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无";
239 table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) 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 table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) 258 table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
242 .Text(string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim()); 259 .Text(string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim());
243 table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3).Text(cat); 260 table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3).Text(cat);
@@ -251,7 +268,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -251,7 +268,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
251 table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) 268 table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
252 .Text(FormatLocationText(x.LocName, x.LocCode)); 269 .Text(FormatLocationText(x.LocName, x.LocCode));
253 table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3) 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,7 +355,14 @@ public class ReportsAppService : ApplicationService, IReportsAppService
338 var userMap = await LoadUserNameMapAsync(pageRows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x)) 355 var userMap = await LoadUserNameMapAsync(pageRows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x))
339 .Select(x => x!).Distinct().ToList()); 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 var ms = ReportsPrintLogExcelHelper.BuildWorkbook(items); 367 var ms = ReportsPrintLogExcelHelper.BuildWorkbook(items);
344 var fileName = $"print-log_{Clock.Now:yyyyMMdd-HHmmss}.xlsx"; 368 var fileName = $"print-log_{Clock.Now:yyyyMMdd-HHmmss}.xlsx";
@@ -351,6 +375,84 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -351,6 +375,84 @@ public class ReportsAppService : ApplicationService, IReportsAppService
351 _usAppLabelingAppService.ReprintAsync(input); 375 _usAppLabelingAppService.ReprintAsync(input);
352 376
353 /// <inheritdoc /> 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 public async Task<ReportsLabelReportOutputDto> GetLabelReportAsync(ReportsLabelReportQueryInputVo input) 456 public async Task<ReportsLabelReportOutputDto> GetLabelReportAsync(ReportsLabelReportQueryInputVo input)
355 { 457 {
356 if (input is null) 458 if (input is null)
@@ -363,7 +465,12 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -363,7 +465,12 @@ public class ReportsAppService : ApplicationService, IReportsAppService
363 throw new UserFriendlyException("用户未登录"); 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 if (locationIds is not null && locationIds.Count == 0) 474 if (locationIds is not null && locationIds.Count == 0)
368 { 475 {
369 return new ReportsLabelReportOutputDto(); 476 return new ReportsLabelReportOutputDto();
@@ -382,13 +489,13 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -382,13 +489,13 @@ public class ReportsAppService : ApplicationService, IReportsAppService
382 var currentUserIdStr = CurrentUser.Id.Value.ToString(); 489 var currentUserIdStr = CurrentUser.Id.Value.ToString();
383 var keyword = input.Keyword?.Trim(); 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 .Where((t, l, p, lc, pc, loc) => 493 .Where((t, l, p, lc, pc, loc) =>
387 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && 494 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart &&
388 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) 495 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl)
389 .CountAsync(); 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 .Where((t, l, p, lc, pc, loc) => 499 .Where((t, l, p, lc, pc, loc) =>
393 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart && 500 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart &&
394 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl) 501 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl)
@@ -399,7 +506,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -399,7 +506,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
399 var avgDaily = Math.Round((decimal)totalCur / dayCount, 2); 506 var avgDaily = Math.Round((decimal)totalCur / dayCount, 2);
400 var avgDailyPrev = Math.Round((decimal)totalPrev / prevDayCount, 2); 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 .Where((t, l, p, lc, pc, loc) => 510 .Where((t, l, p, lc, pc, loc) =>
404 l.LabelCategoryId != null && 511 l.LabelCategoryId != null &&
405 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && 512 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart &&
@@ -410,7 +517,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -410,7 +517,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
410 517
411 var topCat = categoryRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); 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 .Where((t, l, p, lc, pc, loc) => 521 .Where((t, l, p, lc, pc, loc) =>
415 !string.IsNullOrEmpty(p.Id) && 522 !string.IsNullOrEmpty(p.Id) &&
416 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && 523 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart &&
@@ -431,7 +538,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -431,7 +538,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
431 538
432 var trendEndExcl = trendEndDay.AddDays(1); 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 .Where((t, l, p, lc, pc, loc) => 542 .Where((t, l, p, lc, pc, loc) =>
436 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay && 543 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay &&
437 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl) 544 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl)
@@ -585,7 +692,8 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -585,7 +692,8 @@ public class ReportsAppService : ApplicationService, IReportsAppService
585 List<string>? locationIds, 692 List<string>? locationIds,
586 bool isAdmin, 693 bool isAdmin,
587 string currentUserIdStr, 694 string currentUserIdStr,
588 - string? keyword) 695 + string? keyword,
  696 + bool restrictToCreator = true)
589 { 697 {
590 return _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>() 698 return _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>()
591 .LeftJoin<FlLabelDbEntity>((t, l) => t.LabelId == l.Id) 699 .LeftJoin<FlLabelDbEntity>((t, l) => t.LabelId == l.Id)
@@ -595,7 +703,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -595,7 +703,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
595 .LeftJoin<LocationAggregateRoot>((t, l, p, lc, pc, loc) => 703 .LeftJoin<LocationAggregateRoot>((t, l, p, lc, pc, loc) =>
596 t.LocationId != null && SqlFunc.ToString(loc.Id) == t.LocationId) 704 t.LocationId != null && SqlFunc.ToString(loc.Id) == t.LocationId)
597 .Where((t, l, p, lc, pc, loc) => !loc.IsDeleted) 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 .WhereIF(locationIds is not null, (t, l, p, lc, pc, loc) => locationIds!.Contains(t.LocationId!)) 707 .WhereIF(locationIds is not null, (t, l, p, lc, pc, loc) => locationIds!.Contains(t.LocationId!))
600 .WhereIF(!string.IsNullOrWhiteSpace(keyword), 708 .WhereIF(!string.IsNullOrWhiteSpace(keyword),
601 (t, l, p, lc, pc, loc) => 709 (t, l, p, lc, pc, loc) =>
@@ -697,6 +805,21 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -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 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, 823 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total,
701 List<T> items) 824 List<T> items)
702 { 825 {
@@ -799,54 +922,6 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -799,54 +922,6 @@ public class ReportsAppService : ApplicationService, IReportsAppService
799 return $"{ws}x{hs}{normalizedUnit}"; 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 private static IActionResult BuildEmptyPdf(string fileName) 925 private static IActionResult BuildEmptyPdf(string fileName)
851 { 926 {
852 QuestPDF.Settings.License = LicenseType.Community; 927 QuestPDF.Settings.License = LicenseType.Community;
@@ -897,8 +972,10 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -897,8 +972,10 @@ public class ReportsAppService : ApplicationService, IReportsAppService
897 public string? LocCode { get; set; } 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 var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName) 980 var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName)
904 ? x.ProductCategoryName!.Trim() 981 ? x.ProductCategoryName!.Trim()
@@ -906,10 +983,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -906,10 +983,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService
906 var templateText = FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName); 983 var templateText = FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName);
907 var locText = FormatLocationText(x.LocName, x.LocCode); 984 var locText = FormatLocationText(x.LocName, x.LocCode);
908 var printedAt = x.PrintedAt ?? DateTime.MinValue; 985 var printedAt = x.PrintedAt ?? DateTime.MinValue;
  986 + var labelDisplayId = dailyLabelIdMap.TryGetValue(x.Id, out var dailyId) ? dailyId : "无";
909 return new ReportsPrintLogListItemDto 987 return new ReportsPrintLogListItemDto
910 { 988 {
911 TaskId = x.Id, 989 TaskId = x.Id,
912 - LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(), 990 + LabelCode = labelDisplayId,
913 ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(), 991 ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(),
914 ProductCategoryName = string.IsNullOrWhiteSpace(x.ProductCategoryName) 992 ProductCategoryName = string.IsNullOrWhiteSpace(x.ProductCategoryName)
915 ? "无" 993 ? "无"
@@ -923,7 +1001,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -923,7 +1001,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
923 PrintedByName = ResolveUserName(userMap, x.CreatedBy), 1001 PrintedByName = ResolveUserName(userMap, x.CreatedBy),
924 LocationText = locText, 1002 LocationText = locText,
925 LocationId = x.LocationId?.Trim(), 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,6 +17,8 @@ using Volo.Abp.Application.Services;
17 using Volo.Abp.Domain.Entities; 17 using Volo.Abp.Domain.Entities;
18 using Volo.Abp.Guids; 18 using Volo.Abp.Guids;
19 using Yi.Framework.Rbac.Domain.Entities; 19 using Yi.Framework.Rbac.Domain.Entities;
  20 +using Yi.Framework.Rbac.Domain.Entities.ValueObjects;
  21 +using Yi.Framework.Rbac.Domain.Helpers;
20 using Yi.Framework.Rbac.Domain.Managers; 22 using Yi.Framework.Rbac.Domain.Managers;
21 using Yi.Framework.SqlSugarCore.Abstractions; 23 using Yi.Framework.SqlSugarCore.Abstractions;
22 24
@@ -54,7 +56,10 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService @@ -54,7 +56,10 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
54 var pageSize = input.MaxResultCount; 56 var pageSize = input.MaxResultCount;
55 RefAsync<int> total = 0; 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 var users = await query 63 var users = await query
59 .OrderByIF(!string.IsNullOrWhiteSpace(input.Sorting), input.Sorting!) 64 .OrderByIF(!string.IsNullOrWhiteSpace(input.Sorting), input.Sorting!)
60 .OrderByDescending(u => u.CreationTime) 65 .OrderByDescending(u => u.CreationTime)
@@ -62,8 +67,8 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService @@ -62,8 +67,8 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
62 67
63 var items = await MapUsersToOutputAsync( 68 var items = await MapUsersToOutputAsync(
64 users, 69 users,
65 - input.LocationId,  
66 - restrictAssignedLocationsToFilter: !string.IsNullOrWhiteSpace(input.LocationId)); 70 + scopeLocationIds,
  71 + restrictAssignedLocationsToFilter: scopeLocationIds is not null);
67 72
68 var totalCount = (long)total; 73 var totalCount = (long)total;
69 return new PagedResultWithPageDto<TeamMemberGetListOutputDto> 74 return new PagedResultWithPageDto<TeamMemberGetListOutputDto>
@@ -133,11 +138,15 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService @@ -133,11 +138,15 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
133 { 138 {
134 var mergedLocationIds = await ResolveTeamMemberLocationIdsForSaveAsync(input); 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 Name = input.FullName.Trim(), 144 Name = input.FullName.Trim(),
  145 + Nick = input.FullName.Trim(),
139 Email = input.Email?.Trim(), 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 EntityHelper.TrySetId(user, _guidGenerator.Create); 152 EntityHelper.TrySetId(user, _guidGenerator.Create);
@@ -172,13 +181,22 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService @@ -172,13 +181,22 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
172 user.Phone = input.Phone; 181 user.Phone = input.Phone;
173 user.State = input.State; 182 user.State = input.State;
174 183
  184 + var passwordChanged = false;
175 if (!string.IsNullOrWhiteSpace(input.Password)) 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 await _userRepository.UpdateAsync(user); 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 if (input.RoleId != null) 201 if (input.RoleId != null)
184 { 202 {
@@ -254,13 +272,19 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService @@ -254,13 +272,19 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
254 { 272 {
255 QuestPDF.Settings.License = LicenseType.Community; 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 var users = await query 279 var users = await query
259 .OrderByIF(!string.IsNullOrWhiteSpace(input.Sorting), input.Sorting!) 280 .OrderByIF(!string.IsNullOrWhiteSpace(input.Sorting), input.Sorting!)
260 .OrderByDescending(u => u.CreationTime) 281 .OrderByDescending(u => u.CreationTime)
261 .ToListAsync(); 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 var fileName = $"team-members_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf"; 289 var fileName = $"team-members_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf";
266 var document = Document.Create(container => 290 var document = Document.Create(container =>
@@ -492,7 +516,9 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService @@ -492,7 +516,9 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
492 return result.Distinct().ToList(); 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 var keyword = input.Keyword?.Trim(); 523 var keyword = input.Keyword?.Trim();
498 var query = _userRepository._DbQueryable 524 var query = _userRepository._DbQueryable
@@ -513,15 +539,22 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService @@ -513,15 +539,22 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
513 query = query.Where(u => userIds.Contains(u.Id)); 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 return query; 560 return query;
@@ -529,7 +562,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService @@ -529,7 +562,7 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
529 562
530 private async Task<List<TeamMemberGetListOutputDto>> MapUsersToOutputAsync( 563 private async Task<List<TeamMemberGetListOutputDto>> MapUsersToOutputAsync(
531 List<UserAggregateRoot> users, 564 List<UserAggregateRoot> users,
532 - string? locationFilter, 565 + List<string>? scopeLocationIds,
533 bool restrictAssignedLocationsToFilter) 566 bool restrictAssignedLocationsToFilter)
534 { 567 {
535 if (users.Count == 0) 568 if (users.Count == 0)
@@ -552,9 +585,10 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService @@ -552,9 +585,10 @@ public class TeamMemberAppService : ApplicationService, ITeamMemberAppService
552 var userLocQuery = _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>() 585 var userLocQuery = _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>()
553 .Where(x => !x.IsDeleted) 586 .Where(x => !x.IsDeleted)
554 .Where(x => userIdStrings.Contains(x.UserId)); 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 var userLocations = await userLocQuery.ToListAsync(); 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,10 +25,10 @@ using Volo.Abp.EventBus.Local;
25 using Volo.Abp.Security.Claims; 25 using Volo.Abp.Security.Claims;
26 using Volo.Abp.Uow; 26 using Volo.Abp.Uow;
27 using Volo.Abp.Users; 27 using Volo.Abp.Users;
28 -using Yi.Framework.Core.Helper;  
29 using Yi.Framework.Rbac.Application.Contracts.Dtos.Account; 28 using Yi.Framework.Rbac.Application.Contracts.Dtos.Account;
30 using Yi.Framework.Rbac.Application.Contracts.IServices; 29 using Yi.Framework.Rbac.Application.Contracts.IServices;
31 using Yi.Framework.Rbac.Domain.Entities; 30 using Yi.Framework.Rbac.Domain.Entities;
  31 +using Yi.Framework.Rbac.Domain.Helpers;
32 using Yi.Framework.Rbac.Domain.Managers; 32 using Yi.Framework.Rbac.Domain.Managers;
33 using Yi.Framework.Rbac.Domain.Shared.Consts; 33 using Yi.Framework.Rbac.Domain.Shared.Consts;
34 using Yi.Framework.Rbac.Domain.Shared.Dtos; 34 using Yi.Framework.Rbac.Domain.Shared.Dtos;
@@ -102,7 +102,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService @@ -102,7 +102,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
102 throw new UserFriendlyException("登录失败!邮箱不存在!"); 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 throw new UserFriendlyException(UserConst.Login_Error); 107 throw new UserFriendlyException(UserConst.Login_Error);
108 } 108 }
@@ -158,7 +158,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService @@ -158,7 +158,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
158 } 158 }
159 159
160 /// <summary> 160 /// <summary>
161 - /// 查询单个门店详情(Location 页):地址、门店电话、营业时间占位、店长(角色含 manager 的绑定用户) 161 + /// 查询单个门店详情(Location 页):地址、门店电话、经营时间、店长(角色含 manager 的绑定用户)
162 /// </summary> 162 /// </summary>
163 /// <remarks> 163 /// <remarks>
164 /// 仅当当前登录用户在 <c>userlocation</c> 中绑定该 <c>locationId</c> 时可查;否则返回业务异常。 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,6 +7,7 @@ using System.Threading.Tasks;
7 using FoodLabeling.Application.Contracts.Dtos.Common; 7 using FoodLabeling.Application.Contracts.Dtos.Common;
8 using FoodLabeling.Application.Contracts.Dtos.Label; 8 using FoodLabeling.Application.Contracts.Dtos.Label;
9 using FoodLabeling.Application.Contracts.Dtos.LabelTemplate; 9 using FoodLabeling.Application.Contracts.Dtos.LabelTemplate;
  10 +using FoodLabeling.Application.Contracts.Dtos.Reports;
10 using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling; 11 using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
11 using FoodLabeling.Application.Contracts.IServices; 12 using FoodLabeling.Application.Contracts.IServices;
12 using FoodLabeling.Application.Helpers; 13 using FoodLabeling.Application.Helpers;
@@ -53,7 +54,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -53,7 +54,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
53 /// L1 标签分类 fl_label_category(含 buttonAppearance;COLOR/IMAGE 展示值在 categoryPhotoUrl);仅对当前门店可用:ALL 或 SPECIFIED 且在 fl_label_category_location; 54 /// L1 标签分类 fl_label_category(含 buttonAppearance;COLOR/IMAGE 展示值在 categoryPhotoUrl);仅对当前门店可用:ALL 或 SPECIFIED 且在 fl_label_category_location;
54 /// L2 产品分类 fl_product.CategoryId join fl_product_category(同上,展示值在 categoryPhotoUrl); 55 /// L2 产品分类 fl_product.CategoryId join fl_product_category(同上,展示值在 categoryPhotoUrl);
55 /// L3 产品卡片:按「产品 + 标签模板」拆分(同一 productId、不同 fl_label.TemplateId 为多张卡);L4 为该卡下与门店、标签分类、该产品、该模板关联的标签实例(fl_label + fl_label_type)。 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 /// 未归类或分类行未关联到 fl_product_category 时仍归入「无」节点。 59 /// 未归类或分类行未关联到 fl_product_category 时仍归入「无」节点。
58 /// </remarks> 60 /// </remarks>
59 [Authorize] 61 [Authorize]
@@ -68,6 +70,9 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -68,6 +70,9 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
68 var keyword = input.Keyword?.Trim(); 70 var keyword = input.Keyword?.Trim();
69 var filterCategoryId = input.LabelCategoryId?.Trim(); 71 var filterCategoryId = input.LabelCategoryId?.Trim();
70 72
  73 + await UsAppPrintLogScopeHelper.EnsureUserBoundToLocationAsync(
  74 + CurrentUser, _dbContext.SqlSugarClient, locationId);
  75 +
71 var productIds = await _dbContext.SqlSugarClient.Queryable<FlLocationProductDbEntity>() 76 var productIds = await _dbContext.SqlSugarClient.Queryable<FlLocationProductDbEntity>()
72 .Where(x => x.LocationId == locationId) 77 .Where(x => x.LocationId == locationId)
73 .Select(x => x.ProductId) 78 .Select(x => x.ProductId)
@@ -624,9 +629,10 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -624,9 +629,10 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
624 throw new UserFriendlyException("打印任务不存在"); 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 throw new UserFriendlyException("无权限重打该任务"); 637 throw new UserFriendlyException("无权限重打该任务");
632 } 638 }
@@ -726,13 +732,14 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -726,13 +732,14 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
726 } 732 }
727 733
728 /// <summary> 734 /// <summary>
729 - /// App 打印日志:获取当前登录账号在当前门店打印的记录(分页,时间倒序) 735 + /// App 打印日志:当前门店打印记录(分页,时间倒序)
730 /// </summary> 736 /// </summary>
731 /// <remarks> 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 /// ```json 745 /// ```json
@@ -774,9 +781,12 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -774,9 +781,12 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
774 } 781 }
775 782
776 var currentUserIdStr = CurrentUser.Id.Value.ToString(); 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 var locationName = "无"; 791 var locationName = "无";
782 if (Guid.TryParse(locationId, out var locationGuid)) 792 if (Guid.TryParse(locationId, out var locationGuid))
@@ -797,13 +807,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -797,13 +807,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
797 807
798 RefAsync<int> total = 0; 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 .OrderBy((t, l, p, lt, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc) 812 .OrderBy((t, l, p, lt, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc)
808 .OrderBy((t, l, p, lt, tpl) => t.CreationTime, OrderByType.Desc) 813 .OrderBy((t, l, p, lt, tpl) => t.CreationTime, OrderByType.Desc)
809 .Select((t, l, p, lt, tpl) => new 814 .Select((t, l, p, lt, tpl) => new
@@ -821,11 +826,16 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -821,11 +826,16 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
821 TemplateUnit = tpl.Unit, 826 TemplateUnit = tpl.Unit,
822 t.PrintInputJson, 827 t.PrintInputJson,
823 t.PrintedAt, 828 t.PrintedAt,
824 - t.CreationTime 829 + t.CreationTime,
  830 + t.CreatedBy
825 }); 831 });
826 832
827 var pageRows = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); 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 var items = pageRows.Select(x => new PrintLogItemDto 839 var items = pageRows.Select(x => new PrintLogItemDto
830 { 840 {
831 TaskId = x.Id, 841 TaskId = x.Id,
@@ -839,7 +849,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -839,7 +849,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
839 LabelSizeText = FormatLabelSizeWithUnit(x.TemplateWidth, x.TemplateHeight, x.TemplateUnit), 849 LabelSizeText = FormatLabelSizeWithUnit(x.TemplateWidth, x.TemplateHeight, x.TemplateUnit),
840 PrintInputJson = x.PrintInputJson, 850 PrintInputJson = x.PrintInputJson,
841 PrintedAt = x.PrintedAt ?? x.CreationTime, 851 PrintedAt = x.PrintedAt ?? x.CreationTime,
842 - OperatorName = operatorName, 852 + OperatorName = UsAppPrintLogScopeHelper.ResolveOperatorName(operatorMap, x.CreatedBy),
843 LocationName = locationName 853 LocationName = locationName
844 }).ToList(); 854 }).ToList();
845 855
@@ -858,6 +868,204 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -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 private async Task<string?> ResolveTemplateProductDefaultValuesJsonAsync( 1069 private async Task<string?> ResolveTemplateProductDefaultValuesJsonAsync(
862 string templateId, 1070 string templateId,
863 string? productId, 1071 string? productId,
@@ -896,23 +1104,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -896,23 +1104,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
896 .Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId) 1104 .Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId)
897 .Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State) 1105 .Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State)
898 .Where((lp, l, p, c, t, tpl, pc) => !p.IsDeleted && p.State) 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 .Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State) 1108 .Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State)
907 .Where((lp, l, p, c, t, tpl, pc) => !tpl.IsDeleted) 1109 .Where((lp, l, p, c, t, tpl, pc) => !tpl.IsDeleted)
908 .Where((lp, l, p, c, t, tpl, pc) => 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 .WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId) 1112 .WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId)
917 .WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) => 1113 .WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) =>
918 (l.LabelName != null && l.LabelName.Contains(keyword!)) || 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,6 +9,7 @@ using Yi.Framework.Rbac.Application.Contracts.Dtos.User;
9 using Yi.Framework.Rbac.Application.Contracts.IServices; 9 using Yi.Framework.Rbac.Application.Contracts.IServices;
10 using Yi.Framework.Rbac.Domain.Authorization; 10 using Yi.Framework.Rbac.Domain.Authorization;
11 using Yi.Framework.Rbac.Domain.Entities; 11 using Yi.Framework.Rbac.Domain.Entities;
  12 +using Yi.Framework.Rbac.Domain.Helpers;
12 using Yi.Framework.Rbac.Domain.Entities.ValueObjects; 13 using Yi.Framework.Rbac.Domain.Entities.ValueObjects;
13 using Yi.Framework.Rbac.Domain.Managers; 14 using Yi.Framework.Rbac.Domain.Managers;
14 using Yi.Framework.Rbac.Domain.Repositories; 15 using Yi.Framework.Rbac.Domain.Repositories;
@@ -155,15 +156,21 @@ namespace Yi.Framework.Rbac.Application.Services.System @@ -155,15 +156,21 @@ namespace Yi.Framework.Rbac.Application.Services.System
155 156
156 var entity = await _repository.GetByIdAsync(id); 157 var entity = await _repository.GetByIdAsync(id);
157 //更新密码,特殊处理 158 //更新密码,特殊处理
  159 + var passwordChanged = false;
158 if (!string.IsNullOrWhiteSpace(input.Password)) 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 await MapToEntityAsync(input, entity); 166 await MapToEntityAsync(input, entity);
165 167
166 var res1 = await _repository.UpdateAsync(entity); 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 await _userManager.GiveUserSetRoleAsync(new List<Guid> { id }, input.RoleIds); 174 await _userManager.GiveUserSetRoleAsync(new List<Guid> { id }, input.RoleIds);
168 await _userManager.GiveUserSetPostAsync(new List<Guid> { id }, input.PostIds); 175 await _userManager.GiveUserSetPostAsync(new List<Guid> { id }, input.PostIds);
169 return await MapToGetOutputDtoAsync(entity); 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,6 +4,7 @@ using Volo.Abp.Domain.Entities;
4 using Yi.Framework.Core.Data; 4 using Yi.Framework.Core.Data;
5 using Yi.Framework.Core.Helper; 5 using Yi.Framework.Core.Helper;
6 using Yi.Framework.Rbac.Domain.Entities.ValueObjects; 6 using Yi.Framework.Rbac.Domain.Entities.ValueObjects;
  7 +using Yi.Framework.Rbac.Domain.Helpers;
7 using Yi.Framework.Rbac.Domain.Shared.Enums; 8 using Yi.Framework.Rbac.Domain.Shared.Enums;
8 9
9 namespace Yi.Framework.Rbac.Domain.Entities 10 namespace Yi.Framework.Rbac.Domain.Entities
@@ -197,19 +198,8 @@ namespace Yi.Framework.Rbac.Domain.Entities @@ -197,19 +198,8 @@ namespace Yi.Framework.Rbac.Domain.Entities
197 /// </summary> 198 /// </summary>
198 /// <param name="password"></param> 199 /// <param name="password"></param>
199 /// <returns></returns> 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,6 +13,7 @@ using Volo.Abp.EventBus.Local;
13 using Volo.Abp.Security.Claims; 13 using Volo.Abp.Security.Claims;
14 using Yi.Framework.Core.Helper; 14 using Yi.Framework.Core.Helper;
15 using Yi.Framework.Rbac.Domain.Entities; 15 using Yi.Framework.Rbac.Domain.Entities;
  16 +using Yi.Framework.Rbac.Domain.Helpers;
16 using Yi.Framework.Rbac.Domain.Repositories; 17 using Yi.Framework.Rbac.Domain.Repositories;
17 using Yi.Framework.Rbac.Domain.Shared.Caches; 18 using Yi.Framework.Rbac.Domain.Shared.Caches;
18 using Yi.Framework.Rbac.Domain.Shared.Consts; 19 using Yi.Framework.Rbac.Domain.Shared.Consts;
@@ -244,9 +245,10 @@ namespace Yi.Framework.Rbac.Domain.Managers @@ -244,9 +245,10 @@ namespace Yi.Framework.Rbac.Domain.Managers
244 { 245 {
245 throw new UserFriendlyException("无效更新!原密码错误!"); 246 throw new UserFriendlyException("无效更新!原密码错误!");
246 } 247 }
247 - user.EncryPassword.Password = newPassword;  
248 - user.BuildPassword(); 248 + UserPasswordHelper.ApplyPlainPassword(user, newPassword);
249 await _repository.UpdateAsync(user); 249 await _repository.UpdateAsync(user);
  250 + await UserPasswordHelper.EnsurePasswordColumnsPersistedAsync(
  251 + _repository, userId, user.EncryPassword.Password, user.EncryPassword.Salt);
250 } 252 }
251 253
252 /// <summary> 254 /// <summary>
@@ -258,9 +260,11 @@ namespace Yi.Framework.Rbac.Domain.Managers @@ -258,9 +260,11 @@ namespace Yi.Framework.Rbac.Domain.Managers
258 public async Task<bool> RestPasswordAsync(Guid userId, string password) 260 public async Task<bool> RestPasswordAsync(Guid userId, string password)
259 { 261 {
260 var user = await _repository.GetByIdAsync(userId); 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 /// <summary> 270 /// <summary>