Commit 395c9e97b0d50753a75631c04266690e61f76e40

Authored by 杨鑫
2 parents 699ea6e8 c2e3194d

合并

Showing 97 changed files with 5923 additions and 231 deletions
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserBriefDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.AuthSession;
  2 +
  3 +/// <summary>
  4 +/// 当前登录用户简要信息(不含敏感字段)
  5 +/// </summary>
  6 +public class CurrentUserBriefDto
  7 +{
  8 + public Guid Id { get; set; }
  9 +
  10 + public string UserName { get; set; } = string.Empty;
  11 +
  12 + public string? Nick { get; set; }
  13 +
  14 + public string? Email { get; set; }
  15 +
  16 + public string? Icon { get; set; }
  17 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuNodeDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.AuthSession;
  2 +
  3 +/// <summary>
  4 +/// 当前用户可见菜单树节点(与权限分配一致)
  5 +/// </summary>
  6 +public class CurrentUserMenuNodeDto
  7 +{
  8 + public string Id { get; set; } = string.Empty;
  9 +
  10 + public string ParentId { get; set; } = "0";
  11 +
  12 + public string MenuName { get; set; } = string.Empty;
  13 +
  14 + public string? RouterName { get; set; }
  15 +
  16 + public string? Router { get; set; }
  17 +
  18 + public string? PermissionCode { get; set; }
  19 +
  20 + public int MenuType { get; set; }
  21 +
  22 + public int MenuSource { get; set; }
  23 +
  24 + public int OrderNum { get; set; }
  25 +
  26 + public bool State { get; set; }
  27 +
  28 + public string? MenuIcon { get; set; }
  29 +
  30 + public string? Component { get; set; }
  31 +
  32 + public bool IsLink { get; set; }
  33 +
  34 + public bool IsCache { get; set; }
  35 +
  36 + public bool IsShow { get; set; }
  37 +
  38 + public string? Query { get; set; }
  39 +
  40 + public string? Remark { get; set; }
  41 +
  42 + public List<CurrentUserMenuNodeDto> Children { get; set; } = new();
  43 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/AuthSession/CurrentUserMenuPermissionsOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.AuthSession;
  2 +
  3 +/// <summary>
  4 +/// 当前登录用户的菜单与权限码(用于前端动态路由/按钮权限)
  5 +/// </summary>
  6 +public class CurrentUserMenuPermissionsOutputDto
  7 +{
  8 + public CurrentUserBriefDto User { get; set; } = new();
  9 +
  10 + public List<string> RoleCodes { get; set; } = new();
  11 +
  12 + public List<string> PermissionCodes { get; set; } = new();
  13 +
  14 + public List<CurrentUserMenuNodeDto> Menus { get; set; } = new();
  15 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardCategoryDistributionDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard;
  2 +
  3 +/// <summary>
  4 +/// 分类分布项
  5 +/// </summary>
  6 +public class DashboardCategoryDistributionDto
  7 +{
  8 + /// <summary>分类Id</summary>
  9 + public string CategoryId { get; set; } = string.Empty;
  10 +
  11 + /// <summary>分类名称</summary>
  12 + public string CategoryName { get; set; } = string.Empty;
  13 +
  14 + /// <summary>数量</summary>
  15 + public int Count { get; set; }
  16 +
  17 + /// <summary>占比(百分比)</summary>
  18 + public decimal Ratio { get; set; }
  19 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardDailyTrendPointDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard;
  2 +
  3 +/// <summary>
  4 +/// 日趋势点
  5 +/// </summary>
  6 +public class DashboardDailyTrendPointDto
  7 +{
  8 + /// <summary>日期(yyyy-MM-dd)</summary>
  9 + public string Date { get; set; } = string.Empty;
  10 +
  11 + /// <summary>值</summary>
  12 + public int Value { get; set; }
  13 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardMetricCardDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard;
  2 +
  3 +/// <summary>
  4 +/// 仪表盘指标卡片
  5 +/// </summary>
  6 +public class DashboardMetricCardDto
  7 +{
  8 + /// <summary>指标唯一标识(如 labelsPrintedToday)</summary>
  9 + public string Key { get; set; } = string.Empty;
  10 +
  11 + /// <summary>指标标题</summary>
  12 + public string Title { get; set; } = string.Empty;
  13 +
  14 + /// <summary>当前值</summary>
  15 + public int Value { get; set; }
  16 +
  17 + /// <summary>对比周期值</summary>
  18 + public int PreviousValue { get; set; }
  19 +
  20 + /// <summary>增减值(Value - PreviousValue)</summary>
  21 + public int ChangeValue { get; set; }
  22 +
  23 + /// <summary>增减比例(百分比)</summary>
  24 + public decimal ChangeRate { get; set; }
  25 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardOverviewOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard;
  2 +
  3 +/// <summary>
  4 +/// 仪表盘总览输出
  5 +/// </summary>
  6 +public class DashboardOverviewOutputDto
  7 +{
  8 + /// <summary>今日打印标签</summary>
  9 + public DashboardMetricCardDto LabelsPrintedToday { get; set; } = new();
  10 +
  11 + /// <summary>启用模板数</summary>
  12 + public DashboardMetricCardDto ActiveTemplates { get; set; } = new();
  13 +
  14 + /// <summary>活跃用户数</summary>
  15 + public DashboardMetricCardDto ActiveUsers { get; set; } = new();
  16 +
  17 + /// <summary>门店数</summary>
  18 + public DashboardMetricCardDto Locations { get; set; } = new();
  19 +
  20 + /// <summary>人员数</summary>
  21 + public DashboardMetricCardDto People { get; set; } = new();
  22 +
  23 + /// <summary>产品数</summary>
  24 + public DashboardMetricCardDto Products { get; set; } = new();
  25 +
  26 + /// <summary>指标卡片</summary>
  27 + public List<DashboardMetricCardDto> MetricCards { get; set; } = new();
  28 +
  29 + /// <summary>近7天打印趋势</summary>
  30 + public List<DashboardDailyTrendPointDto> WeeklyPrintVolume { get; set; } = new();
  31 +
  32 + /// <summary>按分类分布</summary>
  33 + public List<DashboardCategoryDistributionDto> CategoryDistribution { get; set; } = new();
  34 +
  35 + /// <summary>分类分布总数</summary>
  36 + public int CategoryDistributionTotal { get; set; }
  37 +
  38 + /// <summary>按分类分布(前端直观命名)</summary>
  39 + public List<DashboardCategoryDistributionDto> ByCategory { get; set; } = new();
  40 +
  41 + /// <summary>按分类分布总数(前端直观命名)</summary>
  42 + public int ByCategoryTotal { get; set; }
  43 +
  44 + /// <summary>最近打印标签(全门店最新若干条,用于 Recent Labels 区块)</summary>
  45 + public List<DashboardRecentLabelItemDto> RecentLabels { get; set; } = new();
  46 +
  47 + /// <summary>统计时间</summary>
  48 + public DateTime GeneratedAt { get; set; }
  49 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Dashboard/DashboardRecentLabelItemDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Dashboard;
  2 +
  3 +/// <summary>
  4 +/// Dashboard「Recent Labels」单行数据(最近打印记录)
  5 +/// </summary>
  6 +public class DashboardRecentLabelItemDto
  7 +{
  8 + /// <summary>打印任务 Id(fl_label_print_task.Id)</summary>
  9 + public string TaskId { get; set; } = string.Empty;
  10 +
  11 + /// <summary>标签编码(界面 Serial / Label ID,如 1-251201)</summary>
  12 + public string LabelCode { get; set; } = string.Empty;
  13 +
  14 + /// <summary>展示名称:优先产品名,否则标签名称</summary>
  15 + public string DisplayName { get; set; } = "无";
  16 +
  17 + /// <summary>打印人用户 Id(CreatedBy)</summary>
  18 + public string? PrintedByUserId { get; set; }
  19 +
  20 + /// <summary>打印人展示名(User.Name 或 UserName)</summary>
  21 + public string PrintedByName { get; set; } = "无";
  22 +
  23 + /// <summary>打印时间(PrintedAt 优先,否则 CreationTime)</summary>
  24 + public DateTime PrintedAt { get; set; }
  25 +
  26 + /// <summary>状态:<c>active</c> 或 <c>expired</c>(依据 PrintInputJson 中保质期与当前日期比较)</summary>
  27 + public string Status { get; set; } = "active";
  28 +
  29 + /// <summary>角标/尺寸短文案(如 2"x2",用于左侧圆标)</summary>
  30 + public string LabelTypeBadge { get; set; } = "无";
  31 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupCreateInputVo.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Group;
  2 +
  3 +/// <summary>
  4 +/// 新建组织入参
  5 +/// </summary>
  6 +public class GroupCreateInputVo
  7 +{
  8 + public string GroupName { get; set; } = string.Empty;
  9 +
  10 + /// <summary>
  11 + /// 指派到的合作伙伴 Id(Assign to Partner)
  12 + /// </summary>
  13 + public string PartnerId { get; set; } = string.Empty;
  14 +
  15 + public bool State { get; set; } = true;
  16 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListInputVo.cs 0 → 100644
  1 +using Volo.Abp.Application.Dtos;
  2 +
  3 +namespace FoodLabeling.Application.Contracts.Dtos.Group;
  4 +
  5 +/// <summary>
  6 +/// 组织(Group)分页查询入参
  7 +/// </summary>
  8 +public class GroupGetListInputVo : PagedAndSortedResultRequestDto
  9 +{
  10 + /// <summary>
  11 + /// 模糊搜索(GroupName、所属 Partner 的 PartnerName)
  12 + /// </summary>
  13 + public string? Keyword { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 按所属合作伙伴筛选(fl_partner.Id)
  17 + /// </summary>
  18 + public string? PartnerId { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 启用状态
  22 + /// </summary>
  23 + public bool? State { get; set; }
  24 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetListOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Group;
  2 +
  3 +/// <summary>
  4 +/// 组织列表项
  5 +/// </summary>
  6 +public class GroupGetListOutputDto
  7 +{
  8 + public string Id { get; set; } = string.Empty;
  9 +
  10 + public string GroupName { get; set; } = string.Empty;
  11 +
  12 + public string PartnerId { get; set; } = string.Empty;
  13 +
  14 + /// <summary>
  15 + /// 所属合作伙伴名称(列表「Parent Partner」列)
  16 + /// </summary>
  17 + public string PartnerName { get; set; } = string.Empty;
  18 +
  19 + public bool State { get; set; }
  20 +
  21 + public DateTime CreationTime { get; set; }
  22 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupGetOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Group;
  2 +
  3 +/// <summary>
  4 +/// 组织详情
  5 +/// </summary>
  6 +public class GroupGetOutputDto
  7 +{
  8 + public string Id { get; set; } = string.Empty;
  9 +
  10 + public string GroupName { get; set; } = string.Empty;
  11 +
  12 + public string PartnerId { get; set; } = string.Empty;
  13 +
  14 + public string PartnerName { get; set; } = string.Empty;
  15 +
  16 + public bool State { get; set; }
  17 +
  18 + public DateTime CreationTime { get; set; }
  19 +
  20 + public DateTime? LastModificationTime { get; set; }
  21 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Group/GroupUpdateInputVo.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Group;
  2 +
  3 +/// <summary>
  4 +/// 编辑组织入参
  5 +/// </summary>
  6 +public class GroupUpdateInputVo
  7 +{
  8 + public string GroupName { get; set; } = string.Empty;
  9 +
  10 + public string PartnerId { get; set; } = string.Empty;
  11 +
  12 + public bool State { get; set; } = true;
  13 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelCreateInputVo.cs
... ... @@ -2,7 +2,7 @@ namespace FoodLabeling.Application.Contracts.Dtos.Label;
2 2  
3 3 public class LabelCreateInputVo
4 4 {
5   - public string LabelCode { get; set; } = string.Empty;
  5 + public string? LabelCode { get; set; }
6 6  
7 7 public string LabelName { get; set; } = string.Empty;
8 8  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryCreateInputVo.cs
... ... @@ -6,10 +6,33 @@ public class LabelCategoryCreateInputVo
6 6  
7 7 public string CategoryName { get; set; } = string.Empty;
8 8  
  9 + /// <summary>
  10 + /// 按钮展示文案(为空则默认使用 CategoryName)
  11 + /// </summary>
  12 + public string? DisplayText { get; set; }
  13 +
  14 + /// <summary>
  15 + /// COLOR 模式存色值、IMAGE 模式存图片 URL、TEXT 可为分类小图或空(与 buttonAppearance 配合)
  16 + /// </summary>
9 17 public string? CategoryPhotoUrl { get; set; }
10 18  
11 19 public bool State { get; set; } = true;
12 20  
  21 + /// <summary>
  22 + /// 按钮外观:TEXT / COLOR / IMAGE(展示值见 categoryPhotoUrl)
  23 + /// </summary>
  24 + public string ButtonAppearance { get; set; } = "TEXT";
  25 +
  26 + /// <summary>
  27 + /// 门店可用范围:ALL / SPECIFIED
  28 + /// </summary>
  29 + public string AvailabilityType { get; set; } = "ALL";
  30 +
  31 + /// <summary>
  32 + /// 指定门店 Id 列表(当 AvailabilityType=SPECIFIED 时必填)
  33 + /// </summary>
  34 + public List<string>? LocationIds { get; set; }
  35 +
13 36 public int OrderNum { get; set; }
14 37 }
15 38  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListOutputDto.cs
... ... @@ -8,10 +8,16 @@ public class LabelCategoryGetListOutputDto
8 8  
9 9 public string CategoryName { get; set; } = string.Empty;
10 10  
  11 + public string? DisplayText { get; set; }
  12 +
11 13 public string? CategoryPhotoUrl { get; set; }
12 14  
13 15 public bool State { get; set; }
14 16  
  17 + public string ButtonAppearance { get; set; } = "TEXT";
  18 +
  19 + public string AvailabilityType { get; set; } = "ALL";
  20 +
15 21 public int OrderNum { get; set; }
16 22  
17 23 public long NoOfLabels { get; set; }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetOutputDto.cs
... ... @@ -8,10 +8,18 @@ public class LabelCategoryGetOutputDto
8 8  
9 9 public string CategoryName { get; set; } = string.Empty;
10 10  
  11 + public string? DisplayText { get; set; }
  12 +
11 13 public string? CategoryPhotoUrl { get; set; }
12 14  
13 15 public bool State { get; set; }
14 16  
  17 + public string ButtonAppearance { get; set; } = "TEXT";
  18 +
  19 + public string AvailabilityType { get; set; } = "ALL";
  20 +
  21 + public List<string> LocationIds { get; set; } = new();
  22 +
15 23 public int OrderNum { get; set; }
16 24 }
17 25  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelTemplate/LabelTemplateElementDto.cs
... ... @@ -3,7 +3,7 @@ using System.Text.Json.Serialization;
3 3 namespace FoodLabeling.Application.Contracts.Dtos.LabelTemplate;
4 4  
5 5 /// <summary>
6   -/// 模板元素(对齐你给的 editor JSON:id/type/x/y/width/height/rotation/border/config
  6 +/// 模板元素(对齐 editor JSON:id/type/typeAdd/elementName/x/y/width/height/rotation/border/config 等
7 7 /// </summary>
8 8 public class LabelTemplateElementDto
9 9 {
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportCreateInputVo.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.LocationSupport;
  2 +
  3 +/// <summary>
  4 +/// 新增门店 Support 联系方式
  5 +/// </summary>
  6 +public class LocationSupportCreateInputVo
  7 +{
  8 + public string SupportPhone { get; set; } = string.Empty;
  9 +
  10 + public string SupportEmail { get; set; } = string.Empty;
  11 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportGetOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.LocationSupport;
  2 +
  3 +/// <summary>
  4 +/// 门店 Support 联系方式详情
  5 +/// </summary>
  6 +public class LocationSupportGetOutputDto
  7 +{
  8 + public string Id { get; set; } = string.Empty;
  9 +
  10 + public string SupportPhone { get; set; } = string.Empty;
  11 +
  12 + public string SupportEmail { get; set; } = string.Empty;
  13 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LocationSupport/LocationSupportUpdateInputVo.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.LocationSupport;
  2 +
  3 +/// <summary>
  4 +/// 编辑门店 Support 联系方式
  5 +/// </summary>
  6 +public class LocationSupportUpdateInputVo
  7 +{
  8 + public string SupportPhone { get; set; } = string.Empty;
  9 +
  10 + public string SupportEmail { get; set; } = string.Empty;
  11 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerCreateInputVo.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Partner;
  2 +
  3 +/// <summary>
  4 +/// 新建合作伙伴入参
  5 +/// </summary>
  6 +public class PartnerCreateInputVo
  7 +{
  8 + public string PartnerName { get; set; } = string.Empty;
  9 +
  10 + public string? ContactEmail { get; set; }
  11 +
  12 + public string? PhoneNumber { get; set; }
  13 +
  14 + public bool State { get; set; } = true;
  15 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListInputVo.cs 0 → 100644
  1 +using Volo.Abp.Application.Dtos;
  2 +
  3 +namespace FoodLabeling.Application.Contracts.Dtos.Partner;
  4 +
  5 +/// <summary>
  6 +/// 合作伙伴分页查询入参
  7 +/// </summary>
  8 +public class PartnerGetListInputVo : PagedAndSortedResultRequestDto
  9 +{
  10 + /// <summary>
  11 + /// 模糊搜索(PartnerName / ContactEmail / PhoneNumber)
  12 + /// </summary>
  13 + public string? Keyword { get; set; }
  14 +
  15 + /// <summary>
  16 + /// 启用状态(与列表筛选一致)
  17 + /// </summary>
  18 + public bool? State { get; set; }
  19 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetListOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Partner;
  2 +
  3 +/// <summary>
  4 +/// 合作伙伴列表项
  5 +/// </summary>
  6 +public class PartnerGetListOutputDto
  7 +{
  8 + public string Id { get; set; } = string.Empty;
  9 +
  10 + public string PartnerName { get; set; } = string.Empty;
  11 +
  12 + public string? ContactEmail { get; set; }
  13 +
  14 + public string? PhoneNumber { get; set; }
  15 +
  16 + public bool State { get; set; }
  17 +
  18 + public DateTime CreationTime { get; set; }
  19 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerGetOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Partner;
  2 +
  3 +/// <summary>
  4 +/// 合作伙伴详情
  5 +/// </summary>
  6 +public class PartnerGetOutputDto
  7 +{
  8 + public string Id { get; set; } = string.Empty;
  9 +
  10 + public string PartnerName { get; set; } = string.Empty;
  11 +
  12 + public string? ContactEmail { get; set; }
  13 +
  14 + public string? PhoneNumber { get; set; }
  15 +
  16 + public bool State { get; set; }
  17 +
  18 + public DateTime CreationTime { get; set; }
  19 +
  20 + public DateTime? LastModificationTime { get; set; }
  21 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Partner/PartnerUpdateInputVo.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Partner;
  2 +
  3 +/// <summary>
  4 +/// 编辑合作伙伴入参
  5 +/// </summary>
  6 +public class PartnerUpdateInputVo
  7 +{
  8 + public string PartnerName { get; set; } = string.Empty;
  9 +
  10 + public string? ContactEmail { get; set; }
  11 +
  12 + public string? PhoneNumber { get; set; }
  13 +
  14 + public bool State { get; set; } = true;
  15 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Product/ProductCreateInputVo.cs
  1 +using System.Collections.Generic;
  2 +
1 3 namespace FoodLabeling.Application.Contracts.Dtos.Product;
2 4  
3 5 public class ProductCreateInputVo
... ... @@ -11,5 +13,11 @@ public class ProductCreateInputVo
11 13 public string? ProductImageUrl { get; set; }
12 14  
13 15 public bool State { get; set; } = true;
  16 +
  17 + /// <summary>
  18 + /// 可选。门店 Id 列表;每个 Id 在 fl_location_product 落一行(同一 fl_product 可对应多门店)。
  19 + /// 不传或空列表则不在本接口写入门店关联(仍可用 product-location 接口维护)。
  20 + /// </summary>
  21 + public List<string>? LocationIds { get; set; }
14 22 }
15 23  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryCreateInputVo.cs
... ... @@ -9,10 +9,33 @@ public class ProductCategoryCreateInputVo
9 9  
10 10 public string CategoryName { get; set; } = string.Empty;
11 11  
  12 + /// <summary>
  13 + /// 按钮展示文案(为空则默认使用 CategoryName)
  14 + /// </summary>
  15 + public string? DisplayText { get; set; }
  16 +
  17 + /// <summary>
  18 + /// COLOR 模式存色值、IMAGE 模式存图片 URL、TEXT 可为分类小图或空(与 buttonAppearance 配合)
  19 + /// </summary>
12 20 public string? CategoryPhotoUrl { get; set; }
13 21  
  22 + /// <summary>
  23 + /// 按钮外观:TEXT / COLOR / IMAGE(展示值见 categoryPhotoUrl)
  24 + /// </summary>
  25 + public string ButtonAppearance { get; set; } = "TEXT";
  26 +
14 27 public bool State { get; set; } = true;
15 28  
  29 + /// <summary>
  30 + /// 门店可用范围:ALL / SPECIFIED
  31 + /// </summary>
  32 + public string AvailabilityType { get; set; } = "ALL";
  33 +
  34 + /// <summary>
  35 + /// 指定门店 Id 列表(当 AvailabilityType=SPECIFIED 时必填)
  36 + /// </summary>
  37 + public List<string>? LocationIds { get; set; }
  38 +
16 39 public int OrderNum { get; set; } = 0;
17 40 }
18 41  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListOutputDto.cs
... ... @@ -11,10 +11,16 @@ public class ProductCategoryGetListOutputDto
11 11  
12 12 public string CategoryName { get; set; } = string.Empty;
13 13  
  14 + public string? DisplayText { get; set; }
  15 +
14 16 public string? CategoryPhotoUrl { get; set; }
15 17  
  18 + public string ButtonAppearance { get; set; } = "TEXT";
  19 +
16 20 public bool State { get; set; }
17 21  
  22 + public string AvailabilityType { get; set; } = "ALL";
  23 +
18 24 public int OrderNum { get; set; }
19 25  
20 26 public DateTime LastEdited { get; set; }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetOutputDto.cs
... ... @@ -11,10 +11,19 @@ public class ProductCategoryGetOutputDto
11 11  
12 12 public string CategoryName { get; set; } = string.Empty;
13 13  
  14 + public string? DisplayText { get; set; }
  15 +
  16 + /// <summary>COLOR 色值 / IMAGE 图片 URL / TEXT 可选图</summary>
14 17 public string? CategoryPhotoUrl { get; set; }
15 18  
  19 + public string ButtonAppearance { get; set; } = "TEXT";
  20 +
16 21 public bool State { get; set; }
17 22  
  23 + public string AvailabilityType { get; set; } = "ALL";
  24 +
  25 + public List<string> LocationIds { get; set; } = new();
  26 +
18 27 public int OrderNum { get; set; }
19 28 }
20 29  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/RbacMenu/RbacMenuGetListOutputDto.cs
... ... @@ -11,6 +11,10 @@ public class RbacMenuGetListOutputDto
11 11  
12 12 public string MenuName { get; set; } = string.Empty;
13 13  
  14 + public string? RouterName { get; set; }
  15 +
  16 + public string? Router { get; set; }
  17 +
14 18 public string? PermissionCode { get; set; }
15 19  
16 20 public int MenuType { get; set; }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Reports;
  2 +
  3 +/// <summary>
  4 +/// Label Report 聚合结果(卡片 + 图表 + Top 产品表)
  5 +/// </summary>
  6 +public class ReportsLabelReportOutputDto
  7 +{
  8 + public ReportsLabelReportSummaryDto Summary { get; set; } = new();
  9 +
  10 + /// <summary>按标签分类的打印量(柱状图)</summary>
  11 + public List<ReportsCategoryCountDto> LabelsByCategory { get; set; } = new();
  12 +
  13 + /// <summary>折线图:默认统计区间内最后 7 个自然日(按日汇总)</summary>
  14 + public List<ReportsDailyCountDto> PrintVolumeTrend { get; set; } = new();
  15 +
  16 + /// <summary>用量最高的产品(含占比)</summary>
  17 + public List<ReportsTopProductRowDto> MostUsedProducts { get; set; } = new();
  18 +}
  19 +
  20 +public class ReportsLabelReportSummaryDto
  21 +{
  22 + public int TotalLabelsPrinted { get; set; }
  23 +
  24 + public int TotalLabelsPrintedPrevPeriod { get; set; }
  25 +
  26 + /// <summary>相对上一同长周期变化率(百分比,如 20.1 表示 +20.1%)</summary>
  27 + public decimal TotalLabelsPrintedChangeRate { get; set; }
  28 +
  29 + public string? MostPrintedCategoryName { get; set; }
  30 +
  31 + public int MostPrintedCategoryCount { get; set; }
  32 +
  33 + public string? TopProductName { get; set; }
  34 +
  35 + public int TopProductCount { get; set; }
  36 +
  37 + public decimal AvgDailyPrints { get; set; }
  38 +
  39 + public decimal AvgDailyPrintsPrevPeriod { get; set; }
  40 +
  41 + public decimal AvgDailyPrintsChangeRate { get; set; }
  42 +}
  43 +
  44 +public class ReportsCategoryCountDto
  45 +{
  46 + public string? CategoryId { get; set; }
  47 +
  48 + public string? CategoryName { get; set; }
  49 +
  50 + public int Count { get; set; }
  51 +}
  52 +
  53 +public class ReportsDailyCountDto
  54 +{
  55 + public string Date { get; set; } = string.Empty;
  56 +
  57 + public int Count { get; set; }
  58 +}
  59 +
  60 +public class ReportsTopProductRowDto
  61 +{
  62 + public string? ProductId { get; set; }
  63 +
  64 + public string? ProductName { get; set; }
  65 +
  66 + public string? CategoryName { get; set; }
  67 +
  68 + public int TotalPrinted { get; set; }
  69 +
  70 + public decimal UsagePercent { get; set; }
  71 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsLabelReportQueryInputVo.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Reports;
  2 +
  3 +/// <summary>
  4 +/// Label Report 统计与导出共用筛选
  5 +/// </summary>
  6 +public class ReportsLabelReportQueryInputVo
  7 +{
  8 + public string? PartnerId { get; set; }
  9 +
  10 + public string? GroupId { get; set; }
  11 +
  12 + public string? LocationId { get; set; }
  13 +
  14 + public DateTime? StartDate { get; set; }
  15 +
  16 + public DateTime? EndDate { get; set; }
  17 +
  18 + public string? Keyword { get; set; }
  19 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogGetListInputVo.cs 0 → 100644
  1 +using Volo.Abp.Application.Dtos;
  2 +
  3 +namespace FoodLabeling.Application.Contracts.Dtos.Reports;
  4 +
  5 +/// <summary>
  6 +/// Web Reports — Print Log 分页查询
  7 +/// </summary>
  8 +public class ReportsPrintLogGetListInputVo : PagedAndSortedResultRequestDto
  9 +{
  10 + public string? PartnerId { get; set; }
  11 +
  12 + public string? GroupId { get; set; }
  13 +
  14 + public string? LocationId { get; set; }
  15 +
  16 + public DateTime? StartDate { get; set; }
  17 +
  18 + public DateTime? EndDate { get; set; }
  19 +
  20 + public string? Keyword { get; set; }
  21 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Reports/ReportsPrintLogListItemDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.Reports;
  2 +
  3 +/// <summary>
  4 +/// Print Log 列表行(Reports 管理端)
  5 +/// </summary>
  6 +public class ReportsPrintLogListItemDto
  7 +{
  8 + /// <summary>打印任务 Id(fl_label_print_task.Id),重打时使用</summary>
  9 + public string TaskId { get; set; } = string.Empty;
  10 +
  11 + /// <summary>标签编码(展示为 Label ID)</summary>
  12 + public string LabelCode { get; set; } = string.Empty;
  13 +
  14 + public string ProductName { get; set; } = "无";
  15 +
  16 + /// <summary>分类展示名(优先产品分类,否则标签分类)</summary>
  17 + public string CategoryName { get; set; } = "无";
  18 +
  19 + /// <summary>模板展示(尺寸 + 模板名)</summary>
  20 + public string TemplateText { get; set; } = "无";
  21 +
  22 + public DateTime PrintedAt { get; set; }
  23 +
  24 + /// <summary>打印人姓名</summary>
  25 + public string PrintedByName { get; set; } = "无";
  26 +
  27 + /// <summary>门店展示:名称 (编码)</summary>
  28 + public string LocationText { get; set; } = "无";
  29 +
  30 + /// <summary>门店 Id(重打校验用)</summary>
  31 + public string? LocationId { get; set; }
  32 +
  33 + /// <summary>从 PrintInputJson 尽力解析的保质期文本;无则「无」</summary>
  34 + public string ExpiryDateText { get; set; } = "无";
  35 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppChangePasswordInputVo.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
  2 +
  3 +/// <summary>
  4 +/// App 修改密码入参
  5 +/// </summary>
  6 +public class UsAppChangePasswordInputVo
  7 +{
  8 + /// <summary>当前密码</summary>
  9 + public string CurrentPassword { get; set; } = string.Empty;
  10 +
  11 + /// <summary>新密码</summary>
  12 + public string NewPassword { get; set; } = string.Empty;
  13 +
  14 + /// <summary>确认新密码</summary>
  15 + public string ConfirmNewPassword { get; set; } = string.Empty;
  16 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppLocationDetailOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
  2 +
  3 +/// <summary>
  4 +/// App「Location」门店详情(与原型字段对齐)
  5 +/// </summary>
  6 +public class UsAppLocationDetailOutputDto
  7 +{
  8 + /// <summary>门店主键(Guid 字符串)</summary>
  9 + public string LocationId { get; set; } = string.Empty;
  10 +
  11 + /// <summary>门店名称</summary>
  12 + public string LocationName { get; set; } = string.Empty;
  13 +
  14 + /// <summary>完整地址(街道、城市、州、邮编拼接;无则为「无」)</summary>
  15 + public string FullAddress { get; set; } = string.Empty;
  16 +
  17 + /// <summary>门店电话(来自 location.Phone;空为「无」)</summary>
  18 + public string StorePhone { get; set; } = string.Empty;
  19 +
  20 + /// <summary>营业时间;当前库无字段,固定返回「无」直至业务落库</summary>
  21 + public string OperatingHours { get; set; } = string.Empty;
  22 +
  23 + /// <summary>店长姓名;优先取绑定本店且角色名/编码含 manager 的用户</summary>
  24 + public string ManagerName { get; set; } = string.Empty;
  25 +
  26 + /// <summary>店长电话;同上用户 User.Phone 格式化;无则为「无」</summary>
  27 + public string ManagerPhone { get; set; } = string.Empty;
  28 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppAuth/UsAppMyProfileOutputDto.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
  2 +
  3 +/// <summary>
  4 +/// App「我的资料」展示数据(My Profile)
  5 +/// </summary>
  6 +public class UsAppMyProfileOutputDto
  7 +{
  8 + /// <summary>全名(Name,无则 Nick,再无则 UserName)</summary>
  9 + public string FullName { get; set; } = string.Empty;
  10 +
  11 + /// <summary>邮箱</summary>
  12 + public string Email { get; set; } = "无";
  13 +
  14 + /// <summary>电话展示(如 +1 (555) 123-4567);无则「无」</summary>
  15 + public string Phone { get; set; } = "无";
  16 +
  17 + /// <summary>员工号/登录名(当前使用 <c>User.UserName</c>,可与业务约定为工号)</summary>
  18 + public string EmployeeId { get; set; } = string.Empty;
  19 +
  20 + /// <summary>角色展示名(多角色英文逗号拼接,按角色 OrderNum)</summary>
  21 + public string RoleDisplay { get; set; } = "无";
  22 +
  23 + /// <summary>主角色编码(第一个角色 RoleCode,供前端样式)</summary>
  24 + public string? PrimaryRoleCode { get; set; }
  25 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelCategoryTreeNodeDto.cs
... ... @@ -11,6 +11,9 @@ public class UsAppLabelCategoryTreeNodeDto
11 11  
12 12 public string? CategoryPhotoUrl { get; set; }
13 13  
  14 + /// <summary>按钮外观:TEXT / COLOR / IMAGE(COLOR/IMAGE 的展示值在 categoryPhotoUrl)</summary>
  15 + public string ButtonAppearance { get; set; } = "TEXT";
  16 +
14 17 public int OrderNum { get; set; }
15 18  
16 19 public List<UsAppProductCategoryNodeDto> ProductCategories { get; set; } = new();
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppLabelPrintInputVo.cs
... ... @@ -49,11 +49,6 @@ public class UsAppLabelPrintInputVo
49 49 public JsonElement? PrintInputJson { get; set; }
50 50  
51 51 /// <summary>
52   - /// 客户端幂等请求 Id(可选);重复相同值时由服务端决定是否直接返回首次结果(见接口文档)。
53   - /// </summary>
54   - public string? ClientRequestId { get; set; }
55   -
56   - /// <summary>
57 52 /// 打印机Id(可选,若业务需要追踪)
58 53 /// </summary>
59 54 public string? PrinterId { get; set; }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/UsAppLabeling/UsAppProductCategoryNodeDto.cs
... ... @@ -14,6 +14,18 @@ public class UsAppProductCategoryNodeDto
14 14 /// <summary>分类显示名;空为「无」</summary>
15 15 public string Name { get; set; } = string.Empty;
16 16  
  17 + /// <summary>按钮展示文案;为空时客户端可回退使用 Name</summary>
  18 + public string? DisplayText { get; set; }
  19 +
  20 + /// <summary>按钮外观:TEXT / COLOR / IMAGE(COLOR/IMAGE 的展示值在 categoryPhotoUrl)</summary>
  21 + public string ButtonAppearance { get; set; } = "TEXT";
  22 +
  23 + /// <summary>门店可用范围:ALL / SPECIFIED(本树已按当前门店过滤)</summary>
  24 + public string AvailabilityType { get; set; } = "ALL";
  25 +
  26 + /// <summary>排序号(来自 fl_product_category;未归类为较大值以排在后)</summary>
  27 + public int OrderNum { get; set; }
  28 +
17 29 public int ItemCount { get; set; }
18 30  
19 31 public List<UsAppLabelingProductNodeDto> Products { get; set; } = new();
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IAuthSessionAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.AuthSession;
  2 +using Volo.Abp.Application.Services;
  3 +
  4 +namespace FoodLabeling.Application.Contracts.IServices;
  5 +
  6 +/// <summary>
  7 +/// 当前登录会话:菜单权限与退出(美国版 Web 管理端)
  8 +/// </summary>
  9 +public interface IAuthSessionAppService : IApplicationService
  10 +{
  11 + /// <summary>
  12 + /// 获取当前登录用户的角色编码、权限码与可见菜单树
  13 + /// </summary>
  14 + /// <remarks>
  15 + /// 与框架 <c>UserManager.GetInfoAsync</c> 一致;用户名为 <c>admin</c> 时返回全部未删除菜单(与 <c>AccountService.GetVue3Router</c> 行为对齐)。
  16 + /// </remarks>
  17 + /// <returns>用户简要信息、权限码与菜单树</returns>
  18 + /// <response code="200">成功</response>
  19 + /// <response code="401">未登录或令牌无效</response>
  20 + /// <response code="500">服务器错误</response>
  21 + Task<CurrentUserMenuPermissionsOutputDto> GetMyMenusAsync();
  22 +
  23 + /// <summary>
  24 + /// 退出登录:清除服务端用户信息缓存(JWT 仍由前端丢弃)
  25 + /// </summary>
  26 + /// <remarks>
  27 + /// 与框架 <c>AccountService.PostLogout</c> 一致;未登录时返回 <c>false</c>。
  28 + /// </remarks>
  29 + /// <returns>是否执行了缓存清理(已登录为 true)</returns>
  30 + /// <response code="200">成功</response>
  31 + /// <response code="500">服务器错误</response>
  32 + Task<bool> LogoutAsync();
  33 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IDashboardAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.Dashboard;
  2 +using Volo.Abp.Application.Services;
  3 +
  4 +namespace FoodLabeling.Application.Contracts.IServices;
  5 +
  6 +/// <summary>
  7 +/// Dashboard 统计接口(美国版)
  8 +/// </summary>
  9 +public interface IDashboardAppService : IApplicationService
  10 +{
  11 + /// <summary>
  12 + /// 获取 Dashboard 总览统计(卡片 + 周趋势 + 分类分布 + Recent Labels 最近打印)
  13 + /// </summary>
  14 + Task<DashboardOverviewOutputDto> GetOverviewAsync();
  15 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IGroupAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.Common;
  2 +using FoodLabeling.Application.Contracts.Dtos.Group;
  3 +using Microsoft.AspNetCore.Mvc;
  4 +using Volo.Abp.Application.Dtos;
  5 +using Volo.Abp.Application.Services;
  6 +
  7 +namespace FoodLabeling.Application.Contracts.IServices;
  8 +
  9 +/// <summary>
  10 +/// 组织(Group)管理接口(fl_group)
  11 +/// </summary>
  12 +public interface IGroupAppService : IApplicationService
  13 +{
  14 + /// <summary>
  15 + /// 组织分页列表(与导出使用相同筛选条件)
  16 + /// </summary>
  17 + /// <param name="input">分页与筛选;SkipCount 为页码(从 1 起)</param>
  18 + Task<PagedResultWithPageDto<GroupGetListOutputDto>> GetListAsync(GroupGetListInputVo input);
  19 +
  20 + /// <summary>
  21 + /// 组织详情
  22 + /// </summary>
  23 + Task<GroupGetOutputDto> GetAsync(string id);
  24 +
  25 + /// <summary>
  26 + /// 新增组织
  27 + /// </summary>
  28 + Task<GroupGetOutputDto> CreateAsync(GroupCreateInputVo input);
  29 +
  30 + /// <summary>
  31 + /// 编辑组织
  32 + /// </summary>
  33 + Task<GroupGetOutputDto> UpdateAsync(string id, GroupUpdateInputVo input);
  34 +
  35 + /// <summary>
  36 + /// 删除组织(逻辑删除)
  37 + /// </summary>
  38 + Task DeleteAsync(string id);
  39 +
  40 + /// <summary>
  41 + /// 按列表相同筛选条件导出组织为 PDF(上限 5000 条)
  42 + /// </summary>
  43 + Task<IActionResult> ExportPdfAsync(GroupGetListInputVo input);
  44 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILocationSupportAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.LocationSupport;
  2 +using Volo.Abp.Application.Services;
  3 +
  4 +namespace FoodLabeling.Application.Contracts.IServices;
  5 +
  6 +/// <summary>
  7 +/// 全局 Support 联系方式(全平台共用;Web 可增改查,App 仅可查)
  8 +/// </summary>
  9 +public interface ILocationSupportAppService : IApplicationService
  10 +{
  11 + /// <summary>
  12 + /// 查询全局 Support 联系方式(已登录即可;App / Web 共用)
  13 + /// </summary>
  14 + Task<LocationSupportGetOutputDto?> GetSupportAsync();
  15 +
  16 + /// <summary>
  17 + /// 新增全局 Support 联系方式(系统仅允许一条;Web 管理端)
  18 + /// </summary>
  19 + /// <param name="input">联系方式</param>
  20 + Task<LocationSupportGetOutputDto> CreateAsync(LocationSupportCreateInputVo input);
  21 +
  22 + /// <summary>
  23 + /// 编辑全局 Support 联系方式(Web 管理端)
  24 + /// </summary>
  25 + /// <param name="id">联系方式主键</param>
  26 + /// <param name="input">联系方式</param>
  27 + Task<LocationSupportGetOutputDto> UpdateAsync(string id, LocationSupportUpdateInputVo input);
  28 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IPartnerAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.Common;
  2 +using FoodLabeling.Application.Contracts.Dtos.Partner;
  3 +using Microsoft.AspNetCore.Mvc;
  4 +using Volo.Abp.Application.Dtos;
  5 +using Volo.Abp.Application.Services;
  6 +
  7 +namespace FoodLabeling.Application.Contracts.IServices;
  8 +
  9 +/// <summary>
  10 +/// 合作伙伴管理接口(fl_partner)
  11 +/// </summary>
  12 +public interface IPartnerAppService : IApplicationService
  13 +{
  14 + /// <summary>
  15 + /// 合作伙伴分页列表(与导出使用相同筛选条件)
  16 + /// </summary>
  17 + /// <param name="input">分页与筛选;SkipCount 为页码(从 1 起)</param>
  18 + /// <returns>分页数据</returns>
  19 + /// <response code="200">成功</response>
  20 + /// <response code="400">参数错误</response>
  21 + /// <response code="500">服务器错误</response>
  22 + Task<PagedResultWithPageDto<PartnerGetListOutputDto>> GetListAsync(PartnerGetListInputVo input);
  23 +
  24 + /// <summary>
  25 + /// 合作伙伴详情
  26 + /// </summary>
  27 + /// <param name="id">主键 Id</param>
  28 + /// <returns>详情</returns>
  29 + /// <response code="200">成功</response>
  30 + /// <response code="400">Id 无效</response>
  31 + /// <response code="500">服务器错误</response>
  32 + Task<PartnerGetOutputDto> GetAsync(string id);
  33 +
  34 + /// <summary>
  35 + /// 新增合作伙伴
  36 + /// </summary>
  37 + /// <param name="input">名称、邮箱、电话、启用状态</param>
  38 + /// <returns>新建后的详情</returns>
  39 + /// <remarks>
  40 + /// 示例请求:
  41 + /// ```json
  42 + /// {
  43 + /// "partnerName": "Global Foods Inc.",
  44 + /// "contactEmail": "admin@globalfoods.com",
  45 + /// "phoneNumber": "+1 (555) 100-2000",
  46 + /// "state": true
  47 + /// }
  48 + /// ```
  49 + /// </remarks>
  50 + /// <response code="200">成功</response>
  51 + /// <response code="400">校验失败</response>
  52 + /// <response code="500">服务器错误</response>
  53 + Task<PartnerGetOutputDto> CreateAsync(PartnerCreateInputVo input);
  54 +
  55 + /// <summary>
  56 + /// 编辑合作伙伴
  57 + /// </summary>
  58 + /// <param name="id">主键 Id</param>
  59 + /// <param name="input">名称、邮箱、电话、启用状态</param>
  60 + /// <returns>更新后的详情</returns>
  61 + /// <response code="200">成功</response>
  62 + /// <response code="400">校验失败或记录不存在</response>
  63 + /// <response code="500">服务器错误</response>
  64 + Task<PartnerGetOutputDto> UpdateAsync(string id, PartnerUpdateInputVo input);
  65 +
  66 + /// <summary>
  67 + /// 删除合作伙伴(逻辑删除)
  68 + /// </summary>
  69 + /// <param name="id">主键 Id</param>
  70 + /// <response code="200">成功</response>
  71 + /// <response code="400">Id 无效或记录不存在</response>
  72 + /// <response code="500">服务器错误</response>
  73 + Task DeleteAsync(string id);
  74 +
  75 + /// <summary>
  76 + /// 按当前列表筛选条件批量导出合作伙伴为 PDF(不分页,上限 5000 条)
  77 + /// </summary>
  78 + /// <param name="input">与列表相同的 Keyword、State;分页字段忽略</param>
  79 + /// <returns>PDF 文件流</returns>
  80 + /// <remarks>
  81 + /// 筛选条件需与 <see cref="GetListAsync"/> 一致,便于统计与导出数据对齐。
  82 + /// </remarks>
  83 + /// <response code="200">成功返回 application/pdf</response>
  84 + /// <response code="400">参数错误</response>
  85 + /// <response code="500">服务器错误</response>
  86 + Task<IActionResult> ExportPdfAsync(PartnerGetListInputVo input);
  87 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IProductAppService.cs
... ... @@ -23,11 +23,18 @@ public interface IProductAppService : IApplicationService
23 23 /// <summary>
24 24 /// 新增产品
25 25 /// </summary>
  26 + /// <remarks>
  27 + /// 若 <see cref="ProductCreateInputVo.LocationIds"/> 有值,将在同一事务内批量写入 fl_location_product(一门店一条)。
  28 + /// </remarks>
26 29 Task<ProductGetOutputDto> CreateAsync(ProductCreateInputVo input);
27 30  
28 31 /// <summary>
29 32 /// 编辑产品
30 33 /// </summary>
  34 + /// <remarks>
  35 + /// 当请求体包含 <see cref="ProductCreateInputVo.LocationIds"/> 属性时,按该列表整表替换本产品在各门店的关联;
  36 + /// 不传该属性则不改门店关联(兼容仅改名称/分类等调用)。
  37 + /// </remarks>
31 38 Task<ProductGetOutputDto> UpdateAsync(string id, ProductUpdateInputVo input);
32 39  
33 40 /// <summary>
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IReportsAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.Common;
  2 +using FoodLabeling.Application.Contracts.Dtos.Reports;
  3 +using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  4 +using Microsoft.AspNetCore.Mvc;
  5 +using Volo.Abp.Application.Services;
  6 +
  7 +namespace FoodLabeling.Application.Contracts.IServices;
  8 +
  9 +/// <summary>
  10 +/// Reports(Print Log / Label Report)管理端接口
  11 +/// </summary>
  12 +public interface IReportsAppService : IApplicationService
  13 +{
  14 + /// <summary>
  15 + /// Print Log 分页列表;角色 <c>admin</c> 可查全部,否则仅当前用户打印记录。
  16 + /// </summary>
  17 + Task<PagedResultWithPageDto<ReportsPrintLogListItemDto>> GetPrintLogListAsync(ReportsPrintLogGetListInputVo input);
  18 +
  19 + /// <summary>
  20 + /// Print Log 导出 PDF(筛选与列表一致,最多 5000 条)
  21 + /// </summary>
  22 + Task<IActionResult> ExportPrintLogPdfAsync(ReportsPrintLogGetListInputVo input);
  23 +
  24 + /// <summary>
  25 + /// 根据历史任务重打(与 App 入参一致);<c>admin</c> 可重打任意用户任务,否则仅本人任务。
  26 + /// </summary>
  27 + Task<UsAppLabelPrintOutputDto> ReprintPrintLogAsync(UsAppLabelReprintInputVo input);
  28 +
  29 + /// <summary>
  30 + /// Label Report 统计(卡片 + 分类柱数据 + 7 日趋势 + Top 产品);<c>admin</c> 统计全部,否则仅当前用户。
  31 + /// </summary>
  32 + Task<ReportsLabelReportOutputDto> GetLabelReportAsync(ReportsLabelReportQueryInputVo input);
  33 +
  34 + /// <summary>
  35 + /// Label Report 导出 PDF
  36 + /// </summary>
  37 + Task<IActionResult> ExportLabelReportPdfAsync(ReportsLabelReportQueryInputVo input);
  38 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs
... ... @@ -17,4 +17,35 @@ public interface IUsAppAuthAppService : IApplicationService
17 17 /// 获取当前登录账号已绑定的门店(用于切换门店等场景)
18 18 /// </summary>
19 19 Task<List<UsAppBoundLocationDto>> GetMyLocationsAsync();
  20 +
  21 + /// <summary>
  22 + /// 获取当前登录用户资料(My Profile:姓名、邮箱、电话、员工号、角色)
  23 + /// </summary>
  24 + /// <returns>资料 DTO</returns>
  25 + /// <response code="200">成功</response>
  26 + /// <response code="400">未登录或用户不存在</response>
  27 + /// <response code="500">服务器错误</response>
  28 + Task<UsAppMyProfileOutputDto> GetMyProfileAsync();
  29 +
  30 + /// <summary>
  31 + /// 当前登录用户修改密码(校验原密码与复杂度规则)
  32 + /// </summary>
  33 + /// <param name="input">当前密码、新密码、确认密码</param>
  34 + /// <remarks>
  35 + /// 新密码需满足:至少 8 位;含大写与小写字母;至少 1 位数字;至少 1 个非字母数字特殊字符。
  36 + /// </remarks>
  37 + /// <response code="200">成功</response>
  38 + /// <response code="400">参数或校验失败</response>
  39 + /// <response code="500">服务器错误</response>
  40 + Task ChangePasswordAsync(UsAppChangePasswordInputVo input);
  41 +
  42 + /// <summary>
  43 + /// 按门店 Id 查询 Location 详情(须为当前账号 userlocation 绑定门店)
  44 + /// </summary>
  45 + /// <param name="locationId">门店 Guid 字符串</param>
  46 + /// <returns>店名、地址、电话、营业时间占位、店长信息</returns>
  47 + /// <response code="200">成功</response>
  48 + /// <response code="400">参数非法、未绑定或无权限</response>
  49 + /// <response code="500">服务器错误</response>
  50 + Task<UsAppLocationDetailOutputDto> GetLocationDetailAsync(string locationId);
20 51 }
... ...
美国版/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 2 using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
3   -using FoodLabeling.Application.Contracts.Dtos.Common;
4 3 using Volo.Abp.Application.Services;
5 4  
6 5 namespace FoodLabeling.Application.Contracts.IServices;
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/UsAppJwtClaims.cs 0 → 100644
  1 +namespace FoodLabeling.Application.Contracts;
  2 +
  3 +/// <summary>
  4 +/// 美国版 App JWT 自定义声明(用于与 Web 管理端 Token 区分能力)
  5 +/// </summary>
  6 +public static class UsAppJwtClaims
  7 +{
  8 + /// <summary>声明类型:客户端种类</summary>
  9 + public const string ClientKind = "client_kind";
  10 +
  11 + /// <summary>美国版移动端 App</summary>
  12 + public const string ClientKindUsApp = "us-app";
  13 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/FoodLabeling.Application.csproj
... ... @@ -3,6 +3,7 @@
3 3  
4 4 <ItemGroup>
5 5 <PackageReference Include="Lazy.Captcha.Core" Version="2.0.7" />
  6 + <PackageReference Include="QuestPDF" Version="2024.12.2" />
6 7 </ItemGroup>
7 8  
8 9 <ItemGroup>
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/CategoryAppearanceStorageHelper.cs 0 → 100644
  1 +using System.Text.Json;
  2 +using Volo.Abp;
  3 +
  4 +namespace FoodLabeling.Application.Helpers;
  5 +
  6 +/// <summary>
  7 +/// 将标签/产品类别的按钮外观与展示字段按「JSON 字符串」落库;兼容历史单行 TEXT/COLOR/IMAGE。
  8 +/// </summary>
  9 +public static class CategoryAppearanceStorageHelper
  10 +{
  11 + /// <summary>未传按钮外观时的默认 JSON(与前端数组语义一致)。</summary>
  12 + public const string DefaultButtonAppearanceJson = """["TEXT"]""";
  13 +
  14 + /// <summary>
  15 + /// 规范化 <see cref="FoodLabeling.Application.Services.DbModels.FlLabelCategoryDbEntity.ButtonAppearance"/> /
  16 + /// 产品类别同名字段:落库为合法 JSON 文本,不做整串 ToUpper(避免破坏 JSON)。
  17 + /// </summary>
  18 + public static string NormalizeButtonAppearanceForStorage(string? raw)
  19 + {
  20 + if (string.IsNullOrWhiteSpace(raw))
  21 + {
  22 + return DefaultButtonAppearanceJson;
  23 + }
  24 +
  25 + var t = raw.Trim();
  26 + var legacy = t.ToUpperInvariant();
  27 + if (legacy is "TEXT" or "COLOR" or "IMAGE")
  28 + {
  29 + return JsonSerializer.Serialize(new[] { legacy });
  30 + }
  31 +
  32 + try
  33 + {
  34 + using var _ = JsonDocument.Parse(t);
  35 + return t;
  36 + }
  37 + catch (JsonException)
  38 + {
  39 + throw new UserFriendlyException("按钮外观格式不正确,须为合法 JSON(或兼容旧的 TEXT/COLOR/IMAGE)");
  40 + }
  41 + }
  42 +
  43 + /// <summary>
  44 + /// 规范化 <see cref="FoodLabeling.Application.Services.DbModels.FlLabelCategoryDbEntity.CategoryPhotoUrl"/> /
  45 + /// 产品类别同名字段:已是 JSON 则原样落库;否则将整段文本序列化为 JSON 字符串(兼容历史单行色值/URL)。
  46 + /// </summary>
  47 + public static string? NormalizeCategoryPhotoUrlForStorage(string? raw)
  48 + {
  49 + if (string.IsNullOrWhiteSpace(raw))
  50 + {
  51 + return null;
  52 + }
  53 +
  54 + var t = raw.Trim();
  55 + try
  56 + {
  57 + using var _ = JsonDocument.Parse(t);
  58 + return t;
  59 + }
  60 + catch (JsonException)
  61 + {
  62 + return JsonSerializer.Serialize(t);
  63 + }
  64 + }
  65 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsRoleHelper.cs 0 → 100644
  1 +using Volo.Abp.Users;
  2 +
  3 +namespace FoodLabeling.Application.Helpers;
  4 +
  5 +/// <summary>
  6 +/// Reports 模块角色判断(与 JWT / CurrentUser.Roles 中的角色码一致)
  7 +/// </summary>
  8 +public static class ReportsRoleHelper
  9 +{
  10 + /// <summary>
  11 + /// 是否为管理员:任一角色码等于 <c>admin</c>(忽略大小写)则视为可查看全部打印数据。
  12 + /// </summary>
  13 + public static bool IsAdminRole(ICurrentUser currentUser)
  14 + {
  15 + if (currentUser.Roles is null)
  16 + {
  17 + return false;
  18 + }
  19 +
  20 + foreach (var r in currentUser.Roles)
  21 + {
  22 + if (!string.IsNullOrWhiteSpace(r) &&
  23 + string.Equals(r.Trim(), "admin", StringComparison.OrdinalIgnoreCase))
  24 + {
  25 + return true;
  26 + }
  27 + }
  28 +
  29 + return false;
  30 + }
  31 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/AuthSessionAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts.Dtos.AuthSession;
  2 +using FoodLabeling.Application.Contracts.IServices;
  3 +using Microsoft.AspNetCore.Authorization;
  4 +using Microsoft.AspNetCore.Mvc;
  5 +using Volo.Abp;
  6 +using Volo.Abp.Application.Services;
  7 +using Volo.Abp.Caching;
  8 +using FoodLabeling.Application.Services.DbModels;
  9 +using Yi.Framework.Rbac.Domain.Entities;
  10 +using Yi.Framework.Rbac.Domain.Shared.Caches;
  11 +using Yi.Framework.Rbac.Domain.Shared.Consts;
  12 +using Yi.Framework.SqlSugarCore.Abstractions;
  13 +
  14 +namespace FoodLabeling.Application.Services;
  15 +
  16 +/// <summary>
  17 +/// 当前登录会话:菜单权限与退出
  18 +/// </summary>
  19 +[Authorize]
  20 +public class AuthSessionAppService : ApplicationService, IAuthSessionAppService
  21 +{
  22 + private readonly IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> _userCache;
  23 + private readonly ISqlSugarDbContext _dbContext;
  24 + private readonly ISqlSugarRepository<UserAggregateRoot, Guid> _userRepository;
  25 +
  26 + public AuthSessionAppService(
  27 + ISqlSugarDbContext dbContext,
  28 + ISqlSugarRepository<UserAggregateRoot, Guid> userRepository,
  29 + IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> userCache)
  30 + {
  31 + _dbContext = dbContext;
  32 + _userRepository = userRepository;
  33 + _userCache = userCache;
  34 + }
  35 +
  36 + /// <inheritdoc />
  37 + public virtual async Task<CurrentUserMenuPermissionsOutputDto> GetMyMenusAsync()
  38 + {
  39 + if (!CurrentUser.Id.HasValue)
  40 + {
  41 + throw new UserFriendlyException("用户未登录");
  42 + }
  43 +
  44 + // 避免走 UserManager.GetInfoAsync -> UserRepository.GetUserAllInfoAsync 的导航加载
  45 + // 这里直接按 UserRole/RoleMenu/Menu 表关联查询当前用户可见菜单与权限码
  46 + var userId = CurrentUser.Id.Value;
  47 + var user = await _userRepository.GetByIdAsync(userId);
  48 + if (user is null || user.IsDeleted)
  49 + {
  50 + throw new UserFriendlyException("用户不存在");
  51 + }
  52 +
  53 + List<MenuDbEntity> menus;
  54 + if (UserConst.Admin.Equals(user.UserName))
  55 + {
  56 + // MenuAggregateRoot(ParentId 为 Guid) 无法兼容 menu.ParentId=0/字符串:这里统一用 MenuDbEntity
  57 + menus = await _dbContext.SqlSugarClient.Queryable<MenuDbEntity>()
  58 + .Where(x => x.IsDeleted == false)
  59 + .ToListAsync();
  60 + }
  61 + else
  62 + {
  63 + var roleIds = await _dbContext.SqlSugarClient.Queryable<UserRoleEntity>()
  64 + .Where(x => x.UserId == userId)
  65 + .Select(x => x.RoleId)
  66 + .ToListAsync();
  67 +
  68 + var roleIdStrs = roleIds.Select(x => x.ToString()).Distinct().ToList();
  69 + if (roleIdStrs.Count == 0)
  70 + {
  71 + menus = new List<MenuDbEntity>();
  72 + }
  73 + else
  74 + {
  75 + var menuIds = await _dbContext.SqlSugarClient.Queryable<RoleMenuDbEntity>()
  76 + .Where(x => roleIdStrs.Contains(x.RoleId))
  77 + .Select(x => x.MenuId)
  78 + .Distinct()
  79 + .ToListAsync();
  80 +
  81 + menus = menuIds.Count == 0
  82 + ? new List<MenuDbEntity>()
  83 + : await _dbContext.SqlSugarClient.Queryable<MenuDbEntity>()
  84 + .Where(x => x.IsDeleted == false && menuIds.Contains(x.Id))
  85 + .ToListAsync();
  86 + }
  87 + }
  88 +
  89 + var menuNodes = menus
  90 + .Select(MapToNode)
  91 + .OrderByDescending(x => x.OrderNum)
  92 + .ThenBy(x => x.MenuName)
  93 + .ToList();
  94 +
  95 + // 注意:查询 RoleAggregateRoot 会触发 YiRbacDbContext 的 IDataPermission 过滤,
  96 + // 其表达式包含 roleInfo.Select(...).Contains(...),在当前 SqlSugar 版本下会报“不支持 Select”。
  97 + // 这里直接使用 JWT 中的角色码(CurrentUser.Roles)返回,避免触发过滤器。
  98 + var roleCodes = CurrentUser.Roles?.ToList() ?? new List<string>();
  99 +
  100 + var permissionCodes = menuNodes
  101 + .Where(x => !string.IsNullOrWhiteSpace(x.PermissionCode))
  102 + .Select(x => x.PermissionCode!.Trim())
  103 + .Distinct()
  104 + .OrderBy(x => x)
  105 + .ToList();
  106 +
  107 + return new CurrentUserMenuPermissionsOutputDto
  108 + {
  109 + User = new CurrentUserBriefDto
  110 + {
  111 + Id = user.Id,
  112 + UserName = user.UserName,
  113 + Nick = user.Nick,
  114 + Email = user.Email,
  115 + Icon = user.Icon
  116 + },
  117 + RoleCodes = roleCodes.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().OrderBy(x => x).ToList(),
  118 + PermissionCodes = permissionCodes,
  119 + Menus = BuildMenuTree(menuNodes)
  120 + };
  121 + }
  122 +
  123 + /// <inheritdoc />
  124 + [HttpPost]
  125 + public virtual async Task<bool> LogoutAsync()
  126 + {
  127 + if (!CurrentUser.Id.HasValue)
  128 + {
  129 + return false;
  130 + }
  131 +
  132 + await _userCache.RemoveAsync(new UserInfoCacheKey(CurrentUser.Id.Value));
  133 + return true;
  134 + }
  135 +
  136 + private static List<CurrentUserMenuNodeDto> BuildMenuTree(List<CurrentUserMenuNodeDto> flat)
  137 + {
  138 + var nodes = flat
  139 + .GroupBy(x => x.Id)
  140 + .Select(g => g.First())
  141 + .ToList();
  142 + var byId = nodes.ToDictionary(n => n.Id, n => n);
  143 +
  144 + foreach (var n in nodes)
  145 + {
  146 + n.Children = new List<CurrentUserMenuNodeDto>();
  147 + }
  148 +
  149 + var roots = new List<CurrentUserMenuNodeDto>();
  150 + foreach (var n in nodes)
  151 + {
  152 + var pid = string.IsNullOrWhiteSpace(n.ParentId) ? "0" : n.ParentId.Trim();
  153 + if (pid == "0" || pid == "00000000-0000-0000-0000-000000000000")
  154 + {
  155 + roots.Add(n);
  156 + continue;
  157 + }
  158 +
  159 + if (byId.TryGetValue(pid, out var parent))
  160 + {
  161 + parent.Children.Add(n);
  162 + }
  163 + else
  164 + {
  165 + roots.Add(n);
  166 + }
  167 + }
  168 +
  169 + SortMenuTree(roots);
  170 + return roots;
  171 + }
  172 +
  173 + private static CurrentUserMenuNodeDto MapToNode(MenuDbEntity m)
  174 + {
  175 + return new CurrentUserMenuNodeDto
  176 + {
  177 + Id = m.Id,
  178 + ParentId = string.IsNullOrWhiteSpace(m.ParentId) ? "0" : m.ParentId.Trim(),
  179 + MenuName = m.MenuName ?? string.Empty,
  180 + RouterName = m.RouterName,
  181 + Router = m.Router,
  182 + PermissionCode = m.PermissionCode,
  183 + MenuType = m.MenuType,
  184 + MenuSource = m.MenuSource,
  185 + OrderNum = m.OrderNum,
  186 + State = m.State,
  187 + MenuIcon = m.MenuIcon,
  188 + Component = m.Component,
  189 + IsLink = m.IsLink,
  190 + IsCache = m.IsCache,
  191 + IsShow = m.IsShow,
  192 + Query = m.Query,
  193 + Remark = m.Remark
  194 + };
  195 + }
  196 +
  197 + private static void SortMenuTree(List<CurrentUserMenuNodeDto> level)
  198 + {
  199 + level.Sort((a, b) =>
  200 + {
  201 + var o = b.OrderNum.CompareTo(a.OrderNum);
  202 + return o != 0 ? o : string.Compare(a.MenuName, b.MenuName, StringComparison.Ordinal);
  203 + });
  204 +
  205 + foreach (var n in level)
  206 + {
  207 + if (n.Children.Count > 0)
  208 + {
  209 + SortMenuTree(n.Children);
  210 + }
  211 + }
  212 + }
  213 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DashboardAppService.cs 0 → 100644
  1 +using System.Globalization;
  2 +using System.Text.Json;
  3 +using FoodLabeling.Application.Contracts.Dtos.Dashboard;
  4 +using FoodLabeling.Application.Contracts.IServices;
  5 +using FoodLabeling.Application.Services.DbModels;
  6 +using FoodLabeling.Domain.Entities;
  7 +using SqlSugar;
  8 +using Volo.Abp.Application.Services;
  9 +using Yi.Framework.Rbac.Domain.Entities;
  10 +using Yi.Framework.SqlSugarCore.Abstractions;
  11 +
  12 +namespace FoodLabeling.Application.Services;
  13 +
  14 +/// <summary>
  15 +/// Dashboard 统计服务(美国版)
  16 +/// </summary>
  17 +public class DashboardAppService : ApplicationService, IDashboardAppService
  18 +{
  19 + private readonly ISqlSugarDbContext _dbContext;
  20 + private readonly ISqlSugarRepository<LocationAggregateRoot, Guid> _locationRepository;
  21 + private readonly ISqlSugarRepository<UserAggregateRoot, Guid> _userRepository;
  22 +
  23 + public DashboardAppService(
  24 + ISqlSugarDbContext dbContext,
  25 + ISqlSugarRepository<LocationAggregateRoot, Guid> locationRepository,
  26 + ISqlSugarRepository<UserAggregateRoot, Guid> userRepository)
  27 + {
  28 + _dbContext = dbContext;
  29 + _locationRepository = locationRepository;
  30 + _userRepository = userRepository;
  31 + }
  32 +
  33 + /// <inheritdoc />
  34 + public async Task<DashboardOverviewOutputDto> GetOverviewAsync()
  35 + {
  36 + var now = DateTime.Now;
  37 + var todayStart = now.Date;
  38 + var tomorrowStart = todayStart.AddDays(1);
  39 + var yesterdayStart = todayStart.AddDays(-1);
  40 + var weekStart = todayStart.AddDays(-6);
  41 + var prevWeekStart = todayStart.AddDays(-13);
  42 +
  43 + var printedToday = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>()
  44 + .CountAsync(x => x.CreationTime >= todayStart && x.CreationTime < tomorrowStart);
  45 +
  46 + var printedYesterday = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>()
  47 + .CountAsync(x => x.CreationTime >= yesterdayStart && x.CreationTime < todayStart);
  48 +
  49 + var activeTemplates = await _dbContext.SqlSugarClient.Queryable<FlLabelTemplateDbEntity>()
  50 + .CountAsync(x => !x.IsDeleted && x.State);
  51 + var activeTemplatesPrevWeek = await _dbContext.SqlSugarClient.Queryable<FlLabelTemplateDbEntity>()
  52 + .CountAsync(x => !x.IsDeleted && x.State && x.CreationTime < weekStart);
  53 +
  54 + var activeUsers = await _userRepository._DbQueryable
  55 + .CountAsync(x => !x.IsDeleted && x.State);
  56 + var activeUsersPrevWeek = await _userRepository._DbQueryable
  57 + .CountAsync(x => !x.IsDeleted && x.State && x.CreationTime < weekStart);
  58 +
  59 + var locations = await _locationRepository._DbQueryable
  60 + .CountAsync(x => !x.IsDeleted);
  61 + var locationsPrevWeek = await _locationRepository._DbQueryable
  62 + .CountAsync(x => !x.IsDeleted && x.CreationTime < weekStart);
  63 +
  64 + var people = await _userRepository._DbQueryable
  65 + .CountAsync(x => !x.IsDeleted);
  66 + var peoplePrevWeek = await _userRepository._DbQueryable
  67 + .CountAsync(x => !x.IsDeleted && x.CreationTime < weekStart);
  68 +
  69 + var products = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>()
  70 + .CountAsync(x => !x.IsDeleted);
  71 + // fl_product 当前实体未映射 CreationTime,无法按时间切分对比,先回退为同口径总量对比
  72 + var productsPrevWeek = products;
  73 +
  74 + var weeklyPrintRaw = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>()
  75 + .Where(x => x.CreationTime >= weekStart && x.CreationTime < tomorrowStart)
  76 + .Select(x => x.CreationTime)
  77 + .ToListAsync();
  78 +
  79 + var weeklyDict = weeklyPrintRaw
  80 + .GroupBy(x => x.Date)
  81 + .ToDictionary(g => g.Key, g => g.Count());
  82 +
  83 + var weeklyTrend = Enumerable.Range(0, 7)
  84 + .Select(i =>
  85 + {
  86 + var d = weekStart.AddDays(i).Date;
  87 + return new DashboardDailyTrendPointDto
  88 + {
  89 + Date = d.ToString("yyyy-MM-dd"),
  90 + Value = weeklyDict.TryGetValue(d, out var v) ? v : 0
  91 + };
  92 + })
  93 + .ToList();
  94 +
  95 + var categories = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
  96 + .Where(x => !x.IsDeleted && x.State)
  97 + .ToListAsync();
  98 +
  99 + var labelCategoryIds = categories.Select(x => x.Id).ToList();
  100 + var labelRows = labelCategoryIds.Count == 0
  101 + ? new List<string?>()
  102 + : await _dbContext.SqlSugarClient.Queryable<FlLabelDbEntity>()
  103 + .Where(x => !x.IsDeleted && x.LabelCategoryId != null && labelCategoryIds.Contains(x.LabelCategoryId))
  104 + .Select(x => x.LabelCategoryId)
  105 + .ToListAsync();
  106 +
  107 + var labelCountByCategory = labelRows
  108 + .Where(x => !string.IsNullOrWhiteSpace(x))
  109 + .GroupBy(x => x!)
  110 + .ToDictionary(g => g.Key, g => g.Count());
  111 +
  112 + var categoryDistributionTotal = labelCountByCategory.Values.Sum();
  113 + var categoryDistribution = categories
  114 + .Select(c =>
  115 + {
  116 + var count = labelCountByCategory.TryGetValue(c.Id, out var v) ? v : 0;
  117 + var ratio = categoryDistributionTotal == 0
  118 + ? 0m
  119 + : Math.Round(count * 100m / categoryDistributionTotal, 2);
  120 + return new DashboardCategoryDistributionDto
  121 + {
  122 + CategoryId = c.Id,
  123 + CategoryName = c.CategoryName,
  124 + Count = count,
  125 + Ratio = ratio
  126 + };
  127 + })
  128 + .Where(x => x.Count > 0)
  129 + .OrderByDescending(x => x.Count)
  130 + .ThenBy(x => x.CategoryName)
  131 + .ToList();
  132 +
  133 + const int recentLabelsTake = 10;
  134 + var recentRaw = await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>()
  135 + .LeftJoin<FlLabelDbEntity>((t, l) => t.LabelId == l.Id)
  136 + .LeftJoin<FlProductDbEntity>((t, l, p) => t.ProductId == p.Id)
  137 + .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, tpl) => t.TemplateId == tpl.Id)
  138 + .OrderBy((t, l, p, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc)
  139 + .Take(recentLabelsTake)
  140 + .Select((t, l, p, tpl) => new
  141 + {
  142 + t.Id,
  143 + LabelCode = l.LabelCode,
  144 + LabelName = l.LabelName,
  145 + ProductName = p.ProductName,
  146 + tpl.Width,
  147 + tpl.Height,
  148 + tpl.Unit,
  149 + t.PrintInputJson,
  150 + PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime),
  151 + t.CreatedBy
  152 + })
  153 + .ToListAsync();
  154 +
  155 + var recentUserIds = recentRaw
  156 + .Select(x => x.CreatedBy)
  157 + .Where(x => !string.IsNullOrWhiteSpace(x))
  158 + .Select(x => x!.Trim())
  159 + .Distinct()
  160 + .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null)
  161 + .Where(x => x.HasValue)
  162 + .Select(x => x!.Value)
  163 + .ToList();
  164 +
  165 + var recentUserMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  166 + if (recentUserIds.Count > 0)
  167 + {
  168 + var users = await _userRepository._DbQueryable
  169 + .Where(u => !u.IsDeleted && recentUserIds.Contains(u.Id))
  170 + .Select(u => new { u.Id, u.Name, u.UserName })
  171 + .ToListAsync();
  172 + foreach (var u in users)
  173 + {
  174 + var display = !string.IsNullOrWhiteSpace(u.Name) ? u.Name.Trim() : u.UserName.Trim();
  175 + recentUserMap[u.Id.ToString()] = string.IsNullOrWhiteSpace(display) ? "无" : display;
  176 + }
  177 + }
  178 +
  179 + var recentLabels = recentRaw.Select(x =>
  180 + {
  181 + var displayName = !string.IsNullOrWhiteSpace(x.ProductName)
  182 + ? x.ProductName.Trim()
  183 + : (string.IsNullOrWhiteSpace(x.LabelName) ? "无" : x.LabelName.Trim());
  184 + var printedAt = x.PrintedAt ?? DateTime.MinValue;
  185 + var status = ResolveRecentLabelStatus(x.PrintInputJson);
  186 + return new DashboardRecentLabelItemDto
  187 + {
  188 + TaskId = x.Id,
  189 + LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(),
  190 + DisplayName = displayName,
  191 + PrintedByUserId = x.CreatedBy?.Trim(),
  192 + PrintedByName = ResolveRecentUserName(recentUserMap, x.CreatedBy),
  193 + PrintedAt = printedAt,
  194 + Status = status,
  195 + LabelTypeBadge = FormatTemplateBadge(x.Width, x.Height, x.Unit)
  196 + };
  197 + }).ToList();
  198 +
  199 + var labelsPrintedTodayCard = BuildMetricCard("labelsPrintedToday", "Labels Printed Today", printedToday, printedYesterday);
  200 + var activeTemplatesCard = BuildMetricCard("activeTemplates", "Active Templates", activeTemplates, activeTemplatesPrevWeek);
  201 + var activeUsersCard = BuildMetricCard("activeUsers", "Active Users", activeUsers, activeUsersPrevWeek);
  202 + var locationsCard = BuildMetricCard("locations", "Locations", locations, locationsPrevWeek);
  203 + var peopleCard = BuildMetricCard("people", "People", people, peoplePrevWeek);
  204 + var productsCard = BuildMetricCard("products", "Products", products, productsPrevWeek);
  205 +
  206 + var output = new DashboardOverviewOutputDto
  207 + {
  208 + LabelsPrintedToday = labelsPrintedTodayCard,
  209 + ActiveTemplates = activeTemplatesCard,
  210 + ActiveUsers = activeUsersCard,
  211 + Locations = locationsCard,
  212 + People = peopleCard,
  213 + Products = productsCard,
  214 + MetricCards = new List<DashboardMetricCardDto>
  215 + {
  216 + labelsPrintedTodayCard,
  217 + activeTemplatesCard,
  218 + activeUsersCard,
  219 + locationsCard,
  220 + peopleCard,
  221 + productsCard
  222 + },
  223 + WeeklyPrintVolume = weeklyTrend,
  224 + CategoryDistribution = categoryDistribution,
  225 + CategoryDistributionTotal = categoryDistributionTotal,
  226 + ByCategory = categoryDistribution,
  227 + ByCategoryTotal = categoryDistributionTotal,
  228 + RecentLabels = recentLabels,
  229 + GeneratedAt = now
  230 + };
  231 +
  232 + return output;
  233 + }
  234 +
  235 + private static string ResolveRecentUserName(Dictionary<string, string> map, string? createdBy)
  236 + {
  237 + if (string.IsNullOrWhiteSpace(createdBy))
  238 + {
  239 + return "无";
  240 + }
  241 +
  242 + return map.TryGetValue(createdBy.Trim(), out var n) ? n : "无";
  243 + }
  244 +
  245 + /// <summary>
  246 + /// 依据 PrintInputJson 中的保质期字段与「当前日期」比较得到 active/expired。
  247 + /// </summary>
  248 + private static string ResolveRecentLabelStatus(string? printInputJson)
  249 + {
  250 + if (!TryParseExpiryDate(printInputJson, out var expiryDate))
  251 + {
  252 + return "active";
  253 + }
  254 +
  255 + var today = DateTime.Now.Date;
  256 + return expiryDate.Date < today ? "expired" : "active";
  257 + }
  258 +
  259 + private static bool TryParseExpiryDate(string? printInputJson, out DateTime expiryDate)
  260 + {
  261 + expiryDate = default;
  262 + if (string.IsNullOrWhiteSpace(printInputJson))
  263 + {
  264 + return false;
  265 + }
  266 +
  267 + try
  268 + {
  269 + using var doc = JsonDocument.Parse(printInputJson);
  270 + if (doc.RootElement.ValueKind != JsonValueKind.Object)
  271 + {
  272 + return false;
  273 + }
  274 +
  275 + foreach (var prop in doc.RootElement.EnumerateObject())
  276 + {
  277 + var key = prop.Name.Trim();
  278 + if (!key.Equals("expiryDate", StringComparison.OrdinalIgnoreCase) &&
  279 + !key.Equals("expiry", StringComparison.OrdinalIgnoreCase) &&
  280 + !key.Equals("expirationDate", StringComparison.OrdinalIgnoreCase))
  281 + {
  282 + continue;
  283 + }
  284 +
  285 + var v = prop.Value;
  286 + if (v.ValueKind == JsonValueKind.String)
  287 + {
  288 + var s = v.GetString();
  289 + if (!string.IsNullOrWhiteSpace(s) &&
  290 + DateTime.TryParse(s, CultureInfo.InvariantCulture,
  291 + DateTimeStyles.AssumeLocal | DateTimeStyles.AllowWhiteSpaces, out var dt))
  292 + {
  293 + expiryDate = dt;
  294 + return true;
  295 + }
  296 + }
  297 + else if (v.ValueKind == JsonValueKind.Number && v.TryGetInt64(out var unix))
  298 + {
  299 + expiryDate = DateTimeOffset.FromUnixTimeSeconds(unix).LocalDateTime;
  300 + return true;
  301 + }
  302 + }
  303 + }
  304 + catch
  305 + {
  306 + return false;
  307 + }
  308 +
  309 + return false;
  310 + }
  311 +
  312 + private static string FormatTemplateBadge(decimal w, decimal h, string? unit)
  313 + {
  314 + var u = (unit ?? "inch").Trim().ToLowerInvariant();
  315 + var ws = w.ToString(CultureInfo.InvariantCulture);
  316 + var hs = h.ToString(CultureInfo.InvariantCulture);
  317 + return u is "inch" or "in"
  318 + ? $"{ws}\"x{hs}\""
  319 + : $"{ws}x{hs}{u}";
  320 + }
  321 +
  322 + private static DashboardMetricCardDto BuildMetricCard(string key, string title, int value, int previousValue)
  323 + {
  324 + var changeValue = value - previousValue;
  325 + var changeRate = previousValue <= 0
  326 + ? (value > 0 ? 100m : 0m)
  327 + : Math.Round(changeValue * 100m / previousValue, 2);
  328 +
  329 + return new DashboardMetricCardDto
  330 + {
  331 + Key = key,
  332 + Title = title,
  333 + Value = value,
  334 + PreviousValue = previousValue,
  335 + ChangeValue = changeValue,
  336 + ChangeRate = changeRate
  337 + };
  338 + }
  339 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlGroupDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +/// <summary>
  6 +/// 组织/分组(Account Management / Group,表 fl_group)
  7 +/// </summary>
  8 +[SugarTable("fl_group")]
  9 +public class FlGroupDbEntity
  10 +{
  11 + [SugarColumn(IsPrimaryKey = true)]
  12 + public string Id { get; set; } = string.Empty;
  13 +
  14 + public bool IsDeleted { get; set; }
  15 +
  16 + public DateTime CreationTime { get; set; }
  17 +
  18 + public string? CreatorId { get; set; }
  19 +
  20 + public string? LastModifierId { get; set; }
  21 +
  22 + public DateTime? LastModificationTime { get; set; }
  23 +
  24 + /// <summary>
  25 + /// 组织名称(Group Name)
  26 + /// </summary>
  27 + public string GroupName { get; set; } = string.Empty;
  28 +
  29 + /// <summary>
  30 + /// 所属合作伙伴 Id(fl_partner.Id)
  31 + /// </summary>
  32 + public string PartnerId { get; set; } = string.Empty;
  33 +
  34 + /// <summary>
  35 + /// 是否启用(对应 UI Active)
  36 + /// </summary>
  37 + public bool State { get; set; }
  38 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryDbEntity.cs
... ... @@ -24,10 +24,28 @@ public class FlLabelCategoryDbEntity
24 24  
25 25 public string CategoryName { get; set; } = string.Empty;
26 26  
  27 + /// <summary>
  28 + /// 按钮展示文案(为空则默认使用 CategoryName)
  29 + /// </summary>
  30 + public string? DisplayText { get; set; }
  31 +
  32 + /// <summary>
  33 + /// 分类图/展示值:TEXT 可为图或空;COLOR 存色值(如 #409EFF);IMAGE 存图片 URL(与 ButtonAppearance 配合)
  34 + /// </summary>
27 35 public string? CategoryPhotoUrl { get; set; }
28 36  
29 37 public int OrderNum { get; set; }
30 38  
31 39 public bool State { get; set; }
  40 +
  41 + /// <summary>
  42 + /// 按钮外观:TEXT / COLOR / IMAGE(展示数据见 CategoryPhotoUrl)
  43 + /// </summary>
  44 + public string ButtonAppearance { get; set; } = "TEXT";
  45 +
  46 + /// <summary>
  47 + /// 门店可用范围:ALL / SPECIFIED
  48 + /// </summary>
  49 + public string AvailabilityType { get; set; } = "ALL";
32 50 }
33 51  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryLocationDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +/// <summary>
  6 +/// 标签分类可用门店关联(对应表:fl_label_category_location)
  7 +/// </summary>
  8 +[SugarTable("fl_label_category_location")]
  9 +public class FlLabelCategoryLocationDbEntity
  10 +{
  11 + [SugarColumn(IsPrimaryKey = true)]
  12 + public string Id { get; set; } = string.Empty;
  13 +
  14 + public string CategoryId { get; set; } = string.Empty;
  15 +
  16 + public string LocationId { get; set; } = string.Empty;
  17 +
  18 + public DateTime CreationTime { get; set; }
  19 +
  20 + public string? CreatorId { get; set; }
  21 +}
  22 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelDbEntity.cs
... ... @@ -20,7 +20,7 @@ public class FlLabelDbEntity
20 20  
21 21 public string ConcurrencyStamp { get; set; } = string.Empty;
22 22  
23   - public string LabelCode { get; set; } = string.Empty;
  23 + public string? LabelCode { get; set; }
24 24  
25 25 public string LabelName { get; set; } = string.Empty;
26 26  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLocationSupportDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +/// <summary>
  6 +/// 门店 Support 联系方式(每个门店仅一条,对 App Support 页展示)
  7 +/// </summary>
  8 +[SugarTable("fl_location_support")]
  9 +public class FlLocationSupportDbEntity
  10 +{
  11 + [SugarColumn(IsPrimaryKey = true)]
  12 + public string Id { get; set; } = string.Empty;
  13 +
  14 + public bool IsDeleted { get; set; }
  15 +
  16 + public DateTime CreationTime { get; set; }
  17 +
  18 + public string? CreatorId { get; set; }
  19 +
  20 + public string? LastModifierId { get; set; }
  21 +
  22 + public DateTime? LastModificationTime { get; set; }
  23 +
  24 + public string SupportPhone { get; set; } = string.Empty;
  25 +
  26 + public string SupportEmail { get; set; } = string.Empty;
  27 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlPartnerDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +/// <summary>
  6 +/// 合作伙伴主数据(Account Management / Partner,表 fl_partner)
  7 +/// </summary>
  8 +[SugarTable("fl_partner")]
  9 +public class FlPartnerDbEntity
  10 +{
  11 + [SugarColumn(IsPrimaryKey = true)]
  12 + public string Id { get; set; } = string.Empty;
  13 +
  14 + public bool IsDeleted { get; set; }
  15 +
  16 + public DateTime CreationTime { get; set; }
  17 +
  18 + public string? CreatorId { get; set; }
  19 +
  20 + public string? LastModifierId { get; set; }
  21 +
  22 + public DateTime? LastModificationTime { get; set; }
  23 +
  24 + /// <summary>
  25 + /// 合作伙伴名称(公司名)
  26 + /// </summary>
  27 + public string PartnerName { get; set; } = string.Empty;
  28 +
  29 + /// <summary>
  30 + /// 联系邮箱
  31 + /// </summary>
  32 + public string? ContactEmail { get; set; }
  33 +
  34 + /// <summary>
  35 + /// 电话
  36 + /// </summary>
  37 + public string? PhoneNumber { get; set; }
  38 +
  39 + /// <summary>
  40 + /// 是否启用(对应 UI Active)
  41 + /// </summary>
  42 + public bool State { get; set; }
  43 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryDbEntity.cs
... ... @@ -24,10 +24,28 @@ public class FlProductCategoryDbEntity
24 24  
25 25 public string CategoryName { get; set; } = string.Empty;
26 26  
  27 + /// <summary>
  28 + /// 按钮展示文案(为空则默认使用 CategoryName)
  29 + /// </summary>
  30 + public string? DisplayText { get; set; }
  31 +
  32 + /// <summary>
  33 + /// 分类图/展示值:TEXT 可为图或空;COLOR 存色值;IMAGE 存图片 URL(与 ButtonAppearance 配合)
  34 + /// </summary>
27 35 public string? CategoryPhotoUrl { get; set; }
28 36  
  37 + /// <summary>
  38 + /// 按钮外观:TEXT / COLOR / IMAGE(展示数据见 CategoryPhotoUrl)
  39 + /// </summary>
  40 + public string ButtonAppearance { get; set; } = "TEXT";
  41 +
29 42 public bool State { get; set; }
30 43  
  44 + /// <summary>
  45 + /// 门店可用范围:ALL / SPECIFIED
  46 + /// </summary>
  47 + public string AvailabilityType { get; set; } = "ALL";
  48 +
31 49 public int OrderNum { get; set; }
32 50 }
33 51  
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlProductCategoryLocationDbEntity.cs 0 → 100644
  1 +using SqlSugar;
  2 +
  3 +namespace FoodLabeling.Application.Services.DbModels;
  4 +
  5 +/// <summary>
  6 +/// 产品类别可用门店关联(对应表:fl_product_category_location)
  7 +/// </summary>
  8 +[SugarTable("fl_product_category_location")]
  9 +public class FlProductCategoryLocationDbEntity
  10 +{
  11 + [SugarColumn(IsPrimaryKey = true)]
  12 + public string Id { get; set; } = string.Empty;
  13 +
  14 + public string CategoryId { get; set; } = string.Empty;
  15 +
  16 + public string LocationId { get; set; } = string.Empty;
  17 +
  18 + public DateTime CreationTime { get; set; }
  19 +
  20 + public string? CreatorId { get; set; }
  21 +}
  22 +
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/GroupAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Helpers;
  2 +using FoodLabeling.Application.Contracts.Dtos.Common;
  3 +using FoodLabeling.Application.Contracts.Dtos.Group;
  4 +using FoodLabeling.Application.Contracts.IServices;
  5 +using FoodLabeling.Application.Services.DbModels;
  6 +using Microsoft.AspNetCore.Mvc;
  7 +using QuestPDF.Fluent;
  8 +using QuestPDF.Helpers;
  9 +using QuestPDF.Infrastructure;
  10 +using SqlSugar;
  11 +using Volo.Abp;
  12 +using Volo.Abp.Application.Services;
  13 +using Volo.Abp.Guids;
  14 +using Volo.Abp.Uow;
  15 +using Yi.Framework.SqlSugarCore.Abstractions;
  16 +
  17 +namespace FoodLabeling.Application.Services;
  18 +
  19 +/// <summary>
  20 +/// 组织(Group)管理(fl_group)
  21 +/// </summary>
  22 +public class GroupAppService : ApplicationService, IGroupAppService
  23 +{
  24 + private const int ExportPdfMaxRows = 5000;
  25 +
  26 + private readonly ISqlSugarDbContext _dbContext;
  27 + private readonly IGuidGenerator _guidGenerator;
  28 +
  29 + public GroupAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator)
  30 + {
  31 + _dbContext = dbContext;
  32 + _guidGenerator = guidGenerator;
  33 + }
  34 +
  35 + /// <inheritdoc />
  36 + public async Task<PagedResultWithPageDto<GroupGetListOutputDto>> GetListAsync(GroupGetListInputVo input)
  37 + {
  38 + RefAsync<int> total = 0;
  39 + var query = BuildGroupJoinedQuery(input);
  40 + var projected = query.Select((g, p) => new GroupGetListOutputDto
  41 + {
  42 + Id = g.Id,
  43 + GroupName = g.GroupName,
  44 + PartnerId = g.PartnerId,
  45 + PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "无" : p.PartnerName.Trim(),
  46 + State = g.State,
  47 + CreationTime = g.CreationTime
  48 + });
  49 +
  50 + var items = await projected.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
  51 + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items);
  52 + }
  53 +
  54 + /// <inheritdoc />
  55 + public async Task<GroupGetOutputDto> GetAsync(string id)
  56 + {
  57 + var groupId = id?.Trim();
  58 + if (string.IsNullOrWhiteSpace(groupId))
  59 + {
  60 + throw new UserFriendlyException("组织Id不能为空");
  61 + }
  62 +
  63 + var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>()
  64 + .FirstAsync(x => !x.IsDeleted && x.Id == groupId);
  65 + if (entity is null)
  66 + {
  67 + throw new UserFriendlyException("组织不存在");
  68 + }
  69 +
  70 + var partnerName = await ResolvePartnerNameAsync(entity.PartnerId);
  71 + return MapDetail(entity, partnerName);
  72 + }
  73 +
  74 + /// <inheritdoc />
  75 + [UnitOfWork]
  76 + public async Task<GroupGetOutputDto> CreateAsync(GroupCreateInputVo input)
  77 + {
  78 + var name = input.GroupName?.Trim();
  79 + if (string.IsNullOrWhiteSpace(name))
  80 + {
  81 + throw new UserFriendlyException("组织名称不能为空");
  82 + }
  83 +
  84 + var partnerId = input.PartnerId?.Trim();
  85 + if (string.IsNullOrWhiteSpace(partnerId))
  86 + {
  87 + throw new UserFriendlyException("请选择所属合作伙伴");
  88 + }
  89 +
  90 + await EnsurePartnerExistsAsync(partnerId);
  91 +
  92 + var now = Clock.Now;
  93 + var entity = new FlGroupDbEntity
  94 + {
  95 + Id = _guidGenerator.Create().ToString(),
  96 + IsDeleted = false,
  97 + GroupName = name,
  98 + PartnerId = partnerId,
  99 + State = input.State,
  100 + CreationTime = now,
  101 + CreatorId = CurrentUser?.Id?.ToString(),
  102 + LastModificationTime = now,
  103 + LastModifierId = CurrentUser?.Id?.ToString()
  104 + };
  105 +
  106 + await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  107 + return await GetAsync(entity.Id);
  108 + }
  109 +
  110 + /// <inheritdoc />
  111 + [UnitOfWork]
  112 + public async Task<GroupGetOutputDto> UpdateAsync(string id, GroupUpdateInputVo input)
  113 + {
  114 + var groupId = id?.Trim();
  115 + if (string.IsNullOrWhiteSpace(groupId))
  116 + {
  117 + throw new UserFriendlyException("组织Id不能为空");
  118 + }
  119 +
  120 + var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>()
  121 + .FirstAsync(x => !x.IsDeleted && x.Id == groupId);
  122 + if (entity is null)
  123 + {
  124 + throw new UserFriendlyException("组织不存在");
  125 + }
  126 +
  127 + var name = input.GroupName?.Trim();
  128 + if (string.IsNullOrWhiteSpace(name))
  129 + {
  130 + throw new UserFriendlyException("组织名称不能为空");
  131 + }
  132 +
  133 + var partnerId = input.PartnerId?.Trim();
  134 + if (string.IsNullOrWhiteSpace(partnerId))
  135 + {
  136 + throw new UserFriendlyException("请选择所属合作伙伴");
  137 + }
  138 +
  139 + await EnsurePartnerExistsAsync(partnerId);
  140 +
  141 + entity.GroupName = name;
  142 + entity.PartnerId = partnerId;
  143 + entity.State = input.State;
  144 + entity.LastModificationTime = Clock.Now;
  145 + entity.LastModifierId = CurrentUser?.Id?.ToString();
  146 +
  147 + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  148 + return await GetAsync(groupId);
  149 + }
  150 +
  151 + /// <inheritdoc />
  152 + [UnitOfWork]
  153 + public async Task DeleteAsync(string id)
  154 + {
  155 + var groupId = id?.Trim();
  156 + if (string.IsNullOrWhiteSpace(groupId))
  157 + {
  158 + throw new UserFriendlyException("组织Id不能为空");
  159 + }
  160 +
  161 + var entity = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>()
  162 + .FirstAsync(x => !x.IsDeleted && x.Id == groupId);
  163 + if (entity is null)
  164 + {
  165 + throw new UserFriendlyException("组织不存在");
  166 + }
  167 +
  168 + entity.IsDeleted = true;
  169 + entity.LastModificationTime = Clock.Now;
  170 + entity.LastModifierId = CurrentUser?.Id?.ToString();
  171 + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  172 + }
  173 +
  174 + /// <inheritdoc />
  175 + public async Task<IActionResult> ExportPdfAsync(GroupGetListInputVo input)
  176 + {
  177 + QuestPDF.Settings.License = LicenseType.Community;
  178 +
  179 + var exportBase = BuildGroupJoinedQuery(input);
  180 + var count = await exportBase.CountAsync();
  181 + if (count > ExportPdfMaxRows)
  182 + {
  183 + throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围");
  184 + }
  185 +
  186 + var rows = await BuildGroupJoinedQuery(input)
  187 + .Select((g, p) => new GroupGetListOutputDto
  188 + {
  189 + Id = g.Id,
  190 + GroupName = g.GroupName,
  191 + PartnerId = g.PartnerId,
  192 + PartnerName = string.IsNullOrWhiteSpace(p.PartnerName) ? "无" : p.PartnerName.Trim(),
  193 + State = g.State,
  194 + CreationTime = g.CreationTime
  195 + })
  196 + .Take(ExportPdfMaxRows)
  197 + .ToListAsync();
  198 +
  199 + var fileName = $"groups_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf";
  200 + var document = Document.Create(container =>
  201 + {
  202 + container.Page(page =>
  203 + {
  204 + page.Margin(28);
  205 + page.DefaultTextStyle(x => x.FontSize(10));
  206 + page.Header().Text("Groups").SemiBold().FontSize(18);
  207 + page.Content().PaddingTop(12).Table(table =>
  208 + {
  209 + table.ColumnsDefinition(c =>
  210 + {
  211 + c.RelativeColumn(2.2f);
  212 + c.RelativeColumn(2.4f);
  213 + c.RelativeColumn(1f);
  214 + c.RelativeColumn(1.6f);
  215 + });
  216 +
  217 + static IContainer CellHeader(IContainer c) =>
  218 + c.Background(Colors.Grey.Lighten3).Padding(6).DefaultTextStyle(x => x.SemiBold());
  219 +
  220 + table.Cell().Element(CellHeader).Text("Group Name");
  221 + table.Cell().Element(CellHeader).Text("Parent Partner");
  222 + table.Cell().Element(CellHeader).Text("Status");
  223 + table.Cell().Element(CellHeader).Text("Created");
  224 +
  225 + foreach (var e in rows)
  226 + {
  227 + var status = e.State ? "active" : "inactive";
  228 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5)
  229 + .Text(e.GroupName ?? string.Empty);
  230 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5)
  231 + .Text(string.IsNullOrWhiteSpace(e.PartnerName) ? "无" : e.PartnerName);
  232 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status);
  233 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5)
  234 + .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm"));
  235 + }
  236 + });
  237 + });
  238 + });
  239 +
  240 + var stream = new MemoryStream();
  241 + document.GeneratePdf(stream);
  242 + stream.Position = 0;
  243 + return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName };
  244 + }
  245 +
  246 + private ISugarQueryable<FlGroupDbEntity, FlPartnerDbEntity> BuildGroupJoinedQuery(GroupGetListInputVo input)
  247 + {
  248 + var keyword = input.Keyword?.Trim();
  249 + var partnerId = input.PartnerId?.Trim();
  250 +
  251 + var query = _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>()
  252 + .LeftJoin<FlPartnerDbEntity>((g, p) => g.PartnerId == p.Id && !p.IsDeleted)
  253 + .Where((g, p) => !g.IsDeleted)
  254 + .WhereIF(input.State != null, (g, p) => g.State == input.State)
  255 + .WhereIF(!string.IsNullOrWhiteSpace(partnerId), (g, p) => g.PartnerId == partnerId)
  256 + .WhereIF(!string.IsNullOrWhiteSpace(keyword),
  257 + (g, p) => g.GroupName.Contains(keyword!) ||
  258 + (p.PartnerName != null && p.PartnerName.Contains(keyword!)));
  259 +
  260 + if (!string.IsNullOrWhiteSpace(input.Sorting))
  261 + {
  262 + var sorting = input.Sorting.Trim();
  263 + if (sorting.Equals("GroupName desc", StringComparison.OrdinalIgnoreCase))
  264 + {
  265 + query = query.OrderByDescending((g, p) => g.GroupName);
  266 + }
  267 + else if (sorting.Equals("GroupName asc", StringComparison.OrdinalIgnoreCase))
  268 + {
  269 + query = query.OrderBy((g, p) => g.GroupName);
  270 + }
  271 + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase))
  272 + {
  273 + query = query.OrderByDescending((g, p) => g.CreationTime);
  274 + }
  275 + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase))
  276 + {
  277 + query = query.OrderBy((g, p) => g.CreationTime);
  278 + }
  279 + else if (sorting.Equals("State desc", StringComparison.OrdinalIgnoreCase))
  280 + {
  281 + query = query.OrderByDescending((g, p) => g.State);
  282 + }
  283 + else if (sorting.Equals("State asc", StringComparison.OrdinalIgnoreCase))
  284 + {
  285 + query = query.OrderBy((g, p) => g.State);
  286 + }
  287 + else if (sorting.Equals("PartnerName desc", StringComparison.OrdinalIgnoreCase))
  288 + {
  289 + query = query.OrderByDescending((g, p) => p.PartnerName);
  290 + }
  291 + else if (sorting.Equals("PartnerName asc", StringComparison.OrdinalIgnoreCase))
  292 + {
  293 + query = query.OrderBy((g, p) => p.PartnerName);
  294 + }
  295 + else
  296 + {
  297 + query = query.OrderByDescending((g, p) => g.CreationTime);
  298 + }
  299 + }
  300 + else
  301 + {
  302 + query = query.OrderByDescending((g, p) => g.CreationTime);
  303 + }
  304 +
  305 + return query;
  306 + }
  307 +
  308 + private async Task EnsurePartnerExistsAsync(string partnerId)
  309 + {
  310 + var ok = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
  311 + .AnyAsync(x => !x.IsDeleted && x.Id == partnerId);
  312 + if (!ok)
  313 + {
  314 + throw new UserFriendlyException("所选合作伙伴不存在或已删除");
  315 + }
  316 + }
  317 +
  318 + private async Task<string> ResolvePartnerNameAsync(string partnerId)
  319 + {
  320 + var p = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
  321 + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId);
  322 + if (p is null || string.IsNullOrWhiteSpace(p.PartnerName))
  323 + {
  324 + return "无";
  325 + }
  326 +
  327 + return p.PartnerName.Trim();
  328 + }
  329 +
  330 + private static GroupGetOutputDto MapDetail(FlGroupDbEntity x, string partnerName) => new()
  331 + {
  332 + Id = x.Id,
  333 + GroupName = x.GroupName,
  334 + PartnerId = x.PartnerId,
  335 + PartnerName = partnerName,
  336 + State = x.State,
  337 + CreationTime = x.CreationTime,
  338 + LastModificationTime = x.LastModificationTime
  339 + };
  340 +
  341 + private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total,
  342 + List<T> items)
  343 + {
  344 + var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount;
  345 + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount);
  346 + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize);
  347 + return new PagedResultWithPageDto<T>
  348 + {
  349 + PageIndex = pageIndex,
  350 + PageSize = pageSize,
  351 + TotalCount = total,
  352 + TotalPages = totalPages,
  353 + Items = items
  354 + };
  355 + }
  356 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs
... ... @@ -261,7 +261,7 @@ public class LabelAppService : ApplicationService, ILabelAppService
261 261  
262 262 return new LabelGetOutputDto
263 263 {
264   - Id = label.LabelCode,
  264 + Id = label.LabelCode ?? string.Empty,
265 265 LabelName = label.LabelName,
266 266 LocationId = label.LocationId ?? string.Empty,
267 267 LocationName = location?.LocationName ?? location?.LocationCode ?? "无",
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelCategoryAppService.cs
... ... @@ -30,12 +30,34 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
30 30 var query = _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
31 31 .Where(x => !x.IsDeleted)
32 32 .WhereIF(!string.IsNullOrWhiteSpace(keyword),
33   - x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!))
  33 + x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!) ||
  34 + (x.DisplayText != null && x.DisplayText.Contains(keyword!)))
34 35 .WhereIF(input.State != null, x => x.State == input.State);
35 36  
  37 + // Sorting 仅允许白名单字段,避免 Unknown column/注入风险
36 38 if (!string.IsNullOrWhiteSpace(input.Sorting))
37 39 {
38   - query = query.OrderBy(input.Sorting);
  40 + var sorting = input.Sorting.Trim();
  41 + if (sorting.Equals("OrderNum desc", StringComparison.OrdinalIgnoreCase))
  42 + {
  43 + query = query.OrderByDescending(x => x.OrderNum);
  44 + }
  45 + else if (sorting.Equals("OrderNum asc", StringComparison.OrdinalIgnoreCase))
  46 + {
  47 + query = query.OrderBy(x => x.OrderNum);
  48 + }
  49 + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase))
  50 + {
  51 + query = query.OrderByDescending(x => x.CreationTime);
  52 + }
  53 + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase))
  54 + {
  55 + query = query.OrderBy(x => x.CreationTime);
  56 + }
  57 + else
  58 + {
  59 + query = query.OrderByDescending(x => x.OrderNum).OrderByDescending(x => x.CreationTime);
  60 + }
39 61 }
40 62 else
41 63 {
... ... @@ -58,8 +80,11 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
58 80 Id = x.Id,
59 81 CategoryCode = x.CategoryCode,
60 82 CategoryName = x.CategoryName,
  83 + DisplayText = x.DisplayText,
61 84 CategoryPhotoUrl = x.CategoryPhotoUrl,
62 85 State = x.State,
  86 + ButtonAppearance = x.ButtonAppearance,
  87 + AvailabilityType = x.AvailabilityType,
63 88 OrderNum = x.OrderNum,
64 89 NoOfLabels = countMap.TryGetValue(x.Id, out var count) ? count : 0,
65 90 LastEdited = x.LastModificationTime ?? x.CreationTime
... ... @@ -77,7 +102,17 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
77 102 throw new UserFriendlyException("标签分类不存在");
78 103 }
79 104  
80   - return MapToGetOutput(entity);
  105 + var dto = MapToGetOutput(entity);
  106 + if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))
  107 + {
  108 + var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryLocationDbEntity>()
  109 + .Where(x => x.CategoryId == entity.Id)
  110 + .Select(x => x.LocationId)
  111 + .ToListAsync();
  112 + dto.LocationIds = locationIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList() ?? new();
  113 + }
  114 +
  115 + return dto;
81 116 }
82 117  
83 118 public async Task<LabelCategoryGetOutputDto> CreateAsync(LabelCategoryCreateInputVo input)
... ... @@ -89,6 +124,12 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
89 124 throw new UserFriendlyException("分类编码和名称不能为空");
90 125 }
91 126  
  127 + var displayText = input.DisplayText?.Trim();
  128 + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance);
  129 + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
  130 + var locationIds = NormalizeLocationIds(input.LocationIds);
  131 + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
  132 +
92 133 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
93 134 .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name));
94 135 if (duplicated)
... ... @@ -96,17 +137,23 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
96 137 throw new UserFriendlyException("分类编码或名称已存在");
97 138 }
98 139  
  140 + var now = DateTime.Now;
  141 + var currentUserId = CurrentUser?.Id?.ToString();
99 142 var entity = new FlLabelCategoryDbEntity
100 143 {
101 144 Id = _guidGenerator.Create().ToString(),
102 145 CategoryCode = code,
103 146 CategoryName = name,
104   - CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(),
  147 + DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText,
  148 + CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl),
105 149 State = input.State,
  150 + ButtonAppearance = appearance,
  151 + AvailabilityType = availabilityType,
106 152 OrderNum = input.OrderNum
107 153 };
108 154  
109 155 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  156 + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, currentUserId, now);
110 157 return await GetAsync(entity.Id);
111 158 }
112 159  
... ... @@ -126,6 +173,12 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
126 173 throw new UserFriendlyException("分类编码和名称不能为空");
127 174 }
128 175  
  176 + var displayText = input.DisplayText?.Trim();
  177 + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance);
  178 + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
  179 + var locationIds = NormalizeLocationIds(input.LocationIds);
  180 + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
  181 +
129 182 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
130 183 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name));
131 184 if (duplicated)
... ... @@ -135,13 +188,17 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
135 188  
136 189 entity.CategoryCode = code;
137 190 entity.CategoryName = name;
138   - entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim();
  191 + entity.DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText;
  192 + entity.CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl);
139 193 entity.State = input.State;
  194 + entity.ButtonAppearance = appearance;
  195 + entity.AvailabilityType = availabilityType;
140 196 entity.OrderNum = input.OrderNum;
141 197 entity.LastModificationTime = DateTime.Now;
142 198 entity.LastModifierId = CurrentUser?.Id?.ToString();
143 199  
144 200 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  201 + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now);
145 202 return await GetAsync(id);
146 203 }
147 204  
... ... @@ -174,12 +231,70 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
174 231 Id = x.Id,
175 232 CategoryCode = x.CategoryCode,
176 233 CategoryName = x.CategoryName,
  234 + DisplayText = x.DisplayText,
177 235 CategoryPhotoUrl = x.CategoryPhotoUrl,
178 236 State = x.State,
  237 + ButtonAppearance = x.ButtonAppearance,
  238 + AvailabilityType = x.AvailabilityType,
179 239 OrderNum = x.OrderNum
180 240 };
181 241 }
182 242  
  243 + private static void ValidateAvailabilityTypeAndLocations(string availabilityType, List<string> locationIds)
  244 + {
  245 + if (availabilityType != "ALL" && availabilityType != "SPECIFIED")
  246 + {
  247 + throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)");
  248 + }
  249 +
  250 + if (availabilityType == "SPECIFIED" && locationIds.Count == 0)
  251 + {
  252 + throw new UserFriendlyException("指定门店范围时必须至少选择一个门店");
  253 + }
  254 + }
  255 +
  256 + private static List<string> NormalizeLocationIds(List<string>? locationIds)
  257 + {
  258 + return locationIds?
  259 + .Where(x => !string.IsNullOrWhiteSpace(x))
  260 + .Select(x => x.Trim())
  261 + .Distinct()
  262 + .ToList() ?? new();
  263 + }
  264 +
  265 + private async Task SaveCategoryLocationsAsync(
  266 + string categoryId,
  267 + string availabilityType,
  268 + List<string> locationIds,
  269 + string? currentUserId,
  270 + DateTime now)
  271 + {
  272 + await _dbContext.SqlSugarClient.Deleteable<FlLabelCategoryLocationDbEntity>()
  273 + .Where(x => x.CategoryId == categoryId)
  274 + .ExecuteCommandAsync();
  275 +
  276 + if (availabilityType != "SPECIFIED")
  277 + {
  278 + return;
  279 + }
  280 +
  281 + if (locationIds.Count == 0)
  282 + {
  283 + return;
  284 + }
  285 +
  286 + var rows = locationIds.Select(locId => new FlLabelCategoryLocationDbEntity
  287 + {
  288 + Id = _guidGenerator.Create().ToString(),
  289 + CategoryId = categoryId,
  290 + LocationId = locId,
  291 + CreationTime = now,
  292 + CreatorId = currentUserId
  293 + }).ToList();
  294 +
  295 + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
  296 + }
  297 +
183 298 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items)
184 299 {
185 300 var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount;
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LocationSupportAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Contracts;
  2 +using FoodLabeling.Application.Contracts.Dtos.LocationSupport;
  3 +using FoodLabeling.Application.Contracts.IServices;
  4 +using FoodLabeling.Application.Services.DbModels;
  5 +using Microsoft.AspNetCore.Authorization;
  6 +using Volo.Abp;
  7 +using Volo.Abp.Application.Services;
  8 +using Volo.Abp.Guids;
  9 +using Volo.Abp.Uow;
  10 +using Yi.Framework.SqlSugarCore.Abstractions;
  11 +
  12 +namespace FoodLabeling.Application.Services;
  13 +
  14 +/// <summary>
  15 +/// 全局 Support 联系方式(全门店共用;Web 可增改查,App JWT 仅可读)
  16 +/// </summary>
  17 +[Authorize]
  18 +public class LocationSupportAppService : ApplicationService, ILocationSupportAppService
  19 +{
  20 + private readonly ISqlSugarDbContext _dbContext;
  21 + private readonly IGuidGenerator _guidGenerator;
  22 +
  23 + public LocationSupportAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator)
  24 + {
  25 + _dbContext = dbContext;
  26 + _guidGenerator = guidGenerator;
  27 + }
  28 +
  29 + /// <inheritdoc />
  30 + public async Task<LocationSupportGetOutputDto?> GetSupportAsync()
  31 + {
  32 + var rows = await _dbContext.SqlSugarClient.Queryable<FlLocationSupportDbEntity>()
  33 + .Where(x => !x.IsDeleted)
  34 + .ToListAsync();
  35 + var entity = rows.FirstOrDefault();
  36 + return MapOutput(entity);
  37 + }
  38 +
  39 + /// <inheritdoc />
  40 + [UnitOfWork]
  41 + public async Task<LocationSupportGetOutputDto> CreateAsync(LocationSupportCreateInputVo input)
  42 + {
  43 + EnsureNotUsAppClient();
  44 +
  45 + if (input is null)
  46 + {
  47 + throw new UserFriendlyException("Request body is required.");
  48 + }
  49 +
  50 + var phone = NormalizeRequired(input.SupportPhone, "Support phone is required.");
  51 + var email = NormalizeRequired(input.SupportEmail, "Support email is required.");
  52 + EnsureEmailFormat(email);
  53 +
  54 + var existed = await _dbContext.SqlSugarClient.Queryable<FlLocationSupportDbEntity>()
  55 + .AnyAsync(x => !x.IsDeleted);
  56 + if (existed)
  57 + {
  58 + throw new UserFriendlyException(
  59 + "Global support contact already exists. Use update instead.");
  60 + }
  61 +
  62 + var now = Clock.Now;
  63 + var entity = new FlLocationSupportDbEntity
  64 + {
  65 + Id = _guidGenerator.Create().ToString(),
  66 + IsDeleted = false,
  67 + CreationTime = now,
  68 + CreatorId = CurrentUser?.Id?.ToString(),
  69 + LastModificationTime = now,
  70 + LastModifierId = CurrentUser?.Id?.ToString(),
  71 + SupportPhone = phone,
  72 + SupportEmail = email
  73 + };
  74 +
  75 + await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  76 + return MapOutput(entity)!;
  77 + }
  78 +
  79 + /// <inheritdoc />
  80 + [UnitOfWork]
  81 + public async Task<LocationSupportGetOutputDto> UpdateAsync(string id, LocationSupportUpdateInputVo input)
  82 + {
  83 + EnsureNotUsAppClient();
  84 +
  85 + var supportId = id?.Trim();
  86 + if (string.IsNullOrWhiteSpace(supportId))
  87 + {
  88 + throw new UserFriendlyException("Support record id is required.");
  89 + }
  90 +
  91 + if (input is null)
  92 + {
  93 + throw new UserFriendlyException("Request body is required.");
  94 + }
  95 +
  96 + var rows = await _dbContext.SqlSugarClient.Queryable<FlLocationSupportDbEntity>()
  97 + .Where(x => !x.IsDeleted && x.Id == supportId)
  98 + .ToListAsync();
  99 + var entity = rows.FirstOrDefault();
  100 + if (entity is null)
  101 + {
  102 + throw new UserFriendlyException("Support record not found.");
  103 + }
  104 +
  105 + var phone = NormalizeRequired(input.SupportPhone, "Support phone is required.");
  106 + var email = NormalizeRequired(input.SupportEmail, "Support email is required.");
  107 + EnsureEmailFormat(email);
  108 +
  109 + entity.SupportPhone = phone;
  110 + entity.SupportEmail = email;
  111 + entity.LastModificationTime = Clock.Now;
  112 + entity.LastModifierId = CurrentUser?.Id?.ToString();
  113 +
  114 + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  115 + return MapOutput(entity)!;
  116 + }
  117 +
  118 + private void EnsureNotUsAppClient()
  119 + {
  120 + if (CurrentUser.FindClaim(UsAppJwtClaims.ClientKind)?.Value == UsAppJwtClaims.ClientKindUsApp)
  121 + {
  122 + throw new UserFriendlyException(
  123 + "The mobile app can only view support contacts. Please use the web console to edit.");
  124 + }
  125 + }
  126 +
  127 + private static string NormalizeRequired(string? value, string message)
  128 + {
  129 + var normalized = value?.Trim();
  130 + if (string.IsNullOrWhiteSpace(normalized))
  131 + {
  132 + throw new UserFriendlyException(message);
  133 + }
  134 +
  135 + return normalized;
  136 + }
  137 +
  138 + private static void EnsureEmailFormat(string email)
  139 + {
  140 + if (!email.Contains("@", StringComparison.Ordinal) || email.StartsWith("@", StringComparison.Ordinal) ||
  141 + email.EndsWith("@", StringComparison.Ordinal))
  142 + {
  143 + throw new UserFriendlyException("Support email format is invalid.");
  144 + }
  145 + }
  146 +
  147 + private static LocationSupportGetOutputDto? MapOutput(FlLocationSupportDbEntity? entity)
  148 + {
  149 + if (entity is null)
  150 + {
  151 + return null;
  152 + }
  153 +
  154 + return new LocationSupportGetOutputDto
  155 + {
  156 + Id = entity.Id,
  157 + SupportPhone = entity.SupportPhone,
  158 + SupportEmail = entity.SupportEmail
  159 + };
  160 + }
  161 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/PartnerAppService.cs 0 → 100644
  1 +using FoodLabeling.Application.Helpers;
  2 +using FoodLabeling.Application.Contracts.Dtos.Common;
  3 +using FoodLabeling.Application.Contracts.Dtos.Partner;
  4 +using FoodLabeling.Application.Contracts.IServices;
  5 +using FoodLabeling.Application.Services.DbModels;
  6 +using Microsoft.AspNetCore.Mvc;
  7 +using QuestPDF.Fluent;
  8 +using QuestPDF.Helpers;
  9 +using QuestPDF.Infrastructure;
  10 +using SqlSugar;
  11 +using Volo.Abp;
  12 +using Volo.Abp.Application.Services;
  13 +using Volo.Abp.Guids;
  14 +using Volo.Abp.Uow;
  15 +using Yi.Framework.SqlSugarCore.Abstractions;
  16 +
  17 +namespace FoodLabeling.Application.Services;
  18 +
  19 +/// <summary>
  20 +/// 合作伙伴管理(fl_partner)
  21 +/// </summary>
  22 +public class PartnerAppService : ApplicationService, IPartnerAppService
  23 +{
  24 + private const int ExportPdfMaxRows = 5000;
  25 +
  26 + private readonly ISqlSugarDbContext _dbContext;
  27 + private readonly IGuidGenerator _guidGenerator;
  28 +
  29 + public PartnerAppService(ISqlSugarDbContext dbContext, IGuidGenerator guidGenerator)
  30 + {
  31 + _dbContext = dbContext;
  32 + _guidGenerator = guidGenerator;
  33 + }
  34 +
  35 + /// <inheritdoc />
  36 + public async Task<PagedResultWithPageDto<PartnerGetListOutputDto>> GetListAsync(PartnerGetListInputVo input)
  37 + {
  38 + RefAsync<int> total = 0;
  39 + var query = BuildPartnerListQuery(input);
  40 +
  41 + var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
  42 + var items = entities.Select(MapListItem).ToList();
  43 + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items);
  44 + }
  45 +
  46 + /// <inheritdoc />
  47 + public async Task<PartnerGetOutputDto> GetAsync(string id)
  48 + {
  49 + var partnerId = id?.Trim();
  50 + if (string.IsNullOrWhiteSpace(partnerId))
  51 + {
  52 + throw new UserFriendlyException("合作伙伴Id不能为空");
  53 + }
  54 +
  55 + var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
  56 + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId);
  57 + if (entity is null)
  58 + {
  59 + throw new UserFriendlyException("合作伙伴不存在");
  60 + }
  61 +
  62 + return MapDetail(entity);
  63 + }
  64 +
  65 + /// <inheritdoc />
  66 + [UnitOfWork]
  67 + public async Task<PartnerGetOutputDto> CreateAsync(PartnerCreateInputVo input)
  68 + {
  69 + var name = input.PartnerName?.Trim();
  70 + if (string.IsNullOrWhiteSpace(name))
  71 + {
  72 + throw new UserFriendlyException("合作伙伴名称不能为空");
  73 + }
  74 +
  75 + var email = input.ContactEmail?.Trim();
  76 + if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email))
  77 + {
  78 + throw new UserFriendlyException("联系邮箱格式不正确");
  79 + }
  80 +
  81 + var now = Clock.Now;
  82 + var entity = new FlPartnerDbEntity
  83 + {
  84 + Id = _guidGenerator.Create().ToString(),
  85 + IsDeleted = false,
  86 + PartnerName = name,
  87 + ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email,
  88 + PhoneNumber = string.IsNullOrWhiteSpace(input.PhoneNumber) ? null : input.PhoneNumber.Trim(),
  89 + State = input.State,
  90 + CreationTime = now,
  91 + CreatorId = CurrentUser?.Id?.ToString(),
  92 + LastModificationTime = now,
  93 + LastModifierId = CurrentUser?.Id?.ToString()
  94 + };
  95 +
  96 + await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  97 + return await GetAsync(entity.Id);
  98 + }
  99 +
  100 + /// <inheritdoc />
  101 + [UnitOfWork]
  102 + public async Task<PartnerGetOutputDto> UpdateAsync(string id, PartnerUpdateInputVo input)
  103 + {
  104 + var partnerId = id?.Trim();
  105 + if (string.IsNullOrWhiteSpace(partnerId))
  106 + {
  107 + throw new UserFriendlyException("合作伙伴Id不能为空");
  108 + }
  109 +
  110 + var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
  111 + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId);
  112 + if (entity is null)
  113 + {
  114 + throw new UserFriendlyException("合作伙伴不存在");
  115 + }
  116 +
  117 + var name = input.PartnerName?.Trim();
  118 + if (string.IsNullOrWhiteSpace(name))
  119 + {
  120 + throw new UserFriendlyException("合作伙伴名称不能为空");
  121 + }
  122 +
  123 + var email = input.ContactEmail?.Trim();
  124 + if (!string.IsNullOrWhiteSpace(email) && !IsPlausibleEmail(email))
  125 + {
  126 + throw new UserFriendlyException("联系邮箱格式不正确");
  127 + }
  128 +
  129 + entity.PartnerName = name;
  130 + entity.ContactEmail = string.IsNullOrWhiteSpace(email) ? null : email;
  131 + entity.PhoneNumber = string.IsNullOrWhiteSpace(input.PhoneNumber) ? null : input.PhoneNumber.Trim();
  132 + entity.State = input.State;
  133 + entity.LastModificationTime = Clock.Now;
  134 + entity.LastModifierId = CurrentUser?.Id?.ToString();
  135 +
  136 + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  137 + return await GetAsync(partnerId);
  138 + }
  139 +
  140 + /// <inheritdoc />
  141 + [UnitOfWork]
  142 + public async Task DeleteAsync(string id)
  143 + {
  144 + var partnerId = id?.Trim();
  145 + if (string.IsNullOrWhiteSpace(partnerId))
  146 + {
  147 + throw new UserFriendlyException("合作伙伴Id不能为空");
  148 + }
  149 +
  150 + var entity = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
  151 + .FirstAsync(x => !x.IsDeleted && x.Id == partnerId);
  152 + if (entity is null)
  153 + {
  154 + throw new UserFriendlyException("合作伙伴不存在");
  155 + }
  156 +
  157 + entity.IsDeleted = true;
  158 + entity.LastModificationTime = Clock.Now;
  159 + entity.LastModifierId = CurrentUser?.Id?.ToString();
  160 + await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  161 + }
  162 +
  163 + /// <inheritdoc />
  164 + public async Task<IActionResult> ExportPdfAsync(PartnerGetListInputVo input)
  165 + {
  166 + QuestPDF.Settings.License = LicenseType.Community;
  167 +
  168 + var count = await BuildPartnerListQuery(input).CountAsync();
  169 + var query = BuildPartnerListQuery(input);
  170 + if (count > ExportPdfMaxRows)
  171 + {
  172 + throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围");
  173 + }
  174 +
  175 + var rows = await query.Take(ExportPdfMaxRows).ToListAsync();
  176 + var fileName = $"partners_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf";
  177 +
  178 + var document = Document.Create(container =>
  179 + {
  180 + container.Page(page =>
  181 + {
  182 + page.Margin(28);
  183 + page.DefaultTextStyle(x => x.FontSize(10));
  184 + page.Header().Text("Partners").SemiBold().FontSize(18);
  185 + page.Content().PaddingTop(12).Table(table =>
  186 + {
  187 + table.ColumnsDefinition(c =>
  188 + {
  189 + c.RelativeColumn(2.2f);
  190 + c.RelativeColumn(2.4f);
  191 + c.RelativeColumn(1.8f);
  192 + c.RelativeColumn(1f);
  193 + c.RelativeColumn(1.6f);
  194 + });
  195 +
  196 + static IContainer CellHeader(IContainer c) =>
  197 + c.Background(Colors.Grey.Lighten3).Padding(6).DefaultTextStyle(x => x.SemiBold());
  198 +
  199 + table.Cell().Element(CellHeader).Text("Partner");
  200 + table.Cell().Element(CellHeader).Text("Contact");
  201 + table.Cell().Element(CellHeader).Text("Phone");
  202 + table.Cell().Element(CellHeader).Text("Status");
  203 + table.Cell().Element(CellHeader).Text("Created");
  204 +
  205 + foreach (var e in rows)
  206 + {
  207 + var status = e.State ? "active" : "inactive";
  208 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5)
  209 + .Text(e.PartnerName ?? string.Empty);
  210 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5)
  211 + .Text(string.IsNullOrWhiteSpace(e.ContactEmail) ? "无" : e.ContactEmail!);
  212 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5)
  213 + .Text(string.IsNullOrWhiteSpace(e.PhoneNumber) ? "无" : e.PhoneNumber!);
  214 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5).Text(status);
  215 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(5)
  216 + .Text(e.CreationTime.ToString("yyyy-MM-dd HH:mm"));
  217 + }
  218 + });
  219 + });
  220 + });
  221 +
  222 + var stream = new MemoryStream();
  223 + document.GeneratePdf(stream);
  224 + stream.Position = 0;
  225 + return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName };
  226 + }
  227 +
  228 + private ISugarQueryable<FlPartnerDbEntity> BuildPartnerListQuery(PartnerGetListInputVo input)
  229 + {
  230 + var keyword = input.Keyword?.Trim();
  231 + var query = _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
  232 + .Where(x => !x.IsDeleted)
  233 + .WhereIF(input.State != null, x => x.State == input.State)
  234 + .WhereIF(!string.IsNullOrWhiteSpace(keyword),
  235 + x => x.PartnerName.Contains(keyword!) ||
  236 + (x.ContactEmail != null && x.ContactEmail.Contains(keyword!)) ||
  237 + (x.PhoneNumber != null && x.PhoneNumber.Contains(keyword!)));
  238 +
  239 + if (!string.IsNullOrWhiteSpace(input.Sorting))
  240 + {
  241 + var sorting = input.Sorting.Trim();
  242 + if (sorting.Equals("PartnerName desc", StringComparison.OrdinalIgnoreCase))
  243 + {
  244 + query = query.OrderByDescending(x => x.PartnerName);
  245 + }
  246 + else if (sorting.Equals("PartnerName asc", StringComparison.OrdinalIgnoreCase))
  247 + {
  248 + query = query.OrderBy(x => x.PartnerName);
  249 + }
  250 + else if (sorting.Equals("CreationTime desc", StringComparison.OrdinalIgnoreCase))
  251 + {
  252 + query = query.OrderByDescending(x => x.CreationTime);
  253 + }
  254 + else if (sorting.Equals("CreationTime asc", StringComparison.OrdinalIgnoreCase))
  255 + {
  256 + query = query.OrderBy(x => x.CreationTime);
  257 + }
  258 + else if (sorting.Equals("State desc", StringComparison.OrdinalIgnoreCase))
  259 + {
  260 + query = query.OrderByDescending(x => x.State);
  261 + }
  262 + else if (sorting.Equals("State asc", StringComparison.OrdinalIgnoreCase))
  263 + {
  264 + query = query.OrderBy(x => x.State);
  265 + }
  266 + else
  267 + {
  268 + query = query.OrderByDescending(x => x.CreationTime);
  269 + }
  270 + }
  271 + else
  272 + {
  273 + query = query.OrderByDescending(x => x.CreationTime);
  274 + }
  275 +
  276 + return query;
  277 + }
  278 +
  279 + private static PartnerGetListOutputDto MapListItem(FlPartnerDbEntity x) => new()
  280 + {
  281 + Id = x.Id,
  282 + PartnerName = x.PartnerName,
  283 + ContactEmail = x.ContactEmail,
  284 + PhoneNumber = x.PhoneNumber,
  285 + State = x.State,
  286 + CreationTime = x.CreationTime
  287 + };
  288 +
  289 + private static PartnerGetOutputDto MapDetail(FlPartnerDbEntity x) => new()
  290 + {
  291 + Id = x.Id,
  292 + PartnerName = x.PartnerName,
  293 + ContactEmail = x.ContactEmail,
  294 + PhoneNumber = x.PhoneNumber,
  295 + State = x.State,
  296 + CreationTime = x.CreationTime,
  297 + LastModificationTime = x.LastModificationTime
  298 + };
  299 +
  300 + private static bool IsPlausibleEmail(string email)
  301 + {
  302 + if (email.Length > 256)
  303 + {
  304 + return false;
  305 + }
  306 +
  307 + var at = email.IndexOf('@');
  308 + return at > 0 && at < email.Length - 1 && email.IndexOf('@', at + 1) < 0;
  309 + }
  310 +
  311 + private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total,
  312 + List<T> items)
  313 + {
  314 + var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount;
  315 + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount);
  316 + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize);
  317 + return new PagedResultWithPageDto<T>
  318 + {
  319 + PageIndex = pageIndex,
  320 + PageSize = pageSize,
  321 + TotalCount = total,
  322 + TotalPages = totalPages,
  323 + Items = items
  324 + };
  325 + }
  326 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductAppService.cs
... ... @@ -3,6 +3,7 @@ using FoodLabeling.Application.Contracts.Dtos.Common;
3 3 using FoodLabeling.Application.Contracts.Dtos.Product;
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;
... ... @@ -188,6 +189,13 @@ public class ProductAppService : ApplicationService, IProductAppService
188 189 };
189 190  
190 191 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  192 +
  193 + if (input.LocationIds is not null)
  194 + {
  195 + var locIds = await NormalizeAndValidateLocationIdsAsync(input.LocationIds);
  196 + await ReplaceProductLocationLinksAsync(entity.Id, locIds);
  197 + }
  198 +
191 199 return await GetAsync(entity.Id);
192 200 }
193 201  
... ... @@ -228,6 +236,13 @@ public class ProductAppService : ApplicationService, IProductAppService
228 236 entity.State = input.State;
229 237  
230 238 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  239 +
  240 + if (input.LocationIds is not null)
  241 + {
  242 + var locIds = await NormalizeAndValidateLocationIdsAsync(input.LocationIds);
  243 + await ReplaceProductLocationLinksAsync(productId, locIds);
  244 + }
  245 +
231 246 return await GetAsync(productId);
232 247 }
233 248  
... ... @@ -251,6 +266,66 @@ public class ProductAppService : ApplicationService, IProductAppService
251 266 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
252 267 }
253 268  
  269 + /// <summary>
  270 + /// 去重、校验门店 Id 格式与存在性。
  271 + /// </summary>
  272 + private async Task<List<string>> NormalizeAndValidateLocationIdsAsync(IEnumerable<string> rawIds)
  273 + {
  274 + var distinct = rawIds
  275 + .Where(x => !string.IsNullOrWhiteSpace(x))
  276 + .Select(x => x.Trim())
  277 + .Distinct(StringComparer.Ordinal)
  278 + .ToList();
  279 +
  280 + if (distinct.Count == 0)
  281 + {
  282 + return new List<string>();
  283 + }
  284 +
  285 + foreach (var id in distinct)
  286 + {
  287 + if (!Guid.TryParse(id, out _))
  288 + {
  289 + throw new UserFriendlyException("门店Id格式不正确");
  290 + }
  291 + }
  292 +
  293 + var guidList = distinct.Select(Guid.Parse).ToList();
  294 + var existCount = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>()
  295 + .Where(x => !x.IsDeleted && guidList.Contains(x.Id))
  296 + .CountAsync();
  297 + if (existCount != distinct.Count)
  298 + {
  299 + throw new UserFriendlyException("门店不存在");
  300 + }
  301 +
  302 + return distinct;
  303 + }
  304 +
  305 + /// <summary>
  306 + /// 按产品维度替换 fl_location_product:先删本产品全部关联,再按列表插入(每门店一行)。
  307 + /// </summary>
  308 + private async Task ReplaceProductLocationLinksAsync(string productId, List<string> locationIds)
  309 + {
  310 + await _dbContext.SqlSugarClient.Deleteable<FlLocationProductDbEntity>()
  311 + .Where(x => x.ProductId == productId)
  312 + .ExecuteCommandAsync();
  313 +
  314 + if (locationIds.Count == 0)
  315 + {
  316 + return;
  317 + }
  318 +
  319 + var rows = locationIds.Select(lid => new FlLocationProductDbEntity
  320 + {
  321 + Id = _guidGenerator.Create().ToString(),
  322 + LocationId = lid,
  323 + ProductId = productId
  324 + }).ToList();
  325 +
  326 + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
  327 + }
  328 +
254 329 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items)
255 330 {
256 331 var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount;
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs
  1 +using FoodLabeling.Application.Helpers;
1 2 using FoodLabeling.Application.Contracts.Dtos.Common;
2 3 using FoodLabeling.Application.Contracts.Dtos.ProductCategory;
3 4 using FoodLabeling.Application.Contracts.IServices;
... ... @@ -35,7 +36,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
35 36 var query = _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
36 37 .Where(x => !x.IsDeleted)
37 38 .WhereIF(!string.IsNullOrWhiteSpace(keyword),
38   - x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!))
  39 + x => x.CategoryCode.Contains(keyword!) || x.CategoryName.Contains(keyword!) ||
  40 + (x.DisplayText != null && x.DisplayText.Contains(keyword!)))
39 41 .WhereIF(input.State != null, x => x.State == input.State);
40 42  
41 43 // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column
... ... @@ -77,8 +79,11 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
77 79 Id = x.Id,
78 80 CategoryCode = x.CategoryCode,
79 81 CategoryName = x.CategoryName,
  82 + DisplayText = x.DisplayText,
80 83 CategoryPhotoUrl = x.CategoryPhotoUrl,
  84 + ButtonAppearance = x.ButtonAppearance,
81 85 State = x.State,
  86 + AvailabilityType = x.AvailabilityType,
82 87 OrderNum = x.OrderNum,
83 88 LastEdited = x.LastModificationTime ?? x.CreationTime
84 89 }).ToList();
... ... @@ -98,7 +103,17 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
98 103 throw new UserFriendlyException("类别不存在");
99 104 }
100 105  
101   - return MapToGetOutput(entity);
  106 + var dto = MapToGetOutput(entity);
  107 + if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))
  108 + {
  109 + var locationIds = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryLocationDbEntity>()
  110 + .Where(x => x.CategoryId == entity.Id)
  111 + .Select(x => x.LocationId)
  112 + .ToListAsync();
  113 + dto.LocationIds = locationIds?.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().ToList() ?? new();
  114 + }
  115 +
  116 + return dto;
102 117 }
103 118  
104 119 /// <summary>
... ... @@ -113,6 +128,12 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
113 128 throw new UserFriendlyException("类别编码和名称不能为空");
114 129 }
115 130  
  131 + var displayText = input.DisplayText?.Trim();
  132 + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance);
  133 + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
  134 + var locationIds = NormalizeLocationIds(input.LocationIds);
  135 + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
  136 +
116 137 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
117 138 .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name));
118 139 if (duplicated)
... ... @@ -133,12 +154,16 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
133 154 ConcurrencyStamp = _guidGenerator.Create().ToString("N"),
134 155 CategoryCode = code,
135 156 CategoryName = name,
136   - CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim(),
  157 + DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText,
  158 + CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl),
  159 + ButtonAppearance = appearance,
137 160 State = input.State,
  161 + AvailabilityType = availabilityType,
138 162 OrderNum = input.OrderNum
139 163 };
140 164  
141 165 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  166 + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, currentUserId, now);
142 167 return await GetAsync(entity.Id);
143 168 }
144 169  
... ... @@ -161,6 +186,12 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
161 186 throw new UserFriendlyException("类别编码和名称不能为空");
162 187 }
163 188  
  189 + var displayText = input.DisplayText?.Trim();
  190 + var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance);
  191 + var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
  192 + var locationIds = NormalizeLocationIds(input.LocationIds);
  193 + ValidateAvailabilityTypeAndLocations(availabilityType, locationIds);
  194 +
164 195 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryDbEntity>()
165 196 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name));
166 197 if (duplicated)
... ... @@ -170,13 +201,17 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
170 201  
171 202 entity.CategoryCode = code;
172 203 entity.CategoryName = name;
173   - entity.CategoryPhotoUrl = input.CategoryPhotoUrl?.Trim();
  204 + entity.DisplayText = string.IsNullOrWhiteSpace(displayText) ? null : displayText;
  205 + entity.CategoryPhotoUrl = CategoryAppearanceStorageHelper.NormalizeCategoryPhotoUrlForStorage(input.CategoryPhotoUrl);
  206 + entity.ButtonAppearance = appearance;
174 207 entity.State = input.State;
  208 + entity.AvailabilityType = availabilityType;
175 209 entity.OrderNum = input.OrderNum;
176 210 entity.LastModificationTime = DateTime.Now;
177 211 entity.LastModifierId = CurrentUser?.Id?.ToString();
178 212  
179 213 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  214 + await SaveCategoryLocationsAsync(entity.Id, availabilityType, locationIds, entity.LastModifierId, entity.LastModificationTime ?? DateTime.Now);
180 215 return await GetAsync(id);
181 216 }
182 217  
... ... @@ -213,12 +248,70 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
213 248 Id = x.Id,
214 249 CategoryCode = x.CategoryCode,
215 250 CategoryName = x.CategoryName,
  251 + DisplayText = x.DisplayText,
216 252 CategoryPhotoUrl = x.CategoryPhotoUrl,
  253 + ButtonAppearance = x.ButtonAppearance,
217 254 State = x.State,
  255 + AvailabilityType = x.AvailabilityType,
218 256 OrderNum = x.OrderNum
219 257 };
220 258 }
221 259  
  260 + private static void ValidateAvailabilityTypeAndLocations(string availabilityType, List<string> locationIds)
  261 + {
  262 + if (availabilityType != "ALL" && availabilityType != "SPECIFIED")
  263 + {
  264 + throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)");
  265 + }
  266 +
  267 + if (availabilityType == "SPECIFIED" && locationIds.Count == 0)
  268 + {
  269 + throw new UserFriendlyException("指定门店范围时必须至少选择一个门店");
  270 + }
  271 + }
  272 +
  273 + private static List<string> NormalizeLocationIds(List<string>? locationIds)
  274 + {
  275 + return locationIds?
  276 + .Where(x => !string.IsNullOrWhiteSpace(x))
  277 + .Select(x => x.Trim())
  278 + .Distinct()
  279 + .ToList() ?? new();
  280 + }
  281 +
  282 + private async Task SaveCategoryLocationsAsync(
  283 + string categoryId,
  284 + string availabilityType,
  285 + List<string> locationIds,
  286 + string? currentUserId,
  287 + DateTime now)
  288 + {
  289 + await _dbContext.SqlSugarClient.Deleteable<FlProductCategoryLocationDbEntity>()
  290 + .Where(x => x.CategoryId == categoryId)
  291 + .ExecuteCommandAsync();
  292 +
  293 + if (availabilityType != "SPECIFIED")
  294 + {
  295 + return;
  296 + }
  297 +
  298 + if (locationIds.Count == 0)
  299 + {
  300 + return;
  301 + }
  302 +
  303 + var rows = locationIds.Select(locId => new FlProductCategoryLocationDbEntity
  304 + {
  305 + Id = _guidGenerator.Create().ToString(),
  306 + CategoryId = categoryId,
  307 + LocationId = locId,
  308 + CreationTime = now,
  309 + CreatorId = currentUserId
  310 + }).ToList();
  311 +
  312 + await _dbContext.SqlSugarClient.Insertable(rows).ExecuteCommandAsync();
  313 + }
  314 +
222 315 private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total, List<T> items)
223 316 {
224 317 var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount;
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/RbacMenuAppService.cs
... ... @@ -38,6 +38,8 @@ public class RbacMenuAppService : ApplicationService, IRbacMenuAppService
38 38 Id = x.Id,
39 39 ParentId = x.ParentId,
40 40 MenuName = x.MenuName ?? string.Empty,
  41 + RouterName = x.RouterName,
  42 + Router = x.Router,
41 43 PermissionCode = x.PermissionCode,
42 44 MenuType = x.MenuType,
43 45 MenuSource = x.MenuSource,
... ... @@ -62,6 +64,8 @@ public class RbacMenuAppService : ApplicationService, IRbacMenuAppService
62 64 Id = entity.Id,
63 65 ParentId = entity.ParentId,
64 66 MenuName = entity.MenuName ?? string.Empty,
  67 + RouterName = entity.RouterName,
  68 + Router = entity.Router,
65 69 PermissionCode = entity.PermissionCode,
66 70 MenuType = entity.MenuType,
67 71 MenuSource = entity.MenuSource,
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs 0 → 100644
  1 +using System.Globalization;
  2 +using System.Text.Json;
  3 +using FoodLabeling.Application.Helpers;
  4 +using FoodLabeling.Application.Contracts.Dtos.Common;
  5 +using FoodLabeling.Application.Contracts.Dtos.Reports;
  6 +using FoodLabeling.Application.Contracts.Dtos.UsAppLabeling;
  7 +using FoodLabeling.Application.Contracts.IServices;
  8 +using FoodLabeling.Application.Services.DbModels;
  9 +using FoodLabeling.Domain.Entities;
  10 +using Microsoft.AspNetCore.Authorization;
  11 +using Microsoft.AspNetCore.Mvc;
  12 +using QuestPDF.Fluent;
  13 +using QuestPDF.Helpers;
  14 +using QuestPDF.Infrastructure;
  15 +using SqlSugar;
  16 +using Volo.Abp;
  17 +using Volo.Abp.Application.Services;
  18 +using Yi.Framework.Rbac.Domain.Entities;
  19 +using Yi.Framework.SqlSugarCore.Abstractions;
  20 +
  21 +namespace FoodLabeling.Application.Services;
  22 +
  23 +/// <summary>
  24 +/// Reports(Print Log / Label Report)
  25 +/// </summary>
  26 +[Authorize]
  27 +public class ReportsAppService : ApplicationService, IReportsAppService
  28 +{
  29 + private const int ExportPdfMaxRows = 5000;
  30 +
  31 + private readonly ISqlSugarDbContext _dbContext;
  32 + private readonly IUsAppLabelingAppService _usAppLabelingAppService;
  33 +
  34 + public ReportsAppService(ISqlSugarDbContext dbContext, IUsAppLabelingAppService usAppLabelingAppService)
  35 + {
  36 + _dbContext = dbContext;
  37 + _usAppLabelingAppService = usAppLabelingAppService;
  38 + }
  39 +
  40 + /// <inheritdoc />
  41 + public async Task<PagedResultWithPageDto<ReportsPrintLogListItemDto>> GetPrintLogListAsync(
  42 + ReportsPrintLogGetListInputVo input)
  43 + {
  44 + if (input is null)
  45 + {
  46 + throw new UserFriendlyException("入参不能为空");
  47 + }
  48 +
  49 + if (!CurrentUser.Id.HasValue)
  50 + {
  51 + throw new UserFriendlyException("用户未登录");
  52 + }
  53 +
  54 + var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId);
  55 + if (locationIds is not null && locationIds.Count == 0)
  56 + {
  57 + return EmptyPrintLogPage(input);
  58 + }
  59 +
  60 + var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate);
  61 + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser);
  62 + var currentUserIdStr = CurrentUser.Id.Value.ToString();
  63 + var keyword = input.Keyword?.Trim();
  64 +
  65 + RefAsync<int> total = 0;
  66 +
  67 + var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword)
  68 + .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id)
  69 + .Where((t, l, p, lc, pc, loc, tpl) =>
  70 + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart &&
  71 + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < rangeEndExcl);
  72 +
  73 + if (!string.IsNullOrWhiteSpace(input.Sorting) &&
  74 + input.Sorting.Trim().Equals("PrintedAt asc", StringComparison.OrdinalIgnoreCase))
  75 + {
  76 + query = query.OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime),
  77 + OrderByType.Asc);
  78 + }
  79 + else
  80 + {
  81 + query = query.OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime),
  82 + OrderByType.Desc);
  83 + }
  84 +
  85 + var pageRows = await query
  86 + .Select((t, l, p, lc, pc, loc, tpl) => new
  87 + {
  88 + t.Id,
  89 + LabelCode = l.LabelCode,
  90 + ProductName = p.ProductName,
  91 + LabelCategoryName = lc.CategoryName,
  92 + ProductCategoryName = pc.CategoryName,
  93 + tpl.Width,
  94 + tpl.Height,
  95 + tpl.Unit,
  96 + tpl.TemplateName,
  97 + t.PrintInputJson,
  98 + PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime),
  99 + t.CreatedBy,
  100 + t.LocationId,
  101 + LocName = loc.LocationName,
  102 + LocCode = loc.LocationCode
  103 + })
  104 + .ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
  105 +
  106 + var userMap = await LoadUserNameMapAsync(pageRows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x))
  107 + .Select(x => x!).Distinct().ToList());
  108 +
  109 + var items = pageRows.Select(x =>
  110 + {
  111 + var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName)
  112 + ? x.ProductCategoryName!.Trim()
  113 + : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim());
  114 + var templateText = FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName);
  115 + var locText = FormatLocationText(x.LocName, x.LocCode);
  116 + var printedAt = x.PrintedAt ?? DateTime.MinValue;
  117 + return new ReportsPrintLogListItemDto
  118 + {
  119 + TaskId = x.Id,
  120 + LabelCode = string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim(),
  121 + ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim(),
  122 + CategoryName = string.IsNullOrWhiteSpace(cat) ? "无" : cat,
  123 + TemplateText = string.IsNullOrWhiteSpace(templateText) ? "无" : templateText,
  124 + PrintedAt = printedAt,
  125 + PrintedByName = ResolveUserName(userMap, x.CreatedBy),
  126 + LocationText = locText,
  127 + LocationId = x.LocationId?.Trim(),
  128 + ExpiryDateText = TryExtractExpiryText(x.PrintInputJson)
  129 + };
  130 + }).ToList();
  131 +
  132 + return BuildPagedResult(input.SkipCount, input.MaxResultCount, total, items);
  133 + }
  134 +
  135 + /// <inheritdoc />
  136 + public async Task<IActionResult> ExportPrintLogPdfAsync(ReportsPrintLogGetListInputVo input)
  137 + {
  138 + QuestPDF.Settings.License = LicenseType.Community;
  139 + if (input is null)
  140 + {
  141 + throw new UserFriendlyException("入参不能为空");
  142 + }
  143 +
  144 + if (!CurrentUser.Id.HasValue)
  145 + {
  146 + throw new UserFriendlyException("用户未登录");
  147 + }
  148 +
  149 + var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId);
  150 + if (locationIds is not null && locationIds.Count == 0)
  151 + {
  152 + return BuildEmptyPdf("print-log-empty.pdf");
  153 + }
  154 +
  155 + var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate);
  156 + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser);
  157 + var currentUserIdStr = CurrentUser.Id.Value.ToString();
  158 + var keyword = input.Keyword?.Trim();
  159 +
  160 + var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword)
  161 + .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id)
  162 + .Where((t, l, p, lc, pc, loc, tpl) =>
  163 + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart &&
  164 + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < rangeEndExcl)
  165 + .OrderBy((t, l, p, lc, pc, loc, tpl) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime), OrderByType.Desc);
  166 +
  167 + var count = await query.CountAsync();
  168 + if (count > ExportPdfMaxRows)
  169 + {
  170 + throw new UserFriendlyException($"导出数据超过上限 {ExportPdfMaxRows} 条,请缩小筛选范围");
  171 + }
  172 +
  173 + var rows = await query.Take(ExportPdfMaxRows)
  174 + .Select((t, l, p, lc, pc, loc, tpl) => new
  175 + {
  176 + t.Id,
  177 + LabelCode = l.LabelCode,
  178 + ProductName = p.ProductName,
  179 + LabelCategoryName = lc.CategoryName,
  180 + ProductCategoryName = pc.CategoryName,
  181 + tpl.Width,
  182 + tpl.Height,
  183 + tpl.Unit,
  184 + tpl.TemplateName,
  185 + t.PrintInputJson,
  186 + PrintedAt = SqlFunc.IsNull(t.PrintedAt, t.CreationTime),
  187 + t.CreatedBy,
  188 + LocName = loc.LocationName,
  189 + LocCode = loc.LocationCode
  190 + })
  191 + .ToListAsync();
  192 +
  193 + var userMap = await LoadUserNameMapAsync(rows.Select(x => x.CreatedBy).Where(x => !string.IsNullOrWhiteSpace(x))
  194 + .Select(x => x!).Distinct().ToList());
  195 +
  196 + var fileName = $"print-log_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf";
  197 + var document = Document.Create(container =>
  198 + {
  199 + container.Page(page =>
  200 + {
  201 + page.Margin(22);
  202 + page.DefaultTextStyle(x => x.FontSize(8.5f));
  203 + page.Header().Text("Print Log").SemiBold().FontSize(16);
  204 + page.Content().PaddingTop(10).Table(table =>
  205 + {
  206 + table.ColumnsDefinition(c =>
  207 + {
  208 + c.RelativeColumn(1.1f);
  209 + c.RelativeColumn(1.2f);
  210 + c.RelativeColumn(0.9f);
  211 + c.RelativeColumn(1.1f);
  212 + c.RelativeColumn(1f);
  213 + c.RelativeColumn(0.9f);
  214 + c.RelativeColumn(0.9f);
  215 + c.RelativeColumn(0.8f);
  216 + });
  217 + static IContainer H(IContainer x) =>
  218 + x.Background(Colors.Grey.Lighten3).Padding(4).DefaultTextStyle(s => s.SemiBold());
  219 + table.Cell().Element(H).Text("Label ID");
  220 + table.Cell().Element(H).Text("Product");
  221 + table.Cell().Element(H).Text("Category");
  222 + table.Cell().Element(H).Text("Template");
  223 + table.Cell().Element(H).Text("Printed At");
  224 + table.Cell().Element(H).Text("Printed By");
  225 + table.Cell().Element(H).Text("Location");
  226 + table.Cell().Element(H).Text("Expiry");
  227 +
  228 + foreach (var x in rows)
  229 + {
  230 + var cat = !string.IsNullOrWhiteSpace(x.ProductCategoryName)
  231 + ? x.ProductCategoryName!.Trim()
  232 + : (string.IsNullOrWhiteSpace(x.LabelCategoryName) ? "无" : x.LabelCategoryName.Trim());
  233 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
  234 + .Text(string.IsNullOrWhiteSpace(x.LabelCode) ? "无" : x.LabelCode.Trim());
  235 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
  236 + .Text(string.IsNullOrWhiteSpace(x.ProductName) ? "无" : x.ProductName.Trim());
  237 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3).Text(cat);
  238 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
  239 + .Text(FormatTemplateDisplay(x.Width, x.Height, x.Unit, x.TemplateName));
  240 + var printedAt = x.PrintedAt ?? DateTime.MinValue;
  241 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
  242 + .Text(printedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture));
  243 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
  244 + .Text(ResolveUserName(userMap, x.CreatedBy));
  245 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
  246 + .Text(FormatLocationText(x.LocName, x.LocCode));
  247 + table.Cell().BorderBottom(0.5f).BorderColor(Colors.Grey.Lighten2).Padding(3)
  248 + .Text(TryExtractExpiryText(x.PrintInputJson));
  249 + }
  250 + });
  251 + });
  252 + });
  253 +
  254 + var ms = new MemoryStream();
  255 + document.GeneratePdf(ms);
  256 + ms.Position = 0;
  257 + return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName };
  258 + }
  259 +
  260 + /// <inheritdoc />
  261 + public Task<UsAppLabelPrintOutputDto> ReprintPrintLogAsync(UsAppLabelReprintInputVo input) =>
  262 + _usAppLabelingAppService.ReprintAsync(input);
  263 +
  264 + /// <inheritdoc />
  265 + public async Task<ReportsLabelReportOutputDto> GetLabelReportAsync(ReportsLabelReportQueryInputVo input)
  266 + {
  267 + if (input is null)
  268 + {
  269 + throw new UserFriendlyException("入参不能为空");
  270 + }
  271 +
  272 + if (!CurrentUser.Id.HasValue)
  273 + {
  274 + throw new UserFriendlyException("用户未登录");
  275 + }
  276 +
  277 + var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId);
  278 + if (locationIds is not null && locationIds.Count == 0)
  279 + {
  280 + return new ReportsLabelReportOutputDto();
  281 + }
  282 +
  283 + var (curStart, curEndExcl) = ResolveDateRange(input.StartDate, input.EndDate);
  284 + var span = curEndExcl - curStart;
  285 + if (span.TotalDays < 1)
  286 + {
  287 + span = TimeSpan.FromDays(1);
  288 + }
  289 +
  290 + var prevEndExcl = curStart;
  291 + var prevStart = curStart - span;
  292 + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser);
  293 + var currentUserIdStr = CurrentUser.Id.Value.ToString();
  294 + var keyword = input.Keyword?.Trim();
  295 +
  296 + var totalCur = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword)
  297 + .Where((t, l, p, lc, pc, loc) =>
  298 + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart &&
  299 + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl)
  300 + .CountAsync();
  301 +
  302 + var totalPrev = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword)
  303 + .Where((t, l, p, lc, pc, loc) =>
  304 + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart &&
  305 + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl)
  306 + .CountAsync();
  307 +
  308 + var dayCount = Math.Max(1, (int)Math.Ceiling((curEndExcl - curStart).TotalDays));
  309 + var prevDayCount = Math.Max(1, (int)Math.Ceiling((prevEndExcl - prevStart).TotalDays));
  310 + var avgDaily = Math.Round((decimal)totalCur / dayCount, 2);
  311 + var avgDailyPrev = Math.Round((decimal)totalPrev / prevDayCount, 2);
  312 +
  313 + var categoryRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword)
  314 + .Where((t, l, p, lc, pc, loc) =>
  315 + l.LabelCategoryId != null &&
  316 + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart &&
  317 + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl)
  318 + .GroupBy((t, l, p, lc, pc, loc) => new { lc.Id, lc.CategoryName })
  319 + .Select((t, l, p, lc, pc, loc) => new { lc.Id, lc.CategoryName, Cnt = SqlFunc.AggregateCount(t.Id) })
  320 + .ToListAsync();
  321 +
  322 + var topCat = categoryRows.OrderByDescending(x => x.Cnt).FirstOrDefault();
  323 +
  324 + var productRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword)
  325 + .Where((t, l, p, lc, pc, loc) =>
  326 + !string.IsNullOrEmpty(p.Id) &&
  327 + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart &&
  328 + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl)
  329 + .GroupBy((t, l, p, lc, pc, loc) => new { p.Id, p.ProductName, Cat = pc.CategoryName })
  330 + .Select((t, l, p, lc, pc, loc) => new { p.Id, p.ProductName, CategoryName = pc.CategoryName, Cnt = SqlFunc.AggregateCount(t.Id) })
  331 + .ToListAsync();
  332 +
  333 + var topProd = productRows.OrderByDescending(x => x.Cnt).FirstOrDefault();
  334 + var topList = productRows.OrderByDescending(x => x.Cnt).Take(20).ToList();
  335 +
  336 + var trendEndDay = curEndExcl.Date.AddDays(-1);
  337 + var trendStartDay = trendEndDay.AddDays(-6);
  338 + if (trendStartDay < curStart.Date)
  339 + {
  340 + trendStartDay = curStart.Date;
  341 + }
  342 +
  343 + var trendEndExcl = trendEndDay.AddDays(1);
  344 +
  345 + var trendRaw = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword)
  346 + .Where((t, l, p, lc, pc, loc) =>
  347 + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay &&
  348 + SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl)
  349 + .Select((t, l, p, lc, pc, loc) => SqlFunc.IsNull(t.PrintedAt, t.CreationTime))
  350 + .ToListAsync();
  351 +
  352 + var trendDict = trendRaw
  353 + .Where(x => x.HasValue)
  354 + .GroupBy(x => x!.Value.Date)
  355 + .ToDictionary(g => g.Key, g => g.Count());
  356 +
  357 + var trend = new List<ReportsDailyCountDto>();
  358 + for (var d = trendStartDay; d <= trendEndDay; d = d.AddDays(1))
  359 + {
  360 + trend.Add(new ReportsDailyCountDto
  361 + {
  362 + Date = d.ToString("yyyy-MM-dd"),
  363 + Count = trendDict.TryGetValue(d, out var c) ? c : 0
  364 + });
  365 + }
  366 +
  367 + var byCategory = categoryRows
  368 + .OrderByDescending(x => x.Cnt)
  369 + .Select(x => new ReportsCategoryCountDto
  370 + {
  371 + CategoryId = string.IsNullOrWhiteSpace(x.Id) ? null : x.Id.Trim(),
  372 + CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? null : x.CategoryName.Trim(),
  373 + Count = x.Cnt
  374 + })
  375 + .ToList();
  376 +
  377 + var mostUsed = topList.Select(x =>
  378 + {
  379 + var pct = totalCur <= 0 ? 0m : Math.Round(x.Cnt * 100m / totalCur, 2);
  380 + return new ReportsTopProductRowDto
  381 + {
  382 + ProductId = string.IsNullOrWhiteSpace(x.Id) ? null : x.Id.Trim(),
  383 + ProductName = string.IsNullOrWhiteSpace(x.ProductName) ? null : x.ProductName.Trim(),
  384 + CategoryName = string.IsNullOrWhiteSpace(x.CategoryName) ? null : x.CategoryName!.Trim(),
  385 + TotalPrinted = x.Cnt,
  386 + UsagePercent = pct
  387 + };
  388 + }).ToList();
  389 +
  390 + return new ReportsLabelReportOutputDto
  391 + {
  392 + Summary = new ReportsLabelReportSummaryDto
  393 + {
  394 + TotalLabelsPrinted = totalCur,
  395 + TotalLabelsPrintedPrevPeriod = totalPrev,
  396 + TotalLabelsPrintedChangeRate = CalcChangeRate(totalCur, totalPrev),
  397 + MostPrintedCategoryName = string.IsNullOrWhiteSpace(topCat?.CategoryName) ? null : topCat.CategoryName.Trim(),
  398 + MostPrintedCategoryCount = topCat?.Cnt ?? 0,
  399 + TopProductName = string.IsNullOrWhiteSpace(topProd?.ProductName) ? null : topProd.ProductName.Trim(),
  400 + TopProductCount = topProd?.Cnt ?? 0,
  401 + AvgDailyPrints = avgDaily,
  402 + AvgDailyPrintsPrevPeriod = avgDailyPrev,
  403 + AvgDailyPrintsChangeRate = CalcChangeRate(avgDaily, avgDailyPrev)
  404 + },
  405 + LabelsByCategory = byCategory,
  406 + PrintVolumeTrend = trend,
  407 + MostUsedProducts = mostUsed
  408 + };
  409 + }
  410 +
  411 + /// <inheritdoc />
  412 + public async Task<IActionResult> ExportLabelReportPdfAsync(ReportsLabelReportQueryInputVo input)
  413 + {
  414 + QuestPDF.Settings.License = LicenseType.Community;
  415 + var data = await GetLabelReportAsync(input);
  416 + var fileName = $"label-report_{Clock.Now:yyyy-MM-dd_HH-mm-ss}.pdf";
  417 + var document = Document.Create(container =>
  418 + {
  419 + container.Page(page =>
  420 + {
  421 + page.Margin(24);
  422 + page.DefaultTextStyle(x => x.FontSize(9));
  423 + page.Header().Text("Label Report").SemiBold().FontSize(16);
  424 + page.Content().Column(col =>
  425 + {
  426 + col.Spacing(10);
  427 + col.Item().Text(
  428 + $"Total printed: {data.Summary.TotalLabelsPrinted} (prev: {data.Summary.TotalLabelsPrintedPrevPeriod}, Δ%: {data.Summary.TotalLabelsPrintedChangeRate:0.##}%)");
  429 + col.Item().Text(
  430 + $"Top category: {data.Summary.MostPrintedCategoryName} ({data.Summary.MostPrintedCategoryCount})");
  431 + col.Item().Text($"Top product: {data.Summary.TopProductName} ({data.Summary.TopProductCount})");
  432 + col.Item().Text(
  433 + $"Avg daily: {data.Summary.AvgDailyPrints:0.##} (prev: {data.Summary.AvgDailyPrintsPrevPeriod:0.##}, Δ%: {data.Summary.AvgDailyPrintsChangeRate:0.##}%)");
  434 + col.Item().Text("By category:").SemiBold();
  435 + col.Item().Table(t =>
  436 + {
  437 + t.ColumnsDefinition(c => { c.RelativeColumn(2); c.RelativeColumn(1); });
  438 + t.Cell().Element(HeaderCell).Text("Category");
  439 + t.Cell().Element(HeaderCell).Text("Count");
  440 + foreach (var r in data.LabelsByCategory)
  441 + {
  442 + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.CategoryName);
  443 + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Count.ToString());
  444 + }
  445 + });
  446 + col.Item().Text("Daily trend:").SemiBold();
  447 + col.Item().Table(t =>
  448 + {
  449 + t.ColumnsDefinition(c => { c.RelativeColumn(1.2f); c.RelativeColumn(1); });
  450 + t.Cell().Element(HeaderCell).Text("Date");
  451 + t.Cell().Element(HeaderCell).Text("Count");
  452 + foreach (var r in data.PrintVolumeTrend)
  453 + {
  454 + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Date);
  455 + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.Count.ToString());
  456 + }
  457 + });
  458 + col.Item().Text("Most used products:").SemiBold();
  459 + col.Item().Table(t =>
  460 + {
  461 + t.ColumnsDefinition(c =>
  462 + {
  463 + c.RelativeColumn(1.5f);
  464 + c.RelativeColumn(1f);
  465 + c.RelativeColumn(0.8f);
  466 + c.RelativeColumn(0.7f);
  467 + });
  468 + t.Cell().Element(HeaderCell).Text("Product");
  469 + t.Cell().Element(HeaderCell).Text("Category");
  470 + t.Cell().Element(HeaderCell).Text("Total");
  471 + t.Cell().Element(HeaderCell).Text("%");
  472 + foreach (var r in data.MostUsedProducts)
  473 + {
  474 + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.ProductName);
  475 + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.CategoryName);
  476 + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.TotalPrinted.ToString());
  477 + t.Cell().BorderBottom(0.5f).Padding(3).Text(r.UsagePercent.ToString("0.##"));
  478 + }
  479 + });
  480 + });
  481 + });
  482 + });
  483 +
  484 + static IContainer HeaderCell(IContainer x) =>
  485 + x.Background(Colors.Grey.Lighten3).Padding(4).DefaultTextStyle(s => s.SemiBold());
  486 +
  487 + var ms = new MemoryStream();
  488 + document.GeneratePdf(ms);
  489 + ms.Position = 0;
  490 + return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName };
  491 + }
  492 +
  493 + private ISugarQueryable<FlLabelPrintTaskDbEntity, FlLabelDbEntity, FlProductDbEntity, FlLabelCategoryDbEntity,
  494 + FlProductCategoryDbEntity, LocationAggregateRoot>
  495 + BuildReportTaskCore(
  496 + List<string>? locationIds,
  497 + bool isAdmin,
  498 + string currentUserIdStr,
  499 + string? keyword)
  500 + {
  501 + return _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>()
  502 + .LeftJoin<FlLabelDbEntity>((t, l) => t.LabelId == l.Id)
  503 + .LeftJoin<FlProductDbEntity>((t, l, p) => t.ProductId == p.Id)
  504 + .LeftJoin<FlLabelCategoryDbEntity>((t, l, p, lc) => l.LabelCategoryId == lc.Id)
  505 + .LeftJoin<FlProductCategoryDbEntity>((t, l, p, lc, pc) => p.CategoryId == pc.Id)
  506 + .LeftJoin<LocationAggregateRoot>((t, l, p, lc, pc, loc) =>
  507 + t.LocationId != null && SqlFunc.ToString(loc.Id) == t.LocationId)
  508 + .Where((t, l, p, lc, pc, loc) => !loc.IsDeleted)
  509 + .WhereIF(!isAdmin, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr)
  510 + .WhereIF(locationIds is not null, (t, l, p, lc, pc, loc) => locationIds!.Contains(t.LocationId!))
  511 + .WhereIF(!string.IsNullOrWhiteSpace(keyword),
  512 + (t, l, p, lc, pc, loc) =>
  513 + (p.ProductName != null && p.ProductName.Contains(keyword!)) ||
  514 + (lc.CategoryName != null && lc.CategoryName.Contains(keyword!)) ||
  515 + (pc.CategoryName != null && pc.CategoryName.Contains(keyword!)));
  516 + }
  517 +
  518 + private static decimal CalcChangeRate(decimal current, decimal previous)
  519 + {
  520 + if (previous == 0)
  521 + {
  522 + return current > 0 ? 100m : 0m;
  523 + }
  524 +
  525 + return Math.Round((current - previous) * 100m / previous, 2);
  526 + }
  527 +
  528 + private static decimal CalcChangeRate(int current, int previous) =>
  529 + CalcChangeRate((decimal)current, (decimal)previous);
  530 +
  531 + private async Task<List<string>?> ResolveFilteredLocationIdsAsync(string? partnerId, string? groupId,
  532 + string? locationId)
  533 + {
  534 + var locId = locationId?.Trim();
  535 + if (!string.IsNullOrWhiteSpace(locId))
  536 + {
  537 + return new List<string> { locId };
  538 + }
  539 +
  540 + var gid = groupId?.Trim();
  541 + var pid = partnerId?.Trim();
  542 +
  543 + if (string.IsNullOrWhiteSpace(pid) && string.IsNullOrWhiteSpace(gid))
  544 + {
  545 + return null;
  546 + }
  547 +
  548 + var q = _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>().Where(x => !x.IsDeleted);
  549 +
  550 + if (!string.IsNullOrWhiteSpace(gid))
  551 + {
  552 + var g = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>()
  553 + .FirstAsync(x => !x.IsDeleted && x.Id == gid);
  554 + if (g is null)
  555 + {
  556 + return new List<string>();
  557 + }
  558 +
  559 + var gName = g.GroupName?.Trim() ?? string.Empty;
  560 + var partner = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
  561 + .FirstAsync(x => !x.IsDeleted && x.Id == g.PartnerId);
  562 + var pName = partner?.PartnerName?.Trim() ?? string.Empty;
  563 + q = q.Where(x => x.GroupName == gName && x.Partner == pName);
  564 + }
  565 + else if (!string.IsNullOrWhiteSpace(pid))
  566 + {
  567 + var partner = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
  568 + .FirstAsync(x => !x.IsDeleted && x.Id == pid);
  569 + if (partner is null)
  570 + {
  571 + return new List<string>();
  572 + }
  573 +
  574 + var pName = partner.PartnerName?.Trim() ?? string.Empty;
  575 + q = q.Where(x => x.Partner == pName);
  576 + }
  577 +
  578 + var ids = await q.Select(x => SqlFunc.ToString(x.Id)).ToListAsync();
  579 + return ids;
  580 + }
  581 +
  582 + private static (DateTime rangeStart, DateTime rangeEndExcl) ResolveDateRange(DateTime? startDate,
  583 + DateTime? endDate)
  584 + {
  585 + var endDay = (endDate ?? DateTime.Today).Date;
  586 + var endExcl = endDay.AddDays(1);
  587 + var start = (startDate ?? endDay.AddDays(-29)).Date;
  588 + if (start >= endExcl)
  589 + {
  590 + start = endExcl.AddDays(-1);
  591 + }
  592 +
  593 + return (start, endExcl);
  594 + }
  595 +
  596 + private static PagedResultWithPageDto<ReportsPrintLogListItemDto> EmptyPrintLogPage(
  597 + ReportsPrintLogGetListInputVo input)
  598 + {
  599 + var pageSize = input.MaxResultCount <= 0 ? 0 : input.MaxResultCount;
  600 + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount);
  601 + return new PagedResultWithPageDto<ReportsPrintLogListItemDto>
  602 + {
  603 + PageIndex = pageIndex,
  604 + PageSize = pageSize,
  605 + TotalCount = 0,
  606 + TotalPages = 0,
  607 + Items = new List<ReportsPrintLogListItemDto>()
  608 + };
  609 + }
  610 +
  611 + private static PagedResultWithPageDto<T> BuildPagedResult<T>(int skipCount, int maxResultCount, int total,
  612 + List<T> items)
  613 + {
  614 + var pageSize = maxResultCount <= 0 ? items.Count : maxResultCount;
  615 + var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(skipCount);
  616 + var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize);
  617 + return new PagedResultWithPageDto<T>
  618 + {
  619 + PageIndex = pageIndex,
  620 + PageSize = pageSize,
  621 + TotalCount = total,
  622 + TotalPages = totalPages,
  623 + Items = items
  624 + };
  625 + }
  626 +
  627 + private async Task<Dictionary<string, string>> LoadUserNameMapAsync(List<string> userIdStrings)
  628 + {
  629 + var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  630 + if (userIdStrings.Count == 0)
  631 + {
  632 + return map;
  633 + }
  634 +
  635 + var guids = userIdStrings
  636 + .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null)
  637 + .Where(x => x.HasValue)
  638 + .Select(x => x!.Value)
  639 + .Distinct()
  640 + .ToList();
  641 + if (guids.Count == 0)
  642 + {
  643 + return map;
  644 + }
  645 +
  646 + var users = await _dbContext.SqlSugarClient.Queryable<UserAggregateRoot>()
  647 + .Where(u => !u.IsDeleted && guids.Contains(u.Id))
  648 + .Select(u => new { u.Id, u.Name, u.UserName })
  649 + .ToListAsync();
  650 +
  651 + foreach (var u in users)
  652 + {
  653 + var display = !string.IsNullOrWhiteSpace(u.Name) ? u.Name.Trim() : u.UserName.Trim();
  654 + map[u.Id.ToString()] = string.IsNullOrWhiteSpace(display) ? "无" : display;
  655 + }
  656 +
  657 + return map;
  658 + }
  659 +
  660 + private static string ResolveUserName(Dictionary<string, string> map, string? createdBy)
  661 + {
  662 + if (string.IsNullOrWhiteSpace(createdBy))
  663 + {
  664 + return "无";
  665 + }
  666 +
  667 + return map.TryGetValue(createdBy.Trim(), out var n) ? n : "无";
  668 + }
  669 +
  670 + private static string FormatLocationText(string? locName, string? locCode)
  671 + {
  672 + var n = locName?.Trim();
  673 + var c = locCode?.Trim();
  674 + if (string.IsNullOrWhiteSpace(n) && string.IsNullOrWhiteSpace(c))
  675 + {
  676 + return "无";
  677 + }
  678 +
  679 + if (string.IsNullOrWhiteSpace(c))
  680 + {
  681 + return n ?? "无";
  682 + }
  683 +
  684 + if (string.IsNullOrWhiteSpace(n))
  685 + {
  686 + return $"({c})";
  687 + }
  688 +
  689 + return $"{n} ({c})";
  690 + }
  691 +
  692 + private static string FormatTemplateDisplay(decimal w, decimal h, string? unit, string? templateName)
  693 + {
  694 + var size = FormatLabelSizeWithUnit(w, h, unit ?? "inch");
  695 + var tn = templateName?.Trim();
  696 + if (string.IsNullOrWhiteSpace(tn))
  697 + {
  698 + return size ?? "无";
  699 + }
  700 +
  701 + return string.IsNullOrWhiteSpace(size) ? tn : $"{size} {tn}";
  702 + }
  703 +
  704 + private static string? FormatLabelSizeWithUnit(decimal w, decimal h, string unit)
  705 + {
  706 + var u = (unit ?? "inch").Trim().ToLowerInvariant();
  707 + var ws = w.ToString(CultureInfo.InvariantCulture);
  708 + var hs = h.ToString(CultureInfo.InvariantCulture);
  709 + var normalizedUnit = u is "in" ? "inch" : u;
  710 + return $"{ws}x{hs}{normalizedUnit}";
  711 + }
  712 +
  713 + private static string TryExtractExpiryText(string? printInputJson)
  714 + {
  715 + if (string.IsNullOrWhiteSpace(printInputJson))
  716 + {
  717 + return "无";
  718 + }
  719 +
  720 + try
  721 + {
  722 + using var doc = JsonDocument.Parse(printInputJson);
  723 + if (doc.RootElement.ValueKind != JsonValueKind.Object)
  724 + {
  725 + return "无";
  726 + }
  727 +
  728 + foreach (var prop in doc.RootElement.EnumerateObject())
  729 + {
  730 + var key = prop.Name.Trim();
  731 + if (!key.Equals("expiryDate", StringComparison.OrdinalIgnoreCase) &&
  732 + !key.Equals("expiry", StringComparison.OrdinalIgnoreCase) &&
  733 + !key.Equals("expirationDate", StringComparison.OrdinalIgnoreCase))
  734 + {
  735 + continue;
  736 + }
  737 +
  738 + var v = prop.Value;
  739 + if (v.ValueKind == JsonValueKind.String)
  740 + {
  741 + var s = v.GetString();
  742 + return string.IsNullOrWhiteSpace(s) ? "无" : s.Trim();
  743 + }
  744 +
  745 + if (v.ValueKind == JsonValueKind.Number && v.TryGetInt64(out var n))
  746 + {
  747 + return n.ToString(CultureInfo.InvariantCulture);
  748 + }
  749 +
  750 + return v.ToString();
  751 + }
  752 + }
  753 + catch
  754 + {
  755 + return "无";
  756 + }
  757 +
  758 + return "无";
  759 + }
  760 +
  761 + private static IActionResult BuildEmptyPdf(string fileName)
  762 + {
  763 + QuestPDF.Settings.License = LicenseType.Community;
  764 + var document = Document.Create(c =>
  765 + {
  766 + c.Page(p =>
  767 + {
  768 + p.Margin(30);
  769 + p.Content().Text("No data for current filters.");
  770 + });
  771 + });
  772 + var ms = new MemoryStream();
  773 + document.GeneratePdf(ms);
  774 + ms.Position = 0;
  775 + return new FileStreamResult(ms, "application/pdf") { FileDownloadName = fileName };
  776 + }
  777 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs
1 1 using System;
2 2 using System.Collections.Generic;
  3 +using System.Globalization;
3 4 using System.IdentityModel.Tokens.Jwt;
4 5 using System.Linq;
5 6 using System.Security.Claims;
6 7 using System.Text;
7 8 using System.Threading.Tasks;
  9 +using FoodLabeling.Application.Contracts;
8 10 using FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
9 11 using FoodLabeling.Application.Contracts.IServices;
10 12 using FoodLabeling.Application.Services.DbModels;
... ... @@ -20,6 +22,7 @@ using Volo.Abp;
20 22 using Volo.Abp.Application.Services;
21 23 using Volo.Abp.EventBus.Local;
22 24 using Volo.Abp.Security.Claims;
  25 +using Volo.Abp.Uow;
23 26 using Volo.Abp.Users;
24 27 using Yi.Framework.Core.Helper;
25 28 using Yi.Framework.Rbac.Domain.Entities;
... ... @@ -135,6 +138,267 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
135 138 return await LoadBoundLocationsAsync(CurrentUser.Id.Value);
136 139 }
137 140  
  141 + /// <summary>
  142 + /// 查询单个门店详情(Location 页):地址、门店电话、营业时间占位、店长(角色含 manager 的绑定用户)
  143 + /// </summary>
  144 + /// <remarks>
  145 + /// 仅当当前登录用户在 <c>userlocation</c> 中绑定该 <c>locationId</c> 时可查;否则返回业务异常。
  146 + ///
  147 + /// 店长:在同店绑定用户中,取 <c>Role.RoleCode</c> 或 <c>Role.RoleName</c>(忽略大小写)包含 <c>manager</c> 的第一条;
  148 + /// 若无匹配则店长姓名与电话均为「无」。
  149 + ///
  150 + /// <c>OperatingHours</c>:当前 <c>location</c> 表无营业时间字段,固定返回「无」。
  151 + /// </remarks>
  152 + /// <param name="locationId">门店主键(Guid 字符串)</param>
  153 + /// <returns>与原型一致的展示字段</returns>
  154 + /// <response code="200">成功</response>
  155 + /// <response code="400">未登录、门店标识无效、未绑定或门店不存在</response>
  156 + /// <response code="500">服务器错误</response>
  157 + [Authorize]
  158 + public virtual async Task<UsAppLocationDetailOutputDto> GetLocationDetailAsync(string locationId)
  159 + {
  160 + if (!CurrentUser.Id.HasValue)
  161 + {
  162 + throw new UserFriendlyException("用户未登录");
  163 + }
  164 +
  165 + var lid = (locationId ?? string.Empty).Trim();
  166 + if (string.IsNullOrEmpty(lid) || !Guid.TryParse(lid, out var locationGuid))
  167 + {
  168 + throw new UserFriendlyException("无效的门店标识");
  169 + }
  170 +
  171 + var userIdStr = CurrentUser.Id.Value.ToString();
  172 + var bound = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>()
  173 + .AnyAsync(x => !x.IsDeleted && x.UserId == userIdStr && x.LocationId == lid);
  174 + if (!bound)
  175 + {
  176 + throw new UserFriendlyException("当前账号未绑定该门店,无法查看");
  177 + }
  178 +
  179 + var locRows = await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>()
  180 + .Where(x => !x.IsDeleted && x.Id == locationGuid)
  181 + .ToListAsync();
  182 + var loc = locRows.FirstOrDefault();
  183 + if (loc is null)
  184 + {
  185 + throw new UserFriendlyException("门店不存在或已删除");
  186 + }
  187 +
  188 + var (mgrName, mgrPhone) = await TryResolveStoreManagerAsync(lid);
  189 +
  190 + return new UsAppLocationDetailOutputDto
  191 + {
  192 + LocationId = loc.Id.ToString(),
  193 + LocationName = string.IsNullOrWhiteSpace(loc.LocationName) ? "无" : loc.LocationName.Trim(),
  194 + FullAddress = BuildFullAddress(loc),
  195 + StorePhone = FormatStorePhoneDisplay(loc.Phone),
  196 + OperatingHours = "无",
  197 + ManagerName = mgrName,
  198 + ManagerPhone = mgrPhone
  199 + };
  200 + }
  201 +
  202 + /// <inheritdoc />
  203 + [Authorize]
  204 + public virtual async Task<UsAppMyProfileOutputDto> GetMyProfileAsync()
  205 + {
  206 + if (!CurrentUser.Id.HasValue)
  207 + {
  208 + throw new UserFriendlyException("用户未登录");
  209 + }
  210 +
  211 + var userId = CurrentUser.Id.Value;
  212 + var user = await _userRepository.GetByIdAsync(userId);
  213 + if (user is null || user.IsDeleted || !user.State)
  214 + {
  215 + throw new UserFriendlyException("用户不存在或已停用");
  216 + }
  217 +
  218 + // 避免 SqlSugar 在该环境下对 Role 关联表达式解析异常(Select 不支持),这里改用显式 SQL 查询角色。
  219 + var roleRows = await _dbContext.SqlSugarClient.Ado.SqlQueryAsync<MyProfileRoleRow>(
  220 + @"SELECT r.RoleName, r.RoleCode
  221 + FROM UserRole ur
  222 + INNER JOIN Role r ON ur.RoleId = r.Id
  223 + WHERE ur.UserId = @UserId AND r.IsDeleted = 0 AND r.State = 1
  224 + ORDER BY r.OrderNum ASC",
  225 + new { UserId = userId });
  226 +
  227 + var roleNames = roleRows.Select(x => x.RoleName?.Trim()).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
  228 + var roleDisplay = roleNames.Count == 0 ? "无" : string.Join(", ", roleNames);
  229 + var primaryCode = roleRows.FirstOrDefault()?.RoleCode?.Trim();
  230 +
  231 + var fullName = !string.IsNullOrWhiteSpace(user.Name?.Trim())
  232 + ? user.Name.Trim()
  233 + : (!string.IsNullOrWhiteSpace(user.Nick?.Trim()) ? user.Nick.Trim() : user.UserName.Trim());
  234 +
  235 + return new UsAppMyProfileOutputDto
  236 + {
  237 + FullName = fullName,
  238 + Email = string.IsNullOrWhiteSpace(user.Email) ? "无" : user.Email.Trim(),
  239 + Phone = FormatPhoneDisplay(user.Phone),
  240 + EmployeeId = string.IsNullOrWhiteSpace(user.UserName) ? "无" : user.UserName.Trim(),
  241 + RoleDisplay = roleDisplay,
  242 + PrimaryRoleCode = string.IsNullOrWhiteSpace(primaryCode) ? null : primaryCode
  243 + };
  244 + }
  245 +
  246 + /// <inheritdoc />
  247 + [Authorize]
  248 + [UnitOfWork]
  249 + public virtual async Task ChangePasswordAsync(UsAppChangePasswordInputVo input)
  250 + {
  251 + if (input is null)
  252 + {
  253 + throw new UserFriendlyException("入参不能为空");
  254 + }
  255 +
  256 + if (!CurrentUser.Id.HasValue)
  257 + {
  258 + throw new UserFriendlyException("用户未登录");
  259 + }
  260 +
  261 + var current = input.CurrentPassword ?? string.Empty;
  262 + var newPwd = input.NewPassword ?? string.Empty;
  263 + var confirm = input.ConfirmNewPassword ?? string.Empty;
  264 +
  265 + if (string.IsNullOrWhiteSpace(current) || string.IsNullOrWhiteSpace(newPwd) || string.IsNullOrWhiteSpace(confirm))
  266 + {
  267 + throw new UserFriendlyException("请填写当前密码、新密码与确认密码");
  268 + }
  269 +
  270 + if (!string.Equals(newPwd, confirm, StringComparison.Ordinal))
  271 + {
  272 + throw new UserFriendlyException("新密码与确认密码不一致");
  273 + }
  274 +
  275 + if (string.Equals(current, newPwd, StringComparison.Ordinal))
  276 + {
  277 + throw new UserFriendlyException("新密码不能与当前密码相同");
  278 + }
  279 +
  280 + ValidateAppPasswordComplexity(newPwd);
  281 +
  282 + var userId = CurrentUser.Id.Value;
  283 + var user = await _userRepository.GetByIdAsync(userId);
  284 + if (user is null || user.IsDeleted || !user.State)
  285 + {
  286 + throw new UserFriendlyException("用户不存在或已停用");
  287 + }
  288 +
  289 + if (!user.JudgePassword(current))
  290 + {
  291 + throw new UserFriendlyException(UserConst.Login_Error);
  292 + }
  293 +
  294 + user.EncryPassword.Password = newPwd;
  295 + user.BuildPassword();
  296 + await _userRepository.UpdateAsync(user);
  297 + }
  298 +
  299 + private static string FormatPhoneDisplay(long? phone)
  300 + {
  301 + if (!phone.HasValue)
  302 + {
  303 + return "无";
  304 + }
  305 +
  306 + var digits = Math.Abs(phone.Value).ToString(CultureInfo.InvariantCulture);
  307 + if (digits.Length == 10)
  308 + {
  309 + return $"+1 ({digits[..3]}) {digits.Substring(3, 3)}-{digits.Substring(6, 4)}";
  310 + }
  311 +
  312 + if (digits.Length == 11 && digits.StartsWith("1", StringComparison.Ordinal))
  313 + {
  314 + return $"+1 ({digits[1..4]}) {digits.Substring(4, 3)}-{digits.Substring(7, 4)}";
  315 + }
  316 +
  317 + return $"+{digits}";
  318 + }
  319 +
  320 + private static string FormatStorePhoneDisplay(string? phone)
  321 + {
  322 + var t = phone?.Trim();
  323 + return string.IsNullOrEmpty(t) ? "无" : t;
  324 + }
  325 +
  326 + private async Task<(string Name, string Phone)> TryResolveStoreManagerAsync(string locationIdTrimmed)
  327 + {
  328 + var userIdStrings = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>()
  329 + .Where(x => !x.IsDeleted && x.LocationId == locationIdTrimmed)
  330 + .Select(x => x.UserId)
  331 + .Distinct()
  332 + .ToListAsync();
  333 +
  334 + var userGuids = userIdStrings
  335 + .Select(s => Guid.TryParse(s, out var g) ? (Guid?)g : null)
  336 + .Where(g => g.HasValue)
  337 + .Select(g => g!.Value)
  338 + .ToList();
  339 +
  340 + if (userGuids.Count == 0)
  341 + {
  342 + return ("无", "无");
  343 + }
  344 +
  345 + var rows = await _dbContext.SqlSugarClient.Ado.SqlQueryAsync<LocationManagerRow>(
  346 + @"SELECT u.Name, u.Nick, u.UserName, u.Phone
  347 + FROM User u
  348 + INNER JOIN UserRole ur ON u.Id = ur.UserId
  349 + INNER JOIN Role r ON ur.RoleId = r.Id
  350 + WHERE u.IsDeleted = 0
  351 + AND u.State = 1
  352 + AND r.IsDeleted = 0
  353 + AND r.State = 1
  354 + AND (LOWER(r.RoleCode) LIKE '%manager%' OR LOWER(r.RoleName) LIKE '%manager%')
  355 + AND u.Id IN (@UserIds)
  356 + ORDER BY u.Name ASC",
  357 + new { UserIds = userGuids });
  358 +
  359 + var row = rows.FirstOrDefault();
  360 + if (row is null)
  361 + {
  362 + return ("无", "无");
  363 + }
  364 +
  365 + var displayName = !string.IsNullOrWhiteSpace(row.Name?.Trim())
  366 + ? row.Name!.Trim()
  367 + : (!string.IsNullOrWhiteSpace(row.Nick?.Trim())
  368 + ? row.Nick!.Trim()
  369 + : (row.UserName?.Trim() ?? "无"));
  370 +
  371 + return (displayName, FormatPhoneDisplay(row.Phone));
  372 + }
  373 +
  374 + private static void ValidateAppPasswordComplexity(string password)
  375 + {
  376 + if (password.Length < 8)
  377 + {
  378 + throw new UserFriendlyException("新密码至少 8 位");
  379 + }
  380 +
  381 + if (!password.Any(char.IsUpper))
  382 + {
  383 + throw new UserFriendlyException("新密码需包含大写字母");
  384 + }
  385 +
  386 + if (!password.Any(char.IsLower))
  387 + {
  388 + throw new UserFriendlyException("新密码需包含小写字母");
  389 + }
  390 +
  391 + if (!password.Any(char.IsDigit))
  392 + {
  393 + throw new UserFriendlyException("新密码需包含至少一个数字");
  394 + }
  395 +
  396 + if (!password.Any(c => !char.IsLetterOrDigit(c)))
  397 + {
  398 + throw new UserFriendlyException("新密码需包含至少一个特殊字符");
  399 + }
  400 + }
  401 +
138 402 private void ValidationImageCaptcha(string? uuid, string? code)
139 403 {
140 404 if (!_rbacOptions.EnableCaptcha)
... ... @@ -149,16 +413,21 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
149 413 }
150 414  
151 415 /// <summary>
152   - /// 按邮箱查找未删除且启用的用户(邮箱比较忽略大小写)
  416 + /// 按邮箱或用户名(邮箱形字符串写在 UserName 时)查找未删除且启用的用户;比较忽略大小写,Email 命中优先。
153 417 /// </summary>
154 418 private async Task<UserAggregateRoot?> FindActiveUserByEmailAsync(string email)
155 419 {
156 420 var normalized = email.Trim().ToLowerInvariant();
157 421 var users = await _userRepository._DbQueryable
158 422 .Where(u => !u.IsDeleted && u.State == true)
159   - .Where(u => u.Email != null && SqlFunc.ToLower(u.Email) == normalized)
  423 + .Where(u =>
  424 + (u.Email != null && SqlFunc.ToLower(u.Email) == normalized) ||
  425 + SqlFunc.ToLower(u.UserName) == normalized)
160 426 .ToListAsync();
161   - return users.FirstOrDefault();
  427 + return users.FirstOrDefault(u =>
  428 + u.Email != null &&
  429 + string.Equals(u.Email.Trim(), normalized, StringComparison.OrdinalIgnoreCase))
  430 + ?? users.FirstOrDefault();
162 431 }
163 432  
164 433 private string CreateAppAccessToken(UserAggregateRoot user)
... ... @@ -169,7 +438,8 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
169 438 var claims = new List<Claim>
170 439 {
171 440 new(AbpClaimTypes.UserId, user.Id.ToString()),
172   - new(AbpClaimTypes.UserName, user.UserName)
  441 + new(AbpClaimTypes.UserName, user.UserName),
  442 + new(UsAppJwtClaims.ClientKind, UsAppJwtClaims.ClientKindUsApp)
173 443 };
174 444  
175 445 if (!string.IsNullOrWhiteSpace(user.Email))
... ... @@ -257,4 +527,22 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
257 527  
258 528 return segments.Count == 0 ? "无" : string.Join(", ", segments);
259 529 }
  530 +
  531 + private sealed class MyProfileRoleRow
  532 + {
  533 + public string? RoleName { get; set; }
  534 +
  535 + public string? RoleCode { get; set; }
  536 + }
  537 +
  538 + private sealed class LocationManagerRow
  539 + {
  540 + public string? Name { get; set; }
  541 +
  542 + public string? Nick { get; set; }
  543 +
  544 + public string? UserName { get; set; }
  545 +
  546 + public long? Phone { get; set; }
  547 + }
260 548 }
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs
... ... @@ -50,8 +50,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
50 50 /// 获取当前门店下四级嵌套数据
51 51 /// </summary>
52 52 /// <remarks>
53   - /// L1 标签分类 fl_label_category;L2 产品分类 fl_product.CategoryId join fl_product_category;
  53 + /// L1 标签分类 fl_label_category(含 buttonAppearance;COLOR/IMAGE 展示值在 categoryPhotoUrl);仅对当前门店可用:ALL 或 SPECIFIED 且在 fl_label_category_location;
  54 + /// L2 产品分类 fl_product.CategoryId join fl_product_category(同上,展示值在 categoryPhotoUrl);
54 55 /// L3 产品;L4 与该门店、该标签分类、该产品关联的标签实例(fl_label + fl_label_type)。
  56 + /// L2 仅包含对当前门店可用的类别:AvailabilityType=ALL,或 SPECIFIED 且在 fl_product_category_location 存在该门店记录;
  57 + /// 未归类或分类行未关联到 fl_product_category 时仍归入「无」节点。
55 58 /// </remarks>
56 59 [Authorize]
57 60 public virtual async Task<List<UsAppLabelCategoryTreeNodeDto>> GetLabelingTreeAsync(UsAppLabelingTreeInputVo input)
... ... @@ -83,10 +86,15 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
83 86 LabelCategoryId = c.Id,
84 87 LabelCategoryName = c.CategoryName,
85 88 LabelCategoryPhotoUrl = c.CategoryPhotoUrl,
  89 + LabelCategoryButtonAppearance = c.ButtonAppearance,
86 90 LabelCategoryOrderNum = c.OrderNum,
87 91 ProductCategoryId = p.CategoryId,
88 92 ProductCategoryName = pc.CategoryName,
89 93 ProductCategoryPhotoUrl = pc.CategoryPhotoUrl,
  94 + ProductCategoryDisplayText = pc.DisplayText,
  95 + ProductCategoryButtonAppearance = pc.ButtonAppearance,
  96 + ProductCategoryAvailabilityType = pc.AvailabilityType,
  97 + ProductCategoryOrderNum = pc.OrderNum,
90 98 ProductId = p.Id,
91 99 ProductName = p.ProductName,
92 100 ProductCode = p.ProductCode,
... ... @@ -112,17 +120,22 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
112 120 x.LabelCategoryId,
113 121 x.LabelCategoryName,
114 122 x.LabelCategoryPhotoUrl,
  123 + x.LabelCategoryButtonAppearance,
115 124 x.LabelCategoryOrderNum
116 125 }).OrderBy(g => g.Key.LabelCategoryOrderNum).ThenBy(g => g.Key.LabelCategoryName);
117 126  
118 127 var result = new List<UsAppLabelCategoryTreeNodeDto>();
119 128 foreach (var g1 in byL1)
120 129 {
  130 + var l1Appearance = string.IsNullOrWhiteSpace(g1.Key.LabelCategoryButtonAppearance)
  131 + ? "TEXT"
  132 + : g1.Key.LabelCategoryButtonAppearance.Trim();
121 133 var l1 = new UsAppLabelCategoryTreeNodeDto
122 134 {
123 135 Id = g1.Key.LabelCategoryId,
124 136 CategoryName = g1.Key.LabelCategoryName ?? string.Empty,
125 137 CategoryPhotoUrl = g1.Key.LabelCategoryPhotoUrl,
  138 + ButtonAppearance = l1Appearance,
126 139 OrderNum = g1.Key.LabelCategoryOrderNum,
127 140 ProductCategories = new List<UsAppProductCategoryNodeDto>()
128 141 };
... ... @@ -136,7 +149,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
136 149 {
137 150 CategoryId = (string?)null,
138 151 CategoryName = "无",
139   - CategoryPhotoUrl = (string?)null
  152 + CategoryPhotoUrl = (string?)null,
  153 + DisplayText = (string?)null,
  154 + ButtonAppearance = (string?)null,
  155 + AvailabilityType = (string?)null,
  156 + CategoryOrderNum = int.MaxValue
140 157 };
141 158 }
142 159  
... ... @@ -146,19 +163,34 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
146 163 {
147 164 CategoryId = (string?)categoryId,
148 165 CategoryName = categoryName,
149   - CategoryPhotoUrl = categoryPhotoUrl
  166 + CategoryPhotoUrl = categoryPhotoUrl,
  167 + DisplayText = NormalizeNullableUrl(x.ProductCategoryDisplayText),
  168 + ButtonAppearance = NormalizeNullableId(x.ProductCategoryButtonAppearance),
  169 + AvailabilityType = NormalizeNullableId(x.ProductCategoryAvailabilityType),
  170 + CategoryOrderNum = x.ProductCategoryOrderNum
150 171 };
151 172 })
152   - .OrderBy(g => g.Key.CategoryName);
  173 + .OrderBy(g => g.Key.CategoryOrderNum)
  174 + .ThenBy(g => g.Key.CategoryName);
153 175  
154 176 foreach (var g2 in byL2)
155 177 {
156 178 var productsGrouped = g2.GroupBy(x => x.ProductId).OrderBy(pg => pg.First().ProductName);
  179 + var appearance = string.IsNullOrWhiteSpace(g2.Key.ButtonAppearance)
  180 + ? "TEXT"
  181 + : g2.Key.ButtonAppearance.Trim();
  182 + var availability = string.IsNullOrWhiteSpace(g2.Key.AvailabilityType)
  183 + ? "ALL"
  184 + : g2.Key.AvailabilityType.Trim().ToUpperInvariant();
157 185 var l2 = new UsAppProductCategoryNodeDto
158 186 {
159 187 CategoryId = g2.Key.CategoryId,
160 188 CategoryPhotoUrl = g2.Key.CategoryPhotoUrl,
161 189 Name = g2.Key.CategoryName,
  190 + DisplayText = g2.Key.DisplayText,
  191 + ButtonAppearance = appearance,
  192 + AvailabilityType = availability,
  193 + OrderNum = g2.Key.CategoryOrderNum == int.MaxValue ? 0 : g2.Key.CategoryOrderNum,
162 194 ItemCount = productsGrouped.Count(),
163 195 Products = new List<UsAppLabelingProductNodeDto>()
164 196 };
... ... @@ -424,7 +456,7 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
424 456 }
425 457  
426 458 var previewProductId = await ResolvePreviewProductIdAsync(labelRow.Id, input.ProductId);
427   - var normalizedPrintInput = input.PrintInputJson?.ToDictionary(x => x.Key, x => (object?)x.Value);
  459 + var normalizedPrintInput = ParsePrintInputJsonToDictionary(input.PrintInputJson);
428 460  
429 461 // 解析模板 elements(与预览一致的渲染数据)
430 462 var resolvedTemplate = await _labelAppService.PreviewAsync(new LabelPreviewResolveInputVo
... ... @@ -581,8 +613,9 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
581 613 throw new UserFriendlyException("打印任务不存在");
582 614 }
583 615  
584   - // 仅允许重打自己在当前门店的任务
585   - if (!string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase))
  616 + // 非 admin:仅允许重打自己在当前门店的任务;admin 可重打任意用户任务(仍须门店一致)
  617 + var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser);
  618 + if (!isAdmin && !string.Equals(old.CreatedBy?.Trim(), currentUserId, StringComparison.OrdinalIgnoreCase))
586 619 {
587 620 throw new UserFriendlyException("无权限重打该任务");
588 621 }
... ... @@ -852,14 +885,29 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
852 885 .Where((lp, l, p, c, t, tpl, pc) => l.LocationId == locationId)
853 886 .Where((lp, l, p, c, t, tpl, pc) => !l.IsDeleted && l.State)
854 887 .Where((lp, l, p, c, t, tpl, pc) => !p.IsDeleted && p.State)
855   - .Where((lp, l, p, c, t, tpl, pc) => !c.IsDeleted && c.State)
  888 + .Where((lp, l, p, c, t, tpl, pc) =>
  889 + !c.IsDeleted && c.State &&
  890 + (c.AvailabilityType == "ALL" ||
  891 + (c.AvailabilityType == "SPECIFIED" &&
  892 + SqlFunc.Subqueryable<FlLabelCategoryLocationDbEntity>()
  893 + .Where(loc => loc.CategoryId == c.Id && loc.LocationId == locationId)
  894 + .Any())))
856 895 .Where((lp, l, p, c, t, tpl, pc) => !t.IsDeleted && t.State)
857 896 .Where((lp, l, p, c, t, tpl, pc) => !tpl.IsDeleted)
  897 + .Where((lp, l, p, c, t, tpl, pc) =>
  898 + pc.Id == null ||
  899 + (!pc.IsDeleted && pc.State &&
  900 + (pc.AvailabilityType == "ALL" ||
  901 + (pc.AvailabilityType == "SPECIFIED" &&
  902 + SqlFunc.Subqueryable<FlProductCategoryLocationDbEntity>()
  903 + .Where(loc => loc.CategoryId == pc.Id && loc.LocationId == locationId)
  904 + .Any()))))
858 905 .WhereIF(!string.IsNullOrWhiteSpace(filterCategoryId), (lp, l, p, c, t, tpl, pc) => l.LabelCategoryId == filterCategoryId)
859 906 .WhereIF(!string.IsNullOrWhiteSpace(keyword), (lp, l, p, c, t, tpl, pc) =>
860 907 (l.LabelName != null && l.LabelName.Contains(keyword!)) ||
861 908 (p.ProductName != null && p.ProductName.Contains(keyword!)) ||
862 909 (pc.CategoryName != null && pc.CategoryName.Contains(keyword!)) ||
  910 + (pc.DisplayText != null && pc.DisplayText.Contains(keyword!)) ||
863 911 (c.CategoryName != null && c.CategoryName.Contains(keyword!)) ||
864 912 (t.TypeName != null && t.TypeName.Contains(keyword!)) ||
865 913 (l.LabelCode != null && l.LabelCode.Contains(keyword!)));
... ... @@ -875,6 +923,8 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
875 923  
876 924 public string? LabelCategoryPhotoUrl { get; set; }
877 925  
  926 + public string? LabelCategoryButtonAppearance { get; set; }
  927 +
878 928 public int LabelCategoryOrderNum { get; set; }
879 929  
880 930 public string? ProductCategoryId { get; set; }
... ... @@ -883,6 +933,14 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
883 933  
884 934 public string? ProductCategoryPhotoUrl { get; set; }
885 935  
  936 + public string? ProductCategoryDisplayText { get; set; }
  937 +
  938 + public string? ProductCategoryButtonAppearance { get; set; }
  939 +
  940 + public string? ProductCategoryAvailabilityType { get; set; }
  941 +
  942 + public int ProductCategoryOrderNum { get; set; }
  943 +
886 944 public string ProductId { get; set; } = string.Empty;
887 945  
888 946 public string? ProductName { get; set; }
... ... @@ -908,6 +966,32 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
908 966 public string TemplateUnit { get; set; } = "inch";
909 967 }
910 968  
  969 + /// <summary>
  970 + /// 将 App 入参中的 JsonElement(对象或 null)反序列化为 PreviewAsync 所需的扁平字典。
  971 + /// </summary>
  972 + private static Dictionary<string, object?>? ParsePrintInputJsonToDictionary(JsonElement? printInputJson)
  973 + {
  974 + if (printInputJson is null)
  975 + {
  976 + return null;
  977 + }
  978 +
  979 + var je = printInputJson.Value;
  980 + if (je.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
  981 + {
  982 + return null;
  983 + }
  984 +
  985 + try
  986 + {
  987 + return JsonSerializer.Deserialize<Dictionary<string, object?>>(je.GetRawText());
  988 + }
  989 + catch
  990 + {
  991 + return null;
  992 + }
  993 + }
  994 +
911 995 private static string NormalizeCategoryName(string? categoryName)
912 996 {
913 997 var s = categoryName?.Trim();
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_group_create.sql 0 → 100644
  1 +-- 组织/分组(Group),归属合作伙伴(Parent Partner)
  2 +-- 依赖:请先执行 fl_partner_create.sql,保证存在 fl_partner 表。
  3 +
  4 +CREATE TABLE IF NOT EXISTS `fl_group` (
  5 + `Id` varchar(64) NOT NULL COMMENT '主键',
  6 + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
  7 + `CreationTime` datetime(6) NOT NULL COMMENT '创建时间',
  8 + `CreatorId` varchar(64) DEFAULT NULL COMMENT '创建人',
  9 + `LastModificationTime` datetime(6) DEFAULT NULL COMMENT '最后修改时间',
  10 + `LastModifierId` varchar(64) DEFAULT NULL COMMENT '最后修改人',
  11 + `GroupName` varchar(256) NOT NULL COMMENT '组织名称',
  12 + `PartnerId` varchar(64) NOT NULL COMMENT '所属合作伙伴 fl_partner.Id',
  13 + `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
  14 + PRIMARY KEY (`Id`),
  15 + KEY `IX_fl_group_IsDeleted` (`IsDeleted`),
  16 + KEY `IX_fl_group_State` (`State`),
  17 + KEY `IX_fl_group_PartnerId` (`PartnerId`),
  18 + KEY `IX_fl_group_GroupName` (`GroupName`(128)),
  19 + KEY `IX_fl_group_CreationTime` (`CreationTime`),
  20 + CONSTRAINT `FK_fl_group_partner` FOREIGN KEY (`PartnerId`) REFERENCES `fl_partner` (`Id`)
  21 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织(Group)';
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_alter_drop_locationid.sql 0 → 100644
  1 +-- 从旧版「按门店」结构迁移为「全局一条」:删除 LocationId 及门店唯一索引
  2 +-- 执行前请确认库中 `fl_location_support` 已存在;若不存在可跳过本脚本,直接使用 fl_location_support_create.sql
  3 +
  4 +-- 若存在按门店的唯一索引则删除(名称与历史脚本一致)
  5 +ALTER TABLE `fl_location_support` DROP INDEX `uk_fl_location_support_locationid`;
  6 +
  7 +-- 删除门店列(若列不存在会报错,需按环境调整)
  8 +ALTER TABLE `fl_location_support` DROP COLUMN `LocationId`;
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_create.sql 0 → 100644
  1 +-- 全局 Support 联系方式(全门店共用一条)
  2 +CREATE TABLE IF NOT EXISTS `fl_location_support` (
  3 + `Id` varchar(50) NOT NULL COMMENT '主键',
  4 + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
  5 + `CreationTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  6 + `CreatorId` varchar(50) DEFAULT NULL COMMENT '创建人',
  7 + `LastModifierId` varchar(50) DEFAULT NULL COMMENT '最后修改人',
  8 + `LastModificationTime` datetime DEFAULT NULL COMMENT '最后修改时间',
  9 + `SupportPhone` varchar(100) NOT NULL COMMENT 'Support 电话',
  10 + `SupportEmail` varchar(200) NOT NULL COMMENT 'Support 邮箱',
  11 + PRIMARY KEY (`Id`),
  12 + KEY `idx_fl_location_support_isdeleted` (`IsDeleted`)
  13 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='全局 Support 联系方式';
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql 0 → 100644
  1 +-- 合作伙伴主数据(美国版 antis-foodlabeling-us)
  2 +-- 执行前请确认连接库为业务库;若表已存在请勿重复执行。
  3 +
  4 +CREATE TABLE IF NOT EXISTS `fl_partner` (
  5 + `Id` varchar(64) NOT NULL COMMENT '主键',
  6 + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
  7 + `CreationTime` datetime(6) NOT NULL COMMENT '创建时间',
  8 + `CreatorId` varchar(64) DEFAULT NULL COMMENT '创建人',
  9 + `LastModificationTime` datetime(6) DEFAULT NULL COMMENT '最后修改时间',
  10 + `LastModifierId` varchar(64) DEFAULT NULL COMMENT '最后修改人',
  11 + `PartnerName` varchar(256) NOT NULL COMMENT '合作伙伴名称',
  12 + `ContactEmail` varchar(256) DEFAULT NULL COMMENT '联系邮箱',
  13 + `PhoneNumber` varchar(64) DEFAULT NULL COMMENT '电话',
  14 + `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
  15 + PRIMARY KEY (`Id`),
  16 + KEY `IX_fl_partner_IsDeleted` (`IsDeleted`),
  17 + KEY `IX_fl_partner_State` (`State`),
  18 + KEY `IX_fl_partner_CreationTime` (`CreationTime`),
  19 + KEY `IX_fl_partner_PartnerName` (`PartnerName`(128))
  20 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合作伙伴';
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application.Contracts/IServices/IAccountService.cs
... ... @@ -14,12 +14,6 @@ namespace Yi.Framework.Rbac.Application.Contracts.IServices
14 14 Task<bool> RestPasswordAsync(Guid userId, RestPasswordDto input);
15 15  
16 16 /// <summary>
17   - /// 提供其他服务使用,根据用户id,直接返回token
18   - /// </summary>
19   - /// <returns></returns>
20   - Task<LoginOutputDto> PostLoginAsync(Guid userId);
21   -
22   - /// <summary>
23 17 /// 根据信息查询用户,可能为空,代表该用户不存在或禁用
24 18 /// </summary>
25 19 /// <param name="userName"></param>
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/ObjectMapping/RbacMapsterRegister.cs 0 → 100644
  1 +using Mapster;
  2 +using Yi.Framework.Rbac.Application.Contracts.Dtos.Menu;
  3 +using Yi.Framework.Rbac.Domain.Entities;
  4 +using Yi.Framework.Rbac.Domain.Shared;
  5 +using Yi.Framework.Rbac.Domain.Shared.Dtos;
  6 +
  7 +namespace Yi.Framework.Rbac.Application.ObjectMapping;
  8 +
  9 +/// <summary>
  10 +/// <c>Menu.ParentId</c> 实体为字符串,对外 DTO 仍为 <see cref="Guid"/>。
  11 +/// </summary>
  12 +public class RbacMapsterRegister : IRegister
  13 +{
  14 + public void Register(TypeAdapterConfig config)
  15 + {
  16 + config.NewConfig<MenuAggregateRoot, MenuGetOutputDto>()
  17 + .Map(d => d.ParentId, s => MenuParentIdConverter.ToGuid(s.ParentId));
  18 +
  19 + config.NewConfig<MenuAggregateRoot, MenuGetListOutputDto>()
  20 + .Map(d => d.ParentId, s => MenuParentIdConverter.ToGuid(s.ParentId));
  21 +
  22 + // 登录组装用户信息时使用;库中 ParentId 可能为 "0",不能交给 Mapster 默认 string→Guid
  23 + config.NewConfig<MenuAggregateRoot, MenuDto>()
  24 + .Map(d => d.ParentId, s => MenuParentIdConverter.ToGuid(s.ParentId));
  25 + }
  26 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Application/Services/AccountService.cs
... ... @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Http;
6 6 using Microsoft.AspNetCore.Mvc;
7 7 using Microsoft.Extensions.Caching.Distributed;
8 8 using Microsoft.Extensions.Options;
  9 +using SqlSugar;
9 10 using Volo.Abp.Application.Services;
10 11 using Volo.Abp.Authorization;
11 12 using Volo.Abp.Caching;
... ... @@ -83,7 +84,7 @@ namespace Yi.Framework.Rbac.Application.Services
83 84 //登录不想要验证码 ,可不校验
84 85 if (!_captcha.Validate(uuid, code))
85 86 {
86   - throw new UserFriendlyException("验证码错误");
  87 + throw new UserFriendlyException("Invalid captcha.");
87 88 }
88 89 }
89 90 }
... ... @@ -97,21 +98,52 @@ namespace Yi.Framework.Rbac.Application.Services
97 98 [AllowAnonymous]
98 99 public async Task<LoginOutputDto> PostLoginAsync(LoginInputVo input)
99 100 {
100   - if (string.IsNullOrEmpty(input.Password) || string.IsNullOrEmpty(input.UserName))
  101 + var email = input.UserName?.Trim();
  102 + var password = input.Password ?? string.Empty;
  103 + if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(password))
101 104 {
102   - throw new UserFriendlyException("请输入合理数据!");
  105 + throw new UserFriendlyException("Email and password are required.");
  106 + }
  107 +
  108 + if (!IsPlausibleEmail(email))
  109 + {
  110 + // Platform sign-in supports email only.
  111 + throw new UserFriendlyException("Sign-in failed: email is required as the account.");
103 112 }
104 113  
105 114 //校验验证码
106 115 ValidationImageCaptcha(input.Uuid,input.Code);
107 116  
108   - UserAggregateRoot user = new();
109   - //校验
110   - await _accountManager.LoginValidationAsync(input.UserName, input.Password, x => user = x);
  117 + var normalized = email.ToLowerInvariant();
  118 + // 平台登录框为「邮箱」:优先按 Email 匹配;若历史账号仅把邮箱形字符串写在 UserName、Email 为空,则按 UserName 匹配。
  119 + var candidates = await _userRepository._DbQueryable
  120 + .Where(u => !u.IsDeleted && u.State == true)
  121 + .Where(u =>
  122 + (u.Email != null && SqlFunc.ToLower(u.Email) == normalized) ||
  123 + SqlFunc.ToLower(u.UserName) == normalized)
  124 + .ToListAsync();
  125 + var user = candidates.FirstOrDefault(u =>
  126 + u.Email != null &&
  127 + string.Equals(u.Email.Trim(), normalized, StringComparison.OrdinalIgnoreCase))
  128 + ?? candidates.FirstOrDefault();
  129 + if (user is null)
  130 + {
  131 + throw new UserFriendlyException("Sign-in failed: account not found.");
  132 + }
  133 +
  134 + if (!user.JudgePassword(password))
  135 + {
  136 + throw new UserFriendlyException("Sign-in failed: incorrect email or password.");
  137 + }
111 138  
112 139 return await PostLoginAsync(user.Id);
113 140 }
114 141  
  142 + private static bool IsPlausibleEmail(string email) =>
  143 + email.Contains("@", StringComparison.Ordinal) &&
  144 + !email.StartsWith("@", StringComparison.Ordinal) &&
  145 + !email.EndsWith("@", StringComparison.Ordinal);
  146 +
115 147  
116 148 /// <summary>
117 149 /// 提供其他服务使用,根据用户id,直接返回token
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain.Shared/MenuParentIdConverter.cs 0 → 100644
  1 +namespace Yi.Framework.Rbac.Domain.Shared;
  2 +
  3 +/// <summary>
  4 +/// <c>Menu.ParentId</c> 在部分库中为 <c>varchar</c>(如根节点为 <c>0</c>),与 ORM 中 Guid 映射不一致时会导致 SqlSugar 绑定失败;统一用字符串落库并在需要时转为 <see cref="Guid"/>。
  5 +/// </summary>
  6 +public static class MenuParentIdConverter
  7 +{
  8 + public static bool IsRoot(string? raw) =>
  9 + string.IsNullOrWhiteSpace(raw) ||
  10 + raw.Trim() == "0" ||
  11 + string.Equals(raw.Trim(), Guid.Empty.ToString(), StringComparison.OrdinalIgnoreCase);
  12 +
  13 + public static Guid ToGuid(string? raw)
  14 + {
  15 + if (IsRoot(raw))
  16 + {
  17 + return Guid.Empty;
  18 + }
  19 +
  20 + var t = raw!.Trim();
  21 + return Guid.TryParse(t, out var g) ? g : Guid.Empty;
  22 + }
  23 +
  24 + /// <summary>写入数据库:根节点与历史库对齐为 <c>0</c>,否则为标准 GUID 字符串。</summary>
  25 + public static string FromGuid(Guid g) => g == Guid.Empty ? "0" : g.ToString("D");
  26 +}
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Entities/MenuAggregateRoot.cs
1   -using System.Text.RegularExpressions;
  1 +using System.Text.RegularExpressions;
2 2 using System.Web;
3 3 using NUglify.Helpers;
4 4 using SqlSugar;
... ... @@ -7,6 +7,7 @@ using Volo.Abp.Auditing;
7 7 using Volo.Abp.Domain.Entities;
8 8 using Yi.Framework.Core.Data;
9 9 using Yi.Framework.Core.Helper;
  10 +using Yi.Framework.Rbac.Domain.Shared;
10 11 using Yi.Framework.Rbac.Domain.Shared.Dtos;
11 12 using Yi.Framework.Rbac.Domain.Shared.Enums;
12 13  
... ... @@ -25,13 +26,13 @@ namespace Yi.Framework.Rbac.Domain.Entities
25 26 public MenuAggregateRoot(Guid id)
26 27 {
27 28 Id = id;
28   - ParentId = Guid.Empty;
  29 + ParentId = MenuParentIdConverter.FromGuid(Guid.Empty);
29 30 }
30 31  
31 32 public MenuAggregateRoot(Guid id, Guid parentId)
32 33 {
33 34 Id = id;
34   - ParentId = parentId;
  35 + ParentId = MenuParentIdConverter.FromGuid(parentId);
35 36 }
36 37  
37 38 /// <summary>
... ... @@ -100,8 +101,9 @@ namespace Yi.Framework.Rbac.Domain.Entities
100 101 /// <summary>
101 102 ///
102 103 ///</summary>
  104 + /// <summary>父级菜单 Id;库中多为 varchar(如根为 <c>0</c> 或 GUID 文本)。</summary>
103 105 [SugarColumn(ColumnName = "ParentId")]
104   - public Guid ParentId { get; set; }
  106 + public string ParentId { get; set; } = "0";
105 107  
106 108 /// <summary>
107 109 /// 菜单图标
... ... @@ -183,7 +185,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
183 185 r.OrderNum = m.OrderNum;
184 186 var routerName = m.Router?.Split("/").LastOrDefault();
185 187 r.Id = m.Id;
186   - r.ParentId = m.ParentId;
  188 + r.ParentId = MenuParentIdConverter.ToGuid(m.ParentId);
187 189  
188 190 //开头大写
189 191 r.Name = routerName?.First().ToString().ToUpper() + routerName?.Substring(1);
... ... @@ -197,7 +199,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
197 199 r.AlwaysShow = true;
198 200  
199 201 //判断是否为最顶层的路由
200   - if (Guid.Empty == m.ParentId)
  202 + if (MenuParentIdConverter.IsRoot(m.ParentId))
201 203 {
202 204 r.Component = "Layout";
203 205 }
... ... @@ -250,7 +252,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
250 252 var r = new Vue3RouterDto();
251 253 r.OrderNum = m.OrderNum;
252 254 r.Id = m.Id;
253   - r.ParentId = m.ParentId;
  255 + r.ParentId = MenuParentIdConverter.ToGuid(m.ParentId);
254 256 r.Hidden = !m.IsShow;
255 257  
256 258 // 检测是否为 URL 链接(http:// 或 https:// 开头)
... ... @@ -359,7 +361,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
359 361 r.AlwaysShow = false;
360 362  
361 363 // 判断是否为最顶层的路由
362   - if (Guid.Empty == m.ParentId)
  364 + if (MenuParentIdConverter.IsRoot(m.ParentId))
363 365 {
364 366 r.Component = "Layout";
365 367 }
... ... @@ -385,7 +387,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
385 387 r.AlwaysShow = true;
386 388  
387 389 // 判断是否为最顶层的路由
388   - if (Guid.Empty == m.ParentId)
  390 + if (MenuParentIdConverter.IsRoot(m.ParentId))
389 391 {
390 392 r.Component = "Layout";
391 393 }
... ... @@ -449,7 +451,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
449 451 },
450 452 Children =null,
451 453 Id = m.Id,
452   - ParentId = m.ParentId
  454 + ParentId = MenuParentIdConverter.ToGuid(m.ParentId)
453 455 })
454 456 .ToList();
455 457  
... ... @@ -487,7 +489,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
487 489 var treeDto = new MenuTreeDto
488 490 {
489 491 Id = m.Id,
490   - ParentId = m.ParentId,
  492 + ParentId = MenuParentIdConverter.ToGuid(m.ParentId),
491 493 OrderNum = m.OrderNum,
492 494 MenuName = m.MenuName,
493 495 MenuType = m.MenuType,
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs
1   -using System.IdentityModel.Tokens.Jwt;
  1 +using System.IdentityModel.Tokens.Jwt;
2 2 using System.Security.Claims;
3 3 using System.Text;
4 4 using Mapster;
... ... @@ -77,10 +77,8 @@ namespace Yi.Framework.Rbac.Domain.Managers
77 77 {
78 78 throw new UserFriendlyException(UserConst.No_Role);
79 79 }
80   - if (!userInfo.PermissionCodes.Any())
81   - {
82   - throw new UserFriendlyException(UserConst.No_Permission);
83   - }
  80 +
  81 + // 菜单表 PermissionCode 可未落库;当前以角色及角色-菜单绑定为准,不要求 PermissionCodes 非空
84 82  
85 83 if (getUserInfo is not null)
86 84 {
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/UserManager.cs
1   -using System.Text.RegularExpressions;
  1 +using System.Text.RegularExpressions;
2 2 using Mapster;
3 3 using Microsoft.Extensions.Caching.Distributed;
4 4 using Microsoft.Extensions.DependencyInjection;
... ... @@ -10,6 +10,7 @@ using Volo.Abp.EventBus.Local;
10 10 using Volo.Abp.Guids;
11 11 using Yi.Framework.Rbac.Domain.Entities;
12 12 using Yi.Framework.Rbac.Domain.Repositories;
  13 +using Yi.Framework.Rbac.Domain.Shared;
13 14 using Yi.Framework.Rbac.Domain.Shared.Caches;
14 15 using Yi.Framework.Rbac.Domain.Shared.Consts;
15 16 using Yi.Framework.Rbac.Domain.Shared.Dtos;
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuPureDataSeed.cs
... ... @@ -87,7 +87,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
87 87 Router = "/system/user/index",
88 88 MenuIcon = "ri:admin-line",
89 89 OrderNum = 100,
90   - ParentId = system.Id,
  90 + ParentId = system.Id.ToString(),
91 91 RouterName = "SystemUser"
92 92 };
93 93 entities.Add(user);
... ... @@ -99,7 +99,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
99 99 PermissionCode = "system:user:query",
100 100 MenuType = MenuTypeEnum.Component,
101 101 OrderNum = 100,
102   - ParentId = user.Id,
  102 + ParentId = user.Id.ToString(),
103 103 IsDeleted = false
104 104 };
105 105 entities.Add(userQuery);
... ... @@ -111,7 +111,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
111 111 PermissionCode = "system:user:add",
112 112 MenuType = MenuTypeEnum.Component,
113 113 OrderNum = 100,
114   - ParentId = user.Id,
  114 + ParentId = user.Id.ToString(),
115 115 IsDeleted = false
116 116 };
117 117 entities.Add(userAdd);
... ... @@ -123,7 +123,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
123 123 PermissionCode = "system:user:edit",
124 124 MenuType = MenuTypeEnum.Component,
125 125 OrderNum = 100,
126   - ParentId = user.Id,
  126 + ParentId = user.Id.ToString(),
127 127 IsDeleted = false
128 128 };
129 129 entities.Add(userEdit);
... ... @@ -135,7 +135,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
135 135 PermissionCode = "system:user:remove",
136 136 MenuType = MenuTypeEnum.Component,
137 137 OrderNum = 100,
138   - ParentId = user.Id,
  138 + ParentId = user.Id.ToString(),
139 139 IsDeleted = false
140 140 };
141 141 entities.Add(userRemove);
... ... @@ -148,7 +148,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
148 148 PermissionCode = "system:user:resetPwd",
149 149 MenuType = MenuTypeEnum.Component,
150 150 OrderNum = 100,
151   - ParentId = user.Id,
  151 + ParentId = user.Id.ToString(),
152 152 IsDeleted = false
153 153 };
154 154 entities.Add(userResetPwd);
... ... @@ -164,7 +164,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
164 164 Router = "/system/role/index",
165 165 MenuIcon = "ri:admin-fill",
166 166 OrderNum = 99,
167   - ParentId = system.Id,
  167 + ParentId = system.Id.ToString(),
168 168 RouterName = "SystemRole"
169 169 };
170 170 entities.Add(role);
... ... @@ -176,7 +176,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
176 176 PermissionCode = "system:role:query",
177 177 MenuType = MenuTypeEnum.Component,
178 178 OrderNum = 100,
179   - ParentId = role.Id,
  179 + ParentId = role.Id.ToString(),
180 180 IsDeleted = false
181 181 };
182 182 entities.Add(roleQuery);
... ... @@ -188,7 +188,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
188 188 PermissionCode = "system:role:add",
189 189 MenuType = MenuTypeEnum.Component,
190 190 OrderNum = 100,
191   - ParentId = role.Id,
  191 + ParentId = role.Id.ToString(),
192 192 IsDeleted = false
193 193 };
194 194 entities.Add(roleAdd);
... ... @@ -200,7 +200,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
200 200 PermissionCode = "system:role:edit",
201 201 MenuType = MenuTypeEnum.Component,
202 202 OrderNum = 100,
203   - ParentId = role.Id,
  203 + ParentId = role.Id.ToString(),
204 204 IsDeleted = false
205 205 };
206 206 entities.Add(roleEdit);
... ... @@ -212,7 +212,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
212 212 PermissionCode = "system:role:remove",
213 213 MenuType = MenuTypeEnum.Component,
214 214 OrderNum = 100,
215   - ParentId = role.Id,
  215 + ParentId = role.Id.ToString(),
216 216 IsDeleted = false
217 217 };
218 218 entities.Add(roleRemove);
... ... @@ -228,7 +228,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
228 228 Router = "/system/menu/index",
229 229 MenuIcon = "ep:menu",
230 230 OrderNum = 98,
231   - ParentId = system.Id,
  231 + ParentId = system.Id.ToString(),
232 232 RouterName = "SystemMenu"
233 233 };
234 234 entities.Add(menu);
... ... @@ -240,7 +240,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
240 240 PermissionCode = "system:menu:query",
241 241 MenuType = MenuTypeEnum.Component,
242 242 OrderNum = 100,
243   - ParentId = menu.Id,
  243 + ParentId = menu.Id.ToString(),
244 244 IsDeleted = false
245 245 };
246 246 entities.Add(menuQuery);
... ... @@ -252,7 +252,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
252 252 PermissionCode = "system:menu:add",
253 253 MenuType = MenuTypeEnum.Component,
254 254 OrderNum = 100,
255   - ParentId = menu.Id,
  255 + ParentId = menu.Id.ToString(),
256 256 IsDeleted = false
257 257 };
258 258 entities.Add(menuAdd);
... ... @@ -264,7 +264,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
264 264 PermissionCode = "system:menu:edit",
265 265 MenuType = MenuTypeEnum.Component,
266 266 OrderNum = 100,
267   - ParentId = menu.Id,
  267 + ParentId = menu.Id.ToString(),
268 268 IsDeleted = false
269 269 };
270 270 entities.Add(menuEdit);
... ... @@ -276,7 +276,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
276 276 PermissionCode = "system:menu:remove",
277 277 MenuType = MenuTypeEnum.Component,
278 278 OrderNum = 100,
279   - ParentId = menu.Id,
  279 + ParentId = menu.Id.ToString(),
280 280 IsDeleted = false
281 281 };
282 282 entities.Add(menuRemove);
... ... @@ -291,7 +291,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
291 291 Router = "/system/dept/index",
292 292 MenuIcon = "ri:git-branch-line",
293 293 OrderNum = 97,
294   - ParentId = system.Id,
  294 + ParentId = system.Id.ToString(),
295 295 RouterName = "SystemDept"
296 296 };
297 297 entities.Add(dept);
... ... @@ -303,7 +303,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
303 303 PermissionCode = "system:dept:query",
304 304 MenuType = MenuTypeEnum.Component,
305 305 OrderNum = 100,
306   - ParentId = dept.Id,
  306 + ParentId = dept.Id.ToString(),
307 307 IsDeleted = false
308 308 };
309 309 entities.Add(deptQuery);
... ... @@ -315,7 +315,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
315 315 PermissionCode = "system:dept:add",
316 316 MenuType = MenuTypeEnum.Component,
317 317 OrderNum = 100,
318   - ParentId = dept.Id,
  318 + ParentId = dept.Id.ToString(),
319 319 IsDeleted = false
320 320 };
321 321 entities.Add(deptAdd);
... ... @@ -327,7 +327,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
327 327 PermissionCode = "system:dept:edit",
328 328 MenuType = MenuTypeEnum.Component,
329 329 OrderNum = 100,
330   - ParentId = dept.Id,
  330 + ParentId = dept.Id.ToString(),
331 331 IsDeleted = false
332 332 };
333 333 entities.Add(deptEdit);
... ... @@ -339,7 +339,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
339 339 PermissionCode = "system:dept:remove",
340 340 MenuType = MenuTypeEnum.Component,
341 341 OrderNum = 100,
342   - ParentId = dept.Id,
  342 + ParentId = dept.Id.ToString(),
343 343 IsDeleted = false
344 344 };
345 345 entities.Add(deptRemove);
... ... @@ -356,7 +356,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
356 356 Router = "/system/post/index",
357 357 MenuIcon = "ant-design:deployment-unit-outlined",
358 358 OrderNum = 96,
359   - ParentId = system.Id,
  359 + ParentId = system.Id.ToString(),
360 360 RouterName = "SystemPost"
361 361 };
362 362 entities.Add(post);
... ... @@ -368,7 +368,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
368 368 PermissionCode = "system:post:query",
369 369 MenuType = MenuTypeEnum.Component,
370 370 OrderNum = 100,
371   - ParentId = post.Id,
  371 + ParentId = post.Id.ToString(),
372 372 IsDeleted = false
373 373 };
374 374 entities.Add(postQuery);
... ... @@ -380,7 +380,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
380 380 PermissionCode = "system:post:add",
381 381 MenuType = MenuTypeEnum.Component,
382 382 OrderNum = 100,
383   - ParentId = post.Id,
  383 + ParentId = post.Id.ToString(),
384 384 IsDeleted = false
385 385 };
386 386 entities.Add(postAdd);
... ... @@ -392,7 +392,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
392 392 PermissionCode = "system:post:edit",
393 393 MenuType = MenuTypeEnum.Component,
394 394 OrderNum = 100,
395   - ParentId = post.Id,
  395 + ParentId = post.Id.ToString(),
396 396 IsDeleted = false
397 397 };
398 398 entities.Add(postEdit);
... ... @@ -404,7 +404,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
404 404 PermissionCode = "system:post:remove",
405 405 MenuType = MenuTypeEnum.Component,
406 406 OrderNum = 100,
407   - ParentId = post.Id,
  407 + ParentId = post.Id.ToString(),
408 408 IsDeleted = false
409 409 };
410 410 entities.Add(postRemove);
... ... @@ -420,7 +420,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
420 420 Router = "/monitor/operation-logs",
421 421 MenuIcon = "ri:history-fill",
422 422 OrderNum = 100,
423   - ParentId = monitoring.Id,
  423 + ParentId = monitoring.Id.ToString(),
424 424 RouterName = "OperationLog",
425 425 Component = "monitor/logs/operation/index"
426 426 };
... ... @@ -433,7 +433,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
433 433 PermissionCode = "monitor:operlog:query",
434 434 MenuType = MenuTypeEnum.Component,
435 435 OrderNum = 100,
436   - ParentId = operationLog.Id,
  436 + ParentId = operationLog.Id.ToString(),
437 437 IsDeleted = false
438 438 };
439 439 entities.Add(operationLogQuery);
... ... @@ -445,7 +445,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
445 445 PermissionCode = "monitor:operlog:remove",
446 446 MenuType = MenuTypeEnum.Component,
447 447 OrderNum = 100,
448   - ParentId = operationLog.Id,
  448 + ParentId = operationLog.Id.ToString(),
449 449 IsDeleted = false
450 450 };
451 451 entities.Add(operationLogRemove);
... ... @@ -465,7 +465,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
465 465 Component = "monitor/logs/login/index",
466 466 MenuIcon = "ri:window-line",
467 467 OrderNum = 100,
468   - ParentId = monitoring.Id,
  468 + ParentId = monitoring.Id.ToString(),
469 469 RouterName = "LoginLog",
470 470 };
471 471 entities.Add(loginLog);
... ... @@ -477,7 +477,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
477 477 PermissionCode = "monitor:logininfor:query",
478 478 MenuType = MenuTypeEnum.Component,
479 479 OrderNum = 100,
480   - ParentId = loginLog.Id,
  480 + ParentId = loginLog.Id.ToString(),
481 481 IsDeleted = false
482 482 };
483 483 entities.Add(loginLogQuery);
... ... @@ -489,7 +489,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
489 489 PermissionCode = "monitor:logininfor:remove",
490 490 MenuType = MenuTypeEnum.Component,
491 491 OrderNum = 100,
492   - ParentId = loginLog.Id,
  492 + ParentId = loginLog.Id.ToString(),
493 493 IsDeleted = false,
494 494  
495 495 };
... ... @@ -509,7 +509,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
509 509 Component = "/system/config/index",
510 510 MenuIcon = "ri:edit-box-line",
511 511 OrderNum = 94,
512   - ParentId = system.Id,
  512 + ParentId = system.Id.ToString(),
513 513 IsDeleted = false
514 514 };
515 515 entities.Add(config);
... ... @@ -521,7 +521,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
521 521 PermissionCode = "system:config:query",
522 522 MenuType = MenuTypeEnum.Component,
523 523 OrderNum = 100,
524   - ParentId = config.Id,
  524 + ParentId = config.Id.ToString(),
525 525 IsDeleted = false
526 526 };
527 527 entities.Add(configQuery);
... ... @@ -533,7 +533,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
533 533 PermissionCode = "system:config:add",
534 534 MenuType = MenuTypeEnum.Component,
535 535 OrderNum = 100,
536   - ParentId = config.Id,
  536 + ParentId = config.Id.ToString(),
537 537 IsDeleted = false
538 538 };
539 539 entities.Add(configAdd);
... ... @@ -545,7 +545,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
545 545 PermissionCode = "system:config:edit",
546 546 MenuType = MenuTypeEnum.Component,
547 547 OrderNum = 100,
548   - ParentId = config.Id,
  548 + ParentId = config.Id.ToString(),
549 549 IsDeleted = false
550 550 };
551 551 entities.Add(configEdit);
... ... @@ -557,7 +557,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
557 557 PermissionCode = "system:config:remove",
558 558 MenuType = MenuTypeEnum.Component,
559 559 OrderNum = 100,
560   - ParentId = config.Id,
  560 + ParentId = config.Id.ToString(),
561 561 IsDeleted = false
562 562 };
563 563 entities.Add(configRemove);
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuRuoYiDataSeed.cs
... ... @@ -88,7 +88,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
88 88 Component = "code/field/index",
89 89 MenuIcon = "number",
90 90 OrderNum = 99,
91   - ParentId = code.Id,
  91 + ParentId = code.Id.ToString(),
92 92 IsDeleted = false
93 93 };
94 94 entities.Add(field);
... ... @@ -599,7 +599,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
599 599 Component = "system/tenant/index",
600 600 MenuIcon = "list",
601 601 OrderNum = 101,
602   - ParentId = system.Id,
  602 + ParentId = system.Id.ToString(),
603 603 IsDeleted = false
604 604 };
605 605 entities.Add(tenant);
... ... @@ -611,7 +611,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
611 611 PermissionCode = "system:tenant:query",
612 612 MenuType = MenuTypeEnum.Component,
613 613 OrderNum = 100,
614   - ParentId = tenant.Id,
  614 + ParentId = tenant.Id.ToString(),
615 615 IsDeleted = false
616 616 };
617 617 entities.Add(tenantQuery);
... ... @@ -623,7 +623,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
623 623 PermissionCode = "system:tenant:add",
624 624 MenuType = MenuTypeEnum.Component,
625 625 OrderNum = 100,
626   - ParentId = tenant.Id,
  626 + ParentId = tenant.Id.ToString(),
627 627 IsDeleted = false
628 628 };
629 629 entities.Add(tenantAdd);
... ... @@ -635,7 +635,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
635 635 PermissionCode = "system:tenant:edit",
636 636 MenuType = MenuTypeEnum.Component,
637 637 OrderNum = 100,
638   - ParentId = tenant.Id,
  638 + ParentId = tenant.Id.ToString(),
639 639 IsDeleted = false
640 640 };
641 641 entities.Add(tenantEdit);
... ... @@ -647,7 +647,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
647 647 PermissionCode = "system:tenant:remove",
648 648 MenuType = MenuTypeEnum.Component,
649 649 OrderNum = 100,
650   - ParentId = tenant.Id,
  650 + ParentId = tenant.Id.ToString(),
651 651 IsDeleted = false
652 652 };
653 653 entities.Add(tenantRemove);
... ... @@ -677,7 +677,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
677 677 Component = "system/user/index",
678 678 MenuIcon = "user",
679 679 OrderNum = 100,
680   - ParentId = system.Id,
  680 + ParentId = system.Id.ToString(),
681 681 IsDeleted = false
682 682 };
683 683 entities.Add(user);
... ... @@ -689,7 +689,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
689 689 PermissionCode = "system:user:query",
690 690 MenuType = MenuTypeEnum.Component,
691 691 OrderNum = 100,
692   - ParentId = user.Id,
  692 + ParentId = user.Id.ToString(),
693 693 IsDeleted = false
694 694 };
695 695 entities.Add(userQuery);
... ... @@ -701,7 +701,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
701 701 PermissionCode = "system:user:add",
702 702 MenuType = MenuTypeEnum.Component,
703 703 OrderNum = 100,
704   - ParentId = user.Id,
  704 + ParentId = user.Id.ToString(),
705 705 IsDeleted = false
706 706 };
707 707 entities.Add(userAdd);
... ... @@ -713,7 +713,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
713 713 PermissionCode = "system:user:edit",
714 714 MenuType = MenuTypeEnum.Component,
715 715 OrderNum = 100,
716   - ParentId = user.Id,
  716 + ParentId = user.Id.ToString(),
717 717 IsDeleted = false
718 718 };
719 719 entities.Add(userEdit);
... ... @@ -725,7 +725,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
725 725 PermissionCode = "system:user:remove",
726 726 MenuType = MenuTypeEnum.Component,
727 727 OrderNum = 100,
728   - ParentId = user.Id,
  728 + ParentId = user.Id.ToString(),
729 729 IsDeleted = false
730 730 };
731 731 entities.Add(userRemove);
... ... @@ -738,7 +738,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
738 738 PermissionCode = "system:user:resetPwd",
739 739 MenuType = MenuTypeEnum.Component,
740 740 OrderNum = 100,
741   - ParentId = user.Id,
  741 + ParentId = user.Id.ToString(),
742 742 IsDeleted = false
743 743 };
744 744 entities.Add(userResetPwd);
... ... @@ -758,7 +758,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
758 758 Component = "system/role/index",
759 759 MenuIcon = "peoples",
760 760 OrderNum = 99,
761   - ParentId = system.Id,
  761 + ParentId = system.Id.ToString(),
762 762 IsDeleted = false
763 763 };
764 764 entities.Add(role);
... ... @@ -770,7 +770,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
770 770 PermissionCode = "system:role:query",
771 771 MenuType = MenuTypeEnum.Component,
772 772 OrderNum = 100,
773   - ParentId = role.Id,
  773 + ParentId = role.Id.ToString(),
774 774 IsDeleted = false
775 775 };
776 776 entities.Add(roleQuery);
... ... @@ -782,7 +782,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
782 782 PermissionCode = "system:role:add",
783 783 MenuType = MenuTypeEnum.Component,
784 784 OrderNum = 100,
785   - ParentId = role.Id,
  785 + ParentId = role.Id.ToString(),
786 786 IsDeleted = false
787 787 };
788 788 entities.Add(roleAdd);
... ... @@ -794,7 +794,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
794 794 PermissionCode = "system:role:edit",
795 795 MenuType = MenuTypeEnum.Component,
796 796 OrderNum = 100,
797   - ParentId = role.Id,
  797 + ParentId = role.Id.ToString(),
798 798 IsDeleted = false
799 799 };
800 800 entities.Add(roleEdit);
... ... @@ -806,7 +806,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
806 806 PermissionCode = "system:role:remove",
807 807 MenuType = MenuTypeEnum.Component,
808 808 OrderNum = 100,
809   - ParentId = role.Id,
  809 + ParentId = role.Id.ToString(),
810 810 IsDeleted = false
811 811 };
812 812 entities.Add(roleRemove);
... ... @@ -826,7 +826,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
826 826 Component = "system/menu/index",
827 827 MenuIcon = "tree-table",
828 828 OrderNum = 98,
829   - ParentId = system.Id,
  829 + ParentId = system.Id.ToString(),
830 830 IsDeleted = false
831 831 };
832 832 entities.Add(menu);
... ... @@ -838,7 +838,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
838 838 PermissionCode = "system:menu:query",
839 839 MenuType = MenuTypeEnum.Component,
840 840 OrderNum = 100,
841   - ParentId = menu.Id,
  841 + ParentId = menu.Id.ToString(),
842 842 IsDeleted = false
843 843 };
844 844 entities.Add(menuQuery);
... ... @@ -850,7 +850,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
850 850 PermissionCode = "system:menu:add",
851 851 MenuType = MenuTypeEnum.Component,
852 852 OrderNum = 100,
853   - ParentId = menu.Id,
  853 + ParentId = menu.Id.ToString(),
854 854 IsDeleted = false
855 855 };
856 856 entities.Add(menuAdd);
... ... @@ -862,7 +862,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
862 862 PermissionCode = "system:menu:edit",
863 863 MenuType = MenuTypeEnum.Component,
864 864 OrderNum = 100,
865   - ParentId = menu.Id,
  865 + ParentId = menu.Id.ToString(),
866 866 IsDeleted = false
867 867 };
868 868 entities.Add(menuEdit);
... ... @@ -874,7 +874,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
874 874 PermissionCode = "system:menu:remove",
875 875 MenuType = MenuTypeEnum.Component,
876 876 OrderNum = 100,
877   - ParentId = menu.Id,
  877 + ParentId = menu.Id.ToString(),
878 878 IsDeleted = false
879 879 };
880 880 entities.Add(menuRemove);
... ... @@ -893,7 +893,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
893 893 Component = "system/dept/index",
894 894 MenuIcon = "tree",
895 895 OrderNum = 97,
896   - ParentId = system.Id,
  896 + ParentId = system.Id.ToString(),
897 897 IsDeleted = false
898 898 };
899 899 entities.Add(dept);
... ... @@ -905,7 +905,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
905 905 PermissionCode = "system:dept:query",
906 906 MenuType = MenuTypeEnum.Component,
907 907 OrderNum = 100,
908   - ParentId = dept.Id,
  908 + ParentId = dept.Id.ToString(),
909 909 IsDeleted = false
910 910 };
911 911 entities.Add(deptQuery);
... ... @@ -917,7 +917,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
917 917 PermissionCode = "system:dept:add",
918 918 MenuType = MenuTypeEnum.Component,
919 919 OrderNum = 100,
920   - ParentId = dept.Id,
  920 + ParentId = dept.Id.ToString(),
921 921 IsDeleted = false
922 922 };
923 923 entities.Add(deptAdd);
... ... @@ -929,7 +929,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
929 929 PermissionCode = "system:dept:edit",
930 930 MenuType = MenuTypeEnum.Component,
931 931 OrderNum = 100,
932   - ParentId = dept.Id,
  932 + ParentId = dept.Id.ToString(),
933 933 IsDeleted = false
934 934 };
935 935 entities.Add(deptEdit);
... ... @@ -941,7 +941,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
941 941 PermissionCode = "system:dept:remove",
942 942 MenuType = MenuTypeEnum.Component,
943 943 OrderNum = 100,
944   - ParentId = dept.Id,
  944 + ParentId = dept.Id.ToString(),
945 945 IsDeleted = false
946 946 };
947 947 entities.Add(deptRemove);
... ... @@ -962,7 +962,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
962 962 Component = "system/post/index",
963 963 MenuIcon = "post",
964 964 OrderNum = 96,
965   - ParentId = system.Id,
  965 + ParentId = system.Id.ToString(),
966 966 IsDeleted = false
967 967 };
968 968 entities.Add(post);
... ... @@ -974,7 +974,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
974 974 PermissionCode = "system:post:query",
975 975 MenuType = MenuTypeEnum.Component,
976 976 OrderNum = 100,
977   - ParentId = post.Id,
  977 + ParentId = post.Id.ToString(),
978 978 IsDeleted = false
979 979 };
980 980 entities.Add(postQuery);
... ... @@ -986,7 +986,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
986 986 PermissionCode = "system:post:add",
987 987 MenuType = MenuTypeEnum.Component,
988 988 OrderNum = 100,
989   - ParentId = post.Id,
  989 + ParentId = post.Id.ToString(),
990 990 IsDeleted = false
991 991 };
992 992 entities.Add(postAdd);
... ... @@ -998,7 +998,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
998 998 PermissionCode = "system:post:edit",
999 999 MenuType = MenuTypeEnum.Component,
1000 1000 OrderNum = 100,
1001   - ParentId = post.Id,
  1001 + ParentId = post.Id.ToString(),
1002 1002 IsDeleted = false
1003 1003 };
1004 1004 entities.Add(postEdit);
... ... @@ -1010,7 +1010,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1010 1010 PermissionCode = "system:post:remove",
1011 1011 MenuType = MenuTypeEnum.Component,
1012 1012 OrderNum = 100,
1013   - ParentId = post.Id,
  1013 + ParentId = post.Id.ToString(),
1014 1014 IsDeleted = false
1015 1015 };
1016 1016 entities.Add(postRemove);
... ... @@ -1029,7 +1029,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1029 1029 Component = "system/dict/index",
1030 1030 MenuIcon = "dict",
1031 1031 OrderNum = 95,
1032   - ParentId = system.Id,
  1032 + ParentId = system.Id.ToString(),
1033 1033 IsDeleted = false
1034 1034 };
1035 1035 entities.Add(dict);
... ... @@ -1041,7 +1041,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1041 1041 PermissionCode = "system:dict:query",
1042 1042 MenuType = MenuTypeEnum.Component,
1043 1043 OrderNum = 100,
1044   - ParentId = dict.Id,
  1044 + ParentId = dict.Id.ToString(),
1045 1045 IsDeleted = false
1046 1046 };
1047 1047 entities.Add(dictQuery);
... ... @@ -1053,7 +1053,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1053 1053 PermissionCode = "system:dict:add",
1054 1054 MenuType = MenuTypeEnum.Component,
1055 1055 OrderNum = 100,
1056   - ParentId = dict.Id,
  1056 + ParentId = dict.Id.ToString(),
1057 1057 IsDeleted = false
1058 1058 };
1059 1059 entities.Add(dictAdd);
... ... @@ -1065,7 +1065,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1065 1065 PermissionCode = "system:dict:edit",
1066 1066 MenuType = MenuTypeEnum.Component,
1067 1067 OrderNum = 100,
1068   - ParentId = dict.Id,
  1068 + ParentId = dict.Id.ToString(),
1069 1069 IsDeleted = false
1070 1070 };
1071 1071 entities.Add(dictEdit);
... ... @@ -1077,7 +1077,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1077 1077 PermissionCode = "system:dict:remove",
1078 1078 MenuType = MenuTypeEnum.Component,
1079 1079 OrderNum = 100,
1080   - ParentId = dict.Id,
  1080 + ParentId = dict.Id.ToString(),
1081 1081 IsDeleted = false
1082 1082 };
1083 1083 entities.Add(dictRemove);
... ... @@ -1097,7 +1097,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1097 1097 Component = "system/config/index",
1098 1098 MenuIcon = "edit",
1099 1099 OrderNum = 94,
1100   - ParentId = system.Id,
  1100 + ParentId = system.Id.ToString(),
1101 1101 IsDeleted = false
1102 1102 };
1103 1103 entities.Add(config);
... ... @@ -1109,7 +1109,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1109 1109 PermissionCode = "system:config:query",
1110 1110 MenuType = MenuTypeEnum.Component,
1111 1111 OrderNum = 100,
1112   - ParentId = config.Id,
  1112 + ParentId = config.Id.ToString(),
1113 1113 IsDeleted = false
1114 1114 };
1115 1115 entities.Add(configQuery);
... ... @@ -1121,7 +1121,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1121 1121 PermissionCode = "system:config:add",
1122 1122 MenuType = MenuTypeEnum.Component,
1123 1123 OrderNum = 100,
1124   - ParentId = config.Id,
  1124 + ParentId = config.Id.ToString(),
1125 1125 IsDeleted = false
1126 1126 };
1127 1127 entities.Add(configAdd);
... ... @@ -1133,7 +1133,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1133 1133 PermissionCode = "system:config:edit",
1134 1134 MenuType = MenuTypeEnum.Component,
1135 1135 OrderNum = 100,
1136   - ParentId = config.Id,
  1136 + ParentId = config.Id.ToString(),
1137 1137 IsDeleted = false
1138 1138 };
1139 1139 entities.Add(configEdit);
... ... @@ -1145,7 +1145,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1145 1145 PermissionCode = "system:config:remove",
1146 1146 MenuType = MenuTypeEnum.Component,
1147 1147 OrderNum = 100,
1148   - ParentId = config.Id,
  1148 + ParentId = config.Id.ToString(),
1149 1149 IsDeleted = false
1150 1150 };
1151 1151 entities.Add(configRemove);
... ... @@ -1167,7 +1167,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1167 1167 Component = "system/notice/index",
1168 1168 MenuIcon = "message",
1169 1169 OrderNum = 93,
1170   - ParentId = system.Id,
  1170 + ParentId = system.Id.ToString(),
1171 1171 IsDeleted = false
1172 1172 };
1173 1173 entities.Add(notice);
... ... @@ -1179,7 +1179,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1179 1179 PermissionCode = "system:notice:query",
1180 1180 MenuType = MenuTypeEnum.Component,
1181 1181 OrderNum = 100,
1182   - ParentId = notice.Id,
  1182 + ParentId = notice.Id.ToString(),
1183 1183 IsDeleted = false
1184 1184 };
1185 1185 entities.Add(noticeQuery);
... ... @@ -1191,7 +1191,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1191 1191 PermissionCode = "system:notice:add",
1192 1192 MenuType = MenuTypeEnum.Component,
1193 1193 OrderNum = 100,
1194   - ParentId = notice.Id,
  1194 + ParentId = notice.Id.ToString(),
1195 1195 IsDeleted = false
1196 1196 };
1197 1197 entities.Add(noticeAdd);
... ... @@ -1203,7 +1203,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1203 1203 PermissionCode = "system:notice:edit",
1204 1204 MenuType = MenuTypeEnum.Component,
1205 1205 OrderNum = 100,
1206   - ParentId = notice.Id,
  1206 + ParentId = notice.Id.ToString(),
1207 1207 IsDeleted = false
1208 1208 };
1209 1209 entities.Add(noticeEdit);
... ... @@ -1215,7 +1215,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1215 1215 PermissionCode = "system:notice:remove",
1216 1216 MenuType = MenuTypeEnum.Component,
1217 1217 OrderNum = 100,
1218   - ParentId = notice.Id,
  1218 + ParentId = notice.Id.ToString(),
1219 1219 IsDeleted = false
1220 1220 };
1221 1221 entities.Add(noticeRemove);
... ... @@ -1233,7 +1233,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1233 1233 IsLink = false,
1234 1234 MenuIcon = "log",
1235 1235 OrderNum = 92,
1236   - ParentId = system.Id,
  1236 + ParentId = system.Id.ToString(),
1237 1237 IsDeleted = false
1238 1238 };
1239 1239 entities.Add(log);
... ... @@ -1252,7 +1252,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1252 1252 Component = "monitor/operlog/index",
1253 1253 MenuIcon = "form",
1254 1254 OrderNum = 100,
1255   - ParentId = log.Id,
  1255 + ParentId = log.Id.ToString(),
1256 1256 IsDeleted = false
1257 1257 };
1258 1258 entities.Add(operationLog);
... ... @@ -1264,7 +1264,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1264 1264 PermissionCode = "monitor:operlog:query",
1265 1265 MenuType = MenuTypeEnum.Component,
1266 1266 OrderNum = 100,
1267   - ParentId = operationLog.Id,
  1267 + ParentId = operationLog.Id.ToString(),
1268 1268 IsDeleted = false
1269 1269 };
1270 1270 entities.Add(operationLogQuery);
... ... @@ -1276,7 +1276,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1276 1276 PermissionCode = "monitor:operlog:remove",
1277 1277 MenuType = MenuTypeEnum.Component,
1278 1278 OrderNum = 100,
1279   - ParentId = operationLog.Id,
  1279 + ParentId = operationLog.Id.ToString(),
1280 1280 IsDeleted = false
1281 1281 };
1282 1282 entities.Add(operationLogRemove);
... ... @@ -1296,7 +1296,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1296 1296 Component = "monitor/logininfor/index",
1297 1297 MenuIcon = "logininfor",
1298 1298 OrderNum = 100,
1299   - ParentId = log.Id,
  1299 + ParentId = log.Id.ToString(),
1300 1300 IsDeleted = false
1301 1301 };
1302 1302 entities.Add(loginLog);
... ... @@ -1308,7 +1308,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1308 1308 PermissionCode = "monitor:logininfor:query",
1309 1309 MenuType = MenuTypeEnum.Component,
1310 1310 OrderNum = 100,
1311   - ParentId = loginLog.Id,
  1311 + ParentId = loginLog.Id.ToString(),
1312 1312 IsDeleted = false
1313 1313 };
1314 1314 entities.Add(loginLogQuery);
... ... @@ -1320,7 +1320,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
1320 1320 PermissionCode = "monitor:logininfor:remove",
1321 1321 MenuType = MenuTypeEnum.Component,
1322 1322 OrderNum = 100,
1323   - ParentId = loginLog.Id,
  1323 + ParentId = loginLog.Id.ToString(),
1324 1324 IsDeleted = false
1325 1325 };
1326 1326 entities.Add(loginLogRemove);
... ...
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.SqlSugarCore/DataSeeds/MenuVben5DataSeed.cs
1   -using Volo.Abp.Data;
  1 +using Volo.Abp.Data;
2 2 using Volo.Abp.DependencyInjection;
3 3 using Volo.Abp.Guids;
4 4 using Yi.Framework.Rbac.Domain.Entities;
... ... @@ -88,7 +88,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
88 88 // Component = "code/field/index",
89 89 // MenuIcon = "tabler:file-code",
90 90 // OrderNum = 99,
91   - // ParentId = code.Id,
  91 + // ParentId = code.Id.ToString(),
92 92 // IsDeleted = false
93 93 // };
94 94 // entities.Add(field);
... ... @@ -251,7 +251,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
251 251 Component = "system/tenant/index",
252 252 MenuIcon = "tabler:users",
253 253 OrderNum = 101,
254   - ParentId = system.Id,
  254 + ParentId = system.Id.ToString(),
255 255 IsDeleted = false
256 256 };
257 257 entities.Add(tenant);
... ... @@ -263,7 +263,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
263 263 PermissionCode = "system:tenant:query",
264 264 MenuType = MenuTypeEnum.Component,
265 265 OrderNum = 100,
266   - ParentId = tenant.Id,
  266 + ParentId = tenant.Id.ToString(),
267 267 IsDeleted = false
268 268 };
269 269 entities.Add(tenantQuery);
... ... @@ -275,7 +275,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
275 275 PermissionCode = "system:tenant:add",
276 276 MenuType = MenuTypeEnum.Component,
277 277 OrderNum = 100,
278   - ParentId = tenant.Id,
  278 + ParentId = tenant.Id.ToString(),
279 279 IsDeleted = false
280 280 };
281 281 entities.Add(tenantAdd);
... ... @@ -287,7 +287,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
287 287 PermissionCode = "system:tenant:edit",
288 288 MenuType = MenuTypeEnum.Component,
289 289 OrderNum = 100,
290   - ParentId = tenant.Id,
  290 + ParentId = tenant.Id.ToString(),
291 291 IsDeleted = false
292 292 };
293 293 entities.Add(tenantEdit);
... ... @@ -299,7 +299,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
299 299 PermissionCode = "system:tenant:remove",
300 300 MenuType = MenuTypeEnum.Component,
301 301 OrderNum = 100,
302   - ParentId = tenant.Id,
  302 + ParentId = tenant.Id.ToString(),
303 303 IsDeleted = false
304 304 };
305 305 entities.Add(tenantRemove);
... ... @@ -318,7 +318,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
318 318 Component = "system/user/index",
319 319 MenuIcon = "tabler:user",
320 320 OrderNum = 100,
321   - ParentId = system.Id,
  321 + ParentId = system.Id.ToString(),
322 322 IsDeleted = false
323 323 };
324 324 entities.Add(user);
... ... @@ -330,7 +330,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
330 330 PermissionCode = "system:user:query",
331 331 MenuType = MenuTypeEnum.Component,
332 332 OrderNum = 100,
333   - ParentId = user.Id,
  333 + ParentId = user.Id.ToString(),
334 334 IsDeleted = false
335 335 };
336 336 entities.Add(userQuery);
... ... @@ -342,7 +342,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
342 342 PermissionCode = "system:user:add",
343 343 MenuType = MenuTypeEnum.Component,
344 344 OrderNum = 100,
345   - ParentId = user.Id,
  345 + ParentId = user.Id.ToString(),
346 346 IsDeleted = false
347 347 };
348 348 entities.Add(userAdd);
... ... @@ -354,7 +354,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
354 354 PermissionCode = "system:user:edit",
355 355 MenuType = MenuTypeEnum.Component,
356 356 OrderNum = 100,
357   - ParentId = user.Id,
  357 + ParentId = user.Id.ToString(),
358 358 IsDeleted = false
359 359 };
360 360 entities.Add(userEdit);
... ... @@ -366,7 +366,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
366 366 PermissionCode = "system:user:remove",
367 367 MenuType = MenuTypeEnum.Component,
368 368 OrderNum = 100,
369   - ParentId = user.Id,
  369 + ParentId = user.Id.ToString(),
370 370 IsDeleted = false
371 371 };
372 372 entities.Add(userRemove);
... ... @@ -379,7 +379,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
379 379 PermissionCode = "system:user:resetPwd",
380 380 MenuType = MenuTypeEnum.Component,
381 381 OrderNum = 100,
382   - ParentId = user.Id,
  382 + ParentId = user.Id.ToString(),
383 383 IsDeleted = false
384 384 };
385 385 entities.Add(userResetPwd);
... ... @@ -399,7 +399,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
399 399 Component = "system/role/index",
400 400 MenuIcon = "eos-icons:role-binding-outlined",
401 401 OrderNum = 99,
402   - ParentId = system.Id,
  402 + ParentId = system.Id.ToString(),
403 403 IsDeleted = false
404 404 };
405 405 entities.Add(role);
... ... @@ -411,7 +411,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
411 411 PermissionCode = "system:role:query",
412 412 MenuType = MenuTypeEnum.Component,
413 413 OrderNum = 100,
414   - ParentId = role.Id,
  414 + ParentId = role.Id.ToString(),
415 415 IsDeleted = false
416 416 };
417 417 entities.Add(roleQuery);
... ... @@ -423,7 +423,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
423 423 PermissionCode = "system:role:add",
424 424 MenuType = MenuTypeEnum.Component,
425 425 OrderNum = 100,
426   - ParentId = role.Id,
  426 + ParentId = role.Id.ToString(),
427 427 IsDeleted = false
428 428 };
429 429 entities.Add(roleAdd);
... ... @@ -435,7 +435,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
435 435 PermissionCode = "system:role:edit",
436 436 MenuType = MenuTypeEnum.Component,
437 437 OrderNum = 100,
438   - ParentId = role.Id,
  438 + ParentId = role.Id.ToString(),
439 439 IsDeleted = false
440 440 };
441 441 entities.Add(roleEdit);
... ... @@ -447,7 +447,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
447 447 PermissionCode = "system:role:remove",
448 448 MenuType = MenuTypeEnum.Component,
449 449 OrderNum = 100,
450   - ParentId = role.Id,
  450 + ParentId = role.Id.ToString(),
451 451 IsDeleted = false
452 452 };
453 453 entities.Add(roleRemove);
... ... @@ -466,7 +466,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
466 466 MenuIcon = "tabler:user-shield",
467 467 OrderNum = 15,
468 468 IsDeleted = false,
469   - ParentId = system.Id
  469 + ParentId = system.Id.ToString()
470 470 };
471 471 entities.Add(roleAuthUser);
472 472  
... ... @@ -485,7 +485,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
485 485 Component = "system/menu/index",
486 486 MenuIcon = "ic:sharp-menu",
487 487 OrderNum = 98,
488   - ParentId = system.Id,
  488 + ParentId = system.Id.ToString(),
489 489 IsDeleted = false
490 490 };
491 491 entities.Add(menu);
... ... @@ -497,7 +497,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
497 497 PermissionCode = "system:menu:query",
498 498 MenuType = MenuTypeEnum.Component,
499 499 OrderNum = 100,
500   - ParentId = menu.Id,
  500 + ParentId = menu.Id.ToString(),
501 501 IsDeleted = false
502 502 };
503 503 entities.Add(menuQuery);
... ... @@ -509,7 +509,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
509 509 PermissionCode = "system:menu:add",
510 510 MenuType = MenuTypeEnum.Component,
511 511 OrderNum = 100,
512   - ParentId = menu.Id,
  512 + ParentId = menu.Id.ToString(),
513 513 IsDeleted = false
514 514 };
515 515 entities.Add(menuAdd);
... ... @@ -521,7 +521,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
521 521 PermissionCode = "system:menu:edit",
522 522 MenuType = MenuTypeEnum.Component,
523 523 OrderNum = 100,
524   - ParentId = menu.Id,
  524 + ParentId = menu.Id.ToString(),
525 525 IsDeleted = false
526 526 };
527 527 entities.Add(menuEdit);
... ... @@ -533,7 +533,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
533 533 PermissionCode = "system:menu:remove",
534 534 MenuType = MenuTypeEnum.Component,
535 535 OrderNum = 100,
536   - ParentId = menu.Id,
  536 + ParentId = menu.Id.ToString(),
537 537 IsDeleted = false
538 538 };
539 539 entities.Add(menuRemove);
... ... @@ -552,7 +552,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
552 552 Component = "system/dept/index",
553 553 MenuIcon = "mingcute:department-line",
554 554 OrderNum = 97,
555   - ParentId = system.Id,
  555 + ParentId = system.Id.ToString(),
556 556 IsDeleted = false
557 557 };
558 558 entities.Add(dept);
... ... @@ -564,7 +564,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
564 564 PermissionCode = "system:dept:query",
565 565 MenuType = MenuTypeEnum.Component,
566 566 OrderNum = 100,
567   - ParentId = dept.Id,
  567 + ParentId = dept.Id.ToString(),
568 568 IsDeleted = false
569 569 };
570 570 entities.Add(deptQuery);
... ... @@ -576,7 +576,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
576 576 PermissionCode = "system:dept:add",
577 577 MenuType = MenuTypeEnum.Component,
578 578 OrderNum = 100,
579   - ParentId = dept.Id,
  579 + ParentId = dept.Id.ToString(),
580 580 IsDeleted = false
581 581 };
582 582 entities.Add(deptAdd);
... ... @@ -588,7 +588,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
588 588 PermissionCode = "system:dept:edit",
589 589 MenuType = MenuTypeEnum.Component,
590 590 OrderNum = 100,
591   - ParentId = dept.Id,
  591 + ParentId = dept.Id.ToString(),
592 592 IsDeleted = false
593 593 };
594 594 entities.Add(deptEdit);
... ... @@ -600,7 +600,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
600 600 PermissionCode = "system:dept:remove",
601 601 MenuType = MenuTypeEnum.Component,
602 602 OrderNum = 100,
603   - ParentId = dept.Id,
  603 + ParentId = dept.Id.ToString(),
604 604 IsDeleted = false
605 605 };
606 606 entities.Add(deptRemove);
... ... @@ -621,7 +621,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
621 621 Component = "system/post/index",
622 622 MenuIcon = "tabler:user-star",
623 623 OrderNum = 96,
624   - ParentId = system.Id,
  624 + ParentId = system.Id.ToString(),
625 625 IsDeleted = false
626 626 };
627 627 entities.Add(post);
... ... @@ -633,7 +633,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
633 633 PermissionCode = "system:post:query",
634 634 MenuType = MenuTypeEnum.Component,
635 635 OrderNum = 100,
636   - ParentId = post.Id,
  636 + ParentId = post.Id.ToString(),
637 637 IsDeleted = false
638 638 };
639 639 entities.Add(postQuery);
... ... @@ -645,7 +645,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
645 645 PermissionCode = "system:post:add",
646 646 MenuType = MenuTypeEnum.Component,
647 647 OrderNum = 100,
648   - ParentId = post.Id,
  648 + ParentId = post.Id.ToString(),
649 649 IsDeleted = false
650 650 };
651 651 entities.Add(postAdd);
... ... @@ -657,7 +657,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
657 657 PermissionCode = "system:post:edit",
658 658 MenuType = MenuTypeEnum.Component,
659 659 OrderNum = 100,
660   - ParentId = post.Id,
  660 + ParentId = post.Id.ToString(),
661 661 IsDeleted = false
662 662 };
663 663 entities.Add(postEdit);
... ... @@ -669,7 +669,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
669 669 PermissionCode = "system:post:remove",
670 670 MenuType = MenuTypeEnum.Component,
671 671 OrderNum = 100,
672   - ParentId = post.Id,
  672 + ParentId = post.Id.ToString(),
673 673 IsDeleted = false
674 674 };
675 675 entities.Add(postRemove);
... ... @@ -688,7 +688,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
688 688 Component = "system/dict/index",
689 689 MenuIcon = "fluent-mdl2:dictionary",
690 690 OrderNum = 95,
691   - ParentId = system.Id,
  691 + ParentId = system.Id.ToString(),
692 692 IsDeleted = false
693 693 };
694 694 entities.Add(dict);
... ... @@ -700,7 +700,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
700 700 PermissionCode = "system:dict:query",
701 701 MenuType = MenuTypeEnum.Component,
702 702 OrderNum = 100,
703   - ParentId = dict.Id,
  703 + ParentId = dict.Id.ToString(),
704 704 IsDeleted = false
705 705 };
706 706 entities.Add(dictQuery);
... ... @@ -712,7 +712,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
712 712 PermissionCode = "system:dict:add",
713 713 MenuType = MenuTypeEnum.Component,
714 714 OrderNum = 100,
715   - ParentId = dict.Id,
  715 + ParentId = dict.Id.ToString(),
716 716 IsDeleted = false
717 717 };
718 718 entities.Add(dictAdd);
... ... @@ -724,7 +724,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
724 724 PermissionCode = "system:dict:edit",
725 725 MenuType = MenuTypeEnum.Component,
726 726 OrderNum = 100,
727   - ParentId = dict.Id,
  727 + ParentId = dict.Id.ToString(),
728 728 IsDeleted = false
729 729 };
730 730 entities.Add(dictEdit);
... ... @@ -736,7 +736,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
736 736 PermissionCode = "system:dict:remove",
737 737 MenuType = MenuTypeEnum.Component,
738 738 OrderNum = 100,
739   - ParentId = dict.Id,
  739 + ParentId = dict.Id.ToString(),
740 740 IsDeleted = false
741 741 };
742 742 entities.Add(dictRemove);
... ... @@ -756,7 +756,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
756 756 Component = "system/config/index",
757 757 MenuIcon = "ant-design:setting-outlined",
758 758 OrderNum = 94,
759   - ParentId = system.Id,
  759 + ParentId = system.Id.ToString(),
760 760 IsDeleted = false
761 761 };
762 762 entities.Add(config);
... ... @@ -768,7 +768,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
768 768 PermissionCode = "system:config:query",
769 769 MenuType = MenuTypeEnum.Component,
770 770 OrderNum = 100,
771   - ParentId = config.Id,
  771 + ParentId = config.Id.ToString(),
772 772 IsDeleted = false
773 773 };
774 774 entities.Add(configQuery);
... ... @@ -780,7 +780,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
780 780 PermissionCode = "system:config:add",
781 781 MenuType = MenuTypeEnum.Component,
782 782 OrderNum = 100,
783   - ParentId = config.Id,
  783 + ParentId = config.Id.ToString(),
784 784 IsDeleted = false
785 785 };
786 786 entities.Add(configAdd);
... ... @@ -792,7 +792,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
792 792 PermissionCode = "system:config:edit",
793 793 MenuType = MenuTypeEnum.Component,
794 794 OrderNum = 100,
795   - ParentId = config.Id,
  795 + ParentId = config.Id.ToString(),
796 796 IsDeleted = false
797 797 };
798 798 entities.Add(configEdit);
... ... @@ -804,7 +804,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
804 804 PermissionCode = "system:config:remove",
805 805 MenuType = MenuTypeEnum.Component,
806 806 OrderNum = 100,
807   - ParentId = config.Id,
  807 + ParentId = config.Id.ToString(),
808 808 IsDeleted = false
809 809 };
810 810 entities.Add(configRemove);
... ... @@ -826,7 +826,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
826 826 Component = "system/notice/index",
827 827 MenuIcon = "fe:notice-push",
828 828 OrderNum = 93,
829   - ParentId = system.Id,
  829 + ParentId = system.Id.ToString(),
830 830 IsDeleted = false
831 831 };
832 832 entities.Add(notice);
... ... @@ -838,7 +838,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
838 838 PermissionCode = "system:notice:query",
839 839 MenuType = MenuTypeEnum.Component,
840 840 OrderNum = 100,
841   - ParentId = notice.Id,
  841 + ParentId = notice.Id.ToString(),
842 842 IsDeleted = false
843 843 };
844 844 entities.Add(noticeQuery);
... ... @@ -850,7 +850,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
850 850 PermissionCode = "system:notice:add",
851 851 MenuType = MenuTypeEnum.Component,
852 852 OrderNum = 100,
853   - ParentId = notice.Id,
  853 + ParentId = notice.Id.ToString(),
854 854 IsDeleted = false
855 855 };
856 856 entities.Add(noticeAdd);
... ... @@ -862,7 +862,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
862 862 PermissionCode = "system:notice:edit",
863 863 MenuType = MenuTypeEnum.Component,
864 864 OrderNum = 100,
865   - ParentId = notice.Id,
  865 + ParentId = notice.Id.ToString(),
866 866 IsDeleted = false
867 867 };
868 868 entities.Add(noticeEdit);
... ... @@ -874,7 +874,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
874 874 PermissionCode = "system:notice:remove",
875 875 MenuType = MenuTypeEnum.Component,
876 876 OrderNum = 100,
877   - ParentId = notice.Id,
  877 + ParentId = notice.Id.ToString(),
878 878 IsDeleted = false
879 879 };
880 880 entities.Add(noticeRemove);
... ... @@ -892,7 +892,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
892 892 IsLink = false,
893 893 MenuIcon = "material-symbols:logo-dev-outline",
894 894 OrderNum = 92,
895   - ParentId = system.Id,
  895 + ParentId = system.Id.ToString(),
896 896 IsDeleted = false
897 897 };
898 898 entities.Add(log);
... ... @@ -911,7 +911,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
911 911 Component = "monitor/operlog/index",
912 912 MenuIcon = "tabler:align-box-right-middle",
913 913 OrderNum = 100,
914   - ParentId = log.Id,
  914 + ParentId = log.Id.ToString(),
915 915 IsDeleted = false
916 916 };
917 917 entities.Add(operationLog);
... ... @@ -923,7 +923,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
923 923 PermissionCode = "monitor:operlog:query",
924 924 MenuType = MenuTypeEnum.Component,
925 925 OrderNum = 100,
926   - ParentId = operationLog.Id,
  926 + ParentId = operationLog.Id.ToString(),
927 927 IsDeleted = false
928 928 };
929 929 entities.Add(operationLogQuery);
... ... @@ -935,7 +935,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
935 935 PermissionCode = "monitor:operlog:remove",
936 936 MenuType = MenuTypeEnum.Component,
937 937 OrderNum = 100,
938   - ParentId = operationLog.Id,
  938 + ParentId = operationLog.Id.ToString(),
939 939 IsDeleted = false
940 940 };
941 941 entities.Add(operationLogRemove);
... ... @@ -955,7 +955,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
955 955 Component = "monitor/logininfor/index",
956 956 MenuIcon = "tabler:align-box-right-middle",
957 957 OrderNum = 100,
958   - ParentId = log.Id,
  958 + ParentId = log.Id.ToString(),
959 959 IsDeleted = false
960 960 };
961 961 entities.Add(loginLog);
... ... @@ -967,7 +967,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
967 967 PermissionCode = "monitor:logininfor:query",
968 968 MenuType = MenuTypeEnum.Component,
969 969 OrderNum = 100,
970   - ParentId = loginLog.Id,
  970 + ParentId = loginLog.Id.ToString(),
971 971 IsDeleted = false
972 972 };
973 973 entities.Add(loginLogQuery);
... ... @@ -979,7 +979,7 @@ namespace Yi.Framework.Rbac.SqlSugarCore.DataSeeds
979 979 PermissionCode = "monitor:logininfor:remove",
980 980 MenuType = MenuTypeEnum.Component,
981 981 OrderNum = 100,
982   - ParentId = loginLog.Id,
  982 + ParentId = loginLog.Id.ToString(),
983 983 IsDeleted = false
984 984 };
985 985 entities.Add(loginLogRemove);
... ...
项目相关文档/Dashboard统计接口对接说明.md 0 → 100644
  1 +# Dashboard 统计接口对接说明
  2 +
  3 +> 适用范围:美国版 Web 管理端 Dashboard 首页统计
  4 +>
  5 +> 接口实现:`IDashboardAppService.GetOverviewAsync` / `DashboardAppService.GetOverviewAsync`
  6 +
  7 +---
  8 +
  9 +## 1. 接口信息
  10 +
  11 +- **方法**:`GET`
  12 +- **路径**:`/api/app/dashboard/overview`
  13 +- **鉴权**:需要登录(Bearer Token)
  14 +- **请求参数**:无
  15 +
  16 +---
  17 +
  18 +## 2. 返回结构(顶层)
  19 +
  20 +```json
  21 +{
  22 + "labelsPrintedToday": {},
  23 + "activeTemplates": {},
  24 + "activeUsers": {},
  25 + "locations": {},
  26 + "people": {},
  27 + "products": {},
  28 + "weeklyPrintVolume": [],
  29 + "byCategory": [],
  30 + "byCategoryTotal": 0,
  31 + "recentLabels": [],
  32 + "generatedAt": "2026-04-22T10:00:00+08:00",
  33 +
  34 + "metricCards": [],
  35 + "categoryDistribution": [],
  36 + "categoryDistributionTotal": 0
  37 +}
  38 +```
  39 +
  40 +说明:
  41 +- `labelsPrintedToday/activeTemplates/...`、`byCategory/byCategoryTotal` 是**前端直观命名**(推荐使用)。
  42 +- `metricCards`、`categoryDistribution`、`categoryDistributionTotal` 为**兼容字段**(与旧版返回一致)。
  43 +- `recentLabels`:**Recent Labels** 区块数据,全门店按打印时间倒序取最新 **10** 条(`fl_label_print_task`)。
  44 +
  45 +---
  46 +
  47 +## 3. 字段说明
  48 +
  49 +### 3.1 指标卡片对象(`DashboardMetricCardDto`)
  50 +
  51 +用于以下字段:
  52 +- `labelsPrintedToday`
  53 +- `activeTemplates`
  54 +- `activeUsers`
  55 +- `locations`
  56 +- `people`
  57 +- `products`
  58 +- `metricCards[]`(同结构)
  59 +
  60 +| 字段 | 类型 | 说明 |
  61 +|---|---|---|
  62 +| `key` | string | 指标标识(如 `labelsPrintedToday`) |
  63 +| `title` | string | 指标标题 |
  64 +| `value` | int | 当前值 |
  65 +| `previousValue` | int | 对比周期值 |
  66 +| `changeValue` | int | 增减值(`value - previousValue`) |
  67 +| `changeRate` | decimal | 增减比例(百分比,保留 2 位) |
  68 +
  69 +---
  70 +
  71 +### 3.2 周趋势(`weeklyPrintVolume`)
  72 +
  73 +| 字段 | 类型 | 说明 |
  74 +|---|---|---|
  75 +| `date` | string | 日期,格式 `yyyy-MM-dd` |
  76 +| `value` | int | 当天打印量 |
  77 +
  78 +---
  79 +
  80 +### 3.3 分类分布(`byCategory`)
  81 +
  82 +`byCategory` 与 `categoryDistribution` 结构一致。
  83 +
  84 +| 字段 | 类型 | 说明 |
  85 +|---|---|---|
  86 +| `categoryId` | string | 分类 Id(`fl_label_category.Id`) |
  87 +| `categoryName` | string | 分类名称 |
  88 +| `count` | int | 该分类下标签数量 |
  89 +| `ratio` | decimal | 占比(百分比,保留 2 位) |
  90 +
  91 +---
  92 +
  93 +### 3.4 最近打印标签(`recentLabels`)
  94 +
  95 +数组元素类型:`DashboardRecentLabelItemDto`,按 **`PrintedAt`(`PrintedAt` 为空则用 `CreationTime`)倒序**,最多 **10** 条。
  96 +
  97 +| 字段 | 类型 | 说明 |
  98 +|---|---|---|
  99 +| `taskId` | string | 打印任务 Id(`fl_label_print_task.Id`) |
  100 +| `labelCode` | string | 标签编码(界面 Serial,如 `1-251201`) |
  101 +| `displayName` | string | 展示标题:优先 **产品名**,否则 **标签名** |
  102 +| `printedByUserId` | string \| null | 打印人用户 Id(`CreatedBy`) |
  103 +| `printedByName` | string | 打印人展示名(`User.Name` 或 `UserName`) |
  104 +| `printedAt` | string (datetime) | 打印时间(ISO 8601) |
  105 +| `status` | string | `active` 或 `expired`:从 `PrintInputJson` 解析 `expiryDate` / `expiry` / `expirationDate`,与**当天日期**比较;无保质期或解析失败视为 `active` |
  106 +| `labelTypeBadge` | string | 模板尺寸短文案(如 `2"x2"`),用于左侧圆标 |
  107 +
  108 +前端可用 `printedAt` 自行格式化为「10 mins ago」等相对时间。
  109 +
  110 +---
  111 +
  112 +## 4. 返回示例
  113 +
  114 +```json
  115 +{
  116 + "labelsPrintedToday": {
  117 + "key": "labelsPrintedToday",
  118 + "title": "Labels Printed Today",
  119 + "value": 342,
  120 + "previousValue": 305,
  121 + "changeValue": 37,
  122 + "changeRate": 12.13
  123 + },
  124 + "activeTemplates": {
  125 + "key": "activeTemplates",
  126 + "title": "Active Templates",
  127 + "value": 24,
  128 + "previousValue": 22,
  129 + "changeValue": 2,
  130 + "changeRate": 9.09
  131 + },
  132 + "activeUsers": {
  133 + "key": "activeUsers",
  134 + "title": "Active Users",
  135 + "value": 8,
  136 + "previousValue": 7,
  137 + "changeValue": 1,
  138 + "changeRate": 14.29
  139 + },
  140 + "locations": {
  141 + "key": "locations",
  142 + "title": "Locations",
  143 + "value": 12,
  144 + "previousValue": 11,
  145 + "changeValue": 1,
  146 + "changeRate": 9.09
  147 + },
  148 + "people": {
  149 + "key": "people",
  150 + "title": "People",
  151 + "value": 48,
  152 + "previousValue": 45,
  153 + "changeValue": 3,
  154 + "changeRate": 6.67
  155 + },
  156 + "products": {
  157 + "key": "products",
  158 + "title": "Products",
  159 + "value": 156,
  160 + "previousValue": 156,
  161 + "changeValue": 0,
  162 + "changeRate": 0
  163 + },
  164 + "weeklyPrintVolume": [
  165 + { "date": "2026-04-16", "value": 142 },
  166 + { "date": "2026-04-17", "value": 226 },
  167 + { "date": "2026-04-18", "value": 185 },
  168 + { "date": "2026-04-19", "value": 261 },
  169 + { "date": "2026-04-20", "value": 192 },
  170 + { "date": "2026-04-21", "value": 121 },
  171 + { "date": "2026-04-22", "value": 342 }
  172 + ],
  173 + "byCategory": [
  174 + { "categoryId": "CAT001", "categoryName": "Breakfast", "count": 420, "ratio": 42.00 },
  175 + { "categoryId": "CAT002", "categoryName": "Lunch", "count": 350, "ratio": 35.00 },
  176 + { "categoryId": "CAT003", "categoryName": "Dinner", "count": 230, "ratio": 23.00 }
  177 + ],
  178 + "byCategoryTotal": 1000,
  179 + "recentLabels": [
  180 + {
  181 + "taskId": "…",
  182 + "labelCode": "1-251201",
  183 + "displayName": "Chicken Breast",
  184 + "printedByUserId": "…",
  185 + "printedByName": "Alice J.",
  186 + "printedAt": "2026-04-22T09:50:00+08:00",
  187 + "status": "active",
  188 + "labelTypeBadge": "2\"x2\""
  189 + }
  190 + ],
  191 + "generatedAt": "2026-04-22T10:00:00+08:00",
  192 + "metricCards": [],
  193 + "categoryDistribution": [],
  194 + "categoryDistributionTotal": 1000
  195 +}
  196 +```
  197 +
  198 +---
  199 +
  200 +## 5. 统计口径说明
  201 +
  202 +### 5.1 Labels Printed Today
  203 +- 当前值:`fl_label_print_task` 在“今日 00:00~次日 00:00”的记录数。
  204 +- 对比值:昨日同口径记录数。
  205 +
  206 +### 5.2 Active Templates
  207 +- 当前值:`fl_label_template` 中 `IsDeleted = false AND State = true` 数量。
  208 +- 对比值:同口径且 `CreationTime < 最近7天起始日` 的数量。
  209 +
  210 +### 5.3 Active Users
  211 +- 当前值:`User` 表中 `IsDeleted = false AND State = true` 数量。
  212 +- 对比值:同口径且 `CreationTime < 最近7天起始日` 的数量。
  213 +
  214 +### 5.4 Locations
  215 +- 当前值:`location` 表中 `IsDeleted = false` 数量。
  216 +- 对比值:同口径且 `CreationTime < 最近7天起始日` 的数量。
  217 +
  218 +### 5.5 People
  219 +- 当前值:`User` 表中 `IsDeleted = false` 数量。
  220 +- 对比值:同口径且 `CreationTime < 最近7天起始日` 的数量。
  221 +
  222 +### 5.6 Products
  223 +- 当前值:`fl_product` 表中 `IsDeleted = false` 数量。
  224 +- 对比值:当前版本由于 `FlProductDbEntity` 未映射 `CreationTime`,临时按同口径总量返回(即变化可能为 0)。
  225 +
  226 +### 5.7 Weekly Print Volume
  227 +- 统计最近 7 天(含今天)每天 `fl_label_print_task` 数量。
  228 +- 无数据日期补 0。
  229 +
  230 +### 5.8 By Category
  231 +- 基于启用且未删除的 `fl_label_category` 作为分类集合。
  232 +- 统计 `fl_label` 中未删除且 `LabelCategoryId` 命中的数量。
  233 +- 占比按 `count / byCategoryTotal * 100` 计算,保留 2 位。
  234 +
  235 +---
  236 +
  237 +## 6. 前端接入建议
  238 +
  239 +- 新页面优先使用:
  240 + - 指标:`labelsPrintedToday`、`activeTemplates`、`activeUsers`、`locations`、`people`、`products`
  241 + - 图表:`weeklyPrintVolume`
  242 + - 环图:`byCategory` + `byCategoryTotal`
  243 +- 旧逻辑仍可使用兼容字段:
  244 + - `metricCards`
  245 + - `categoryDistribution`
  246 + - `categoryDistributionTotal`
  247 +
... ...
项目相关文档/产品模块Categories接口对接说明.md
... ... @@ -8,7 +8,9 @@
8 8 - **接口前缀**:宿主统一前缀为 `/api/app`
9 9 - **分类表**:`fl_product_category`
10 10 - **关联字段**:`fl_product.category_id` → `fl_product_category.id`
11   -- **图片字段**:`CategoryPhotoUrl`(前端字段:`categoryPhotoUrl`)
  11 +- **外观字段(字符串落库,内容为 JSON 文本)**:
  12 + - `ButtonAppearance`(`buttonAppearance`):如 `["TEXT","COLOR"]`、仅图片 `["IMAGE"]`、或合法 JSON 对象/数组;兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时会规范为 JSON 数组,如 `["TEXT"]`)。
  13 + - `CategoryPhotoUrl`(`categoryPhotoUrl`):与外观配合的**展示数据**,同样为 **JSON 字符串**(如 `["Prep","#10B981"]`、图片 URL 数组等);若传入**非 JSON** 的纯文本(如旧数据中的 `#EC4899` 或 `/picture/...`),后端会序列化为合法 JSON 字符串再存储。列表/详情/App 树**原样返回**库中字符串,由前端解析。
12 14  
13 15 > 说明:本文以 Swagger 为准(本地示例:`http://localhost:19001/swagger`,搜索 `ProductCategory`)。
14 16  
... ... @@ -57,7 +59,10 @@ Authorization: Bearer eyJhbGciOi...
57 59 | `id` | string | 主键 |
58 60 | `categoryCode` | string | 类别编码 |
59 61 | `categoryName` | string | 类别名称 |
60   -| `categoryPhotoUrl` | string \| null | 类别图片 URL(建议用 `/picture/...`) |
  62 +| `displayText` | string \| null | 按钮展示文案(空可回退 `categoryName`) |
  63 +| `categoryPhotoUrl` | string \| null | 分类展示数据,**JSON 格式字符串**(含义由前端与 `buttonAppearance` 约定) |
  64 +| `buttonAppearance` | string | 按钮外观,**JSON 格式字符串**(见上文「外观字段」) |
  65 +| `availabilityType` | string | `ALL` / `SPECIFIED`(门店可用范围) |
61 66 | `state` | boolean | 是否启用 |
62 67 | `orderNum` | number | 排序 |
63 68 | `lastEdited` | string | 最后编辑时间 |
... ... @@ -75,7 +80,10 @@ Authorization: Bearer eyJhbGciOi...
75 80 "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
76 81 "categoryCode": "CAT_PREP",
77 82 "categoryName": "Prep",
78   - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png",
  83 + "displayText": "Prep",
  84 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  85 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  86 + "availabilityType": "ALL",
79 87 "state": true,
80 88 "orderNum": 100,
81 89 "lastEdited": "2026-03-25 12:30:10"
... ... @@ -108,7 +116,11 @@ Authorization: Bearer eyJhbGciOi...
108 116 "id": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
109 117 "categoryCode": "CAT_PREP",
110 118 "categoryName": "Prep",
111   - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png",
  119 + "displayText": "Prep",
  120 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  121 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  122 + "availabilityType": "ALL",
  123 + "locationIds": [],
112 124 "state": true,
113 125 "orderNum": 100
114 126 }
... ... @@ -130,7 +142,11 @@ Authorization: Bearer eyJhbGciOi...
130 142 |------|------|------|------|
131 143 | `categoryCode` | string | 是 | 类别编码(唯一) |
132 144 | `categoryName` | string | 是 | 类别名称(唯一) |
133   -| `categoryPhotoUrl` | string \| null | 否 | 图片 URL(建议先上传图片拿到 `/picture/...` 再保存) |
  145 +| `displayText` | string \| null | 否 | 按钮展示文案 |
  146 +| `categoryPhotoUrl` | string \| null | 否 | **JSON 字符串**;与 `buttonAppearance` 配合(见概述)。纯路径等非 JSON 文本会被后端包成 JSON 字符串存储。 |
  147 +| `buttonAppearance` | string | 否 | **JSON 字符串**;未传或空白时后端默认 `["TEXT"]`。兼容传 `TEXT`/`COLOR`/`IMAGE` 单行(会规范为 `["TEXT"]` 等)。非法非 JSON 且非上述三者时报错。 |
  148 +| `availabilityType` | string | 否 | `ALL`(默认)或 `SPECIFIED` |
  149 +| `locationIds` | string[] | 条件 | `availabilityType=SPECIFIED` 时必填且至少 1 个门店 Id |
134 150 | `state` | boolean | 否 | 是否启用(默认 true) |
135 151 | `orderNum` | number | 否 | 排序(默认 0) |
136 152  
... ... @@ -140,7 +156,11 @@ Authorization: Bearer eyJhbGciOi...
140 156 {
141 157 "categoryCode": "CAT_PREP",
142 158 "categoryName": "Prep",
143   - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png",
  159 + "displayText": "Prep",
  160 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  161 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  162 + "availabilityType": "ALL",
  163 + "locationIds": [],
144 164 "state": true,
145 165 "orderNum": 100
146 166 }
... ... @@ -162,7 +182,11 @@ Authorization: Bearer eyJhbGciOi...
162 182 {
163 183 "categoryCode": "CAT_PREP",
164 184 "categoryName": "Prep",
165   - "categoryPhotoUrl": "/picture/category/20260325123010_xxx.png",
  185 + "displayText": "Prep",
  186 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  187 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  188 + "availabilityType": "ALL",
  189 + "locationIds": [],
166 190 "state": true,
167 191 "orderNum": 100
168 192 }
... ... @@ -179,7 +203,7 @@ Authorization: Bearer eyJhbGciOi...
179 203  
180 204 ### 约束
181 205  
182   -- 若该类别已被 `fl_label` 引用(`fl_label.LabelCategoryId = id`),删除会失败并返回友好提示:`该类别已被标签引用,无法删除`。
  206 +- 若该类别已被 `fl_product` 引用(`fl_product.CategoryId = id`),删除会失败并返回友好提示:`该类别已被产品引用,无法删除`。
183 207  
184 208 ### 请求示例
185 209  
... ... @@ -200,5 +224,5 @@ Authorization: Bearer eyJhbGciOi...
200 224 推荐前端流程:
201 225  
202 226 1. 调用上传接口 `POST /api/app/picture/category/upload` 拿到响应 `url`
203   -2. 新增/编辑类别时把 `categoryPhotoUrl` 设为该 `url`
  227 +2. 新增/编辑类别时:若采用 **JSON** 存展示数据,将 `url` 写入你方约定的 JSON 结构(例如 `["IMAGE","/picture/..."]`);若仍传**纯路径字符串**,后端会将其序列化为 JSON 字符串再入库(与仅图片场景兼容)。
204 228  
... ...
项目相关文档/合作伙伴Partner接口对接说明.md 0 → 100644
  1 +# 合作伙伴(Partner)与组织(Group)接口对接说明
  2 +
  3 +> 适用范围:美国版 Web 管理端「Account Management」下的 **Partner**、**Group** 主数据
  4 +> **Partner** 表:`fl_partner`,接口:`IPartnerAppService` / `PartnerAppService`
  5 +> **Group** 表:`fl_group`(`PartnerId` 关联 `fl_partner.Id`),接口:`IGroupAppService` / `GroupAppService`
  6 +> 宿主路由前缀:`/api/app`(与 `YiAbpWebModule` 中 `RootPath = api/app` 一致)
  7 +
  8 +---
  9 +
  10 +## 0. 通用说明
  11 +
  12 +- **鉴权**:需要登录(`Authorization: Bearer {token}`),与其它 `/api/app/*` 接口一致。
  13 +- **Content-Type**:`POST` / `PUT` 使用 `application/json`。
  14 +- **分页约定(美国版食品标签模块)**:`skipCount` 表示**页码(从 1 起)**,不是 0 基 offset;第一页请传 `skipCount=1`。与 `PagedQueryConvention` 及 SqlSugar `ToPageListAsync` 用法一致。
  15 +- **逻辑删除**:`DELETE` 将对应表的 `IsDeleted` 置为 `true`(`fl_partner` / `fl_group`),不物理删行。
  16 +- **列表与导出筛选一致**:各模块列表的筛选字段与对应 **export-pdf** 接口一致,便于数据对齐。
  17 +- **Group 与 Partner**:新建/编辑 Group 时 `partnerId` 必须指向**未逻辑删除**的 `fl_partner`;列表左联 Partner 时仅展示未删除合作伙伴名称,已删 Partner 在列表中显示为 **`无`**。
  18 +
  19 +---
  20 +
  21 +# 第一部分:Partner(合作伙伴)
  22 +
  23 +## 1. 分页列表
  24 +
  25 +- **方法**:`GET`
  26 +- **路径**:`/api/app/partner`
  27 +
  28 +### 1.1 查询参数(`PartnerGetListInputVo`)
  29 +
  30 +| 参数 | 类型 | 必填 | 说明 |
  31 +|------|------|------|------|
  32 +| `skipCount` | int | 是 | 页码,从 **1** 开始 |
  33 +| `maxResultCount` | int | 是 | 每页条数 |
  34 +| `sorting` | string | 否 | 排序,仅支持白名单(见下) |
  35 +| `keyword` | string | 否 | 模糊匹配 `PartnerName`、`ContactEmail`、`PhoneNumber` |
  36 +| `state` | bool | 否 | 按启用状态筛选;不传则不过滤 |
  37 +
  38 +**排序白名单**(大小写不敏感):
  39 +
  40 +- `PartnerName asc` / `PartnerName desc`
  41 +- `CreationTime asc` / `CreationTime desc`
  42 +- `State asc` / `State desc`
  43 +
  44 +其它值将回退为默认:**按 `CreationTime` 降序**。
  45 +
  46 +### 1.2 请求示例
  47 +
  48 +```http
  49 +GET /api/app/partner?skipCount=1&maxResultCount=10&keyword=Global&state=true&sorting=CreationTime%20desc HTTP/1.1
  50 +Authorization: Bearer {token}
  51 +```
  52 +
  53 +### 1.3 响应结构(`PagedResultWithPageDto<PartnerGetListOutputDto>`)
  54 +
  55 +```json
  56 +{
  57 + "pageIndex": 1,
  58 + "pageSize": 10,
  59 + "totalCount": 2,
  60 + "totalPages": 1,
  61 + "items": [
  62 + {
  63 + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  64 + "partnerName": "Global Foods Inc.",
  65 + "contactEmail": "admin@globalfoods.com",
  66 + "phoneNumber": "+1 (555) 100-2000",
  67 + "state": true,
  68 + "creationTime": "2026-04-27T10:00:00"
  69 + }
  70 + ]
  71 +}
  72 +```
  73 +
  74 +### 1.4 列表项字段(`PartnerGetListOutputDto`)
  75 +
  76 +| 字段 | 类型 | 说明 |
  77 +|------|------|------|
  78 +| `id` | string | 主键 |
  79 +| `partnerName` | string | 合作伙伴名称 |
  80 +| `contactEmail` | string \| null | 联系邮箱 |
  81 +| `phoneNumber` | string \| null | 电话 |
  82 +| `state` | bool | 是否启用(UI Active 开关) |
  83 +| `creationTime` | string (datetime) | 创建时间 |
  84 +
  85 +---
  86 +
  87 +## 2. 详情
  88 +
  89 +- **方法**:`GET`
  90 +- **路径**:`/api/app/partner/{id}`
  91 +
  92 +### 2.1 路径参数
  93 +
  94 +| 参数 | 说明 |
  95 +|------|------|
  96 +| `id` | `fl_partner.Id` |
  97 +
  98 +### 2.2 响应结构(`PartnerGetOutputDto`)
  99 +
  100 +| 字段 | 类型 | 说明 |
  101 +|------|------|------|
  102 +| `id` | string | 主键 |
  103 +| `partnerName` | string | 合作伙伴名称 |
  104 +| `contactEmail` | string \| null | 联系邮箱 |
  105 +| `phoneNumber` | string \| null | 电话 |
  106 +| `state` | bool | 是否启用 |
  107 +| `creationTime` | string (datetime) | 创建时间 |
  108 +| `lastModificationTime` | string (datetime) \| null | 最后修改时间 |
  109 +
  110 +### 2.3 错误说明
  111 +
  112 +- `id` 为空或记录不存在(含已逻辑删除):业务错误提示「合作伙伴不存在」等。
  113 +
  114 +---
  115 +
  116 +## 3. 新增
  117 +
  118 +- **方法**:`POST`
  119 +- **路径**:`/api/app/partner`
  120 +
  121 +### 3.1 Body(`PartnerCreateInputVo`)
  122 +
  123 +```json
  124 +{
  125 + "partnerName": "Global Foods Inc.",
  126 + "contactEmail": "admin@globalfoods.com",
  127 + "phoneNumber": "+1 (555) 100-2000",
  128 + "state": true
  129 +}
  130 +```
  131 +
  132 +| 字段 | 类型 | 必填 | 说明 |
  133 +|------|------|------|------|
  134 +| `partnerName` | string | 是 | 合作伙伴名称,去首尾空格后不能为空 |
  135 +| `contactEmail` | string | 否 | 若填写则做简单格式校验(含 `@` 等) |
  136 +| `phoneNumber` | string | 否 | 电话 |
  137 +| `state` | bool | 否 | 默认 `true` |
  138 +
  139 +### 3.2 响应
  140 +
  141 +- 成功:返回 `PartnerGetOutputDto`(与详情结构一致)。
  142 +
  143 +---
  144 +
  145 +## 4. 编辑
  146 +
  147 +- **方法**:`PUT`
  148 +- **路径**:`/api/app/partner/{id}`
  149 +
  150 +### 4.1 参数
  151 +
  152 +- **Path**:`id` 为当前合作伙伴主键。
  153 +- **Body**:`PartnerUpdateInputVo`,字段与新增相同。
  154 +
  155 +```json
  156 +{
  157 + "partnerName": "Global Foods Inc.",
  158 + "contactEmail": "admin@globalfoods.com",
  159 + "phoneNumber": "+1 (555) 100-2000",
  160 + "state": false
  161 +}
  162 +```
  163 +
  164 +### 4.2 响应
  165 +
  166 +- 成功:返回更新后的 `PartnerGetOutputDto`。
  167 +
  168 +---
  169 +
  170 +## 5. 删除(逻辑删除)
  171 +
  172 +- **方法**:`DELETE`
  173 +- **路径**:`/api/app/partner/{id}`
  174 +
  175 +### 5.1 路径参数
  176 +
  177 +| 参数 | 说明 |
  178 +|------|------|
  179 +| `id` | `fl_partner.Id` |
  180 +
  181 +### 5.2 行为
  182 +
  183 +- 将 `IsDeleted` 置为 `true`,并更新 `LastModificationTime` / `LastModifierId`(若当前用户存在)。
  184 +
  185 +---
  186 +
  187 +## 6. 批量导出 PDF
  188 +
  189 +- **方法**:`GET`
  190 +- **路径**:`/api/app/partner/export-pdf`
  191 +- **响应**:`Content-Type: application/pdf`,附件名形如 `partners_yyyy-MM-dd_HH-mm-ss.pdf`
  192 +
  193 +### 6.1 查询参数
  194 +
  195 +与列表接口相同的筛选字段(分页字段可忽略):
  196 +
  197 +| 参数 | 类型 | 必填 | 说明 |
  198 +|------|------|------|------|
  199 +| `keyword` | string | 否 | 与列表 `keyword` 一致 |
  200 +| `state` | bool | 否 | 与列表 `state` 一致 |
  201 +| `sorting` | string | 否 | 与列表白名单一致,用于导出行顺序 |
  202 +
  203 +### 6.2 限制
  204 +
  205 +- 命中行数 **超过 5000** 时接口返回业务错误,需缩小筛选范围后再导出。
  206 +- 导出最多取 **5000** 条,排序与列表查询逻辑一致。
  207 +
  208 +### 6.3 请求示例
  209 +
  210 +```http
  211 +GET /api/app/partner/export-pdf?keyword=Global&state=true HTTP/1.1
  212 +Authorization: Bearer {token}
  213 +```
  214 +
  215 +### 6.4 PDF 内容说明
  216 +
  217 +- 表头列:**Partner**、**Contact**、**Phone**、**Status**、**Created**。
  218 +- `Status` 文本:`state === true` 时为 `active`,否则 `inactive`。
  219 +- 空邮箱、空电话在 PDF 中显示为 **`无`**(与项目列表空值展示约定一致)。
  220 +
  221 +---
  222 +
  223 +## 7. 数据库与建表(Partner)
  224 +
  225 +- 建表脚本:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_partner_create.sql`
  226 +- 主要字段:`Id`、`IsDeleted`、`CreationTime`、`CreatorId`、`LastModificationTime`、`LastModifierId`、`PartnerName`、`ContactEmail`、`PhoneNumber`、`State`
  227 +
  228 +---
  229 +
  230 +## 8. 与门店字段的关系说明
  231 +
  232 +- 门店(`location`)上可能存在 **`Partner` 字符串字段**(原型/筛选用),与本文 **`fl_partner` 主数据表** 无强制外键关联。
  233 +- 若后续要将门店关联到合作伙伴主数据,需单独产品方案(例如增加 `PartnerId` 或同步名称)。
  234 +
  235 +---
  236 +
  237 +## 9. 前端对接提示(Partner)
  238 +
  239 +- 列表「Search」对应 `keyword`;「Active」筛选对应 `state`。
  240 +- 「Bulk Export (PDF)」调用 **第 6 节** 导出接口,查询参数与当前列表筛选保持一致即可。
  241 +
  242 +---
  243 +
  244 +# 第二部分:Group(组织 / Group)
  245 +
  246 +> UI:**Group Name**、**Parent Partner**(下拉绑定合作伙伴)、**Status**、**Bulk Export (PDF)**、**New+** 弹窗(Group Name、Assign to Partner、Active)。
  247 +
  248 +## 10. 数据库与建表(Group)
  249 +
  250 +- 库中原先**无**独立 Group 业务表;新建表名:`fl_group`。
  251 +- 建表脚本:`美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_group_create.sql`
  252 +- **须先存在 `fl_partner` 表**(脚本内含外键 `FK_fl_group_partner` → `fl_partner(Id)`)。
  253 +- 主要字段:`Id`、`IsDeleted`、`CreationTime`、`CreatorId`、`LastModificationTime`、`LastModifierId`、`GroupName`、`PartnerId`、`State`
  254 +
  255 +**建表 SQL(与脚本文件一致,便于直接执行):**
  256 +
  257 +```sql
  258 +CREATE TABLE IF NOT EXISTS `fl_group` (
  259 + `Id` varchar(64) NOT NULL COMMENT '主键',
  260 + `IsDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
  261 + `CreationTime` datetime(6) NOT NULL COMMENT '创建时间',
  262 + `CreatorId` varchar(64) DEFAULT NULL COMMENT '创建人',
  263 + `LastModificationTime` datetime(6) DEFAULT NULL COMMENT '最后修改时间',
  264 + `LastModifierId` varchar(64) DEFAULT NULL COMMENT '最后修改人',
  265 + `GroupName` varchar(256) NOT NULL COMMENT '组织名称',
  266 + `PartnerId` varchar(64) NOT NULL COMMENT '所属合作伙伴 fl_partner.Id',
  267 + `State` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
  268 + PRIMARY KEY (`Id`),
  269 + KEY `IX_fl_group_IsDeleted` (`IsDeleted`),
  270 + KEY `IX_fl_group_State` (`State`),
  271 + KEY `IX_fl_group_PartnerId` (`PartnerId`),
  272 + KEY `IX_fl_group_GroupName` (`GroupName`(128)),
  273 + KEY `IX_fl_group_CreationTime` (`CreationTime`),
  274 + CONSTRAINT `FK_fl_group_partner` FOREIGN KEY (`PartnerId`) REFERENCES `fl_partner` (`Id`)
  275 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='组织(Group)';
  276 +```
  277 +
  278 +---
  279 +
  280 +## 11. Group — 分页列表
  281 +
  282 +- **方法**:`GET`
  283 +- **路径**:`/api/app/group`
  284 +
  285 +### 11.1 查询参数(`GroupGetListInputVo`)
  286 +
  287 +| 参数 | 类型 | 必填 | 说明 |
  288 +|------|------|------|------|
  289 +| `skipCount` | int | 是 | 页码,从 **1** 开始 |
  290 +| `maxResultCount` | int | 是 | 每页条数 |
  291 +| `sorting` | string | 否 | 排序白名单(见下) |
  292 +| `keyword` | string | 否 | 模糊匹配 `GroupName`、所属 **未删除** Partner 的 `PartnerName` |
  293 +| `partnerId` | string | 否 | 仅查看某合作伙伴下的组织(`fl_partner.Id`) |
  294 +| `state` | bool | 否 | 按启用状态筛选;不传则不过滤 |
  295 +
  296 +**排序白名单**(大小写不敏感):
  297 +
  298 +- `GroupName asc` / `GroupName desc`
  299 +- `CreationTime asc` / `CreationTime desc`
  300 +- `State asc` / `State desc`
  301 +- `PartnerName asc` / `PartnerName desc`(按关联合作伙伴名称)
  302 +
  303 +其它值回退为默认:**按 `CreationTime` 降序**。
  304 +
  305 +### 11.2 请求示例
  306 +
  307 +```http
  308 +GET /api/app/group?skipCount=1&maxResultCount=10&keyword=West&partnerId={partnerGuid}&state=true HTTP/1.1
  309 +Authorization: Bearer {token}
  310 +```
  311 +
  312 +### 11.3 响应结构(`PagedResultWithPageDto<GroupGetListOutputDto>`)
  313 +
  314 +```json
  315 +{
  316 + "pageIndex": 1,
  317 + "pageSize": 10,
  318 + "totalCount": 2,
  319 + "totalPages": 1,
  320 + "items": [
  321 + {
  322 + "id": "…",
  323 + "groupName": "West Coast Region",
  324 + "partnerId": "…",
  325 + "partnerName": "Global Foods Inc.",
  326 + "state": true,
  327 + "creationTime": "2026-04-27T10:00:00"
  328 + }
  329 + ]
  330 +}
  331 +```
  332 +
  333 +### 11.4 列表项字段
  334 +
  335 +| 字段 | 类型 | 说明 |
  336 +|------|------|------|
  337 +| `id` | string | 主键 |
  338 +| `groupName` | string | 组织名称 |
  339 +| `partnerId` | string | 所属合作伙伴 Id |
  340 +| `partnerName` | string | 父级合作伙伴名称(UI「Parent Partner」) |
  341 +| `state` | bool | 是否启用 |
  342 +| `creationTime` | string (datetime) | 创建时间 |
  343 +
  344 +---
  345 +
  346 +## 12. Group — 详情
  347 +
  348 +- **方法**:`GET`
  349 +- **路径**:`/api/app/group/{id}`
  350 +
  351 +路径参数 `id`:`fl_group.Id`。响应为 `GroupGetOutputDto`(在列表字段基础上增加 `lastModificationTime`)。
  352 +
  353 +---
  354 +
  355 +## 13. Group — 新增
  356 +
  357 +- **方法**:`POST`
  358 +- **路径**:`/api/app/group`
  359 +
  360 +### Body(`GroupCreateInputVo`)
  361 +
  362 +```json
  363 +{
  364 + "groupName": "West Coast Region",
  365 + "partnerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  366 + "state": true
  367 +}
  368 +```
  369 +
  370 +| 字段 | 类型 | 必填 | 说明 |
  371 +|------|------|------|------|
  372 +| `groupName` | string | 是 | 组织名称 |
  373 +| `partnerId` | string | 是 | Assign to Partner,须为未删除的 `fl_partner.Id` |
  374 +| `state` | bool | 否 | 默认 `true` |
  375 +
  376 +---
  377 +
  378 +## 14. Group — 编辑
  379 +
  380 +- **方法**:`PUT`
  381 +- **路径**:`/api/app/group/{id}`
  382 +- **Body**:`GroupUpdateInputVo`(字段同新增)
  383 +
  384 +---
  385 +
  386 +## 15. Group — 删除(逻辑删除)
  387 +
  388 +- **方法**:`DELETE`
  389 +- **路径**:`/api/app/group/{id}`
  390 +
  391 +---
  392 +
  393 +## 16. Group — 批量导出 PDF
  394 +
  395 +- **方法**:`GET`
  396 +- **路径**:`/api/app/group/export-pdf`
  397 +- **响应**:`application/pdf`,附件名形如 `groups_yyyy-MM-dd_HH-mm-ss.pdf`
  398 +
  399 +### 16.1 查询参数
  400 +
  401 +与列表一致(分页可忽略):`keyword`、`partnerId`、`state`、`sorting`。
  402 +
  403 +### 16.2 限制
  404 +
  405 +- 命中行数 **超过 5000** 返回业务错误;导出最多 **5000** 条。
  406 +
  407 +### 16.3 PDF 列
  408 +
  409 +**Group Name**、**Parent Partner**、**Status**(`active` / `inactive`)、**Created**。
  410 +
  411 +---
  412 +
  413 +## 17. 前端对接提示(Group)
  414 +
  415 +- 「Search」→ `keyword`;按父级合作伙伴筛选 → `partnerId`(下拉选中项的 `id`);「Active」→ `state`。
  416 +- 「Assign to Partner」下拉数据来自 **Partner 列表接口**(`/api/app/partner`)。
  417 +- 「Bulk Export (PDF)」→ **第 16 节**,查询参数与当前列表筛选一致。
... ...
项目相关文档/平台端Categories图片上传接口说明.md
... ... @@ -54,7 +54,7 @@ curl -X POST &quot;http://localhost:19001/api/app/picture/category/upload&quot; ^
54 54  
55 55 | 字段 | 类型 | 说明 |
56 56 |------|------|------|
57   -| `url` | string | 图片访问的相对路径,可直接保存到 `CategoryPhotoUrl`(例如:`/picture/category/xxx.png`) |
  57 +| `url` | string | 图片访问的相对路径;写入分类接口时,若 `categoryPhotoUrl` 采用 **JSON** 存展示数据,请将该 `url` 放入你方约定的 JSON 结构中。若仍传**纯路径字符串**,后端会序列化为 JSON 字符串再入库。 |
58 58 | `fileName` | string | 服务器保存的文件名 |
59 59 | `size` | number | 文件大小(字节) |
60 60  
... ... @@ -96,7 +96,7 @@ curl -X POST &quot;http://localhost:19001/api/app/picture/category/upload&quot; ^
96 96 推荐前端流程:
97 97  
98 98 1. 调用本上传接口,拿到返回的 `url`
99   -2. 再调用分类新增/编辑接口,把 `categoryPhotoUrl` 设置为该 `url`
  99 +2. 再调用分类新增/编辑接口:按平台与 **`buttonAppearance`(JSON 字符串)** 的约定组装 `categoryPhotoUrl`(JSON);或继续传纯 `url` 由后端自动包成 JSON 字符串。
100 100  
101   -> 说明:分类 CRUD 已支持 `CategoryPhotoUrl` 字段;你只需要在页面表单里新增该字段即可
  101 +> 说明:详见 `项目相关文档/产品模块Categories接口对接说明.md`、`项目相关文档/标签模块接口对接说明.md` 中「JSON 字符串」约定
102 102  
... ...
项目相关文档/报表Reports接口对接说明.md 0 → 100644
  1 +# 报表 Reports 接口对接说明(Print Log / Label Report)
  2 +
  3 +> 适用范围:美国版 Web「Reports」— **Print Log** 列表与导出、**Label Report** 统计与导出、**重打**
  4 +> 实现:`IReportsAppService` / `ReportsAppService`;重打复用 `IUsAppLabelingAppService.ReprintAsync`(已支持 admin 跳过创建人校验)
  5 +> 路由前缀:`/api/app`
  6 +
  7 +---
  8 +
  9 +## 0. 角色与数据范围(必读)
  10 +
  11 +- 判断依据:`CurrentUser.Roles` 中是否存在**忽略大小写**等于 **`admin`** 的角色码(与 JWT 中角色码一致,参见 `AuthSessionAppService` / `ReportsRoleHelper`)。
  12 +- **`admin`**:**不按** `CreatedBy` 过滤,可查看/统计全部 `fl_label_print_task`(仍受 Partner/Group/Location/日期/关键字筛选)。
  13 +- **非 `admin`**:所有列表与统计仅包含 **`CreatedBy == 当前用户 Id`** 的打印任务。
  14 +- **重打**:非 admin 仅能重打本人任务;**`admin` 可重打任意用户任务**,但仍须 `locationId` 与历史任务一致(与 App 重打规则一致)。
  15 +
  16 +---
  17 +
  18 +## 1. Partner / Group / Location 筛选说明
  19 +
  20 +- **`locationId`**:若传则只查该门店(`location.Id` 字符串)。
  21 +- **`partnerId`**(`fl_partner.Id`):按合作伙伴名称与 `location.Partner` **文本全等**(trim 后)匹配,得到门店集合再过滤任务。
  22 +- **`groupId`**(`fl_group.Id`):按组织的 `GroupName` + 父级 `PartnerName` 与门店的 `GroupName` + `Partner` **文本全等**匹配门店。
  23 +- 若 Partner/Group 在库中不存在,返回**空列表/空统计**(不报错)。
  24 +- 未传 Partner/Group/Location 时:**不按门店集合预过滤**(仅日期、关键字、用户范围生效)。
  25 +
  26 +---
  27 +
  28 +## 2. Print Log — 分页查询
  29 +
  30 +- **方法**:`GET`
  31 +- **路径**:`/api/app/reports/print-log-list`(以 Swagger 为准;ABP 约定多为 `get-print-log-list` 映射到该路径)
  32 +
  33 +### 2.1 查询参数(`ReportsPrintLogGetListInputVo`)
  34 +
  35 +| 参数 | 类型 | 说明 |
  36 +|------|------|------|
  37 +| `skipCount` | int | 页码,从 **1** 起 |
  38 +| `maxResultCount` | int | 每页条数 |
  39 +| `sorting` | string | 可选:`PrintedAt asc` / `PrintedAt desc`(默认倒序) |
  40 +| `partnerId` | string | 可选 |
  41 +| `groupId` | string | 可选 |
  42 +| `locationId` | string | 可选 |
  43 +| `startDate` | date | 可选;默认与结束日组成约 30 天窗口 |
  44 +| `endDate` | date | 可选;默认今天(含当日) |
  45 +| `keyword` | string | 可选;匹配产品名、标签分类名、产品分类名(模糊) |
  46 +
  47 +### 2.2 响应项(`ReportsPrintLogListItemDto`)
  48 +
  49 +含:`taskId`、`labelCode`、`productName`、`categoryName`、`templateText`、`printedAt`、`printedByName`、`locationText`、`locationId`(重打必填)、`expiryDateText`(从 `PrintInputJson` 中尝试解析 `expiryDate` / `expiry` / `expirationDate`)。
  50 +
  51 +---
  52 +
  53 +## 3. Print Log — 导出 PDF
  54 +
  55 +- **方法**:`GET`
  56 +- **路径**:`/api/app/reports/export-print-log-pdf`
  57 +- **查询参数**:与 **§2** 相同(分页字段忽略);最多 **5000** 条,超出返回业务错误。
  58 +
  59 +---
  60 +
  61 +## 4. Print Log — 重打(Reprint)
  62 +
  63 +- **方法**:`POST`
  64 +- **路径**:`/api/app/reports/reprint-print-log`(实现内转发至 `UsAppLabelingAppService.ReprintAsync`,以 Swagger 为准)
  65 +- **Body**:`UsAppLabelReprintInputVo`:`locationId`、`taskId`、`printQuantity`、`clientRequestId`(可选)、打印机字段(可选)。
  66 +
  67 +---
  68 +
  69 +## 5. Label Report — 统计聚合
  70 +
  71 +- **方法**:`GET`
  72 +- **路径**:`/api/app/reports/label-report`(以 Swagger 为准)
  73 +
  74 +### 5.1 查询参数(`ReportsLabelReportQueryInputVo`)
  75 +
  76 +与 Print Log 相同的 `partnerId`、`groupId`、`locationId`、`startDate`、`endDate`、`keyword`(无分页)。
  77 +
  78 +### 5.2 默认时间窗
  79 +
  80 +未传日期时:**结束日 = 今天,开始日 = 结束日前推 29 天(共约 30 个自然日,含首尾)**。
  81 +
  82 +### 5.3 返回(`ReportsLabelReportOutputDto`)
  83 +
  84 +- **`summary`**:`totalLabelsPrinted`、上一同长周期 `totalLabelsPrintedPrevPeriod`、`totalLabelsPrintedChangeRate`(%);最热门标签分类名与次数;Top 产品名与次数;`avgDailyPrints` 及环比等。
  85 +- **`labelsByCategory`**:按 **标签分类**(`fl_label_category`)汇总当前区间内打印次数。
  86 +- **`printVolumeTrend`**:在**当前筛选日期区间内**,取**结束日向前最多 7 个自然日**(与区间求交)的按日打印量。
  87 +- **`mostUsedProducts`**:当前区间内产品打印次数 Top20,`usagePercent` 为占**当期总打印次数**的百分比。
  88 +
  89 +---
  90 +
  91 +## 6. Label Report — 导出 PDF
  92 +
  93 +- **方法**:`GET`
  94 +- **路径**:`/api/app/reports/export-label-report-pdf`
  95 +- **查询参数**:与 **§5** 相同;内容为摘要 + 分类表 + 日趋势表 + Top 产品表。
  96 +
  97 +---
  98 +
  99 +## 7. 数据表与字段依赖
  100 +
  101 +- 打印任务:`fl_label_print_task`(`CreatedBy`、`LocationId`、`PrintedAt`、`PrintInputJson` 等)
  102 +- 门店:`location`(`Partner`、`GroupName`、`LocationCode`、`LocationName`)
  103 +- 主数据:`fl_partner`、`fl_group`(筛选用)
  104 +- 用户:`User`(展示 `PrintedByName`)
  105 +
  106 +---
  107 +
  108 +## 8. 前端对接提示
  109 +
  110 +- Print Log 行内 **Reprint**:使用列表返回的 `locationId` + `taskId` 调重打接口。
  111 +- 「Export Report」在 Print Log Tab 调 **§3**;在 Label Report Tab 调 **§6**。
  112 +- 下拉 **Partner / Group / Location** 与列表筛选字段一致;需保证门店维护的 `Partner` / `GroupName` 与主数据名称一致,否则筛选结果为空。
... ...
项目相关文档/本次新增与优化接口汇总.md 0 → 100644
  1 +# 本次接口变更汇总(标签 / 产品类别 / 模板组件 / App 树 / Web 会话)
  2 +
  3 +> 说明:本文只汇总本次迭代中相关内容:
  4 +> - 标签模块 Label Categories:对齐“新增类别”原型图(`buttonAppearance` + `categoryPhotoUrl` / 展示文案 / 门店范围)
  5 +> - 产品模块 Categories:对齐“新增产品类别”原型图(同上)
  6 +> - 模板组件(`fl_label_template_element`):新增字段 `TypeAdd`(并补齐 `ElementName`)
  7 +> - App `labeling-tree`:L1 标签分类返回 `buttonAppearance`
  8 +> - Web 管理端 `auth-session`:**当前用户菜单与权限**、**退出登录**(食品标签-美国版模块)
  9 +> - Web `rbac-menu` 列表/详情:补充返回 `routerName`、`router`
  10 +> - 产品 **Products**:`POST/PUT /api/app/product` 支持可选 Body 字段 **`locationIds`**(多门店批量绑定 / 编辑时整表替换关联),详见 `项目相关文档/标签模块接口对接说明.md` **§6**
  11 +>
  12 +> 其余标签打印相关接口不在本文范围内。
  13 +
  14 +---
  15 +
  16 +## 1. 模板组件字段(`fl_label_template_element`)
  17 +
  18 +### 1.1 字段变更
  19 +
  20 +- 新增字段:`TypeAdd`(元素附加类型,如 `label_Duration`)
  21 +- 新增字段:`ElementName`(元素名称,用于更稳定的显示/快照)
  22 +
  23 +### 1.2 接口影响范围
  24 +
  25 +- 模板新增/编辑:保存 `elements[].typeAdd` / `elements[].elementName`
  26 +- 模板详情/预览:返回 `elements[].typeAdd` / `elements[].elementName`
  27 +
  28 +### 1.3 JSON 对齐(elements[])
  29 +
  30 +| 前端字段 | 后端字段 | 说明 |
  31 +|---|---|---|
  32 +| `type` | `ElementType` | 元素类型 |
  33 +| `typeAdd` | `TypeAdd` | 元素附加类型 |
  34 +| `elementName` | `ElementName` | 元素名称 |
  35 +
  36 +---
  37 +
  38 +## 2. 产品模块 Categories(Products → Categories)
  39 +
  40 +> 数据库侧你已完成:`fl_product_category` 新字段、`fl_product_category_location` 新表。
  41 +
  42 +### 2.1 表结构要点
  43 +
  44 +- `fl_product_category`(主表)关键字段:
  45 + - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`)
  46 + - `ButtonAppearance`:**JSON 格式字符串**落库(如 `["TEXT","COLOR"]`、`["IMAGE"]`);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时规范为 JSON 数组)
  47 + - `CategoryPhotoUrl`:**JSON 格式字符串**落库(展示数据由前端解析);非 JSON 纯文本(色值、URL 等)保存时会被后端包成 JSON 字符串
  48 + - **已删除列**(需在库上执行 `DROP COLUMN`):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson`
  49 + - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围)
  50 +- `fl_product_category_location`(关联表):
  51 + - `(CategoryId, LocationId)` 唯一约束;用于 `AvailabilityType=SPECIFIED` 指定门店
  52 +
  53 +### 2.2 CRUD 接口(字段扩展)
  54 +
  55 +接口路径不变,仅扩展字段。
  56 +
  57 +#### 2.2.1 列表
  58 +
  59 +- **方法**:`GET`
  60 +- **路径**:`/api/app/product-category`
  61 +- **列表行新增返回**:
  62 + - `displayText`
  63 + - `buttonAppearance`
  64 + - `categoryPhotoUrl`
  65 + - `availabilityType`
  66 +
  67 +#### 2.2.2 详情
  68 +
  69 +- **方法**:`GET`
  70 +- **路径**:`/api/app/product-category/{id}`
  71 +- **新增返回字段**:
  72 + - `displayText`
  73 + - `buttonAppearance`、`categoryPhotoUrl`(**JSON 格式字符串**,展示语义由前端解析)
  74 + - `availabilityType`
  75 + - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组)
  76 +
  77 +#### 2.2.3 新增
  78 +
  79 +- **方法**:`POST`
  80 +- **路径**:`/api/app/product-category`
  81 +- **新增入参字段**:
  82 + - `displayText`
  83 + - `buttonAppearance`、`categoryPhotoUrl`
  84 + - `availabilityType`
  85 + - `locationIds`(当 `availabilityType=SPECIFIED` 必填且至少 1 个)
  86 +
  87 +#### 2.2.4 编辑
  88 +
  89 +- **方法**:`PUT`
  90 +- **路径**:`/api/app/product-category/{id}`
  91 +- **入参同新增**
  92 +
  93 +#### 2.2.5 删除
  94 +
  95 +- **方法**:`DELETE`
  96 +- **路径**:`/api/app/product-category/{id}`
  97 +- **说明**:逻辑删除;若被产品引用会阻止删除(保持原行为)
  98 +
  99 +### 2.3 后端校验规则(本次新增)
  100 +
  101 +- `availabilityType` 仅允许 `ALL/SPECIFIED`
  102 + - `SPECIFIED` 时 `locationIds` 至少 1 个
  103 +- `buttonAppearance`:须为 **合法 JSON**(任意对象/数组),或为兼容的 **`TEXT`/`COLOR`/`IMAGE` 单行**;其它字符串拒绝。`categoryPhotoUrl` 非空且非 JSON 时后端会序列化为 JSON 字符串存储;是否必填由业务/前端约定
  104 +
  105 +---
  106 +
  107 +## 3. 标签模块 Label Categories(Labels → Label Categories)
  108 +
  109 +> 数据库侧新增:`fl_label_category` 新字段、`fl_label_category_location` 新表。
  110 +
  111 +### 3.1 表结构要点
  112 +
  113 +- `fl_label_category`(主表)关键字段:
  114 + - `DisplayText`:按钮展示文案(为空可回退 `CategoryName`)
  115 + - `ButtonAppearance`:**JSON 格式字符串**落库(如 `["TEXT","COLOR"]`、`["IMAGE"]`);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时规范为 JSON 数组)
  116 + - `CategoryPhotoUrl`:**JSON 格式字符串**落库(展示数据由前端解析);非 JSON 纯文本(色值、URL 等)保存时会被后端包成 JSON 字符串
  117 + - **已删除列**(需在库上执行 `DROP COLUMN`):`ButtonTextColor`、`ButtonBgColor`、`ButtonImageUrl`、`ButtonStyleJson`
  118 + - `AvailabilityType`:`ALL/SPECIFIED`(门店可用范围)
  119 +- `fl_label_category_location`(关联表):
  120 + - `(CategoryId, LocationId)` 唯一约束;用于 `AvailabilityType=SPECIFIED` 指定门店(`LocationId` 对应 `location` 表主键)
  121 +
  122 +### 3.2 CRUD 接口(字段扩展)
  123 +
  124 +接口路径不变,仅扩展字段。
  125 +
  126 +#### 3.2.1 列表
  127 +
  128 +- **方法**:`GET`
  129 +- **路径**:`/api/app/label-category`
  130 +- **列表行新增返回**:
  131 + - `displayText`
  132 + - `buttonAppearance`
  133 + - `categoryPhotoUrl`
  134 + - `availabilityType`
  135 +
  136 +#### 3.2.2 详情
  137 +
  138 +- **方法**:`GET`
  139 +- **路径**:`/api/app/label-category/{id}`
  140 +- **新增返回字段**:
  141 + - `displayText`
  142 + - `buttonAppearance`、`categoryPhotoUrl`(**JSON 格式字符串**,展示语义由前端解析)
  143 + - `availabilityType`
  144 + - `locationIds`(当 `availabilityType=SPECIFIED` 返回门店 Id 列表,否则为空数组)
  145 +
  146 +#### 3.2.3 新增
  147 +
  148 +- **方法**:`POST`
  149 +- **路径**:`/api/app/label-category`
  150 +- **新增入参字段**:
  151 + - `displayText`
  152 + - `buttonAppearance`、`categoryPhotoUrl`
  153 + - `availabilityType`
  154 + - `locationIds`(当 `availabilityType=SPECIFIED` 必填且至少 1 个)
  155 +
  156 +#### 3.2.4 编辑
  157 +
  158 +- **方法**:`PUT`
  159 +- **路径**:`/api/app/label-category/{id}`
  160 +- **入参同新增**
  161 +
  162 +#### 3.2.5 删除
  163 +
  164 +- **方法**:`DELETE`
  165 +- **路径**:`/api/app/label-category/{id}`
  166 +- **说明**:逻辑删除;若被标签引用会阻止删除(保持原行为)
  167 +
  168 +### 3.3 后端校验规则(本次新增)
  169 +
  170 +- `availabilityType` 仅允许 `ALL/SPECIFIED`
  171 + - `SPECIFIED` 时 `locationIds` 至少 1 个
  172 +- `buttonAppearance`:须为 **合法 JSON**(任意对象/数组),或为兼容的 **`TEXT`/`COLOR`/`IMAGE` 单行**;其它字符串拒绝。`categoryPhotoUrl` 非空且非 JSON 时后端会序列化为 JSON 字符串存储;是否必填由业务/前端约定
  173 +
  174 +---
  175 +
  176 +## 4. App 端 `GET /api/app/us-app-labeling/labeling-tree`
  177 +
  178 +- **L1(标签分类)节点**:返回 `categoryPhotoUrl`、`buttonAppearance`(均为库中字符串,**多为 JSON**,与 CRUD 一致);缺省或空时 `buttonAppearance` 后端默认 **`"TEXT"`**(兼容旧数据,**不再**对整段做 `ToUpperInvariant` 以免破坏 JSON)。
  179 +- **L2(产品分类)节点**:返回 `displayText`、`buttonAppearance`、`categoryPhotoUrl`、`availabilityType`、`orderNum` 等;外观数据由 **`buttonAppearance` + `categoryPhotoUrl`** 承载(已不再返回 `buttonTextColor`、`buttonBgColor`、`buttonImageUrl`、`buttonStyleJson`)。
  180 +
  181 +### 4.1 数据库迁移(两张主表)
  182 +
  183 +在确认历史数据已按需迁到 `CategoryPhotoUrl` 后,可执行(列不存在时需跳过或调整):
  184 +
  185 +```sql
  186 +ALTER TABLE `fl_label_category`
  187 + DROP COLUMN `ButtonTextColor`,
  188 + DROP COLUMN `ButtonBgColor`,
  189 + DROP COLUMN `ButtonImageUrl`,
  190 + DROP COLUMN `ButtonStyleJson`;
  191 +
  192 +ALTER TABLE `fl_product_category`
  193 + DROP COLUMN `ButtonTextColor`,
  194 + DROP COLUMN `ButtonBgColor`,
  195 + DROP COLUMN `ButtonImageUrl`,
  196 + DROP COLUMN `ButtonStyleJson`;
  197 +```
  198 +
  199 +---
  200 +
  201 +## 5. Web 管理端会话(`AuthSession` / 食品标签-美国版)
  202 +
  203 +> 实现:`IAuthSessionAppService` / `AuthSessionAppService`。需携带与后台一致的 **JWT**(`Authorization: Bearer {token}`)。具体 action 路径以部署环境 **Swagger / OpenAPI** 为准;下列为 ABP 常规约定(`RootPath = api/app`)。
  204 +
  205 +### 5.1 获取当前登录用户菜单与权限
  206 +
  207 +- **方法**:`GET`
  208 +- **路径**(约定):`/api/app/auth-session/my-menus`
  209 +- **鉴权**:需要登录
  210 +- **用途**:前端动态路由、侧边栏、按钮级权限(`permissionCodes`)
  211 +- **返回体**(`CurrentUserMenuPermissionsOutputDto`,JSON 字段名为 camelCase):
  212 +
  213 +| 字段 | 类型 | 说明 |
  214 +|---|---|---|
  215 +| `user` | object | 当前用户简要信息(无密码) |
  216 +| `user.id` | guid | 用户 Id |
  217 +| `user.userName` | string | 登录名 |
  218 +| `user.nick` | string? | 昵称 |
  219 +| `user.email` | string? | 邮箱 |
  220 +| `user.icon` | string? | 头像 |
  221 +| `roleCodes` | string[] | 角色编码列表(已排序) |
  222 +| `permissionCodes` | string[] | 权限码列表(已排序;超级管理员常见为 `*:*:*`) |
  223 +| `menus` | array | **菜单树**(根节点列表,子节点在 `children`) |
  224 +
  225 +**菜单树节点**(`CurrentUserMenuNodeDto`)主要字段:
  226 +
  227 +| 字段 | 说明 |
  228 +|---|---|
  229 +| `id` / `parentId` | 菜单 Id、父 Id(根父级多为 `"0"`) |
  230 +| `menuName` | 菜单名称 |
  231 +| `routerName` / `router` | 路由名、路径 |
  232 +| `permissionCode` | 权限标识(按钮/接口控制用) |
  233 +| `menuType` / `menuSource` | 枚举整型值(与 `Menu` 表一致) |
  234 +| `orderNum` / `state` | 排序、是否启用 |
  235 +| `menuIcon` / `component` / `isLink` / `isCache` / `isShow` / `query` / `remark` | 与菜单表一致 |
  236 +| `children` | 子节点数组 |
  237 +
  238 +**业务说明**:
  239 +
  240 +- 数据来源与框架 `UserManager.GetInfoAsync` 一致:按用户角色合并菜单与权限码。
  241 +- 用户名为 **`admin`** 时:与 `AccountService.GetVue3Router` 对齐,返回 **`Menu` 表中未逻辑删除** 的全部菜单再组树(`permissionCodes` 仍为超级管理员约定值)。
  242 +
  243 +### 5.2 退出登录
  244 +
  245 +- **方法**:`POST`
  246 +- **路径**(约定):`/api/app/auth-session/logout`
  247 +- **鉴权**:需要登录(未登录或无法解析用户时返回 `false`)
  248 +- **请求体**:无
  249 +- **返回**:`boolean`
  250 + - `true`:已清除服务端 **用户信息分布式缓存**(与 `AccountService.PostLogout` 一致)
  251 + - `false`:当前请求未识别到用户 Id(例如未登录)
  252 +- **说明**:JWT 为无状态令牌,**前端仍需丢弃本地 Token**;退出接口主要清理服务端缓存侧用户信息。
  253 +
  254 +---
  255 +
  256 +## 6. 权限菜单 `rbac-menu` 列表/详情补充字段
  257 +
  258 +- **路径**:`GET /api/app/rbac-menu`(列表)、`GET /api/app/rbac-menu/{id}`(详情)
  259 +- **新增返回**(与 `menu` 表字段一致,JSON 一般为 camelCase):
  260 + - `routerName`:路由名称
  261 + - `router`:路由路径
  262 +- **说明**:树接口 `GET /api/app/rbac-menu/tree`(若已使用)本身已包含完整菜单字段,无需重复改动。
  263 +
... ...
项目相关文档/标签模块接口对接说明.md
... ... @@ -51,6 +51,12 @@ Swagger 地址:
51 51 }
52 52 ```
53 53  
  54 +### 1.1.1 字段约定:`buttonAppearance` 与 `categoryPhotoUrl`(JSON 字符串)
  55 +
  56 +- **`buttonAppearance`**:库中存 **JSON 文本**(如 `["TEXT","COLOR"]`、仅图片 `["IMAGE"]` 等);兼容历史单行 `TEXT`/`COLOR`/`IMAGE`(保存时规范为 `["TEXT"]` 等)。未传或空白时后端默认 `["TEXT"]`。非法值(非 JSON 且非上述三者)会返回友好错误。
  57 +- **`categoryPhotoUrl`**:同样为 **JSON 文本**(如 `["Prep","#10B981"]`);若传**非 JSON** 的纯文本(色值、`/picture/...` 等),后端会序列化为合法 JSON 字符串再存储。列表/详情/App 树**原样返回**字符串,由客户端解析。
  58 +- 其它常用字段:`displayText`、`availabilityType`(`ALL`/`SPECIFIED`)、`locationIds`(指定门店时必填),与产品类别接口语义一致(见 `项目相关文档/产品模块Categories接口对接说明.md`)。
  59 +
54 60 ### 1.2 详情
55 61  
56 62 方法:`GET /api/app/label-category/{id}`
... ... @@ -69,7 +75,11 @@ Swagger 地址:
69 75 {
70 76 "categoryCode": "CAT_PREP",
71 77 "categoryName": "Prep",
72   - "categoryPhotoUrl": "https://cdn.example.com/cat-prep.png",
  78 + "displayText": "Prep",
  79 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  80 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  81 + "availabilityType": "ALL",
  82 + "locationIds": [],
73 83 "state": true,
74 84 "orderNum": 1
75 85 }
... ... @@ -85,7 +95,11 @@ Swagger 地址:
85 95 {
86 96 "categoryCode": "CAT_PREP",
87 97 "categoryName": "Prep",
88   - "categoryPhotoUrl": null,
  98 + "displayText": "Prep",
  99 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
  100 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  101 + "availabilityType": "ALL",
  102 + "locationIds": [],
89 103 "state": true,
90 104 "orderNum": 2
91 105 }
... ... @@ -560,6 +574,10 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
560 574 入参:
561 575 - `id`:产品Id(`fl_product.Id`)
562 576  
  577 +返回(`ProductGetOutputDto`,与实现一致的主要字段):
  578 +- `id`、`productCode`、`productName`、`categoryId`、`categoryName`、`productImageUrl`、`state`
  579 +- **`locationIds`**:`string[]`,该产品在 **`fl_location_product`** 中绑定的门店 Id(去重);无关联时为空数组
  580 +
563 581 ### 6.3 新增产品
564 582  
565 583 方法:`POST /api/app/product`
... ... @@ -569,15 +587,30 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
569 587 {
570 588 "productCode": "PRD_TEST_001",
571 589 "productName": "Chicken",
572   - "categoryName": "Meat",
  590 + "categoryId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
573 591 "productImageUrl": "https://example.com/img.png",
574   - "state": true
  592 + "state": true,
  593 + "locationIds": [
  594 + "11111111-1111-1111-1111-111111111111",
  595 + "22222222-2222-2222-2222-222222222222"
  596 + ]
575 597 }
576 598 ```
577 599  
  600 +字段说明:
  601 +| 字段 | 类型 | 必填 | 说明 |
  602 +|------|------|------|------|
  603 +| `productCode` | string | 是 | 产品编码 |
  604 +| `productName` | string | 是 | 产品名称 |
  605 +| `categoryId` | string \| null | 否 | 产品分类 Id(`fl_product_category.id`) |
  606 +| `productImageUrl` | string \| null | 否 | 主图 URL |
  607 +| `state` | bool | 否 | 默认 `true` |
  608 +| **`locationIds`** | `string[]` \| **省略** | 否 | **可选。** 有该字段时:在同一事务内按列表批量写入 **`fl_location_product`**(**每个门店 Id 一行**,即「一产品一门店一条关联」)。**请求体中省略该字段**时:本接口不写门店关联,仍可通过 **§7 Product-Location** 维护。传空数组 `[]` 表示新建产品后不绑定任何门店。 |
  609 +
578 610 校验:
579   -- `productCode/productName` 不能为空
  611 +- `productCode` / `productName` 不能为空
580 612 - `productCode` 不能与未删除的数据重复
  613 +- 若传入 **`locationIds`** 且含非空项:每个 Id 须为合法 Guid,且对应门店存在于 **`Location`** 主数据且未删除;否则返回友好错误(如「门店Id格式不正确」「门店不存在」)
581 614  
582 615 ### 6.4 编辑产品
583 616  
... ... @@ -585,7 +618,13 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
585 618  
586 619 入参:
587 620 - Path:`id` 为当前产品Id(`fl_product.Id`)
588   -- Body:字段同新增(`ProductUpdateInputVo`)
  621 +- Body:字段同新增(`ProductUpdateInputVo`,继承 `ProductCreateInputVo`)
  622 +
  623 +**`locationIds` 行为(与新增不同,请注意):**
  624 +- **请求体中省略 `locationIds` 属性**:不修改 **`fl_location_product`**(仅更新 `fl_product` 主表字段;兼容原「先 PUT 产品再调 §7 同步门店」的调用方式)。
  625 +- **请求体中包含 `locationIds` 属性**(含空数组 `[]`):对该产品的门店关联做 **整表替换**——先删除本产品下全部 **`fl_location_product`** 行,再按列表逐条插入;`[]` 表示解除该产品与所有门店的关联。
  626 +
  627 +其它校验同 **§6.3**(含门店存在性校验,当 `locationIds` 含非空项时)。
589 628  
590 629 ### 6.5 删除(逻辑删除)
591 630  
... ... @@ -599,6 +638,7 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
599 638  
600 639 说明:
601 640 - 关联表:`fl_location_product`
  641 +- 也可在 **§6.3 / §6.4** 通过产品 Body 的 **`locationIds`** 一次性维护本产品在各门店的关联(与 §7 写入同一张表);二者可并存,按需选择调用方式。
602 642 - 关联按门店进行批量替换:
603 643 - `Create`:在门店下新增未存在的 product 关联
604 644 - `Update`:替换该门店下全部关联(先删后建)
... ... @@ -706,7 +746,8 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
706 746 |------|------|------|
707 747 | `id` | string | `fl_label_category.Id` |
708 748 | `categoryName` | string | 分类名称 |
709   -| `categoryPhotoUrl` | string \| null | 分类图标/图 |
  749 +| `categoryPhotoUrl` | string \| null | 分类展示数据,**JSON 格式字符串**(与库中 `CategoryPhotoUrl` 一致,客户端解析) |
  750 +| `buttonAppearance` | string | 按钮外观,**JSON 格式字符串**(与库中 `ButtonAppearance` 一致;空时后端默认 `"TEXT"`) |
710 751 | `orderNum` | number | 排序 |
711 752 | `productCategories` | array | 第二级列表(见下表) |
712 753  
... ... @@ -715,8 +756,12 @@ ADD UNIQUE KEY `uk_fl_ltpd_template_product_label_type` (`TemplateId`, `ProductI
715 756 | 字段 | 类型 | 说明 |
716 757 |------|------|------|
717 758 | `categoryId` | string \| null | 产品分类Id;产品未归类或分类不存在时为空 |
718   -| `categoryPhotoUrl` | string \| null | 产品分类图片地址;产品未归类或分类不存在时为空 |
  759 +| `categoryPhotoUrl` | string \| null | 产品分类展示数据,**JSON 格式字符串**;未归类或分类不存在时为空 |
719 760 | `name` | string | 产品分类显示名;空源数据为 **`无`** |
  761 +| `displayText` | string \| null | 按钮展示文案 |
  762 +| `buttonAppearance` | string | **JSON 格式字符串**(与库中一致;空时默认 `"TEXT"`) |
  763 +| `availabilityType` | string | `ALL` / `SPECIFIED`(树已按当前门店过滤,仍返回供客户端展示) |
  764 +| `orderNum` | number | 排序 |
720 765 | `itemCount` | number | 该分类下 **产品个数**(去重后的产品数) |
721 766 | `products` | array | 第三级产品列表(见下表) |
722 767  
... ... @@ -771,13 +816,18 @@ curl -X GET &quot;http://localhost:19001/api/app/us-app-labeling/labeling-tree?locati
771 816 {
772 817 "id": "cat-prep-id",
773 818 "categoryName": "Prep",
774   - "categoryPhotoUrl": "/picture/...",
  819 + "categoryPhotoUrl": "[\"Prep\",\"#10B981\"]",
  820 + "buttonAppearance": "[\"TEXT\",\"COLOR\"]",
775 821 "orderNum": 1,
776 822 "productCategories": [
777 823 {
778 824 "categoryId": "pc-meat-id",
779   - "categoryPhotoUrl": "/picture/product-category/20260325123010_xxx.png",
  825 + "categoryPhotoUrl": "[\"/picture/product-category/20260325123010_xxx.png\"]",
780 826 "name": "Meat",
  827 + "displayText": "Meat",
  828 + "buttonAppearance": "[\"IMAGE\"]",
  829 + "availabilityType": "ALL",
  830 + "orderNum": 10,
781 831 "itemCount": 1,
782 832 "products": [
783 833 {
... ...
项目相关文档/美国版App登录接口说明.md
... ... @@ -149,11 +149,294 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
149 149  
150 150 ---
151 151  
  152 +## 接口 3:我的资料(My Profile)
  153 +
  154 +用于「My Profile」页展示:全名、邮箱、电话、员工号、角色。
  155 +
  156 +### HTTP
  157 +
  158 +- **方法**:`GET`
  159 +- **路径**:`/api/app/us-app-auth/my-profile`(以 Swagger 中 `UsAppAuth` 为准)
  160 +- **鉴权**:需要登录(`Authorization: Bearer {token}`)
  161 +
  162 +### 请求参数
  163 +
  164 +无。
  165 +
  166 +### 响应体(UsAppMyProfileOutputDto)
  167 +
  168 +| 字段(JSON) | 类型 | 说明 |
  169 +|--------------|------|------|
  170 +| `fullName` | string | `User.Name`,无则 `Nick`,再无则 `UserName` |
  171 +| `email` | string | `User.Email`;空为 `"无"` |
  172 +| `phone` | string | 由 `User.Phone`(long)格式化为可读字符串;10 位数字按 `+1 (555) 123-4567` 形式;无则 `"无"` |
  173 +| `employeeId` | string | 当前取 **`User.UserName`**(可与业务约定为工号/登录名) |
  174 +| `roleDisplay` | string | 多角色 `Role.RoleName` 按 `OrderNum` 排序后以英文逗号拼接;无角色为 `"无"` |
  175 +| `primaryRoleCode` | string \| null | 第一个角色的 `RoleCode`,供前端样式 |
  176 +
  177 +---
  178 +
  179 +## 接口 4:修改密码(Change Password)
  180 +
  181 +与 **`User.JudgePassword` / `BuildPassword`** 一致:校验当前密码后写入新盐值哈希。
  182 +
  183 +### HTTP
  184 +
  185 +- **方法**:`POST`
  186 +- **路径**:`/api/app/us-app-auth/change-password`
  187 +- **Content-Type**:`application/json`
  188 +- **鉴权**:需要登录
  189 +
  190 +### 请求体(UsAppChangePasswordInputVo)
  191 +
  192 +| 字段(JSON) | 类型 | 必填 | 说明 |
  193 +|--------------|------|------|------|
  194 +| `currentPassword` | string | 是 | 当前明文密码 |
  195 +| `newPassword` | string | 是 | 新密码 |
  196 +| `confirmNewPassword` | string | 是 | 必须与 `newPassword` **完全一致** |
  197 +
  198 +### 新密码复杂度(与原型「Password Requirements」一致)
  199 +
  200 +1. 至少 **8** 位
  201 +2. 至少 **1** 个大写字母、**1** 个小写字母
  202 +3. 至少 **1** 个数字
  203 +4. 至少 **1** 个**非字母数字**字符(特殊字符)
  204 +
  205 +### 请求示例
  206 +
  207 +```json
  208 +{
  209 + "currentPassword": "OldPass1!",
  210 + "newPassword": "NewPass9@x",
  211 + "confirmNewPassword": "NewPass9@x"
  212 +}
  213 +```
  214 +
  215 +### 响应
  216 +
  217 +成功时 HTTP 200,无业务体要求(ABP 可能返回空对象);失败为业务异常文案。
  218 +
  219 +### 常见错误
  220 +
  221 +- 未登录:`用户未登录`
  222 +- 当前密码错误:与登录失败一致(`UserConst.Login_Error` 文案)
  223 +- 新密码与确认不一致:`新密码与确认密码不一致`
  224 +- 新密码与当前相同:`新密码不能与当前密码相同`
  225 +- 不满足复杂度:如 `新密码至少 8 位`、`新密码需包含大写字母` 等
  226 +
  227 +---
  228 +
  229 +## 接口 5:门店详情(Location)
  230 +
  231 +用于移动端「Location」页:店名、完整地址、门店电话、营业时间占位、店长姓名与电话。**仅当当前 JWT 用户在 `userlocation` 中绑定该门店**时可查(与接口 1、2 的门店范围一致)。
  232 +
  233 +### HTTP
  234 +
  235 +- **方法**:`GET`
  236 +- **路径**:`/api/app/us-app-auth/location-detail/{locationId}`(以 Swagger 中 `UsAppAuth` → `GetLocationDetail` 为准)
  237 +- **鉴权**:需要登录(`Authorization: Bearer {token}`)
  238 +
  239 +### 请求参数
  240 +
  241 +| 参数名 | 位置 | 类型 | 必填 | 说明 |
  242 +|--------|------|------|------|------|
  243 +| `locationId` | Path | string | 是 | 门店主键,Guid 字符串(与 `locations[].id` 一致) |
  244 +
  245 +### 传参示例
  246 +
  247 +```http
  248 +GET /api/app/us-app-auth/location-detail/a2696b9e-2277-11f1-b4c6-00163e0c7c4f HTTP/1.1
  249 +Host: localhost:19001
  250 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
  251 +```
  252 +
  253 +### 响应体(UsAppLocationDetailOutputDto)
  254 +
  255 +| 字段(JSON) | 类型 | 说明 |
  256 +|--------------|------|------|
  257 +| `locationId` | string | 门店主键 |
  258 +| `locationName` | string | 门店名称;空为 `"无"` |
  259 +| `fullAddress` | string | 与登录接口相同的地址拼接规则;无有效片段为 `"无"` |
  260 +| `storePhone` | string | `location.Phone` 去首尾空格;空为 `"无"` |
  261 +| `operatingHours` | string | 当前 **`location` 表无营业时间字段**,固定返回 **`"无"`**(后续若落库可再对接) |
  262 +| `managerName` | string | 在同店 `userlocation` 绑定用户中,取角色 `RoleCode` / `RoleName`(忽略大小写)包含 **`manager`** 的第一人展示姓名(`Name` → `Nick` → `UserName`);无匹配为 **`"无"`** |
  263 +| `managerPhone` | string | 同上用户的 `User.Phone`(long)按与「我的资料」相同的可读格式;无电话为 **`"无"`** |
  264 +
  265 +### 响应示例
  266 +
  267 +```json
  268 +{
  269 + "locationId": "a2696b9e-2277-11f1-b4c6-00163e0c7c4f",
  270 + "locationName": "Downtown Store",
  271 + "fullAddress": "123 Main St, New York, NY 10001",
  272 + "storePhone": "(212) 555-0100",
  273 + "operatingHours": "无",
  274 + "managerName": "Jane Doe",
  275 + "managerPhone": "+1 (555) 123-4567"
  276 +}
  277 +```
  278 +
  279 +### 常见错误(业务异常文案)
  280 +
  281 +- 未登录:`用户未登录`
  282 +- `locationId` 非法或空:`无效的门店标识`
  283 +- 当前用户未绑定该门店:`当前账号未绑定该门店,无法查看`
  284 +- 门店已删或不存在:`门店不存在或已删除`
  285 +
  286 +---
  287 +
  288 +## 接口 6:全局 Support 联系方式(App 只读 + Web 可读)
  289 +
  290 +用于 App「Support」页与 Web 展示**全平台共用**的一条电话与邮箱。
  291 +实现:`LocationSupportAppService`,表 `fl_location_support` **不再包含门店 Id**。
  292 +
  293 +### HTTP(查询)
  294 +
  295 +- **方法**:`GET`
  296 +- **路径**(约定式 API,以 Swagger 中 `LocationSupport` → `GetSupport` 为准):一般为 **`/api/app/location-support/support`**
  297 +- **鉴权**:需要登录(`Authorization: Bearer {token}`)。**App 登录 Token 与 Web Token 均可调用本接口。**
  298 +
  299 +### 请求参数
  300 +
  301 +无 Query / Body。
  302 +
  303 +### 响应体(LocationSupportGetOutputDto)
  304 +
  305 +| 字段(JSON) | 类型 | 说明 |
  306 +|--------------|------|------|
  307 +| `id` | string | 记录主键(Web 编辑 `PUT` 路径中的 `{id}`) |
  308 +| `supportPhone` | string | Support 电话 |
  309 +| `supportEmail` | string | Support 邮箱 |
  310 +
  311 +> 若尚未在后台配置,接口返回 `null`。
  312 +
  313 +### 响应示例
  314 +
  315 +```json
  316 +{
  317 + "id": "3a2f4fda-1a93-4a35-9b98-95dca7bb5d2a",
  318 + "supportPhone": "1-800-SUPPORT",
  319 + "supportEmail": "support@medvantage.com"
  320 +}
  321 +```
  322 +
  323 +### App 与 Web 权限说明
  324 +
  325 +- App 登录签发 JWT 时会写入声明 **`client_kind` = `us-app`**(与 Web 管理端 Token 区分)。
  326 +- **App 仅允许调用本节的 `GET`(查询)**;若使用 App Token 调用新增/编辑,将返回业务错误(英文):`The mobile app can only view support contacts. Please use the web console to edit.`
  327 +
  328 +---
  329 +
  330 +## 后台维护接口:Location Support(Web:新增 / 编辑)
  331 +
  332 +仅 **Web 管理端 Token**(无 `client_kind=us-app`)可调用,用于维护全局 Support 联系方式。
  333 +
  334 +### 接口 A:新增(全局一条)
  335 +
  336 +- **方法**:`POST`
  337 +- **路径**:`/api/app/location-support`
  338 +- **Content-Type**:`application/json`
  339 +
  340 +请求体(LocationSupportCreateInputVo):
  341 +
  342 +```json
  343 +{
  344 + "supportPhone": "1-800-SUPPORT",
  345 + "supportEmail": "support@medvantage.com"
  346 +}
  347 +```
  348 +
  349 +约束:
  350 +
  351 +- 系统内仅允许存在一条未删除记录;若已存在,再次新增会报错:`Global support contact already exists. Use update instead.`
  352 +
  353 +### 接口 B:编辑
  354 +
  355 +- **方法**:`PUT`
  356 +- **路径**:`/api/app/location-support/{id}`
  357 +- **Content-Type**:`application/json`
  358 +
  359 +请求体(LocationSupportUpdateInputVo):
  360 +
  361 +```json
  362 +{
  363 + "supportPhone": "1-800-SUPPORT",
  364 + "supportEmail": "support@medvantage.com"
  365 +}
  366 +```
  367 +
  368 +常见错误(英文):
  369 +
  370 +- `The mobile app can only view support contacts. Please use the web console to edit.`
  371 +- `Support phone is required.` / `Support email is required.` / `Support email format is invalid.`
  372 +- `Global support contact already exists. Use update instead.`
  373 +- `Support record not found.` / `Support record id is required.`
  374 +
  375 +### 数据库迁移(删除 `LocationId`)
  376 +
  377 +若线上表仍为旧结构(含 `LocationId`),请在库中执行脚本:
  378 +
  379 +- `美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_alter_drop_locationid.sql`
  380 +
  381 +新建库请使用:
  382 +
  383 +- `美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_location_support_create.sql`
  384 +
  385 +---
  386 +
  387 +## 平台端登录接口(Web)
  388 +
  389 +> 对应 `RBAC.AccountService.PostLoginAsync`。当前平台端登录规则已调整为:**仅支持 Email 作为账号,不再支持 UserName**。
  390 +
  391 +### HTTP
  392 +
  393 +- **方法**:`POST`
  394 +- **路径**:`/api/app/account/login`(以 Swagger 中 `Account` 为准)
  395 +- **Content-Type**:`application/json`
  396 +- **鉴权**:匿名
  397 +
  398 +### 请求体(LoginInputVo)
  399 +
  400 +| 字段(JSON) | 类型 | 必填 | 说明 |
  401 +|--------------|------|------|------|
  402 +| `userName` | string | 是 | 兼容历史字段名;现必须传 **邮箱**(例如 `admin@example.com`) |
  403 +| `password` | string | 是 | 密码 |
  404 +| `uuid` | string | 条件 | 开启图形验证码时必填 |
  405 +| `code` | string | 条件 | 开启图形验证码时必填 |
  406 +
  407 +### 请求示例
  408 +
  409 +```json
  410 +{
  411 + "userName": "admin@example.com",
  412 + "password": "YourPassword",
  413 + "uuid": "captcha-uuid",
  414 + "code": "captcha-code"
  415 +}
  416 +```
  417 +
  418 +### 响应体(LoginOutputDto)
  419 +
  420 +| 字段(JSON) | 类型 | 说明 |
  421 +|--------------|------|------|
  422 +| `token` | string | Access Token |
  423 +| `refreshToken` | string | Refresh Token |
  424 +
  425 +### 错误文案(英文)
  426 +
  427 +- `Email and password are required.`
  428 +- `Sign-in failed: email is required as the account.`
  429 +- `Sign-in failed: account not found.`
  430 +- `Sign-in failed: incorrect email or password.`
  431 +- `Invalid captcha.`
  432 +
  433 +---
  434 +
152 435 ## 与其他登录方式的区别
153 436  
154 437 | 场景 | 说明 |
155 438 |------|------|
156   -| Web 管理端 | 仍使用 RBAC **`AccountService.PostLoginAsync`**,一般为人 **`userName`** + 密码 |
157   -| 美国版 App | **仅**本模块 **`/api/app/us-app-auth/login`** 使用 **邮箱 + 密码** |
  439 +| Web 管理端 | 使用 RBAC **`/api/app/account/login`**,字段名虽为 `userName`,但值必须是 **Email** |
  440 +| 美国版 App | 使用 **`/api/app/us-app-auth/login`**,显式字段 `email` + `password` |
158 441  
159 442 两者共用同一 `User` 表与 JWT 体系;App 端需保证账号已维护 **`Email`** 字段,否则无法通过邮箱登录。
... ...