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